tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

worker.ts (5854B)


      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, type Bundle, type BundleHash } from '../bundle.js';
     10 import { literals, makeLiteral, outputType, view } from '../codecs.js';
     11 import { runIO } from '../io.js';
     12 import type { ClientMessage, WorkerMessage, RequestId } from '../protocol.js';
     13 const scope = globalThis as unknown as { postMessage(message: WorkerMessage): void; onmessage: ((event: MessageEvent<unknown>) => void) | null };
     14 const send = (message: WorkerMessage): void => scope.postMessage(message);
     15 const running = (): void => send({ type: 'state', state: 'running' });
     16 let phase: 'empty' | 'loading' | 'ready' | 'running' | 'done' = 'empty';
     17 let graph: Graph | undefined, exports: Map<string, import('../graph.js').NodeId> | undefined;
     18 let stored: { hash: BundleHash; bundle: Bundle; bytes: Uint8Array } | undefined;
     19 let requestId = 0;
     20 let pending: { id: RequestId; kind: 'step' | 'input'; resolve(value: string): void; reject(error: Error): void } | undefined;
     21 function request(kind: 'step' | 'input'): Promise<string> {
     22   if (pending) throw new Error('Driver already waiting');
     23   const id = ++requestId as RequestId;
     24   return new Promise((resolve, reject) => { pending = { id, kind, resolve, reject }; send({ type: 'state', state: kind, id }); });
     25 }
     26 async function load(hash: BundleHash, localId: string | undefined): Promise<void> {
     27   phase = 'loading'; running();
     28   const loaded = await publishedBundle(hash, localId);
     29   const { bundle, source } = loaded;
     30   stored = loaded;
     31   const budget = BROWSER_BUDGET;
     32   graph = new Graph(budget, collectionPolicy(budget, PACKAGE_COLLECTION.threshold)); exports = loadBundle(graph, bundle);
     33   phase = 'ready';
     34   send({ type: 'loaded', hash, exports: bundle.exports, nodes: bundle.nodes.length, metadata: bundle.metadata, source });
     35 }
     36 async function run(message: Extract<ClientMessage, { type: 'run' }>): Promise<void> {
     37   if (phase !== 'ready' || !graph || !exports) throw new Error('Load a bundle before running');
     38   phase = 'running'; running();
     39   const fn = message.expression === undefined ? exports.get(message.export) : await compileExpression(graph, exports, message.expression);
     40   if (fn === undefined) throw new Error('Unknown export');
     41   if (message.mode !== 'pure' && message.mode !== 'io') throw new Error('Unsupported execution mode');
     42   const type = outputType(message.output), args = literals(message.arguments).map(value => makeLiteral(graph!, value));
     43   let result = message.expression === undefined ? graph.apply(fn, args) : fn;
     44   graph.collect([result]); // Exclusive one-run session: only this application remains owned.
     45   if (message.mode === 'io') result = await runIO(graph, result, {
     46     async before(event) { send({ type: 'trace', phase: 'before', event }); if (message.step === true) await request('step'); running(); },
     47     after(event) { send({ type: 'trace', phase: 'after', event }); },
     48     write(text) { send({ type: 'output', text }); }, read() { return request('input'); }, running,
     49   }, true);
     50   const displayed = view(graph, result, type);
     51   phase = 'done'; send({ type: 'done', result: displayed, stats: graph.stats });
     52 }
     53 scope.onmessage = ({ data }) => {
     54   if (!data || typeof data !== 'object' || Array.isArray(data)) return;
     55   const message = data as Record<string, unknown>;
     56   if (message['type'] === 'resume') {
     57     if (!pending || message['id'] !== pending.id) return;
     58     const value = resumeValue(pending.kind, message['value']);
     59     if (value === undefined) return;
     60     const waiter = pending; pending = undefined; waiter.resolve(value);
     61     return;
     62   }
     63   if (message['type'] === 'download' && phase === 'ready' && stored) {
     64     send({ type: 'downloaded', hash: stored.hash, bytes: stored.bytes }); return;
     65   }
     66   if (message['type'] === 'export' && phase === 'ready' && stored) {
     67     try {
     68       if (typeof message['export'] !== 'string' || message['export'].length > 512) throw new Error('Invalid export selection');
     69       const format = treeFormat(message['format']);
     70       const file = exportTree(stored.bundle, message['export'], format);
     71       send({ type: 'exported', hash: stored.hash, export: message['export'], format, file });
     72     } catch (error) { send({ type: 'export-error', message: error instanceof Error ? error.message : String(error) }); }
     73     return;
     74   }
     75   let operation: Promise<void>;
     76   try {
     77     if (message['type'] === 'load' && phase === 'empty') {
     78       const localId = message['localId'];
     79       if (localId !== undefined && (typeof localId !== 'string' || localId.length > 128)) throw new Error('Invalid catalogue ID');
     80       operation = load(bundleHash(message['hash']), localId);
     81     } else if (message['type'] === 'run' && phase === 'ready') {
     82       if (typeof message['export'] !== 'string' || message['export'].length > 512) throw new Error('Invalid export selection');
     83       operation = run({ type: 'run', export: message['export'], arguments: message['arguments'],
     84         ...(message['expression'] === undefined ? {} : { expression: sourceText(message['expression']) }),
     85         output: outputType(message['output']), mode: message['mode'] as 'pure' | 'io', step: message['step'] === true });
     86     } else return; // A Worker owns exactly one load and one run, no racing sessions.
     87   } catch (error) { operation = Promise.reject(error); }
     88   operation.catch((error: unknown) => { phase = 'done'; send({ type: 'error', message: error instanceof Error ? error.message : String(error) }); });
     89 };