tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

io-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 }