worker.ts (4775B)
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 { publishedBundle } from '../published-bundle.js'; 5 import { exportTree, treeFormat } from '../tree-export.js'; 6 import { BROWSER_BUDGET, collectionPolicy, PACKAGE_COLLECTION } from '../execution.js'; 7 import { Graph } from '../graph.js'; 8 import { bundleHash, loadBundle, type Bundle, type BundleHash } from '../bundle.js'; 9 import { literals, makeLiteral, outputType, view } from '../codecs.js'; 10 import type { ClientMessage, WorkerMessage } from '../protocol.js'; 11 const scope = globalThis as unknown as { postMessage(message: WorkerMessage): void; onmessage: ((event: MessageEvent<unknown>) => void) | null }; 12 const send = (message: WorkerMessage): void => scope.postMessage(message); 13 const running = (): void => send({ type: 'state', state: 'running' }); 14 let phase: 'empty' | 'loading' | 'ready' | 'running' | 'done' = 'empty'; 15 let graph: Graph | undefined, exports: Map<string, import('../graph.js').NodeId> | undefined; 16 let stored: { hash: BundleHash; bundle: Bundle; bytes: Uint8Array } | undefined; 17 async function load(hash: BundleHash, localId: string | undefined): Promise<void> { 18 phase = 'loading'; running(); 19 const loaded = await publishedBundle(hash, localId); 20 const { bundle, source } = loaded; 21 stored = loaded; 22 const budget = BROWSER_BUDGET; 23 graph = new Graph(budget, collectionPolicy(budget, PACKAGE_COLLECTION.threshold)); exports = loadBundle(graph, bundle); 24 phase = 'ready'; 25 send({ type: 'loaded', hash, exports: bundle.exports, nodes: bundle.nodes.length, metadata: bundle.metadata, source }); 26 } 27 async function run(message: Extract<ClientMessage, { type: 'run' }>): Promise<void> { 28 if (phase !== 'ready' || !graph || !exports) throw new Error('Load a bundle before running'); 29 phase = 'running'; running(); 30 const fn = message.expression === undefined ? exports.get(message.export) : await compileExpression(graph, exports, message.expression); 31 if (fn === undefined) throw new Error('Unknown export'); 32 if (message.mode !== 'pure' && message.mode !== 'io') throw new Error('Unsupported execution mode'); 33 const type = outputType(message.output), args = literals(message.arguments).map(value => makeLiteral(graph!, value)); 34 let result = message.expression === undefined ? graph.apply(fn, args) : fn; 35 graph.collect([result]); // Exclusive one-run session: only this application remains owned. 36 if (message.mode === 'io') throw new Error('Reference graph IO execution is retired; use a native runtime'); 37 const displayed = view(graph, result, type); 38 phase = 'done'; send({ type: 'done', result: displayed, stats: graph.stats }); 39 } 40 scope.onmessage = ({ data }) => { 41 if (!data || typeof data !== 'object' || Array.isArray(data)) return; 42 const message = data as Record<string, unknown>; 43 44 if (message['type'] === 'download' && phase === 'ready' && stored) { 45 send({ type: 'downloaded', hash: stored.hash, bytes: stored.bytes }); return; 46 } 47 if (message['type'] === 'export' && phase === 'ready' && stored) { 48 try { 49 if (typeof message['export'] !== 'string' || message['export'].length > 512) throw new Error('Invalid export selection'); 50 const format = treeFormat(message['format']); 51 const file = exportTree(stored.bundle, message['export'], format); 52 send({ type: 'exported', hash: stored.hash, export: message['export'], format, file }); 53 } catch (error) { send({ type: 'export-error', message: error instanceof Error ? error.message : String(error) }); } 54 return; 55 } 56 let operation: Promise<void>; 57 try { 58 if (message['type'] === 'load' && phase === 'empty') { 59 const localId = message['localId']; 60 if (localId !== undefined && (typeof localId !== 'string' || localId.length > 128)) throw new Error('Invalid catalogue ID'); 61 operation = load(bundleHash(message['hash']), localId); 62 } else if (message['type'] === 'run' && phase === 'ready') { 63 if (typeof message['export'] !== 'string' || message['export'].length > 512) throw new Error('Invalid export selection'); 64 operation = run({ type: 'run', export: message['export'], arguments: message['arguments'], 65 ...(message['expression'] === undefined ? {} : { expression: sourceText(message['expression']) }), 66 output: outputType(message['output']), mode: message['mode'] as 'pure' | 'io', step: message['step'] === true }); 67 } else return; // A Worker owns exactly one load and one run, no racing sessions. 68 } catch (error) { operation = Promise.reject(error); } 69 operation.catch((error: unknown) => { phase = 'done'; send({ type: 'error', message: error instanceof Error ? error.message : String(error) }); }); 70 };