tricu

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

io-resources.js (9625B)


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