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 }