io-v2-resources.js (9628B)
1 import { Tasks } from './io-v2-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 }