wasm-worker.ts (8572B)
1 import { autoInterpretation } from './auto-interpretation.js'; 2 import { importPlan } from './import-plan.js'; 3 import { localModuleInputs, validateLocalModule } from './local-modules.js'; 4 import { validStepResume } from './io.js'; 5 import { createRuntimeSession, type RuntimeSession } from './runtime-session.js'; 6 const selectedBackend = new URL(globalThis.location.href).searchParams.get('backend'); 7 import { publishedBundle } from './published-bundle.js'; 8 import { bundleHash } from './bundle.js'; 9 import { literals, outputType } from './codecs.js'; 10 import { sourceText } from './compiler.js'; 11 import { exportTree, treeFormat } from './tree-export.js'; 12 import type { CompilerMessage } from './compiler-protocol.js'; 13 import type { WorkerMessage, RequestId } from './protocol.js'; 14 const scope = globalThis as unknown as { postMessage(m: CompilerMessage | WorkerMessage): void; onmessage: ((e: MessageEvent<unknown>) => void) | null }; 15 const send = (m: CompilerMessage | WorkerMessage) => scope.postMessage(m); 16 const running = () => send({ type: 'state', state: 'running' }); 17 let session: RuntimeSession | undefined, stored: Awaited<ReturnType<typeof publishedBundle>> | undefined; 18 let busy = false, requestId = 0; 19 let pending: { id: RequestId; resolve(): void; reject(error: Error): void } | undefined; 20 function requestStep(): Promise<void> { 21 if (pending) throw new Error('Driver already waiting'); 22 if (requestId >= Number.MAX_SAFE_INTEGER) throw new Error('Driver request identity exhausted'); 23 const id = ++requestId as RequestId; 24 return new Promise((resolve, reject) => { pending = { id, resolve, reject }; send({ type: 'state', state: 'step', id }); }); 25 } 26 let ioCommand: ((command: import('./io-session.js').Command) => void) | undefined, ioRun: string | undefined, stopRequested = false; 27 function host(stepping: boolean): import('./io.js').IOHost { 28 stopRequested = false; 29 return { 30 selected(version) { send({ type: 'io-version', version }); }, 31 async before(event) { send({ type: 'trace', phase: 'before', event }); if (stepping) await requestStep(); running(); }, 32 structured: { 33 stepping, 34 open(run, command) { ioRun = run; ioCommand = command; send({ type: 'io-open', run }); if (stopRequested) command({ run, kind: 'stop' }); }, 35 event(event) { send({ type: 'io-event', event }); }, 36 close(run, settled) { ioCommand = undefined; ioRun = undefined; pending = undefined; send({ type: 'io-close', run, settled }); }, 37 }, 38 }; 39 } 40 scope.onmessage = ({ data }) => { 41 if (!data || typeof data !== 'object' || Array.isArray(data)) return; 42 const m = data as Record<string, unknown>; 43 if (m['type'] === 'io-stop') { 44 if (!busy) return; 45 stopRequested = true; 46 if (ioRun) ioCommand?.({ run: ioRun, kind: 'stop' }); 47 if (pending) { const waiter = pending; pending = undefined; waiter.resolve(); } 48 return; 49 } 50 if (m['type'] === 'io-command') { 51 const command = m['command']; 52 if (command && typeof command === 'object') ioCommand?.(command as import('./io-session.js').Command); 53 return; 54 } 55 if (m['type'] === 'resume') { 56 if (!pending || m['id'] !== pending.id) return; 57 if (!validStepResume(m['value'])) return; 58 const waiter = pending; pending = undefined; waiter.resolve(); 59 return; 60 } 61 if (busy) return; 62 busy = true; 63 void (async () => { 64 if (m['type'] === 'check-import') { 65 if (session) throw new Error('Already initialized'); 66 const source = sourceText(m['source']); 67 if (typeof m['manifest'] !== 'string' || typeof m['alias'] !== 'string') throw new Error('Invalid import request'); 68 session = await createRuntimeSession(selectedBackend, true); 69 const stage = await session.inspectModule(source); 70 const plan = importPlan(source, stage, m['manifest'], m['alias']); 71 if (!plan.existing) await session.compile(plan.source, 'empty', localModuleInputs(m['localModules'])); 72 send({ type: 'import-checked', plan }); 73 } else if (m['type'] === 'load') { 74 if (session || stored) throw new Error('Already initialized'); 75 const localId = m['localId']; 76 if (localId !== undefined && (typeof localId !== 'string' || localId.length > 128)) throw new Error('Invalid catalogue ID'); 77 running(); stored = await publishedBundle(bundleHash(m['hash']), localId); 78 session = await createRuntimeSession(selectedBackend, false); 79 await session.load(stored.bytes, stored.hash); 80 send({ type: 'loaded', hash: stored.hash, exports: stored.bundle.exports, nodes: stored.bundle.nodes.length, metadata: stored.bundle.metadata, source: stored.source }); 81 } else if (m['type'] === 'compile') { 82 if (session) throw new Error('Already initialized'); 83 const source = sourceText(m['source']), profile = m['environment']; 84 if (profile !== undefined && (typeof profile !== 'string' || profile.length > 128)) throw new Error('Invalid demo environment'); 85 running(); const start = performance.now(); 86 session = await createRuntimeSession(selectedBackend, true); 87 const names = await session.compile(source, profile, localModuleInputs(m['localModules'])); 88 send({ type: 'compiled', names, stats: session.stats(), milliseconds: performance.now() - start }); 89 } else if (m['type'] === 'export-module') { 90 if (!session || stored) throw new Error('Compile a draft first'); 91 running(); 92 try { const module = await validateLocalModule(await session.exportModule()); send({ type: 'module-exported', bytes: module.bytes, hash: module.hash, manifest: module.manifest }); } 93 catch (e) { if (session.disposed) throw e; send({ type: 'module-export-error', message: String(e) }); } 94 } else if (m['type'] === 'inspect') { 95 if (!session) throw new Error('Load or compile first'); 96 if (!Number.isSafeInteger(m['id']) || typeof m['name'] !== 'string' || m['name'].length > 4096) throw new Error('Invalid inspection'); 97 const id = m['id'] as number; 98 try { 99 // Observe the existing binding with no arguments, expression compilation, 100 // or IO interpreter. Observation/decoding can still require reduction. 101 const name = m['name']; 102 if (m['output'] === 'auto' && await session.probeIO(name)) { send({ type: 'inspected', id, io: true }); return; } 103 const result = m['output'] === 'auto' 104 ? await autoInterpretation(output => session!.inspectDefinition(name, output), () => session!.disposed) 105 : await session.inspectDefinition(name, outputType(m['output'])); 106 send({ type: 'inspected', id, result }); 107 } catch (e) { send({ type: 'inspection-error', id, message: e instanceof Error ? e.message : String(e), fatal: session.disposed }); } 108 } else if (m['type'] === 'run-io') { 109 if (!session || typeof m['name'] !== 'string' || m['name'].length > 4096) throw new Error('Invalid IO definition'); 110 const output = outputType(m['output']); running(); 111 const result = await session.runDefinitionIO(m['name'], output, host(m['step'] === true)); 112 send({ type: 'done', result, stats: session.stats() }); 113 } else if (m['type'] === 'run') { 114 if (!session) throw new Error('Load or compile first'); 115 if (typeof m['export'] !== 'string' || m['export'].length > 512) throw new Error('Invalid export'); 116 if (m['mode'] !== 'pure' && m['mode'] !== 'io') throw new Error('Unsupported execution mode'); 117 const args = literals(m['arguments']), output = outputType(m['output']); running(); 118 const ioHost = host(m['step'] === true); 119 const result = m['expression'] === undefined 120 ? await session.run(m['export'], args, output, m['mode'], ioHost) 121 : await session.runExpression(sourceText(m['expression']), output, m['mode'], ioHost); 122 send({ type: 'done', result, stats: session.stats() }); 123 } else if (m['type'] === 'export' && stored) { 124 try { 125 if (typeof m['export'] !== 'string') throw new Error('Invalid export'); 126 const format = treeFormat(m['format']); 127 send({ type: 'exported', hash: stored.hash, export: m['export'], format, file: exportTree(stored.bundle, m['export'], format) }); 128 } catch (e) { send({ type: 'export-error', message: String(e) }); } 129 } else if (m['type'] === 'download' && stored) send({ type: 'downloaded', hash: stored.hash, bytes: stored.bytes }); 130 else throw new Error('Unsupported backend operation'); 131 })().catch((e: unknown) => send({ type: 'error', message: e instanceof Error ? e.message : String(e), fatal: session?.disposed ?? true })) 132 .finally(() => { busy = false; }); 133 };