wasm-worker.ts (6186B)
1 import { importPlan } from './import-plan.js'; 2 import { localModuleInputs, validateLocalModule } from './local-modules.js'; 3 import { resumeValue } from './io.js'; 4 import { createRuntimeSession, type RuntimeSession } from './runtime-session.js'; 5 const selectedBackend = new URL(globalThis.location.href).searchParams.get('backend'); 6 import { publishedBundle } from './published-bundle.js'; 7 import { bundleHash } from './bundle.js'; 8 import { literals, outputType } from './codecs.js'; 9 import { sourceText } from './compiler.js'; 10 import { exportTree, treeFormat } from './tree-export.js'; 11 import type { CompilerMessage } from './compiler-protocol.js'; 12 import type { WorkerMessage, RequestId } from './protocol.js'; 13 const scope = globalThis as unknown as { postMessage(m: CompilerMessage | WorkerMessage): void; onmessage: ((e: MessageEvent<unknown>) => void) | null }; 14 const send = (m: CompilerMessage | WorkerMessage) => scope.postMessage(m); 15 const running = () => send({ type: 'state', state: 'running' }); 16 let session: RuntimeSession | undefined, stored: Awaited<ReturnType<typeof publishedBundle>> | undefined; 17 let busy = false, requestId = 0; 18 let pending: { id: RequestId; resolve(value: string): void; reject(error: Error): void; kind: 'input' | 'step' } | undefined; 19 function request(kind: 'input' | 'step'): Promise<string> { 20 if (pending) throw new Error('Driver already waiting'); 21 const id = ++requestId as RequestId; 22 return new Promise((resolve, reject) => { pending = { id, resolve, reject, kind }; send({ type: 'state', state: kind, id }); }); 23 } 24 scope.onmessage = ({ data }) => { 25 if (!data || typeof data !== 'object' || Array.isArray(data)) return; 26 const m = data as Record<string, unknown>; 27 if (m['type'] === 'resume') { 28 if (!pending || m['id'] !== pending.id) return; 29 const value = resumeValue(pending.kind, m['value']); 30 if (value === undefined) return; 31 const waiter = pending; pending = undefined; waiter.resolve(value); 32 return; 33 } 34 if (busy) return; 35 busy = true; 36 void (async () => { 37 if (m['type'] === 'check-import') { 38 if (session) throw new Error('Already initialized'); 39 const source = sourceText(m['source']); 40 if (typeof m['manifest'] !== 'string' || typeof m['alias'] !== 'string') throw new Error('Invalid import request'); 41 session = await createRuntimeSession(selectedBackend, true); 42 const stage = await session.inspectModule(source); 43 const plan = importPlan(source, stage, m['manifest'], m['alias']); 44 if (!plan.existing) await session.compile(plan.source, 'empty', localModuleInputs(m['localModules'])); 45 send({ type: 'import-checked', plan }); 46 } else if (m['type'] === 'load') { 47 if (session || stored) throw new Error('Already initialized'); 48 const localId = m['localId']; 49 if (localId !== undefined && (typeof localId !== 'string' || localId.length > 128)) throw new Error('Invalid catalogue ID'); 50 running(); stored = await publishedBundle(bundleHash(m['hash']), localId); 51 session = await createRuntimeSession(selectedBackend, false); 52 await session.load(stored.bytes, stored.hash); 53 send({ type: 'loaded', hash: stored.hash, exports: stored.bundle.exports, nodes: stored.bundle.nodes.length, metadata: stored.bundle.metadata, source: stored.source }); 54 } else if (m['type'] === 'compile') { 55 if (session) throw new Error('Already initialized'); 56 const source = sourceText(m['source']), profile = m['environment']; 57 if (profile !== undefined && (typeof profile !== 'string' || profile.length > 128)) throw new Error('Invalid demo environment'); 58 running(); const start = performance.now(); 59 session = await createRuntimeSession(selectedBackend, true); 60 const names = await session.compile(source, profile, localModuleInputs(m['localModules'])); 61 send({ type: 'compiled', names, stats: session.stats(), milliseconds: performance.now() - start }); 62 } else if (m['type'] === 'export-module') { 63 if (!session || stored) throw new Error('Compile a draft first'); 64 running(); 65 try { const module = await validateLocalModule(await session.exportModule()); send({ type: 'module-exported', bytes: module.bytes, hash: module.hash, manifest: module.manifest }); } 66 catch (e) { if (session.disposed) throw e; send({ type: 'module-export-error', message: String(e) }); } 67 } else if (m['type'] === 'run') { 68 if (!session) throw new Error('Load or compile first'); 69 if (typeof m['export'] !== 'string' || m['export'].length > 512) throw new Error('Invalid export'); 70 if (m['mode'] !== 'pure' && m['mode'] !== 'io') throw new Error('Unsupported execution mode'); 71 const args = literals(m['arguments']), output = outputType(m['output']); running(); 72 const host: import('./io.js').IOHost = { 73 async before(event) { send({ type: 'trace', phase: 'before', event }); if (m['step'] === true) await request('step'); running(); }, 74 after(event) { send({ type: 'trace', phase: 'after', event }); }, write(text) { send({ type: 'output', text }); }, 75 read() { return request('input'); }, running, 76 }; 77 const result = m['expression'] === undefined 78 ? await session.run(m['export'], args, output, m['mode'], host) 79 : await session.runExpression(sourceText(m['expression']), output, m['mode'], host); 80 send({ type: 'done', result, stats: session.stats() }); 81 } else if (m['type'] === 'export' && stored) { 82 try { 83 if (typeof m['export'] !== 'string') throw new Error('Invalid export'); 84 const format = treeFormat(m['format']); 85 send({ type: 'exported', hash: stored.hash, export: m['export'], format, file: exportTree(stored.bundle, m['export'], format) }); 86 } catch (e) { send({ type: 'export-error', message: String(e) }); } 87 } else if (m['type'] === 'download' && stored) send({ type: 'downloaded', hash: stored.hash, bytes: stored.bytes }); 88 else throw new Error('Unsupported backend operation'); 89 })().catch((e: unknown) => send({ type: 'error', message: e instanceof Error ? e.message : String(e), fatal: session?.disposed ?? true })) 90 .finally(() => { busy = false; }); 91 };