tree-export.js (4899B)
1 export const FLAT_EXPORT_LIMIT = 33_554_432; 2 export const DAG_EXPORT_LIMIT = 134_217_728; 3 export function treeFormat(value) { 4 if (value !== 'ternary' && value !== 'tricu' && value !== 'triangle' && value !== 'dag') 5 throw new Error('Unknown tree export format'); 6 return value; 7 } 8 // Only immutable, authenticated bundle records are read. No graph observation, 9 // application or reduction. Reachability and capped size calculation count DAG 10 // records, not unfolded occurrences; expanded writers run only after preflight. 11 export function exportTree(bundle, name, format) { 12 format = treeFormat(format); 13 const selected = bundle.exports.find(item => item.name === name); 14 if (!selected) 15 throw new Error('Unknown export'); 16 const reachable = new Set(), todo = [selected.root]; 17 while (todo.length) { 18 const id = todo.pop(); 19 if (reachable.has(id)) 20 continue; 21 reachable.add(id); 22 const node = bundle.nodes[id]; 23 if (node.kind === 'stem') 24 todo.push(node.child); 25 else if (node.kind === 'fork') 26 todo.push(node.left, node.right); 27 } 28 const ids = [...reachable].sort((a, b) => a - b); 29 // Bound the number of fragments, not just output bytes. One array entry per 30 // unfolded node costs far more memory than the resulting text for large trees. 31 const parts = [], chunk = []; 32 let chunkLength = 0; 33 const flush = () => { if (chunk.length) 34 parts.push(chunk.join('')); chunk.length = 0; chunkLength = 0; }; 35 const write = (text) => { 36 chunk.push(text); 37 chunkLength += text.length; 38 if (chunkLength >= 65_536) 39 flush(); 40 }; 41 const complete = () => { flush(); return parts.join(''); }; 42 let bytes = 0; 43 const append = (text) => { 44 const size = text.length + 2 * (text.split('△').length - 1); 45 bytes += size; 46 if (bytes > DAG_EXPORT_LIMIT) 47 throw new Error('DAG export exceeds 128 MiB'); 48 write(text); 49 }; 50 if (format === 'dag') { 51 const keys = new Map(); 52 for (const id of ids) { 53 const node = bundle.nodes[id], key = `:n${id}`; 54 if (node.kind === 'leaf') 55 keys.set(id, '△'); 56 else { 57 const child = keys.get(node.kind === 'stem' ? node.child : node.left); 58 if (node.kind === 'stem') 59 append(`${key} △ ${child}\n`); 60 else { 61 const intermediate = `:f${id}`, right = keys.get(node.right); 62 append(`${intermediate} △ ${child}\n`); 63 append(`${key} ${intermediate} ${right}\n`); 64 } 65 keys.set(id, key); 66 } 67 } 68 const root = keys.get(selected.root); 69 // A safe fixed interface name, independent of arbitrary bundle export names. 70 const ending = `tree ${root}\ntree\n`; 71 append(ending); 72 return { text: complete(), bytes, extension: 'dag', storedNodes: ids.length }; 73 } 74 const sizes = new Map(), triangle = format === 'triangle'; 75 const symbol = triangle ? '△' : 't', symbolBytes = triangle ? 3 : 1; 76 for (const id of ids) { 77 const node = bundle.nodes[id]; 78 let size; 79 if (format === 'ternary') 80 size = 1 + (node.kind === 'leaf' ? 0 : node.kind === 'stem' 81 ? sizes.get(node.child) : sizes.get(node.left) + sizes.get(node.right)); 82 else 83 size = node.kind === 'leaf' ? symbolBytes : node.kind === 'stem' 84 ? symbolBytes + 3 + sizes.get(node.child) 85 : symbolBytes + 4 + sizes.get(node.left) + sizes.get(node.right); 86 sizes.set(id, Math.min(FLAT_EXPORT_LIMIT + 1, size)); 87 } 88 bytes = sizes.get(selected.root); 89 if (bytes > FLAT_EXPORT_LIMIT) 90 throw new Error('Expanded tree exceeds 32 MiB. Choose DAG to preserve sharing.'); 91 const stack = [selected.root]; 92 while (stack.length) { 93 const item = stack.pop(); 94 if (typeof item === 'string') { 95 write(item); 96 continue; 97 } 98 const node = bundle.nodes[item]; 99 if (format === 'ternary') { 100 write(node.kind === 'leaf' ? '0' : node.kind === 'stem' ? '1' : '2'); 101 if (node.kind === 'stem') 102 stack.push(node.child); 103 else if (node.kind === 'fork') 104 stack.push(node.right, node.left); 105 } 106 else if (node.kind === 'leaf') 107 write(symbol); 108 else { 109 write(`(${symbol} `); 110 stack.push(')'); 111 if (node.kind === 'stem') 112 stack.push(node.child); 113 else 114 stack.push(node.right, ' ', node.left); 115 } 116 } 117 return { text: complete(), bytes, extension: format === 'tricu' ? 'tri' : 'txt', storedNodes: ids.length }; 118 }