tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

io-v2-resources.ts (9550B)


      1 import { Tasks, type Token, type Diagnostics } from './io-v2-tasks.js';
      2 export interface Flags { readonly borrowed?: boolean; readonly internal?: boolean; readonly duplex?: boolean; }
      3 export interface Resource<B> {
      4   readonly owner: number; readonly kind: string; readonly backend: B; readonly flags: Flags;
      5   readonly busy: number; readonly closing: boolean; readonly releasing: boolean;
      6 }
      7 type Entry<B> = { -readonly [K in keyof Resource<B>]: Resource<B>[K] };
      8 export type Access = 'exclusive' | 'read' | 'write';
      9 // State only. Construct via lifecycle() to bind task settlement to registry guards.
     10 // Single owning event loop; host effects occur AFTER admission/lease/close claims.
     11 class Resources<P, B> {
     12   private next = 4;
     13   private readonly resources = new Map<number, Entry<B>>();
     14   private readonly admissions = new Map<number, number>();
     15   private readonly leases = new Map<number, { scope: number; resource: number; mask: number }>();
     16   constructor(private readonly tasks: Tasks<P>, standard: readonly [B, B, B], private readonly limit: number) {
     17     if (!Number.isSafeInteger(limit) || limit < 3) throw new Error('invalid resource identity limit');
     18     standard.forEach((backend, index) => this.resources.set(index + 1, {
     19       owner: 0, kind: index === 0 ? 'reader' : 'writer', backend, flags: Object.freeze({ borrowed: true }),
     20       busy: 0, closing: false, releasing: false }));
     21   }
     22   get counts() { return { resources: this.resources.size, admissions: this.admissions.size, leases: this.leases.size }; }
     23   private token(key: number): Token { return Object.freeze({ run: this.tasks.identity, key }); }
     24   private key(token: Token): number {
     25     if (token.run !== this.tasks.identity) throw new Error('stale-handle');
     26     return token.key;
     27   }
     28   private room(count: number): void {
     29     if (count > this.limit - this.next + 1) throw new Error('resource-identity-exhausted');
     30   }
     31   private open(scope: number): void {
     32     const state = this.tasks.state(scope);
     33     if (state.phase !== 'open' || state.reason) throw new Error('closed-scope');
     34   }
     35   private entry(current: number, token: Token): Entry<B> {
     36     const resource = this.resources.get(this.key(token));
     37     if (!resource) throw new Error('stale-handle');
     38     if (!this.tasks.within(current, resource.owner)) throw new Error('out-of-scope');
     39     return resource;
     40   }
     41   lookup(current: number, token: Token): Resource<B> { return { ...this.entry(current, token) }; }
     42   beginAcquire(current: number, owner = current): Token {
     43     this.tasks.checkpoint(current); this.open(owner);
     44     if (!this.tasks.within(current, owner)) throw new Error('out-of-scope');
     45     this.room(1); const token = this.token(this.next++);
     46     this.admissions.set(token.key, owner); return token;
     47   }
     48   abortAcquire(token: Token): void {
     49     if (!this.admissions.delete(this.key(token))) throw new Error('invalid-admission');
     50   }
     51   // An admitted acquisition can finish after owner stop/admission closure.
     52   // Publication failure leaves the admission live: release raw backend, then abort.
     53   publish(admission: Token, kind: string, backend: B, flags: Flags = {}): Token {
     54     const key = this.key(admission), owner = this.admissions.get(key);
     55     if (owner === undefined) throw new Error('invalid-admission');
     56     if (!kind || Object.values(flags).some(value => typeof value !== 'boolean')) throw new Error('invalid-argument');
     57     this.room(1);
     58     const savedFlags = Object.freeze({ ...flags }), token = this.token(this.next);
     59     this.resources.set(token.key, { owner, kind, backend, flags: savedFlags, busy: 0, closing: false, releasing: false });
     60     this.next++; this.admissions.delete(key); return token;
     61   }
     62   // All recoverable validation and preparation precede task admission. No host
     63   // callback/await separates admission from rekeying. JS OOM is fatal, not rollback.
     64   spawn(owner: number, supervised: boolean, keep: boolean, tokens: readonly Token[]) {
     65     this.tasks.checkpoint(owner); this.open(owner);
     66     if (typeof supervised !== 'boolean' || typeof keep !== 'boolean') throw new Error('invalid-argument');
     67     if (tokens.length > 256) throw new Error('transfer-limit');
     68     const seen = new Set<number>();
     69     const entries = tokens.map(token => {
     70       const resource = this.entry(owner, token);
     71       if (seen.has(token.key)) throw new Error('duplicate-handle');
     72       seen.add(token.key);
     73       if (resource.owner !== owner) throw new Error('not-owner');
     74       if (resource.flags.borrowed) throw new Error('borrowed-resource');
     75       if (resource.flags.internal) throw new Error('internal-resource');
     76       if (resource.closing) throw new Error('closed-handle');
     77       if (resource.busy) throw new Error('busy');
     78       return { old: token.key, resource };
     79     }).sort((a, b) => a.old - b.old);
     80     this.room(tokens.length);
     81     const fresh = new Map(entries.map((entry, i) => [entry.old, this.token(this.next + i)]));
     82     const handles = tokens.map(token => fresh.get(token.key)!);
     83     const task = this.tasks.spawn(owner, supervised, keep);
     84     for (const entry of entries) {
     85       this.resources.delete(entry.old); entry.resource.owner = task.key;
     86       this.resources.set(fresh.get(entry.old)!.key, entry.resource);
     87     }
     88     this.next += tokens.length;
     89     return { task, handles };
     90   }
     91   beginLease(current: number, token: Token, kind: string, access: Access, internal = false) {
     92     this.tasks.checkpoint(current);
     93     const resource = this.entry(current, token);
     94     if (resource.kind !== kind) throw new Error('wrong-kind');
     95     if (resource.closing) throw new Error('closed-handle');
     96     if (resource.flags.internal && !internal) throw new Error('internal-resource');
     97     if (!['exclusive', 'read', 'write'].includes(access)) throw new Error('invalid-argument');
     98     const mask = !resource.flags.duplex || access === 'exclusive' ? 3 : access === 'read' ? 1 : 2;
     99     if (resource.busy & mask) throw new Error('busy');
    100     this.room(1); const lease = this.token(this.next++);
    101     this.leases.set(lease.key, { scope: current, resource: token.key, mask }); resource.busy |= mask;
    102     return { token: lease, backend: resource.backend };
    103   }
    104   endLease(token: Token): void {
    105     const key = this.key(token), lease = this.leases.get(key);
    106     if (!lease) throw new Error('invalid-lease');
    107     this.resources.get(lease.resource)!.busy &= ~lease.mask; this.leases.delete(key);
    108   }
    109   claimClose(current: number, token: Token, settling = false): Resource<B> | null {
    110     const resource = this.entry(current, token);
    111     if (settling) {
    112       if (resource.owner !== current || this.tasks.state(current).phase !== 'releasing') throw new Error('invalid-close');
    113     } else {
    114       this.tasks.checkpoint(current);
    115       if (resource.flags.internal) throw new Error('internal-resource');
    116       if (resource.flags.borrowed) throw new Error('borrowed-resource');
    117     }
    118     resource.closing = true;
    119     if (resource.busy || resource.releasing) return null;
    120     resource.releasing = true; return { ...resource };
    121   }
    122   // Once, including failed release. Never retry a physically ambiguous close.
    123   finishClose(token: Token): void {
    124     const key = this.key(token), resource = this.resources.get(key);
    125     if (!resource) throw new Error('stale-handle');
    126     if (!resource.releasing) throw new Error('invalid-close');
    127     this.resources.delete(key);
    128   }
    129   newestOwned(scope: number): Token | null {
    130     let newest = -1;
    131     for (const [key, resource] of this.resources) if (resource.owner === scope && key > newest) newest = key;
    132     return newest < 0 ? null : this.token(newest);
    133   }
    134   readyForRelease(scope: number): boolean {
    135     return ![...this.admissions.values()].includes(scope) && ![...this.leases.values()].some(lease => lease.scope === scope);
    136   }
    137   finishScope(scope: number): void {
    138     if (!this.readyForRelease(scope) || this.newestOwned(scope)) throw new Error('busy');
    139   }
    140   // Use inside HostWork.release, after Tasks.beginRelease. Each provider is
    141   // attempted once, sequentially in reverse publication order; borrowed backends
    142   // stay open. Diagnostics construction failure must not skip remaining releases.
    143   async releaseScope(scope: number, release: (resource: Resource<B>) => void | Promise<void>,
    144     problem: (error: unknown) => P, fallback: P): Promise<Diagnostics<P>> {
    145     if (this.tasks.state(scope).phase !== 'releasing') throw new Error('invalid-transition');
    146     const items: P[] = []; let omitted = 0;
    147     for (let token = this.newestOwned(scope); token; token = this.newestOwned(scope)) {
    148       const resource = this.claimClose(scope, token, true);
    149       if (!resource) throw new Error('busy');
    150       try { if (!resource.flags.borrowed) await release(resource); }
    151       catch (error) {
    152         if (items.length === 16) omitted++;
    153         else {
    154           let value = fallback; try { value = problem(error); } catch { /* retain prebuilt fallback */ }
    155           items.push(value);
    156         }
    157       } finally { this.finishClose(token); }
    158     }
    159     return { items, omitted };
    160   }
    161 }
    162 export function lifecycle<P, B>(identity: bigint, ownerStopped: P, cycle: P, standard: readonly [B, B, B],
    163   limits: { taskIdentities?: number; resourceIdentities?: number } = {}) {
    164   let resources!: Resources<P, B>;
    165   const tasks = new Tasks(identity, ownerStopped, cycle, limits.taskIdentities ?? Number.MAX_SAFE_INTEGER, {
    166     readyForRelease: scope => resources.readyForRelease(scope), finishScope: scope => resources.finishScope(scope) });
    167   resources = new Resources(tasks, standard, limits.resourceIdentities ?? Number.MAX_SAFE_INTEGER);
    168   return { tasks, resources };
    169 }