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