@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.
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,SAAb,MAA4F;CAC3F;CACA;CAIA;CACA;CACA;CAEA,YAAY,SAAoD;EAC/D,MAAM,EACL,aAAa,qBACb,SACA,IACA,OACA,SACA,SACA,UACG;EACJ,MAAM,cAAc,wBAAwB,KAAA,IAAY,IAAI;EAC5D,KAAKG,WAAW;EAChB,KAAKD,WAAW,IAAI,QAAiC;GACpD,GAAI,OAAO,KAAA,IAAY,EAAE,GAAG,IAAI,CAAC;GACjC,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACxC,CAAC;EACD,KAAKF,SAAS,IAAI,MAAuB;GACxC,SAAS,KAAKI,QAAQ,KAAK,IAAI;GAC/B;GACA,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;GAC3C,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;GAC3C,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACxC,CAAC;EAED,MAAM,EAAE,KAAK,IAAI,QAAQ,OAAO,WAAW,QAAQ,SAAS,aAD/C,QAAQ;EAErB,KAAKH,QAAQ,IAAI,KAAgB;GAChC;GACA,KAAK,QAAQ,KAAA,IAAY,cAAc;GACvC,GAAI,WAAW,KAAA,IAAY,EAAE,IAAI,OAAO,IAAI,CAAC;GAC7C,GAAI,cAAc,KAAA,IAAY,EAAE,OAAO,UAAU,IAAI,CAAC;GACtD,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;GAC3C,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC9C,CAAC;EACD,KAAKI,QAAQ;CACd;CAEA,IAAI,UAAqD;EACxD,OAAO,KAAKH;CACb;CAEA,IAAI,QAAgB;EACnB,OAAO,KAAKF,OAAO;CACpB;CAEA,IAAI,SAAiB;EACpB,OAAO,KAAKA,OAAO;CACpB;CAEA,IAAI,SAAkB;EACrB,OAAO,KAAKA,OAAO;CACpB;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAKA,OAAO;CACpB;CAEA,QAAQ,OAAe,SAA+C;EACrE,OAAO,KAAKA,OAAO,QAAQ,OAAO,OAAO;CAC1C;CAEA,UAAyB;EACxB,OAAO,KAAKA,OAAO,QAAQ;CAC5B;CAEA,QAAc;EACb,KAAKA,OAAO,MAAM;CACnB;CAEA,OAAsB;EACrB,OAAO,KAAKA,OAAO,KAAK;CACzB;CAEA,QAAc;EACb,KAAKA,OAAO,MAAM;CACnB;CAEA,SAAe;EACd,KAAKA,OAAO,OAAO;CACpB;CAEA,MAAM,QAAiC;EACtC,OAAO,KAAKA,OAAO,MAAM,MAAM;CAChC;CAEA,QAAuB;EACtB,OAAO,KAAKA,OAAO,MAAM;CAC1B;CAEA,UAAyB;EACxB,IAAI,KAAKM,YAAY,KAAA,GAAW,OAAO,KAAKA,QAAQ;EACpD,MAAM,SAAS,QAAQ,cAAoB;EAC3C,KAAKA,UAAU;EACf,KAAUC,UAAU,MAAM;EAC1B,OAAO,OAAO;CACf;CAEA,MAAMH,QAAQ,OAAe,SAAyC;EACrE,MAAM,QAAQ,MAAM,KAAKH,MAAM,QAAQ,QAAQ,MAAM;EACrD,IAAI;GACH,OAAO,MAAM,KAAKE,SAAS,OAAO,MAAM,OAAO,OAAO;EACvD,UAAU;GACT,MAAM,QAAQ;EACf;CACD;CAEA,MAAMI,UAAU,QAAmD;EAClE,MAAM,WAAsB,CAAC;EAC7B,IAAI;GACH,MAAM,KAAKP,OAAO,QAAQ;EAC3B,SAAS,OAAO;GACf,SAAS,KAAK,KAAK;EACpB;EACA,IAAI;GACH,MAAM,KAAKC,MAAM,QAAQ;EAC1B,SAAS,OAAO;GACf,SAAS,KAAK,KAAK;EACpB;EACA,KAAKC,SAAS,QAAQ;EACtB,IAAI,SAAS,WAAW,GAAG,OAAO,QAAQ;OACrC,IAAI,SAAS,WAAW,GAAG,OAAO,OAAO,SAAS,EAAE;OACpD,OAAO,OAAO,IAAI,eAAe,UAAU,+BAA+B,CAAC;CACjF;CASA,UAAgB;EACf,MAAM,QAAQ,KAAKF,OAAO;EAC1B,MAAM,GAAG,YAAY,OAAO,KAAKE,SAAS,KAAK,WAAW,EAAE,CAAC;EAC7D,MAAM,GAAG,UAAU,OAAO,KAAKA,SAAS,KAAK,SAAS,EAAE,CAAC;EACzD,MAAM,GAAG,UAAU,IAAI,YAAY,KAAKA,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC;EAC3E,MAAM,GAAG,YAAY,IAAI,WAAW,KAAKA,SAAS,KAAK,WAAW,IAAI,MAAM,CAAC;EAC7E,MAAM,GAAG,YAAY,IAAI,UAAU,KAAKA,SAAS,KAAK,WAAW,IAAI,KAAK,CAAC;EAC3E,MAAM,GAAG,UAAU,WAAW,KAAKA,SAAS,KAAK,SAAS,MAAM,CAAC;EACjE,MAAM,GAAG,eAAe,KAAKA,SAAS,KAAK,OAAO,CAAC;CACpD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9JA,SAAgB,aACf,SACmC;CACnC,OAAO,IAAI,OAAO,OAAO;AAC1B"}
@@ -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
- * Internal mutable implementation of the readonly {@link NodeThread} observation contract.
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/validators.ts
217
+ //#region src/server/Dispatch.ts
87
218
  /**
88
- * Narrow an inbound `message` to a {@link Reply} for a given job `id` no assertion.
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
- * A total predicate: a record whose `id` matches and whose `ok` discriminant is well-formed.
92
- * Anything else is rejected so a dispatch listener can ignore foreign or malformed messages.
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
- * @param value - The inbound message to narrow
95
- * @param id - The job id a matching reply must carry
96
- * @returns `true` when the value is this job's well-formed reply
97
- */
98
- function isReply(value, id) {
99
- const outcome = (0, _orkestrel_contract.attempt)(() => {
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
- * @remarks
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.
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
- #execution;
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, execution, result) {
279
+ constructor(thread, input, context, result) {
135
280
  this.#thread = thread;
136
281
  this.#worker = thread.worker;
137
282
  this.#input = input;
138
- this.#execution = execution;
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.#execution.signal.aborted) {
308
+ if (this.#context.signal.aborted) {
164
309
  this.#abort();
165
310
  return;
166
311
  }
167
- this.#execution.signal.addEventListener("abort", this.#abortHandler, { once: true });
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.#execution.id,
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.#execution.signal.reason, notification);
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.#execution.signal.removeEventListener("abort", this.#abortHandler);
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
- * Internal composition entity backing {@link createNodeWorker}.
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 remains the plain core {@link WorkerInterface}.
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 spawnThread(this.#script, this.#workerData);
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, execution) {
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 dispatch(thread, input, execution, this.#result);
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
- * Create a persistent JSON-file {@link QueueStoreInterface} the core
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 just a database table, so JSON persistence reuses the
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
- * Create a CPU-parallel worker over `node:worker_threads` — a thin specialization of the
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 (via `spawnThread`),
531
- * `destroy`s it with `terminate()`, and `validate`s it by `alive && threadId > 0` (so an
532
- * evicted / crashed thread is dropped and replaced) — and an internal handler that
533
- * narrows the input through `options.input` (fail-fast before the structured-clone
534
- * boundary) then `dispatch`es the job to the leased thread, narrowing the reply through
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 (AGENTS §14). An `abort` / `timeout`
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.dispatch = dispatch;
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