io-v2-events.ts (4772B)
1 // Cloneable transport only: no evaluator handles, task tokens or continuations. 2 // A reply acknowledges physical provider completion, not merely abort delivery. 3 export type Event = { run: string; id: number } & ( 4 { kind: 'read'; limit: number } | { kind: 'write'; stream: 'output' | 'error'; bytes: number[] } | 5 { kind: 'cancel'; operation: 'read' | 'write' }); 6 export type Completion = { kind: 'read'; bytes: number[]; eof: boolean } | 7 { kind: 'write'; count: number } | { kind: 'cancelled' } | { kind: 'error' }; 8 interface Pending { 9 event: Event; delivered: boolean; cancelling: boolean; 10 resolve(value: Completion): void; remove(): void; 11 } 12 function bytes(value: unknown, limit: number): value is number[] { 13 return Array.isArray(value) && value.length <= limit && value.every(x => Number.isInteger(x) && x >= 0 && x <= 255); 14 } 15 export class Events { 16 private next = 1; 17 private readonly pending = new Map<number, Pending>(); 18 private queue: Event[] = []; 19 constructor(readonly run: string, private readonly limit = Number.MAX_SAFE_INTEGER) { 20 if (!/^[1-9][0-9]*$/.test(run) || run.length > 20 || BigInt(run) > 0xffffffffffffffffn || 21 !Number.isSafeInteger(limit) || limit < 1) throw new Error('invalid event identity'); 22 } 23 get counts() { return { pending: this.pending.size, queued: this.queue.length }; } 24 read(limit: number, signal: AbortSignal): Promise<Completion> { 25 if (!Number.isInteger(limit) || limit < 1 || limit > 4096) throw new Error('invalid read bound'); 26 return this.start({ run: this.run, id: 0, kind: 'read', limit }, signal); 27 } 28 write(stream: 'output' | 'error', value: readonly number[], signal: AbortSignal): Promise<Completion> { 29 if (!['output', 'error'].includes(stream) || !bytes(value, 4096) || !value.length) throw new Error('invalid write chunk'); 30 return this.start({ run: this.run, id: 0, kind: 'write', stream, bytes: [...value] }, signal); 31 } 32 private start(event: Event, signal: AbortSignal): Promise<Completion> { 33 if (signal.aborted) return Promise.resolve({ kind: 'cancelled' }); 34 if (this.next > this.limit) throw new Error('event identity exhausted'); 35 event.id = this.next++; 36 return new Promise(resolve => { 37 const cancel = () => { 38 const entry = this.pending.get(event.id); if (!entry || entry.cancelling) return; 39 entry.cancelling = true; 40 if (!entry.delivered) this.finish(event.id, { kind: 'cancelled' }); 41 else this.queue.push({ run: this.run, id: event.id, kind: 'cancel', operation: event.kind as 'read' | 'write' }); 42 }; 43 this.pending.set(event.id, { event, delivered: false, cancelling: false, resolve, 44 remove: () => signal.removeEventListener('abort', cancel) }); 45 signal.addEventListener('abort', cancel, { once: true }); this.queue.push(event); 46 }); 47 } 48 take(): Event[] { 49 const out = this.queue; this.queue = []; 50 for (const event of out) this.pending.get(event.id)!.delivered = true; 51 // Never expose the mutable validation record or its byte array to the peer. 52 return out.map(event => event.kind === 'write' ? { ...event, bytes: [...event.bytes] } : { ...event }); 53 } 54 private finish(id: number, value: Completion): void { 55 const entry = this.pending.get(id)!; 56 this.pending.delete(id); this.queue = this.queue.filter(event => event.id !== id); entry.remove(); 57 entry.resolve(entry.cancelling ? { kind: 'cancelled' } : value); 58 } 59 // Wrong run/kind/bounds and stale/duplicate replies never consume a live wait. 60 // A valid normal completion can race with cancellation: it acknowledges work, 61 // but its data is discarded. No extra cancellation acknowledgement is needed. 62 reply(value: unknown): boolean { 63 if (!value || typeof value !== 'object') return false; 64 const v = value as Record<string, unknown>; 65 if (v.run !== this.run || typeof v.id !== 'number' || !Number.isSafeInteger(v.id)) return false; 66 const entry = this.pending.get(v.id); if (!entry || !entry.delivered) return false; 67 let completion: Completion; 68 if (v.kind === 'cancelled' && entry.cancelling) completion = { kind: 'cancelled' }; 69 else if (v.kind === 'error') completion = { kind: 'error' }; 70 else if (v.kind === 'read' && entry.event.kind === 'read' && bytes(v.bytes, entry.event.limit) && 71 typeof v.eof === 'boolean' && (v.eof ? v.bytes.length === 0 : v.bytes.length > 0)) 72 completion = { kind: 'read', bytes: [...v.bytes], eof: v.eof }; 73 else if (v.kind === 'write' && entry.event.kind === 'write' && typeof v.count === 'number' && 74 Number.isInteger(v.count) && v.count > 0 && v.count <= entry.event.bytes.length) 75 completion = { kind: 'write', count: v.count }; 76 else return false; 77 this.finish(v.id, completion); return true; 78 } 79 }