wasm-worker.js (9213B)
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 } 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 const scope = globalThis; 13 const send = (m) => scope.postMessage(m); 14 const running = () => send({ type: 'state', state: 'running' }); 15 let session, stored; 16 let busy = false, requestId = 0; 17 let pending; 18 function requestStep() { 19 if (pending) 20 throw new Error('Driver already waiting'); 21 if (requestId >= Number.MAX_SAFE_INTEGER) 22 throw new Error('Driver request identity exhausted'); 23 const id = ++requestId; 24 return new Promise((resolve, reject) => { pending = { id, resolve, reject }; send({ type: 'state', state: 'step', id }); }); 25 } 26 let ioCommand, ioRun, stopRequested = false; 27 function host(stepping) { 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) 32 await requestStep(); running(); }, 33 structured: { 34 stepping, 35 open(run, command) { ioRun = run; ioCommand = command; send({ type: 'io-open', run }); if (stopRequested) 36 command({ run, kind: 'stop' }); }, 37 event(event) { send({ type: 'io-event', event }); }, 38 close(run, settled) { ioCommand = undefined; ioRun = undefined; pending = undefined; send({ type: 'io-close', run, settled }); }, 39 }, 40 }; 41 } 42 scope.onmessage = ({ data }) => { 43 if (!data || typeof data !== 'object' || Array.isArray(data)) 44 return; 45 const m = data; 46 if (m['type'] === 'io-stop') { 47 if (!busy) 48 return; 49 stopRequested = true; 50 if (ioRun) 51 ioCommand?.({ run: ioRun, kind: 'stop' }); 52 if (pending) { 53 const waiter = pending; 54 pending = undefined; 55 waiter.resolve(); 56 } 57 return; 58 } 59 if (m['type'] === 'io-command') { 60 const command = m['command']; 61 if (command && typeof command === 'object') 62 ioCommand?.(command); 63 return; 64 } 65 if (m['type'] === 'resume') { 66 if (!pending || m['id'] !== pending.id) 67 return; 68 if (!validStepResume(m['value'])) 69 return; 70 const waiter = pending; 71 pending = undefined; 72 waiter.resolve(); 73 return; 74 } 75 if (busy) 76 return; 77 busy = true; 78 void (async () => { 79 if (m['type'] === 'check-import') { 80 if (session) 81 throw new Error('Already initialized'); 82 const source = sourceText(m['source']); 83 if (typeof m['manifest'] !== 'string' || typeof m['alias'] !== 'string') 84 throw new Error('Invalid import request'); 85 session = await createRuntimeSession(selectedBackend, true); 86 const stage = await session.inspectModule(source); 87 const plan = importPlan(source, stage, m['manifest'], m['alias']); 88 if (!plan.existing) 89 await session.compile(plan.source, 'empty', localModuleInputs(m['localModules'])); 90 send({ type: 'import-checked', plan }); 91 } 92 else if (m['type'] === 'load') { 93 if (session || stored) 94 throw new Error('Already initialized'); 95 const localId = m['localId']; 96 if (localId !== undefined && (typeof localId !== 'string' || localId.length > 128)) 97 throw new Error('Invalid catalogue ID'); 98 running(); 99 stored = await publishedBundle(bundleHash(m['hash']), localId); 100 session = await createRuntimeSession(selectedBackend, false); 101 await session.load(stored.bytes, stored.hash); 102 send({ type: 'loaded', hash: stored.hash, exports: stored.bundle.exports, nodes: stored.bundle.nodes.length, metadata: stored.bundle.metadata, source: stored.source }); 103 } 104 else if (m['type'] === 'compile') { 105 if (session) 106 throw new Error('Already initialized'); 107 const source = sourceText(m['source']), profile = m['environment']; 108 if (profile !== undefined && (typeof profile !== 'string' || profile.length > 128)) 109 throw new Error('Invalid demo environment'); 110 running(); 111 const start = performance.now(); 112 session = await createRuntimeSession(selectedBackend, true); 113 const names = await session.compile(source, profile, localModuleInputs(m['localModules'])); 114 send({ type: 'compiled', names, stats: session.stats(), milliseconds: performance.now() - start }); 115 } 116 else if (m['type'] === 'export-module') { 117 if (!session || stored) 118 throw new Error('Compile a draft first'); 119 running(); 120 try { 121 const module = await validateLocalModule(await session.exportModule()); 122 send({ type: 'module-exported', bytes: module.bytes, hash: module.hash, manifest: module.manifest }); 123 } 124 catch (e) { 125 if (session.disposed) 126 throw e; 127 send({ type: 'module-export-error', message: String(e) }); 128 } 129 } 130 else if (m['type'] === 'inspect') { 131 if (!session) 132 throw new Error('Load or compile first'); 133 if (!Number.isSafeInteger(m['id']) || typeof m['name'] !== 'string' || m['name'].length > 4096) 134 throw new Error('Invalid inspection'); 135 const id = m['id']; 136 try { 137 // Observe the existing binding with no arguments, expression compilation, 138 // or IO interpreter. Observation/decoding can still require reduction. 139 const name = m['name']; 140 if (m['output'] === 'auto' && await session.probeIO(name)) { 141 send({ type: 'inspected', id, io: true }); 142 return; 143 } 144 const result = m['output'] === 'auto' 145 ? await autoInterpretation(output => session.inspectDefinition(name, output), () => session.disposed) 146 : await session.inspectDefinition(name, outputType(m['output'])); 147 send({ type: 'inspected', id, result }); 148 } 149 catch (e) { 150 send({ type: 'inspection-error', id, message: e instanceof Error ? e.message : String(e), fatal: session.disposed }); 151 } 152 } 153 else if (m['type'] === 'run-io') { 154 if (!session || typeof m['name'] !== 'string' || m['name'].length > 4096) 155 throw new Error('Invalid IO definition'); 156 const output = outputType(m['output']); 157 running(); 158 const result = await session.runDefinitionIO(m['name'], output, host(m['step'] === true)); 159 send({ type: 'done', result, stats: session.stats() }); 160 } 161 else if (m['type'] === 'run') { 162 if (!session) 163 throw new Error('Load or compile first'); 164 if (typeof m['export'] !== 'string' || m['export'].length > 512) 165 throw new Error('Invalid export'); 166 if (m['mode'] !== 'pure' && m['mode'] !== 'io') 167 throw new Error('Unsupported execution mode'); 168 const args = literals(m['arguments']), output = outputType(m['output']); 169 running(); 170 const ioHost = host(m['step'] === true); 171 const result = m['expression'] === undefined 172 ? await session.run(m['export'], args, output, m['mode'], ioHost) 173 : await session.runExpression(sourceText(m['expression']), output, m['mode'], ioHost); 174 send({ type: 'done', result, stats: session.stats() }); 175 } 176 else if (m['type'] === 'export' && stored) { 177 try { 178 if (typeof m['export'] !== 'string') 179 throw new Error('Invalid export'); 180 const format = treeFormat(m['format']); 181 send({ type: 'exported', hash: stored.hash, export: m['export'], format, file: exportTree(stored.bundle, m['export'], format) }); 182 } 183 catch (e) { 184 send({ type: 'export-error', message: String(e) }); 185 } 186 } 187 else if (m['type'] === 'download' && stored) 188 send({ type: 'downloaded', hash: stored.hash, bytes: stored.bytes }); 189 else 190 throw new Error('Unsupported backend operation'); 191 })().catch((e) => send({ type: 'error', message: e instanceof Error ? e.message : String(e), fatal: session?.disposed ?? true })) 192 .finally(() => { busy = false; }); 193 };