@orkestrel/pool 0.0.14 → 0.0.16

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.
@@ -4,7 +4,7 @@ import type { EmitterInterface } from '@orkestrel/emitter';
4
4
 
5
5
  /**
6
6
  * Creates a distinct {@link PoolInterface} from resource lifecycle hooks, with optional bounded
7
- * capacity, unique ownership, and FIFO settlement.
7
+ * capacity, exclusive leases by default, and FIFO settlement.
8
8
  *
9
9
  * @remarks
10
10
  * Concurrent create and validation hooks may overlap, while acquire promises settle in
@@ -57,18 +57,18 @@ import type { EmitterInterface } from '@orkestrel/emitter';
57
57
  export declare function isPoolError(value: unknown): value is PoolError;
58
58
 
59
59
  /**
60
- * Tests whether a value is a positive safe integer, the only valid explicit pool maximum.
60
+ * Tests whether a value is a positive safe integer for a pool record or lease limit.
61
61
  *
62
- * @param value - The unknown maximum candidate
62
+ * @param value - The unknown record or lease limit candidate
63
63
  * @returns True if the value is a positive safe integer; false otherwise
64
64
  *
65
65
  * @example
66
66
  * ```ts
67
- * isPoolMax(8) // true
68
- * isPoolMax(Infinity) // false
67
+ * isPoolLimit(8) // true
68
+ * isPoolLimit(Infinity) // false
69
69
  * ```
70
70
  */
71
- export declare function isPoolMax(value: unknown): value is number;
71
+ export declare function isPoolLimit(value: unknown): value is number;
72
72
 
73
73
  /**
74
74
  * Tests whether a value is a native `AbortSignal` for the acquire boundary, returning `false`
@@ -112,8 +112,8 @@ import type { EmitterInterface } from '@orkestrel/emitter';
112
112
  * Constructs a pool and synchronously validates its capacity contract.
113
113
  *
114
114
  * @param options - Resource hooks, observation hooks, capacity, and optional bounded warm floor
115
- * @throws {@link PoolError} Thrown with `code: 'invalid'` when `min` or `max` is not a positive
116
- * safe integer, their explicit values differ, `restarts` is absent with `min`, present without
115
+ * @throws {@link PoolError} Thrown with `code: 'invalid'` when `capacity`, `min`, or `max` is not a positive
116
+ * safe integer, explicit `min` and `max` differ, `restarts` is absent with `min`, present without
117
117
  * `min`, or not a non-negative safe integer, or `watch` is not callable.
118
118
  */
119
119
  constructor(options: PoolOptions<T>);
@@ -121,9 +121,9 @@ import type { EmitterInterface } from '@orkestrel/emitter';
121
121
  get emitter(): EmitterInterface<PoolEventMap>;
122
122
  /** Counts all owned records, including records validating, destroying, or retained after failed cleanup. */
123
123
  get size(): number;
124
- /** Counts the records immediately available without validation work. */
124
+ /** Counts available records with no live lease or pending handout. */
125
125
  get idle(): number;
126
- /** Counts the records represented by unsettled released-once lease tokens. */
126
+ /** Counts records with live leases, counting a shared record once rather than counting its leases. */
127
127
  get active(): number;
128
128
  /**
129
129
  * Fills the warm floor and resets the strikes of a spent floor; without a floor, resolves immediately.
@@ -135,8 +135,8 @@ import type { EmitterInterface } from '@orkestrel/emitter';
135
135
  */
136
136
  start(): Promise<void>;
137
137
  /**
138
- * Queues the caller in FIFO order, validates an idle record or waits for a floor refill, and
139
- * creates on demand only without a floor.
138
+ * Queues the caller in FIFO order, shares an occupied record or validates an idle record, and
139
+ * waits for a floor refill or creates on demand without a floor.
140
140
  *
141
141
  * @param signal - Optional native cancellation signal
142
142
  * @returns A promise for the unique resource lease
@@ -144,9 +144,9 @@ import type { EmitterInterface } from '@orkestrel/emitter';
144
144
  * with `code: 'invalid'`. This throw is synchronous rather than a rejected promise, so a caller
145
145
  * that handles failures with `.catch()` alone misses it.
146
146
  * @throws {@link PoolError} Thrown as a rejection when `destroy()` has already begun, with
147
- * `code: 'destroyed'`; when the create hook fails or the floor is spent without an idle record,
147
+ * `code: 'destroyed'`; when the create hook fails or the floor is spent without an eligible record,
148
148
  * with `code: 'create'` and the last cause; and when an invalid record's cleanup fails or
149
- * a floor retains a record, owns `min` records, has nothing idle, and has no refill or disposal
149
+ * a floor retains a record, owns `min` records, has nothing eligible, and has no refill or disposal
150
150
  * pending, even with live leases, with `code: 'cleanup'` and the retained cleanup failure as cause.
151
151
  * A `signal` that aborts rejects with the caller's exact `signal.reason` instead.
152
152
  */
@@ -252,9 +252,9 @@ import type { EmitterInterface } from '@orkestrel/emitter';
252
252
  readonly emitter: EmitterInterface<PoolEventMap>;
253
253
  /** Counts all owned records, including records validating, destroying, or retained after failed cleanup. */
254
254
  readonly size: number;
255
- /** Counts the records immediately available without validation work. */
255
+ /** Counts available records with no live lease or pending handout. */
256
256
  readonly idle: number;
257
- /** Counts the records represented by unsettled released-once lease tokens. */
257
+ /** Counts records with live leases, counting a shared record once rather than counting its leases. */
258
258
  readonly active: number;
259
259
  /**
260
260
  * Fills the warm floor and resets the strikes of a spent floor; without a floor, resolves immediately.
@@ -266,8 +266,8 @@ import type { EmitterInterface } from '@orkestrel/emitter';
266
266
  */
267
267
  start(): Promise<void>;
268
268
  /**
269
- * Queues the caller in FIFO order, validates an idle record or waits for a floor refill, and
270
- * creates on demand only without a floor.
269
+ * Queues the caller in FIFO order, shares an occupied record or validates an idle record, and
270
+ * waits for a floor refill or creates on demand without a floor.
271
271
  *
272
272
  * @param signal - Optional native cancellation signal
273
273
  * @returns A promise for the unique resource lease
@@ -275,9 +275,9 @@ import type { EmitterInterface } from '@orkestrel/emitter';
275
275
  * with `code: 'invalid'`. This throw is synchronous rather than a rejected promise, so a caller
276
276
  * that handles failures with `.catch()` alone misses it.
277
277
  * @throws {@link PoolError} Thrown as a rejection when `destroy()` has already begun, with
278
- * `code: 'destroyed'`; when the create hook fails or the floor is spent without an idle record,
278
+ * `code: 'destroyed'`; when the create hook fails or the floor is spent without an eligible record,
279
279
  * with `code: 'create'` and the last cause; and when an invalid record's cleanup fails or
280
- * a floor retains a record, owns `min` records, has nothing idle, and has no refill or disposal
280
+ * a floor retains a record, owns `min` records, has nothing eligible, and has no refill or disposal
281
281
  * pending, even with live leases, with `code: 'cleanup'` and the retained cleanup failure as cause.
282
282
  * A `signal` that aborts rejects with the caller's exact `signal.reason` instead.
283
283
  */
@@ -309,13 +309,25 @@ import type { EmitterInterface } from '@orkestrel/emitter';
309
309
  *
310
310
  * @remarks
311
311
  * `create` produces resources on demand, or only to restore `min` after `start()`.
312
- * `destroy` tears down a claimed resource. `validate` checks an owned resource before reuse.
312
+ * `destroy` tears down a claimed resource. `validate` checks only idle records before reuse;
313
+ * additional leases on occupied records skip validation, so validation cannot dispose co-holders.
314
+ * With `capacity` greater than 1, an occupied record is never revalidated. Only `watch` detects
315
+ * its loss: without `watch`, a dead record can keep receiving leases until it becomes idle;
316
+ * with `watch`, that exposure lasts until the watch settles.
317
+ * `capacity` bounds simultaneous leases per record, including pending handouts, and is a positive
318
+ * safe integer. Default: 1. Selection prefers the least occupied eligible record, with idle ties
319
+ * in release order and occupied ties in creation order. Reservations precede awaited hooks;
320
+ * waiter settlement stays FIFO.
313
321
  * `min` and `max` are positive safe integers and must be equal when both are set; `max`
314
- * defaults to `min`. Omitting both leaves capacity unbounded. `restarts` is required with
322
+ * defaults to `min`. Omitting both leaves the record count unbounded. `restarts` is required with
315
323
  * `min`, is refused without `min`, has no default, and is a non-negative safe integer.
316
- * A failed refill or loss of a never-leased record adds a strike; a lease grant resets the
317
- * strikes. Exceeding `restarts` stops refills until `start()`, except for one attempt owed
318
- * to each lost leased record.
324
+ * A failed refill or loss of a record with no live lease adds a strike, even after prior use.
325
+ * A lease grant resets the strikes only when its record was created after the last strike;
326
+ * creation alone does not.
327
+ * Exceeding `restarts` stops ordinary refills until `start()` or a qualifying grant.
328
+ * Loss or one lease's `destroy()` invalidates every lease on that record with one disposal.
329
+ * Each lost leased record earns one refill attempt after successful disposal, even when
330
+ * the bound is spent. That credit does not reset strikes; a failed attempt adds a strike.
319
331
  * `watch` settles on loss and receives a signal aborted when disposal begins. Its rejection
320
332
  * reaches `error` with event `watch` only while the record is live. Any settlement after
321
333
  * the signal aborts is ignored. `on` installs initial emitter listeners; `error`
@@ -328,6 +340,7 @@ import type { EmitterInterface } from '@orkestrel/emitter';
328
340
  readonly destroy?: (value: T) => Promise<void> | void;
329
341
  readonly validate?: (value: T) => Promise<boolean> | boolean;
330
342
  readonly watch?: (value: T, signal: AbortSignal) => Promise<unknown>;
343
+ readonly capacity?: number;
331
344
  readonly max?: number;
332
345
  readonly min?: number;
333
346
  readonly restarts?: number;
@@ -341,13 +354,13 @@ import type { EmitterInterface } from '@orkestrel/emitter';
341
354
  /** Holds the leased value. Duplicate values still belong to independent records. */
342
355
  readonly value: T;
343
356
  /**
344
- * Gives this exact record back to the pool once; a repeat call, and a call after loss or teardown
345
- * took ownership, are no-ops.
357
+ * Ends this exact lease once; a repeat call, and a call after loss or teardown took ownership,
358
+ * are no-ops. Other leases on the record remain live.
346
359
  */
347
360
  release(): void;
348
361
  /**
349
- * Destroys this exact record instead of returning it; a repeat call, and a call after release,
350
- * are no-ops.
362
+ * Destroys this exact record for every co-holder and invalidates their leases; a repeat call,
363
+ * and a call after release, are no-ops.
351
364
  *
352
365
  * @returns A promise for this record's cleanup attempt, including an attempt already in progress
353
366
  * @throws {@link PoolError} Thrown as a rejection with `code: 'cleanup'` when disposal fails.
@@ -4,7 +4,7 @@ import type { EmitterInterface } from '@orkestrel/emitter';
4
4
 
5
5
  /**
6
6
  * Creates a distinct {@link PoolInterface} from resource lifecycle hooks, with optional bounded
7
- * capacity, unique ownership, and FIFO settlement.
7
+ * capacity, exclusive leases by default, and FIFO settlement.
8
8
  *
9
9
  * @remarks
10
10
  * Concurrent create and validation hooks may overlap, while acquire promises settle in
@@ -57,18 +57,18 @@ import type { EmitterInterface } from '@orkestrel/emitter';
57
57
  export declare function isPoolError(value: unknown): value is PoolError;
58
58
 
59
59
  /**
60
- * Tests whether a value is a positive safe integer, the only valid explicit pool maximum.
60
+ * Tests whether a value is a positive safe integer for a pool record or lease limit.
61
61
  *
62
- * @param value - The unknown maximum candidate
62
+ * @param value - The unknown record or lease limit candidate
63
63
  * @returns True if the value is a positive safe integer; false otherwise
64
64
  *
65
65
  * @example
66
66
  * ```ts
67
- * isPoolMax(8) // true
68
- * isPoolMax(Infinity) // false
67
+ * isPoolLimit(8) // true
68
+ * isPoolLimit(Infinity) // false
69
69
  * ```
70
70
  */
71
- export declare function isPoolMax(value: unknown): value is number;
71
+ export declare function isPoolLimit(value: unknown): value is number;
72
72
 
73
73
  /**
74
74
  * Tests whether a value is a native `AbortSignal` for the acquire boundary, returning `false`
@@ -112,8 +112,8 @@ import type { EmitterInterface } from '@orkestrel/emitter';
112
112
  * Constructs a pool and synchronously validates its capacity contract.
113
113
  *
114
114
  * @param options - Resource hooks, observation hooks, capacity, and optional bounded warm floor
115
- * @throws {@link PoolError} Thrown with `code: 'invalid'` when `min` or `max` is not a positive
116
- * safe integer, their explicit values differ, `restarts` is absent with `min`, present without
115
+ * @throws {@link PoolError} Thrown with `code: 'invalid'` when `capacity`, `min`, or `max` is not a positive
116
+ * safe integer, explicit `min` and `max` differ, `restarts` is absent with `min`, present without
117
117
  * `min`, or not a non-negative safe integer, or `watch` is not callable.
118
118
  */
119
119
  constructor(options: PoolOptions<T>);
@@ -121,9 +121,9 @@ import type { EmitterInterface } from '@orkestrel/emitter';
121
121
  get emitter(): EmitterInterface<PoolEventMap>;
122
122
  /** Counts all owned records, including records validating, destroying, or retained after failed cleanup. */
123
123
  get size(): number;
124
- /** Counts the records immediately available without validation work. */
124
+ /** Counts available records with no live lease or pending handout. */
125
125
  get idle(): number;
126
- /** Counts the records represented by unsettled released-once lease tokens. */
126
+ /** Counts records with live leases, counting a shared record once rather than counting its leases. */
127
127
  get active(): number;
128
128
  /**
129
129
  * Fills the warm floor and resets the strikes of a spent floor; without a floor, resolves immediately.
@@ -135,8 +135,8 @@ import type { EmitterInterface } from '@orkestrel/emitter';
135
135
  */
136
136
  start(): Promise<void>;
137
137
  /**
138
- * Queues the caller in FIFO order, validates an idle record or waits for a floor refill, and
139
- * creates on demand only without a floor.
138
+ * Queues the caller in FIFO order, shares an occupied record or validates an idle record, and
139
+ * waits for a floor refill or creates on demand without a floor.
140
140
  *
141
141
  * @param signal - Optional native cancellation signal
142
142
  * @returns A promise for the unique resource lease
@@ -144,9 +144,9 @@ import type { EmitterInterface } from '@orkestrel/emitter';
144
144
  * with `code: 'invalid'`. This throw is synchronous rather than a rejected promise, so a caller
145
145
  * that handles failures with `.catch()` alone misses it.
146
146
  * @throws {@link PoolError} Thrown as a rejection when `destroy()` has already begun, with
147
- * `code: 'destroyed'`; when the create hook fails or the floor is spent without an idle record,
147
+ * `code: 'destroyed'`; when the create hook fails or the floor is spent without an eligible record,
148
148
  * with `code: 'create'` and the last cause; and when an invalid record's cleanup fails or
149
- * a floor retains a record, owns `min` records, has nothing idle, and has no refill or disposal
149
+ * a floor retains a record, owns `min` records, has nothing eligible, and has no refill or disposal
150
150
  * pending, even with live leases, with `code: 'cleanup'` and the retained cleanup failure as cause.
151
151
  * A `signal` that aborts rejects with the caller's exact `signal.reason` instead.
152
152
  */
@@ -252,9 +252,9 @@ import type { EmitterInterface } from '@orkestrel/emitter';
252
252
  readonly emitter: EmitterInterface<PoolEventMap>;
253
253
  /** Counts all owned records, including records validating, destroying, or retained after failed cleanup. */
254
254
  readonly size: number;
255
- /** Counts the records immediately available without validation work. */
255
+ /** Counts available records with no live lease or pending handout. */
256
256
  readonly idle: number;
257
- /** Counts the records represented by unsettled released-once lease tokens. */
257
+ /** Counts records with live leases, counting a shared record once rather than counting its leases. */
258
258
  readonly active: number;
259
259
  /**
260
260
  * Fills the warm floor and resets the strikes of a spent floor; without a floor, resolves immediately.
@@ -266,8 +266,8 @@ import type { EmitterInterface } from '@orkestrel/emitter';
266
266
  */
267
267
  start(): Promise<void>;
268
268
  /**
269
- * Queues the caller in FIFO order, validates an idle record or waits for a floor refill, and
270
- * creates on demand only without a floor.
269
+ * Queues the caller in FIFO order, shares an occupied record or validates an idle record, and
270
+ * waits for a floor refill or creates on demand without a floor.
271
271
  *
272
272
  * @param signal - Optional native cancellation signal
273
273
  * @returns A promise for the unique resource lease
@@ -275,9 +275,9 @@ import type { EmitterInterface } from '@orkestrel/emitter';
275
275
  * with `code: 'invalid'`. This throw is synchronous rather than a rejected promise, so a caller
276
276
  * that handles failures with `.catch()` alone misses it.
277
277
  * @throws {@link PoolError} Thrown as a rejection when `destroy()` has already begun, with
278
- * `code: 'destroyed'`; when the create hook fails or the floor is spent without an idle record,
278
+ * `code: 'destroyed'`; when the create hook fails or the floor is spent without an eligible record,
279
279
  * with `code: 'create'` and the last cause; and when an invalid record's cleanup fails or
280
- * a floor retains a record, owns `min` records, has nothing idle, and has no refill or disposal
280
+ * a floor retains a record, owns `min` records, has nothing eligible, and has no refill or disposal
281
281
  * pending, even with live leases, with `code: 'cleanup'` and the retained cleanup failure as cause.
282
282
  * A `signal` that aborts rejects with the caller's exact `signal.reason` instead.
283
283
  */
@@ -309,13 +309,25 @@ import type { EmitterInterface } from '@orkestrel/emitter';
309
309
  *
310
310
  * @remarks
311
311
  * `create` produces resources on demand, or only to restore `min` after `start()`.
312
- * `destroy` tears down a claimed resource. `validate` checks an owned resource before reuse.
312
+ * `destroy` tears down a claimed resource. `validate` checks only idle records before reuse;
313
+ * additional leases on occupied records skip validation, so validation cannot dispose co-holders.
314
+ * With `capacity` greater than 1, an occupied record is never revalidated. Only `watch` detects
315
+ * its loss: without `watch`, a dead record can keep receiving leases until it becomes idle;
316
+ * with `watch`, that exposure lasts until the watch settles.
317
+ * `capacity` bounds simultaneous leases per record, including pending handouts, and is a positive
318
+ * safe integer. Default: 1. Selection prefers the least occupied eligible record, with idle ties
319
+ * in release order and occupied ties in creation order. Reservations precede awaited hooks;
320
+ * waiter settlement stays FIFO.
313
321
  * `min` and `max` are positive safe integers and must be equal when both are set; `max`
314
- * defaults to `min`. Omitting both leaves capacity unbounded. `restarts` is required with
322
+ * defaults to `min`. Omitting both leaves the record count unbounded. `restarts` is required with
315
323
  * `min`, is refused without `min`, has no default, and is a non-negative safe integer.
316
- * A failed refill or loss of a never-leased record adds a strike; a lease grant resets the
317
- * strikes. Exceeding `restarts` stops refills until `start()`, except for one attempt owed
318
- * to each lost leased record.
324
+ * A failed refill or loss of a record with no live lease adds a strike, even after prior use.
325
+ * A lease grant resets the strikes only when its record was created after the last strike;
326
+ * creation alone does not.
327
+ * Exceeding `restarts` stops ordinary refills until `start()` or a qualifying grant.
328
+ * Loss or one lease's `destroy()` invalidates every lease on that record with one disposal.
329
+ * Each lost leased record earns one refill attempt after successful disposal, even when
330
+ * the bound is spent. That credit does not reset strikes; a failed attempt adds a strike.
319
331
  * `watch` settles on loss and receives a signal aborted when disposal begins. Its rejection
320
332
  * reaches `error` with event `watch` only while the record is live. Any settlement after
321
333
  * the signal aborts is ignored. `on` installs initial emitter listeners; `error`
@@ -328,6 +340,7 @@ import type { EmitterInterface } from '@orkestrel/emitter';
328
340
  readonly destroy?: (value: T) => Promise<void> | void;
329
341
  readonly validate?: (value: T) => Promise<boolean> | boolean;
330
342
  readonly watch?: (value: T, signal: AbortSignal) => Promise<unknown>;
343
+ readonly capacity?: number;
331
344
  readonly max?: number;
332
345
  readonly min?: number;
333
346
  readonly restarts?: number;
@@ -341,13 +354,13 @@ import type { EmitterInterface } from '@orkestrel/emitter';
341
354
  /** Holds the leased value. Duplicate values still belong to independent records. */
342
355
  readonly value: T;
343
356
  /**
344
- * Gives this exact record back to the pool once; a repeat call, and a call after loss or teardown
345
- * took ownership, are no-ops.
357
+ * Ends this exact lease once; a repeat call, and a call after loss or teardown took ownership,
358
+ * are no-ops. Other leases on the record remain live.
346
359
  */
347
360
  release(): void;
348
361
  /**
349
- * Destroys this exact record instead of returning it; a repeat call, and a call after release,
350
- * are no-ops.
362
+ * Destroys this exact record for every co-holder and invalidates their leases; a repeat call,
363
+ * and a call after release, are no-ops.
351
364
  *
352
365
  * @returns A promise for this record's cleanup attempt, including an attempt already in progress
353
366
  * @throws {@link PoolError} Thrown as a rejection with `code: 'cleanup'` when disposal fails.
@@ -56,18 +56,18 @@ function isPoolError(value) {
56
56
  //#endregion
57
57
  //#region src/core/validators.ts
58
58
  /**
59
- * Tests whether a value is a positive safe integer, the only valid explicit pool maximum.
59
+ * Tests whether a value is a positive safe integer for a pool record or lease limit.
60
60
  *
61
- * @param value - The unknown maximum candidate
61
+ * @param value - The unknown record or lease limit candidate
62
62
  * @returns True if the value is a positive safe integer; false otherwise
63
63
  *
64
64
  * @example
65
65
  * ```ts
66
- * isPoolMax(8) // true
67
- * isPoolMax(Infinity) // false
66
+ * isPoolLimit(8) // true
67
+ * isPoolLimit(Infinity) // false
68
68
  * ```
69
69
  */
70
- function isPoolMax(value) {
70
+ function isPoolLimit(value) {
71
71
  return isNumber(value) && Number.isSafeInteger(value) && value > 0;
72
72
  }
73
73
  /**
@@ -119,12 +119,13 @@ var Pool = class {
119
119
  #destroy;
120
120
  #validate;
121
121
  #max;
122
+ #capacity;
122
123
  #min;
123
124
  #restarts;
124
125
  #watch;
125
126
  #error;
126
127
  #watches = /* @__PURE__ */ new Map();
127
- #used = /* @__PURE__ */ new WeakSet();
128
+ #births = /* @__PURE__ */ new WeakMap();
128
129
  #settled = /* @__PURE__ */ new WeakSet();
129
130
  #survivors = /* @__PURE__ */ new Map();
130
131
  #owing = /* @__PURE__ */ new Set();
@@ -132,7 +133,7 @@ var Pool = class {
132
133
  #resources = /* @__PURE__ */ new Map();
133
134
  #available = [];
134
135
  #validating = /* @__PURE__ */ new Set();
135
- #leased = /* @__PURE__ */ new Set();
136
+ #leased = /* @__PURE__ */ new Map();
136
137
  #destroying = /* @__PURE__ */ new Map();
137
138
  #waiters = [];
138
139
  #assigned = /* @__PURE__ */ new Set();
@@ -149,26 +150,32 @@ var Pool = class {
149
150
  #starting;
150
151
  #refilling = false;
151
152
  #strikes = 0;
153
+ #epoch = {};
152
154
  #owed = 0;
153
155
  #cause;
154
156
  /**
155
157
  * Constructs a pool and synchronously validates its capacity contract.
156
158
  *
157
159
  * @param options - Resource hooks, observation hooks, capacity, and optional bounded warm floor
158
- * @throws {@link PoolError} Thrown with `code: 'invalid'` when `min` or `max` is not a positive
159
- * safe integer, their explicit values differ, `restarts` is absent with `min`, present without
160
+ * @throws {@link PoolError} Thrown with `code: 'invalid'` when `capacity`, `min`, or `max` is not a positive
161
+ * safe integer, explicit `min` and `max` differ, `restarts` is absent with `min`, present without
160
162
  * `min`, or not a non-negative safe integer, or `watch` is not callable.
161
163
  */
162
164
  constructor(options) {
163
165
  const max = options.max;
166
+ const capacity = options.capacity;
164
167
  const min = options.min;
165
168
  const restarts = options.restarts;
166
169
  const watch = options.watch;
167
- if (max !== void 0 && !isPoolMax(max)) throw new PoolError({
170
+ if (max !== void 0 && !isPoolLimit(max)) throw new PoolError({
168
171
  code: "invalid",
169
172
  context: { value: max }
170
173
  });
171
- if (min !== void 0 && (!isPoolMax(min) || max !== void 0 && min !== max)) throw new PoolError({
174
+ if (capacity !== void 0 && !isPoolLimit(capacity)) throw new PoolError({
175
+ code: "invalid",
176
+ context: { value: capacity }
177
+ });
178
+ if (min !== void 0 && (!isPoolLimit(min) || max !== void 0 && min !== max)) throw new PoolError({
172
179
  code: "invalid",
173
180
  context: { value: min }
174
181
  });
@@ -186,6 +193,7 @@ var Pool = class {
186
193
  this.#destroy = options.destroy;
187
194
  this.#validate = options.validate;
188
195
  this.#max = max ?? min;
196
+ this.#capacity = capacity ?? 1;
189
197
  this.#min = min;
190
198
  this.#restarts = restarts;
191
199
  this.#watch = watch;
@@ -203,11 +211,11 @@ var Pool = class {
203
211
  get size() {
204
212
  return this.#resources.size;
205
213
  }
206
- /** Counts the records immediately available without validation work. */
214
+ /** Counts available records with no live lease or pending handout. */
207
215
  get idle() {
208
216
  return this.#available.length;
209
217
  }
210
- /** Counts the records represented by unsettled released-once lease tokens. */
218
+ /** Counts records with live leases, counting a shared record once rather than counting its leases. */
211
219
  get active() {
212
220
  return this.#leased.size;
213
221
  }
@@ -231,8 +239,8 @@ var Pool = class {
231
239
  return starting.promise;
232
240
  }
233
241
  /**
234
- * Queues the caller in FIFO order, validates an idle record or waits for a floor refill, and
235
- * creates on demand only without a floor.
242
+ * Queues the caller in FIFO order, shares an occupied record or validates an idle record, and
243
+ * waits for a floor refill or creates on demand without a floor.
236
244
  *
237
245
  * @param signal - Optional native cancellation signal
238
246
  * @returns A promise for the unique resource lease
@@ -240,9 +248,9 @@ var Pool = class {
240
248
  * with `code: 'invalid'`. This throw is synchronous rather than a rejected promise, so a caller
241
249
  * that handles failures with `.catch()` alone misses it.
242
250
  * @throws {@link PoolError} Thrown as a rejection when `destroy()` has already begun, with
243
- * `code: 'destroyed'`; when the create hook fails or the floor is spent without an idle record,
251
+ * `code: 'destroyed'`; when the create hook fails or the floor is spent without an eligible record,
244
252
  * with `code: 'create'` and the last cause; and when an invalid record's cleanup fails or
245
- * a floor retains a record, owns `min` records, has nothing idle, and has no refill or disposal
253
+ * a floor retains a record, owns `min` records, has nothing eligible, and has no refill or disposal
246
254
  * pending, even with live leases, with `code: 'cleanup'` and the retained cleanup failure as cause.
247
255
  * A `signal` that aborts rejects with the caller's exact `signal.reason` instead.
248
256
  */
@@ -347,8 +355,7 @@ var Pool = class {
347
355
  });
348
356
  }
349
357
  }
350
- #token(record, value) {
351
- const lease = {};
358
+ #token(record, value, lease) {
352
359
  return {
353
360
  value,
354
361
  release: this.#createRelease(record, lease),
@@ -359,7 +366,7 @@ var Pool = class {
359
366
  return () => {
360
367
  if (this.#settled.has(lease)) return;
361
368
  this.#settled.add(lease);
362
- this.#release(record);
369
+ this.#release(record, lease);
363
370
  };
364
371
  }
365
372
  #createDestroy(record, lease) {
@@ -404,10 +411,12 @@ var Pool = class {
404
411
  this.#repump = false;
405
412
  for (const waiter of this.#waiters) {
406
413
  if (this.#assigned.has(waiter) || this.#ready.has(waiter)) continue;
407
- const record = this.#available.shift();
414
+ const record = this.#select();
408
415
  if (record !== void 0) {
409
416
  this.#assigned.add(waiter);
410
- this.#validating.add(record);
417
+ const index = this.#available.indexOf(record);
418
+ if (index >= 0) this.#available.splice(index, 1);
419
+ if (this.#occupancy(record) === 0) this.#validating.add(record);
411
420
  this.#startValidation(waiter, record);
412
421
  continue;
413
422
  }
@@ -448,6 +457,25 @@ var Pool = class {
448
457
  this.#pumping = false;
449
458
  this.#fill();
450
459
  }
460
+ #occupancy(record) {
461
+ let count = this.#leased.get(record)?.size ?? 0;
462
+ for (const result of this.#ready.values()) if (result.success && result.record === record) count += 1;
463
+ return count;
464
+ }
465
+ #select() {
466
+ let selected;
467
+ let least = this.#capacity;
468
+ for (const record of this.#resources.keys()) {
469
+ if (this.#validating.has(record) || this.#destroying.has(record) || this.#survivors.has(record)) continue;
470
+ const count = this.#occupancy(record);
471
+ if (count === 0 && !this.#available.includes(record)) continue;
472
+ if (count < least) {
473
+ selected = record;
474
+ least = count;
475
+ }
476
+ }
477
+ return this.#available[0] ?? selected;
478
+ }
451
479
  #spent() {
452
480
  return this.#restarts !== void 0 && this.#strikes > this.#restarts;
453
481
  }
@@ -484,8 +512,7 @@ var Pool = class {
484
512
  try {
485
513
  value = await this.#create();
486
514
  } catch (error) {
487
- this.#strikes += 1;
488
- this.#cause = error;
515
+ this.#addStrike(error);
489
516
  return;
490
517
  }
491
518
  const record = this.#insert(value);
@@ -493,6 +520,7 @@ var Pool = class {
493
520
  }
494
521
  #insert(value) {
495
522
  const record = {};
523
+ this.#births.set(record, this.#epoch);
496
524
  this.#resources.set(record, value);
497
525
  if (this.#watch !== void 0) {
498
526
  const controller = new AbortController();
@@ -520,10 +548,12 @@ var Pool = class {
520
548
  this.#pump();
521
549
  }
522
550
  #strike(record, cause) {
523
- if (this.#min !== void 0 && !this.#used.has(record)) {
524
- this.#strikes += 1;
525
- this.#cause = cause;
526
- }
551
+ if (this.#min !== void 0 && !this.#leased.has(record)) this.#addStrike(cause);
552
+ }
553
+ #addStrike(cause) {
554
+ this.#strikes += 1;
555
+ this.#cause = cause;
556
+ this.#epoch = {};
527
557
  }
528
558
  #startCreate(waiter) {
529
559
  const operation = Promise.resolve().then(() => this.#createResource(waiter));
@@ -533,7 +563,7 @@ var Pool = class {
533
563
  #startValidation(waiter, record) {
534
564
  for (const [owned, value] of this.#resources) {
535
565
  if (owned !== record) continue;
536
- if (this.#validate === void 0) {
566
+ if (!this.#validating.has(record) || this.#validate === void 0) {
537
567
  this.#validating.delete(record);
538
568
  this.#prepare(waiter, record, value);
539
569
  return;
@@ -581,7 +611,7 @@ var Pool = class {
581
611
  this.#ready.set(waiter, {
582
612
  success: true,
583
613
  record,
584
- token: this.#token(record, value)
614
+ token: this.#token(record, value, waiter)
585
615
  });
586
616
  this.#commit();
587
617
  }
@@ -649,7 +679,7 @@ var Pool = class {
649
679
  this.#ready.set(waiter, {
650
680
  success: true,
651
681
  record,
652
- token: this.#token(record, value)
682
+ token: this.#token(record, value, waiter)
653
683
  });
654
684
  this.#commit();
655
685
  }
@@ -674,9 +704,10 @@ var Pool = class {
674
704
  waiter.reject(result.error);
675
705
  continue;
676
706
  }
677
- this.#leased.add(result.record);
678
- this.#used.add(result.record);
679
- this.#strikes = 0;
707
+ const leases = this.#leased.get(result.record) ?? /* @__PURE__ */ new Set();
708
+ leases.add(waiter);
709
+ this.#leased.set(result.record, leases);
710
+ if (this.#births.get(result.record) === this.#epoch) this.#strikes = 0;
680
711
  waiter.resolve(result.token);
681
712
  this.#emitter.emit("acquire");
682
713
  }
@@ -702,8 +733,10 @@ var Pool = class {
702
733
  this.#pump();
703
734
  this.#finish();
704
735
  }
705
- #release(record) {
706
- if (!this.#leased.delete(record)) return;
736
+ #release(record, lease) {
737
+ const leases = this.#leased.get(record);
738
+ if (leases === void 0 || !leases.delete(lease)) return;
739
+ if (leases.size === 0) this.#leased.delete(record);
707
740
  this.#recycle(record);
708
741
  }
709
742
  #recycle(record) {
@@ -712,7 +745,7 @@ var Pool = class {
712
745
  this.#own(this.#dispose(record));
713
746
  return;
714
747
  }
715
- this.#available.push(record);
748
+ if (this.#occupancy(record) === 0 && !this.#validating.has(record) && !this.#available.includes(record)) this.#available.push(record);
716
749
  this.#pump();
717
750
  if (this.#available.includes(record)) this.#emitter.emit("release");
718
751
  }
@@ -816,7 +849,7 @@ var Pool = class {
816
849
  //#region src/core/factories.ts
817
850
  /**
818
851
  * Creates a distinct {@link PoolInterface} from resource lifecycle hooks, with optional bounded
819
- * capacity, unique ownership, and FIFO settlement.
852
+ * capacity, exclusive leases by default, and FIFO settlement.
820
853
  *
821
854
  * @remarks
822
855
  * Concurrent create and validation hooks may overlap, while acquire promises settle in
@@ -856,6 +889,6 @@ function createPool(options) {
856
889
  return new Pool(options);
857
890
  }
858
891
  //#endregion
859
- export { Pool, PoolError, createPool, isPoolError, isPoolMax, isPoolSignal };
892
+ export { Pool, PoolError, createPool, isPoolError, isPoolLimit, isPoolSignal };
860
893
 
861
894
  //# sourceMappingURL=index.js.map