tricu

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

io-host-work.ts (4302B)


      1 import { Tasks, hostKey, type HostToken } from './io-tasks.js';
      2 export type Reply<T> = { readonly token: HostToken; readonly outcome:
      3   { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: unknown } };
      4 // Adapter-side asynchronous work. Starting one callback never waits for it, so
      5 // sibling actions/cancellation can proceed. The provider must settle its promise
      6 // AFTER cancellation cleanup; abort delivery alone is never an acknowledgement.
      7 export class HostWork<P, T> {
      8   private readonly active = new Map<string, { token: HostToken; controller: AbortController }>();
      9   private readonly replies = new Map<string, Reply<T>>();
     10   private notification: { promise: Promise<void>; resolve: () => void } | null = null;
     11   constructor(private readonly tasks: Tasks<P>) {}
     12   start(scope: number, operation: (signal: AbortSignal) => T | Promise<T>): HostToken {
     13     return this.register(this.tasks.beginHost(scope), operation);
     14   }
     15   // Cleanup is not cancelled by a new stop request. Its result (including a
     16   // failure) must still reach the driver to preserve primary/secondary outcomes.
     17   release(scope: number, operation: () => T | Promise<T>): HostToken {
     18     return this.register(this.tasks.beginReleaseHost(scope), operation);
     19   }
     20   private register(token: HostToken, operation: (signal: AbortSignal) => T | Promise<T>): HostToken {
     21     const controller = new AbortController();
     22     this.active.set(hostKey(token), { token, controller });
     23     const complete = (outcome: Reply<T>['outcome']) => {
     24       // Settle first; a cancelled reply is discarded without observing its value.
     25       const disposition = this.tasks.endHost(token);
     26       this.active.delete(hostKey(token));
     27       if (disposition === 'reply') this.replies.set(hostKey(token), { token, outcome });
     28       const notice = this.notification; this.notification = null; notice?.resolve();
     29     };
     30     try { Promise.resolve(operation(controller.signal)).then(
     31       value => complete({ ok: true, value }), error => complete({ ok: false, error })); }
     32     catch (error) { complete({ ok: false, error }); }
     33     return token;
     34   }
     35   private usable(token: HostToken): boolean {
     36     if (!this.tasks.within(token.scope, token.scope)) return false;
     37     const state = this.tasks.state(token.scope);
     38     return token.kind === 'release' ? state.phase === 'releasing' : state.phase === 'open' && state.reason === null;
     39   }
     40   // Resource close may interrupt one operation without cancelling its task.
     41   // The resulting provider reply still reaches that body as an ordinary error.
     42   // Never abort a cleanup batch or acknowledge work merely by sending this signal.
     43   cancelBody(token: HostToken): boolean {
     44     const work = this.active.get(hostKey(token));
     45     if (token.kind !== 'body' || !work || work.token.run !== token.run ||
     46         work.token.scope !== token.scope || work.controller.signal.aborted) return false;
     47     work.controller.abort(); return true;
     48   }
     49   cancelStopped(): void {
     50     for (const token of this.tasks.takeCancellations()) {
     51       const work = this.active.get(hostKey(token));
     52       if (!work || work.token.scope !== token.scope) throw new Error('host work ownership mismatch');
     53       work.controller.abort();
     54     }
     55     // Completion may have won the race with cancellation. Drop undelivered data
     56     // too; a queued reply cannot revive a stopped/settled body.
     57     for (const [key, reply] of this.replies) if (!this.usable(reply.token)) this.replies.delete(key);
     58   }
     59   take(): Reply<T> | null {
     60     for (const [key, reply] of this.replies) {
     61       this.replies.delete(key);
     62       if (this.usable(reply.token)) return reply;
     63     }
     64     return null;
     65   }
     66   wait(): Promise<void> {
     67     if (this.replies.size || !this.active.size) return Promise.resolve();
     68     if (!this.notification) {
     69       let resolve!: () => void;
     70       const promise = new Promise<void>(done => { resolve = done; });
     71       this.notification = { promise, resolve };
     72     }
     73     return this.notification.promise;
     74   }
     75   get counts() { return { pending: this.active.size, replies: this.replies.size }; }
     76   // Reply data roots are the embedding's responsibility until taken/discarded.
     77   *values(): Iterable<T> {
     78     for (const reply of this.replies.values()) if (this.usable(reply.token) && reply.outcome.ok) yield reply.outcome.value;
     79   }
     80 }