@orkestrel/scaffold 0.0.92 → 0.0.94

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,11 +1,11 @@
1
1
  # Pool
2
2
 
3
3
  > A typed resource pool with optional bounded capacity, a warm floor, bounded loss recovery,
4
- > unique ownership, FIFO settlement, validated reuse, caller-owned cancellation, and explicit cleanup.
4
+ > exclusive leases by default, FIFO settlement, validated idle reuse, caller-owned cancellation, and explicit cleanup.
5
5
 
6
6
  The pool supports lazy creation or a warm floor, with no eviction timer, acquire timeout,
7
7
  or polling loop. Waits park on promises or signal listeners. An acquire rejects with
8
- `cleanup` when a floor retains a record, owns `min` records, has nothing idle, and has no
8
+ `cleanup` when a floor retains a record, owns `min` records, has nothing eligible, and has no
9
9
  refill or disposal pending, even with live leases. The lifecycle hooks are the caller's,
10
10
  so the engine itself performs no I/O.
11
11
 
@@ -39,9 +39,9 @@ try {
39
39
 
40
40
  ### Factories
41
41
 
42
- | API | Kind | Summary |
43
- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `createPool` | function | Creates a distinct `PoolInterface` from resource lifecycle hooks, with optional bounded capacity, unique ownership, and FIFO settlement. |
42
+ | API | Kind | Summary |
43
+ | ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `createPool` | function | Creates a distinct `PoolInterface` from resource lifecycle hooks, with optional bounded capacity, exclusive leases by default, and FIFO settlement. |
45
45
 
46
46
  ### Classes
47
47
 
@@ -57,27 +57,29 @@ In a guard table a `Shape` cell holds the type the guard narrows to.
57
57
  | API | Kind | Shape | Summary |
58
58
  | -------------- | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
59
59
  | `isPoolError` | function | `PoolError` | Tests whether an unknown value is a `PoolError`, returning `false` for hostile proxies. |
60
- | `isPoolMax` | function | `number` | Tests whether a value is a positive safe integer, the only valid explicit pool maximum. |
60
+ | `isPoolLimit` | function | `number` | Tests whether a value is a positive safe integer for a pool record or lease limit. |
61
61
  | `isPoolSignal` | function | `AbortSignal` | Tests whether a value is a native `AbortSignal` for the acquire boundary, returning `false` for hostile proxies. |
62
62
 
63
63
  ### Types
64
64
 
65
65
  A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
66
66
 
67
- | API | Kind | Shape | Summary |
68
- | ------------------ | --------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
- | `PoolCode` | type | `'invalid' \| 'destroyed' \| 'create' \| 'cleanup'` | Names the machine-readable failure codes produced by `PoolError`. |
70
- | `PoolContext` | interface | `{ value?, failures? }` | Represents the structured context attached to a `PoolError`: the rejected input, or the distinct destroy-hook failures an aggregate cleanup collected. |
71
- | `PoolErrorOptions` | interface | `{ code, cause?, context? }` | Represents the construction options for `PoolError`: the stable code, an optional cause, and optional structured context. |
72
- | `PoolEventMap` | type | `{ create, acquire, release, destroy }` | Represents the observable resource lifecycle events emitted by a `PoolInterface`. |
73
- | `PoolToken` | interface | `{ value } plus release, destroy` | Represents a unique lease over one pool-owned resource record, exposing that record as a readonly `value` and ending it through an idempotent `release` or `destroy`. |
74
- | `PoolOptions` | interface | `{ on?, error?, create, destroy?, validate?, watch?, max?, min?, restarts? }` | Represents the resource lifecycle options for `Pool` and `createPool`: creation, destruction, validation, capacity, a bounded warm floor, and loss observation. |
75
- | `PoolInterface` | interface | `{ emitter, size, idle, active } plus start, acquire, clear, destroy` | Represents a FIFO resource pool with optional bounded capacity, a warm floor, loss recovery, and deterministic teardown, exposing its record counts and a typed lifecycle emitter. |
67
+ | API | Kind | Shape | Summary |
68
+ | ------------------ | --------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `PoolCode` | type | `'invalid' \| 'destroyed' \| 'create' \| 'cleanup'` | Names the machine-readable failure codes produced by `PoolError`. |
70
+ | `PoolContext` | interface | `{ value?, failures? }` | Represents the structured context attached to a `PoolError`: the rejected input, or the distinct destroy-hook failures an aggregate cleanup collected. |
71
+ | `PoolErrorOptions` | interface | `{ code, cause?, context? }` | Represents the construction options for `PoolError`: the stable code, an optional cause, and optional structured context. |
72
+ | `PoolEventMap` | type | `{ create, acquire, release, destroy }` | Represents the observable resource lifecycle events emitted by a `PoolInterface`. |
73
+ | `PoolToken` | interface | `{ value } plus release, destroy` | Represents a unique lease over one pool-owned resource record, exposing that record as a readonly `value` and ending it through an idempotent `release` or `destroy`. |
74
+ | `PoolOptions` | interface | `{ on?, error?, create, destroy?, validate?, watch?, capacity?, max?, min?, restarts? }` | Represents the resource lifecycle options for `Pool` and `createPool`: creation, destruction, validation, capacity, a bounded warm floor, and loss observation. |
75
+ | `PoolInterface` | interface | `{ emitter, size, idle, active } plus start, acquire, clear, destroy` | Represents a FIFO resource pool with optional bounded capacity, a warm floor, loss recovery, and deterministic teardown, exposing its record counts and a typed lifecycle emitter. |
76
76
 
77
77
  `size` counts every owned record, including records being validated, destroyed, or retained
78
- after failed cleanup under `min`. `idle` counts only immediately available records.
79
- `active` counts only leased records. An in-flight create reservation claims capacity but is
80
- not yet an owned record and therefore is not part of `size`.
78
+ after failed cleanup under `min`. `idle` counts available records with no live lease or pending
79
+ handout. `active` counts records with live leases, counting a shared record once. A partially
80
+ occupied record is active and can accept another lease; it is not idle. A pending handout counts
81
+ against `capacity` but becomes active only at its FIFO grant. An in-flight create reservation
82
+ claims record capacity but is not yet an owned record and therefore is not part of `size`.
81
83
 
82
84
  ## Methods
83
85
 
@@ -86,21 +88,21 @@ The public call-signature members of `PoolInterface` and `PoolToken`; `Pool` imp
86
88
 
87
89
  #### `PoolInterface`
88
90
 
89
- | Method | Returns | Summary |
90
- | --------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
91
- | `start` | `Promise<void>` | Fills the warm floor and resets the strikes of a spent floor; without a floor, resolves immediately. |
92
- | `acquire` | `Promise<PoolToken<T>>` | Queues the caller in FIFO order, validates an idle record or waits for a floor refill, and creates on demand only without a floor. |
93
- | `clear` | `Promise<void>` | Destroys the records that are idle at this call's synchronous snapshot and restores a started floor. |
94
- | `destroy` | `Promise<void>` | Tears down the pool permanently and returns its stable completion barrier. |
91
+ | Method | Returns | Summary |
92
+ | --------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `start` | `Promise<void>` | Fills the warm floor and resets the strikes of a spent floor; without a floor, resolves immediately. |
94
+ | `acquire` | `Promise<PoolToken<T>>` | Queues the caller in FIFO order, shares an occupied record or validates an idle record, and waits for a floor refill or creates on demand without a floor. |
95
+ | `clear` | `Promise<void>` | Destroys the records that are idle at this call's synchronous snapshot and restores a started floor. |
96
+ | `destroy` | `Promise<void>` | Tears down the pool permanently and returns its stable completion barrier. |
95
97
 
96
98
  #### `PoolToken`
97
99
 
98
100
  The lease returned by `acquire`, with operations that return or dispose its exact record.
99
101
 
100
- | Method | Returns | Summary |
101
- | --------- | --------------- | --------------------------------------------------------------------------------------------------------------------------- |
102
- | `release` | `void` | Gives this exact record back to the pool once; a repeat call, and a call after loss or teardown took ownership, are no-ops. |
103
- | `destroy` | `Promise<void>` | Destroys this exact record instead of returning it; a repeat call, and a call after release, are no-ops. |
102
+ | Method | Returns | Summary |
103
+ | --------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
104
+ | `release` | `void` | Ends this exact lease once; a repeat call, and a call after loss or teardown took ownership, are no-ops. Other leases on the record remain live. |
105
+ | `destroy` | `Promise<void>` | Destroys this exact record for every co-holder and invalidates their leases; a repeat call, and a call after release, are no-ops. |
104
106
 
105
107
  ## Contract
106
108
 
@@ -133,15 +135,16 @@ holder still holds the token. The holder learns of that loss from its own `watch
133
135
  hook. After disposal, the token's `release()` and `destroy()` calls do nothing. During disposal,
134
136
  the first `destroy()` call waits for that cleanup attempt and rejects with `cleanup` if it fails.
135
137
 
136
- A failed refill adds a strike. Losing a record that has never been leased also adds a strike,
137
- including failed validation. Granting a lease resets the strikes only when the granted record
138
- was created after the last strike; successful creation alone does not. Granting an older healthy
139
- record leaves the strikes unchanged. An idle never-leased record that dies, refills, and dies
138
+ A failed refill adds a strike. Losing a record with no live lease also adds a strike,
139
+ including a previously used record and failed idle validation. Granting a lease resets the
140
+ strikes only when the granted record was created after the last strike; successful creation
141
+ alone does not. Granting an older healthy
142
+ record leaves the strikes unchanged. Any idle record that dies, refills, and dies
140
143
  again without a grant between those losses can spend the floor.
141
144
  When strikes exceed `restarts`, the floor is spent. With `restarts: 1`, the first failed
142
145
  create permits another attempt, and the second refuses the next attempt. `start()` rejects
143
146
  with `create` and the last cause; another `start()` resets the strikes. While the floor is
144
- spent, an acquire without an idle record rejects with `create` after pending refills and
147
+ spent, an acquire without an eligible record rejects with `create` after pending refills and
145
148
  disposal settle. If the floor retains a record and owns `min` records, that acquire rejects
146
149
  with `cleanup` instead, even with live leases. A spent floor and a rejected `start()` still
147
150
  serve their live records. A loss without a thrown cause leaves that cause undefined.
@@ -151,17 +154,22 @@ The credit becomes spendable only when successful disposal removes the record; f
151
154
  grants no credit. That attempt remains owed during a concurrent refill and never resets the strikes.
152
155
  Its failed create adds a strike; its success alone does not reset the strikes. Granting that
153
156
  replacement resets the strikes only if no strike occurred after its creation. A record that was leased
154
- and later released adds no strike when lost. `token.destroy()` disposes the exact leased
155
- record, waits for an existing cleanup attempt, and rejects with `cleanup` if disposal fails.
157
+ and later lost after its last release adds a strike. Under sharing, loss or one lease's
158
+ `destroy()` ends the record for every co-holder, invalidates their leases, and owes one refill
159
+ regardless of the lease count. `token.destroy()` waits for an existing cleanup attempt and
160
+ rejects with `cleanup` if disposal fails.
156
161
  A token ends once: after `release()` or `destroy()`, the other call does nothing. Repeating
157
162
  either call does nothing. A repeat `token.destroy()` after a `cleanup` rejection resolves
158
- while the record stays retained.
163
+ while the record stays retained. Refills hold no lease before a caller's grant, so repeated idle
164
+ losses spend the budget. A consumer that keeps a lease without a holder, or repeatedly acquires
165
+ refilled records that fail during use, can keep earning refills and qualifying grant resets.
166
+ The pool cannot distinguish such a lease from useful work; callers must acquire only for a holder.
159
167
 
160
168
  Under `min`, a failed destroy hook leaves its record counted against `max` and excluded from
161
169
  idle and active counts. The pool never replaces that retained record or retries its destroy
162
170
  hook. The pool can run short; `start()` rejects with `cleanup` if retained records prevent
163
171
  filling. An acquire rejects with `cleanup` when the floor retains a record, owns `min` records,
164
- has nothing idle, and has no refill or disposal pending, even with live leases. The cause is
172
+ has nothing eligible, and has no refill or disposal pending, even with live leases. The cause is
165
173
  the retained record's own cleanup failure. The terminal `destroy()` barrier
166
174
  reports the original cleanup failures. Without `min`, cleanup retains its lazy behavior and
167
175
  frees capacity after either hook outcome.
@@ -189,6 +197,12 @@ await pool.destroy()
189
197
 
190
198
  ### Capacity and FIFO
191
199
 
200
+ `capacity` is a positive safe integer limiting simultaneous leases on each record; it defaults
201
+ to 1. `min` and `max` still bound records. Acquisition chooses the eligible record with the
202
+ fewest live leases and reserved handouts. Idle ties follow release order; occupied ties follow
203
+ record creation order. A validating record is unavailable until validation settles.
204
+ Reservations count before any awaited hook.
205
+
192
206
  Every `acquire` receives its queue position before a create or validation hook starts. The
193
207
  reentrancy-safe pump may assign several hook operations concurrently, but a head commit
194
208
  barrier settles successes and failures in request order. A later fast create or validation
@@ -196,14 +210,16 @@ cannot overtake an earlier slow one. Capacity obeys:
196
210
 
197
211
  ```text
198
212
  owned records + create reservations <= max
213
+ live leases + reserved handouts per record <= capacity
199
214
  ```
200
215
 
201
- `max` must be a positive safe integer. Omit both capacity options for an unbounded pool.
216
+ `max` must be a positive safe integer. Omit `min` and `max` for an unbounded record count.
202
217
  `Infinity`, fractions, zero, negative values, and unsafe integers are invalid.
203
- Construction snapshots `max` once and validates it before retention, then snapshots `on`
204
- and `error` once each.
218
+ Construction snapshots `max` and `capacity` once and validates them before retention, then
219
+ snapshots `on` and `error` once each.
205
220
 
206
- Record phases are disjoint:
221
+ Record ownership follows these transitions; an occupied record can carry several leases and
222
+ reserved handouts, and returns to available after its last lease and reservation end:
207
223
 
208
224
  ```text
209
225
  create reservation -> ready -> leased -> available -> validating -> ready
@@ -212,6 +228,14 @@ create reservation -> ready -> leased -> available -> validating -> ready
212
228
  floor refill --------------------------> available
213
229
  ```
214
230
 
231
+ Validation runs only when reserving an idle record; handouts on an occupied record skip it.
232
+ No caller receives a lease while idle validation is pending. This keeps a failed validation
233
+ from silently disposing a record used by co-holders. After the last lease ends, the next idle
234
+ reuse validates again. A freshly created lazy record keeps its direct handout behavior.
235
+ With `capacity` greater than 1, an occupied record is never revalidated. Only `watch` detects
236
+ its loss: without `watch`, a dead record can keep receiving leases until it becomes idle;
237
+ with `watch`, that exposure lasts until the watch settles.
238
+
215
239
  Invalid validation, whether `false` or a thrown value, claims and cleans the record before a
216
240
  replacement capacity slot becomes available. Cleanup failure rejects that acquire with
217
241
  `PoolError` code `cleanup`; successful cleanup lets the same FIFO waiter seek a replacement.
@@ -234,10 +258,11 @@ eventual `destroy()` barrier.
234
258
 
235
259
  ### Release and cleanup
236
260
 
237
- A token captures its exact opaque record, so `release()` is correct even when multiple
238
- records contain the same value. Release is idempotent and removes the lease synchronously.
239
- With a waiter, the record is validated before handoff. Without an assignable waiter, it
240
- becomes idle and emits `release`. Release after loss or teardown took ownership does nothing.
261
+ A token captures its exact lease and opaque record, so `release()` is correct even when multiple
262
+ records contain the same value or several tokens share a record. Release is idempotent and
263
+ removes only that lease synchronously. A remaining co-holder keeps the record active, and a
264
+ waiter can share the freed place without validation. After the last release, idle reuse validates.
265
+ A record that remains idle emits `release`. Release after loss or teardown took ownership does nothing.
241
266
  The pool disposes a lost lease while its holder still holds the token; the holder learns of
242
267
  the loss through its own `watch` or `destroy` hook. After disposal, both token methods do nothing.
243
268
 
@@ -318,10 +343,10 @@ const pool = createPool({
318
343
  Check a candidate value or error against the public boundary guards before acting on it:
319
344
 
320
345
  ```ts
321
- import { PoolError, isPoolError, isPoolMax, isPoolSignal } from '@orkestrel/pool'
346
+ import { PoolError, isPoolError, isPoolLimit, isPoolSignal } from '@orkestrel/pool'
322
347
 
323
- isPoolMax(4) // true
324
- isPoolMax(Infinity) // false: omit max for unbounded capacity
348
+ isPoolLimit(4) // true
349
+ isPoolLimit(Infinity) // false: omit max for unbounded capacity
325
350
  isPoolSignal(new AbortController().signal) // true
326
351
 
327
352
  const failure = new PoolError({ code: 'destroyed' })