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