@orkestrel/worker 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/README.md CHANGED
@@ -21,7 +21,7 @@ npm install @orkestrel/worker
21
21
 
22
22
  ## Requirements
23
23
 
24
- - Node.js >= 24
24
+ - Node.js >= 22.12.0
25
25
  - ESM and CommonJS builds ship for both the core and server entry points
26
26
 
27
27
  ## Usage
@@ -37,7 +37,7 @@ const worker = createWorker<Query, Connection, Rows>({
37
37
  })
38
38
 
39
39
  const rows = await worker.enqueue(query)
40
- worker.destroy() // tears down the queue, then the pool
40
+ await worker.destroy() // awaits queue cleanup, then pool cleanup, then emitter teardown
41
41
  ```
42
42
 
43
43
  CPU-parallel jobs over `node:worker_threads`:
@@ -55,6 +55,7 @@ const worker = createNodeWorker({
55
55
  })
56
56
 
57
57
  const doubled = await worker.enqueue(21) // 42, computed on a worker thread
58
+ await worker.destroy()
58
59
  ```
59
60
 
60
61
  ## Guide
@@ -12,16 +12,23 @@ let _orkestrel_queue = require("@orkestrel/queue");
12
12
  * `options.pool`) and a `Queue` whose handler ACQUIRES a pooled resource, runs the
13
13
  * user handler against it, and RELEASES it in a `finally`. All concurrency, retries,
14
14
  * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
15
- * - **Resource ↔ concurrency.** The pool's `max` defaults to the worker's
16
- * `concurrency` (default `1`), so at most one resource exists per in-flight job and
17
- * idle resources are reused across jobs.
15
+ * - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive
16
+ * safe integer after caller options are captured once. Only `undefined` defaults
17
+ * `concurrency` to `1` or pool `max` to that value; runtime `null` reaches the owning
18
+ * validator. The queue validates before the pool option is read; every declared pool member
19
+ * is then captured once by direct access, preserving inherited and non-enumerable structural
20
+ * options. At most one resource exists per in-flight job by default, and idle resources are
21
+ * reused across jobs.
18
22
  * - **Acquire over the attempt signal.** Each job acquires using the attempt's
19
23
  * `execution.signal`, so an `abort` / `timeout` while waiting for a resource rejects
20
24
  * the acquire — the Queue then handles retry / rejection, and there is no token to
21
25
  * release (the resource was never leased).
22
26
  * - **Lifecycle (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /
23
27
  * `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`
24
- * read it. `destroy` destroys the queue then tears the pool down, idempotently.
28
+ * read it. `stop` / `abort` / `clear` return the queue's own cleanup barriers.
29
+ * `destroy` returns one stable barrier while it tears down the queue, then the pool,
30
+ * and destroys the worker emitter last. A sole cleanup failure is preserved by
31
+ * identity; failures from both layers become an ordered `AggregateError`.
25
32
  * - **Durability.** An optional `store` is passed straight through to the queue, so the
26
33
  * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).
27
34
  * - **Observable (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the
@@ -40,24 +47,30 @@ var Worker = class {
40
47
  #pool;
41
48
  #emitter;
42
49
  #handler;
43
- #destroyed = false;
50
+ #ending;
44
51
  constructor(options) {
45
- const concurrency = Math.max(1, options.concurrency ?? 1);
46
- this.#handler = options.handler;
52
+ const { concurrency: capturedConcurrency, handler, on, error, retries, timeout, store } = options;
53
+ const concurrency = capturedConcurrency === void 0 ? 1 : capturedConcurrency;
54
+ this.#handler = handler;
47
55
  this.#emitter = new _orkestrel_emitter.Emitter({
48
- ...options.on !== void 0 ? { on: options.on } : {},
49
- ...options.error !== void 0 ? { error: options.error } : {}
50
- });
51
- this.#pool = new _orkestrel_pool.Pool({
52
- ...options.pool,
53
- max: options.pool.max ?? concurrency
56
+ ...on !== void 0 ? { on } : {},
57
+ ...error !== void 0 ? { error } : {}
54
58
  });
55
59
  this.#queue = new _orkestrel_queue.Queue({
56
60
  handler: this.#handle.bind(this),
57
61
  concurrency,
58
- ...options.retries !== void 0 ? { retries: options.retries } : {},
59
- ...options.timeout !== void 0 ? { timeout: options.timeout } : {},
60
- ...options.store !== void 0 ? { store: options.store } : {}
62
+ ...retries !== void 0 ? { retries } : {},
63
+ ...timeout !== void 0 ? { timeout } : {},
64
+ ...store !== void 0 ? { store } : {}
65
+ });
66
+ const { max, on: poolOn, error: poolError, create, destroy, validate } = options.pool;
67
+ this.#pool = new _orkestrel_pool.Pool({
68
+ create,
69
+ max: max === void 0 ? concurrency : max,
70
+ ...poolOn !== void 0 ? { on: poolOn } : {},
71
+ ...poolError !== void 0 ? { error: poolError } : {},
72
+ ...destroy !== void 0 ? { destroy } : {},
73
+ ...validate !== void 0 ? { validate } : {}
61
74
  });
62
75
  this.#bridge();
63
76
  }
@@ -86,7 +99,7 @@ var Worker = class {
86
99
  this.#queue.start();
87
100
  }
88
101
  stop() {
89
- this.#queue.stop();
102
+ return this.#queue.stop();
90
103
  }
91
104
  pause() {
92
105
  this.#queue.pause();
@@ -95,16 +108,17 @@ var Worker = class {
95
108
  this.#queue.resume();
96
109
  }
97
110
  abort(reason) {
98
- this.#queue.abort(reason);
111
+ return this.#queue.abort(reason);
99
112
  }
100
113
  clear() {
101
- this.#queue.clear();
114
+ return this.#queue.clear();
102
115
  }
103
116
  destroy() {
104
- if (this.#destroyed) return;
105
- this.#destroyed = true;
106
- this.#queue.destroy();
107
- this.#pool.destroy();
117
+ if (this.#ending !== void 0) return this.#ending.promise;
118
+ const ending = Promise.withResolvers();
119
+ this.#ending = ending;
120
+ this.#teardown(ending);
121
+ return ending.promise;
108
122
  }
109
123
  async #handle(input, execution) {
110
124
  const token = await this.#pool.acquire(execution.signal);
@@ -114,6 +128,23 @@ var Worker = class {
114
128
  token.release();
115
129
  }
116
130
  }
131
+ async #teardown(ending) {
132
+ const failures = [];
133
+ try {
134
+ await this.#queue.destroy();
135
+ } catch (error) {
136
+ failures.push(error);
137
+ }
138
+ try {
139
+ await this.#pool.destroy();
140
+ } catch (error) {
141
+ failures.push(error);
142
+ }
143
+ this.#emitter.destroy();
144
+ if (failures.length === 0) ending.resolve();
145
+ else if (failures.length === 1) ending.reject(failures[0]);
146
+ else ending.reject(new AggregateError(failures, "worker destroy cleanup failed"));
147
+ }
117
148
  #bridge() {
118
149
  const queue = this.#queue.emitter;
119
150
  queue.on("enqueue", (id) => this.#emitter.emit("enqueue", id));
@@ -150,7 +181,7 @@ var Worker = class {
150
181
  *
151
182
  * @example
152
183
  * ```ts
153
- * import { createWorker } from '@src/core'
184
+ * import { createWorker } from '@orkestrel/worker'
154
185
  *
155
186
  * const worker = createWorker<Query, Connection, Rows>({
156
187
  * pool: { create: () => connect(), destroy: (connection) => connection.close() },
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["#queue","#pool","#emitter","#handler","#handle","#bridge","#destroyed"],"sources":["../../../src/core/Worker.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { QueueEntryOptions, QueueExecution } 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 * A resource-backed job worker — a thin facade composing a `Queue` (`@orkestrel/queue`)\n * 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 ACQUIRES a pooled resource, runs the\n * user handler against it, and RELEASES 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 pool's `max` defaults to the worker's\n * `concurrency` (default `1`), so at most one resource exists per in-flight job and\n * idle resources are reused across jobs.\n * - **Acquire over the attempt signal.** Each job acquires using the attempt's\n * `execution.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 (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /\n * `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`\n * read it. `destroy` destroys the queue then tears the pool down, idempotently.\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 (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the\n * underlying queue's job lifecycle (`enqueue` / `start` / `retry` / `success` / `failure` /\n * `abort` / `drain`) as the worker's OWN events — bridged from the inner queue's emitter at\n * construction — so a consumer observes the worker without reaching through to internals.\n * The bridge re-emits directly on the worker's own emitter; the worker emitter isolates a\n * listener throw and routes it to its `error` handler (the `error` option), so a buggy\n * worker observer can never corrupt the inner queue or pool — the bridge listener never\n * throws, so the inner queue's own emit stays balanced. The pool's create / acquire /\n * release events stay the pool's internal concern (a Worker manages its own resources);\n * 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 (§13) — the worker's OWN emitter, fed by the queue→worker\n\t// bridge. The emitter isolates a worker observer's throw (routing it to the `error`\n\t// handler), so it never escapes into queue or pool.\n\treadonly #emitter: Emitter<WorkerEventMap<TResult>>\n\treadonly #handler: WorkerHandler<TInput, TResource, TResult>\n\t#destroyed = false\n\n\tconstructor(options: WorkerOptions<TInput, TResource, TResult>) {\n\t\tconst concurrency = Math.max(1, options.concurrency ?? 1)\n\t\tthis.#handler = options.handler\n\t\tthis.#emitter = new Emitter<WorkerEventMap<TResult>>({\n\t\t\t...(options.on !== undefined ? { on: options.on } : {}),\n\t\t\t...(options.error !== undefined ? { error: options.error } : {}),\n\t\t})\n\t\tthis.#pool = new Pool<TResource>({\n\t\t\t...options.pool,\n\t\t\tmax: options.pool.max ?? concurrency,\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...(options.retries !== undefined ? { retries: options.retries } : {}),\n\t\t\t...(options.timeout !== undefined ? { timeout: options.timeout } : {}),\n\t\t\t...(options.store !== undefined ? { store: options.store } : {}),\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(): void {\n\t\tthis.#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): void {\n\t\tthis.#queue.abort(reason)\n\t}\n\n\tclear(): void {\n\t\tthis.#queue.clear()\n\t}\n\n\tdestroy(): void {\n\t\tif (this.#destroyed) return\n\t\tthis.#destroyed = true\n\t\tthis.#queue.destroy()\n\t\tvoid this.#pool.destroy()\n\t}\n\n\tasync #handle(input: TInput, execution: QueueExecution): Promise<TResult> {\n\t\tconst token = await this.#pool.acquire(execution.signal)\n\t\ttry {\n\t\t\treturn await this.#handler(input, token.value, execution)\n\t\t} finally {\n\t\t\ttoken.release()\n\t\t}\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 * Create a resource-backed job worker — a `Queue` (`@orkestrel/queue`) marrying a `Pool`\n * (`@orkestrel/pool`). Each enqueued input runs through the handler against an\n * automatically acquired pooled resource (released when the job settles), with the\n * queue's bounded concurrency, retries, and per-attempt timeout / abort.\n *\n * @remarks\n * The pool's `max` defaults to `concurrency`, so resources match the jobs in flight and\n * 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. Observable (§13): a typed\n * `emitter` surfaces the queue lifecycle (`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 optional `concurrency` (default `1`),\n * `retries` (default `0`), and a default per-attempt `timeout` in milliseconds\n * @returns A working {@link WorkerInterface}\n *\n * @example\n * ```ts\n * import { createWorker } from '@src/core'\n *\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 * ```\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.cjs","names":["#queue","#pool","#emitter","#handler","#handle","#bridge","#ending","#teardown"],"sources":["../../../src/core/Worker.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { QueueEntryOptions, QueueExecution } 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 * A resource-backed job worker — a thin facade composing a `Queue` (`@orkestrel/queue`)\n * 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 ACQUIRES a pooled resource, runs the\n * user handler against it, and RELEASES 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 * `execution.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 (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /\n * `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`\n * read it. `stop` / `abort` / `clear` return the queue's own cleanup barriers.\n * `destroy` returns one stable barrier while it tears down the queue, then the pool,\n * 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 (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the\n * underlying queue's job lifecycle (`enqueue` / `start` / `retry` / `success` / `failure` /\n * `abort` / `drain`) as the worker's OWN events — bridged from the inner queue's emitter at\n * construction — so a consumer observes the worker without reaching through to internals.\n * The bridge re-emits directly on the worker's own emitter; the worker emitter isolates a\n * listener throw and routes it to its `error` handler (the `error` option), so a buggy\n * worker observer can never corrupt the inner queue or pool — the bridge listener never\n * throws, so the inner queue's own emit stays balanced. The pool's create / acquire /\n * release events stay the pool's internal concern (a Worker manages its own resources);\n * 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 (§13) — the worker's OWN emitter, fed by the queue→worker\n\t// bridge. The emitter isolates a worker observer's throw (routing it to the `error`\n\t// 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, execution: QueueExecution): Promise<TResult> {\n\t\tconst token = await this.#pool.acquire(execution.signal)\n\t\ttry {\n\t\t\treturn await this.#handler(input, token.value, execution)\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 * Create a resource-backed job worker — a `Queue` (`@orkestrel/queue`) marrying a `Pool`\n * (`@orkestrel/pool`). Each enqueued input runs through the handler against an\n * automatically acquired pooled resource (released when the job settles), with the\n * queue's bounded concurrency, retries, and per-attempt timeout / abort.\n *\n * @remarks\n * The pool's `max` defaults to `concurrency`, so resources match the jobs in flight and\n * 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. Observable (§13): a typed\n * `emitter` surfaces the queue lifecycle (`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 optional `concurrency` (default `1`),\n * `retries` (default `0`), and a default per-attempt `timeout` in milliseconds\n * @returns A working {@link WorkerInterface}\n *\n * @example\n * ```ts\n * import { createWorker } from '@orkestrel/worker'\n *\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 * ```\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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"}
@@ -29,7 +29,7 @@ import { QueueStoreInterface } from '@orkestrel/queue';
29
29
  *
30
30
  * @example
31
31
  * ```ts
32
- * import { createWorker } from '@src/core'
32
+ * import { createWorker } from '@orkestrel/worker'
33
33
  *
34
34
  * const worker = createWorker<Query, Connection, Rows>({
35
35
  * pool: { create: () => connect(), destroy: (connection) => connection.close() },
@@ -52,16 +52,23 @@ export declare function createWorker<TInput, TResource, TResult>(options: Worker
52
52
  * `options.pool`) and a `Queue` whose handler ACQUIRES a pooled resource, runs the
53
53
  * user handler against it, and RELEASES it in a `finally`. All concurrency, retries,
54
54
  * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
55
- * - **Resource ↔ concurrency.** The pool's `max` defaults to the worker's
56
- * `concurrency` (default `1`), so at most one resource exists per in-flight job and
57
- * idle resources are reused across jobs.
55
+ * - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive
56
+ * safe integer after caller options are captured once. Only `undefined` defaults
57
+ * `concurrency` to `1` or pool `max` to that value; runtime `null` reaches the owning
58
+ * validator. The queue validates before the pool option is read; every declared pool member
59
+ * is then captured once by direct access, preserving inherited and non-enumerable structural
60
+ * options. At most one resource exists per in-flight job by default, and idle resources are
61
+ * reused across jobs.
58
62
  * - **Acquire over the attempt signal.** Each job acquires using the attempt's
59
63
  * `execution.signal`, so an `abort` / `timeout` while waiting for a resource rejects
60
64
  * the acquire — the Queue then handles retry / rejection, and there is no token to
61
65
  * release (the resource was never leased).
62
66
  * - **Lifecycle (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /
63
67
  * `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`
64
- * read it. `destroy` destroys the queue then tears the pool down, idempotently.
68
+ * read it. `stop` / `abort` / `clear` return the queue's own cleanup barriers.
69
+ * `destroy` returns one stable barrier while it tears down the queue, then the pool,
70
+ * and destroys the worker emitter last. A sole cleanup failure is preserved by
71
+ * identity; failures from both layers become an ordered `AggregateError`.
65
72
  * - **Durability.** An optional `store` is passed straight through to the queue, so the
66
73
  * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).
67
74
  * - **Observable (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the
@@ -86,12 +93,12 @@ declare class Worker_2<TInput, TResource, TResult> implements WorkerInterface<TI
86
93
  enqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult>;
87
94
  restore(): Promise<void>;
88
95
  start(): void;
89
- stop(): void;
96
+ stop(): Promise<void>;
90
97
  pause(): void;
91
98
  resume(): void;
92
- abort(reason?: unknown): void;
93
- clear(): void;
94
- destroy(): void;
99
+ abort(reason?: unknown): Promise<void>;
100
+ clear(): Promise<void>;
101
+ destroy(): Promise<void>;
95
102
  }
96
103
  export { Worker_2 as Worker }
97
104
 
@@ -124,7 +131,7 @@ declare type WorkerEventMap_2<TResult> = {
124
131
  readonly success: readonly [id: string, result: TResult];
125
132
  /** A job settled with a terminal failure — its id + the error. */
126
133
  readonly failure: readonly [id: string, error: unknown];
127
- /** The worker was aborted — the cancel reason. */
134
+ /** The worker was aborted — the queue's coded abort error retaining the caller reason. */
128
135
  readonly abort: readonly [reason: unknown];
129
136
  /** The worker went idle — no pending jobs and none in flight. */
130
137
  readonly drain: readonly [];
@@ -154,12 +161,26 @@ export declare interface WorkerInterface<TInput, TResult> {
154
161
  /** Re-enqueue outstanding entries loaded from the store; no-op without a store. */
155
162
  restore(): Promise<void>;
156
163
  start(): void;
157
- stop(): void;
164
+ /** Stop the queue and await current-loop and durable cleanup quiescence. */
165
+ stop(): Promise<void>;
158
166
  pause(): void;
159
167
  resume(): void;
160
- abort(reason?: unknown): void;
161
- clear(): void;
162
- destroy(): void;
168
+ /**
169
+ * Cancel in-flight work, reject pending work, and await queue-owned cleanup.
170
+ *
171
+ * @param reason - Optional cause retained by the queue's coded abort error
172
+ * @returns The underlying queue's stable abort barrier
173
+ */
174
+ abort(reason?: unknown): Promise<void>;
175
+ /** Drop pending work and await its durable cleanup. */
176
+ clear(): Promise<void>;
177
+ /**
178
+ * Tear down the queue, then the pool, and finally the worker emitter.
179
+ *
180
+ * @returns One stable barrier shared by every call; it rejects with the original sole
181
+ * cleanup failure or an ordered `AggregateError` when both queue and pool fail
182
+ */
183
+ destroy(): Promise<void>;
163
184
  }
164
185
 
165
186
  /**
@@ -170,7 +191,8 @@ export declare interface WorkerInterface<TInput, TResult> {
170
191
  * retry while attempts remain (delegated to the underlying queue).
171
192
  * - `pool` — the {@link PoolOptions} for the resource the handler runs against; its
172
193
  * `max` defaults to `concurrency` so resources match the jobs in flight.
173
- * - `concurrency` — the maximum jobs in flight at once; defaults to `1`. Floored at `1`.
194
+ * - `concurrency` — the maximum jobs in flight at once; defaults to `1` and must be a
195
+ * positive safe integer, as validated by the underlying queue.
174
196
  * - `retries` — the default extra attempts per job on failure; defaults to `0`.
175
197
  * - `timeout` — the default per-attempt deadline in milliseconds; defaults to none.
176
198
  * - `store` — durable backing; outstanding entries survive a restart; call
@@ -187,6 +209,7 @@ declare interface WorkerOptions_2<TInput, TResource, TResult> {
187
209
  readonly pool: PoolOptions<TResource>;
188
210
  readonly concurrency?: number;
189
211
  readonly retries?: number;
212
+ /** Integer milliseconds in `0..2_147_483_647`; `0` disables the per-attempt deadline. */
190
213
  readonly timeout?: number;
191
214
  readonly store?: QueueStoreInterface<TInput>;
192
215
  }
@@ -29,7 +29,7 @@ import { QueueStoreInterface } from '@orkestrel/queue';
29
29
  *
30
30
  * @example
31
31
  * ```ts
32
- * import { createWorker } from '@src/core'
32
+ * import { createWorker } from '@orkestrel/worker'
33
33
  *
34
34
  * const worker = createWorker<Query, Connection, Rows>({
35
35
  * pool: { create: () => connect(), destroy: (connection) => connection.close() },
@@ -52,16 +52,23 @@ export declare function createWorker<TInput, TResource, TResult>(options: Worker
52
52
  * `options.pool`) and a `Queue` whose handler ACQUIRES a pooled resource, runs the
53
53
  * user handler against it, and RELEASES it in a `finally`. All concurrency, retries,
54
54
  * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
55
- * - **Resource ↔ concurrency.** The pool's `max` defaults to the worker's
56
- * `concurrency` (default `1`), so at most one resource exists per in-flight job and
57
- * idle resources are reused across jobs.
55
+ * - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive
56
+ * safe integer after caller options are captured once. Only `undefined` defaults
57
+ * `concurrency` to `1` or pool `max` to that value; runtime `null` reaches the owning
58
+ * validator. The queue validates before the pool option is read; every declared pool member
59
+ * is then captured once by direct access, preserving inherited and non-enumerable structural
60
+ * options. At most one resource exists per in-flight job by default, and idle resources are
61
+ * reused across jobs.
58
62
  * - **Acquire over the attempt signal.** Each job acquires using the attempt's
59
63
  * `execution.signal`, so an `abort` / `timeout` while waiting for a resource rejects
60
64
  * the acquire — the Queue then handles retry / rejection, and there is no token to
61
65
  * release (the resource was never leased).
62
66
  * - **Lifecycle (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /
63
67
  * `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`
64
- * read it. `destroy` destroys the queue then tears the pool down, idempotently.
68
+ * read it. `stop` / `abort` / `clear` return the queue's own cleanup barriers.
69
+ * `destroy` returns one stable barrier while it tears down the queue, then the pool,
70
+ * and destroys the worker emitter last. A sole cleanup failure is preserved by
71
+ * identity; failures from both layers become an ordered `AggregateError`.
65
72
  * - **Durability.** An optional `store` is passed straight through to the queue, so the
66
73
  * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).
67
74
  * - **Observable (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the
@@ -86,12 +93,12 @@ declare class Worker_2<TInput, TResource, TResult> implements WorkerInterface<TI
86
93
  enqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult>;
87
94
  restore(): Promise<void>;
88
95
  start(): void;
89
- stop(): void;
96
+ stop(): Promise<void>;
90
97
  pause(): void;
91
98
  resume(): void;
92
- abort(reason?: unknown): void;
93
- clear(): void;
94
- destroy(): void;
99
+ abort(reason?: unknown): Promise<void>;
100
+ clear(): Promise<void>;
101
+ destroy(): Promise<void>;
95
102
  }
96
103
  export { Worker_2 as Worker }
97
104
 
@@ -124,7 +131,7 @@ declare type WorkerEventMap_2<TResult> = {
124
131
  readonly success: readonly [id: string, result: TResult];
125
132
  /** A job settled with a terminal failure — its id + the error. */
126
133
  readonly failure: readonly [id: string, error: unknown];
127
- /** The worker was aborted — the cancel reason. */
134
+ /** The worker was aborted — the queue's coded abort error retaining the caller reason. */
128
135
  readonly abort: readonly [reason: unknown];
129
136
  /** The worker went idle — no pending jobs and none in flight. */
130
137
  readonly drain: readonly [];
@@ -154,12 +161,26 @@ export declare interface WorkerInterface<TInput, TResult> {
154
161
  /** Re-enqueue outstanding entries loaded from the store; no-op without a store. */
155
162
  restore(): Promise<void>;
156
163
  start(): void;
157
- stop(): void;
164
+ /** Stop the queue and await current-loop and durable cleanup quiescence. */
165
+ stop(): Promise<void>;
158
166
  pause(): void;
159
167
  resume(): void;
160
- abort(reason?: unknown): void;
161
- clear(): void;
162
- destroy(): void;
168
+ /**
169
+ * Cancel in-flight work, reject pending work, and await queue-owned cleanup.
170
+ *
171
+ * @param reason - Optional cause retained by the queue's coded abort error
172
+ * @returns The underlying queue's stable abort barrier
173
+ */
174
+ abort(reason?: unknown): Promise<void>;
175
+ /** Drop pending work and await its durable cleanup. */
176
+ clear(): Promise<void>;
177
+ /**
178
+ * Tear down the queue, then the pool, and finally the worker emitter.
179
+ *
180
+ * @returns One stable barrier shared by every call; it rejects with the original sole
181
+ * cleanup failure or an ordered `AggregateError` when both queue and pool fail
182
+ */
183
+ destroy(): Promise<void>;
163
184
  }
164
185
 
165
186
  /**
@@ -170,7 +191,8 @@ export declare interface WorkerInterface<TInput, TResult> {
170
191
  * retry while attempts remain (delegated to the underlying queue).
171
192
  * - `pool` — the {@link PoolOptions} for the resource the handler runs against; its
172
193
  * `max` defaults to `concurrency` so resources match the jobs in flight.
173
- * - `concurrency` — the maximum jobs in flight at once; defaults to `1`. Floored at `1`.
194
+ * - `concurrency` — the maximum jobs in flight at once; defaults to `1` and must be a
195
+ * positive safe integer, as validated by the underlying queue.
174
196
  * - `retries` — the default extra attempts per job on failure; defaults to `0`.
175
197
  * - `timeout` — the default per-attempt deadline in milliseconds; defaults to none.
176
198
  * - `store` — durable backing; outstanding entries survive a restart; call
@@ -187,6 +209,7 @@ declare interface WorkerOptions_2<TInput, TResource, TResult> {
187
209
  readonly pool: PoolOptions<TResource>;
188
210
  readonly concurrency?: number;
189
211
  readonly retries?: number;
212
+ /** Integer milliseconds in `0..2_147_483_647`; `0` disables the per-attempt deadline. */
190
213
  readonly timeout?: number;
191
214
  readonly store?: QueueStoreInterface<TInput>;
192
215
  }
@@ -11,16 +11,23 @@ import { Queue } from "@orkestrel/queue";
11
11
  * `options.pool`) and a `Queue` whose handler ACQUIRES a pooled resource, runs the
12
12
  * user handler against it, and RELEASES it in a `finally`. All concurrency, retries,
13
13
  * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
14
- * - **Resource ↔ concurrency.** The pool's `max` defaults to the worker's
15
- * `concurrency` (default `1`), so at most one resource exists per in-flight job and
16
- * idle resources are reused across jobs.
14
+ * - **Resource ↔ concurrency.** The queue strictly validates `concurrency` as a positive
15
+ * safe integer after caller options are captured once. Only `undefined` defaults
16
+ * `concurrency` to `1` or pool `max` to that value; runtime `null` reaches the owning
17
+ * validator. The queue validates before the pool option is read; every declared pool member
18
+ * is then captured once by direct access, preserving inherited and non-enumerable structural
19
+ * options. At most one resource exists per in-flight job by default, and idle resources are
20
+ * reused across jobs.
17
21
  * - **Acquire over the attempt signal.** Each job acquires using the attempt's
18
22
  * `execution.signal`, so an `abort` / `timeout` while waiting for a resource rejects
19
23
  * the acquire — the Queue then handles retry / rejection, and there is no token to
20
24
  * release (the resource was never leased).
21
25
  * - **Lifecycle (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /
22
26
  * `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`
23
- * read it. `destroy` destroys the queue then tears the pool down, idempotently.
27
+ * read it. `stop` / `abort` / `clear` return the queue's own cleanup barriers.
28
+ * `destroy` returns one stable barrier while it tears down the queue, then the pool,
29
+ * and destroys the worker emitter last. A sole cleanup failure is preserved by
30
+ * identity; failures from both layers become an ordered `AggregateError`.
24
31
  * - **Durability.** An optional `store` is passed straight through to the queue, so the
25
32
  * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).
26
33
  * - **Observable (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the
@@ -39,24 +46,30 @@ var Worker = class {
39
46
  #pool;
40
47
  #emitter;
41
48
  #handler;
42
- #destroyed = false;
49
+ #ending;
43
50
  constructor(options) {
44
- const concurrency = Math.max(1, options.concurrency ?? 1);
45
- this.#handler = options.handler;
51
+ const { concurrency: capturedConcurrency, handler, on, error, retries, timeout, store } = options;
52
+ const concurrency = capturedConcurrency === void 0 ? 1 : capturedConcurrency;
53
+ this.#handler = handler;
46
54
  this.#emitter = new Emitter({
47
- ...options.on !== void 0 ? { on: options.on } : {},
48
- ...options.error !== void 0 ? { error: options.error } : {}
49
- });
50
- this.#pool = new Pool({
51
- ...options.pool,
52
- max: options.pool.max ?? concurrency
55
+ ...on !== void 0 ? { on } : {},
56
+ ...error !== void 0 ? { error } : {}
53
57
  });
54
58
  this.#queue = new Queue({
55
59
  handler: this.#handle.bind(this),
56
60
  concurrency,
57
- ...options.retries !== void 0 ? { retries: options.retries } : {},
58
- ...options.timeout !== void 0 ? { timeout: options.timeout } : {},
59
- ...options.store !== void 0 ? { store: options.store } : {}
61
+ ...retries !== void 0 ? { retries } : {},
62
+ ...timeout !== void 0 ? { timeout } : {},
63
+ ...store !== void 0 ? { store } : {}
64
+ });
65
+ const { max, on: poolOn, error: poolError, create, destroy, validate } = options.pool;
66
+ this.#pool = new Pool({
67
+ create,
68
+ max: max === void 0 ? concurrency : max,
69
+ ...poolOn !== void 0 ? { on: poolOn } : {},
70
+ ...poolError !== void 0 ? { error: poolError } : {},
71
+ ...destroy !== void 0 ? { destroy } : {},
72
+ ...validate !== void 0 ? { validate } : {}
60
73
  });
61
74
  this.#bridge();
62
75
  }
@@ -85,7 +98,7 @@ var Worker = class {
85
98
  this.#queue.start();
86
99
  }
87
100
  stop() {
88
- this.#queue.stop();
101
+ return this.#queue.stop();
89
102
  }
90
103
  pause() {
91
104
  this.#queue.pause();
@@ -94,16 +107,17 @@ var Worker = class {
94
107
  this.#queue.resume();
95
108
  }
96
109
  abort(reason) {
97
- this.#queue.abort(reason);
110
+ return this.#queue.abort(reason);
98
111
  }
99
112
  clear() {
100
- this.#queue.clear();
113
+ return this.#queue.clear();
101
114
  }
102
115
  destroy() {
103
- if (this.#destroyed) return;
104
- this.#destroyed = true;
105
- this.#queue.destroy();
106
- this.#pool.destroy();
116
+ if (this.#ending !== void 0) return this.#ending.promise;
117
+ const ending = Promise.withResolvers();
118
+ this.#ending = ending;
119
+ this.#teardown(ending);
120
+ return ending.promise;
107
121
  }
108
122
  async #handle(input, execution) {
109
123
  const token = await this.#pool.acquire(execution.signal);
@@ -113,6 +127,23 @@ var Worker = class {
113
127
  token.release();
114
128
  }
115
129
  }
130
+ async #teardown(ending) {
131
+ const failures = [];
132
+ try {
133
+ await this.#queue.destroy();
134
+ } catch (error) {
135
+ failures.push(error);
136
+ }
137
+ try {
138
+ await this.#pool.destroy();
139
+ } catch (error) {
140
+ failures.push(error);
141
+ }
142
+ this.#emitter.destroy();
143
+ if (failures.length === 0) ending.resolve();
144
+ else if (failures.length === 1) ending.reject(failures[0]);
145
+ else ending.reject(new AggregateError(failures, "worker destroy cleanup failed"));
146
+ }
116
147
  #bridge() {
117
148
  const queue = this.#queue.emitter;
118
149
  queue.on("enqueue", (id) => this.#emitter.emit("enqueue", id));
@@ -149,7 +180,7 @@ var Worker = class {
149
180
  *
150
181
  * @example
151
182
  * ```ts
152
- * import { createWorker } from '@src/core'
183
+ * import { createWorker } from '@orkestrel/worker'
153
184
  *
154
185
  * const worker = createWorker<Query, Connection, Rows>({
155
186
  * pool: { create: () => connect(), destroy: (connection) => connection.close() },