@orkestrel/pool 0.0.4 → 0.0.6

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