worker.js (5534B)
1 // Test-only JS evaluator driver; excluded from the production site. 2 import { sourceText } from '../compiler.js'; 3 import { compileExpression } from './expression.js'; 4 import { resumeValue } from '../io.js'; 5 import { publishedBundle } from '../published-bundle.js'; 6 import { exportTree, treeFormat } from '../tree-export.js'; 7 import { BROWSER_BUDGET, collectionPolicy, PACKAGE_COLLECTION } from '../execution.js'; 8 import { Graph } from '../graph.js'; 9 import { bundleHash, loadBundle } from '../bundle.js'; 10 import { literals, makeLiteral, outputType, view } from '../codecs.js'; 11 import { runIO } from '../io.js'; 12 const scope = globalThis; 13 const send = (message) => scope.postMessage(message); 14 const running = () => send({ type: 'state', state: 'running' }); 15 let phase = 'empty'; 16 let graph, exports; 17 let stored; 18 let requestId = 0; 19 let pending; 20 function request(kind) { 21 if (pending) 22 throw new Error('Driver already waiting'); 23 const id = ++requestId; 24 return new Promise((resolve, reject) => { pending = { id, kind, resolve, reject }; send({ type: 'state', state: kind, id }); }); 25 } 26 async function load(hash, localId) { 27 phase = 'loading'; 28 running(); 29 const loaded = await publishedBundle(hash, localId); 30 const { bundle, source } = loaded; 31 stored = loaded; 32 const budget = BROWSER_BUDGET; 33 graph = new Graph(budget, collectionPolicy(budget, PACKAGE_COLLECTION.threshold)); 34 exports = loadBundle(graph, bundle); 35 phase = 'ready'; 36 send({ type: 'loaded', hash, exports: bundle.exports, nodes: bundle.nodes.length, metadata: bundle.metadata, source }); 37 } 38 async function run(message) { 39 if (phase !== 'ready' || !graph || !exports) 40 throw new Error('Load a bundle before running'); 41 phase = 'running'; 42 running(); 43 const fn = message.expression === undefined ? exports.get(message.export) : await compileExpression(graph, exports, message.expression); 44 if (fn === undefined) 45 throw new Error('Unknown export'); 46 if (message.mode !== 'pure' && message.mode !== 'io') 47 throw new Error('Unsupported execution mode'); 48 const type = outputType(message.output), args = literals(message.arguments).map(value => makeLiteral(graph, value)); 49 let result = message.expression === undefined ? graph.apply(fn, args) : fn; 50 graph.collect([result]); // Exclusive one-run session: only this application remains owned. 51 if (message.mode === 'io') 52 result = await runIO(graph, result, { 53 async before(event) { send({ type: 'trace', phase: 'before', event }); if (message.step === true) 54 await request('step'); running(); }, 55 after(event) { send({ type: 'trace', phase: 'after', event }); }, 56 write(text) { send({ type: 'output', text }); }, read() { return request('input'); }, running, 57 }, true); 58 const displayed = view(graph, result, type); 59 phase = 'done'; 60 send({ type: 'done', result: displayed, stats: graph.stats }); 61 } 62 scope.onmessage = ({ data }) => { 63 if (!data || typeof data !== 'object' || Array.isArray(data)) 64 return; 65 const message = data; 66 if (message['type'] === 'resume') { 67 if (!pending || message['id'] !== pending.id) 68 return; 69 const value = resumeValue(pending.kind, message['value']); 70 if (value === undefined) 71 return; 72 const waiter = pending; 73 pending = undefined; 74 waiter.resolve(value); 75 return; 76 } 77 if (message['type'] === 'download' && phase === 'ready' && stored) { 78 send({ type: 'downloaded', hash: stored.hash, bytes: stored.bytes }); 79 return; 80 } 81 if (message['type'] === 'export' && phase === 'ready' && stored) { 82 try { 83 if (typeof message['export'] !== 'string' || message['export'].length > 512) 84 throw new Error('Invalid export selection'); 85 const format = treeFormat(message['format']); 86 const file = exportTree(stored.bundle, message['export'], format); 87 send({ type: 'exported', hash: stored.hash, export: message['export'], format, file }); 88 } 89 catch (error) { 90 send({ type: 'export-error', message: error instanceof Error ? error.message : String(error) }); 91 } 92 return; 93 } 94 let operation; 95 try { 96 if (message['type'] === 'load' && phase === 'empty') { 97 const localId = message['localId']; 98 if (localId !== undefined && (typeof localId !== 'string' || localId.length > 128)) 99 throw new Error('Invalid catalogue ID'); 100 operation = load(bundleHash(message['hash']), localId); 101 } 102 else if (message['type'] === 'run' && phase === 'ready') { 103 if (typeof message['export'] !== 'string' || message['export'].length > 512) 104 throw new Error('Invalid export selection'); 105 operation = run({ type: 'run', export: message['export'], arguments: message['arguments'], 106 ...(message['expression'] === undefined ? {} : { expression: sourceText(message['expression']) }), 107 output: outputType(message['output']), mode: message['mode'], step: message['step'] === true }); 108 } 109 else 110 return; // A Worker owns exactly one load and one run, no racing sessions. 111 } 112 catch (error) { 113 operation = Promise.reject(error); 114 } 115 operation.catch((error) => { phase = 'done'; send({ type: 'error', message: error instanceof Error ? error.message : String(error) }); }); 116 };