tricu

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

io-v2-driver.ts (4505B)


      1 import type { IOHost } from './io.js';
      2 import type { Command, Session } from './io-v2-session.js';
      3 import type { ValueView } from './codecs.js';
      4 import type { Event } from './io-v2-events.js';
      5 export interface V2Host {
      6   readonly stepping?: boolean;
      7   // These transport callbacks must not throw or synchronously reenter a turn.
      8   open(run: string, command: (command: Command) => void): void;
      9   event(event: Event): void;
     10   close(run: string, settled: boolean): void;
     11 }
     12 export type Report = Awaited<ReturnType<Session<unknown>['turn']>>;
     13 export interface Driver {
     14   run: string;
     15   // Emergency runtime loss, distinct from structured stop/acknowledgement.
     16   signal?: AbortSignal;
     17   turn(command: Command): Promise<Report>;
     18   view(): Promise<ValueView>;
     19   discard?(): void;
     20 }
     21 // Unlike repeated Promise.race against a lifetime-long promise, listeners are
     22 // removed on EVERY turn/step settlement, not retained until runtime disposal.
     23 function interrupt<T>(promise: Promise<T>, signal: AbortSignal | undefined, aborted: () => T): Promise<T> {
     24   if (!signal) return promise;
     25   let onAbort = () => {};
     26   return new Promise<T>((resolve, reject) => {
     27     onAbort = () => { try { resolve(aborted()); } catch (error) { reject(error); } };
     28     if (signal.aborted) onAbort(); else signal.addEventListener('abort', onAbort, { once: true });
     29     void promise.then(resolve, reject);
     30   }).finally(() => signal.removeEventListener('abort', onAbort));
     31 }
     32 // The queue stays alive while a read or step prompt is waiting. Stop is sticky,
     33 // independent of the resume queue, and never implemented by worker destruction.
     34 export async function drive(driver: Driver, host: IOHost): Promise<ValueView> {
     35   const transport = host.v2;
     36   if (!transport) throw new Error('IO version 2 requires a structured terminal host');
     37   let settled = false;
     38   const alive = <T>(promise: Promise<T>): Promise<T> => interrupt(promise, driver.signal, () => { throw driver.signal!.reason; });
     39   let active = true, stopping = false, stopSent = false, wake = () => {}, step = 0;
     40   const replies: Command[] = [];
     41   let boundaryFault = false, fault: unknown;
     42   const stopped = new AbortController();
     43   const enqueue = (command: Command) => {
     44     if (!active || !command || command.run !== driver.run) return;
     45     if (command.kind === 'stop') { stopping = true; stopped.abort(); }
     46     else if (command.kind === 'reply') replies.push(command);
     47     wake();
     48   };
     49   const close = () => {
     50     if (!active) return;
     51     active = false; stopped.abort(); wake(); wake = () => {}; replies.length = 0;
     52     // A failed transport/runtime cannot be reused with admitted work abandoned.
     53     if (!settled) driver.discard?.();
     54     transport.close(driver.run, settled);
     55   };
     56   try {
     57     transport.open(driver.run, enqueue);
     58     for (;;) {
     59       if (!stopping && transport.stepping === true) {
     60         try { await alive(interrupt(host.before({ step: ++step, name: 'v2 scheduler turn', detail: 'one scheduler advance; all tasks paused', depth: 0 }), stopped.signal, () => {})); }
     61         catch (error) { if (driver.signal?.aborted) throw error; boundaryFault = true; fault = error; enqueue({ run: driver.run, kind: 'stop' }); }
     62       }
     63       const command: Command = stopping && !stopSent ? (stopSent = true, { run: driver.run, kind: 'stop' }) :
     64         replies.shift() ?? { run: driver.run, kind: 'poll' };
     65       const report = await alive(driver.turn(command));
     66       for (const event of report.events) transport.event(event);
     67       if (report.done) {
     68         if (!Object.values(report.counts).every(group => Object.values(group).every(value => value === 0))) throw new Error('IO v2 settlement retains work');
     69         settled = true; close();
     70         if (boundaryFault) throw fault;
     71         if (report.outcome?.fault) throw new Error('IO v2 interpreter fault');
     72         if (report.outcome?.kind !== 'completed') throw new Error(`IO v2 ${report.outcome?.kind ?? 'failed'}`);
     73         return await alive(driver.view()); // Full codec, only after physical settlement.
     74       }
     75       // Yield even during pure scheduler progress so the outer worker can receive
     76       // stop/replies. This is not a hard deadline for native tree reduction.
     77       if (report.waiting && !replies.length && (!stopping || stopSent)) await alive(new Promise<void>(resolve => { wake = resolve; }));
     78       else await alive(new Promise<void>(resolve => { const timer = setTimeout(resolve, 0); wake = () => { clearTimeout(timer); resolve(); }; }));
     79       wake = () => {};
     80     }
     81   } finally { close(); }
     82 }