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