@orkestrel/pool 0.0.15 → 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,23 @@ 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 only when its record was created after the last strike; creation alone does not.
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.
318
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.
319
329
  * Each lost leased record earns one refill attempt after successful disposal, even when
320
330
  * the bound is spent. That credit does not reset strikes; a failed attempt adds a strike.
321
331
  * `watch` settles on loss and receives a signal aborted when disposal begins. Its rejection
@@ -330,6 +340,7 @@ import type { EmitterInterface } from '@orkestrel/emitter';
330
340
  readonly destroy?: (value: T) => Promise<void> | void;
331
341
  readonly validate?: (value: T) => Promise<boolean> | boolean;
332
342
  readonly watch?: (value: T, signal: AbortSignal) => Promise<unknown>;
343
+ readonly capacity?: number;
333
344
  readonly max?: number;
334
345
  readonly min?: number;
335
346
  readonly restarts?: number;
@@ -343,13 +354,13 @@ import type { EmitterInterface } from '@orkestrel/emitter';
343
354
  /** Holds the leased value. Duplicate values still belong to independent records. */
344
355
  readonly value: T;
345
356
  /**
346
- * Gives this exact record back to the pool once; a repeat call, and a call after loss or teardown
347
- * 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.
348
359
  */
349
360
  release(): void;
350
361
  /**
351
- * Destroys this exact record instead of returning it; a repeat call, and a call after release,
352
- * 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.
353
364
  *
354
365
  * @returns A promise for this record's cleanup attempt, including an attempt already in progress
355
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,23 @@ 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 only when its record was created after the last strike; creation alone does not.
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.
318
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.
319
329
  * Each lost leased record earns one refill attempt after successful disposal, even when
320
330
  * the bound is spent. That credit does not reset strikes; a failed attempt adds a strike.
321
331
  * `watch` settles on loss and receives a signal aborted when disposal begins. Its rejection
@@ -330,6 +340,7 @@ import type { EmitterInterface } from '@orkestrel/emitter';
330
340
  readonly destroy?: (value: T) => Promise<void> | void;
331
341
  readonly validate?: (value: T) => Promise<boolean> | boolean;
332
342
  readonly watch?: (value: T, signal: AbortSignal) => Promise<unknown>;
343
+ readonly capacity?: number;
333
344
  readonly max?: number;
334
345
  readonly min?: number;
335
346
  readonly restarts?: number;
@@ -343,13 +354,13 @@ import type { EmitterInterface } from '@orkestrel/emitter';
343
354
  /** Holds the leased value. Duplicate values still belong to independent records. */
344
355
  readonly value: T;
345
356
  /**
346
- * Gives this exact record back to the pool once; a repeat call, and a call after loss or teardown
347
- * 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.
348
359
  */
349
360
  release(): void;
350
361
  /**
351
- * Destroys this exact record instead of returning it; a repeat call, and a call after release,
352
- * 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.
353
364
  *
354
365
  * @returns A promise for this record's cleanup attempt, including an attempt already in progress
355
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,12 @@ 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
128
  #births = /* @__PURE__ */ new WeakMap();
129
129
  #settled = /* @__PURE__ */ new WeakSet();
130
130
  #survivors = /* @__PURE__ */ new Map();
@@ -133,7 +133,7 @@ var Pool = class {
133
133
  #resources = /* @__PURE__ */ new Map();
134
134
  #available = [];
135
135
  #validating = /* @__PURE__ */ new Set();
136
- #leased = /* @__PURE__ */ new Set();
136
+ #leased = /* @__PURE__ */ new Map();
137
137
  #destroying = /* @__PURE__ */ new Map();
138
138
  #waiters = [];
139
139
  #assigned = /* @__PURE__ */ new Set();
@@ -157,20 +157,25 @@ var Pool = class {
157
157
  * Constructs a pool and synchronously validates its capacity contract.
158
158
  *
159
159
  * @param options - Resource hooks, observation hooks, capacity, and optional bounded warm floor
160
- * @throws {@link PoolError} Thrown with `code: 'invalid'` when `min` or `max` is not a positive
161
- * 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
162
162
  * `min`, or not a non-negative safe integer, or `watch` is not callable.
163
163
  */
164
164
  constructor(options) {
165
165
  const max = options.max;
166
+ const capacity = options.capacity;
166
167
  const min = options.min;
167
168
  const restarts = options.restarts;
168
169
  const watch = options.watch;
169
- if (max !== void 0 && !isPoolMax(max)) throw new PoolError({
170
+ if (max !== void 0 && !isPoolLimit(max)) throw new PoolError({
170
171
  code: "invalid",
171
172
  context: { value: max }
172
173
  });
173
- 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({
174
179
  code: "invalid",
175
180
  context: { value: min }
176
181
  });
@@ -188,6 +193,7 @@ var Pool = class {
188
193
  this.#destroy = options.destroy;
189
194
  this.#validate = options.validate;
190
195
  this.#max = max ?? min;
196
+ this.#capacity = capacity ?? 1;
191
197
  this.#min = min;
192
198
  this.#restarts = restarts;
193
199
  this.#watch = watch;
@@ -205,11 +211,11 @@ var Pool = class {
205
211
  get size() {
206
212
  return this.#resources.size;
207
213
  }
208
- /** Counts the records immediately available without validation work. */
214
+ /** Counts available records with no live lease or pending handout. */
209
215
  get idle() {
210
216
  return this.#available.length;
211
217
  }
212
- /** 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. */
213
219
  get active() {
214
220
  return this.#leased.size;
215
221
  }
@@ -233,8 +239,8 @@ var Pool = class {
233
239
  return starting.promise;
234
240
  }
235
241
  /**
236
- * Queues the caller in FIFO order, validates an idle record or waits for a floor refill, and
237
- * 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.
238
244
  *
239
245
  * @param signal - Optional native cancellation signal
240
246
  * @returns A promise for the unique resource lease
@@ -242,9 +248,9 @@ var Pool = class {
242
248
  * with `code: 'invalid'`. This throw is synchronous rather than a rejected promise, so a caller
243
249
  * that handles failures with `.catch()` alone misses it.
244
250
  * @throws {@link PoolError} Thrown as a rejection when `destroy()` has already begun, with
245
- * `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,
246
252
  * with `code: 'create'` and the last cause; and when an invalid record's cleanup fails or
247
- * 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
248
254
  * pending, even with live leases, with `code: 'cleanup'` and the retained cleanup failure as cause.
249
255
  * A `signal` that aborts rejects with the caller's exact `signal.reason` instead.
250
256
  */
@@ -349,8 +355,7 @@ var Pool = class {
349
355
  });
350
356
  }
351
357
  }
352
- #token(record, value) {
353
- const lease = {};
358
+ #token(record, value, lease) {
354
359
  return {
355
360
  value,
356
361
  release: this.#createRelease(record, lease),
@@ -361,7 +366,7 @@ var Pool = class {
361
366
  return () => {
362
367
  if (this.#settled.has(lease)) return;
363
368
  this.#settled.add(lease);
364
- this.#release(record);
369
+ this.#release(record, lease);
365
370
  };
366
371
  }
367
372
  #createDestroy(record, lease) {
@@ -406,10 +411,12 @@ var Pool = class {
406
411
  this.#repump = false;
407
412
  for (const waiter of this.#waiters) {
408
413
  if (this.#assigned.has(waiter) || this.#ready.has(waiter)) continue;
409
- const record = this.#available.shift();
414
+ const record = this.#select();
410
415
  if (record !== void 0) {
411
416
  this.#assigned.add(waiter);
412
- 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);
413
420
  this.#startValidation(waiter, record);
414
421
  continue;
415
422
  }
@@ -450,6 +457,25 @@ var Pool = class {
450
457
  this.#pumping = false;
451
458
  this.#fill();
452
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
+ }
453
479
  #spent() {
454
480
  return this.#restarts !== void 0 && this.#strikes > this.#restarts;
455
481
  }
@@ -522,7 +548,7 @@ var Pool = class {
522
548
  this.#pump();
523
549
  }
524
550
  #strike(record, cause) {
525
- if (this.#min !== void 0 && !this.#used.has(record)) this.#addStrike(cause);
551
+ if (this.#min !== void 0 && !this.#leased.has(record)) this.#addStrike(cause);
526
552
  }
527
553
  #addStrike(cause) {
528
554
  this.#strikes += 1;
@@ -537,7 +563,7 @@ var Pool = class {
537
563
  #startValidation(waiter, record) {
538
564
  for (const [owned, value] of this.#resources) {
539
565
  if (owned !== record) continue;
540
- if (this.#validate === void 0) {
566
+ if (!this.#validating.has(record) || this.#validate === void 0) {
541
567
  this.#validating.delete(record);
542
568
  this.#prepare(waiter, record, value);
543
569
  return;
@@ -585,7 +611,7 @@ var Pool = class {
585
611
  this.#ready.set(waiter, {
586
612
  success: true,
587
613
  record,
588
- token: this.#token(record, value)
614
+ token: this.#token(record, value, waiter)
589
615
  });
590
616
  this.#commit();
591
617
  }
@@ -653,7 +679,7 @@ var Pool = class {
653
679
  this.#ready.set(waiter, {
654
680
  success: true,
655
681
  record,
656
- token: this.#token(record, value)
682
+ token: this.#token(record, value, waiter)
657
683
  });
658
684
  this.#commit();
659
685
  }
@@ -678,8 +704,9 @@ var Pool = class {
678
704
  waiter.reject(result.error);
679
705
  continue;
680
706
  }
681
- this.#leased.add(result.record);
682
- this.#used.add(result.record);
707
+ const leases = this.#leased.get(result.record) ?? /* @__PURE__ */ new Set();
708
+ leases.add(waiter);
709
+ this.#leased.set(result.record, leases);
683
710
  if (this.#births.get(result.record) === this.#epoch) this.#strikes = 0;
684
711
  waiter.resolve(result.token);
685
712
  this.#emitter.emit("acquire");
@@ -706,8 +733,10 @@ var Pool = class {
706
733
  this.#pump();
707
734
  this.#finish();
708
735
  }
709
- #release(record) {
710
- 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);
711
740
  this.#recycle(record);
712
741
  }
713
742
  #recycle(record) {
@@ -716,7 +745,7 @@ var Pool = class {
716
745
  this.#own(this.#dispose(record));
717
746
  return;
718
747
  }
719
- this.#available.push(record);
748
+ if (this.#occupancy(record) === 0 && !this.#validating.has(record) && !this.#available.includes(record)) this.#available.push(record);
720
749
  this.#pump();
721
750
  if (this.#available.includes(record)) this.#emitter.emit("release");
722
751
  }
@@ -820,7 +849,7 @@ var Pool = class {
820
849
  //#region src/core/factories.ts
821
850
  /**
822
851
  * Creates a distinct {@link PoolInterface} from resource lifecycle hooks, with optional bounded
823
- * capacity, unique ownership, and FIFO settlement.
852
+ * capacity, exclusive leases by default, and FIFO settlement.
824
853
  *
825
854
  * @remarks
826
855
  * Concurrent create and validation hooks may overlap, while acquire promises settle in
@@ -860,6 +889,6 @@ function createPool(options) {
860
889
  return new Pool(options);
861
890
  }
862
891
  //#endregion
863
- export { Pool, PoolError, createPool, isPoolError, isPoolMax, isPoolSignal };
892
+ export { Pool, PoolError, createPool, isPoolError, isPoolLimit, isPoolSignal };
864
893
 
865
894
  //# sourceMappingURL=index.js.map