@orkestrel/pool 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,254 +1,622 @@
1
1
  import { Emitter } from "@orkestrel/emitter";
2
+ //#region src/core/errors.ts
3
+ /**
4
+ * A stable, machine-readable pool failure with the original cause and structured context.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { PoolError, isPoolError } from '@orkestrel/pool'
9
+ *
10
+ * try {
11
+ * await pool.acquire()
12
+ * } catch (error: unknown) {
13
+ * if (isPoolError(error)) console.error(error.code, error.cause)
14
+ * }
15
+ * ```
16
+ */
17
+ var PoolError = class extends Error {
18
+ /** Stable machine-readable failure category. */
19
+ code;
20
+ /** Optional structured input or aggregate-cleanup details. */
21
+ context;
22
+ /**
23
+ * Create a pool failure without coercing a hostile thrown value.
24
+ *
25
+ * @param options - Stable code plus optional cause and structured context
26
+ */
27
+ constructor(options) {
28
+ let message = "pool input is invalid";
29
+ if (options.code === "destroyed") message = "pool is destroyed";
30
+ if (options.code === "create") message = "pool create failed";
31
+ if (options.code === "cleanup") message = "pool cleanup failed";
32
+ try {
33
+ if (options.cause instanceof Error && typeof options.cause.message === "string" && options.cause.message.length > 0) message = `${message}: ${options.cause.message}`;
34
+ } catch {}
35
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
36
+ this.name = "PoolError";
37
+ this.code = options.code;
38
+ this.context = options.context;
39
+ }
40
+ };
41
+ /**
42
+ * Test whether an unknown value is a {@link PoolError}, returning `false` for hostile proxies.
43
+ *
44
+ * @param value - The unknown boundary value
45
+ * @returns Whether the value is a real `PoolError` instance
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * isPoolError(new PoolError({ code: 'destroyed' })) // true
50
+ * isPoolError(new Error('other')) // false
51
+ * ```
52
+ */
53
+ function isPoolError(value) {
54
+ try {
55
+ return value instanceof PoolError;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+ //#endregion
61
+ //#region src/core/validators.ts
62
+ /**
63
+ * Test whether a value is a valid finite pool maximum.
64
+ *
65
+ * @param value - The unknown maximum candidate
66
+ * @returns Whether the value is a positive safe integer
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * isPoolMax(8) // true
71
+ * isPoolMax(Infinity) // false
72
+ * ```
73
+ */
74
+ function isPoolMax(value) {
75
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
76
+ }
77
+ /**
78
+ * Test whether a value is a native `AbortSignal`, returning `false` for hostile proxies.
79
+ *
80
+ * @param value - The unknown signal candidate
81
+ * @returns Whether the value is a native `AbortSignal`
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * isPoolSignal(new AbortController().signal) // true
86
+ * isPoolSignal({ aborted: false }) // false
87
+ * ```
88
+ */
89
+ function isPoolSignal(value) {
90
+ try {
91
+ const getter = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")?.get;
92
+ if (getter === void 0) return false;
93
+ Reflect.apply(getter, value, []);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+ //#endregion
2
100
  //#region src/core/Pool.ts
3
101
  /**
4
- * A bounded resource pool with idle reuse + FIFO waiting.
102
+ * A capacity-aware resource pool whose opaque ownership records preserve FIFO settlement,
103
+ * cancellation, exact lease release, and deterministic teardown under concurrent hooks.
5
104
  *
6
- * @remarks
7
- * - **Idle reuse.** `acquire` first takes an idle resource (validating it when a
8
- * `validate` hook is set — an invalid one is destroyed and the next idle / a fresh
9
- * one is tried). When no usable idle resource exists and the pool is below `max`, it
10
- * `create`s a new one. At `max` with none idle, the acquire PARKS on a FIFO waiter
11
- * list until a `release` hands it a resource.
12
- * - **FIFO handoff (validated).** `release` (on the token) hands the resource to the next
13
- * parked waiter (oldest first) the resource stays leased, the lessee just changes —
14
- * or returns it to idle when no one is waiting. With a waiter parked the resource is
15
- * re-validated first (the same `validate` hook the idle path uses), so a resource that
16
- * went invalid WHILE leased (e.g. a terminated worker thread) is destroyed and the
17
- * waiter is served a fresh/valid one instead — a dead resource is never handed on. The
18
- * no-waiter path stays synchronous; releasing the same token twice is a no-op (an
19
- * idempotent token guard).
20
- * - **`validate` is total.** A `validate` hook that THROWS is treated exactly like one
21
- * returning `false` — the resource is "not usable", so it is destroyed and replaced —
22
- * rather than escaping. This holds on both the idle reuse and the FIFO handoff paths, so
23
- * a throwing validator can never strand a parked waiter on an unhandled rejection.
24
- * - **Abort-cancellable waiting.** A parked `acquire` given an `AbortSignal` rejects
25
- * when that signal fires and removes its waiter from the queue — no leaked waiter, so
26
- * a later `release` still serves the next live waiter. The signal is supplied by the
27
- * caller (a worker, for example, passes its per-attempt execution signal); the pool adds no abort
28
- * of its own.
29
- * - **Counts.** `size` = idle + leased; `idle` = available now; `active` = leased out.
30
- * - **Teardown.** `clear` destroys every IDLE resource (leased ones keep running);
31
- * `destroy` destroys ALL resources and rejects any parked waiters. Both await the
32
- * `destroy` hook.
33
- * - **Observable (§13).** The owned {@link emitter} ({@link PoolEventMap}) carries the
34
- * resource lifecycle — `create` / `acquire` / `release` / `destroy` — for fire-and-forget
35
- * observers. Every event is emitted directly, strictly AFTER the relevant transition —
36
- * OUTSIDE the `#handoff` / `#serve` await-chain, never across a waiter's resolve; the
37
- * emitter isolates a listener throw and routes it to its `error` handler (the `error`
38
- * option), so a buggy observer can NEVER corrupt the validated FIFO handoff-eviction
39
- * machinery (it cannot strand a parked waiter or unbalance the lease count). Observation is
40
- * purely a side-channel.
41
- * - **De-bloated.** No warm-floor / `min`, no eviction timers — lean.
105
+ * @typeParam T - The pooled resource value
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { Pool } from '@orkestrel/pool'
110
+ *
111
+ * const pool = new Pool({ create: () => new Uint8Array(64), max: 2 })
112
+ * const token = await pool.acquire()
113
+ * try {
114
+ * consume(token.value)
115
+ * } finally {
116
+ * token.release()
117
+ * }
118
+ * await pool.destroy()
119
+ * ```
42
120
  */
43
121
  var Pool = class {
44
122
  #create;
45
- #destroy;
123
+ #cleanup;
46
124
  #validate;
47
125
  #max;
48
126
  #emitter;
49
- #idle = [];
127
+ #resources = /* @__PURE__ */ new Map();
128
+ #available = [];
129
+ #validating = /* @__PURE__ */ new Set();
130
+ #leased = /* @__PURE__ */ new Set();
131
+ #readyRecords = /* @__PURE__ */ new Set();
132
+ #destroying = /* @__PURE__ */ new Map();
50
133
  #waiters = [];
51
- #active = 0;
52
- #destroyed = false;
134
+ #assigned = /* @__PURE__ */ new Set();
135
+ #reservations = /* @__PURE__ */ new Set();
136
+ #signals = /* @__PURE__ */ new Map();
137
+ #ready = /* @__PURE__ */ new Map();
138
+ #operations = /* @__PURE__ */ new Set();
139
+ #owned = /* @__PURE__ */ new Set();
140
+ #failures = [];
141
+ #ending;
142
+ #pumping = false;
143
+ #repump = false;
144
+ /**
145
+ * Construct a pool and synchronously validate its capacity contract.
146
+ *
147
+ * @param options - Resource hooks, observation hooks, and optional positive safe `max`
148
+ */
53
149
  constructor(options) {
150
+ if (options.max !== void 0 && !isPoolMax(options.max)) throw new PoolError({
151
+ code: "invalid",
152
+ context: { value: options.max }
153
+ });
54
154
  this.#create = options.create;
55
- this.#destroy = options.destroy;
155
+ this.#cleanup = options.destroy;
56
156
  this.#validate = options.validate;
57
- this.#max = Math.max(1, options.max ?? Number.POSITIVE_INFINITY);
157
+ this.#max = options.max;
58
158
  this.#emitter = new Emitter({
59
- on: options?.on,
60
- error: options?.error
159
+ ...options.on === void 0 ? {} : { on: options.on },
160
+ ...options.error === void 0 ? {} : { error: options.error }
61
161
  });
62
162
  }
163
+ /** The typed synchronous lifecycle observation surface. */
63
164
  get emitter() {
64
165
  return this.#emitter;
65
166
  }
167
+ /** All owned records, including records validating or destroying. */
66
168
  get size() {
67
- return this.#idle.length + this.#active;
169
+ return this.#resources.size;
68
170
  }
171
+ /** Records immediately available without validation work. */
69
172
  get idle() {
70
- return this.#idle.length;
173
+ return this.#available.length;
71
174
  }
175
+ /** Records represented by unsettled released-once lease tokens. */
72
176
  get active() {
73
- return this.#active;
177
+ return this.#leased.size;
74
178
  }
75
- async acquire(signal) {
76
- if (this.#destroyed) throw new Error("pool is destroyed");
77
- if (signal?.aborted === true) throw signal.reason;
78
- const reused = await this.#reuse();
79
- if (reused !== void 0) {
80
- this.#emitter.emit("acquire");
81
- return reused;
179
+ /**
180
+ * Queue and lease one resource in FIFO settlement order.
181
+ *
182
+ * @param signal - Optional native cancellation signal
183
+ * @returns A promise for the unique resource lease
184
+ */
185
+ acquire(signal) {
186
+ if (signal !== void 0 && !isPoolSignal(signal)) throw new PoolError({
187
+ code: "invalid",
188
+ context: { value: signal }
189
+ });
190
+ if (this.#ending !== void 0) return Promise.reject(new PoolError({ code: "destroyed" }));
191
+ if (signal !== void 0) {
192
+ const state = this.#state(signal);
193
+ if (state[0]) return Promise.reject(state[1]);
82
194
  }
83
- if (this.size < this.#max) {
84
- const grown = await this.#grow();
85
- this.#emitter.emit("acquire");
86
- return grown;
195
+ const waiter = Promise.withResolvers();
196
+ this.#waiters.push(waiter);
197
+ if (signal !== void 0) {
198
+ const listener = this.#createAbort(waiter, signal);
199
+ AbortSignal.prototype.addEventListener.call(signal, "abort", listener, { once: true });
200
+ this.#signals.set(waiter, {
201
+ signal,
202
+ listener
203
+ });
204
+ const state = this.#state(signal);
205
+ if (state[0]) this.#abort(waiter, state[1]);
87
206
  }
88
- return await this.#wait(signal);
207
+ this.#pump();
208
+ return waiter.promise;
89
209
  }
90
- async clear() {
91
- const resources = this.#idle.splice(0);
92
- await Promise.all(resources.map((resource) => this.#release(resource)));
210
+ /**
211
+ * Destroy the records that are idle at this call's synchronous snapshot.
212
+ *
213
+ * @returns A promise that settles after every snapshot cleanup attempt
214
+ */
215
+ clear() {
216
+ if (this.#ending !== void 0) return Promise.reject(new PoolError({ code: "destroyed" }));
217
+ const records = this.#available.splice(0);
218
+ const cleanups = [];
219
+ for (const record of records) cleanups.push(this.#dispose(record));
220
+ return this.#settleClear(cleanups);
93
221
  }
94
- async destroy() {
95
- if (this.#destroyed) return;
96
- this.#destroyed = true;
222
+ /**
223
+ * Permanently tear down the pool and return its stable completion barrier.
224
+ *
225
+ * @returns The exact promise shared by every destroy call
226
+ */
227
+ destroy() {
228
+ if (this.#ending !== void 0) return this.#ending.promise;
229
+ const ending = Promise.withResolvers();
230
+ this.#ending = ending;
97
231
  const waiters = this.#waiters.splice(0);
98
- const error = /* @__PURE__ */ new Error("pool is destroyed");
99
232
  for (const waiter of waiters) {
100
- waiter.clear();
101
- waiter.reject(error);
233
+ this.#detach(waiter);
234
+ waiter.reject(new PoolError({ code: "destroyed" }));
102
235
  }
103
- const resources = this.#idle.splice(0);
104
- await Promise.all(resources.map((resource) => this.#release(resource)));
105
- }
106
- async #reuse() {
107
- while (this.#idle.length > 0) {
108
- const resource = this.#idle.shift();
109
- if (resource === void 0) continue;
110
- if (await this.#valid(resource)) {
111
- this.#active += 1;
112
- return this.#token(resource);
236
+ this.#ready.clear();
237
+ this.#assigned.clear();
238
+ this.#available.splice(0);
239
+ for (const cleanup of this.#destroying.values()) this.#own(cleanup);
240
+ for (const record of this.#resources.keys()) if (!this.#validating.has(record)) this.#own(this.#dispose(record));
241
+ this.#finish();
242
+ return ending.promise;
243
+ }
244
+ #createAbort(waiter, signal) {
245
+ return () => {
246
+ let reason;
247
+ try {
248
+ reason = this.#state(signal)[1];
249
+ } catch (error) {
250
+ reason = error;
113
251
  }
114
- await this.#release(resource);
115
- }
252
+ this.#abort(waiter, reason);
253
+ };
116
254
  }
117
- async #grow() {
118
- this.#active += 1;
119
- let resource;
255
+ #state(signal) {
256
+ const aborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")?.get;
257
+ const reason = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "reason")?.get;
258
+ if (aborted === void 0 || reason === void 0) throw new PoolError({
259
+ code: "invalid",
260
+ context: { value: signal }
261
+ });
120
262
  try {
121
- resource = await this.#create();
263
+ const stopped = Reflect.apply(aborted, signal, []) === true;
264
+ return [stopped, stopped ? Reflect.apply(reason, signal, []) : void 0];
122
265
  } catch (error) {
123
- this.#active -= 1;
124
- throw error instanceof Error ? error : new Error(String(error));
266
+ throw new PoolError({
267
+ code: "invalid",
268
+ cause: error,
269
+ context: { value: signal }
270
+ });
125
271
  }
126
- if (this.#destroyed) {
127
- this.#active -= 1;
128
- await this.#release(resource);
129
- throw new Error("pool is destroyed");
130
- }
131
- this.#emitter.emit("create");
132
- return this.#token(resource);
133
- }
134
- #wait(signal) {
135
- return new Promise((resolve, reject) => {
136
- const waiter = {
137
- resolve,
138
- reject,
139
- clear: () => {}
140
- };
141
- this.#waiters.push(waiter);
142
- if (signal !== void 0) {
143
- const onAbort = () => {
144
- const index = this.#waiters.indexOf(waiter);
145
- if (index >= 0) this.#waiters.splice(index, 1);
146
- reject(signal.reason);
147
- };
148
- signal.addEventListener("abort", onAbort, { once: true });
149
- waiter.clear = () => signal.removeEventListener("abort", onAbort);
150
- }
151
- });
152
272
  }
153
- #token(resource) {
273
+ #createRelease(record) {
154
274
  let released = false;
275
+ return () => {
276
+ if (released) return;
277
+ released = true;
278
+ this.#release(record);
279
+ };
280
+ }
281
+ #token(record, value) {
155
282
  return {
156
- value: resource,
157
- release: () => {
158
- if (released) return;
159
- released = true;
160
- this.#return(resource);
161
- }
283
+ value,
284
+ release: this.#createRelease(record)
162
285
  };
163
286
  }
164
- #return(resource) {
165
- if (this.#waiters.length === 0) {
166
- this.#active -= 1;
167
- if (this.#destroyed) {
168
- this.#release(resource);
169
- return;
170
- }
171
- this.#idle.push(resource);
172
- this.#emitter.emit("release");
287
+ #abort(waiter, reason) {
288
+ const index = this.#waiters.indexOf(waiter);
289
+ if (index < 0) return;
290
+ this.#waiters.splice(index, 1);
291
+ this.#detach(waiter);
292
+ const ready = this.#ready.get(waiter);
293
+ this.#ready.delete(waiter);
294
+ this.#assigned.delete(waiter);
295
+ if (ready?.success === true) {
296
+ this.#readyRecords.delete(ready.record);
297
+ this.#recycle(ready.record);
298
+ }
299
+ waiter.reject(reason);
300
+ this.#commit();
301
+ this.#pump();
302
+ }
303
+ #detach(waiter) {
304
+ const entry = this.#signals.get(waiter);
305
+ if (entry === void 0) return;
306
+ AbortSignal.prototype.removeEventListener.call(entry.signal, "abort", entry.listener);
307
+ this.#signals.delete(waiter);
308
+ }
309
+ #pump() {
310
+ if (this.#ending !== void 0) return;
311
+ if (this.#pumping) {
312
+ this.#repump = true;
173
313
  return;
174
314
  }
175
- this.#handoff(resource);
315
+ this.#pumping = true;
316
+ do {
317
+ this.#repump = false;
318
+ for (const waiter of this.#waiters) {
319
+ if (this.#assigned.has(waiter) || this.#ready.has(waiter)) continue;
320
+ const record = this.#available.shift();
321
+ if (record !== void 0) {
322
+ this.#assigned.add(waiter);
323
+ this.#validating.add(record);
324
+ this.#startValidation(waiter, record);
325
+ continue;
326
+ }
327
+ if (this.#max === void 0 || this.#resources.size + this.#reservations.size < this.#max) {
328
+ this.#assigned.add(waiter);
329
+ this.#reservations.add(waiter);
330
+ this.#startCreate(waiter);
331
+ continue;
332
+ }
333
+ break;
334
+ }
335
+ } while (this.#repump);
336
+ this.#pumping = false;
337
+ }
338
+ #startCreate(waiter) {
339
+ const operation = Promise.resolve().then(() => this.#createResource(waiter));
340
+ this.#operations.add(operation);
341
+ operation.then(() => this.#completeOperation(operation), (error) => this.#failOperation(operation, waiter, error, "create"));
176
342
  }
177
- async #handoff(resource) {
178
- if (await this.#valid(resource)) {
179
- this.#serve(resource);
343
+ #startValidation(waiter, record) {
344
+ for (const [owned, value] of this.#resources) {
345
+ if (owned !== record) continue;
346
+ if (this.#validate === void 0) {
347
+ this.#validating.delete(record);
348
+ this.#prepare(waiter, record, value);
349
+ return;
350
+ }
351
+ const operation = Promise.resolve().then(() => this.#validateResource(waiter, record, value));
352
+ this.#operations.add(operation);
353
+ operation.then(() => this.#completeOperation(operation), (error) => this.#failOperation(operation, waiter, error, "invalid"));
180
354
  return;
181
355
  }
182
- this.#release(resource);
183
- let replacement;
356
+ this.#validating.delete(record);
357
+ this.#assigned.delete(waiter);
358
+ this.#repump = true;
359
+ }
360
+ async #createResource(waiter) {
361
+ let value;
184
362
  try {
185
- replacement = await this.#create();
363
+ value = await this.#create();
186
364
  } catch (error) {
187
- this.#active -= 1;
188
- const waiter = this.#waiters.shift();
189
- waiter?.clear();
190
- waiter?.reject(error instanceof Error ? error : new Error(String(error)));
365
+ this.#reservations.delete(waiter);
366
+ if (this.#waiters.includes(waiter)) this.#ready.set(waiter, {
367
+ success: false,
368
+ error: new PoolError({
369
+ code: "create",
370
+ cause: error
371
+ })
372
+ });
373
+ else this.#assigned.delete(waiter);
374
+ this.#commit();
191
375
  return;
192
376
  }
377
+ this.#reservations.delete(waiter);
378
+ const record = {};
379
+ this.#resources.set(record, value);
380
+ this.#readyRecords.add(record);
193
381
  this.#emitter.emit("create");
194
- this.#serve(replacement);
195
- }
196
- #serve(resource) {
197
- const waiter = this.#destroyed ? void 0 : this.#waiters.shift();
198
- if (waiter !== void 0) {
199
- waiter.clear();
200
- waiter.resolve(this.#token(resource));
201
- this.#emitter.emit("acquire");
382
+ if (this.#ending !== void 0) {
383
+ this.#readyRecords.delete(record);
384
+ this.#assigned.delete(waiter);
385
+ try {
386
+ await this.#dispose(record);
387
+ } catch {}
202
388
  return;
203
389
  }
204
- this.#active -= 1;
205
- if (this.#destroyed) {
206
- this.#release(resource);
390
+ if (!this.#waiters.includes(waiter)) {
391
+ this.#readyRecords.delete(record);
392
+ this.#assigned.delete(waiter);
393
+ this.#recycle(record);
207
394
  return;
208
395
  }
209
- this.#idle.push(resource);
210
- this.#emitter.emit("release");
396
+ this.#ready.set(waiter, {
397
+ success: true,
398
+ record,
399
+ token: this.#token(record, value)
400
+ });
401
+ this.#commit();
211
402
  }
212
- async #valid(resource) {
213
- if (this.#validate === void 0) return true;
403
+ async #validateResource(waiter, record, value) {
404
+ let valid = false;
214
405
  try {
215
- return await this.#validate(resource);
216
- } catch {
217
- return false;
406
+ valid = await this.#validate?.(value) === true;
407
+ } catch {}
408
+ this.#validating.delete(record);
409
+ if (this.#ending !== void 0) {
410
+ this.#assigned.delete(waiter);
411
+ try {
412
+ await this.#dispose(record);
413
+ } catch {}
414
+ return;
218
415
  }
416
+ if (!this.#waiters.includes(waiter)) {
417
+ this.#assigned.delete(waiter);
418
+ if (valid) this.#recycle(record);
419
+ else try {
420
+ await this.#dispose(record);
421
+ } catch (error) {
422
+ this.#record(error);
423
+ }
424
+ return;
425
+ }
426
+ if (valid) {
427
+ this.#prepare(waiter, record, value);
428
+ return;
429
+ }
430
+ try {
431
+ await this.#dispose(record);
432
+ } catch (error) {
433
+ if (this.#waiters.includes(waiter)) {
434
+ this.#ready.set(waiter, {
435
+ success: false,
436
+ error: new PoolError({
437
+ code: "cleanup",
438
+ cause: error
439
+ })
440
+ });
441
+ this.#commit();
442
+ return;
443
+ }
444
+ this.#record(error);
445
+ }
446
+ this.#assigned.delete(waiter);
447
+ this.#pump();
448
+ }
449
+ #prepare(waiter, record, value) {
450
+ if (!this.#waiters.includes(waiter) || this.#ending !== void 0) {
451
+ this.#assigned.delete(waiter);
452
+ this.#recycle(record);
453
+ return;
454
+ }
455
+ this.#readyRecords.add(record);
456
+ this.#ready.set(waiter, {
457
+ success: true,
458
+ record,
459
+ token: this.#token(record, value)
460
+ });
461
+ this.#commit();
462
+ }
463
+ #commit() {
464
+ while (this.#ending === void 0) {
465
+ const waiter = this.#waiters[0];
466
+ if (waiter === void 0) return;
467
+ const result = this.#ready.get(waiter);
468
+ if (result === void 0) return;
469
+ this.#waiters.shift();
470
+ this.#ready.delete(waiter);
471
+ this.#assigned.delete(waiter);
472
+ this.#detach(waiter);
473
+ if (!result.success) {
474
+ waiter.reject(result.error);
475
+ continue;
476
+ }
477
+ this.#readyRecords.delete(result.record);
478
+ this.#leased.add(result.record);
479
+ waiter.resolve(result.token);
480
+ this.#emitter.emit("acquire");
481
+ }
482
+ }
483
+ #completeOperation(operation) {
484
+ this.#operations.delete(operation);
485
+ this.#commit();
486
+ this.#pump();
487
+ this.#finish();
488
+ }
489
+ #failOperation(operation, waiter, error, code) {
490
+ this.#operations.delete(operation);
491
+ this.#reservations.delete(waiter);
492
+ this.#assigned.delete(waiter);
493
+ if (this.#waiters.includes(waiter)) this.#ready.set(waiter, {
494
+ success: false,
495
+ error: new PoolError({
496
+ code,
497
+ cause: error
498
+ })
499
+ });
500
+ this.#commit();
501
+ this.#pump();
502
+ this.#finish();
503
+ }
504
+ #release(record) {
505
+ if (!this.#leased.delete(record)) return;
506
+ if (this.#ending !== void 0) {
507
+ this.#own(this.#dispose(record));
508
+ return;
509
+ }
510
+ this.#available.push(record);
511
+ this.#pump();
512
+ if (this.#available.includes(record)) this.#emitter.emit("release");
219
513
  }
220
- async #release(resource) {
221
- if (this.#destroy === void 0) {
222
- this.#emitter.emit("destroy");
514
+ #recycle(record) {
515
+ if (this.#ending !== void 0) {
516
+ this.#own(this.#dispose(record));
223
517
  return;
224
518
  }
519
+ this.#available.push(record);
520
+ this.#pump();
521
+ if (this.#available.includes(record)) this.#emitter.emit("release");
522
+ }
523
+ #dispose(record) {
524
+ const existing = this.#destroying.get(record);
525
+ if (existing !== void 0) return existing;
526
+ for (const [owned, value] of this.#resources) {
527
+ if (owned !== record) continue;
528
+ const cleanup = Promise.withResolvers();
529
+ this.#destroying.set(record, cleanup.promise);
530
+ const index = this.#available.indexOf(record);
531
+ if (index >= 0) this.#available.splice(index, 1);
532
+ this.#validating.delete(record);
533
+ this.#leased.delete(record);
534
+ this.#readyRecords.delete(record);
535
+ if (this.#ending !== void 0) this.#own(cleanup.promise);
536
+ this.#clean(record, value, cleanup);
537
+ return cleanup.promise;
538
+ }
539
+ return Promise.resolve();
540
+ }
541
+ async #clean(record, value, cleanup) {
542
+ let failure;
543
+ let failed = false;
225
544
  try {
226
- await this.#destroy(resource);
227
- } catch {}
545
+ await this.#cleanup?.(value);
546
+ } catch (error) {
547
+ failure = error;
548
+ failed = true;
549
+ }
550
+ this.#resources.delete(record);
228
551
  this.#emitter.emit("destroy");
552
+ this.#destroying.delete(record);
553
+ this.#pump();
554
+ if (failed) cleanup.reject(failure);
555
+ else cleanup.resolve();
556
+ this.#finish();
557
+ }
558
+ async #settleClear(cleanups) {
559
+ const settled = await Promise.allSettled(cleanups);
560
+ const failures = [];
561
+ for (const result of settled) if (result.status === "rejected" && !failures.some((failure) => Object.is(failure, result.reason))) failures.push(result.reason);
562
+ if (failures.length > 0) throw this.#cleanupError(failures);
563
+ }
564
+ #own(cleanup) {
565
+ if (this.#owned.has(cleanup)) return;
566
+ this.#owned.add(cleanup);
567
+ cleanup.then(() => this.#completeCleanup(cleanup), (error) => this.#failCleanup(cleanup, error));
568
+ }
569
+ #completeCleanup(cleanup) {
570
+ this.#owned.delete(cleanup);
571
+ this.#finish();
572
+ }
573
+ #failCleanup(cleanup, error) {
574
+ this.#owned.delete(cleanup);
575
+ this.#record(error);
576
+ this.#finish();
577
+ }
578
+ #record(error) {
579
+ if (!this.#failures.some((failure) => Object.is(failure, error))) this.#failures.push(error);
580
+ }
581
+ #cleanupError(failures) {
582
+ return new PoolError({
583
+ code: "cleanup",
584
+ ...failures[0] === void 0 ? {} : { cause: failures[0] },
585
+ context: { failures: [...failures] }
586
+ });
587
+ }
588
+ #finish() {
589
+ const ending = this.#ending;
590
+ if (ending === void 0 || this.#operations.size > 0 || this.#owned.size > 0) return;
591
+ let claimed = false;
592
+ for (const record of this.#resources.keys()) {
593
+ if (this.#validating.has(record) || this.#destroying.has(record)) continue;
594
+ claimed = true;
595
+ this.#own(this.#dispose(record));
596
+ }
597
+ if (claimed || this.#resources.size > 0 || this.#destroying.size > 0) return;
598
+ this.#emitter.destroy();
599
+ if (this.#failures.length > 0) ending.reject(this.#cleanupError(this.#failures));
600
+ else ending.resolve();
229
601
  }
230
602
  };
231
603
  //#endregion
232
604
  //#region src/core/factories.ts
233
605
  /**
234
- * Create a bounded resource pool with idle reuse and FIFO waiting `acquire` leases a
235
- * resource (reusing a validated idle one, growing up to `max`, or parking until a
236
- * `release` frees one) and the returned token's `release` returns it for reuse.
606
+ * Create a resource pool with optional bounded capacity, unique ownership, and FIFO settlement.
237
607
  *
238
608
  * @remarks
239
- * A parked `acquire` given an `AbortSignal` rejects + de-queues itself when the signal
240
- * fires (no leaked waiter). `clear` destroys idle resources (leased ones keep running);
241
- * `destroy` destroys all and rejects waiters. The pool is lean no warm-floor (`min`),
242
- * no eviction timers — and observable (§13): a typed `emitter` surfaces
243
- * `create` / `acquire` / `release` / `destroy`.
609
+ * Concurrent create and validation hooks may overlap, while acquire promises settle in
610
+ * request order. `clear` owns its idle snapshot; `destroy` returns one stable barrier and
611
+ * waits for every in-flight hook and cleanup before destroying the emitter last.
244
612
  *
245
613
  * @typeParam T - The pooled resource type
246
- * @param options - The `create` hook plus optional `destroy` / `validate` / `max`
614
+ * @param options - Lifecycle hooks, optional positive safe `max`, and observation hooks
247
615
  * @returns A working {@link PoolInterface}
248
616
  *
249
617
  * @example
250
618
  * ```ts
251
- * import { createPool } from '@src/core'
619
+ * import { createPool } from '@orkestrel/pool'
252
620
  *
253
621
  * const pool = createPool<Connection>({
254
622
  * create: () => connect(),
@@ -269,6 +637,6 @@ function createPool(options) {
269
637
  return new Pool(options);
270
638
  }
271
639
  //#endregion
272
- export { Pool, createPool };
640
+ export { Pool, PoolError, createPool, isPoolError, isPoolMax, isPoolSignal };
273
641
 
274
642
  //# sourceMappingURL=index.js.map