@orkestrel/worker 0.0.10 → 0.0.11
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 +6 -4
- package/dist/src/core/index.cjs +11 -11
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +36 -35
- package/dist/src/core/index.d.ts +36 -35
- package/dist/src/core/index.js +11 -11
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +238 -216
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +131 -81
- package/dist/src/server/index.d.ts +131 -81
- package/dist/src/server/index.js +237 -215
- package/dist/src/server/index.js.map +1 -1
- package/package.json +14 -15
|
@@ -1 +1 @@
|
|
|
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
|
+
{"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 { 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 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 * `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 (§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, 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`) 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 * 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. 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 the optional `concurrency`, `retries`,\n * `timeout`, `store`, `on`, and `error` keys (see {@link WorkerOptions})\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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"}
|
|
@@ -4,9 +4,140 @@ 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");
|
|
7
|
+
//#region src/server/helpers.ts
|
|
8
|
+
/**
|
|
9
|
+
* Narrows an inbound `message` to a {@link Reply} for a given job `id` — no assertion.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* A total predicate: a record whose `id` matches and whose `ok` discriminant is well-formed.
|
|
13
|
+
* Anything else is rejected so a dispatch listener can ignore foreign or malformed messages.
|
|
14
|
+
* It correlates against the `id` argument rather than narrowing one value alone, so it is a
|
|
15
|
+
* correlated predicate rather than a `Guard<Reply>` and is not accepted where a `Guard` is.
|
|
16
|
+
*
|
|
17
|
+
* @param value - The inbound message to narrow
|
|
18
|
+
* @param id - The job id a matching reply must carry
|
|
19
|
+
* @returns True if the value is this job's well-formed reply; false otherwise
|
|
20
|
+
*/
|
|
21
|
+
function isReply(value, id) {
|
|
22
|
+
const outcome = (0, _orkestrel_contract.attempt)(() => {
|
|
23
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
24
|
+
if (value.id !== id) return false;
|
|
25
|
+
if (value.ok === true) return "value" in value;
|
|
26
|
+
return value.ok === false && typeof value.error === "string";
|
|
27
|
+
});
|
|
28
|
+
return outcome.success && outcome.value;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/server/handlers.ts
|
|
32
|
+
/**
|
|
33
|
+
* Registers a worker-thread handler — the worker-side half of {@link createNodeWorker}.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Must be the spawned thread's module entry. It listens on the parent port for the
|
|
37
|
+
* run/abort protocol: a `run` message narrows its `input` through `options.input` (an
|
|
38
|
+
* invalid payload replies with an error envelope, never running the handler), then runs
|
|
39
|
+
* `options.handler(input, { id: job, signal })` and replies `{ id, ok: true, value }` on success or
|
|
40
|
+
* `{ id, ok: false, error }` on throw. Input-guard throws use the same failure envelope. If a
|
|
41
|
+
* success value cannot be cloned, the post is retried as a clone-safe failure; if that post also
|
|
42
|
+
* fails, the parent port closes so the main side observes thread exit instead of waiting forever.
|
|
43
|
+
* The run envelope's `id` is fresh per dispatch and keys controllers, aborts, and replies;
|
|
44
|
+
* its `job` is the stable Queue idempotency key exposed as `context.id` across retries
|
|
45
|
+
* and restore. That job id identifies work, not a caller, and is not authentication or
|
|
46
|
+
* authorization evidence. Each attempt has its own `AbortController`, so an `abort`
|
|
47
|
+
* message for the correlation id fires the handler's `signal` (cooperative — the main
|
|
48
|
+
* side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
|
|
49
|
+
* Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
|
|
50
|
+
* (`parentPort === null`) it is a no-op.
|
|
51
|
+
*
|
|
52
|
+
* @typeParam TInput - The work payload (inferred from `options.input`)
|
|
53
|
+
* @typeParam TResult - The value the handler resolves (the reply payload)
|
|
54
|
+
* @param options - The `input` guard and the `handler` (see {@link ServeWorkerOptions})
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* // double.ts — a worker script
|
|
59
|
+
* import { serveWorker } from '@orkestrel/worker/server'
|
|
60
|
+
*
|
|
61
|
+
* serveWorker<number, number>({
|
|
62
|
+
* input: (value): value is number => typeof value === 'number',
|
|
63
|
+
* handler: (value) => value * 2,
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
function serveWorker(options) {
|
|
68
|
+
const port = node_worker_threads.parentPort;
|
|
69
|
+
if (port === null) return;
|
|
70
|
+
const input = options.input;
|
|
71
|
+
const handler = options.handler;
|
|
72
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
73
|
+
port.on("message", (raw) => {
|
|
74
|
+
let command;
|
|
75
|
+
let correlation;
|
|
76
|
+
let job;
|
|
77
|
+
let payload;
|
|
78
|
+
let carried = false;
|
|
79
|
+
try {
|
|
80
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
81
|
+
if ("command" in raw) command = raw.command;
|
|
82
|
+
if ("id" in raw) correlation = raw.id;
|
|
83
|
+
if ("job" in raw) job = raw.job;
|
|
84
|
+
if ("input" in raw) {
|
|
85
|
+
payload = raw.input;
|
|
86
|
+
carried = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (typeof correlation !== "string") return;
|
|
93
|
+
const id = correlation;
|
|
94
|
+
if (command === "abort") {
|
|
95
|
+
controllers.get(id)?.abort();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (command !== "run" || typeof job !== "string" || !carried) return;
|
|
99
|
+
const entry = job;
|
|
100
|
+
const value = payload;
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
controllers.set(id, controller);
|
|
103
|
+
Promise.resolve().then(() => {
|
|
104
|
+
if (!input(value)) throw new Error("input did not satisfy input guard");
|
|
105
|
+
return handler(value, {
|
|
106
|
+
id: entry,
|
|
107
|
+
signal: controller.signal
|
|
108
|
+
});
|
|
109
|
+
}).then((result) => {
|
|
110
|
+
controllers.delete(id);
|
|
111
|
+
port.postMessage({
|
|
112
|
+
id,
|
|
113
|
+
ok: true,
|
|
114
|
+
value: result
|
|
115
|
+
});
|
|
116
|
+
}).catch((error) => {
|
|
117
|
+
controllers.delete(id);
|
|
118
|
+
let message = "worker operation failed";
|
|
119
|
+
try {
|
|
120
|
+
message = error instanceof Error ? error.message : String(error);
|
|
121
|
+
} catch {}
|
|
122
|
+
try {
|
|
123
|
+
port.postMessage({
|
|
124
|
+
id,
|
|
125
|
+
ok: false,
|
|
126
|
+
error: message
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
try {
|
|
130
|
+
port.close();
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
7
137
|
//#region src/server/Thread.ts
|
|
8
138
|
/**
|
|
9
|
-
*
|
|
139
|
+
* Represents the internal mutable implementation of the readonly {@link NodeThread} observation
|
|
140
|
+
* contract.
|
|
10
141
|
*
|
|
11
142
|
* @remarks
|
|
12
143
|
* Liveness and the first terminal error live behind runtime-private fields. Thread `error`,
|
|
@@ -83,43 +214,57 @@ var Thread = class {
|
|
|
83
214
|
}
|
|
84
215
|
};
|
|
85
216
|
//#endregion
|
|
86
|
-
//#region src/server/
|
|
217
|
+
//#region src/server/Dispatch.ts
|
|
87
218
|
/**
|
|
88
|
-
*
|
|
219
|
+
* Represents one dispatched worker-thread job — the lifecycle entity behind a job posted to a
|
|
220
|
+
* leased {@link NodeThread}, whose {@link promise} settles with the narrowed reply.
|
|
89
221
|
*
|
|
90
222
|
* @remarks
|
|
91
|
-
*
|
|
92
|
-
*
|
|
223
|
+
* Mints a fresh per-dispatch correlation `id`, posts it with `job: context.id`, and settles
|
|
224
|
+
* when the thread replies for that correlation id. The stable Queue job id reaches the worker
|
|
225
|
+
* handler for idempotency across retries and restore; it is not caller identity or
|
|
226
|
+
* authentication / authorization evidence. Per-job consumer context is explicit,
|
|
227
|
+
* structured-cloneable `input`; ambient context is not worker-thread transport. A success
|
|
228
|
+
* `value` is narrowed through `result` (a value that fails the guard rejects — the zero-`as`
|
|
229
|
+
* type bridge); a failure rejects with the thread's error string. A thread that ALREADY died
|
|
230
|
+
* rejects synchronously at construction from the latched {@link NodeThread.death} — its death
|
|
231
|
+
* events fired before this dispatch existed and will never fire again, so waiting on the
|
|
232
|
+
* listeners would dangle forever; the latch makes death total across every event ordering. If
|
|
233
|
+
* the thread `error`s / `exit`s mid-flight the job rejects. On a `context.signal` abort it
|
|
234
|
+
* contains the cooperative `abort` post, evicts the thread, and observes `terminate()`
|
|
235
|
+
* settlement because CPU-bound work cannot honour the signal.
|
|
93
236
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
101
|
-
if (value.id !== id) return false;
|
|
102
|
-
if (value.ok === true) return "value" in value;
|
|
103
|
-
return value.ok === false && typeof value.error === "string";
|
|
104
|
-
});
|
|
105
|
-
return outcome.success && outcome.value;
|
|
106
|
-
}
|
|
107
|
-
//#endregion
|
|
108
|
-
//#region src/server/Dispatch.ts
|
|
109
|
-
/**
|
|
110
|
-
* Internal lifecycle entity for one dispatched worker-thread job.
|
|
237
|
+
* It owns stable `message` / `messageerror` / death listener identities, settlement,
|
|
238
|
+
* result-guard containment, and abort eviction for one dispatch. Deserialization failure, a
|
|
239
|
+
* matching-id malformed reply, and abort each evict and terminate the thread before rejecting,
|
|
240
|
+
* with termination failure preserved. Non-record, id-less, hostile-id, and foreign-id chatter
|
|
241
|
+
* is ignored. Every per-job listener (`message` / `messageerror` / `error` / `exit` / `abort`)
|
|
242
|
+
* is removed on settle.
|
|
111
243
|
*
|
|
112
|
-
* @
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
244
|
+
* Eviction reaches `alive` for a {@link NodeThread} this package produced. Against a
|
|
245
|
+
* consumer-supplied `NodeThread` an abort or a `messageerror` still terminates the supplied
|
|
246
|
+
* `worker` and rejects the job, and the implementer owns flipping its own `alive`.
|
|
247
|
+
*
|
|
248
|
+
* @typeParam TResult - The reply type the `result` guard narrows to
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* import { createThread, Dispatch } from '@orkestrel/worker/server'
|
|
253
|
+
*
|
|
254
|
+
* const isNumber = (value: unknown): value is number => typeof value === 'number'
|
|
255
|
+
*
|
|
256
|
+
* const thread = await createThread(new URL('./double.js', import.meta.url))
|
|
257
|
+
* const controller = new AbortController()
|
|
258
|
+
* const job = new Dispatch(thread, 21, { id: 'job-1', signal: controller.signal }, isNumber)
|
|
259
|
+
* console.log(await job.promise) // 42
|
|
260
|
+
* await thread.worker.terminate()
|
|
261
|
+
* ```
|
|
117
262
|
*/
|
|
118
263
|
var Dispatch = class {
|
|
119
264
|
#thread;
|
|
120
265
|
#worker;
|
|
121
266
|
#input;
|
|
122
|
-
#
|
|
267
|
+
#context;
|
|
123
268
|
#result;
|
|
124
269
|
#id = crypto.randomUUID();
|
|
125
270
|
#promise;
|
|
@@ -131,11 +276,11 @@ var Dispatch = class {
|
|
|
131
276
|
#exitHandler;
|
|
132
277
|
#abortHandler;
|
|
133
278
|
#settled = false;
|
|
134
|
-
constructor(thread, input,
|
|
279
|
+
constructor(thread, input, context, result) {
|
|
135
280
|
this.#thread = thread;
|
|
136
281
|
this.#worker = thread.worker;
|
|
137
282
|
this.#input = input;
|
|
138
|
-
this.#
|
|
283
|
+
this.#context = context;
|
|
139
284
|
this.#result = result;
|
|
140
285
|
const settlement = Promise.withResolvers();
|
|
141
286
|
this.#promise = settlement.promise;
|
|
@@ -160,15 +305,15 @@ var Dispatch = class {
|
|
|
160
305
|
this.#worker.on("messageerror", this.#messageErrorHandler);
|
|
161
306
|
this.#worker.on("error", this.#errorHandler);
|
|
162
307
|
this.#worker.on("exit", this.#exitHandler);
|
|
163
|
-
if (this.#
|
|
308
|
+
if (this.#context.signal.aborted) {
|
|
164
309
|
this.#abort();
|
|
165
310
|
return;
|
|
166
311
|
}
|
|
167
|
-
this.#
|
|
312
|
+
this.#context.signal.addEventListener("abort", this.#abortHandler, { once: true });
|
|
168
313
|
try {
|
|
169
314
|
this.#worker.postMessage({
|
|
170
315
|
id: this.#id,
|
|
171
|
-
job: this.#
|
|
316
|
+
job: this.#context.id,
|
|
172
317
|
command: "run",
|
|
173
318
|
input: this.#input
|
|
174
319
|
});
|
|
@@ -215,7 +360,7 @@ var Dispatch = class {
|
|
|
215
360
|
} catch (cause) {
|
|
216
361
|
notification.push(cause);
|
|
217
362
|
}
|
|
218
|
-
this.#terminate(this.#
|
|
363
|
+
this.#terminate(this.#context.signal.reason, notification);
|
|
219
364
|
}
|
|
220
365
|
#terminate(error, notification = []) {
|
|
221
366
|
if (this.#settled) return;
|
|
@@ -259,183 +404,21 @@ var Dispatch = class {
|
|
|
259
404
|
this.#worker.off("messageerror", this.#messageErrorHandler);
|
|
260
405
|
this.#worker.off("error", this.#errorHandler);
|
|
261
406
|
this.#worker.off("exit", this.#exitHandler);
|
|
262
|
-
this.#
|
|
407
|
+
this.#context.signal.removeEventListener("abort", this.#abortHandler);
|
|
263
408
|
}
|
|
264
409
|
};
|
|
265
410
|
//#endregion
|
|
266
|
-
//#region src/server/helpers.ts
|
|
267
|
-
/**
|
|
268
|
-
* Spawn one worker thread and resolve a live {@link NodeThread} once it comes online.
|
|
269
|
-
*
|
|
270
|
-
* @remarks
|
|
271
|
-
* Constructs the thread with the `script` module and the cloned `workerData`, then
|
|
272
|
-
* resolves on the thread's `online` event (rejecting on an early `error` OR an `exit`
|
|
273
|
-
* that arrives before `online`, so the spawn promise is total — it can never dangle on a
|
|
274
|
-
* thread that died without erroring). The wrapper attaches persistent `error` / `exit`
|
|
275
|
-
* listeners that flip `alive` to `false` AND latch the first terminal event on
|
|
276
|
-
* {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via
|
|
277
|
-
* its own listeners), to the pool's `validate` (via `alive`), and — crucially — to a
|
|
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.
|
|
283
|
-
*
|
|
284
|
-
* @param script - The worker module each thread runs (must call `serveWorker`)
|
|
285
|
-
* @param workerData - Opaque, structured-cloneable data handed to the thread at spawn
|
|
286
|
-
* @returns A promise resolving the online {@link NodeThread}
|
|
287
|
-
*/
|
|
288
|
-
function spawnThread(script, workerData) {
|
|
289
|
-
return new Thread(script, workerData).promise;
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Dispatch one job to a leased {@link NodeThread} and await its narrowed reply.
|
|
293
|
-
*
|
|
294
|
-
* @remarks
|
|
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
|
|
301
|
-
* rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.
|
|
302
|
-
* A thread that ALREADY died rejects synchronously at entry from the latched
|
|
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.
|
|
312
|
-
*
|
|
313
|
-
* @typeParam TResult - The reply type the `result` guard narrows to
|
|
314
|
-
* @param thread - The leased thread to run the job on
|
|
315
|
-
* @param input - The work payload (structured-cloned to the thread)
|
|
316
|
-
* @param execution - The per-attempt handle; its `signal` aborts → terminate + evict
|
|
317
|
-
* @param result - The {@link Guard} narrowing the reply value with no assertion
|
|
318
|
-
* @returns A promise resolving the narrowed `TResult`, or rejecting on error / abort
|
|
319
|
-
*/
|
|
320
|
-
function dispatch(thread, input, execution, result) {
|
|
321
|
-
return new Dispatch(thread, input, execution, result).promise;
|
|
322
|
-
}
|
|
323
|
-
//#endregion
|
|
324
|
-
//#region src/server/handlers.ts
|
|
325
|
-
/**
|
|
326
|
-
* Register a worker-thread handler — the worker-side half of {@link createNodeWorker}.
|
|
327
|
-
*
|
|
328
|
-
* @remarks
|
|
329
|
-
* Must be the spawned thread's module entry. It listens on the parent port for the
|
|
330
|
-
* run/abort protocol: a `run` message narrows its `input` through `options.input` (an
|
|
331
|
-
* invalid payload replies with an error envelope, never running the handler), then runs
|
|
332
|
-
* `options.handler(input, { id: job, signal })` and replies `{ id, ok: true, value }` on success or
|
|
333
|
-
* `{ id, ok: false, error }` on throw. Input-guard throws use the same failure envelope. If a
|
|
334
|
-
* success value cannot be cloned, the post is retried as a clone-safe failure; if that post also
|
|
335
|
-
* fails, the parent port closes so the main side observes thread exit instead of waiting forever.
|
|
336
|
-
* The run envelope's `id` is fresh per dispatch and keys controllers, aborts, and replies;
|
|
337
|
-
* its `job` is the stable Queue idempotency key exposed as `execution.id` across retries
|
|
338
|
-
* and restore. That job id identifies work, not a caller, and is not authentication or
|
|
339
|
-
* authorization evidence. Each attempt has its own `AbortController`, so an `abort`
|
|
340
|
-
* message for the correlation id fires the handler's `signal` (cooperative — the main
|
|
341
|
-
* side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
|
|
342
|
-
* Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
|
|
343
|
-
* (`parentPort === null`) it is a no-op.
|
|
344
|
-
*
|
|
345
|
-
* @typeParam TInput - The work payload (inferred from `options.input`)
|
|
346
|
-
* @typeParam TResult - The value the handler resolves (the reply payload)
|
|
347
|
-
* @param options - The `input` guard and the `handler` (see {@link ServeWorkerOptions})
|
|
348
|
-
*
|
|
349
|
-
* @example
|
|
350
|
-
* ```ts
|
|
351
|
-
* // double.ts — a worker script
|
|
352
|
-
* import { serveWorker } from '@orkestrel/worker/server'
|
|
353
|
-
*
|
|
354
|
-
* serveWorker<number, number>({
|
|
355
|
-
* input: (value): value is number => typeof value === 'number',
|
|
356
|
-
* handler: (value) => value * 2,
|
|
357
|
-
* })
|
|
358
|
-
* ```
|
|
359
|
-
*/
|
|
360
|
-
function serveWorker(options) {
|
|
361
|
-
const port = node_worker_threads.parentPort;
|
|
362
|
-
if (port === null) return;
|
|
363
|
-
const input = options.input;
|
|
364
|
-
const handler = options.handler;
|
|
365
|
-
const controllers = /* @__PURE__ */ new Map();
|
|
366
|
-
port.on("message", (raw) => {
|
|
367
|
-
let command;
|
|
368
|
-
let correlation;
|
|
369
|
-
let job;
|
|
370
|
-
let payload;
|
|
371
|
-
let carried = false;
|
|
372
|
-
try {
|
|
373
|
-
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
374
|
-
if ("command" in raw) command = raw.command;
|
|
375
|
-
if ("id" in raw) correlation = raw.id;
|
|
376
|
-
if ("job" in raw) job = raw.job;
|
|
377
|
-
if ("input" in raw) {
|
|
378
|
-
payload = raw.input;
|
|
379
|
-
carried = true;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
} catch {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (typeof correlation !== "string") return;
|
|
386
|
-
const id = correlation;
|
|
387
|
-
if (command === "abort") {
|
|
388
|
-
controllers.get(id)?.abort();
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (command !== "run" || typeof job !== "string" || !carried) return;
|
|
392
|
-
const execution = job;
|
|
393
|
-
const value = payload;
|
|
394
|
-
const controller = new AbortController();
|
|
395
|
-
controllers.set(id, controller);
|
|
396
|
-
Promise.resolve().then(() => {
|
|
397
|
-
if (!input(value)) throw new Error("input did not satisfy input guard");
|
|
398
|
-
return handler(value, {
|
|
399
|
-
id: execution,
|
|
400
|
-
signal: controller.signal
|
|
401
|
-
});
|
|
402
|
-
}).then((result) => {
|
|
403
|
-
controllers.delete(id);
|
|
404
|
-
port.postMessage({
|
|
405
|
-
id,
|
|
406
|
-
ok: true,
|
|
407
|
-
value: result
|
|
408
|
-
});
|
|
409
|
-
}).catch((error) => {
|
|
410
|
-
controllers.delete(id);
|
|
411
|
-
let message = "worker operation failed";
|
|
412
|
-
try {
|
|
413
|
-
message = error instanceof Error ? error.message : String(error);
|
|
414
|
-
} catch {}
|
|
415
|
-
try {
|
|
416
|
-
port.postMessage({
|
|
417
|
-
id,
|
|
418
|
-
ok: false,
|
|
419
|
-
error: message
|
|
420
|
-
});
|
|
421
|
-
} catch {
|
|
422
|
-
try {
|
|
423
|
-
port.close();
|
|
424
|
-
} catch {}
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
//#endregion
|
|
430
411
|
//#region src/server/NodeWorker.ts
|
|
431
412
|
/**
|
|
432
|
-
*
|
|
413
|
+
* Represents the internal composition entity backing {@link createNodeWorker}.
|
|
433
414
|
*
|
|
434
415
|
* @remarks
|
|
435
416
|
* Supplies bound Pool and Queue operations without nested function assignments. The resulting
|
|
436
|
-
* public entity
|
|
417
|
+
* public entity is the plain core {@link WorkerInterface}.
|
|
437
418
|
*/
|
|
438
419
|
var NodeWorker = class {
|
|
420
|
+
#on;
|
|
421
|
+
#error;
|
|
439
422
|
#script;
|
|
440
423
|
#input;
|
|
441
424
|
#result;
|
|
@@ -445,6 +428,8 @@ var NodeWorker = class {
|
|
|
445
428
|
#timeout;
|
|
446
429
|
#store;
|
|
447
430
|
constructor(options) {
|
|
431
|
+
this.#on = options.on;
|
|
432
|
+
this.#error = options.error;
|
|
448
433
|
this.#script = options.script;
|
|
449
434
|
this.#input = options.input;
|
|
450
435
|
this.#result = options.result;
|
|
@@ -463,6 +448,8 @@ var NodeWorker = class {
|
|
|
463
448
|
...this.#concurrency !== void 0 ? { max: this.#concurrency } : {}
|
|
464
449
|
},
|
|
465
450
|
handler: this.#handle.bind(this),
|
|
451
|
+
...this.#on !== void 0 ? { on: this.#on } : {},
|
|
452
|
+
...this.#error !== void 0 ? { error: this.#error } : {},
|
|
466
453
|
...this.#concurrency !== void 0 ? { concurrency: this.#concurrency } : {},
|
|
467
454
|
...this.#retries !== void 0 ? { retries: this.#retries } : {},
|
|
468
455
|
...this.#timeout !== void 0 ? { timeout: this.#timeout } : {},
|
|
@@ -470,7 +457,7 @@ var NodeWorker = class {
|
|
|
470
457
|
});
|
|
471
458
|
}
|
|
472
459
|
#create() {
|
|
473
|
-
return
|
|
460
|
+
return new Thread(this.#script, this.#workerData).promise;
|
|
474
461
|
}
|
|
475
462
|
async #destroy(thread) {
|
|
476
463
|
await thread.worker.terminate();
|
|
@@ -478,21 +465,55 @@ var NodeWorker = class {
|
|
|
478
465
|
#validate(thread) {
|
|
479
466
|
return thread.alive && thread.worker.threadId > 0;
|
|
480
467
|
}
|
|
481
|
-
#handle(input, thread,
|
|
468
|
+
#handle(input, thread, context) {
|
|
482
469
|
const outcome = (0, _orkestrel_contract.attempt)(() => this.#input(input));
|
|
483
470
|
if (!outcome.success) return Promise.reject(outcome.error);
|
|
484
471
|
if (!outcome.value) return Promise.reject(/* @__PURE__ */ new Error("input did not satisfy input guard"));
|
|
485
|
-
return
|
|
472
|
+
return new Dispatch(thread, input, context, this.#result).promise;
|
|
486
473
|
}
|
|
487
474
|
};
|
|
488
475
|
//#endregion
|
|
489
476
|
//#region src/server/factories.ts
|
|
490
477
|
/**
|
|
491
|
-
*
|
|
478
|
+
* Creates one live worker thread and resolves it as a {@link NodeThread} after it comes
|
|
479
|
+
* online.
|
|
480
|
+
*
|
|
481
|
+
* @remarks
|
|
482
|
+
* Constructs the thread with the `script` module and the cloned `workerData`, then
|
|
483
|
+
* resolves on the thread's `online` event (rejecting on an early `error` OR an `exit`
|
|
484
|
+
* that arrives before `online`, so the spawn promise is total — it can never dangle on a
|
|
485
|
+
* thread that died without erroring). The returned entity attaches persistent `error` /
|
|
486
|
+
* `exit` listeners that flip `alive` to `false` AND latch the first terminal event on
|
|
487
|
+
* {@link NodeThread.death}: a crash is observable to an in-flight {@link Dispatch} (through
|
|
488
|
+
* its own listeners), to a pool's `validate` (through `alive`), and — crucially — to a
|
|
489
|
+
* dispatch that attaches AFTER the death (through the latch). A `messageerror` is terminal
|
|
490
|
+
* too, so a thread whose inbound payload could not be deserialized is never reused. The latch
|
|
491
|
+
* closes a real race: a thread can become terminal before the readiness promise continuation
|
|
492
|
+
* hands it to a {@link Dispatch}, leaving no future death event for that dispatch to observe.
|
|
493
|
+
* Without the latch, that job would wait forever. {@link createNodeWorker} spawns its pooled
|
|
494
|
+
* threads the same way; reach for this to drive one thread yourself.
|
|
495
|
+
*
|
|
496
|
+
* @param script - The worker module the thread runs (its module must call `serveWorker`)
|
|
497
|
+
* @param workerData - Opaque, structured-cloneable data handed to the thread at spawn
|
|
498
|
+
* @returns A promise resolving the online {@link NodeThread}
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```ts
|
|
502
|
+
* import { createThread } from '@orkestrel/worker/server'
|
|
503
|
+
*
|
|
504
|
+
* const thread = await createThread(new URL('./double.js', import.meta.url))
|
|
505
|
+
* await thread.worker.terminate()
|
|
506
|
+
* ```
|
|
507
|
+
*/
|
|
508
|
+
function createThread(script, workerData) {
|
|
509
|
+
return new Thread(script, workerData).promise;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Creates a persistent JSON-file {@link QueueStoreInterface} — the core
|
|
492
513
|
* `createDatabaseQueueStore` over a server {@link createJSONDriver}.
|
|
493
514
|
*
|
|
494
515
|
* @remarks
|
|
495
|
-
* A queue's durable state is
|
|
516
|
+
* A queue's durable state is a database table, so JSON persistence reuses the
|
|
496
517
|
* existing JSON-file driver rather than a bespoke store: the entries are written to
|
|
497
518
|
* (and reloaded from) the file at `path`, surviving a process restart. There is no new
|
|
498
519
|
* class — the store engine ({@link createDatabaseQueueStore}) is shared, and only the
|
|
@@ -521,20 +542,21 @@ function createJSONQueueStore(path, input) {
|
|
|
521
542
|
return (0, _orkestrel_queue.createDatabaseQueueStore)(input, (0, _orkestrel_database_server.createJSONDriver)(path));
|
|
522
543
|
}
|
|
523
544
|
/**
|
|
524
|
-
*
|
|
545
|
+
* Creates a CPU-parallel worker over `node:worker_threads` — a thin specialization of the
|
|
525
546
|
* core `createWorker` whose pooled resource is a worker THREAD.
|
|
526
547
|
*
|
|
527
548
|
* @remarks
|
|
528
549
|
* Composition, not reimplementation: all concurrency, retries, per-attempt timeout,
|
|
529
550
|
* lifecycle, and durability are the core `Worker`'s (a `Queue` ⨉ `Pool`). This factory
|
|
530
|
-
* supplies only the thread pairing — the pool `create`s a thread (
|
|
531
|
-
* `destroy`s it with `terminate()`, and `validate`s it by
|
|
532
|
-
* evicted / crashed thread is dropped and replaced) — and an
|
|
533
|
-
* narrows the input through `options.input` (fail-fast before the
|
|
534
|
-
* boundary) then
|
|
551
|
+
* supplies only the thread pairing — the pool `create`s a thread (the same spawn
|
|
552
|
+
* {@link createThread} publishes), `destroy`s it with `terminate()`, and `validate`s it by
|
|
553
|
+
* `alive && threadId > 0` (so an evicted / crashed thread is dropped and replaced) — and an
|
|
554
|
+
* internal handler that narrows the input through `options.input` (fail-fast before the
|
|
555
|
+
* structured-clone boundary) then runs a {@link Dispatch} against the leased thread,
|
|
556
|
+
* narrowing the reply through
|
|
535
557
|
* `options.result`. Both generics INFER from the `input` / `result` guards, so call sites
|
|
536
558
|
* need no explicit type arguments. The boundary is crossed with ZERO `as`: the guards
|
|
537
|
-
* reconstruct `TInput` / `TResult` by validation
|
|
559
|
+
* reconstruct `TInput` / `TResult` by validation. An `abort` / `timeout`
|
|
538
560
|
* TERMINATES the in-flight thread (CPU-bound work can't honour a signal) and evicts it; a
|
|
539
561
|
* subsequent job spawns a fresh thread. The worker script's module must call
|
|
540
562
|
* `serveWorker`. Returns the plain {@link WorkerInterface} — its methods are the Worker's.
|
|
@@ -542,7 +564,7 @@ function createJSONQueueStore(path, input) {
|
|
|
542
564
|
* @typeParam TInput - The work payload each job carries (inferred from `input`)
|
|
543
565
|
* @typeParam TResult - The value a thread resolves for a job (inferred from `result`)
|
|
544
566
|
* @param options - The `script` plus the `input` / `result` guards and optional
|
|
545
|
-
* `workerData` / `concurrency` / `retries` / `timeout` / `store`
|
|
567
|
+
* `on` / `error` / `workerData` / `concurrency` / `retries` / `timeout` / `store`
|
|
546
568
|
* (see {@link NodeWorkerOptions})
|
|
547
569
|
* @returns A working {@link WorkerInterface} backed by a thread pool
|
|
548
570
|
*
|
|
@@ -565,11 +587,11 @@ function createNodeWorker(options) {
|
|
|
565
587
|
return new NodeWorker(options).build();
|
|
566
588
|
}
|
|
567
589
|
//#endregion
|
|
590
|
+
exports.Dispatch = Dispatch;
|
|
568
591
|
exports.createJSONQueueStore = createJSONQueueStore;
|
|
569
592
|
exports.createNodeWorker = createNodeWorker;
|
|
570
|
-
exports.
|
|
593
|
+
exports.createThread = createThread;
|
|
571
594
|
exports.isReply = isReply;
|
|
572
595
|
exports.serveWorker = serveWorker;
|
|
573
|
-
exports.spawnThread = spawnThread;
|
|
574
596
|
|
|
575
597
|
//# sourceMappingURL=index.cjs.map
|