@orkestrel/pool 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/src/core/index.cjs +276 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +194 -0
- package/dist/src/core/index.d.ts +194 -0
- package/dist/src/core/index.js +274 -0
- package/dist/src/core/index.js.map +1 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orkestrel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @orkestrel/pool
|
|
2
|
+
|
|
3
|
+
A bounded, typed **resource pool**: idle reuse + FIFO waiting. `acquire` leases
|
|
4
|
+
a resource — reusing a validated idle one, growing up to `max`, or parking on
|
|
5
|
+
a FIFO waiter list until a `release` frees one — and the returned token's
|
|
6
|
+
`release()` returns it for reuse (or hands it straight to the next waiter).
|
|
7
|
+
The FIFO handoff is validated, so a resource that goes bad while leased is
|
|
8
|
+
never handed to the next lessee, and a parked `acquire` given an `AbortSignal`
|
|
9
|
+
rejects and de-queues itself when the signal fires — no leaked waiter. The
|
|
10
|
+
pool is observable (a typed `emitter` surfaces `create` / `acquire` /
|
|
11
|
+
`release` / `destroy`) and deliberately de-bloated — no warm-floor, no
|
|
12
|
+
eviction timers. Environment-agnostic — no I/O, no browser or server
|
|
13
|
+
assumptions. Part of the `@orkestrel` line.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install @orkestrel/pool
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Node.js >= 24
|
|
24
|
+
- ESM-only (no CommonJS build)
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createPool } from '@orkestrel/pool'
|
|
30
|
+
|
|
31
|
+
const pool = createPool<Connection>({
|
|
32
|
+
create: () => connect(),
|
|
33
|
+
destroy: (connection) => connection.close(),
|
|
34
|
+
validate: (connection) => connection.alive,
|
|
35
|
+
max: 8,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const token = await pool.acquire()
|
|
39
|
+
try {
|
|
40
|
+
await token.value.query('select 1')
|
|
41
|
+
} finally {
|
|
42
|
+
token.release()
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Guide
|
|
47
|
+
|
|
48
|
+
For the full surface — the `Pool` engine, options, the observable `emitter`,
|
|
49
|
+
and usage patterns — see [`guides/src/pool.md`](guides/src/pool.md).
|
|
50
|
+
|
|
51
|
+
## Package
|
|
52
|
+
|
|
53
|
+
Published as a single typed entry point per the `exports` field in
|
|
54
|
+
`package.json`.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
3
|
+
//#region src/core/Pool.ts
|
|
4
|
+
/**
|
|
5
|
+
* A bounded resource pool with idle reuse + FIFO waiting.
|
|
6
|
+
*
|
|
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.
|
|
43
|
+
*/
|
|
44
|
+
var Pool = class {
|
|
45
|
+
#create;
|
|
46
|
+
#destroy;
|
|
47
|
+
#validate;
|
|
48
|
+
#max;
|
|
49
|
+
#emitter;
|
|
50
|
+
#idle = [];
|
|
51
|
+
#waiters = [];
|
|
52
|
+
#active = 0;
|
|
53
|
+
#destroyed = false;
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.#create = options.create;
|
|
56
|
+
this.#destroy = options.destroy;
|
|
57
|
+
this.#validate = options.validate;
|
|
58
|
+
this.#max = Math.max(1, options.max ?? Number.POSITIVE_INFINITY);
|
|
59
|
+
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
60
|
+
on: options?.on,
|
|
61
|
+
error: options?.error
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
get emitter() {
|
|
65
|
+
return this.#emitter;
|
|
66
|
+
}
|
|
67
|
+
get size() {
|
|
68
|
+
return this.#idle.length + this.#active;
|
|
69
|
+
}
|
|
70
|
+
get idle() {
|
|
71
|
+
return this.#idle.length;
|
|
72
|
+
}
|
|
73
|
+
get active() {
|
|
74
|
+
return this.#active;
|
|
75
|
+
}
|
|
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;
|
|
83
|
+
}
|
|
84
|
+
if (this.size < this.#max) {
|
|
85
|
+
const grown = await this.#grow();
|
|
86
|
+
this.#emitter.emit("acquire");
|
|
87
|
+
return grown;
|
|
88
|
+
}
|
|
89
|
+
return await this.#wait(signal);
|
|
90
|
+
}
|
|
91
|
+
async clear() {
|
|
92
|
+
const resources = this.#idle.splice(0);
|
|
93
|
+
await Promise.all(resources.map((resource) => this.#release(resource)));
|
|
94
|
+
}
|
|
95
|
+
async destroy() {
|
|
96
|
+
if (this.#destroyed) return;
|
|
97
|
+
this.#destroyed = true;
|
|
98
|
+
const waiters = this.#waiters.splice(0);
|
|
99
|
+
const error = /* @__PURE__ */ new Error("pool is destroyed");
|
|
100
|
+
for (const waiter of waiters) {
|
|
101
|
+
waiter.clear();
|
|
102
|
+
waiter.reject(error);
|
|
103
|
+
}
|
|
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);
|
|
114
|
+
}
|
|
115
|
+
await this.#release(resource);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async #grow() {
|
|
119
|
+
this.#active += 1;
|
|
120
|
+
let resource;
|
|
121
|
+
try {
|
|
122
|
+
resource = await this.#create();
|
|
123
|
+
} 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");
|
|
131
|
+
}
|
|
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: () => {}
|
|
141
|
+
};
|
|
142
|
+
this.#waiters.push(waiter);
|
|
143
|
+
if (signal !== void 0) {
|
|
144
|
+
const onAbort = () => {
|
|
145
|
+
const index = this.#waiters.indexOf(waiter);
|
|
146
|
+
if (index >= 0) this.#waiters.splice(index, 1);
|
|
147
|
+
reject(signal.reason);
|
|
148
|
+
};
|
|
149
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
150
|
+
waiter.clear = () => signal.removeEventListener("abort", onAbort);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
#token(resource) {
|
|
155
|
+
let released = false;
|
|
156
|
+
return {
|
|
157
|
+
value: resource,
|
|
158
|
+
release: () => {
|
|
159
|
+
if (released) return;
|
|
160
|
+
released = true;
|
|
161
|
+
this.#return(resource);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
#return(resource) {
|
|
166
|
+
if (this.#waiters.length === 0) {
|
|
167
|
+
this.#active -= 1;
|
|
168
|
+
if (this.#destroyed) {
|
|
169
|
+
this.#release(resource);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.#idle.push(resource);
|
|
173
|
+
this.#emitter.emit("release");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.#handoff(resource);
|
|
177
|
+
}
|
|
178
|
+
async #handoff(resource) {
|
|
179
|
+
if (await this.#valid(resource)) {
|
|
180
|
+
this.#serve(resource);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.#release(resource);
|
|
184
|
+
let replacement;
|
|
185
|
+
try {
|
|
186
|
+
replacement = await this.#create();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.#active -= 1;
|
|
189
|
+
const waiter = this.#waiters.shift();
|
|
190
|
+
waiter?.clear();
|
|
191
|
+
waiter?.reject(error instanceof Error ? error : new Error(String(error)));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.#emitter.emit("create");
|
|
195
|
+
this.#serve(replacement);
|
|
196
|
+
}
|
|
197
|
+
#serve(resource) {
|
|
198
|
+
const waiter = this.#destroyed ? void 0 : this.#waiters.shift();
|
|
199
|
+
if (waiter !== void 0) {
|
|
200
|
+
waiter.clear();
|
|
201
|
+
waiter.resolve(this.#token(resource));
|
|
202
|
+
this.#emitter.emit("acquire");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.#active -= 1;
|
|
206
|
+
if (this.#destroyed) {
|
|
207
|
+
this.#release(resource);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.#idle.push(resource);
|
|
211
|
+
this.#emitter.emit("release");
|
|
212
|
+
}
|
|
213
|
+
async #valid(resource) {
|
|
214
|
+
if (this.#validate === void 0) return true;
|
|
215
|
+
try {
|
|
216
|
+
return await this.#validate(resource);
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async #release(resource) {
|
|
222
|
+
if (this.#destroy === void 0) {
|
|
223
|
+
this.#emitter.emit("destroy");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
await this.#destroy(resource);
|
|
228
|
+
} catch {}
|
|
229
|
+
this.#emitter.emit("destroy");
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/core/factories.ts
|
|
234
|
+
/**
|
|
235
|
+
* Create a bounded resource pool with idle reuse and FIFO waiting — `acquire` leases a
|
|
236
|
+
* resource (reusing a validated idle one, growing up to `max`, or parking until a
|
|
237
|
+
* `release` frees one) and the returned token's `release` returns it for reuse.
|
|
238
|
+
*
|
|
239
|
+
* @remarks
|
|
240
|
+
* A parked `acquire` given an `AbortSignal` rejects + de-queues itself when the signal
|
|
241
|
+
* fires (no leaked waiter). `clear` destroys idle resources (leased ones keep running);
|
|
242
|
+
* `destroy` destroys all and rejects waiters. The pool is lean — no warm-floor (`min`),
|
|
243
|
+
* no eviction timers — and observable (§13): a typed `emitter` surfaces
|
|
244
|
+
* `create` / `acquire` / `release` / `destroy`.
|
|
245
|
+
*
|
|
246
|
+
* @typeParam T - The pooled resource type
|
|
247
|
+
* @param options - The `create` hook plus optional `destroy` / `validate` / `max`
|
|
248
|
+
* @returns A working {@link PoolInterface}
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* import { createPool } from '@src/core'
|
|
253
|
+
*
|
|
254
|
+
* const pool = createPool<Connection>({
|
|
255
|
+
* create: () => connect(),
|
|
256
|
+
* destroy: (connection) => connection.close(),
|
|
257
|
+
* validate: (connection) => connection.alive,
|
|
258
|
+
* max: 8,
|
|
259
|
+
* })
|
|
260
|
+
*
|
|
261
|
+
* const token = await pool.acquire()
|
|
262
|
+
* try {
|
|
263
|
+
* await token.value.query('select 1')
|
|
264
|
+
* } finally {
|
|
265
|
+
* token.release()
|
|
266
|
+
* }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
function createPool(options) {
|
|
270
|
+
return new Pool(options);
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
exports.Pool = Pool;
|
|
274
|
+
exports.createPool = createPool;
|
|
275
|
+
|
|
276
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#create","#destroy","#validate","#max","#emitter","#idle","#waiters","#active","#destroyed","#reuse","#grow","#wait","#release","#valid","#token","#return","#handoff","#serve"],"sources":["../../../src/core/Pool.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { PoolEventMap, PoolInterface, PoolOptions, PoolToken, PoolWaiter } from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\n\n/**\n * A bounded resource pool with idle reuse + FIFO waiting.\n *\n * @remarks\n * - **Idle reuse.** `acquire` first takes an idle resource (validating it when a\n * `validate` hook is set — an invalid one is destroyed and the next idle / a fresh\n * one is tried). When no usable idle resource exists and the pool is below `max`, it\n * `create`s a new one. At `max` with none idle, the acquire PARKS on a FIFO waiter\n * list until a `release` hands it a resource.\n * - **FIFO handoff (validated).** `release` (on the token) hands the resource to the next\n * parked waiter (oldest first) — the resource stays leased, the lessee just changes —\n * or returns it to idle when no one is waiting. With a waiter parked the resource is\n * re-validated first (the same `validate` hook the idle path uses), so a resource that\n * went invalid WHILE leased (e.g. a terminated worker thread) is destroyed and the\n * waiter is served a fresh/valid one instead — a dead resource is never handed on. The\n * no-waiter path stays synchronous; releasing the same token twice is a no-op (an\n * idempotent token guard).\n * - **`validate` is total.** A `validate` hook that THROWS is treated exactly like one\n * returning `false` — the resource is \"not usable\", so it is destroyed and replaced —\n * rather than escaping. This holds on both the idle reuse and the FIFO handoff paths, so\n * a throwing validator can never strand a parked waiter on an unhandled rejection.\n * - **Abort-cancellable waiting.** A parked `acquire` given an `AbortSignal` rejects\n * when that signal fires and removes its waiter from the queue — no leaked waiter, so\n * a later `release` still serves the next live waiter. The signal is supplied by the\n * caller (a worker, for example, passes its per-attempt execution signal); the pool adds no abort\n * of its own.\n * - **Counts.** `size` = idle + leased; `idle` = available now; `active` = leased out.\n * - **Teardown.** `clear` destroys every IDLE resource (leased ones keep running);\n * `destroy` destroys ALL resources and rejects any parked waiters. Both await the\n * `destroy` hook.\n * - **Observable (§13).** The owned {@link emitter} ({@link PoolEventMap}) carries the\n * resource lifecycle — `create` / `acquire` / `release` / `destroy` — for fire-and-forget\n * observers. Every event is emitted directly, strictly AFTER the relevant transition —\n * OUTSIDE the `#handoff` / `#serve` await-chain, never across a waiter's resolve; the\n * emitter isolates a listener throw and routes it to its `error` handler (the `error`\n * option), so a buggy observer can NEVER corrupt the validated FIFO handoff-eviction\n * machinery (it cannot strand a parked waiter or unbalance the lease count). Observation is\n * purely a side-channel.\n * - **De-bloated.** No warm-floor / `min`, no eviction timers — lean.\n */\nexport class Pool<T> implements PoolInterface<T> {\n\treadonly #create: () => Promise<T> | T\n\treadonly #destroy: ((value: T) => Promise<void> | void) | undefined\n\treadonly #validate: ((value: T) => Promise<boolean> | boolean) | undefined\n\treadonly #max: number\n\t// The PUSH observation surface (§13) — owned, never inherited. The emitter isolates a\n\t// listener throw (routing it to the `error` handler), so it can never escape into the\n\t// validated FIFO handoff-eviction path.\n\treadonly #emitter: Emitter<PoolEventMap>\n\n\treadonly #idle: T[] = []\n\treadonly #waiters: PoolWaiter<T>[] = []\n\t#active = 0\n\t#destroyed = false\n\n\tconstructor(options: PoolOptions<T>) {\n\t\tthis.#create = options.create\n\t\tthis.#destroy = options.destroy\n\t\tthis.#validate = options.validate\n\t\tthis.#max = Math.max(1, options.max ?? Number.POSITIVE_INFINITY)\n\t\tthis.#emitter = new Emitter<PoolEventMap>({ on: options?.on, error: options?.error })\n\t}\n\n\tget emitter(): EmitterInterface<PoolEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget size(): number {\n\t\treturn this.#idle.length + this.#active\n\t}\n\n\tget idle(): number {\n\t\treturn this.#idle.length\n\t}\n\n\tget active(): number {\n\t\treturn this.#active\n\t}\n\n\tasync acquire(signal?: AbortSignal): Promise<PoolToken<T>> {\n\t\tif (this.#destroyed) throw new Error('pool is destroyed')\n\t\tif (signal?.aborted === true) throw signal.reason\n\t\t// Reuse a validated idle resource if one is available.\n\t\tconst reused = await this.#reuse()\n\t\tif (reused !== undefined) {\n\t\t\t// Observe the lease — AFTER `#reuse` resolved the token (the resource is already\n\t\t\t// leased; emit only OBSERVES it).\n\t\t\tthis.#emitter.emit('acquire')\n\t\t\treturn reused\n\t\t}\n\t\t// Otherwise grow the pool when below the cap.\n\t\tif (this.size < this.#max) {\n\t\t\tconst grown = await this.#grow()\n\t\t\t// Observe the lease — AFTER `#grow` resolved the fresh token.\n\t\t\tthis.#emitter.emit('acquire')\n\t\t\treturn grown\n\t\t}\n\t\t// At capacity with nothing idle — park until a release hands us a resource. The\n\t\t// `acquire` for THIS lease is emitted by `#serve` once a `release` hands it a resource\n\t\t// (after `waiter.resolve`), NOT here — so a served waiter emits `acquire` exactly once.\n\t\treturn await this.#wait(signal)\n\t}\n\n\tasync clear(): Promise<void> {\n\t\tconst resources = this.#idle.splice(0)\n\t\tawait Promise.all(resources.map((resource) => this.#release(resource)))\n\t}\n\n\tasync destroy(): Promise<void> {\n\t\tif (this.#destroyed) return\n\t\tthis.#destroyed = true\n\t\tconst waiters = this.#waiters.splice(0)\n\t\tconst error = new Error('pool is destroyed')\n\t\tfor (const waiter of waiters) {\n\t\t\twaiter.clear()\n\t\t\twaiter.reject(error)\n\t\t}\n\t\tconst resources = this.#idle.splice(0)\n\t\tawait Promise.all(resources.map((resource) => this.#release(resource)))\n\t}\n\n\t// Take + validate the next idle resource; destroy + skip invalid ones. Returns a\n\t// token for the first valid idle resource, or `undefined` when none is usable.\n\tasync #reuse(): Promise<PoolToken<T> | undefined> {\n\t\twhile (this.#idle.length > 0) {\n\t\t\tconst resource = this.#idle.shift()\n\t\t\tif (resource === undefined) continue\n\t\t\tif (await this.#valid(resource)) {\n\t\t\t\tthis.#active += 1\n\t\t\t\treturn this.#token(resource)\n\t\t\t}\n\t\t\tawait this.#release(resource)\n\t\t}\n\t\treturn undefined\n\t}\n\n\t// Create a fresh resource and lease it. Reserve the active slot before awaiting\n\t// `create` so a concurrent acquire sees the pool at capacity (no overshoot of `max`).\n\tasync #grow(): Promise<PoolToken<T>> {\n\t\tthis.#active += 1\n\t\tlet resource: T\n\t\ttry {\n\t\t\tresource = await this.#create()\n\t\t} catch (error: unknown) {\n\t\t\tthis.#active -= 1\n\t\t\tthrow error instanceof Error ? error : new Error(String(error))\n\t\t}\n\t\t// The pool may have been destroyed during `create` — never hand a live resource into\n\t\t// a torn-down pool. Free the active slot, destroy the just-created resource, and throw.\n\t\tif (this.#destroyed) {\n\t\t\tthis.#active -= 1\n\t\t\tawait this.#release(resource)\n\t\t\tthrow new Error('pool is destroyed')\n\t\t}\n\t\t// Observe the fresh resource — AFTER `create` resolved and the not-destroyed re-check\n\t\t// passed (the resource is here to stay), BEFORE the token is built.\n\t\tthis.#emitter.emit('create')\n\t\treturn this.#token(resource)\n\t}\n\n\t// Park a resolver on the FIFO waiter list until a release hands it a resource; an\n\t// abort on `signal` rejects the acquire and removes its waiter (no leak).\n\t#wait(signal: AbortSignal | undefined): Promise<PoolToken<T>> {\n\t\treturn new Promise<PoolToken<T>>((resolve, reject) => {\n\t\t\tconst waiter: PoolWaiter<T> = { resolve, reject, clear: () => {} }\n\t\t\tthis.#waiters.push(waiter)\n\t\t\tif (signal !== undefined) {\n\t\t\t\tconst onAbort = (): void => {\n\t\t\t\t\tconst index = this.#waiters.indexOf(waiter)\n\t\t\t\t\tif (index >= 0) this.#waiters.splice(index, 1)\n\t\t\t\t\treject(signal.reason)\n\t\t\t\t}\n\t\t\t\tsignal.addEventListener('abort', onAbort, { once: true })\n\t\t\t\twaiter.clear = (): void => signal.removeEventListener('abort', onAbort)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Build an idempotent token; its `release` returns the resource exactly once.\n\t#token(resource: T): PoolToken<T> {\n\t\tlet released = false\n\t\treturn {\n\t\t\tvalue: resource,\n\t\t\trelease: (): void => {\n\t\t\t\tif (released) return\n\t\t\t\treleased = true\n\t\t\t\tthis.#return(resource)\n\t\t\t},\n\t\t}\n\t}\n\n\t// Return a leased resource. No waiter parked → synchronously drop it to idle (or destroy\n\t// it if torn down), `#active -= 1`. A waiter IS parked → hand off asynchronously, after\n\t// re-validating the resource so an invalid one (e.g. a terminated thread) is never served.\n\t#return(resource: T): void {\n\t\tif (this.#waiters.length === 0) {\n\t\t\tthis.#active -= 1\n\t\t\tif (this.#destroyed) {\n\t\t\t\tvoid this.#release(resource)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.#idle.push(resource)\n\t\t\t// Observe the resource dropping to idle — AFTER `#active` was balanced down and it\n\t\t\t// is back on the idle list (the synchronous no-waiter path). A waiter-handoff\n\t\t\t// instead keeps the resource LEASED, so it emits no `release` here.\n\t\t\tthis.#emitter.emit('release')\n\t\t\treturn\n\t\t}\n\t\tvoid this.#handoff(resource)\n\t}\n\n\t// Hand a released resource to the oldest parked waiter, re-validating it first. A VALID\n\t// resource is served as-is (it stays leased — `#active` unchanged). An INVALID one is\n\t// destroyed and the waiter is served a fresh resource that REUSES the dead one's `#active`\n\t// slot (the count never dips below the lease, so a concurrent acquire can't overshoot\n\t// `max`). The waiter is re-shifted after every `await` (the queue can change underneath\n\t// us), and the pool is re-checked for teardown — so no waiter is leaked or double-settled\n\t// and no resource is handed into a destroyed pool.\n\tasync #handoff(resource: T): Promise<void> {\n\t\t// VALID release → serve the released resource directly (the common, unchanged path).\n\t\tif (await this.#valid(resource)) {\n\t\t\tthis.#serve(resource)\n\t\t\treturn\n\t\t}\n\t\t// INVALID release → destroy it and replace it, holding its `#active` slot steady.\n\t\tvoid this.#release(resource)\n\t\tlet replacement: T\n\t\ttry {\n\t\t\treplacement = await this.#create()\n\t\t} catch (error: unknown) {\n\t\t\t// The dead resource cannot be replaced — free its slot and reject the oldest waiter\n\t\t\t// (it can't be served), or drop the error if the queue raced empty.\n\t\t\tthis.#active -= 1\n\t\t\tconst waiter = this.#waiters.shift()\n\t\t\twaiter?.clear()\n\t\t\twaiter?.reject(error instanceof Error ? error : new Error(String(error)))\n\t\t\treturn\n\t\t}\n\t\t// Observe the fresh replacement — AFTER `create` resolved, BEFORE it is served.\n\t\tthis.#emitter.emit('create')\n\t\tthis.#serve(replacement)\n\t}\n\n\t// Hand a validated resource (the released one, or its fresh replacement) to the oldest\n\t// live waiter, holding its `#active` slot. If the queue raced empty across the awaits, the\n\t// resource is idle (or destroyed when torn down) and its slot is freed.\n\t#serve(resource: T): void {\n\t\tconst waiter = this.#destroyed ? undefined : this.#waiters.shift()\n\t\tif (waiter !== undefined) {\n\t\t\twaiter.clear()\n\t\t\twaiter.resolve(this.#token(resource))\n\t\t\t// Observe the served-waiter lease — strictly AFTER `waiter.resolve(...)` (the token\n\t\t\t// is already handed off; the emit only OBSERVES it and cannot sit across the resolve\n\t\t\t// or perturb the served acquirer's continuation). This is the `acquire` for a parked\n\t\t\t// `acquire`, so the public `acquire` deliberately does NOT emit on its `#wait` branch.\n\t\t\tthis.#emitter.emit('acquire')\n\t\t\treturn\n\t\t}\n\t\tthis.#active -= 1\n\t\tif (this.#destroyed) {\n\t\t\tvoid this.#release(resource)\n\t\t\treturn\n\t\t}\n\t\tthis.#idle.push(resource)\n\t\t// The waiter queue raced empty across the handoff awaits — the resource lands back on\n\t\t// idle instead of being leased. Observe that as a `release` (the return-to-idle that\n\t\t// `#return` would have emitted had no waiter been parked when the token was released).\n\t\tthis.#emitter.emit('release')\n\t}\n\n\t// Is this resource usable? No validator trusts it; otherwise run the hook. A validator\n\t// that THROWS is treated exactly like one returning `false` — the resource is \"not\n\t// usable\", so it is destroyed + replaced rather than escaping as an unhandled rejection\n\t// that would strand a parked waiter on the handoff path (the same total-function spirit\n\t// as a guard, AGENTS §12 / §14). The single gate for both the idle (`#reuse`) and the\n\t// handoff (`#handoff`) validation, so the two paths can never diverge.\n\tasync #valid(resource: T): Promise<boolean> {\n\t\tif (this.#validate === undefined) return true\n\t\ttry {\n\t\t\treturn await this.#validate(resource)\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Destroy one resource via the optional hook, swallowing destruction failures. Observe the\n\t// drop with a `destroy` event AFTER the hook ran (or after the no-hook drop) — the resource\n\t// is gone from the pool either way; a swallowed `destroy`-hook failure still emits (the pool\n\t// abandoned it). The emit is the LAST thing here, strictly after the teardown transition.\n\tasync #release(resource: T): Promise<void> {\n\t\tif (this.#destroy === undefined) {\n\t\t\tthis.#emitter.emit('destroy')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait this.#destroy(resource)\n\t\t} catch {\n\t\t\t// A destroy failure abandons the resource — there is nothing to recover.\n\t\t}\n\t\tthis.#emitter.emit('destroy')\n\t}\n}\n","import type { PoolInterface, PoolOptions } from './types.js'\nimport { Pool } from './Pool.js'\n\n/**\n * Create a bounded resource pool with idle reuse and FIFO waiting — `acquire` leases a\n * resource (reusing a validated idle one, growing up to `max`, or parking until a\n * `release` frees one) and the returned token's `release` returns it for reuse.\n *\n * @remarks\n * A parked `acquire` given an `AbortSignal` rejects + de-queues itself when the signal\n * fires (no leaked waiter). `clear` destroys idle resources (leased ones keep running);\n * `destroy` destroys all and rejects waiters. The pool is lean — no warm-floor (`min`),\n * no eviction timers — and observable (§13): a typed `emitter` surfaces\n * `create` / `acquire` / `release` / `destroy`.\n *\n * @typeParam T - The pooled resource type\n * @param options - The `create` hook plus optional `destroy` / `validate` / `max`\n * @returns A working {@link PoolInterface}\n *\n * @example\n * ```ts\n * import { createPool } from '@src/core'\n *\n * const pool = createPool<Connection>({\n * \tcreate: () => connect(),\n * \tdestroy: (connection) => connection.close(),\n * \tvalidate: (connection) => connection.alive,\n * \tmax: 8,\n * })\n *\n * const token = await pool.acquire()\n * try {\n * \tawait token.value.query('select 1')\n * } finally {\n * \ttoken.release()\n * }\n * ```\n */\nexport function createPool<T>(options: PoolOptions<T>): PoolInterface<T> {\n\treturn new Pool(options)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
|
+
import { EmitterHooks } from '@orkestrel/emitter';
|
|
3
|
+
import { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a bounded resource pool with idle reuse and FIFO waiting — `acquire` leases a
|
|
7
|
+
* resource (reusing a validated idle one, growing up to `max`, or parking until a
|
|
8
|
+
* `release` frees one) and the returned token's `release` returns it for reuse.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* A parked `acquire` given an `AbortSignal` rejects + de-queues itself when the signal
|
|
12
|
+
* fires (no leaked waiter). `clear` destroys idle resources (leased ones keep running);
|
|
13
|
+
* `destroy` destroys all and rejects waiters. The pool is lean — no warm-floor (`min`),
|
|
14
|
+
* no eviction timers — and observable (§13): a typed `emitter` surfaces
|
|
15
|
+
* `create` / `acquire` / `release` / `destroy`.
|
|
16
|
+
*
|
|
17
|
+
* @typeParam T - The pooled resource type
|
|
18
|
+
* @param options - The `create` hook plus optional `destroy` / `validate` / `max`
|
|
19
|
+
* @returns A working {@link PoolInterface}
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { createPool } from '@src/core'
|
|
24
|
+
*
|
|
25
|
+
* const pool = createPool<Connection>({
|
|
26
|
+
* create: () => connect(),
|
|
27
|
+
* destroy: (connection) => connection.close(),
|
|
28
|
+
* validate: (connection) => connection.alive,
|
|
29
|
+
* max: 8,
|
|
30
|
+
* })
|
|
31
|
+
*
|
|
32
|
+
* const token = await pool.acquire()
|
|
33
|
+
* try {
|
|
34
|
+
* await token.value.query('select 1')
|
|
35
|
+
* } finally {
|
|
36
|
+
* token.release()
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function createPool<T>(options: PoolOptions<T>): PoolInterface<T>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A bounded resource pool with idle reuse + FIFO waiting.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* - **Idle reuse.** `acquire` first takes an idle resource (validating it when a
|
|
47
|
+
* `validate` hook is set — an invalid one is destroyed and the next idle / a fresh
|
|
48
|
+
* one is tried). When no usable idle resource exists and the pool is below `max`, it
|
|
49
|
+
* `create`s a new one. At `max` with none idle, the acquire PARKS on a FIFO waiter
|
|
50
|
+
* list until a `release` hands it a resource.
|
|
51
|
+
* - **FIFO handoff (validated).** `release` (on the token) hands the resource to the next
|
|
52
|
+
* parked waiter (oldest first) — the resource stays leased, the lessee just changes —
|
|
53
|
+
* or returns it to idle when no one is waiting. With a waiter parked the resource is
|
|
54
|
+
* re-validated first (the same `validate` hook the idle path uses), so a resource that
|
|
55
|
+
* went invalid WHILE leased (e.g. a terminated worker thread) is destroyed and the
|
|
56
|
+
* waiter is served a fresh/valid one instead — a dead resource is never handed on. The
|
|
57
|
+
* no-waiter path stays synchronous; releasing the same token twice is a no-op (an
|
|
58
|
+
* idempotent token guard).
|
|
59
|
+
* - **`validate` is total.** A `validate` hook that THROWS is treated exactly like one
|
|
60
|
+
* returning `false` — the resource is "not usable", so it is destroyed and replaced —
|
|
61
|
+
* rather than escaping. This holds on both the idle reuse and the FIFO handoff paths, so
|
|
62
|
+
* a throwing validator can never strand a parked waiter on an unhandled rejection.
|
|
63
|
+
* - **Abort-cancellable waiting.** A parked `acquire` given an `AbortSignal` rejects
|
|
64
|
+
* when that signal fires and removes its waiter from the queue — no leaked waiter, so
|
|
65
|
+
* a later `release` still serves the next live waiter. The signal is supplied by the
|
|
66
|
+
* caller (a worker, for example, passes its per-attempt execution signal); the pool adds no abort
|
|
67
|
+
* of its own.
|
|
68
|
+
* - **Counts.** `size` = idle + leased; `idle` = available now; `active` = leased out.
|
|
69
|
+
* - **Teardown.** `clear` destroys every IDLE resource (leased ones keep running);
|
|
70
|
+
* `destroy` destroys ALL resources and rejects any parked waiters. Both await the
|
|
71
|
+
* `destroy` hook.
|
|
72
|
+
* - **Observable (§13).** The owned {@link emitter} ({@link PoolEventMap}) carries the
|
|
73
|
+
* resource lifecycle — `create` / `acquire` / `release` / `destroy` — for fire-and-forget
|
|
74
|
+
* observers. Every event is emitted directly, strictly AFTER the relevant transition —
|
|
75
|
+
* OUTSIDE the `#handoff` / `#serve` await-chain, never across a waiter's resolve; the
|
|
76
|
+
* emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
77
|
+
* option), so a buggy observer can NEVER corrupt the validated FIFO handoff-eviction
|
|
78
|
+
* machinery (it cannot strand a parked waiter or unbalance the lease count). Observation is
|
|
79
|
+
* purely a side-channel.
|
|
80
|
+
* - **De-bloated.** No warm-floor / `min`, no eviction timers — lean.
|
|
81
|
+
*/
|
|
82
|
+
export declare class Pool<T> implements PoolInterface<T> {
|
|
83
|
+
#private;
|
|
84
|
+
constructor(options: PoolOptions<T>);
|
|
85
|
+
get emitter(): EmitterInterface<PoolEventMap>;
|
|
86
|
+
get size(): number;
|
|
87
|
+
get idle(): number;
|
|
88
|
+
get active(): number;
|
|
89
|
+
acquire(signal?: AbortSignal): Promise<PoolToken<T>>;
|
|
90
|
+
clear(): Promise<void>;
|
|
91
|
+
destroy(): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The push observation surface of a {@link PoolInterface} (AGENTS §13) — the resource
|
|
96
|
+
* lifecycle moments a fire-and-forget observer subscribes to.
|
|
97
|
+
*
|
|
98
|
+
* @remarks
|
|
99
|
+
* Pure signals (no `T` payload — `Pool<T>` carries no resource value on its events, so a
|
|
100
|
+
* non-generic map stays lean). Listener isolation is the emitter's (AGENTS §13): every event
|
|
101
|
+
* is emitted directly and a listener throw is routed to the emitter's `error` handler (the
|
|
102
|
+
* `error` option), never onto this map, and sits AFTER the relevant create / acquire /
|
|
103
|
+
* release / destroy transition — so a throwing observer can never corrupt the FIFO
|
|
104
|
+
* handoff-eviction machinery (it cannot strand a parked waiter or unbalance the lease count).
|
|
105
|
+
* Subscribe via `pool.emitter.on(...)`. Declared as a `type` alias (§4.5 — `EventMap` is a
|
|
106
|
+
* `type` kind).
|
|
107
|
+
*/
|
|
108
|
+
export declare type PoolEventMap = {
|
|
109
|
+
/** A fresh resource was created (`create` resolved) and leased. */
|
|
110
|
+
readonly create: readonly [];
|
|
111
|
+
/** A token was handed to a lessee (a reused idle one, a fresh one, or a served waiter). */
|
|
112
|
+
readonly acquire: readonly [];
|
|
113
|
+
/** A leased resource returned to idle (no waiter was parked). */
|
|
114
|
+
readonly release: readonly [];
|
|
115
|
+
/** A resource was destroyed (`clear` / `destroy`, or a failed `validate`). */
|
|
116
|
+
readonly destroy: readonly [];
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A bounded resource pool with idle reuse + FIFO waiting.
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* Exposes a typed {@link emitter} (AGENTS §13) carrying its resource lifecycle moments
|
|
124
|
+
* ({@link PoolEventMap}) for fire-and-forget observers. Emitting is observation-only —
|
|
125
|
+
* every event fires AFTER the relevant create / acquire / release / destroy transition, so a
|
|
126
|
+
* buggy observer can never corrupt the FIFO handoff-eviction machinery: the emitter isolates
|
|
127
|
+
* a listener throw and routes it to its `error` handler (the `error` option), never the pool.
|
|
128
|
+
*/
|
|
129
|
+
export declare interface PoolInterface<T> {
|
|
130
|
+
readonly emitter: EmitterInterface<PoolEventMap>;
|
|
131
|
+
readonly size: number;
|
|
132
|
+
readonly idle: number;
|
|
133
|
+
readonly active: number;
|
|
134
|
+
acquire(signal?: AbortSignal): Promise<PoolToken<T>>;
|
|
135
|
+
clear(): Promise<void>;
|
|
136
|
+
destroy(): Promise<void>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Options for `createPool` — the resource lifecycle hooks.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* - `create` — make a fresh resource; called when no idle resource is reusable and
|
|
144
|
+
* the pool is below `max`. May be async.
|
|
145
|
+
* - `destroy` — tear a resource down when the pool drops it (`clear` / `destroy`, or
|
|
146
|
+
* a failed `validate`); optional and awaited.
|
|
147
|
+
* - `validate` — check an idle resource is still usable before leasing it; an invalid
|
|
148
|
+
* resource is destroyed and replaced. Optional (an absent validator trusts idle).
|
|
149
|
+
* - `max` — the most resources that may exist at once (idle + leased); defaults to
|
|
150
|
+
* unbounded. A surplus `acquire` waits (FIFO) for a `release`.
|
|
151
|
+
* - `on` — the reserved {@link EmitterHooks} key (§8): initial listeners for the pool's
|
|
152
|
+
* {@link PoolEventMap}, wired at construction (e.g. `{ create: () => count() }`).
|
|
153
|
+
*/
|
|
154
|
+
export declare interface PoolOptions<T> {
|
|
155
|
+
readonly on?: EmitterHooks<PoolEventMap>;
|
|
156
|
+
/** The emitter's listener-error handler (AGENTS §13) — a listener throw routes here, not to a domain event. */
|
|
157
|
+
readonly error?: EmitterErrorHandler;
|
|
158
|
+
readonly create: () => Promise<T> | T;
|
|
159
|
+
readonly destroy?: (value: T) => Promise<void> | void;
|
|
160
|
+
readonly validate?: (value: T) => Promise<boolean> | boolean;
|
|
161
|
+
readonly max?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A leased resource from a {@link PoolInterface} — `value` is the live resource and
|
|
166
|
+
* `release()` returns it to the pool for reuse (or hands it to the next waiter).
|
|
167
|
+
*/
|
|
168
|
+
export declare interface PoolToken<T> {
|
|
169
|
+
/** The leased resource. */
|
|
170
|
+
readonly value: T;
|
|
171
|
+
/** Return the resource to the pool; calling more than once is a no-op. */
|
|
172
|
+
release(): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A parked acquirer on a {@link PoolInterface}'s FIFO waiter list — its promise resolvers
|
|
177
|
+
* plus the cleanup that detaches its abort listener.
|
|
178
|
+
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* Held only inside the {@link PoolInterface} engine (a resource at `max` parks the acquirer
|
|
181
|
+
* here until a `release` hands it a token); not part of the public call surface, but
|
|
182
|
+
* centralized here per AGENTS §5. `resolve` hands the waiter its leased {@link PoolToken};
|
|
183
|
+
* `reject` fails its `acquire` (a teardown or an aborted wait); `clear` detaches the abort
|
|
184
|
+
* listener so a settled waiter leaks nothing.
|
|
185
|
+
*
|
|
186
|
+
* @typeParam T - The resource the pool leases
|
|
187
|
+
*/
|
|
188
|
+
export declare interface PoolWaiter<T> {
|
|
189
|
+
readonly resolve: (token: PoolToken<T>) => void;
|
|
190
|
+
readonly reject: (error: unknown) => void;
|
|
191
|
+
clear(): void;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { }
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
|
+
import { EmitterHooks } from '@orkestrel/emitter';
|
|
3
|
+
import { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a bounded resource pool with idle reuse and FIFO waiting — `acquire` leases a
|
|
7
|
+
* resource (reusing a validated idle one, growing up to `max`, or parking until a
|
|
8
|
+
* `release` frees one) and the returned token's `release` returns it for reuse.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* A parked `acquire` given an `AbortSignal` rejects + de-queues itself when the signal
|
|
12
|
+
* fires (no leaked waiter). `clear` destroys idle resources (leased ones keep running);
|
|
13
|
+
* `destroy` destroys all and rejects waiters. The pool is lean — no warm-floor (`min`),
|
|
14
|
+
* no eviction timers — and observable (§13): a typed `emitter` surfaces
|
|
15
|
+
* `create` / `acquire` / `release` / `destroy`.
|
|
16
|
+
*
|
|
17
|
+
* @typeParam T - The pooled resource type
|
|
18
|
+
* @param options - The `create` hook plus optional `destroy` / `validate` / `max`
|
|
19
|
+
* @returns A working {@link PoolInterface}
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { createPool } from '@src/core'
|
|
24
|
+
*
|
|
25
|
+
* const pool = createPool<Connection>({
|
|
26
|
+
* create: () => connect(),
|
|
27
|
+
* destroy: (connection) => connection.close(),
|
|
28
|
+
* validate: (connection) => connection.alive,
|
|
29
|
+
* max: 8,
|
|
30
|
+
* })
|
|
31
|
+
*
|
|
32
|
+
* const token = await pool.acquire()
|
|
33
|
+
* try {
|
|
34
|
+
* await token.value.query('select 1')
|
|
35
|
+
* } finally {
|
|
36
|
+
* token.release()
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function createPool<T>(options: PoolOptions<T>): PoolInterface<T>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A bounded resource pool with idle reuse + FIFO waiting.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* - **Idle reuse.** `acquire` first takes an idle resource (validating it when a
|
|
47
|
+
* `validate` hook is set — an invalid one is destroyed and the next idle / a fresh
|
|
48
|
+
* one is tried). When no usable idle resource exists and the pool is below `max`, it
|
|
49
|
+
* `create`s a new one. At `max` with none idle, the acquire PARKS on a FIFO waiter
|
|
50
|
+
* list until a `release` hands it a resource.
|
|
51
|
+
* - **FIFO handoff (validated).** `release` (on the token) hands the resource to the next
|
|
52
|
+
* parked waiter (oldest first) — the resource stays leased, the lessee just changes —
|
|
53
|
+
* or returns it to idle when no one is waiting. With a waiter parked the resource is
|
|
54
|
+
* re-validated first (the same `validate` hook the idle path uses), so a resource that
|
|
55
|
+
* went invalid WHILE leased (e.g. a terminated worker thread) is destroyed and the
|
|
56
|
+
* waiter is served a fresh/valid one instead — a dead resource is never handed on. The
|
|
57
|
+
* no-waiter path stays synchronous; releasing the same token twice is a no-op (an
|
|
58
|
+
* idempotent token guard).
|
|
59
|
+
* - **`validate` is total.** A `validate` hook that THROWS is treated exactly like one
|
|
60
|
+
* returning `false` — the resource is "not usable", so it is destroyed and replaced —
|
|
61
|
+
* rather than escaping. This holds on both the idle reuse and the FIFO handoff paths, so
|
|
62
|
+
* a throwing validator can never strand a parked waiter on an unhandled rejection.
|
|
63
|
+
* - **Abort-cancellable waiting.** A parked `acquire` given an `AbortSignal` rejects
|
|
64
|
+
* when that signal fires and removes its waiter from the queue — no leaked waiter, so
|
|
65
|
+
* a later `release` still serves the next live waiter. The signal is supplied by the
|
|
66
|
+
* caller (a worker, for example, passes its per-attempt execution signal); the pool adds no abort
|
|
67
|
+
* of its own.
|
|
68
|
+
* - **Counts.** `size` = idle + leased; `idle` = available now; `active` = leased out.
|
|
69
|
+
* - **Teardown.** `clear` destroys every IDLE resource (leased ones keep running);
|
|
70
|
+
* `destroy` destroys ALL resources and rejects any parked waiters. Both await the
|
|
71
|
+
* `destroy` hook.
|
|
72
|
+
* - **Observable (§13).** The owned {@link emitter} ({@link PoolEventMap}) carries the
|
|
73
|
+
* resource lifecycle — `create` / `acquire` / `release` / `destroy` — for fire-and-forget
|
|
74
|
+
* observers. Every event is emitted directly, strictly AFTER the relevant transition —
|
|
75
|
+
* OUTSIDE the `#handoff` / `#serve` await-chain, never across a waiter's resolve; the
|
|
76
|
+
* emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
77
|
+
* option), so a buggy observer can NEVER corrupt the validated FIFO handoff-eviction
|
|
78
|
+
* machinery (it cannot strand a parked waiter or unbalance the lease count). Observation is
|
|
79
|
+
* purely a side-channel.
|
|
80
|
+
* - **De-bloated.** No warm-floor / `min`, no eviction timers — lean.
|
|
81
|
+
*/
|
|
82
|
+
export declare class Pool<T> implements PoolInterface<T> {
|
|
83
|
+
#private;
|
|
84
|
+
constructor(options: PoolOptions<T>);
|
|
85
|
+
get emitter(): EmitterInterface<PoolEventMap>;
|
|
86
|
+
get size(): number;
|
|
87
|
+
get idle(): number;
|
|
88
|
+
get active(): number;
|
|
89
|
+
acquire(signal?: AbortSignal): Promise<PoolToken<T>>;
|
|
90
|
+
clear(): Promise<void>;
|
|
91
|
+
destroy(): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The push observation surface of a {@link PoolInterface} (AGENTS §13) — the resource
|
|
96
|
+
* lifecycle moments a fire-and-forget observer subscribes to.
|
|
97
|
+
*
|
|
98
|
+
* @remarks
|
|
99
|
+
* Pure signals (no `T` payload — `Pool<T>` carries no resource value on its events, so a
|
|
100
|
+
* non-generic map stays lean). Listener isolation is the emitter's (AGENTS §13): every event
|
|
101
|
+
* is emitted directly and a listener throw is routed to the emitter's `error` handler (the
|
|
102
|
+
* `error` option), never onto this map, and sits AFTER the relevant create / acquire /
|
|
103
|
+
* release / destroy transition — so a throwing observer can never corrupt the FIFO
|
|
104
|
+
* handoff-eviction machinery (it cannot strand a parked waiter or unbalance the lease count).
|
|
105
|
+
* Subscribe via `pool.emitter.on(...)`. Declared as a `type` alias (§4.5 — `EventMap` is a
|
|
106
|
+
* `type` kind).
|
|
107
|
+
*/
|
|
108
|
+
export declare type PoolEventMap = {
|
|
109
|
+
/** A fresh resource was created (`create` resolved) and leased. */
|
|
110
|
+
readonly create: readonly [];
|
|
111
|
+
/** A token was handed to a lessee (a reused idle one, a fresh one, or a served waiter). */
|
|
112
|
+
readonly acquire: readonly [];
|
|
113
|
+
/** A leased resource returned to idle (no waiter was parked). */
|
|
114
|
+
readonly release: readonly [];
|
|
115
|
+
/** A resource was destroyed (`clear` / `destroy`, or a failed `validate`). */
|
|
116
|
+
readonly destroy: readonly [];
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A bounded resource pool with idle reuse + FIFO waiting.
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* Exposes a typed {@link emitter} (AGENTS §13) carrying its resource lifecycle moments
|
|
124
|
+
* ({@link PoolEventMap}) for fire-and-forget observers. Emitting is observation-only —
|
|
125
|
+
* every event fires AFTER the relevant create / acquire / release / destroy transition, so a
|
|
126
|
+
* buggy observer can never corrupt the FIFO handoff-eviction machinery: the emitter isolates
|
|
127
|
+
* a listener throw and routes it to its `error` handler (the `error` option), never the pool.
|
|
128
|
+
*/
|
|
129
|
+
export declare interface PoolInterface<T> {
|
|
130
|
+
readonly emitter: EmitterInterface<PoolEventMap>;
|
|
131
|
+
readonly size: number;
|
|
132
|
+
readonly idle: number;
|
|
133
|
+
readonly active: number;
|
|
134
|
+
acquire(signal?: AbortSignal): Promise<PoolToken<T>>;
|
|
135
|
+
clear(): Promise<void>;
|
|
136
|
+
destroy(): Promise<void>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Options for `createPool` — the resource lifecycle hooks.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* - `create` — make a fresh resource; called when no idle resource is reusable and
|
|
144
|
+
* the pool is below `max`. May be async.
|
|
145
|
+
* - `destroy` — tear a resource down when the pool drops it (`clear` / `destroy`, or
|
|
146
|
+
* a failed `validate`); optional and awaited.
|
|
147
|
+
* - `validate` — check an idle resource is still usable before leasing it; an invalid
|
|
148
|
+
* resource is destroyed and replaced. Optional (an absent validator trusts idle).
|
|
149
|
+
* - `max` — the most resources that may exist at once (idle + leased); defaults to
|
|
150
|
+
* unbounded. A surplus `acquire` waits (FIFO) for a `release`.
|
|
151
|
+
* - `on` — the reserved {@link EmitterHooks} key (§8): initial listeners for the pool's
|
|
152
|
+
* {@link PoolEventMap}, wired at construction (e.g. `{ create: () => count() }`).
|
|
153
|
+
*/
|
|
154
|
+
export declare interface PoolOptions<T> {
|
|
155
|
+
readonly on?: EmitterHooks<PoolEventMap>;
|
|
156
|
+
/** The emitter's listener-error handler (AGENTS §13) — a listener throw routes here, not to a domain event. */
|
|
157
|
+
readonly error?: EmitterErrorHandler;
|
|
158
|
+
readonly create: () => Promise<T> | T;
|
|
159
|
+
readonly destroy?: (value: T) => Promise<void> | void;
|
|
160
|
+
readonly validate?: (value: T) => Promise<boolean> | boolean;
|
|
161
|
+
readonly max?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A leased resource from a {@link PoolInterface} — `value` is the live resource and
|
|
166
|
+
* `release()` returns it to the pool for reuse (or hands it to the next waiter).
|
|
167
|
+
*/
|
|
168
|
+
export declare interface PoolToken<T> {
|
|
169
|
+
/** The leased resource. */
|
|
170
|
+
readonly value: T;
|
|
171
|
+
/** Return the resource to the pool; calling more than once is a no-op. */
|
|
172
|
+
release(): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A parked acquirer on a {@link PoolInterface}'s FIFO waiter list — its promise resolvers
|
|
177
|
+
* plus the cleanup that detaches its abort listener.
|
|
178
|
+
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* Held only inside the {@link PoolInterface} engine (a resource at `max` parks the acquirer
|
|
181
|
+
* here until a `release` hands it a token); not part of the public call surface, but
|
|
182
|
+
* centralized here per AGENTS §5. `resolve` hands the waiter its leased {@link PoolToken};
|
|
183
|
+
* `reject` fails its `acquire` (a teardown or an aborted wait); `clear` detaches the abort
|
|
184
|
+
* listener so a settled waiter leaks nothing.
|
|
185
|
+
*
|
|
186
|
+
* @typeParam T - The resource the pool leases
|
|
187
|
+
*/
|
|
188
|
+
export declare interface PoolWaiter<T> {
|
|
189
|
+
readonly resolve: (token: PoolToken<T>) => void;
|
|
190
|
+
readonly reject: (error: unknown) => void;
|
|
191
|
+
clear(): void;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { }
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Emitter } from "@orkestrel/emitter";
|
|
2
|
+
//#region src/core/Pool.ts
|
|
3
|
+
/**
|
|
4
|
+
* A bounded resource pool with idle reuse + FIFO waiting.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* - **Idle reuse.** `acquire` first takes an idle resource (validating it when a
|
|
8
|
+
* `validate` hook is set — an invalid one is destroyed and the next idle / a fresh
|
|
9
|
+
* one is tried). When no usable idle resource exists and the pool is below `max`, it
|
|
10
|
+
* `create`s a new one. At `max` with none idle, the acquire PARKS on a FIFO waiter
|
|
11
|
+
* list until a `release` hands it a resource.
|
|
12
|
+
* - **FIFO handoff (validated).** `release` (on the token) hands the resource to the next
|
|
13
|
+
* parked waiter (oldest first) — the resource stays leased, the lessee just changes —
|
|
14
|
+
* or returns it to idle when no one is waiting. With a waiter parked the resource is
|
|
15
|
+
* re-validated first (the same `validate` hook the idle path uses), so a resource that
|
|
16
|
+
* went invalid WHILE leased (e.g. a terminated worker thread) is destroyed and the
|
|
17
|
+
* waiter is served a fresh/valid one instead — a dead resource is never handed on. The
|
|
18
|
+
* no-waiter path stays synchronous; releasing the same token twice is a no-op (an
|
|
19
|
+
* idempotent token guard).
|
|
20
|
+
* - **`validate` is total.** A `validate` hook that THROWS is treated exactly like one
|
|
21
|
+
* returning `false` — the resource is "not usable", so it is destroyed and replaced —
|
|
22
|
+
* rather than escaping. This holds on both the idle reuse and the FIFO handoff paths, so
|
|
23
|
+
* a throwing validator can never strand a parked waiter on an unhandled rejection.
|
|
24
|
+
* - **Abort-cancellable waiting.** A parked `acquire` given an `AbortSignal` rejects
|
|
25
|
+
* when that signal fires and removes its waiter from the queue — no leaked waiter, so
|
|
26
|
+
* a later `release` still serves the next live waiter. The signal is supplied by the
|
|
27
|
+
* caller (a worker, for example, passes its per-attempt execution signal); the pool adds no abort
|
|
28
|
+
* of its own.
|
|
29
|
+
* - **Counts.** `size` = idle + leased; `idle` = available now; `active` = leased out.
|
|
30
|
+
* - **Teardown.** `clear` destroys every IDLE resource (leased ones keep running);
|
|
31
|
+
* `destroy` destroys ALL resources and rejects any parked waiters. Both await the
|
|
32
|
+
* `destroy` hook.
|
|
33
|
+
* - **Observable (§13).** The owned {@link emitter} ({@link PoolEventMap}) carries the
|
|
34
|
+
* resource lifecycle — `create` / `acquire` / `release` / `destroy` — for fire-and-forget
|
|
35
|
+
* observers. Every event is emitted directly, strictly AFTER the relevant transition —
|
|
36
|
+
* OUTSIDE the `#handoff` / `#serve` await-chain, never across a waiter's resolve; the
|
|
37
|
+
* emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
38
|
+
* option), so a buggy observer can NEVER corrupt the validated FIFO handoff-eviction
|
|
39
|
+
* machinery (it cannot strand a parked waiter or unbalance the lease count). Observation is
|
|
40
|
+
* purely a side-channel.
|
|
41
|
+
* - **De-bloated.** No warm-floor / `min`, no eviction timers — lean.
|
|
42
|
+
*/
|
|
43
|
+
var Pool = class {
|
|
44
|
+
#create;
|
|
45
|
+
#destroy;
|
|
46
|
+
#validate;
|
|
47
|
+
#max;
|
|
48
|
+
#emitter;
|
|
49
|
+
#idle = [];
|
|
50
|
+
#waiters = [];
|
|
51
|
+
#active = 0;
|
|
52
|
+
#destroyed = false;
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.#create = options.create;
|
|
55
|
+
this.#destroy = options.destroy;
|
|
56
|
+
this.#validate = options.validate;
|
|
57
|
+
this.#max = Math.max(1, options.max ?? Number.POSITIVE_INFINITY);
|
|
58
|
+
this.#emitter = new Emitter({
|
|
59
|
+
on: options?.on,
|
|
60
|
+
error: options?.error
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
get emitter() {
|
|
64
|
+
return this.#emitter;
|
|
65
|
+
}
|
|
66
|
+
get size() {
|
|
67
|
+
return this.#idle.length + this.#active;
|
|
68
|
+
}
|
|
69
|
+
get idle() {
|
|
70
|
+
return this.#idle.length;
|
|
71
|
+
}
|
|
72
|
+
get active() {
|
|
73
|
+
return this.#active;
|
|
74
|
+
}
|
|
75
|
+
async acquire(signal) {
|
|
76
|
+
if (this.#destroyed) throw new Error("pool is destroyed");
|
|
77
|
+
if (signal?.aborted === true) throw signal.reason;
|
|
78
|
+
const reused = await this.#reuse();
|
|
79
|
+
if (reused !== void 0) {
|
|
80
|
+
this.#emitter.emit("acquire");
|
|
81
|
+
return reused;
|
|
82
|
+
}
|
|
83
|
+
if (this.size < this.#max) {
|
|
84
|
+
const grown = await this.#grow();
|
|
85
|
+
this.#emitter.emit("acquire");
|
|
86
|
+
return grown;
|
|
87
|
+
}
|
|
88
|
+
return await this.#wait(signal);
|
|
89
|
+
}
|
|
90
|
+
async clear() {
|
|
91
|
+
const resources = this.#idle.splice(0);
|
|
92
|
+
await Promise.all(resources.map((resource) => this.#release(resource)));
|
|
93
|
+
}
|
|
94
|
+
async destroy() {
|
|
95
|
+
if (this.#destroyed) return;
|
|
96
|
+
this.#destroyed = true;
|
|
97
|
+
const waiters = this.#waiters.splice(0);
|
|
98
|
+
const error = /* @__PURE__ */ new Error("pool is destroyed");
|
|
99
|
+
for (const waiter of waiters) {
|
|
100
|
+
waiter.clear();
|
|
101
|
+
waiter.reject(error);
|
|
102
|
+
}
|
|
103
|
+
const resources = this.#idle.splice(0);
|
|
104
|
+
await Promise.all(resources.map((resource) => this.#release(resource)));
|
|
105
|
+
}
|
|
106
|
+
async #reuse() {
|
|
107
|
+
while (this.#idle.length > 0) {
|
|
108
|
+
const resource = this.#idle.shift();
|
|
109
|
+
if (resource === void 0) continue;
|
|
110
|
+
if (await this.#valid(resource)) {
|
|
111
|
+
this.#active += 1;
|
|
112
|
+
return this.#token(resource);
|
|
113
|
+
}
|
|
114
|
+
await this.#release(resource);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async #grow() {
|
|
118
|
+
this.#active += 1;
|
|
119
|
+
let resource;
|
|
120
|
+
try {
|
|
121
|
+
resource = await this.#create();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.#active -= 1;
|
|
124
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
125
|
+
}
|
|
126
|
+
if (this.#destroyed) {
|
|
127
|
+
this.#active -= 1;
|
|
128
|
+
await this.#release(resource);
|
|
129
|
+
throw new Error("pool is destroyed");
|
|
130
|
+
}
|
|
131
|
+
this.#emitter.emit("create");
|
|
132
|
+
return this.#token(resource);
|
|
133
|
+
}
|
|
134
|
+
#wait(signal) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const waiter = {
|
|
137
|
+
resolve,
|
|
138
|
+
reject,
|
|
139
|
+
clear: () => {}
|
|
140
|
+
};
|
|
141
|
+
this.#waiters.push(waiter);
|
|
142
|
+
if (signal !== void 0) {
|
|
143
|
+
const onAbort = () => {
|
|
144
|
+
const index = this.#waiters.indexOf(waiter);
|
|
145
|
+
if (index >= 0) this.#waiters.splice(index, 1);
|
|
146
|
+
reject(signal.reason);
|
|
147
|
+
};
|
|
148
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
149
|
+
waiter.clear = () => signal.removeEventListener("abort", onAbort);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
#token(resource) {
|
|
154
|
+
let released = false;
|
|
155
|
+
return {
|
|
156
|
+
value: resource,
|
|
157
|
+
release: () => {
|
|
158
|
+
if (released) return;
|
|
159
|
+
released = true;
|
|
160
|
+
this.#return(resource);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
#return(resource) {
|
|
165
|
+
if (this.#waiters.length === 0) {
|
|
166
|
+
this.#active -= 1;
|
|
167
|
+
if (this.#destroyed) {
|
|
168
|
+
this.#release(resource);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.#idle.push(resource);
|
|
172
|
+
this.#emitter.emit("release");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.#handoff(resource);
|
|
176
|
+
}
|
|
177
|
+
async #handoff(resource) {
|
|
178
|
+
if (await this.#valid(resource)) {
|
|
179
|
+
this.#serve(resource);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this.#release(resource);
|
|
183
|
+
let replacement;
|
|
184
|
+
try {
|
|
185
|
+
replacement = await this.#create();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.#active -= 1;
|
|
188
|
+
const waiter = this.#waiters.shift();
|
|
189
|
+
waiter?.clear();
|
|
190
|
+
waiter?.reject(error instanceof Error ? error : new Error(String(error)));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.#emitter.emit("create");
|
|
194
|
+
this.#serve(replacement);
|
|
195
|
+
}
|
|
196
|
+
#serve(resource) {
|
|
197
|
+
const waiter = this.#destroyed ? void 0 : this.#waiters.shift();
|
|
198
|
+
if (waiter !== void 0) {
|
|
199
|
+
waiter.clear();
|
|
200
|
+
waiter.resolve(this.#token(resource));
|
|
201
|
+
this.#emitter.emit("acquire");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.#active -= 1;
|
|
205
|
+
if (this.#destroyed) {
|
|
206
|
+
this.#release(resource);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.#idle.push(resource);
|
|
210
|
+
this.#emitter.emit("release");
|
|
211
|
+
}
|
|
212
|
+
async #valid(resource) {
|
|
213
|
+
if (this.#validate === void 0) return true;
|
|
214
|
+
try {
|
|
215
|
+
return await this.#validate(resource);
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async #release(resource) {
|
|
221
|
+
if (this.#destroy === void 0) {
|
|
222
|
+
this.#emitter.emit("destroy");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
await this.#destroy(resource);
|
|
227
|
+
} catch {}
|
|
228
|
+
this.#emitter.emit("destroy");
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/core/factories.ts
|
|
233
|
+
/**
|
|
234
|
+
* Create a bounded resource pool with idle reuse and FIFO waiting — `acquire` leases a
|
|
235
|
+
* resource (reusing a validated idle one, growing up to `max`, or parking until a
|
|
236
|
+
* `release` frees one) and the returned token's `release` returns it for reuse.
|
|
237
|
+
*
|
|
238
|
+
* @remarks
|
|
239
|
+
* A parked `acquire` given an `AbortSignal` rejects + de-queues itself when the signal
|
|
240
|
+
* fires (no leaked waiter). `clear` destroys idle resources (leased ones keep running);
|
|
241
|
+
* `destroy` destroys all and rejects waiters. The pool is lean — no warm-floor (`min`),
|
|
242
|
+
* no eviction timers — and observable (§13): a typed `emitter` surfaces
|
|
243
|
+
* `create` / `acquire` / `release` / `destroy`.
|
|
244
|
+
*
|
|
245
|
+
* @typeParam T - The pooled resource type
|
|
246
|
+
* @param options - The `create` hook plus optional `destroy` / `validate` / `max`
|
|
247
|
+
* @returns A working {@link PoolInterface}
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* import { createPool } from '@src/core'
|
|
252
|
+
*
|
|
253
|
+
* const pool = createPool<Connection>({
|
|
254
|
+
* create: () => connect(),
|
|
255
|
+
* destroy: (connection) => connection.close(),
|
|
256
|
+
* validate: (connection) => connection.alive,
|
|
257
|
+
* max: 8,
|
|
258
|
+
* })
|
|
259
|
+
*
|
|
260
|
+
* const token = await pool.acquire()
|
|
261
|
+
* try {
|
|
262
|
+
* await token.value.query('select 1')
|
|
263
|
+
* } finally {
|
|
264
|
+
* token.release()
|
|
265
|
+
* }
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
function createPool(options) {
|
|
269
|
+
return new Pool(options);
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
export { Pool, createPool };
|
|
273
|
+
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#create","#destroy","#validate","#max","#emitter","#idle","#waiters","#active","#destroyed","#reuse","#grow","#wait","#release","#valid","#token","#return","#handoff","#serve"],"sources":["../../../src/core/Pool.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { PoolEventMap, PoolInterface, PoolOptions, PoolToken, PoolWaiter } from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\n\n/**\n * A bounded resource pool with idle reuse + FIFO waiting.\n *\n * @remarks\n * - **Idle reuse.** `acquire` first takes an idle resource (validating it when a\n * `validate` hook is set — an invalid one is destroyed and the next idle / a fresh\n * one is tried). When no usable idle resource exists and the pool is below `max`, it\n * `create`s a new one. At `max` with none idle, the acquire PARKS on a FIFO waiter\n * list until a `release` hands it a resource.\n * - **FIFO handoff (validated).** `release` (on the token) hands the resource to the next\n * parked waiter (oldest first) — the resource stays leased, the lessee just changes —\n * or returns it to idle when no one is waiting. With a waiter parked the resource is\n * re-validated first (the same `validate` hook the idle path uses), so a resource that\n * went invalid WHILE leased (e.g. a terminated worker thread) is destroyed and the\n * waiter is served a fresh/valid one instead — a dead resource is never handed on. The\n * no-waiter path stays synchronous; releasing the same token twice is a no-op (an\n * idempotent token guard).\n * - **`validate` is total.** A `validate` hook that THROWS is treated exactly like one\n * returning `false` — the resource is \"not usable\", so it is destroyed and replaced —\n * rather than escaping. This holds on both the idle reuse and the FIFO handoff paths, so\n * a throwing validator can never strand a parked waiter on an unhandled rejection.\n * - **Abort-cancellable waiting.** A parked `acquire` given an `AbortSignal` rejects\n * when that signal fires and removes its waiter from the queue — no leaked waiter, so\n * a later `release` still serves the next live waiter. The signal is supplied by the\n * caller (a worker, for example, passes its per-attempt execution signal); the pool adds no abort\n * of its own.\n * - **Counts.** `size` = idle + leased; `idle` = available now; `active` = leased out.\n * - **Teardown.** `clear` destroys every IDLE resource (leased ones keep running);\n * `destroy` destroys ALL resources and rejects any parked waiters. Both await the\n * `destroy` hook.\n * - **Observable (§13).** The owned {@link emitter} ({@link PoolEventMap}) carries the\n * resource lifecycle — `create` / `acquire` / `release` / `destroy` — for fire-and-forget\n * observers. Every event is emitted directly, strictly AFTER the relevant transition —\n * OUTSIDE the `#handoff` / `#serve` await-chain, never across a waiter's resolve; the\n * emitter isolates a listener throw and routes it to its `error` handler (the `error`\n * option), so a buggy observer can NEVER corrupt the validated FIFO handoff-eviction\n * machinery (it cannot strand a parked waiter or unbalance the lease count). Observation is\n * purely a side-channel.\n * - **De-bloated.** No warm-floor / `min`, no eviction timers — lean.\n */\nexport class Pool<T> implements PoolInterface<T> {\n\treadonly #create: () => Promise<T> | T\n\treadonly #destroy: ((value: T) => Promise<void> | void) | undefined\n\treadonly #validate: ((value: T) => Promise<boolean> | boolean) | undefined\n\treadonly #max: number\n\t// The PUSH observation surface (§13) — owned, never inherited. The emitter isolates a\n\t// listener throw (routing it to the `error` handler), so it can never escape into the\n\t// validated FIFO handoff-eviction path.\n\treadonly #emitter: Emitter<PoolEventMap>\n\n\treadonly #idle: T[] = []\n\treadonly #waiters: PoolWaiter<T>[] = []\n\t#active = 0\n\t#destroyed = false\n\n\tconstructor(options: PoolOptions<T>) {\n\t\tthis.#create = options.create\n\t\tthis.#destroy = options.destroy\n\t\tthis.#validate = options.validate\n\t\tthis.#max = Math.max(1, options.max ?? Number.POSITIVE_INFINITY)\n\t\tthis.#emitter = new Emitter<PoolEventMap>({ on: options?.on, error: options?.error })\n\t}\n\n\tget emitter(): EmitterInterface<PoolEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget size(): number {\n\t\treturn this.#idle.length + this.#active\n\t}\n\n\tget idle(): number {\n\t\treturn this.#idle.length\n\t}\n\n\tget active(): number {\n\t\treturn this.#active\n\t}\n\n\tasync acquire(signal?: AbortSignal): Promise<PoolToken<T>> {\n\t\tif (this.#destroyed) throw new Error('pool is destroyed')\n\t\tif (signal?.aborted === true) throw signal.reason\n\t\t// Reuse a validated idle resource if one is available.\n\t\tconst reused = await this.#reuse()\n\t\tif (reused !== undefined) {\n\t\t\t// Observe the lease — AFTER `#reuse` resolved the token (the resource is already\n\t\t\t// leased; emit only OBSERVES it).\n\t\t\tthis.#emitter.emit('acquire')\n\t\t\treturn reused\n\t\t}\n\t\t// Otherwise grow the pool when below the cap.\n\t\tif (this.size < this.#max) {\n\t\t\tconst grown = await this.#grow()\n\t\t\t// Observe the lease — AFTER `#grow` resolved the fresh token.\n\t\t\tthis.#emitter.emit('acquire')\n\t\t\treturn grown\n\t\t}\n\t\t// At capacity with nothing idle — park until a release hands us a resource. The\n\t\t// `acquire` for THIS lease is emitted by `#serve` once a `release` hands it a resource\n\t\t// (after `waiter.resolve`), NOT here — so a served waiter emits `acquire` exactly once.\n\t\treturn await this.#wait(signal)\n\t}\n\n\tasync clear(): Promise<void> {\n\t\tconst resources = this.#idle.splice(0)\n\t\tawait Promise.all(resources.map((resource) => this.#release(resource)))\n\t}\n\n\tasync destroy(): Promise<void> {\n\t\tif (this.#destroyed) return\n\t\tthis.#destroyed = true\n\t\tconst waiters = this.#waiters.splice(0)\n\t\tconst error = new Error('pool is destroyed')\n\t\tfor (const waiter of waiters) {\n\t\t\twaiter.clear()\n\t\t\twaiter.reject(error)\n\t\t}\n\t\tconst resources = this.#idle.splice(0)\n\t\tawait Promise.all(resources.map((resource) => this.#release(resource)))\n\t}\n\n\t// Take + validate the next idle resource; destroy + skip invalid ones. Returns a\n\t// token for the first valid idle resource, or `undefined` when none is usable.\n\tasync #reuse(): Promise<PoolToken<T> | undefined> {\n\t\twhile (this.#idle.length > 0) {\n\t\t\tconst resource = this.#idle.shift()\n\t\t\tif (resource === undefined) continue\n\t\t\tif (await this.#valid(resource)) {\n\t\t\t\tthis.#active += 1\n\t\t\t\treturn this.#token(resource)\n\t\t\t}\n\t\t\tawait this.#release(resource)\n\t\t}\n\t\treturn undefined\n\t}\n\n\t// Create a fresh resource and lease it. Reserve the active slot before awaiting\n\t// `create` so a concurrent acquire sees the pool at capacity (no overshoot of `max`).\n\tasync #grow(): Promise<PoolToken<T>> {\n\t\tthis.#active += 1\n\t\tlet resource: T\n\t\ttry {\n\t\t\tresource = await this.#create()\n\t\t} catch (error: unknown) {\n\t\t\tthis.#active -= 1\n\t\t\tthrow error instanceof Error ? error : new Error(String(error))\n\t\t}\n\t\t// The pool may have been destroyed during `create` — never hand a live resource into\n\t\t// a torn-down pool. Free the active slot, destroy the just-created resource, and throw.\n\t\tif (this.#destroyed) {\n\t\t\tthis.#active -= 1\n\t\t\tawait this.#release(resource)\n\t\t\tthrow new Error('pool is destroyed')\n\t\t}\n\t\t// Observe the fresh resource — AFTER `create` resolved and the not-destroyed re-check\n\t\t// passed (the resource is here to stay), BEFORE the token is built.\n\t\tthis.#emitter.emit('create')\n\t\treturn this.#token(resource)\n\t}\n\n\t// Park a resolver on the FIFO waiter list until a release hands it a resource; an\n\t// abort on `signal` rejects the acquire and removes its waiter (no leak).\n\t#wait(signal: AbortSignal | undefined): Promise<PoolToken<T>> {\n\t\treturn new Promise<PoolToken<T>>((resolve, reject) => {\n\t\t\tconst waiter: PoolWaiter<T> = { resolve, reject, clear: () => {} }\n\t\t\tthis.#waiters.push(waiter)\n\t\t\tif (signal !== undefined) {\n\t\t\t\tconst onAbort = (): void => {\n\t\t\t\t\tconst index = this.#waiters.indexOf(waiter)\n\t\t\t\t\tif (index >= 0) this.#waiters.splice(index, 1)\n\t\t\t\t\treject(signal.reason)\n\t\t\t\t}\n\t\t\t\tsignal.addEventListener('abort', onAbort, { once: true })\n\t\t\t\twaiter.clear = (): void => signal.removeEventListener('abort', onAbort)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Build an idempotent token; its `release` returns the resource exactly once.\n\t#token(resource: T): PoolToken<T> {\n\t\tlet released = false\n\t\treturn {\n\t\t\tvalue: resource,\n\t\t\trelease: (): void => {\n\t\t\t\tif (released) return\n\t\t\t\treleased = true\n\t\t\t\tthis.#return(resource)\n\t\t\t},\n\t\t}\n\t}\n\n\t// Return a leased resource. No waiter parked → synchronously drop it to idle (or destroy\n\t// it if torn down), `#active -= 1`. A waiter IS parked → hand off asynchronously, after\n\t// re-validating the resource so an invalid one (e.g. a terminated thread) is never served.\n\t#return(resource: T): void {\n\t\tif (this.#waiters.length === 0) {\n\t\t\tthis.#active -= 1\n\t\t\tif (this.#destroyed) {\n\t\t\t\tvoid this.#release(resource)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.#idle.push(resource)\n\t\t\t// Observe the resource dropping to idle — AFTER `#active` was balanced down and it\n\t\t\t// is back on the idle list (the synchronous no-waiter path). A waiter-handoff\n\t\t\t// instead keeps the resource LEASED, so it emits no `release` here.\n\t\t\tthis.#emitter.emit('release')\n\t\t\treturn\n\t\t}\n\t\tvoid this.#handoff(resource)\n\t}\n\n\t// Hand a released resource to the oldest parked waiter, re-validating it first. A VALID\n\t// resource is served as-is (it stays leased — `#active` unchanged). An INVALID one is\n\t// destroyed and the waiter is served a fresh resource that REUSES the dead one's `#active`\n\t// slot (the count never dips below the lease, so a concurrent acquire can't overshoot\n\t// `max`). The waiter is re-shifted after every `await` (the queue can change underneath\n\t// us), and the pool is re-checked for teardown — so no waiter is leaked or double-settled\n\t// and no resource is handed into a destroyed pool.\n\tasync #handoff(resource: T): Promise<void> {\n\t\t// VALID release → serve the released resource directly (the common, unchanged path).\n\t\tif (await this.#valid(resource)) {\n\t\t\tthis.#serve(resource)\n\t\t\treturn\n\t\t}\n\t\t// INVALID release → destroy it and replace it, holding its `#active` slot steady.\n\t\tvoid this.#release(resource)\n\t\tlet replacement: T\n\t\ttry {\n\t\t\treplacement = await this.#create()\n\t\t} catch (error: unknown) {\n\t\t\t// The dead resource cannot be replaced — free its slot and reject the oldest waiter\n\t\t\t// (it can't be served), or drop the error if the queue raced empty.\n\t\t\tthis.#active -= 1\n\t\t\tconst waiter = this.#waiters.shift()\n\t\t\twaiter?.clear()\n\t\t\twaiter?.reject(error instanceof Error ? error : new Error(String(error)))\n\t\t\treturn\n\t\t}\n\t\t// Observe the fresh replacement — AFTER `create` resolved, BEFORE it is served.\n\t\tthis.#emitter.emit('create')\n\t\tthis.#serve(replacement)\n\t}\n\n\t// Hand a validated resource (the released one, or its fresh replacement) to the oldest\n\t// live waiter, holding its `#active` slot. If the queue raced empty across the awaits, the\n\t// resource is idle (or destroyed when torn down) and its slot is freed.\n\t#serve(resource: T): void {\n\t\tconst waiter = this.#destroyed ? undefined : this.#waiters.shift()\n\t\tif (waiter !== undefined) {\n\t\t\twaiter.clear()\n\t\t\twaiter.resolve(this.#token(resource))\n\t\t\t// Observe the served-waiter lease — strictly AFTER `waiter.resolve(...)` (the token\n\t\t\t// is already handed off; the emit only OBSERVES it and cannot sit across the resolve\n\t\t\t// or perturb the served acquirer's continuation). This is the `acquire` for a parked\n\t\t\t// `acquire`, so the public `acquire` deliberately does NOT emit on its `#wait` branch.\n\t\t\tthis.#emitter.emit('acquire')\n\t\t\treturn\n\t\t}\n\t\tthis.#active -= 1\n\t\tif (this.#destroyed) {\n\t\t\tvoid this.#release(resource)\n\t\t\treturn\n\t\t}\n\t\tthis.#idle.push(resource)\n\t\t// The waiter queue raced empty across the handoff awaits — the resource lands back on\n\t\t// idle instead of being leased. Observe that as a `release` (the return-to-idle that\n\t\t// `#return` would have emitted had no waiter been parked when the token was released).\n\t\tthis.#emitter.emit('release')\n\t}\n\n\t// Is this resource usable? No validator trusts it; otherwise run the hook. A validator\n\t// that THROWS is treated exactly like one returning `false` — the resource is \"not\n\t// usable\", so it is destroyed + replaced rather than escaping as an unhandled rejection\n\t// that would strand a parked waiter on the handoff path (the same total-function spirit\n\t// as a guard, AGENTS §12 / §14). The single gate for both the idle (`#reuse`) and the\n\t// handoff (`#handoff`) validation, so the two paths can never diverge.\n\tasync #valid(resource: T): Promise<boolean> {\n\t\tif (this.#validate === undefined) return true\n\t\ttry {\n\t\t\treturn await this.#validate(resource)\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Destroy one resource via the optional hook, swallowing destruction failures. Observe the\n\t// drop with a `destroy` event AFTER the hook ran (or after the no-hook drop) — the resource\n\t// is gone from the pool either way; a swallowed `destroy`-hook failure still emits (the pool\n\t// abandoned it). The emit is the LAST thing here, strictly after the teardown transition.\n\tasync #release(resource: T): Promise<void> {\n\t\tif (this.#destroy === undefined) {\n\t\t\tthis.#emitter.emit('destroy')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait this.#destroy(resource)\n\t\t} catch {\n\t\t\t// A destroy failure abandons the resource — there is nothing to recover.\n\t\t}\n\t\tthis.#emitter.emit('destroy')\n\t}\n}\n","import type { PoolInterface, PoolOptions } from './types.js'\nimport { Pool } from './Pool.js'\n\n/**\n * Create a bounded resource pool with idle reuse and FIFO waiting — `acquire` leases a\n * resource (reusing a validated idle one, growing up to `max`, or parking until a\n * `release` frees one) and the returned token's `release` returns it for reuse.\n *\n * @remarks\n * A parked `acquire` given an `AbortSignal` rejects + de-queues itself when the signal\n * fires (no leaked waiter). `clear` destroys idle resources (leased ones keep running);\n * `destroy` destroys all and rejects waiters. The pool is lean — no warm-floor (`min`),\n * no eviction timers — and observable (§13): a typed `emitter` surfaces\n * `create` / `acquire` / `release` / `destroy`.\n *\n * @typeParam T - The pooled resource type\n * @param options - The `create` hook plus optional `destroy` / `validate` / `max`\n * @returns A working {@link PoolInterface}\n *\n * @example\n * ```ts\n * import { createPool } from '@src/core'\n *\n * const pool = createPool<Connection>({\n * \tcreate: () => connect(),\n * \tdestroy: (connection) => connection.close(),\n * \tvalidate: (connection) => connection.alive,\n * \tmax: 8,\n * })\n *\n * const token = await pool.acquire()\n * try {\n * \tawait token.value.query('select 1')\n * } finally {\n * \ttoken.release()\n * }\n * ```\n */\nexport function createPool<T>(options: PoolOptions<T>): PoolInterface<T> {\n\treturn new Pool(options)\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orkestrel/pool",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A bounded, typed resource pool — idle reuse, max backpressure, FIFO abort-cancellable waiting, and an observable lifecycle emitter. Part of the @orkestrel line.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"connection-pool",
|
|
7
|
+
"fifo",
|
|
8
|
+
"pool",
|
|
9
|
+
"resource-pool",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/orkestrel/pool#readme",
|
|
13
|
+
"bugs": "https://github.com/orkestrel/pool/issues",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/orkestrel/pool.git"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"main": "./dist/src/core/index.cjs",
|
|
26
|
+
"module": "./dist/src/core/index.js",
|
|
27
|
+
"types": "./dist/src/core/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/src/core/index.d.ts",
|
|
32
|
+
"default": "./dist/src/core/index.js"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/src/core/index.d.cts",
|
|
36
|
+
"default": "./dist/src/core/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"clean": "node -e \"try{require('node:fs').rmSync('dist',{recursive:true,force:true})}catch{}\"",
|
|
46
|
+
"copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
|
|
47
|
+
"tmp:txt": "node -e \"const fs=require('node:fs'),p=require('node:path');function walk(d){for(const e of fs.readdirSync(d,{withFileTypes:true})){const f=p.join(d,e.name);if(e.isDirectory()){walk(f)}else if(!e.name.endsWith('.md')&&!e.name.endsWith('.txt')){const t=f+'.txt';if(!fs.existsSync(t)){fs.renameSync(f,t)}else{console.warn('Skipping '+f+' — target exists: '+t)}}}}try{walk('tmp')}catch(e){if(e.code!=='ENOENT')throw e}\"",
|
|
48
|
+
"lint": "oxlint --config .oxlintrc.json --fix .",
|
|
49
|
+
"check": "tsc --noEmit --project tsconfig.json",
|
|
50
|
+
"check:src": "npm run check:src:core",
|
|
51
|
+
"check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
|
|
52
|
+
"format": "oxfmt --config .oxfmtrc.json --write .",
|
|
53
|
+
"format:check": "oxfmt --config .oxfmtrc.json --check .",
|
|
54
|
+
"lint:check": "oxlint --config .oxlintrc.json .",
|
|
55
|
+
"test": "npm run test:src && npm run test:guides",
|
|
56
|
+
"test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
|
|
57
|
+
"test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
|
|
58
|
+
"test:guides": "vitest run --config vite.config.ts --reporter=dot --project guides",
|
|
59
|
+
"build": "npm run clean && npm run build:src",
|
|
60
|
+
"build:src": "npm run build:src:core",
|
|
61
|
+
"build:src:core": "vite build --config configs/src/vite.core.config.ts && npm run copy dist/src/core/index.d.ts dist/src/core/index.d.cts",
|
|
62
|
+
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run check:src && npm run build && npm test"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@orkestrel/emitter": "^0.0.1"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@microsoft/api-extractor": "^7.58.9",
|
|
69
|
+
"@orkestrel/guide": "^0.0.1",
|
|
70
|
+
"@types/node": "^26.1.1",
|
|
71
|
+
"oxfmt": "^0.58.0",
|
|
72
|
+
"oxlint": "^1.73.0",
|
|
73
|
+
"typescript": "^6.0.3",
|
|
74
|
+
"vite": "^8.1.4",
|
|
75
|
+
"vite-plugin-dts": "^5.0.3",
|
|
76
|
+
"vitest": "^4.1.10"
|
|
77
|
+
},
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=24"
|
|
80
|
+
}
|
|
81
|
+
}
|