@orkestrel/worker 0.0.14 → 0.0.15
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 +15 -6
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +13 -6
- package/dist/src/core/index.d.ts +13 -6
- package/dist/src/core/index.js +15 -6
- package/dist/src/core/index.js.map +1 -1
- package/package.json +10 -10
package/dist/src/core/index.cjs
CHANGED
|
@@ -14,11 +14,15 @@ let _orkestrel_queue = require("@orkestrel/queue");
|
|
|
14
14
|
* timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
|
|
15
15
|
* - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive
|
|
16
16
|
* safe integer after caller options are captured once. Only `undefined` defaults
|
|
17
|
-
* `concurrency` to `1
|
|
18
|
-
*
|
|
17
|
+
* `concurrency` to `1`; pool `max` defaults to concurrency only when `max` and `min` are
|
|
18
|
+
* both absent. With `min`, Pool defaults `max` to `min`, requires their equality and a
|
|
19
|
+
* `restarts` bound, and owns validation. Runtime `null` reaches the owning validator.
|
|
20
|
+
* The queue validates before the pool option is read; every declared pool member
|
|
19
21
|
* is then captured once by direct access, preserving inherited and non-enumerable structural
|
|
20
22
|
* options. At most one resource exists per in-flight job by default, and idle resources are
|
|
21
|
-
* reused across jobs.
|
|
23
|
+
* reused across jobs. A configured floor starts warming at construction, independently of
|
|
24
|
+
* queue concurrency, and can retain more resources than jobs in flight. A spent floor's
|
|
25
|
+
* startup failure reaches jobs through acquire; startup rejection is observed internally.
|
|
22
26
|
* - **Acquire over the attempt signal.** Each job acquires using the attempt's
|
|
23
27
|
* `context.signal`, so an `abort` / `timeout` while waiting for a resource rejects
|
|
24
28
|
* the acquire — the Queue then handles retry / rejection, and there is no token to
|
|
@@ -63,16 +67,20 @@ var Worker = class {
|
|
|
63
67
|
...timeout !== void 0 ? { timeout } : {},
|
|
64
68
|
...store !== void 0 ? { store } : {}
|
|
65
69
|
});
|
|
66
|
-
const { max, on: poolOn, error: poolError, create, destroy, validate } = options.pool;
|
|
70
|
+
const { max, min, restarts, watch, on: poolOn, error: poolError, create, destroy, validate } = options.pool;
|
|
67
71
|
this.#pool = new _orkestrel_pool.Pool({
|
|
68
72
|
create,
|
|
69
|
-
max
|
|
73
|
+
...max === void 0 ? min === void 0 ? { max: concurrency } : {} : { max },
|
|
74
|
+
...min !== void 0 ? { min } : {},
|
|
75
|
+
...restarts !== void 0 ? { restarts } : {},
|
|
76
|
+
...watch !== void 0 ? { watch } : {},
|
|
70
77
|
...poolOn !== void 0 ? { on: poolOn } : {},
|
|
71
78
|
...poolError !== void 0 ? { error: poolError } : {},
|
|
72
79
|
...destroy !== void 0 ? { destroy } : {},
|
|
73
80
|
...validate !== void 0 ? { validate } : {}
|
|
74
81
|
});
|
|
75
82
|
this.#bridge();
|
|
83
|
+
this.#pool.start().catch(() => {});
|
|
76
84
|
}
|
|
77
85
|
get emitter() {
|
|
78
86
|
return this.#emitter;
|
|
@@ -165,7 +173,8 @@ var Worker = class {
|
|
|
165
173
|
*
|
|
166
174
|
* @remarks
|
|
167
175
|
* Bounded concurrency, retries, and the per-attempt timeout and abort are the queue's.
|
|
168
|
-
*
|
|
176
|
+
* When neither pool `max` nor `min` is given, `max` defaults to `concurrency`. With `min`,
|
|
177
|
+
* Pool owns the capacity defaults and validation, and the worker starts warming the floor.
|
|
169
178
|
* Resources are reused across jobs. A handler that throws still releases its resource (the
|
|
170
179
|
* acquire/release pair brackets the call in a `finally`), so a later job reuses it. The
|
|
171
180
|
* lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["Emitter","Queue","Pool"],"sources":["../../../src/core/Worker.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { QueueContext, QueueEntryOptions } from '@orkestrel/queue'\nimport type { WorkerEventMap, WorkerHandler, WorkerInterface, WorkerOptions } from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Pool } from '@orkestrel/pool'\nimport { Queue } from '@orkestrel/queue'\n\n/**\n * Represents a resource-backed job worker — a thin facade composing a `Queue`\n * (`@orkestrel/queue`) with a `Pool` (`@orkestrel/pool`).\n *\n * @remarks\n * - **Composition, not reimplementation.** The Worker owns a `Pool` (built from\n * `options.pool`) and a `Queue` whose handler `acquire`s a pooled resource, runs the\n * user handler against it, and `release`s it in a `finally`. All concurrency, retries,\n * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.\n * - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive\n * safe integer after caller options are captured once. Only `undefined` defaults\n * `concurrency` to `1` or pool `max` to that value; runtime `null` reaches the owning\n * validator. The queue validates before the pool option is read; every declared pool member\n * is then captured once by direct access, preserving inherited and non-enumerable structural\n * options. At most one resource exists per in-flight job by default, and idle resources are\n * reused across jobs.\n * - **Acquire over the attempt signal.** Each job acquires using the attempt's\n * `context.signal`, so an `abort` / `timeout` while waiting for a resource rejects\n * the acquire — the Queue then handles retry / rejection, and there is no token to\n * release (the resource was never leased).\n * - **Lifecycle (see the guide's `## Methods` section).** `enqueue` / `restore` / `start` /\n * `stop` / `pause` / `resume` / `abort` / `clear` delegate to the queue; `count` / `active` /\n * `paused` / `stopped` read it. `stop` / `abort` / `clear` return the queue's own cleanup\n * barriers. `destroy` returns one stable barrier while it tears down the queue, then the\n * pool, and destroys the worker emitter last. A sole cleanup failure is preserved by\n * identity; failures from both layers become an ordered `AggregateError`.\n * - **Durability.** An optional `store` is passed straight through to the queue, so the\n * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).\n * - **Observable (see the guide's `## Observing` section).** The owned {@link emitter}\n * ({@link WorkerEventMap}) re-exposes the underlying queue's job lifecycle (`enqueue` /\n * `start` / `retry` / `success` / `failure` / `abort` / `drain`) as the worker's own events —\n * bridged from the inner queue's emitter at construction — so a consumer observes the worker\n * without reaching through to internals. The bridge re-emits directly on the worker's own\n * emitter; the worker emitter isolates a listener throw and routes it to its `error` handler\n * (the `error` option), so a buggy worker observer can never corrupt the inner queue or pool\n * — the bridge listener never throws, so the inner queue's own emit stays balanced. The\n * pool's create / acquire / release events stay the pool's internal concern (a Worker manages\n * its own resources); observe a `Pool` directly for those.\n */\nexport class Worker<TInput, TResource, TResult> implements WorkerInterface<TInput, TResult> {\n\treadonly #queue: Queue<TInput, TResult>\n\treadonly #pool: Pool<TResource>\n\t// The push observation surface (see the guide's `## Observing` section) — the worker's own\n\t// emitter, fed by the queue→worker bridge. The emitter isolates a worker observer's throw\n\t// (routing it to the `error` handler), so it never escapes into queue or pool.\n\treadonly #emitter: Emitter<WorkerEventMap<TResult>>\n\treadonly #handler: WorkerHandler<TInput, TResource, TResult>\n\t#ending: PromiseWithResolvers<void> | undefined\n\n\tconstructor(options: WorkerOptions<TInput, TResource, TResult>) {\n\t\tconst {\n\t\t\tconcurrency: capturedConcurrency,\n\t\t\thandler,\n\t\t\ton,\n\t\t\terror,\n\t\t\tretries,\n\t\t\ttimeout,\n\t\t\tstore,\n\t\t} = options\n\t\tconst concurrency = capturedConcurrency === undefined ? 1 : capturedConcurrency\n\t\tthis.#handler = handler\n\t\tthis.#emitter = new Emitter<WorkerEventMap<TResult>>({\n\t\t\t...(on !== undefined ? { on } : {}),\n\t\t\t...(error !== undefined ? { error } : {}),\n\t\t})\n\t\tthis.#queue = new Queue<TInput, TResult>({\n\t\t\thandler: this.#handle.bind(this),\n\t\t\tconcurrency,\n\t\t\t...(retries !== undefined ? { retries } : {}),\n\t\t\t...(timeout !== undefined ? { timeout } : {}),\n\t\t\t...(store !== undefined ? { store } : {}),\n\t\t})\n\t\tconst pool = options.pool\n\t\tconst { max, on: poolOn, error: poolError, create, destroy, validate } = pool\n\t\tthis.#pool = new Pool<TResource>({\n\t\t\tcreate,\n\t\t\tmax: max === undefined ? concurrency : max,\n\t\t\t...(poolOn !== undefined ? { on: poolOn } : {}),\n\t\t\t...(poolError !== undefined ? { error: poolError } : {}),\n\t\t\t...(destroy !== undefined ? { destroy } : {}),\n\t\t\t...(validate !== undefined ? { validate } : {}),\n\t\t})\n\t\tthis.#bridge()\n\t}\n\n\tget emitter(): EmitterInterface<WorkerEventMap<TResult>> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#queue.count\n\t}\n\n\tget active(): number {\n\t\treturn this.#queue.active\n\t}\n\n\tget paused(): boolean {\n\t\treturn this.#queue.paused\n\t}\n\n\tget stopped(): boolean {\n\t\treturn this.#queue.stopped\n\t}\n\n\tenqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult> {\n\t\treturn this.#queue.enqueue(input, options)\n\t}\n\n\trestore(): Promise<void> {\n\t\treturn this.#queue.restore()\n\t}\n\n\tstart(): void {\n\t\tthis.#queue.start()\n\t}\n\n\tstop(): Promise<void> {\n\t\treturn this.#queue.stop()\n\t}\n\n\tpause(): void {\n\t\tthis.#queue.pause()\n\t}\n\n\tresume(): void {\n\t\tthis.#queue.resume()\n\t}\n\n\tabort(reason?: unknown): Promise<void> {\n\t\treturn this.#queue.abort(reason)\n\t}\n\n\tclear(): Promise<void> {\n\t\treturn this.#queue.clear()\n\t}\n\n\tdestroy(): Promise<void> {\n\t\tif (this.#ending !== undefined) return this.#ending.promise\n\t\tconst ending = Promise.withResolvers<void>()\n\t\tthis.#ending = ending\n\t\tvoid this.#teardown(ending)\n\t\treturn ending.promise\n\t}\n\n\tasync #handle(input: TInput, context: QueueContext): Promise<TResult> {\n\t\tconst token = await this.#pool.acquire(context.signal)\n\t\ttry {\n\t\t\treturn await this.#handler(input, token.value, context)\n\t\t} finally {\n\t\t\ttoken.release()\n\t\t}\n\t}\n\n\tasync #teardown(ending: PromiseWithResolvers<void>): Promise<void> {\n\t\tconst failures: unknown[] = []\n\t\ttry {\n\t\t\tawait this.#queue.destroy()\n\t\t} catch (error) {\n\t\t\tfailures.push(error)\n\t\t}\n\t\ttry {\n\t\t\tawait this.#pool.destroy()\n\t\t} catch (error) {\n\t\t\tfailures.push(error)\n\t\t}\n\t\tthis.#emitter.destroy()\n\t\tif (failures.length === 0) ending.resolve()\n\t\telse if (failures.length === 1) ending.reject(failures[0])\n\t\telse ending.reject(new AggregateError(failures, 'worker destroy cleanup failed'))\n\t}\n\n\t// Bridge the inner queue's lifecycle onto the worker's own emitter, once at construction.\n\t// Each listener re-emits the queue event directly on the worker's emitter, which isolates a\n\t// worker observer's throw (routing it to the worker's `error` handler). Because the bridge\n\t// listener itself never throws, the queue's own `#emitter.emit` — which invoked this\n\t// listener — sees no throw, so the inner queue's engine stays balanced regardless of what a\n\t// worker observer does. The events are already post-transition (they fire from the queue's\n\t// own post-settle / post-wake emits), so this stays observation.\n\t#bridge(): void {\n\t\tconst queue = this.#queue.emitter\n\t\tqueue.on('enqueue', (id) => this.#emitter.emit('enqueue', id))\n\t\tqueue.on('start', (id) => this.#emitter.emit('start', id))\n\t\tqueue.on('retry', (id, attempt) => this.#emitter.emit('retry', id, attempt))\n\t\tqueue.on('success', (id, result) => this.#emitter.emit('success', id, result))\n\t\tqueue.on('failure', (id, error) => this.#emitter.emit('failure', id, error))\n\t\tqueue.on('abort', (reason) => this.#emitter.emit('abort', reason))\n\t\tqueue.on('drain', () => this.#emitter.emit('drain'))\n\t}\n}\n","import type { WorkerInterface, WorkerOptions } from './types.js'\nimport { Worker } from './Worker.js'\n\n/**\n * Creates a resource-backed job worker — a `Queue` (`@orkestrel/queue`) composed with a\n * `Pool` (`@orkestrel/pool`), where each enqueued input runs through the handler against\n * an automatically acquired pooled resource released when the job settles.\n *\n * @remarks\n * Bounded concurrency, retries, and the per-attempt timeout and abort are the queue's.\n * Default for the pool's `max`: the `concurrency` value, so resources match the jobs in flight.\n * Resources are reused across jobs. A handler that throws still releases its resource (the\n * acquire/release pair brackets the call in a `finally`), so a later job reuses it. The\n * lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)\n * delegates to the queue; `destroy` also tears the pool down. It is observable (see the\n * guide's `## Observing` section): a typed `emitter` surfaces the queue lifecycle\n * (`enqueue` / `start` / `success` / `failure` / …).\n *\n * @typeParam TInput - The work input each job carries\n * @typeParam TResource - The pooled resource each job runs against\n * @typeParam TResult - The value the handler resolves for a job\n * @param options - The `handler` and `pool` plus the optional `concurrency`, `retries`,\n * `timeout`, `store`, `on`, and `error` keys (see {@link WorkerOptions})\n * @returns A working {@link WorkerInterface}\n *\n * @example A resource-backed worker\n * ```ts\n * import { createWorker } from '@orkestrel/worker'\n *\n * // A Queue whose handler runs each job against a pooled resource (acquired before the\n * // handler, released after it — even on throw). The pool's `max` defaults to `concurrency`.\n * const worker = createWorker<Query, Connection, Rows>({\n * \tpool: { create: () => connect(), destroy: (connection) => connection.close() },\n * \thandler: (query, connection, { signal }) => connection.run(query, signal),\n * \tconcurrency: 4,\n * \tretries: 1,\n * })\n *\n * const rows = await worker.enqueue(query)\n * await worker.destroy() // awaits queue cleanup, pool cleanup, then emitter teardown\n * ```\n */\nexport function createWorker<TInput, TResource, TResult>(\n\toptions: WorkerOptions<TInput, TResource, TResult>,\n): WorkerInterface<TInput, TResult> {\n\treturn new Worker(options)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,SAAb,MAA4F;CAC3F;CACA;CAIA;CACA;CACA;CAEA,YAAY,SAAoD;EAC/D,MAAM,EACL,aAAa,qBACb,SACA,IACA,OACA,SACA,SACA,UACG;EACJ,MAAM,cAAc,wBAAwB,KAAA,IAAY,IAAI;EAC5D,KAAK,WAAW;EAChB,KAAK,WAAW,IAAIA,mBAAAA,QAAiC;GACpD,GAAI,OAAO,KAAA,IAAY,EAAE,GAAG,IAAI,CAAC;GACjC,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACxC,CAAC;EACD,KAAK,SAAS,IAAIC,iBAAAA,MAAuB;GACxC,SAAS,KAAK,QAAQ,KAAK,IAAI;GAC/B;GACA,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;GAC3C,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;GAC3C,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACxC,CAAC;EAED,MAAM,EAAE,KAAK,IAAI,QAAQ,OAAO,WAAW,QAAQ,SAAS,aAD/C,QAAQ;EAErB,KAAK,QAAQ,IAAIC,gBAAAA,KAAgB;GAChC;GACA,KAAK,QAAQ,KAAA,IAAY,cAAc;GACvC,GAAI,WAAW,KAAA,IAAY,EAAE,IAAI,OAAO,IAAI,CAAC;GAC7C,GAAI,cAAc,KAAA,IAAY,EAAE,OAAO,UAAU,IAAI,CAAC;GACtD,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;GAC3C,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC9C,CAAC;EACD,KAAK,QAAQ;CACd;CAEA,IAAI,UAAqD;EACxD,OAAO,KAAK;CACb;CAEA,IAAI,QAAgB;EACnB,OAAO,KAAK,OAAO;CACpB;CAEA,IAAI,SAAiB;EACpB,OAAO,KAAK,OAAO;CACpB;CAEA,IAAI,SAAkB;EACrB,OAAO,KAAK,OAAO;CACpB;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAK,OAAO;CACpB;CAEA,QAAQ,OAAe,SAA+C;EACrE,OAAO,KAAK,OAAO,QAAQ,OAAO,OAAO;CAC1C;CAEA,UAAyB;EACxB,OAAO,KAAK,OAAO,QAAQ;CAC5B;CAEA,QAAc;EACb,KAAK,OAAO,MAAM;CACnB;CAEA,OAAsB;EACrB,OAAO,KAAK,OAAO,KAAK;CACzB;CAEA,QAAc;EACb,KAAK,OAAO,MAAM;CACnB;CAEA,SAAe;EACd,KAAK,OAAO,OAAO;CACpB;CAEA,MAAM,QAAiC;EACtC,OAAO,KAAK,OAAO,MAAM,MAAM;CAChC;CAEA,QAAuB;EACtB,OAAO,KAAK,OAAO,MAAM;CAC1B;CAEA,UAAyB;EACxB,IAAI,KAAK,YAAY,KAAA,GAAW,OAAO,KAAK,QAAQ;EACpD,MAAM,SAAS,QAAQ,cAAoB;EAC3C,KAAK,UAAU;EACf,KAAU,UAAU,MAAM;EAC1B,OAAO,OAAO;CACf;CAEA,MAAM,QAAQ,OAAe,SAAyC;EACrE,MAAM,QAAQ,MAAM,KAAK,MAAM,QAAQ,QAAQ,MAAM;EACrD,IAAI;GACH,OAAO,MAAM,KAAK,SAAS,OAAO,MAAM,OAAO,OAAO;EACvD,UAAU;GACT,MAAM,QAAQ;EACf;CACD;CAEA,MAAM,UAAU,QAAmD;EAClE,MAAM,WAAsB,CAAC;EAC7B,IAAI;GACH,MAAM,KAAK,OAAO,QAAQ;EAC3B,SAAS,OAAO;GACf,SAAS,KAAK,KAAK;EACpB;EACA,IAAI;GACH,MAAM,KAAK,MAAM,QAAQ;EAC1B,SAAS,OAAO;GACf,SAAS,KAAK,KAAK;EACpB;EACA,KAAK,SAAS,QAAQ;EACtB,IAAI,SAAS,WAAW,GAAG,OAAO,QAAQ;OACrC,IAAI,SAAS,WAAW,GAAG,OAAO,OAAO,SAAS,EAAE;OACpD,OAAO,OAAO,IAAI,eAAe,UAAU,+BAA+B,CAAC;CACjF;CASA,UAAgB;EACf,MAAM,QAAQ,KAAK,OAAO;EAC1B,MAAM,GAAG,YAAY,OAAO,KAAK,SAAS,KAAK,WAAW,EAAE,CAAC;EAC7D,MAAM,GAAG,UAAU,OAAO,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC;EACzD,MAAM,GAAG,UAAU,IAAI,YAAY,KAAK,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC;EAC3E,MAAM,GAAG,YAAY,IAAI,WAAW,KAAK,SAAS,KAAK,WAAW,IAAI,MAAM,CAAC;EAC7E,MAAM,GAAG,YAAY,IAAI,UAAU,KAAK,SAAS,KAAK,WAAW,IAAI,KAAK,CAAC;EAC3E,MAAM,GAAG,UAAU,WAAW,KAAK,SAAS,KAAK,SAAS,MAAM,CAAC;EACjE,MAAM,GAAG,eAAe,KAAK,SAAS,KAAK,OAAO,CAAC;CACpD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC1JA,SAAgB,aACf,SACmC;CACnC,OAAO,IAAI,OAAO,OAAO;AAC1B"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["Emitter","Queue","Pool"],"sources":["../../../src/core/Worker.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { QueueContext, QueueEntryOptions } from '@orkestrel/queue'\nimport type { WorkerEventMap, WorkerHandler, WorkerInterface, WorkerOptions } from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Pool } from '@orkestrel/pool'\nimport { Queue } from '@orkestrel/queue'\n\n/**\n * Represents a resource-backed job worker — a thin facade composing a `Queue`\n * (`@orkestrel/queue`) with a `Pool` (`@orkestrel/pool`).\n *\n * @remarks\n * - **Composition, not reimplementation.** The Worker owns a `Pool` (built from\n * `options.pool`) and a `Queue` whose handler `acquire`s a pooled resource, runs the\n * user handler against it, and `release`s it in a `finally`. All concurrency, retries,\n * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.\n * - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive\n * safe integer after caller options are captured once. Only `undefined` defaults\n * `concurrency` to `1`; pool `max` defaults to concurrency only when `max` and `min` are\n * both absent. With `min`, Pool defaults `max` to `min`, requires their equality and a\n * `restarts` bound, and owns validation. Runtime `null` reaches the owning validator.\n * The queue validates before the pool option is read; every declared pool member\n * is then captured once by direct access, preserving inherited and non-enumerable structural\n * options. At most one resource exists per in-flight job by default, and idle resources are\n * reused across jobs. A configured floor starts warming at construction, independently of\n * queue concurrency, and can retain more resources than jobs in flight. A spent floor's\n * startup failure reaches jobs through acquire; startup rejection is observed internally.\n * - **Acquire over the attempt signal.** Each job acquires using the attempt's\n * `context.signal`, so an `abort` / `timeout` while waiting for a resource rejects\n * the acquire — the Queue then handles retry / rejection, and there is no token to\n * release (the resource was never leased).\n * - **Lifecycle (see the guide's `## Methods` section).** `enqueue` / `restore` / `start` /\n * `stop` / `pause` / `resume` / `abort` / `clear` delegate to the queue; `count` / `active` /\n * `paused` / `stopped` read it. `stop` / `abort` / `clear` return the queue's own cleanup\n * barriers. `destroy` returns one stable barrier while it tears down the queue, then the\n * pool, and destroys the worker emitter last. A sole cleanup failure is preserved by\n * identity; failures from both layers become an ordered `AggregateError`.\n * - **Durability.** An optional `store` is passed straight through to the queue, so the\n * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).\n * - **Observable (see the guide's `## Observing` section).** The owned {@link emitter}\n * ({@link WorkerEventMap}) re-exposes the underlying queue's job lifecycle (`enqueue` /\n * `start` / `retry` / `success` / `failure` / `abort` / `drain`) as the worker's own events —\n * bridged from the inner queue's emitter at construction — so a consumer observes the worker\n * without reaching through to internals. The bridge re-emits directly on the worker's own\n * emitter; the worker emitter isolates a listener throw and routes it to its `error` handler\n * (the `error` option), so a buggy worker observer can never corrupt the inner queue or pool\n * — the bridge listener never throws, so the inner queue's own emit stays balanced. The\n * pool's create / acquire / release events stay the pool's internal concern (a Worker manages\n * its own resources); observe a `Pool` directly for those.\n */\nexport class Worker<TInput, TResource, TResult> implements WorkerInterface<TInput, TResult> {\n\treadonly #queue: Queue<TInput, TResult>\n\treadonly #pool: Pool<TResource>\n\t// The push observation surface (see the guide's `## Observing` section) — the worker's own\n\t// emitter, fed by the queue→worker bridge. The emitter isolates a worker observer's throw\n\t// (routing it to the `error` handler), so it never escapes into queue or pool.\n\treadonly #emitter: Emitter<WorkerEventMap<TResult>>\n\treadonly #handler: WorkerHandler<TInput, TResource, TResult>\n\t#ending: PromiseWithResolvers<void> | undefined\n\n\tconstructor(options: WorkerOptions<TInput, TResource, TResult>) {\n\t\tconst {\n\t\t\tconcurrency: capturedConcurrency,\n\t\t\thandler,\n\t\t\ton,\n\t\t\terror,\n\t\t\tretries,\n\t\t\ttimeout,\n\t\t\tstore,\n\t\t} = options\n\t\tconst concurrency = capturedConcurrency === undefined ? 1 : capturedConcurrency\n\t\tthis.#handler = handler\n\t\tthis.#emitter = new Emitter<WorkerEventMap<TResult>>({\n\t\t\t...(on !== undefined ? { on } : {}),\n\t\t\t...(error !== undefined ? { error } : {}),\n\t\t})\n\t\tthis.#queue = new Queue<TInput, TResult>({\n\t\t\thandler: this.#handle.bind(this),\n\t\t\tconcurrency,\n\t\t\t...(retries !== undefined ? { retries } : {}),\n\t\t\t...(timeout !== undefined ? { timeout } : {}),\n\t\t\t...(store !== undefined ? { store } : {}),\n\t\t})\n\t\tconst pool = options.pool\n\t\tconst {\n\t\t\tmax,\n\t\t\tmin,\n\t\t\trestarts,\n\t\t\twatch,\n\t\t\ton: poolOn,\n\t\t\terror: poolError,\n\t\t\tcreate,\n\t\t\tdestroy,\n\t\t\tvalidate,\n\t\t} = pool\n\t\tthis.#pool = new Pool<TResource>({\n\t\t\tcreate,\n\t\t\t...(max === undefined ? (min === undefined ? { max: concurrency } : {}) : { max }),\n\t\t\t...(min !== undefined ? { min } : {}),\n\t\t\t...(restarts !== undefined ? { restarts } : {}),\n\t\t\t...(watch !== undefined ? { watch } : {}),\n\t\t\t...(poolOn !== undefined ? { on: poolOn } : {}),\n\t\t\t...(poolError !== undefined ? { error: poolError } : {}),\n\t\t\t...(destroy !== undefined ? { destroy } : {}),\n\t\t\t...(validate !== undefined ? { validate } : {}),\n\t\t})\n\t\tthis.#bridge()\n\t\t// Acquires report a spent floor's failure; observe startup rejection before jobs arrive.\n\t\tvoid this.#pool.start().catch(() => {})\n\t}\n\n\tget emitter(): EmitterInterface<WorkerEventMap<TResult>> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#queue.count\n\t}\n\n\tget active(): number {\n\t\treturn this.#queue.active\n\t}\n\n\tget paused(): boolean {\n\t\treturn this.#queue.paused\n\t}\n\n\tget stopped(): boolean {\n\t\treturn this.#queue.stopped\n\t}\n\n\tenqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult> {\n\t\treturn this.#queue.enqueue(input, options)\n\t}\n\n\trestore(): Promise<void> {\n\t\treturn this.#queue.restore()\n\t}\n\n\tstart(): void {\n\t\tthis.#queue.start()\n\t}\n\n\tstop(): Promise<void> {\n\t\treturn this.#queue.stop()\n\t}\n\n\tpause(): void {\n\t\tthis.#queue.pause()\n\t}\n\n\tresume(): void {\n\t\tthis.#queue.resume()\n\t}\n\n\tabort(reason?: unknown): Promise<void> {\n\t\treturn this.#queue.abort(reason)\n\t}\n\n\tclear(): Promise<void> {\n\t\treturn this.#queue.clear()\n\t}\n\n\tdestroy(): Promise<void> {\n\t\tif (this.#ending !== undefined) return this.#ending.promise\n\t\tconst ending = Promise.withResolvers<void>()\n\t\tthis.#ending = ending\n\t\tvoid this.#teardown(ending)\n\t\treturn ending.promise\n\t}\n\n\tasync #handle(input: TInput, context: QueueContext): Promise<TResult> {\n\t\tconst token = await this.#pool.acquire(context.signal)\n\t\ttry {\n\t\t\treturn await this.#handler(input, token.value, context)\n\t\t} finally {\n\t\t\ttoken.release()\n\t\t}\n\t}\n\n\tasync #teardown(ending: PromiseWithResolvers<void>): Promise<void> {\n\t\tconst failures: unknown[] = []\n\t\ttry {\n\t\t\tawait this.#queue.destroy()\n\t\t} catch (error) {\n\t\t\tfailures.push(error)\n\t\t}\n\t\ttry {\n\t\t\tawait this.#pool.destroy()\n\t\t} catch (error) {\n\t\t\tfailures.push(error)\n\t\t}\n\t\tthis.#emitter.destroy()\n\t\tif (failures.length === 0) ending.resolve()\n\t\telse if (failures.length === 1) ending.reject(failures[0])\n\t\telse ending.reject(new AggregateError(failures, 'worker destroy cleanup failed'))\n\t}\n\n\t// Bridge the inner queue's lifecycle onto the worker's own emitter, once at construction.\n\t// Each listener re-emits the queue event directly on the worker's emitter, which isolates a\n\t// worker observer's throw (routing it to the worker's `error` handler). Because the bridge\n\t// listener itself never throws, the queue's own `#emitter.emit` — which invoked this\n\t// listener — sees no throw, so the inner queue's engine stays balanced regardless of what a\n\t// worker observer does. The events are already post-transition (they fire from the queue's\n\t// own post-settle / post-wake emits), so this stays observation.\n\t#bridge(): void {\n\t\tconst queue = this.#queue.emitter\n\t\tqueue.on('enqueue', (id) => this.#emitter.emit('enqueue', id))\n\t\tqueue.on('start', (id) => this.#emitter.emit('start', id))\n\t\tqueue.on('retry', (id, attempt) => this.#emitter.emit('retry', id, attempt))\n\t\tqueue.on('success', (id, result) => this.#emitter.emit('success', id, result))\n\t\tqueue.on('failure', (id, error) => this.#emitter.emit('failure', id, error))\n\t\tqueue.on('abort', (reason) => this.#emitter.emit('abort', reason))\n\t\tqueue.on('drain', () => this.#emitter.emit('drain'))\n\t}\n}\n","import type { WorkerInterface, WorkerOptions } from './types.js'\nimport { Worker } from './Worker.js'\n\n/**\n * Creates a resource-backed job worker — a `Queue` (`@orkestrel/queue`) composed with a\n * `Pool` (`@orkestrel/pool`), where each enqueued input runs through the handler against\n * an automatically acquired pooled resource released when the job settles.\n *\n * @remarks\n * Bounded concurrency, retries, and the per-attempt timeout and abort are the queue's.\n * When neither pool `max` nor `min` is given, `max` defaults to `concurrency`. With `min`,\n * Pool owns the capacity defaults and validation, and the worker starts warming the floor.\n * Resources are reused across jobs. A handler that throws still releases its resource (the\n * acquire/release pair brackets the call in a `finally`), so a later job reuses it. The\n * lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)\n * delegates to the queue; `destroy` also tears the pool down. It is observable (see the\n * guide's `## Observing` section): a typed `emitter` surfaces the queue lifecycle\n * (`enqueue` / `start` / `success` / `failure` / …).\n *\n * @typeParam TInput - The work input each job carries\n * @typeParam TResource - The pooled resource each job runs against\n * @typeParam TResult - The value the handler resolves for a job\n * @param options - The `handler` and `pool` plus the optional `concurrency`, `retries`,\n * `timeout`, `store`, `on`, and `error` keys (see {@link WorkerOptions})\n * @returns A working {@link WorkerInterface}\n *\n * @example A resource-backed worker\n * ```ts\n * import { createWorker } from '@orkestrel/worker'\n *\n * // A Queue whose handler runs each job against a pooled resource (acquired before the\n * // handler, released after it — even on throw). The pool's `max` defaults to `concurrency`.\n * const worker = createWorker<Query, Connection, Rows>({\n * \tpool: { create: () => connect(), destroy: (connection) => connection.close() },\n * \thandler: (query, connection, { signal }) => connection.run(query, signal),\n * \tconcurrency: 4,\n * \tretries: 1,\n * })\n *\n * const rows = await worker.enqueue(query)\n * await worker.destroy() // awaits queue cleanup, pool cleanup, then emitter teardown\n * ```\n */\nexport function createWorker<TInput, TResource, TResult>(\n\toptions: WorkerOptions<TInput, TResource, TResult>,\n): WorkerInterface<TInput, TResult> {\n\treturn new Worker(options)\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,8 @@ import type { QueueStoreInterface } from '@orkestrel/queue';
|
|
|
13
13
|
*
|
|
14
14
|
* @remarks
|
|
15
15
|
* Bounded concurrency, retries, and the per-attempt timeout and abort are the queue's.
|
|
16
|
-
*
|
|
16
|
+
* When neither pool `max` nor `min` is given, `max` defaults to `concurrency`. With `min`,
|
|
17
|
+
* Pool owns the capacity defaults and validation, and the worker starts warming the floor.
|
|
17
18
|
* Resources are reused across jobs. A handler that throws still releases its resource (the
|
|
18
19
|
* acquire/release pair brackets the call in a `finally`), so a later job reuses it. The
|
|
19
20
|
* lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)
|
|
@@ -58,11 +59,15 @@ export declare function createWorker<TInput, TResource, TResult>(options: Worker
|
|
|
58
59
|
* timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
|
|
59
60
|
* - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive
|
|
60
61
|
* safe integer after caller options are captured once. Only `undefined` defaults
|
|
61
|
-
* `concurrency` to `1
|
|
62
|
-
*
|
|
62
|
+
* `concurrency` to `1`; pool `max` defaults to concurrency only when `max` and `min` are
|
|
63
|
+
* both absent. With `min`, Pool defaults `max` to `min`, requires their equality and a
|
|
64
|
+
* `restarts` bound, and owns validation. Runtime `null` reaches the owning validator.
|
|
65
|
+
* The queue validates before the pool option is read; every declared pool member
|
|
63
66
|
* is then captured once by direct access, preserving inherited and non-enumerable structural
|
|
64
67
|
* options. At most one resource exists per in-flight job by default, and idle resources are
|
|
65
|
-
* reused across jobs.
|
|
68
|
+
* reused across jobs. A configured floor starts warming at construction, independently of
|
|
69
|
+
* queue concurrency, and can retain more resources than jobs in flight. A spent floor's
|
|
70
|
+
* startup failure reaches jobs through acquire; startup rejection is observed internally.
|
|
66
71
|
* - **Acquire over the attempt signal.** Each job acquires using the attempt's
|
|
67
72
|
* `context.signal`, so an `abort` / `timeout` while waiting for a resource rejects
|
|
68
73
|
* the acquire — the Queue then handles retry / rejection, and there is no token to
|
|
@@ -211,8 +216,10 @@ export declare interface WorkerInterface<TInput, TResult> {
|
|
|
211
216
|
* @remarks
|
|
212
217
|
* - `handler` — runs each job against an acquired pool resource; rejecting triggers a
|
|
213
218
|
* retry while attempts remain (delegated to the underlying queue).
|
|
214
|
-
* - `pool` — the {@link PoolOptions}
|
|
215
|
-
*
|
|
219
|
+
* - `pool` — the {@link PoolOptions} forwarded to the owned pool for validation. When both
|
|
220
|
+
* `max` and `min` are absent, `max` defaults to `concurrency`. With `min`, the pool defaults
|
|
221
|
+
* `max` to `min`, requires equality and `restarts`, and starts warming at construction.
|
|
222
|
+
* The floor can exceed queue concurrency; `watch` observes each resource's loss.
|
|
216
223
|
* - `concurrency` — the maximum jobs in flight at once; it must be a positive safe
|
|
217
224
|
* integer, as validated by the underlying queue. Default: 1.
|
|
218
225
|
* - `retries` — the default extra attempts per job on failure. Default: 0.
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ import type { QueueStoreInterface } from '@orkestrel/queue';
|
|
|
13
13
|
*
|
|
14
14
|
* @remarks
|
|
15
15
|
* Bounded concurrency, retries, and the per-attempt timeout and abort are the queue's.
|
|
16
|
-
*
|
|
16
|
+
* When neither pool `max` nor `min` is given, `max` defaults to `concurrency`. With `min`,
|
|
17
|
+
* Pool owns the capacity defaults and validation, and the worker starts warming the floor.
|
|
17
18
|
* Resources are reused across jobs. A handler that throws still releases its resource (the
|
|
18
19
|
* acquire/release pair brackets the call in a `finally`), so a later job reuses it. The
|
|
19
20
|
* lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)
|
|
@@ -58,11 +59,15 @@ export declare function createWorker<TInput, TResource, TResult>(options: Worker
|
|
|
58
59
|
* timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
|
|
59
60
|
* - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive
|
|
60
61
|
* safe integer after caller options are captured once. Only `undefined` defaults
|
|
61
|
-
* `concurrency` to `1
|
|
62
|
-
*
|
|
62
|
+
* `concurrency` to `1`; pool `max` defaults to concurrency only when `max` and `min` are
|
|
63
|
+
* both absent. With `min`, Pool defaults `max` to `min`, requires their equality and a
|
|
64
|
+
* `restarts` bound, and owns validation. Runtime `null` reaches the owning validator.
|
|
65
|
+
* The queue validates before the pool option is read; every declared pool member
|
|
63
66
|
* is then captured once by direct access, preserving inherited and non-enumerable structural
|
|
64
67
|
* options. At most one resource exists per in-flight job by default, and idle resources are
|
|
65
|
-
* reused across jobs.
|
|
68
|
+
* reused across jobs. A configured floor starts warming at construction, independently of
|
|
69
|
+
* queue concurrency, and can retain more resources than jobs in flight. A spent floor's
|
|
70
|
+
* startup failure reaches jobs through acquire; startup rejection is observed internally.
|
|
66
71
|
* - **Acquire over the attempt signal.** Each job acquires using the attempt's
|
|
67
72
|
* `context.signal`, so an `abort` / `timeout` while waiting for a resource rejects
|
|
68
73
|
* the acquire — the Queue then handles retry / rejection, and there is no token to
|
|
@@ -211,8 +216,10 @@ export declare interface WorkerInterface<TInput, TResult> {
|
|
|
211
216
|
* @remarks
|
|
212
217
|
* - `handler` — runs each job against an acquired pool resource; rejecting triggers a
|
|
213
218
|
* retry while attempts remain (delegated to the underlying queue).
|
|
214
|
-
* - `pool` — the {@link PoolOptions}
|
|
215
|
-
*
|
|
219
|
+
* - `pool` — the {@link PoolOptions} forwarded to the owned pool for validation. When both
|
|
220
|
+
* `max` and `min` are absent, `max` defaults to `concurrency`. With `min`, the pool defaults
|
|
221
|
+
* `max` to `min`, requires equality and `restarts`, and starts warming at construction.
|
|
222
|
+
* The floor can exceed queue concurrency; `watch` observes each resource's loss.
|
|
216
223
|
* - `concurrency` — the maximum jobs in flight at once; it must be a positive safe
|
|
217
224
|
* integer, as validated by the underlying queue. Default: 1.
|
|
218
225
|
* - `retries` — the default extra attempts per job on failure. Default: 0.
|
package/dist/src/core/index.js
CHANGED
|
@@ -13,11 +13,15 @@ import { Queue } from "@orkestrel/queue";
|
|
|
13
13
|
* timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
|
|
14
14
|
* - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive
|
|
15
15
|
* safe integer after caller options are captured once. Only `undefined` defaults
|
|
16
|
-
* `concurrency` to `1
|
|
17
|
-
*
|
|
16
|
+
* `concurrency` to `1`; pool `max` defaults to concurrency only when `max` and `min` are
|
|
17
|
+
* both absent. With `min`, Pool defaults `max` to `min`, requires their equality and a
|
|
18
|
+
* `restarts` bound, and owns validation. Runtime `null` reaches the owning validator.
|
|
19
|
+
* The queue validates before the pool option is read; every declared pool member
|
|
18
20
|
* is then captured once by direct access, preserving inherited and non-enumerable structural
|
|
19
21
|
* options. At most one resource exists per in-flight job by default, and idle resources are
|
|
20
|
-
* reused across jobs.
|
|
22
|
+
* reused across jobs. A configured floor starts warming at construction, independently of
|
|
23
|
+
* queue concurrency, and can retain more resources than jobs in flight. A spent floor's
|
|
24
|
+
* startup failure reaches jobs through acquire; startup rejection is observed internally.
|
|
21
25
|
* - **Acquire over the attempt signal.** Each job acquires using the attempt's
|
|
22
26
|
* `context.signal`, so an `abort` / `timeout` while waiting for a resource rejects
|
|
23
27
|
* the acquire — the Queue then handles retry / rejection, and there is no token to
|
|
@@ -62,16 +66,20 @@ var Worker = class {
|
|
|
62
66
|
...timeout !== void 0 ? { timeout } : {},
|
|
63
67
|
...store !== void 0 ? { store } : {}
|
|
64
68
|
});
|
|
65
|
-
const { max, on: poolOn, error: poolError, create, destroy, validate } = options.pool;
|
|
69
|
+
const { max, min, restarts, watch, on: poolOn, error: poolError, create, destroy, validate } = options.pool;
|
|
66
70
|
this.#pool = new Pool({
|
|
67
71
|
create,
|
|
68
|
-
max
|
|
72
|
+
...max === void 0 ? min === void 0 ? { max: concurrency } : {} : { max },
|
|
73
|
+
...min !== void 0 ? { min } : {},
|
|
74
|
+
...restarts !== void 0 ? { restarts } : {},
|
|
75
|
+
...watch !== void 0 ? { watch } : {},
|
|
69
76
|
...poolOn !== void 0 ? { on: poolOn } : {},
|
|
70
77
|
...poolError !== void 0 ? { error: poolError } : {},
|
|
71
78
|
...destroy !== void 0 ? { destroy } : {},
|
|
72
79
|
...validate !== void 0 ? { validate } : {}
|
|
73
80
|
});
|
|
74
81
|
this.#bridge();
|
|
82
|
+
this.#pool.start().catch(() => {});
|
|
75
83
|
}
|
|
76
84
|
get emitter() {
|
|
77
85
|
return this.#emitter;
|
|
@@ -164,7 +172,8 @@ var Worker = class {
|
|
|
164
172
|
*
|
|
165
173
|
* @remarks
|
|
166
174
|
* Bounded concurrency, retries, and the per-attempt timeout and abort are the queue's.
|
|
167
|
-
*
|
|
175
|
+
* When neither pool `max` nor `min` is given, `max` defaults to `concurrency`. With `min`,
|
|
176
|
+
* Pool owns the capacity defaults and validation, and the worker starts warming the floor.
|
|
168
177
|
* Resources are reused across jobs. A handler that throws still releases its resource (the
|
|
169
178
|
* acquire/release pair brackets the call in a `finally`), so a later job reuses it. The
|
|
170
179
|
* lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/Worker.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { QueueContext, QueueEntryOptions } from '@orkestrel/queue'\nimport type { WorkerEventMap, WorkerHandler, WorkerInterface, WorkerOptions } from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Pool } from '@orkestrel/pool'\nimport { Queue } from '@orkestrel/queue'\n\n/**\n * Represents a resource-backed job worker — a thin facade composing a `Queue`\n * (`@orkestrel/queue`) with a `Pool` (`@orkestrel/pool`).\n *\n * @remarks\n * - **Composition, not reimplementation.** The Worker owns a `Pool` (built from\n * `options.pool`) and a `Queue` whose handler `acquire`s a pooled resource, runs the\n * user handler against it, and `release`s it in a `finally`. All concurrency, retries,\n * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.\n * - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive\n * safe integer after caller options are captured once. Only `undefined` defaults\n * `concurrency` to `1` or pool `max` to that value; runtime `null` reaches the owning\n * validator. The queue validates before the pool option is read; every declared pool member\n * is then captured once by direct access, preserving inherited and non-enumerable structural\n * options. At most one resource exists per in-flight job by default, and idle resources are\n * reused across jobs.\n * - **Acquire over the attempt signal.** Each job acquires using the attempt's\n * `context.signal`, so an `abort` / `timeout` while waiting for a resource rejects\n * the acquire — the Queue then handles retry / rejection, and there is no token to\n * release (the resource was never leased).\n * - **Lifecycle (see the guide's `## Methods` section).** `enqueue` / `restore` / `start` /\n * `stop` / `pause` / `resume` / `abort` / `clear` delegate to the queue; `count` / `active` /\n * `paused` / `stopped` read it. `stop` / `abort` / `clear` return the queue's own cleanup\n * barriers. `destroy` returns one stable barrier while it tears down the queue, then the\n * pool, and destroys the worker emitter last. A sole cleanup failure is preserved by\n * identity; failures from both layers become an ordered `AggregateError`.\n * - **Durability.** An optional `store` is passed straight through to the queue, so the\n * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).\n * - **Observable (see the guide's `## Observing` section).** The owned {@link emitter}\n * ({@link WorkerEventMap}) re-exposes the underlying queue's job lifecycle (`enqueue` /\n * `start` / `retry` / `success` / `failure` / `abort` / `drain`) as the worker's own events —\n * bridged from the inner queue's emitter at construction — so a consumer observes the worker\n * without reaching through to internals. The bridge re-emits directly on the worker's own\n * emitter; the worker emitter isolates a listener throw and routes it to its `error` handler\n * (the `error` option), so a buggy worker observer can never corrupt the inner queue or pool\n * — the bridge listener never throws, so the inner queue's own emit stays balanced. The\n * pool's create / acquire / release events stay the pool's internal concern (a Worker manages\n * its own resources); observe a `Pool` directly for those.\n */\nexport class Worker<TInput, TResource, TResult> implements WorkerInterface<TInput, TResult> {\n\treadonly #queue: Queue<TInput, TResult>\n\treadonly #pool: Pool<TResource>\n\t// The push observation surface (see the guide's `## Observing` section) — the worker's own\n\t// emitter, fed by the queue→worker bridge. The emitter isolates a worker observer's throw\n\t// (routing it to the `error` handler), so it never escapes into queue or pool.\n\treadonly #emitter: Emitter<WorkerEventMap<TResult>>\n\treadonly #handler: WorkerHandler<TInput, TResource, TResult>\n\t#ending: PromiseWithResolvers<void> | undefined\n\n\tconstructor(options: WorkerOptions<TInput, TResource, TResult>) {\n\t\tconst {\n\t\t\tconcurrency: capturedConcurrency,\n\t\t\thandler,\n\t\t\ton,\n\t\t\terror,\n\t\t\tretries,\n\t\t\ttimeout,\n\t\t\tstore,\n\t\t} = options\n\t\tconst concurrency = capturedConcurrency === undefined ? 1 : capturedConcurrency\n\t\tthis.#handler = handler\n\t\tthis.#emitter = new Emitter<WorkerEventMap<TResult>>({\n\t\t\t...(on !== undefined ? { on } : {}),\n\t\t\t...(error !== undefined ? { error } : {}),\n\t\t})\n\t\tthis.#queue = new Queue<TInput, TResult>({\n\t\t\thandler: this.#handle.bind(this),\n\t\t\tconcurrency,\n\t\t\t...(retries !== undefined ? { retries } : {}),\n\t\t\t...(timeout !== undefined ? { timeout } : {}),\n\t\t\t...(store !== undefined ? { store } : {}),\n\t\t})\n\t\tconst pool = options.pool\n\t\tconst { max, on: poolOn, error: poolError, create, destroy, validate } = pool\n\t\tthis.#pool = new Pool<TResource>({\n\t\t\tcreate,\n\t\t\tmax: max === undefined ? concurrency : max,\n\t\t\t...(poolOn !== undefined ? { on: poolOn } : {}),\n\t\t\t...(poolError !== undefined ? { error: poolError } : {}),\n\t\t\t...(destroy !== undefined ? { destroy } : {}),\n\t\t\t...(validate !== undefined ? { validate } : {}),\n\t\t})\n\t\tthis.#bridge()\n\t}\n\n\tget emitter(): EmitterInterface<WorkerEventMap<TResult>> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#queue.count\n\t}\n\n\tget active(): number {\n\t\treturn this.#queue.active\n\t}\n\n\tget paused(): boolean {\n\t\treturn this.#queue.paused\n\t}\n\n\tget stopped(): boolean {\n\t\treturn this.#queue.stopped\n\t}\n\n\tenqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult> {\n\t\treturn this.#queue.enqueue(input, options)\n\t}\n\n\trestore(): Promise<void> {\n\t\treturn this.#queue.restore()\n\t}\n\n\tstart(): void {\n\t\tthis.#queue.start()\n\t}\n\n\tstop(): Promise<void> {\n\t\treturn this.#queue.stop()\n\t}\n\n\tpause(): void {\n\t\tthis.#queue.pause()\n\t}\n\n\tresume(): void {\n\t\tthis.#queue.resume()\n\t}\n\n\tabort(reason?: unknown): Promise<void> {\n\t\treturn this.#queue.abort(reason)\n\t}\n\n\tclear(): Promise<void> {\n\t\treturn this.#queue.clear()\n\t}\n\n\tdestroy(): Promise<void> {\n\t\tif (this.#ending !== undefined) return this.#ending.promise\n\t\tconst ending = Promise.withResolvers<void>()\n\t\tthis.#ending = ending\n\t\tvoid this.#teardown(ending)\n\t\treturn ending.promise\n\t}\n\n\tasync #handle(input: TInput, context: QueueContext): Promise<TResult> {\n\t\tconst token = await this.#pool.acquire(context.signal)\n\t\ttry {\n\t\t\treturn await this.#handler(input, token.value, context)\n\t\t} finally {\n\t\t\ttoken.release()\n\t\t}\n\t}\n\n\tasync #teardown(ending: PromiseWithResolvers<void>): Promise<void> {\n\t\tconst failures: unknown[] = []\n\t\ttry {\n\t\t\tawait this.#queue.destroy()\n\t\t} catch (error) {\n\t\t\tfailures.push(error)\n\t\t}\n\t\ttry {\n\t\t\tawait this.#pool.destroy()\n\t\t} catch (error) {\n\t\t\tfailures.push(error)\n\t\t}\n\t\tthis.#emitter.destroy()\n\t\tif (failures.length === 0) ending.resolve()\n\t\telse if (failures.length === 1) ending.reject(failures[0])\n\t\telse ending.reject(new AggregateError(failures, 'worker destroy cleanup failed'))\n\t}\n\n\t// Bridge the inner queue's lifecycle onto the worker's own emitter, once at construction.\n\t// Each listener re-emits the queue event directly on the worker's emitter, which isolates a\n\t// worker observer's throw (routing it to the worker's `error` handler). Because the bridge\n\t// listener itself never throws, the queue's own `#emitter.emit` — which invoked this\n\t// listener — sees no throw, so the inner queue's engine stays balanced regardless of what a\n\t// worker observer does. The events are already post-transition (they fire from the queue's\n\t// own post-settle / post-wake emits), so this stays observation.\n\t#bridge(): void {\n\t\tconst queue = this.#queue.emitter\n\t\tqueue.on('enqueue', (id) => this.#emitter.emit('enqueue', id))\n\t\tqueue.on('start', (id) => this.#emitter.emit('start', id))\n\t\tqueue.on('retry', (id, attempt) => this.#emitter.emit('retry', id, attempt))\n\t\tqueue.on('success', (id, result) => this.#emitter.emit('success', id, result))\n\t\tqueue.on('failure', (id, error) => this.#emitter.emit('failure', id, error))\n\t\tqueue.on('abort', (reason) => this.#emitter.emit('abort', reason))\n\t\tqueue.on('drain', () => this.#emitter.emit('drain'))\n\t}\n}\n","import type { WorkerInterface, WorkerOptions } from './types.js'\nimport { Worker } from './Worker.js'\n\n/**\n * Creates a resource-backed job worker — a `Queue` (`@orkestrel/queue`) composed with a\n * `Pool` (`@orkestrel/pool`), where each enqueued input runs through the handler against\n * an automatically acquired pooled resource released when the job settles.\n *\n * @remarks\n * Bounded concurrency, retries, and the per-attempt timeout and abort are the queue's.\n * Default for the pool's `max`: the `concurrency` value, so resources match the jobs in flight.\n * Resources are reused across jobs. A handler that throws still releases its resource (the\n * acquire/release pair brackets the call in a `finally`), so a later job reuses it. The\n * lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)\n * delegates to the queue; `destroy` also tears the pool down. It is observable (see the\n * guide's `## Observing` section): a typed `emitter` surfaces the queue lifecycle\n * (`enqueue` / `start` / `success` / `failure` / …).\n *\n * @typeParam TInput - The work input each job carries\n * @typeParam TResource - The pooled resource each job runs against\n * @typeParam TResult - The value the handler resolves for a job\n * @param options - The `handler` and `pool` plus the optional `concurrency`, `retries`,\n * `timeout`, `store`, `on`, and `error` keys (see {@link WorkerOptions})\n * @returns A working {@link WorkerInterface}\n *\n * @example A resource-backed worker\n * ```ts\n * import { createWorker } from '@orkestrel/worker'\n *\n * // A Queue whose handler runs each job against a pooled resource (acquired before the\n * // handler, released after it — even on throw). The pool's `max` defaults to `concurrency`.\n * const worker = createWorker<Query, Connection, Rows>({\n * \tpool: { create: () => connect(), destroy: (connection) => connection.close() },\n * \thandler: (query, connection, { signal }) => connection.run(query, signal),\n * \tconcurrency: 4,\n * \tretries: 1,\n * })\n *\n * const rows = await worker.enqueue(query)\n * await worker.destroy() // awaits queue cleanup, pool cleanup, then emitter teardown\n * ```\n */\nexport function createWorker<TInput, TResource, TResult>(\n\toptions: WorkerOptions<TInput, TResource, TResult>,\n): WorkerInterface<TInput, TResult> {\n\treturn new Worker(options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/Worker.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { QueueContext, QueueEntryOptions } from '@orkestrel/queue'\nimport type { WorkerEventMap, WorkerHandler, WorkerInterface, WorkerOptions } from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Pool } from '@orkestrel/pool'\nimport { Queue } from '@orkestrel/queue'\n\n/**\n * Represents a resource-backed job worker — a thin facade composing a `Queue`\n * (`@orkestrel/queue`) with a `Pool` (`@orkestrel/pool`).\n *\n * @remarks\n * - **Composition, not reimplementation.** The Worker owns a `Pool` (built from\n * `options.pool`) and a `Queue` whose handler `acquire`s a pooled resource, runs the\n * user handler against it, and `release`s it in a `finally`. All concurrency, retries,\n * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.\n * - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive\n * safe integer after caller options are captured once. Only `undefined` defaults\n * `concurrency` to `1`; pool `max` defaults to concurrency only when `max` and `min` are\n * both absent. With `min`, Pool defaults `max` to `min`, requires their equality and a\n * `restarts` bound, and owns validation. Runtime `null` reaches the owning validator.\n * The queue validates before the pool option is read; every declared pool member\n * is then captured once by direct access, preserving inherited and non-enumerable structural\n * options. At most one resource exists per in-flight job by default, and idle resources are\n * reused across jobs. A configured floor starts warming at construction, independently of\n * queue concurrency, and can retain more resources than jobs in flight. A spent floor's\n * startup failure reaches jobs through acquire; startup rejection is observed internally.\n * - **Acquire over the attempt signal.** Each job acquires using the attempt's\n * `context.signal`, so an `abort` / `timeout` while waiting for a resource rejects\n * the acquire — the Queue then handles retry / rejection, and there is no token to\n * release (the resource was never leased).\n * - **Lifecycle (see the guide's `## Methods` section).** `enqueue` / `restore` / `start` /\n * `stop` / `pause` / `resume` / `abort` / `clear` delegate to the queue; `count` / `active` /\n * `paused` / `stopped` read it. `stop` / `abort` / `clear` return the queue's own cleanup\n * barriers. `destroy` returns one stable barrier while it tears down the queue, then the\n * pool, and destroys the worker emitter last. A sole cleanup failure is preserved by\n * identity; failures from both layers become an ordered `AggregateError`.\n * - **Durability.** An optional `store` is passed straight through to the queue, so the\n * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).\n * - **Observable (see the guide's `## Observing` section).** The owned {@link emitter}\n * ({@link WorkerEventMap}) re-exposes the underlying queue's job lifecycle (`enqueue` /\n * `start` / `retry` / `success` / `failure` / `abort` / `drain`) as the worker's own events —\n * bridged from the inner queue's emitter at construction — so a consumer observes the worker\n * without reaching through to internals. The bridge re-emits directly on the worker's own\n * emitter; the worker emitter isolates a listener throw and routes it to its `error` handler\n * (the `error` option), so a buggy worker observer can never corrupt the inner queue or pool\n * — the bridge listener never throws, so the inner queue's own emit stays balanced. The\n * pool's create / acquire / release events stay the pool's internal concern (a Worker manages\n * its own resources); observe a `Pool` directly for those.\n */\nexport class Worker<TInput, TResource, TResult> implements WorkerInterface<TInput, TResult> {\n\treadonly #queue: Queue<TInput, TResult>\n\treadonly #pool: Pool<TResource>\n\t// The push observation surface (see the guide's `## Observing` section) — the worker's own\n\t// emitter, fed by the queue→worker bridge. The emitter isolates a worker observer's throw\n\t// (routing it to the `error` handler), so it never escapes into queue or pool.\n\treadonly #emitter: Emitter<WorkerEventMap<TResult>>\n\treadonly #handler: WorkerHandler<TInput, TResource, TResult>\n\t#ending: PromiseWithResolvers<void> | undefined\n\n\tconstructor(options: WorkerOptions<TInput, TResource, TResult>) {\n\t\tconst {\n\t\t\tconcurrency: capturedConcurrency,\n\t\t\thandler,\n\t\t\ton,\n\t\t\terror,\n\t\t\tretries,\n\t\t\ttimeout,\n\t\t\tstore,\n\t\t} = options\n\t\tconst concurrency = capturedConcurrency === undefined ? 1 : capturedConcurrency\n\t\tthis.#handler = handler\n\t\tthis.#emitter = new Emitter<WorkerEventMap<TResult>>({\n\t\t\t...(on !== undefined ? { on } : {}),\n\t\t\t...(error !== undefined ? { error } : {}),\n\t\t})\n\t\tthis.#queue = new Queue<TInput, TResult>({\n\t\t\thandler: this.#handle.bind(this),\n\t\t\tconcurrency,\n\t\t\t...(retries !== undefined ? { retries } : {}),\n\t\t\t...(timeout !== undefined ? { timeout } : {}),\n\t\t\t...(store !== undefined ? { store } : {}),\n\t\t})\n\t\tconst pool = options.pool\n\t\tconst {\n\t\t\tmax,\n\t\t\tmin,\n\t\t\trestarts,\n\t\t\twatch,\n\t\t\ton: poolOn,\n\t\t\terror: poolError,\n\t\t\tcreate,\n\t\t\tdestroy,\n\t\t\tvalidate,\n\t\t} = pool\n\t\tthis.#pool = new Pool<TResource>({\n\t\t\tcreate,\n\t\t\t...(max === undefined ? (min === undefined ? { max: concurrency } : {}) : { max }),\n\t\t\t...(min !== undefined ? { min } : {}),\n\t\t\t...(restarts !== undefined ? { restarts } : {}),\n\t\t\t...(watch !== undefined ? { watch } : {}),\n\t\t\t...(poolOn !== undefined ? { on: poolOn } : {}),\n\t\t\t...(poolError !== undefined ? { error: poolError } : {}),\n\t\t\t...(destroy !== undefined ? { destroy } : {}),\n\t\t\t...(validate !== undefined ? { validate } : {}),\n\t\t})\n\t\tthis.#bridge()\n\t\t// Acquires report a spent floor's failure; observe startup rejection before jobs arrive.\n\t\tvoid this.#pool.start().catch(() => {})\n\t}\n\n\tget emitter(): EmitterInterface<WorkerEventMap<TResult>> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#queue.count\n\t}\n\n\tget active(): number {\n\t\treturn this.#queue.active\n\t}\n\n\tget paused(): boolean {\n\t\treturn this.#queue.paused\n\t}\n\n\tget stopped(): boolean {\n\t\treturn this.#queue.stopped\n\t}\n\n\tenqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult> {\n\t\treturn this.#queue.enqueue(input, options)\n\t}\n\n\trestore(): Promise<void> {\n\t\treturn this.#queue.restore()\n\t}\n\n\tstart(): void {\n\t\tthis.#queue.start()\n\t}\n\n\tstop(): Promise<void> {\n\t\treturn this.#queue.stop()\n\t}\n\n\tpause(): void {\n\t\tthis.#queue.pause()\n\t}\n\n\tresume(): void {\n\t\tthis.#queue.resume()\n\t}\n\n\tabort(reason?: unknown): Promise<void> {\n\t\treturn this.#queue.abort(reason)\n\t}\n\n\tclear(): Promise<void> {\n\t\treturn this.#queue.clear()\n\t}\n\n\tdestroy(): Promise<void> {\n\t\tif (this.#ending !== undefined) return this.#ending.promise\n\t\tconst ending = Promise.withResolvers<void>()\n\t\tthis.#ending = ending\n\t\tvoid this.#teardown(ending)\n\t\treturn ending.promise\n\t}\n\n\tasync #handle(input: TInput, context: QueueContext): Promise<TResult> {\n\t\tconst token = await this.#pool.acquire(context.signal)\n\t\ttry {\n\t\t\treturn await this.#handler(input, token.value, context)\n\t\t} finally {\n\t\t\ttoken.release()\n\t\t}\n\t}\n\n\tasync #teardown(ending: PromiseWithResolvers<void>): Promise<void> {\n\t\tconst failures: unknown[] = []\n\t\ttry {\n\t\t\tawait this.#queue.destroy()\n\t\t} catch (error) {\n\t\t\tfailures.push(error)\n\t\t}\n\t\ttry {\n\t\t\tawait this.#pool.destroy()\n\t\t} catch (error) {\n\t\t\tfailures.push(error)\n\t\t}\n\t\tthis.#emitter.destroy()\n\t\tif (failures.length === 0) ending.resolve()\n\t\telse if (failures.length === 1) ending.reject(failures[0])\n\t\telse ending.reject(new AggregateError(failures, 'worker destroy cleanup failed'))\n\t}\n\n\t// Bridge the inner queue's lifecycle onto the worker's own emitter, once at construction.\n\t// Each listener re-emits the queue event directly on the worker's emitter, which isolates a\n\t// worker observer's throw (routing it to the worker's `error` handler). Because the bridge\n\t// listener itself never throws, the queue's own `#emitter.emit` — which invoked this\n\t// listener — sees no throw, so the inner queue's engine stays balanced regardless of what a\n\t// worker observer does. The events are already post-transition (they fire from the queue's\n\t// own post-settle / post-wake emits), so this stays observation.\n\t#bridge(): void {\n\t\tconst queue = this.#queue.emitter\n\t\tqueue.on('enqueue', (id) => this.#emitter.emit('enqueue', id))\n\t\tqueue.on('start', (id) => this.#emitter.emit('start', id))\n\t\tqueue.on('retry', (id, attempt) => this.#emitter.emit('retry', id, attempt))\n\t\tqueue.on('success', (id, result) => this.#emitter.emit('success', id, result))\n\t\tqueue.on('failure', (id, error) => this.#emitter.emit('failure', id, error))\n\t\tqueue.on('abort', (reason) => this.#emitter.emit('abort', reason))\n\t\tqueue.on('drain', () => this.#emitter.emit('drain'))\n\t}\n}\n","import type { WorkerInterface, WorkerOptions } from './types.js'\nimport { Worker } from './Worker.js'\n\n/**\n * Creates a resource-backed job worker — a `Queue` (`@orkestrel/queue`) composed with a\n * `Pool` (`@orkestrel/pool`), where each enqueued input runs through the handler against\n * an automatically acquired pooled resource released when the job settles.\n *\n * @remarks\n * Bounded concurrency, retries, and the per-attempt timeout and abort are the queue's.\n * When neither pool `max` nor `min` is given, `max` defaults to `concurrency`. With `min`,\n * Pool owns the capacity defaults and validation, and the worker starts warming the floor.\n * Resources are reused across jobs. A handler that throws still releases its resource (the\n * acquire/release pair brackets the call in a `finally`), so a later job reuses it. The\n * lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)\n * delegates to the queue; `destroy` also tears the pool down. It is observable (see the\n * guide's `## Observing` section): a typed `emitter` surfaces the queue lifecycle\n * (`enqueue` / `start` / `success` / `failure` / …).\n *\n * @typeParam TInput - The work input each job carries\n * @typeParam TResource - The pooled resource each job runs against\n * @typeParam TResult - The value the handler resolves for a job\n * @param options - The `handler` and `pool` plus the optional `concurrency`, `retries`,\n * `timeout`, `store`, `on`, and `error` keys (see {@link WorkerOptions})\n * @returns A working {@link WorkerInterface}\n *\n * @example A resource-backed worker\n * ```ts\n * import { createWorker } from '@orkestrel/worker'\n *\n * // A Queue whose handler runs each job against a pooled resource (acquired before the\n * // handler, released after it — even on throw). The pool's `max` defaults to `concurrency`.\n * const worker = createWorker<Query, Connection, Rows>({\n * \tpool: { create: () => connect(), destroy: (connection) => connection.close() },\n * \thandler: (query, connection, { signal }) => connection.run(query, signal),\n * \tconcurrency: 4,\n * \tretries: 1,\n * })\n *\n * const rows = await worker.enqueue(query)\n * await worker.destroy() // awaits queue cleanup, pool cleanup, then emitter teardown\n * ```\n */\nexport function createWorker<TInput, TResource, TResult>(\n\toptions: WorkerOptions<TInput, TResource, TResult>,\n): WorkerInterface<TInput, TResult> {\n\treturn new Worker(options)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "A typed, resource-backed job worker for the @orkestrel line — a Queue paired with a Pool over an execution seam, plus a node:worker_threads server surface for CPU-parallel jobs. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"async",
|
|
@@ -83,23 +83,23 @@
|
|
|
83
83
|
"test:setup": "vitest run --config vite.config.ts --no-cache --reporter=dot --project setup"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@orkestrel/contract": "^0.0.
|
|
87
|
-
"@orkestrel/database": "^0.0.
|
|
86
|
+
"@orkestrel/contract": "^0.0.19",
|
|
87
|
+
"@orkestrel/database": "^0.0.17",
|
|
88
88
|
"@orkestrel/emitter": "^0.0.11",
|
|
89
|
-
"@orkestrel/pool": "^0.0.
|
|
90
|
-
"@orkestrel/queue": "^0.0.
|
|
89
|
+
"@orkestrel/pool": "^0.0.14",
|
|
90
|
+
"@orkestrel/queue": "^0.0.16"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@microsoft/api-extractor": "^7.59.3",
|
|
94
|
-
"@orkestrel/guide": "^0.0.
|
|
95
|
-
"@orkestrel/probe": "^0.0.
|
|
96
|
-
"@orkestrel/scaffold": "^0.0.
|
|
94
|
+
"@orkestrel/guide": "^0.0.24",
|
|
95
|
+
"@orkestrel/probe": "^0.0.19",
|
|
96
|
+
"@orkestrel/scaffold": "^0.0.90",
|
|
97
97
|
"@orkestrel/test": "^0.0.24",
|
|
98
|
-
"@types/node": "^26.6.
|
|
98
|
+
"@types/node": "^26.6.4",
|
|
99
99
|
"oxfmt": "^0.71.0",
|
|
100
100
|
"oxlint": "^1.86.0",
|
|
101
101
|
"typescript": "^6.0.3",
|
|
102
|
-
"vite": "^8.3.
|
|
102
|
+
"vite": "^8.3.2",
|
|
103
103
|
"vitest": "^4.1.11"
|
|
104
104
|
},
|
|
105
105
|
"engines": {
|