@orkestrel/worker 0.0.4 → 0.0.6
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 +11 -4
- package/dist/src/core/index.cjs +55 -24
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +38 -15
- package/dist/src/core/index.d.ts +38 -15
- package/dist/src/core/index.js +55 -24
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +170 -74
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +57 -60
- package/dist/src/server/index.d.ts +57 -60
- package/dist/src/server/index.js +170 -74
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","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.js","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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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let node_worker_threads = require("node:worker_threads");
|
|
3
2
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
|
+
let node_worker_threads = require("node:worker_threads");
|
|
4
4
|
let _orkestrel_database_server = require("@orkestrel/database/server");
|
|
5
5
|
let _orkestrel_queue = require("@orkestrel/queue");
|
|
6
6
|
let _src_core = require("../core/index.cjs");
|
|
@@ -9,16 +9,16 @@ let _src_core = require("../core/index.cjs");
|
|
|
9
9
|
* Internal mutable implementation of the readonly {@link NodeThread} observation contract.
|
|
10
10
|
*
|
|
11
11
|
* @remarks
|
|
12
|
-
* Liveness and the first terminal error live behind runtime-private fields.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Liveness and the first terminal error live behind runtime-private fields. Thread `error`,
|
|
13
|
+
* `messageerror`, and `exit` all latch death, so pool validation cannot reuse a thread whose
|
|
14
|
+
* inbound message could not be deserialized.
|
|
15
15
|
*/
|
|
16
16
|
var Thread = class {
|
|
17
17
|
#worker;
|
|
18
18
|
#promise;
|
|
19
19
|
#resolve;
|
|
20
20
|
#reject;
|
|
21
|
-
#
|
|
21
|
+
#recordHandler;
|
|
22
22
|
#recordExitHandler;
|
|
23
23
|
#onlineHandler;
|
|
24
24
|
#spawnErrorHandler;
|
|
@@ -31,12 +31,13 @@ var Thread = class {
|
|
|
31
31
|
this.#promise = readiness.promise;
|
|
32
32
|
this.#resolve = readiness.resolve;
|
|
33
33
|
this.#reject = readiness.reject;
|
|
34
|
-
this.#
|
|
34
|
+
this.#recordHandler = this.#record.bind(this);
|
|
35
35
|
this.#recordExitHandler = this.#recordExit.bind(this);
|
|
36
36
|
this.#onlineHandler = this.#online.bind(this);
|
|
37
37
|
this.#spawnErrorHandler = this.#spawnError.bind(this);
|
|
38
38
|
this.#spawnExitHandler = this.#spawnExit.bind(this);
|
|
39
|
-
this.#worker.on("error", this.#
|
|
39
|
+
this.#worker.on("error", this.#recordHandler);
|
|
40
|
+
this.#worker.on("messageerror", this.#recordHandler);
|
|
40
41
|
this.#worker.on("exit", this.#recordExitHandler);
|
|
41
42
|
this.#worker.once("online", this.#onlineHandler);
|
|
42
43
|
this.#worker.once("error", this.#spawnErrorHandler);
|
|
@@ -57,7 +58,7 @@ var Thread = class {
|
|
|
57
58
|
evict() {
|
|
58
59
|
this.#alive = false;
|
|
59
60
|
}
|
|
60
|
-
#
|
|
61
|
+
#record(error) {
|
|
61
62
|
this.#alive = false;
|
|
62
63
|
if (this.#death === void 0) this.#death = error;
|
|
63
64
|
}
|
|
@@ -109,9 +110,10 @@ function isReply(value, id) {
|
|
|
109
110
|
* Internal lifecycle entity for one dispatched worker-thread job.
|
|
110
111
|
*
|
|
111
112
|
* @remarks
|
|
112
|
-
* Owns
|
|
113
|
-
* eviction for one dispatch.
|
|
114
|
-
*
|
|
113
|
+
* Owns stable `message` / `messageerror` / death listener identities, settlement, result-guard
|
|
114
|
+
* containment, and abort eviction for one dispatch. Deserialization failure, a matching-id
|
|
115
|
+
* malformed reply, and abort each evict and terminate the thread before rejecting, with
|
|
116
|
+
* termination failure preserved. Non-record, id-less, hostile-id, and foreign-id chatter is ignored.
|
|
115
117
|
*/
|
|
116
118
|
var Dispatch = class {
|
|
117
119
|
#thread;
|
|
@@ -124,6 +126,7 @@ var Dispatch = class {
|
|
|
124
126
|
#fulfill;
|
|
125
127
|
#reject;
|
|
126
128
|
#messageHandler;
|
|
129
|
+
#messageErrorHandler;
|
|
127
130
|
#errorHandler;
|
|
128
131
|
#exitHandler;
|
|
129
132
|
#abortHandler;
|
|
@@ -139,6 +142,7 @@ var Dispatch = class {
|
|
|
139
142
|
this.#fulfill = settlement.resolve;
|
|
140
143
|
this.#reject = settlement.reject;
|
|
141
144
|
this.#messageHandler = this.#message.bind(this);
|
|
145
|
+
this.#messageErrorHandler = this.#messageError.bind(this);
|
|
142
146
|
this.#errorHandler = this.#error.bind(this);
|
|
143
147
|
this.#exitHandler = this.#exit.bind(this);
|
|
144
148
|
this.#abortHandler = this.#abort.bind(this);
|
|
@@ -153,6 +157,7 @@ var Dispatch = class {
|
|
|
153
157
|
return;
|
|
154
158
|
}
|
|
155
159
|
this.#worker.on("message", this.#messageHandler);
|
|
160
|
+
this.#worker.on("messageerror", this.#messageErrorHandler);
|
|
156
161
|
this.#worker.on("error", this.#errorHandler);
|
|
157
162
|
this.#worker.on("exit", this.#exitHandler);
|
|
158
163
|
if (this.#execution.signal.aborted) {
|
|
@@ -163,6 +168,7 @@ var Dispatch = class {
|
|
|
163
168
|
try {
|
|
164
169
|
this.#worker.postMessage({
|
|
165
170
|
id: this.#id,
|
|
171
|
+
job: this.#execution.id,
|
|
166
172
|
command: "run",
|
|
167
173
|
input: this.#input
|
|
168
174
|
});
|
|
@@ -171,29 +177,70 @@ var Dispatch = class {
|
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
179
|
#message(value) {
|
|
174
|
-
if (!
|
|
180
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return;
|
|
181
|
+
const id = (0, _orkestrel_contract.attempt)(() => value.id);
|
|
182
|
+
if (!id.success || id.value !== this.#id) return;
|
|
183
|
+
if (!isReply(value, this.#id)) {
|
|
184
|
+
this.#terminate(/* @__PURE__ */ new Error("worker reply was malformed"));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
175
187
|
if (value.ok) {
|
|
176
188
|
const reply = value.value;
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
try {
|
|
190
|
+
if (this.#result(reply)) this.#succeed(reply);
|
|
191
|
+
else this.#fail(/* @__PURE__ */ new Error("reply did not satisfy result guard"));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
this.#fail(error);
|
|
194
|
+
}
|
|
179
195
|
return;
|
|
180
196
|
}
|
|
181
197
|
this.#fail(new Error(value.error));
|
|
182
198
|
}
|
|
199
|
+
#messageError(error) {
|
|
200
|
+
this.#terminate(error);
|
|
201
|
+
}
|
|
183
202
|
#error(error) {
|
|
184
203
|
this.#fail(error);
|
|
185
204
|
}
|
|
186
205
|
#exit() {
|
|
187
|
-
this.#fail(/* @__PURE__ */ new Error("worker thread exited"));
|
|
206
|
+
this.#fail(this.#thread.death ?? /* @__PURE__ */ new Error("worker thread exited"));
|
|
188
207
|
}
|
|
189
208
|
#abort() {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
209
|
+
const notification = [];
|
|
210
|
+
try {
|
|
211
|
+
this.#worker.postMessage({
|
|
212
|
+
id: this.#id,
|
|
213
|
+
command: "abort"
|
|
214
|
+
});
|
|
215
|
+
} catch (cause) {
|
|
216
|
+
notification.push(cause);
|
|
217
|
+
}
|
|
218
|
+
this.#terminate(this.#execution.signal.reason, notification);
|
|
219
|
+
}
|
|
220
|
+
#terminate(error, notification = []) {
|
|
221
|
+
if (this.#settled) return;
|
|
222
|
+
this.#settled = true;
|
|
223
|
+
this.#detach();
|
|
194
224
|
if (this.#thread instanceof Thread) this.#thread.evict();
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
let termination;
|
|
226
|
+
try {
|
|
227
|
+
termination = this.#worker.terminate();
|
|
228
|
+
} catch (cause) {
|
|
229
|
+
this.#reject(new AggregateError([
|
|
230
|
+
error,
|
|
231
|
+
...notification,
|
|
232
|
+
cause
|
|
233
|
+
], "worker termination failed"));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
termination.then(() => {
|
|
237
|
+
if (notification.length === 0) this.#reject(error);
|
|
238
|
+
else this.#reject(new AggregateError([error, ...notification], "worker abort notification failed"));
|
|
239
|
+
}, (cause) => this.#reject(new AggregateError([
|
|
240
|
+
error,
|
|
241
|
+
...notification,
|
|
242
|
+
cause
|
|
243
|
+
], "worker termination failed")));
|
|
197
244
|
}
|
|
198
245
|
#succeed(value) {
|
|
199
246
|
if (this.#settled) return;
|
|
@@ -209,6 +256,7 @@ var Dispatch = class {
|
|
|
209
256
|
}
|
|
210
257
|
#detach() {
|
|
211
258
|
this.#worker.off("message", this.#messageHandler);
|
|
259
|
+
this.#worker.off("messageerror", this.#messageErrorHandler);
|
|
212
260
|
this.#worker.off("error", this.#errorHandler);
|
|
213
261
|
this.#worker.off("exit", this.#exitHandler);
|
|
214
262
|
this.#execution.signal.removeEventListener("abort", this.#abortHandler);
|
|
@@ -227,11 +275,11 @@ var Dispatch = class {
|
|
|
227
275
|
* listeners that flip `alive` to `false` AND latch the first terminal event on
|
|
228
276
|
* {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via
|
|
229
277
|
* its own listeners), to the pool's `validate` (via `alive`), and — crucially — to a
|
|
230
|
-
* dispatch that attaches AFTER the death (via the latch).
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
278
|
+
* dispatch that attaches AFTER the death (via the latch). A `messageerror` is terminal too,
|
|
279
|
+
* so a thread whose inbound payload could not be deserialized is never reused. The latch
|
|
280
|
+
* closes a real race: a thread can become terminal before the readiness promise continuation
|
|
281
|
+
* hands it to `dispatch`, leaving no future death event for that dispatch to observe. Without
|
|
282
|
+
* the latch, that job would wait forever. The pool's `create` hook calls this.
|
|
235
283
|
*
|
|
236
284
|
* @param script - The worker module each thread runs (must call `serveWorker`)
|
|
237
285
|
* @param workerData - Opaque, structured-cloneable data handed to the thread at spawn
|
|
@@ -244,19 +292,23 @@ function spawnThread(script, workerData) {
|
|
|
244
292
|
* Dispatch one job to a leased {@link NodeThread} and await its narrowed reply.
|
|
245
293
|
*
|
|
246
294
|
* @remarks
|
|
247
|
-
* Mints a fresh `id`, posts
|
|
248
|
-
*
|
|
295
|
+
* Mints a fresh per-dispatch correlation `id`, posts it with `job: execution.id`, and
|
|
296
|
+
* resolves when the thread replies for that correlation id. The stable Queue job id reaches
|
|
297
|
+
* the worker handler for idempotency across retries and restore; it is not caller identity or
|
|
298
|
+
* authentication / authorization evidence. Per-job consumer context remains explicit,
|
|
299
|
+
* structured-cloneable `input`; ambient context is not worker-thread transport. A success
|
|
300
|
+
* `value` is narrowed through `result` (a value that fails the guard
|
|
249
301
|
* rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.
|
|
250
302
|
* A thread that ALREADY died rejects synchronously at entry from the latched
|
|
251
|
-
* {@link NodeThread.death} — its death events fired before this dispatch existed
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* job rejects.
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
303
|
+
* {@link NodeThread.death} — its death events fired before this dispatch existed and will
|
|
304
|
+
* never fire again, so waiting on the listeners below would dangle forever; the latch makes
|
|
305
|
+
* death total across every event ordering. If the thread `error`s / `exit`s mid-flight it is
|
|
306
|
+
* marked dead and the
|
|
307
|
+
* job rejects. An inbound `messageerror` also evicts and terminates the thread before
|
|
308
|
+
* rejection. On `execution.signal` abort it contains the cooperative `abort` post,
|
|
309
|
+
* evicts the thread, and observes `terminate()` settlement because CPU-bound work cannot
|
|
310
|
+
* honour the signal; the freed pool slot then gets a fresh thread. Every per-job listener
|
|
311
|
+
* (`message` / `messageerror` / `error` / `exit` / `abort`) is removed on settle.
|
|
260
312
|
*
|
|
261
313
|
* @typeParam TResult - The reply type the `result` guard narrows to
|
|
262
314
|
* @param thread - The leased thread to run the job on
|
|
@@ -271,13 +323,25 @@ function dispatch(thread, input, execution, result) {
|
|
|
271
323
|
//#endregion
|
|
272
324
|
//#region src/server/serve.ts
|
|
273
325
|
function isRecord(value) {
|
|
274
|
-
|
|
326
|
+
try {
|
|
327
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
275
331
|
}
|
|
276
332
|
function isRun(value) {
|
|
277
|
-
|
|
333
|
+
try {
|
|
334
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.job === "string" && value.command === "run" && "input" in value;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
278
338
|
}
|
|
279
339
|
function isAbort(value) {
|
|
280
|
-
|
|
340
|
+
try {
|
|
341
|
+
return isRecord(value) && typeof value.id === "string" && value.command === "abort";
|
|
342
|
+
} catch {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
281
345
|
}
|
|
282
346
|
/**
|
|
283
347
|
* Register a worker-thread handler — the worker-side half of {@link createNodeWorker}.
|
|
@@ -286,9 +350,15 @@ function isAbort(value) {
|
|
|
286
350
|
* Must be the spawned thread's module entry. It listens on the parent port for the
|
|
287
351
|
* run/abort protocol: a `run` message narrows its `input` through `options.input` (an
|
|
288
352
|
* invalid payload replies with an error envelope, never running the handler), then runs
|
|
289
|
-
* `options.handler(input, { signal })` and replies `{ id, ok: true, value }` on success or
|
|
290
|
-
* `{ id, ok: false, error }` on throw.
|
|
291
|
-
*
|
|
353
|
+
* `options.handler(input, { id: job, signal })` and replies `{ id, ok: true, value }` on success or
|
|
354
|
+
* `{ id, ok: false, error }` on throw. Input-guard throws use the same failure envelope. If a
|
|
355
|
+
* success value cannot be cloned, the post is retried as a clone-safe failure; if that post also
|
|
356
|
+
* fails, the parent port closes so the main side observes thread exit instead of waiting forever.
|
|
357
|
+
* The run envelope's `id` is fresh per dispatch and keys controllers, aborts, and replies;
|
|
358
|
+
* its `job` is the stable Queue idempotency key exposed as `execution.id` across retries
|
|
359
|
+
* and restore. That job id identifies work, not a caller, and is not authentication or
|
|
360
|
+
* authorization evidence. Each attempt has its own `AbortController`, so an `abort`
|
|
361
|
+
* message for the correlation id fires the handler's `signal` (cooperative — the main
|
|
292
362
|
* side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
|
|
293
363
|
* Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
|
|
294
364
|
* (`parentPort === null`) it is a no-op.
|
|
@@ -300,7 +370,7 @@ function isAbort(value) {
|
|
|
300
370
|
* @example
|
|
301
371
|
* ```ts
|
|
302
372
|
* // double.ts — a worker script
|
|
303
|
-
* import { serveWorker } from '@
|
|
373
|
+
* import { serveWorker } from '@orkestrel/worker/server'
|
|
304
374
|
*
|
|
305
375
|
* serveWorker<number, number>({
|
|
306
376
|
* input: (value): value is number => typeof value === 'number',
|
|
@@ -311,6 +381,8 @@ function isAbort(value) {
|
|
|
311
381
|
function serveWorker(options) {
|
|
312
382
|
const port = node_worker_threads.parentPort;
|
|
313
383
|
if (port === null) return;
|
|
384
|
+
const input = options.input;
|
|
385
|
+
const handler = options.handler;
|
|
314
386
|
const controllers = /* @__PURE__ */ new Map();
|
|
315
387
|
port.on("message", (raw) => {
|
|
316
388
|
if (isAbort(raw)) {
|
|
@@ -319,31 +391,39 @@ function serveWorker(options) {
|
|
|
319
391
|
}
|
|
320
392
|
if (!isRun(raw)) return;
|
|
321
393
|
const id = raw.id;
|
|
322
|
-
if (!options.input(raw.input)) {
|
|
323
|
-
port.postMessage({
|
|
324
|
-
id,
|
|
325
|
-
ok: false,
|
|
326
|
-
error: "input did not satisfy input guard"
|
|
327
|
-
});
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
const input = raw.input;
|
|
331
394
|
const controller = new AbortController();
|
|
332
395
|
controllers.set(id, controller);
|
|
333
|
-
Promise.resolve().then(() =>
|
|
396
|
+
Promise.resolve().then(() => {
|
|
397
|
+
if (!input(raw.input)) throw new Error("input did not satisfy input guard");
|
|
398
|
+
const value = raw.input;
|
|
399
|
+
return handler(value, {
|
|
400
|
+
id: raw.job,
|
|
401
|
+
signal: controller.signal
|
|
402
|
+
});
|
|
403
|
+
}).then((value) => {
|
|
334
404
|
controllers.delete(id);
|
|
335
405
|
port.postMessage({
|
|
336
406
|
id,
|
|
337
407
|
ok: true,
|
|
338
408
|
value
|
|
339
409
|
});
|
|
340
|
-
}
|
|
410
|
+
}).catch((error) => {
|
|
341
411
|
controllers.delete(id);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
412
|
+
let message = "worker operation failed";
|
|
413
|
+
try {
|
|
414
|
+
message = error instanceof Error ? error.message : String(error);
|
|
415
|
+
} catch {}
|
|
416
|
+
try {
|
|
417
|
+
port.postMessage({
|
|
418
|
+
id,
|
|
419
|
+
ok: false,
|
|
420
|
+
error: message
|
|
421
|
+
});
|
|
422
|
+
} catch {
|
|
423
|
+
try {
|
|
424
|
+
port.close();
|
|
425
|
+
} catch {}
|
|
426
|
+
}
|
|
347
427
|
});
|
|
348
428
|
});
|
|
349
429
|
}
|
|
@@ -357,9 +437,23 @@ function serveWorker(options) {
|
|
|
357
437
|
* public entity remains the plain core {@link WorkerInterface}.
|
|
358
438
|
*/
|
|
359
439
|
var NodeWorker = class {
|
|
360
|
-
#
|
|
440
|
+
#script;
|
|
441
|
+
#input;
|
|
442
|
+
#result;
|
|
443
|
+
#workerData;
|
|
444
|
+
#concurrency;
|
|
445
|
+
#retries;
|
|
446
|
+
#timeout;
|
|
447
|
+
#store;
|
|
361
448
|
constructor(options) {
|
|
362
|
-
this.#
|
|
449
|
+
this.#script = options.script;
|
|
450
|
+
this.#input = options.input;
|
|
451
|
+
this.#result = options.result;
|
|
452
|
+
this.#workerData = options.workerData;
|
|
453
|
+
this.#concurrency = options.concurrency;
|
|
454
|
+
this.#retries = options.retries;
|
|
455
|
+
this.#timeout = options.timeout;
|
|
456
|
+
this.#store = options.store;
|
|
363
457
|
}
|
|
364
458
|
build() {
|
|
365
459
|
return (0, _src_core.createWorker)({
|
|
@@ -367,17 +461,17 @@ var NodeWorker = class {
|
|
|
367
461
|
create: this.#create.bind(this),
|
|
368
462
|
destroy: this.#destroy.bind(this),
|
|
369
463
|
validate: this.#validate.bind(this),
|
|
370
|
-
...this.#
|
|
464
|
+
...this.#concurrency !== void 0 ? { max: this.#concurrency } : {}
|
|
371
465
|
},
|
|
372
466
|
handler: this.#handle.bind(this),
|
|
373
|
-
...this.#
|
|
374
|
-
...this.#
|
|
375
|
-
...this.#
|
|
376
|
-
...this.#
|
|
467
|
+
...this.#concurrency !== void 0 ? { concurrency: this.#concurrency } : {},
|
|
468
|
+
...this.#retries !== void 0 ? { retries: this.#retries } : {},
|
|
469
|
+
...this.#timeout !== void 0 ? { timeout: this.#timeout } : {},
|
|
470
|
+
...this.#store !== void 0 ? { store: this.#store } : {}
|
|
377
471
|
});
|
|
378
472
|
}
|
|
379
473
|
#create() {
|
|
380
|
-
return spawnThread(this.#
|
|
474
|
+
return spawnThread(this.#script, this.#workerData);
|
|
381
475
|
}
|
|
382
476
|
async #destroy(thread) {
|
|
383
477
|
await thread.worker.terminate();
|
|
@@ -386,8 +480,10 @@ var NodeWorker = class {
|
|
|
386
480
|
return thread.alive && thread.worker.threadId > 0;
|
|
387
481
|
}
|
|
388
482
|
#handle(input, thread, execution) {
|
|
389
|
-
|
|
390
|
-
|
|
483
|
+
const outcome = (0, _orkestrel_contract.attempt)(() => this.#input(input));
|
|
484
|
+
if (!outcome.success) return Promise.reject(outcome.error);
|
|
485
|
+
if (!outcome.value) return Promise.reject(/* @__PURE__ */ new Error("input did not satisfy input guard"));
|
|
486
|
+
return dispatch(thread, input, execution, this.#result);
|
|
391
487
|
}
|
|
392
488
|
};
|
|
393
489
|
//#endregion
|
|
@@ -412,8 +508,8 @@ var NodeWorker = class {
|
|
|
412
508
|
*
|
|
413
509
|
* @example
|
|
414
510
|
* ```ts
|
|
415
|
-
* import { stringShape } from '@
|
|
416
|
-
* import { createJSONQueueStore } from '@
|
|
511
|
+
* import { stringShape } from '@orkestrel/contract'
|
|
512
|
+
* import { createJSONQueueStore } from '@orkestrel/worker/server'
|
|
417
513
|
*
|
|
418
514
|
* const store = createJSONQueueStore('data/queue.json', stringShape())
|
|
419
515
|
* await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })
|
|
@@ -453,7 +549,7 @@ function createJSONQueueStore(path, input) {
|
|
|
453
549
|
*
|
|
454
550
|
* @example
|
|
455
551
|
* ```ts
|
|
456
|
-
* import { createNodeWorker } from '@
|
|
552
|
+
* import { createNodeWorker } from '@orkestrel/worker/server'
|
|
457
553
|
*
|
|
458
554
|
* const worker = createNodeWorker({
|
|
459
555
|
* script: new URL('./double.js', import.meta.url),
|
|
@@ -463,7 +559,7 @@ function createJSONQueueStore(path, input) {
|
|
|
463
559
|
* })
|
|
464
560
|
*
|
|
465
561
|
* const doubled = await worker.enqueue(21) // 42, computed on a worker thread
|
|
466
|
-
* worker.destroy() // terminates every thread
|
|
562
|
+
* await worker.destroy() // terminates every thread
|
|
467
563
|
* ```
|
|
468
564
|
*/
|
|
469
565
|
function createNodeWorker(options) {
|