module-bundle.js (4625B)
1 // Explicit publication boundary: snapshots observe finite constructor trees. 2 // Canonical indexing preserves DAG sharing. 3 import { BUNDLE_LIMIT, decodeBundle } from './bundle.js'; 4 import { snapshotBundle } from './tree-result.js'; 5 const encoder = new TextEncoder(); 6 const join = (parts) => { 7 const length = parts.reduce((n, p) => n + p.length, 0); 8 if (length > BUNDLE_LIMIT) 9 throw new Error('Module bundle exceeds 16 MiB'); 10 const out = new Uint8Array(length); 11 let at = 0; 12 for (const part of parts) { 13 out.set(part, at); 14 at += part.length; 15 } 16 return out; 17 }; 18 function word(n, size = 4) { 19 const bytes = new Uint8Array(size), view = new DataView(bytes.buffer); 20 if (size === 8) 21 view.setBigUint64(0, BigInt(n)); 22 else if (size === 2) 23 view.setUint16(0, n); 24 else 25 view.setUint32(0, n); 26 return bytes; 27 } 28 const string = (s) => { const bytes = encoder.encode(s); return join([word(bytes.length), bytes]); }; 29 export function encodeModule(bundle) { 30 if (!bundle.exports.length || bundle.exports.length > 4096) 31 throw new Error('Invalid module export count'); 32 const mapped = new Map(), interned = new Map(), records = []; 33 for (const entry of bundle.exports) { 34 if (!/^[A-Za-z_][A-Za-z0-9_?$%'.-]*$/u.test(entry.name) || encoder.encode(entry.name).length > 512) 35 throw new Error('Invalid module export name'); 36 const stack = [[entry.root, false]]; 37 while (stack.length) { 38 const [id, leave] = stack.pop(); 39 if (mapped.has(id)) 40 continue; 41 const node = bundle.nodes[id]; 42 if (!node) 43 throw new Error('Missing module node'); 44 const children = node.kind === 'leaf' ? [] : node.kind === 'stem' ? [node.child] : [node.left, node.right]; 45 if (children.some(c => c < 0 || c >= id)) 46 throw new Error('Invalid module edge'); 47 if (!leave) { 48 stack.push([id, true]); 49 for (const c of [...children].reverse()) 50 stack.push([c, false]); 51 } 52 else { 53 const row = children.map(c => mapped.get(c)), key = row.length + ':' + row.join(','); 54 let index = interned.get(key); 55 if (index === undefined) { 56 index = records.length; 57 interned.set(key, index); 58 records.push(row); 59 } 60 if (records.length > 100_000) 61 throw new Error('Module node limit exceeded'); 62 mapped.set(id, index); 63 } 64 } 65 } 66 const manifest = join([encoder.encode('ARBMNFST'), word(1, 2), word(1, 2), 67 ...['arboricx.bundle.manifest.v1', 'tree-calculus-executable-object', 'tree-calculus.v1', 'indexed', 68 'arboricx.indexed.node.v1', 'arboricx.indexed.payload.v1', 'tree-calculus.v1', 'normal-order', 'arboricx.abi.tree.v1'].map(string), 69 word(0), new Uint8Array([0]), word(bundle.exports.length), 70 ...bundle.exports.flatMap((e, i) => [word(mapped.get(e.root)), string(i ? 'root' : 'default')]), 71 word(bundle.exports.length), ...bundle.exports.flatMap(e => [string(e.name), word(mapped.get(e.root)), string('term'), string('arboricx.abi.tree.v1')]), 72 word(0), word(0)]); 73 const nodes = join([word(records.length, 8), ...records.map(row => { 74 const payload = join([new Uint8Array([row.length]), ...row.map(n => word(n))]); 75 return join([word(payload.length), payload]); 76 })]); 77 const section = (type, at, size) => join([word(type), word(1, 2), word(1, 2), word(0), word(at, 8), word(size, 8), word(0)]); 78 const bytes = join([encoder.encode('ARBORICX'), word(1, 2), word(0, 2), word(2), word(0, 8), word(32, 8), 79 section(1, 96, manifest.length), section(2, 96 + manifest.length, nodes.length), manifest, nodes]); 80 decodeBundle(bytes); // Independent reader enforces the actual portable profile. 81 return bytes; 82 } 83 export function moduleSnapshot(names, snapshot) { 84 const bundle = snapshotBundle(snapshot); 85 let spine = bundle.roots[0]; 86 bundle.exports = names.map(name => { 87 const cell = bundle.nodes[spine]; 88 if (cell?.kind !== 'fork') 89 throw new Error('Invalid module snapshot spine'); 90 spine = cell.right; 91 return { name, root: cell.left, kind: 'term', abi: 'arboricx.abi.tree.v1' }; 92 }); 93 if (bundle.nodes[spine]?.kind !== 'leaf') 94 throw new Error('Invalid module snapshot tail'); 95 return encodeModule(bundle); 96 }