@orkestrel/worker 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/serve.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { QueueExecution } from '@orkestrel/queue'\nimport type { Guard, NodeThread, Reply } from './types.js'\nimport { Worker as ThreadWorker } from 'node:worker_threads'\nimport { isRecord } from '@orkestrel/contract'\n\n// === The wire protocol (main ↔ thread)\n//\n// The main-side half of the run/abort/reply protocol `serveWorker` answers — spawning a\n// pooled thread, narrowing its replies, and dispatching one job at a time. The envelope\n// types ({@link Reply}, {@link NodeThread}) live in `./types.js` (AGENTS §5); the public\n// bridge across the structured-clone boundary is the `input` / `result` `Guard`s, which\n// narrow the envelopes' opaque `unknown` payloads with no assertion (AGENTS §14).\n\n/**\n * Spawn one worker thread and resolve a live {@link NodeThread} once it comes online.\n *\n * @remarks\n * Constructs the thread with the `script` module and the cloned `workerData`, then\n * resolves on the thread's `online` event (rejecting on an early `error` OR an `exit`\n * that arrives before `online`, so the spawn promise is total — it can never dangle on a\n * thread that died without erroring). The wrapper attaches persistent `error` / `exit`\n * listeners that flip `alive` to `false` AND latch the first terminal event on\n * {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via\n * its own listeners), to the pool's `validate` (via `alive`), and — crucially — to a\n * dispatch that attaches AFTER the death (via the latch). The latch closes a real race:\n * under event-loop pressure a dead thread's `online` + `error` + `exit` are delivered in\n * ONE synchronous exit-drain batch, so every death event fires before the microtask chain\n * resolving this spawn can hand the thread to `dispatch` — without the latch that job\n * would await events that already fired, forever. The pool's `create` hook calls this.\n *\n * @param script - The worker module each thread runs (must call `serveWorker`)\n * @param workerData - Opaque, structured-cloneable data handed to the thread at spawn\n * @returns A promise resolving the online {@link NodeThread}\n */\nexport function spawnThread(script: string | URL, workerData: unknown): Promise<NodeThread> {\n\tconst worker = new ThreadWorker(script, { workerData })\n\tconst thread: NodeThread = { worker, alive: true, death: undefined }\n\t// The persistent death latch — attached BEFORE any once-listener, so the first terminal\n\t// event records its cause on the record even when it fires inside a batched exit drain.\n\tworker.on('error', (error: Error) => {\n\t\tthread.alive = false\n\t\tif (thread.death === undefined) thread.death = error\n\t})\n\tworker.on('exit', (code: number) => {\n\t\tthread.alive = false\n\t\tif (thread.death === undefined) thread.death = new Error(`worker thread exited (code ${code})`)\n\t})\n\treturn new Promise<NodeThread>((resolve, reject) => {\n\t\tconst onOnline = (): void => {\n\t\t\tworker.off('error', onError)\n\t\t\tworker.off('exit', onExit)\n\t\t\tresolve(thread)\n\t\t}\n\t\tconst onError = (error: Error): void => {\n\t\t\tworker.off('online', onOnline)\n\t\t\tworker.off('exit', onExit)\n\t\t\treject(error)\n\t\t}\n\t\tconst onExit = (code: number): void => {\n\t\t\tworker.off('online', onOnline)\n\t\t\tworker.off('error', onError)\n\t\t\treject(new Error(`worker thread exited before coming online (code ${code})`))\n\t\t}\n\t\tworker.once('online', onOnline)\n\t\tworker.once('error', onError)\n\t\tworker.once('exit', onExit)\n\t})\n}\n\n/**\n * Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.\n *\n * @remarks\n * A total {@link Guard}-style predicate (never throws): a record whose `id` matches and\n * whose `ok` discriminant is well-formed (a `true` carries any `value`; a `false` carries a\n * string `error`). Anything else — another job's reply, a malformed payload — is `false`, so\n * a {@link dispatch} listener ignores it (a thread that chatters on the channel can't corrupt\n * a job).\n *\n * @param value - The inbound message to narrow\n * @param id - The job id a matching reply must carry\n * @returns `true` (narrowing `value` to {@link Reply}) when it is this job's well-formed reply\n */\nexport function isReply(value: unknown, id: string): value is Reply {\n\tif (!isRecord(value)) return false\n\tif (value.id !== id) return false\n\tif (value.ok === true) return true\n\treturn value.ok === false && typeof value.error === 'string'\n}\n\n/**\n * Dispatch one job to a leased {@link NodeThread} and await its narrowed reply.\n *\n * @remarks\n * Mints a fresh `id`, posts a `run` envelope, and resolves when the thread replies for\n * that id: a success `value` is narrowed through `result` (a value that fails the guard\n * rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.\n * A thread that ALREADY died rejects synchronously at entry from the latched\n * {@link NodeThread.death} — its death events fired before this dispatch existed (under\n * load they arrive in one batched exit drain) and will never fire again, so waiting on\n * the listeners below would dangle forever; the latch makes the death total across every\n * event ordering. If the thread `error`s / `exit`s mid-flight it is marked dead and the\n * job rejects. On `execution.signal` abort it posts an `abort` envelope (cooperative) AND\n * evicts the thread — `alive = false` + `terminate()` — because CPU-bound work cannot\n * honour the signal; the freed pool slot then gets a fresh thread. Every listener (the\n * thread's `message` / `error` / `exit` and the signal's `abort`) is removed on settle,\n * and a `settled` guard prevents a double-settle.\n *\n * @typeParam TResult - The reply type the `result` guard narrows to\n * @param thread - The leased thread to run the job on\n * @param input - The work payload (structured-cloned to the thread)\n * @param execution - The per-attempt handle; its `signal` aborts → terminate + evict\n * @param result - The {@link Guard} narrowing the reply value with no assertion\n * @returns A promise resolving the narrowed `TResult`, or rejecting on error / abort\n */\nexport function dispatch<TResult>(\n\tthread: NodeThread,\n\tinput: unknown,\n\texecution: QueueExecution,\n\tresult: Guard<TResult>,\n): Promise<TResult> {\n\tconst id = crypto.randomUUID()\n\tconst worker = thread.worker\n\treturn new Promise<TResult>((resolve, reject) => {\n\t\t// The latched-death entry check: a thread that died BEFORE this dispatch attached has\n\t\t// already emitted its `error` / `exit` (a batched exit drain delivers them before this\n\t\t// microtask runs) — no listener below will ever fire, and a `postMessage` to it is a\n\t\t// silent no-op. Reject NOW from the latch; this check + the attaches are synchronous,\n\t\t// so there is no gap a death can slip through.\n\t\tif (thread.death !== undefined || !thread.alive) {\n\t\t\treject(thread.death ?? new Error('worker thread is dead'))\n\t\t\treturn\n\t\t}\n\t\tlet settled = false\n\t\tlet detach = (): void => {}\n\t\tconst settle = (action: () => void): void => {\n\t\t\tif (settled) return\n\t\t\tsettled = true\n\t\t\tdetach()\n\t\t\taction()\n\t\t}\n\t\tconst onMessage = (value: unknown): void => {\n\t\t\tif (!isReply(value, id)) return\n\t\t\tif (value.ok) {\n\t\t\t\tconst reply = value.value\n\t\t\t\tif (result(reply)) settle(() => resolve(reply))\n\t\t\t\telse settle(() => reject(new Error('reply did not satisfy result guard')))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst message = value.error\n\t\t\tsettle(() => reject(new Error(message)))\n\t\t}\n\t\tconst onError = (error: Error): void => {\n\t\t\tthread.alive = false\n\t\t\tsettle(() => reject(error))\n\t\t}\n\t\tconst onExit = (): void => {\n\t\t\tthread.alive = false\n\t\t\tsettle(() => reject(new Error('worker thread exited')))\n\t\t}\n\t\t// Cooperative abort first, then EVICT: CPU-bound work won't honour the signal, so\n\t\t// terminate the thread and mark it dead — the pool replaces it on the next acquire.\n\t\t// NOTE: this `terminate()` may run TWICE — once here, and again when the pool's\n\t\t// `destroy` hook (`thread.worker.terminate()`) tears down the now-dead thread its\n\t\t// `validate` evicts. A second `terminate()` on an already-terminated Node thread is a\n\t\t// safe, idempotent no-op (it resolves with the prior exit code), so do NOT \"dedupe\" it\n\t\t// by gating on `alive` — that would skip the pool's eviction and reuse a tainted thread.\n\t\tconst onAbort = (): void => {\n\t\t\tworker.postMessage({ id, command: 'abort' })\n\t\t\tthread.alive = false\n\t\t\tvoid worker.terminate()\n\t\t\tsettle(() => reject(new Error('job aborted')))\n\t\t}\n\t\tdetach = (): void => {\n\t\t\tworker.off('message', onMessage)\n\t\t\tworker.off('error', onError)\n\t\t\tworker.off('exit', onExit)\n\t\t\texecution.signal.removeEventListener('abort', onAbort)\n\t\t}\n\t\tworker.on('message', onMessage)\n\t\tworker.on('error', onError)\n\t\tworker.on('exit', onExit)\n\t\tif (execution.signal.aborted) {\n\t\t\tonAbort()\n\t\t\treturn\n\t\t}\n\t\texecution.signal.addEventListener('abort', onAbort, { once: true })\n\t\t// `postMessage` structured-clones `input`; a non-cloneable payload throws here —\n\t\t// settle-reject so the listeners detach (no leak) rather than escaping the executor.\n\t\ttry {\n\t\t\tworker.postMessage({ id, command: 'run', input })\n\t\t} catch (error: unknown) {\n\t\t\tsettle(() => reject(error instanceof Error ? error : new Error(String(error))))\n\t\t}\n\t})\n}\n","import type { ServeWorkerOptions } from './types.js'\nimport { parentPort } from 'node:worker_threads'\n\n// The worker-side entry. SELF-CONTAINED by necessity: this module loads as RAW `.ts`\n// inside a spawned thread (Node ≥ 23.6 type-stripping), so it imports ONLY\n// `node:worker_threads` at runtime — no `@src/*`, no `.js`-relative value imports (the\n// only non-node import is the type-only `ServeWorkerOptions`, fully erased at runtime).\n// Its guards are inlined for the same reason. A worker script that needs the cloned\n// `workerData` reads it directly from `node:worker_threads` (it is in a thread already).\n\n// Inlined record guard (do NOT import `isRecord` from `@src/core` — see above). Total:\n// adversarial input returns `false`, never throws (AGENTS §14).\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n// Narrow an inbound message to a `run` envelope (a string `id` + a `'run'` command + an\n// `input` payload) — no assertion.\nfunction isRun(value: unknown): value is { readonly id: string; readonly input: unknown } {\n\treturn (\n\t\tisRecord(value) && typeof value.id === 'string' && value.command === 'run' && 'input' in value\n\t)\n}\n\n// Narrow an inbound message to an `abort` envelope (a string `id` + an `'abort'` command).\nfunction isAbort(value: unknown): value is { readonly id: string } {\n\treturn isRecord(value) && typeof value.id === 'string' && value.command === 'abort'\n}\n\n/**\n * Register a worker-thread handler — the worker-side half of {@link createNodeWorker}.\n *\n * @remarks\n * Must be the spawned thread's module entry. It listens on the parent port for the\n * run/abort protocol: a `run` message narrows its `input` through `options.input` (an\n * invalid payload replies with an error envelope, never running the handler), then runs\n * `options.handler(input, { signal })` and replies `{ id, ok: true, value }` on success or\n * `{ id, ok: false, error }` on throw. Each in-flight job has its own `AbortController`,\n * so an `abort` message for that id fires the handler's `signal` (cooperative — the main\n * side ALSO terminates the thread, so a handler that ignores its signal is still stopped).\n * Every inbound message is narrowed with the inlined guards — no `as`. On the main thread\n * (`parentPort === null`) it is a no-op.\n *\n * @typeParam TInput - The work payload (inferred from `options.input`)\n * @typeParam TResult - The value the handler resolves (the reply payload)\n * @param options - The `input` guard and the `handler` (see {@link ServeWorkerOptions})\n *\n * @example\n * ```ts\n * // double.ts — a worker script\n * import { serveWorker } from '@src/server'\n *\n * serveWorker<number, number>({\n * \tinput: (value): value is number => typeof value === 'number',\n * \thandler: (value) => value * 2,\n * })\n * ```\n */\nexport function serveWorker<TInput, TResult>(options: ServeWorkerOptions<TInput, TResult>): void {\n\tconst port = parentPort\n\tif (port === null) return\n\tconst controllers = new Map<string, AbortController>()\n\tport.on('message', (raw: unknown) => {\n\t\tif (isAbort(raw)) {\n\t\t\tcontrollers.get(raw.id)?.abort()\n\t\t\treturn\n\t\t}\n\t\tif (!isRun(raw)) return\n\t\tconst id = raw.id\n\t\tif (!options.input(raw.input)) {\n\t\t\tport.postMessage({ id, ok: false, error: 'input did not satisfy input guard' })\n\t\t\treturn\n\t\t}\n\t\tconst input = raw.input\n\t\tconst controller = new AbortController()\n\t\tcontrollers.set(id, controller)\n\t\t// Defer the handler call into the `then` so a SYNCHRONOUS throw becomes a rejection\n\t\t// (not an uncaught thread exception) and is reported as an error reply.\n\t\tPromise.resolve()\n\t\t\t.then(() => options.handler(input, { signal: controller.signal }))\n\t\t\t.then(\n\t\t\t\t(value) => {\n\t\t\t\t\tcontrollers.delete(id)\n\t\t\t\t\tport.postMessage({ id, ok: true, value })\n\t\t\t\t},\n\t\t\t\t(error: unknown) => {\n\t\t\t\t\tcontrollers.delete(id)\n\t\t\t\t\tport.postMessage({\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t)\n\t})\n}\n","import type { WorkerInterface } from '@src/core'\nimport type { ContractShape, Infer } from '@orkestrel/contract'\nimport type { QueueStoreInterface } from '@orkestrel/queue'\nimport type { NodeThread, NodeWorkerOptions } from './types.js'\nimport { createWorker } from '@src/core'\nimport { createJSONDriver } from '@orkestrel/database/server'\nimport { createDatabaseQueueStore } from '@orkestrel/queue'\nimport { dispatch, spawnThread } from './helpers.js'\n\n/**\n * Create a persistent JSON-file {@link QueueStoreInterface} — the core\n * `createDatabaseQueueStore` over a server {@link createJSONDriver}.\n *\n * @remarks\n * A queue's durable state is just a database table, so JSON persistence reuses the\n * existing JSON-file driver rather than a bespoke store: the entries are written to\n * (and reloaded from) the file at `path`, surviving a process restart. There is no new\n * class — the store engine ({@link createDatabaseQueueStore}) is shared, and only the\n * driver changes where the bytes live. The `input` shape must be JSON-serializable\n * (the JSON driver round-trips it as JSON). Build a second store over the SAME `path` to\n * resume the outstanding entries a prior store persisted.\n *\n * @typeParam TInput - The contract shape of each entry's `input` payload\n * @param path - The JSON file the entries are loaded from and flushed to\n * @param input - The {@link ContractShape} for the work payload (the `input` column)\n * @returns A JSON-file-backed {@link QueueStoreInterface}, typed by `input`\n *\n * @example\n * ```ts\n * import { stringShape } from '@src/core'\n * import { createJSONQueueStore } from '@src/server'\n *\n * const store = createJSONQueueStore('data/queue.json', stringShape())\n * await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })\n * // A later process resumes the outstanding work:\n * const resumed = createJSONQueueStore('data/queue.json', stringShape())\n * const outstanding = await resumed.load()\n * ```\n */\nexport function createJSONQueueStore<TInput extends ContractShape>(\n\tpath: string,\n\tinput: TInput,\n): QueueStoreInterface<Infer<TInput>> {\n\treturn createDatabaseQueueStore(input, createJSONDriver(path))\n}\n\n/**\n * Create a CPU-parallel worker over `node:worker_threads` — a thin specialization of the\n * core `createWorker` whose pooled resource is a worker THREAD.\n *\n * @remarks\n * Composition, not reimplementation: all concurrency, retries, per-attempt timeout,\n * lifecycle, and durability are the core `Worker`'s (a `Queue` ⨉ `Pool`). This factory\n * supplies only the thread pairing — the pool `create`s a thread (via `spawnThread`),\n * `destroy`s it with `terminate()`, and `validate`s it by `alive && threadId > 0` (so an\n * evicted / crashed thread is dropped and replaced) — and an internal handler that\n * narrows the input through `options.input` (fail-fast before the structured-clone\n * boundary) then `dispatch`es the job to the leased thread, narrowing the reply through\n * `options.result`. Both generics INFER from the `input` / `result` guards, so call sites\n * need no explicit type arguments. The boundary is crossed with ZERO `as`: the guards\n * reconstruct `TInput` / `TResult` by validation (AGENTS §14). An `abort` / `timeout`\n * TERMINATES the in-flight thread (CPU-bound work can't honour a signal) and evicts it; a\n * subsequent job spawns a fresh thread. The worker script's module must call\n * `serveWorker`. Returns the plain {@link WorkerInterface} — its methods are the Worker's.\n *\n * @typeParam TInput - The work payload each job carries (inferred from `input`)\n * @typeParam TResult - The value a thread resolves for a job (inferred from `result`)\n * @param options - The `script` plus the `input` / `result` guards and optional\n * `workerData` / `concurrency` / `retries` / `timeout` / `store`\n * (see {@link NodeWorkerOptions})\n * @returns A working {@link WorkerInterface} backed by a thread pool\n *\n * @example\n * ```ts\n * import { createNodeWorker } from '@src/server'\n *\n * const worker = createNodeWorker({\n * \tscript: new URL('./double.js', import.meta.url),\n * \tinput: (value): value is number => typeof value === 'number',\n * \tresult: (value): value is number => typeof value === 'number',\n * \tconcurrency: 4,\n * })\n *\n * const doubled = await worker.enqueue(21) // 42, computed on a worker thread\n * worker.destroy() // terminates every thread\n * ```\n */\nexport function createNodeWorker<TInput, TResult>(\n\toptions: NodeWorkerOptions<TInput, TResult>,\n): WorkerInterface<TInput, TResult> {\n\treturn createWorker<TInput, NodeThread, TResult>({\n\t\tpool: {\n\t\t\tcreate: () => spawnThread(options.script, options.workerData),\n\t\t\tdestroy: (thread) => thread.worker.terminate().then(() => {}),\n\t\t\tvalidate: (thread) => thread.alive && thread.worker.threadId > 0,\n\t\t\tmax: options.concurrency,\n\t\t},\n\t\thandler: (input, thread, execution) => {\n\t\t\tif (!options.input(input)) {\n\t\t\t\treturn Promise.reject(new Error('input did not satisfy input guard'))\n\t\t\t}\n\t\t\treturn dispatch(thread, input, execution, options.result)\n\t\t},\n\t\tconcurrency: options.concurrency,\n\t\tretries: options.retries,\n\t\ttimeout: options.timeout,\n\t\tstore: options.store,\n\t})\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,YAAY,QAAsB,YAA0C;CAC3F,MAAM,SAAS,IAAI,oBAAA,OAAa,QAAQ,EAAE,WAAW,CAAC;CACtD,MAAM,SAAqB;EAAE;EAAQ,OAAO;EAAM,OAAO,KAAA;CAAU;CAGnE,OAAO,GAAG,UAAU,UAAiB;EACpC,OAAO,QAAQ;EACf,IAAI,OAAO,UAAU,KAAA,GAAW,OAAO,QAAQ;CAChD,CAAC;CACD,OAAO,GAAG,SAAS,SAAiB;EACnC,OAAO,QAAQ;EACf,IAAI,OAAO,UAAU,KAAA,GAAW,OAAO,wBAAQ,IAAI,MAAM,8BAA8B,KAAK,EAAE;CAC/F,CAAC;CACD,OAAO,IAAI,SAAqB,SAAS,WAAW;EACnD,MAAM,iBAAuB;GAC5B,OAAO,IAAI,SAAS,OAAO;GAC3B,OAAO,IAAI,QAAQ,MAAM;GACzB,QAAQ,MAAM;EACf;EACA,MAAM,WAAW,UAAuB;GACvC,OAAO,IAAI,UAAU,QAAQ;GAC7B,OAAO,IAAI,QAAQ,MAAM;GACzB,OAAO,KAAK;EACb;EACA,MAAM,UAAU,SAAuB;GACtC,OAAO,IAAI,UAAU,QAAQ;GAC7B,OAAO,IAAI,SAAS,OAAO;GAC3B,uBAAO,IAAI,MAAM,mDAAmD,KAAK,EAAE,CAAC;EAC7E;EACA,OAAO,KAAK,UAAU,QAAQ;EAC9B,OAAO,KAAK,SAAS,OAAO;EAC5B,OAAO,KAAK,QAAQ,MAAM;CAC3B,CAAC;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,QAAQ,OAAgB,IAA4B;CACnE,IAAI,EAAA,GAAA,oBAAA,SAAA,CAAU,KAAK,GAAG,OAAO;CAC7B,IAAI,MAAM,OAAO,IAAI,OAAO;CAC5B,IAAI,MAAM,OAAO,MAAM,OAAO;CAC9B,OAAO,MAAM,OAAO,SAAS,OAAO,MAAM,UAAU;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,SACf,QACA,OACA,WACA,QACmB;CACnB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,SAAS,OAAO;CACtB,OAAO,IAAI,SAAkB,SAAS,WAAW;EAMhD,IAAI,OAAO,UAAU,KAAA,KAAa,CAAC,OAAO,OAAO;GAChD,OAAO,OAAO,yBAAS,IAAI,MAAM,uBAAuB,CAAC;GACzD;EACD;EACA,IAAI,UAAU;EACd,IAAI,eAAqB,CAAC;EAC1B,MAAM,UAAU,WAA6B;GAC5C,IAAI,SAAS;GACb,UAAU;GACV,OAAO;GACP,OAAO;EACR;EACA,MAAM,aAAa,UAAyB;GAC3C,IAAI,CAAC,QAAQ,OAAO,EAAE,GAAG;GACzB,IAAI,MAAM,IAAI;IACb,MAAM,QAAQ,MAAM;IACpB,IAAI,OAAO,KAAK,GAAG,aAAa,QAAQ,KAAK,CAAC;SACzC,aAAa,uBAAO,IAAI,MAAM,oCAAoC,CAAC,CAAC;IACzE;GACD;GACA,MAAM,UAAU,MAAM;GACtB,aAAa,OAAO,IAAI,MAAM,OAAO,CAAC,CAAC;EACxC;EACA,MAAM,WAAW,UAAuB;GACvC,OAAO,QAAQ;GACf,aAAa,OAAO,KAAK,CAAC;EAC3B;EACA,MAAM,eAAqB;GAC1B,OAAO,QAAQ;GACf,aAAa,uBAAO,IAAI,MAAM,sBAAsB,CAAC,CAAC;EACvD;EAQA,MAAM,gBAAsB;GAC3B,OAAO,YAAY;IAAE;IAAI,SAAS;GAAQ,CAAC;GAC3C,OAAO,QAAQ;GACf,OAAY,UAAU;GACtB,aAAa,uBAAO,IAAI,MAAM,aAAa,CAAC,CAAC;EAC9C;EACA,eAAqB;GACpB,OAAO,IAAI,WAAW,SAAS;GAC/B,OAAO,IAAI,SAAS,OAAO;GAC3B,OAAO,IAAI,QAAQ,MAAM;GACzB,UAAU,OAAO,oBAAoB,SAAS,OAAO;EACtD;EACA,OAAO,GAAG,WAAW,SAAS;EAC9B,OAAO,GAAG,SAAS,OAAO;EAC1B,OAAO,GAAG,QAAQ,MAAM;EACxB,IAAI,UAAU,OAAO,SAAS;GAC7B,QAAQ;GACR;EACD;EACA,UAAU,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAGlE,IAAI;GACH,OAAO,YAAY;IAAE;IAAI,SAAS;IAAO;GAAM,CAAC;EACjD,SAAS,OAAgB;GACxB,aAAa,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC,CAAC;EAC/E;CACD,CAAC;AACF;;;ACvLA,SAAS,SAAS,OAAkD;CACnE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC3E;AAIA,SAAS,MAAM,OAA2E;CACzF,OACC,SAAS,KAAK,KAAK,OAAO,MAAM,OAAO,YAAY,MAAM,YAAY,SAAS,WAAW;AAE3F;AAGA,SAAS,QAAQ,OAAkD;CAClE,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,OAAO,YAAY,MAAM,YAAY;AAC7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,YAA6B,SAAoD;CAChG,MAAM,OAAO,oBAAA;CACb,IAAI,SAAS,MAAM;CACnB,MAAM,8BAAc,IAAI,IAA6B;CACrD,KAAK,GAAG,YAAY,QAAiB;EACpC,IAAI,QAAQ,GAAG,GAAG;GACjB,YAAY,IAAI,IAAI,EAAE,CAAC,EAAE,MAAM;GAC/B;EACD;EACA,IAAI,CAAC,MAAM,GAAG,GAAG;EACjB,MAAM,KAAK,IAAI;EACf,IAAI,CAAC,QAAQ,MAAM,IAAI,KAAK,GAAG;GAC9B,KAAK,YAAY;IAAE;IAAI,IAAI;IAAO,OAAO;GAAoC,CAAC;GAC9E;EACD;EACA,MAAM,QAAQ,IAAI;EAClB,MAAM,aAAa,IAAI,gBAAgB;EACvC,YAAY,IAAI,IAAI,UAAU;EAG9B,QAAQ,QAAQ,CAAC,CACf,WAAW,QAAQ,QAAQ,OAAO,EAAE,QAAQ,WAAW,OAAO,CAAC,CAAC,CAAC,CACjE,MACC,UAAU;GACV,YAAY,OAAO,EAAE;GACrB,KAAK,YAAY;IAAE;IAAI,IAAI;IAAM;GAAM,CAAC;EACzC,IACC,UAAmB;GACnB,YAAY,OAAO,EAAE;GACrB,KAAK,YAAY;IAChB;IACA,IAAI;IACJ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC7D,CAAC;EACF,CACD;CACF,CAAC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxDA,SAAgB,qBACf,MACA,OACqC;CACrC,QAAA,GAAA,iBAAA,yBAAA,CAAgC,QAAA,GAAA,2BAAA,iBAAA,CAAwB,IAAI,CAAC;AAC9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,iBACf,SACmC;CACnC,QAAA,GAAA,UAAA,aAAA,CAAiD;EAChD,MAAM;GACL,cAAc,YAAY,QAAQ,QAAQ,QAAQ,UAAU;GAC5D,UAAU,WAAW,OAAO,OAAO,UAAU,CAAC,CAAC,WAAW,CAAC,CAAC;GAC5D,WAAW,WAAW,OAAO,SAAS,OAAO,OAAO,WAAW;GAC/D,KAAK,QAAQ;EACd;EACA,UAAU,OAAO,QAAQ,cAAc;GACtC,IAAI,CAAC,QAAQ,MAAM,KAAK,GACvB,OAAO,QAAQ,uBAAO,IAAI,MAAM,mCAAmC,CAAC;GAErE,OAAO,SAAS,QAAQ,OAAO,WAAW,QAAQ,MAAM;EACzD;EACA,aAAa,QAAQ;EACrB,SAAS,QAAQ;EACjB,SAAS,QAAQ;EACjB,OAAO,QAAQ;CAChB,CAAC;AACF"}
1
+ {"version":3,"file":"index.cjs","names":["#worker","#promise","#resolve","#reject","#recordHandler","#recordExitHandler","#onlineHandler","#spawnErrorHandler","#spawnExitHandler","#record","#recordExit","#online","#spawnError","#spawnExit","#alive","#death","#thread","#worker","#input","#execution","#result","#id","#promise","#fulfill","#reject","#messageHandler","#messageErrorHandler","#errorHandler","#exitHandler","#abortHandler","#message","#messageError","#error","#exit","#abort","#start","#fail","#terminate","#succeed","#settled","#detach","#script","#input","#result","#workerData","#concurrency","#retries","#timeout","#store","#create","#destroy","#validate","#handle"],"sources":["../../../src/server/Thread.ts","../../../src/server/validators.ts","../../../src/server/Dispatch.ts","../../../src/server/helpers.ts","../../../src/server/serve.ts","../../../src/server/NodeWorker.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { NodeThread } from './types.js'\nimport { Worker as ThreadWorker } from 'node:worker_threads'\n\n/**\n * Internal mutable implementation of the readonly {@link NodeThread} observation contract.\n *\n * @remarks\n * Liveness and the first terminal error live behind runtime-private fields. Thread `error`,\n * `messageerror`, and `exit` all latch death, so pool validation cannot reuse a thread whose\n * inbound message could not be deserialized.\n */\nexport class Thread implements NodeThread {\n\treadonly #worker: ThreadWorker\n\treadonly #promise: Promise<NodeThread>\n\treadonly #resolve: (value: NodeThread | PromiseLike<NodeThread>) => void\n\treadonly #reject: (reason?: unknown) => void\n\treadonly #recordHandler: (error: Error) => void\n\treadonly #recordExitHandler: (code: number) => void\n\treadonly #onlineHandler: () => void\n\treadonly #spawnErrorHandler: (error: Error) => void\n\treadonly #spawnExitHandler: (code: number) => void\n\t#alive = true\n\t#death: Error | undefined\n\n\tconstructor(script: string | URL, workerData: unknown) {\n\t\tthis.#worker = new ThreadWorker(script, {\n\t\t\t...(workerData !== undefined ? { workerData } : {}),\n\t\t})\n\t\tconst readiness = Promise.withResolvers<NodeThread>()\n\t\tthis.#promise = readiness.promise\n\t\tthis.#resolve = readiness.resolve\n\t\tthis.#reject = readiness.reject\n\t\tthis.#recordHandler = this.#record.bind(this)\n\t\tthis.#recordExitHandler = this.#recordExit.bind(this)\n\t\tthis.#onlineHandler = this.#online.bind(this)\n\t\tthis.#spawnErrorHandler = this.#spawnError.bind(this)\n\t\tthis.#spawnExitHandler = this.#spawnExit.bind(this)\n\n\t\tthis.#worker.on('error', this.#recordHandler)\n\t\tthis.#worker.on('messageerror', this.#recordHandler)\n\t\tthis.#worker.on('exit', this.#recordExitHandler)\n\t\tthis.#worker.once('online', this.#onlineHandler)\n\t\tthis.#worker.once('error', this.#spawnErrorHandler)\n\t\tthis.#worker.once('exit', this.#spawnExitHandler)\n\t}\n\n\tget worker(): ThreadWorker {\n\t\treturn this.#worker\n\t}\n\n\tget alive(): boolean {\n\t\treturn this.#alive\n\t}\n\n\tget death(): Error | undefined {\n\t\treturn this.#death\n\t}\n\n\tget promise(): Promise<NodeThread> {\n\t\treturn this.#promise\n\t}\n\n\tevict(): void {\n\t\tthis.#alive = false\n\t}\n\n\t#record(error: Error): void {\n\t\tthis.#alive = false\n\t\tif (this.#death === undefined) this.#death = error\n\t}\n\n\t#recordExit(code: number): void {\n\t\tthis.#alive = false\n\t\tif (this.#death === undefined) {\n\t\t\tthis.#death = new Error(`worker thread exited (code ${String(code)})`)\n\t\t}\n\t}\n\n\t#online(): void {\n\t\tthis.#worker.off('error', this.#spawnErrorHandler)\n\t\tthis.#worker.off('exit', this.#spawnExitHandler)\n\t\tthis.#resolve(this)\n\t}\n\n\t#spawnError(error: Error): void {\n\t\tthis.#worker.off('online', this.#onlineHandler)\n\t\tthis.#worker.off('exit', this.#spawnExitHandler)\n\t\tthis.#reject(error)\n\t}\n\n\t#spawnExit(code: number): void {\n\t\tthis.#worker.off('online', this.#onlineHandler)\n\t\tthis.#worker.off('error', this.#spawnErrorHandler)\n\t\tthis.#reject(new Error(`worker thread exited before coming online (code ${String(code)})`))\n\t}\n}\n","import type { Reply } from './types.js'\nimport { attempt, isRecord } from '@orkestrel/contract'\n\n/**\n * Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.\n *\n * @remarks\n * A total predicate: a record whose `id` matches and whose `ok` discriminant is well-formed.\n * Anything else is rejected so a dispatch listener can ignore foreign or malformed messages.\n *\n * @param value - The inbound message to narrow\n * @param id - The job id a matching reply must carry\n * @returns `true` when the value is this job's well-formed reply\n */\nexport function isReply(value: unknown, id: string): value is Reply {\n\tconst outcome = attempt(() => {\n\t\tif (!isRecord(value)) return false\n\t\tif (value.id !== id) return false\n\t\tif (value.ok === true) return 'value' in value\n\t\treturn value.ok === false && typeof value.error === 'string'\n\t})\n\treturn outcome.success && outcome.value\n}\n","import type { QueueExecution } from '@orkestrel/queue'\nimport type { Guard } from '@orkestrel/contract'\nimport type { NodeThread } from './types.js'\nimport type { Worker as ThreadWorker } from 'node:worker_threads'\nimport { attempt, isRecord } from '@orkestrel/contract'\nimport { Thread } from './Thread.js'\nimport { isReply } from './validators.js'\n\n/**\n * Internal lifecycle entity for one dispatched worker-thread job.\n *\n * @remarks\n * Owns stable `message` / `messageerror` / death listener identities, settlement, result-guard\n * containment, and abort eviction for one dispatch. Deserialization failure, a matching-id\n * malformed reply, and abort each evict and terminate the thread before rejecting, with\n * termination failure preserved. Non-record, id-less, hostile-id, and foreign-id chatter is ignored.\n */\nexport class Dispatch<TResult> {\n\treadonly #thread: NodeThread\n\treadonly #worker: ThreadWorker\n\treadonly #input: unknown\n\treadonly #execution: QueueExecution\n\treadonly #result: Guard<TResult>\n\treadonly #id = crypto.randomUUID()\n\treadonly #promise: Promise<TResult>\n\treadonly #fulfill: (value: TResult | PromiseLike<TResult>) => void\n\treadonly #reject: (reason?: unknown) => void\n\treadonly #messageHandler: (value: unknown) => void\n\treadonly #messageErrorHandler: (error: Error) => void\n\treadonly #errorHandler: (error: Error) => void\n\treadonly #exitHandler: () => void\n\treadonly #abortHandler: () => void\n\t#settled = false\n\n\tconstructor(\n\t\tthread: NodeThread,\n\t\tinput: unknown,\n\t\texecution: QueueExecution,\n\t\tresult: Guard<TResult>,\n\t) {\n\t\tthis.#thread = thread\n\t\tthis.#worker = thread.worker\n\t\tthis.#input = input\n\t\tthis.#execution = execution\n\t\tthis.#result = result\n\t\tconst settlement = Promise.withResolvers<TResult>()\n\t\tthis.#promise = settlement.promise\n\t\tthis.#fulfill = settlement.resolve\n\t\tthis.#reject = settlement.reject\n\t\tthis.#messageHandler = this.#message.bind(this)\n\t\tthis.#messageErrorHandler = this.#messageError.bind(this)\n\t\tthis.#errorHandler = this.#error.bind(this)\n\t\tthis.#exitHandler = this.#exit.bind(this)\n\t\tthis.#abortHandler = this.#abort.bind(this)\n\t\tthis.#start()\n\t}\n\n\tget promise(): Promise<TResult> {\n\t\treturn this.#promise\n\t}\n\n\t#start(): void {\n\t\tif (this.#thread.death !== undefined || !this.#thread.alive) {\n\t\t\tthis.#fail(this.#thread.death ?? new Error('worker thread is dead'))\n\t\t\treturn\n\t\t}\n\t\tthis.#worker.on('message', this.#messageHandler)\n\t\tthis.#worker.on('messageerror', this.#messageErrorHandler)\n\t\tthis.#worker.on('error', this.#errorHandler)\n\t\tthis.#worker.on('exit', this.#exitHandler)\n\t\tif (this.#execution.signal.aborted) {\n\t\t\tthis.#abort()\n\t\t\treturn\n\t\t}\n\t\tthis.#execution.signal.addEventListener('abort', this.#abortHandler, { once: true })\n\t\ttry {\n\t\t\tthis.#worker.postMessage({ id: this.#id, command: 'run', input: this.#input })\n\t\t} catch (error: unknown) {\n\t\t\tthis.#fail(error instanceof Error ? error : new Error(String(error)))\n\t\t}\n\t}\n\n\t#message(value: unknown): void {\n\t\tif (!isRecord(value)) return\n\t\tconst id = attempt(() => value.id)\n\t\tif (!id.success || id.value !== this.#id) return\n\t\tif (!isReply(value, this.#id)) {\n\t\t\tthis.#terminate(new Error('worker reply was malformed'))\n\t\t\treturn\n\t\t}\n\t\tif (value.ok) {\n\t\t\tconst reply = value.value\n\t\t\ttry {\n\t\t\t\tif (this.#result(reply)) this.#succeed(reply)\n\t\t\t\telse this.#fail(new Error('reply did not satisfy result guard'))\n\t\t\t} catch (error: unknown) {\n\t\t\t\tthis.#fail(error)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tthis.#fail(new Error(value.error))\n\t}\n\n\t#messageError(error: Error): void {\n\t\tthis.#terminate(error)\n\t}\n\n\t#error(error: Error): void {\n\t\tthis.#fail(error)\n\t}\n\n\t#exit(): void {\n\t\tthis.#fail(this.#thread.death ?? new Error('worker thread exited'))\n\t}\n\n\t#abort(): void {\n\t\tconst notification: unknown[] = []\n\t\ttry {\n\t\t\tthis.#worker.postMessage({ id: this.#id, command: 'abort' })\n\t\t} catch (cause: unknown) {\n\t\t\tnotification.push(cause)\n\t\t}\n\t\tthis.#terminate(this.#execution.signal.reason, notification)\n\t}\n\n\t#terminate(error: unknown, notification: readonly unknown[] = []): void {\n\t\tif (this.#settled) return\n\t\tthis.#settled = true\n\t\tthis.#detach()\n\t\tif (this.#thread instanceof Thread) this.#thread.evict()\n\t\tlet termination: Promise<number>\n\t\ttry {\n\t\t\ttermination = this.#worker.terminate()\n\t\t} catch (cause: unknown) {\n\t\t\tthis.#reject(new AggregateError([error, ...notification, cause], 'worker termination failed'))\n\t\t\treturn\n\t\t}\n\t\tvoid termination.then(\n\t\t\t() => {\n\t\t\t\tif (notification.length === 0) this.#reject(error)\n\t\t\t\telse {\n\t\t\t\t\tthis.#reject(\n\t\t\t\t\t\tnew AggregateError([error, ...notification], 'worker abort notification failed'),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t},\n\t\t\t(cause: unknown) =>\n\t\t\t\tthis.#reject(\n\t\t\t\t\tnew AggregateError([error, ...notification, cause], 'worker termination failed'),\n\t\t\t\t),\n\t\t)\n\t}\n\n\t#succeed(value: TResult): void {\n\t\tif (this.#settled) return\n\t\tthis.#settled = true\n\t\tthis.#detach()\n\t\tthis.#fulfill(value)\n\t}\n\n\t#fail(error: unknown): void {\n\t\tif (this.#settled) return\n\t\tthis.#settled = true\n\t\tthis.#detach()\n\t\tthis.#reject(error)\n\t}\n\n\t#detach(): void {\n\t\tthis.#worker.off('message', this.#messageHandler)\n\t\tthis.#worker.off('messageerror', this.#messageErrorHandler)\n\t\tthis.#worker.off('error', this.#errorHandler)\n\t\tthis.#worker.off('exit', this.#exitHandler)\n\t\tthis.#execution.signal.removeEventListener('abort', this.#abortHandler)\n\t}\n}\n","import type { QueueExecution } from '@orkestrel/queue'\nimport type { Guard } from '@orkestrel/contract'\nimport type { NodeThread } from './types.js'\nimport { Dispatch } from './Dispatch.js'\nimport { Thread } from './Thread.js'\n\n// === The wire protocol (main ↔ thread)\n//\n// The main-side half of the run/abort/reply protocol `serveWorker` answers — spawning a\n// pooled thread, narrowing its replies, and dispatching one job at a time. The envelope\n// types ({@link Reply}, {@link NodeThread}) live in `./types.js` (AGENTS §5); the public\n// bridge across the structured-clone boundary is the `input` / `result` `Guard`s, which\n// narrow the envelopes' opaque `unknown` payloads with no assertion (AGENTS §14).\n\n/**\n * Spawn one worker thread and resolve a live {@link NodeThread} once it comes online.\n *\n * @remarks\n * Constructs the thread with the `script` module and the cloned `workerData`, then\n * resolves on the thread's `online` event (rejecting on an early `error` OR an `exit`\n * that arrives before `online`, so the spawn promise is total — it can never dangle on a\n * thread that died without erroring). The wrapper attaches persistent `error` / `exit`\n * listeners that flip `alive` to `false` AND latch the first terminal event on\n * {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via\n * its own listeners), to the pool's `validate` (via `alive`), and — crucially — to a\n * dispatch that attaches AFTER the death (via the latch). A `messageerror` is terminal too,\n * so a thread whose inbound payload could not be deserialized is never reused. The latch\n * closes a real race: a thread can become terminal before the readiness promise continuation\n * hands it to `dispatch`, leaving no future death event for that dispatch to observe. Without\n * the latch, that job would wait forever. The pool's `create` hook calls this.\n *\n * @param script - The worker module each thread runs (must call `serveWorker`)\n * @param workerData - Opaque, structured-cloneable data handed to the thread at spawn\n * @returns A promise resolving the online {@link NodeThread}\n */\nexport function spawnThread(script: string | URL, workerData: unknown): Promise<NodeThread> {\n\treturn new Thread(script, workerData).promise\n}\n\n/**\n * Dispatch one job to a leased {@link NodeThread} and await its narrowed reply.\n *\n * @remarks\n * Mints a fresh `id`, posts a `run` envelope, and resolves when the thread replies for\n * that id: a success `value` is narrowed through `result` (a value that fails the guard\n * rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.\n * A thread that ALREADY died rejects synchronously at entry from the latched\n * {@link NodeThread.death} — its death events fired before this dispatch existed and will\n * never fire again, so waiting on the listeners below would dangle forever; the latch makes\n * death total across every event ordering. If the thread `error`s / `exit`s mid-flight it is\n * marked dead and the\n * job rejects. An inbound `messageerror` also evicts and terminates the thread before\n * rejection. On `execution.signal` abort it contains the cooperative `abort` post,\n * evicts the thread, and observes `terminate()` settlement because CPU-bound work cannot\n * honour the signal; the freed pool slot then gets a fresh thread. Every per-job listener\n * (`message` / `messageerror` / `error` / `exit` / `abort`) is removed on settle.\n *\n * @typeParam TResult - The reply type the `result` guard narrows to\n * @param thread - The leased thread to run the job on\n * @param input - The work payload (structured-cloned to the thread)\n * @param execution - The per-attempt handle; its `signal` aborts → terminate + evict\n * @param result - The {@link Guard} narrowing the reply value with no assertion\n * @returns A promise resolving the narrowed `TResult`, or rejecting on error / abort\n */\nexport function dispatch<TResult>(\n\tthread: NodeThread,\n\tinput: unknown,\n\texecution: QueueExecution,\n\tresult: Guard<TResult>,\n): Promise<TResult> {\n\treturn new Dispatch(thread, input, execution, result).promise\n}\n","import type { ServeWorkerOptions } from './types.js'\nimport { parentPort } from 'node:worker_threads'\n\n// The worker-side entry. SELF-CONTAINED by necessity: this module loads as RAW `.ts`\n// inside a spawned thread (Node ≥ 23.6 type-stripping), so it imports ONLY\n// `node:worker_threads` at runtime — no `@src/*`, no `.js`-relative value imports (the\n// only non-node import is the type-only `ServeWorkerOptions`, fully erased at runtime).\n// Its guards are inlined for the same reason. A worker script that needs the cloned\n// `workerData` reads it directly from `node:worker_threads` (it is in a thread already).\n\n// Inlined record guard (do NOT import `isRecord` from `@src/core` — see above). Total:\n// adversarial input returns `false`, never throws (AGENTS §14).\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n// Narrow an inbound message to a `run` envelope (a string `id` + a `'run'` command + an\n// `input` payload) — no assertion.\nfunction isRun(value: unknown): value is { readonly id: string; readonly input: unknown } {\n\treturn (\n\t\tisRecord(value) && typeof value.id === 'string' && value.command === 'run' && 'input' in value\n\t)\n}\n\n// Narrow an inbound message to an `abort` envelope (a string `id` + an `'abort'` command).\nfunction isAbort(value: unknown): value is { readonly id: string } {\n\treturn isRecord(value) && typeof value.id === 'string' && value.command === 'abort'\n}\n\n/**\n * Register a worker-thread handler — the worker-side half of {@link createNodeWorker}.\n *\n * @remarks\n * Must be the spawned thread's module entry. It listens on the parent port for the\n * run/abort protocol: a `run` message narrows its `input` through `options.input` (an\n * invalid payload replies with an error envelope, never running the handler), then runs\n * `options.handler(input, { signal })` and replies `{ id, ok: true, value }` on success or\n * `{ id, ok: false, error }` on throw. Input-guard throws use the same failure envelope. If a\n * success value cannot be cloned, the post is retried as a clone-safe failure; if that post also\n * fails, the parent port closes so the main side observes thread exit instead of waiting forever.\n * Each in-flight job has its own `AbortController`,\n * so an `abort` message for that id fires the handler's `signal` (cooperative — the main\n * side ALSO terminates the thread, so a handler that ignores its signal is still stopped).\n * Every inbound message is narrowed with the inlined guards — no `as`. On the main thread\n * (`parentPort === null`) it is a no-op.\n *\n * @typeParam TInput - The work payload (inferred from `options.input`)\n * @typeParam TResult - The value the handler resolves (the reply payload)\n * @param options - The `input` guard and the `handler` (see {@link ServeWorkerOptions})\n *\n * @example\n * ```ts\n * // double.ts — a worker script\n * import { serveWorker } from '@orkestrel/worker/server'\n *\n * serveWorker<number, number>({\n * \tinput: (value): value is number => typeof value === 'number',\n * \thandler: (value) => value * 2,\n * })\n * ```\n */\nexport function serveWorker<TInput, TResult>(options: ServeWorkerOptions<TInput, TResult>): void {\n\tconst port = parentPort\n\tif (port === null) return\n\tconst input = options.input\n\tconst handler = options.handler\n\tconst controllers = new Map<string, AbortController>()\n\tport.on('message', (raw: unknown) => {\n\t\tif (isAbort(raw)) {\n\t\t\tcontrollers.get(raw.id)?.abort()\n\t\t\treturn\n\t\t}\n\t\tif (!isRun(raw)) return\n\t\tconst id = raw.id\n\t\tconst controller = new AbortController()\n\t\tcontrollers.set(id, controller)\n\t\tvoid Promise.resolve()\n\t\t\t.then(() => {\n\t\t\t\tif (!input(raw.input)) {\n\t\t\t\t\tthrow new Error('input did not satisfy input guard')\n\t\t\t\t}\n\t\t\t\tconst value = raw.input\n\t\t\t\treturn handler(value, { signal: controller.signal })\n\t\t\t})\n\t\t\t.then((value) => {\n\t\t\t\tcontrollers.delete(id)\n\t\t\t\tport.postMessage({ id, ok: true, value })\n\t\t\t})\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tcontrollers.delete(id)\n\t\t\t\tlet message = 'worker operation failed'\n\t\t\t\ttry {\n\t\t\t\t\tmessage = error instanceof Error ? error.message : String(error)\n\t\t\t\t} catch {}\n\t\t\t\ttry {\n\t\t\t\t\tport.postMessage({ id, ok: false, error: message })\n\t\t\t\t} catch {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tport.close()\n\t\t\t\t\t} catch {}\n\t\t\t\t}\n\t\t\t})\n\t})\n}\n","import type { WorkerInterface } from '@src/core'\nimport type { Guard } from '@orkestrel/contract'\nimport type { QueueExecution, QueueStoreInterface } from '@orkestrel/queue'\nimport type { NodeThread, NodeWorkerOptions } from './types.js'\nimport { createWorker } from '@src/core'\nimport { attempt } from '@orkestrel/contract'\nimport { dispatch, spawnThread } from './helpers.js'\n\n/**\n * Internal composition entity backing {@link createNodeWorker}.\n *\n * @remarks\n * Supplies bound Pool and Queue operations without nested function assignments. The resulting\n * public entity remains the plain core {@link WorkerInterface}.\n */\nexport class NodeWorker<TInput, TResult> {\n\treadonly #script: string | URL\n\treadonly #input: Guard<TInput>\n\treadonly #result: Guard<TResult>\n\treadonly #workerData: unknown\n\treadonly #concurrency: number | undefined\n\treadonly #retries: number | undefined\n\treadonly #timeout: number | undefined\n\treadonly #store: QueueStoreInterface<TInput> | undefined\n\n\tconstructor(options: NodeWorkerOptions<TInput, TResult>) {\n\t\tthis.#script = options.script\n\t\tthis.#input = options.input\n\t\tthis.#result = options.result\n\t\tthis.#workerData = options.workerData\n\t\tthis.#concurrency = options.concurrency\n\t\tthis.#retries = options.retries\n\t\tthis.#timeout = options.timeout\n\t\tthis.#store = options.store\n\t}\n\n\tbuild(): WorkerInterface<TInput, TResult> {\n\t\treturn createWorker<TInput, NodeThread, TResult>({\n\t\t\tpool: {\n\t\t\t\tcreate: this.#create.bind(this),\n\t\t\t\tdestroy: this.#destroy.bind(this),\n\t\t\t\tvalidate: this.#validate.bind(this),\n\t\t\t\t...(this.#concurrency !== undefined ? { max: this.#concurrency } : {}),\n\t\t\t},\n\t\t\thandler: this.#handle.bind(this),\n\t\t\t...(this.#concurrency !== undefined ? { concurrency: this.#concurrency } : {}),\n\t\t\t...(this.#retries !== undefined ? { retries: this.#retries } : {}),\n\t\t\t...(this.#timeout !== undefined ? { timeout: this.#timeout } : {}),\n\t\t\t...(this.#store !== undefined ? { store: this.#store } : {}),\n\t\t})\n\t}\n\n\t#create(): Promise<NodeThread> {\n\t\treturn spawnThread(this.#script, this.#workerData)\n\t}\n\n\tasync #destroy(thread: NodeThread): Promise<void> {\n\t\tawait thread.worker.terminate()\n\t}\n\n\t#validate(thread: NodeThread): boolean {\n\t\treturn thread.alive && thread.worker.threadId > 0\n\t}\n\n\t#handle(input: TInput, thread: NodeThread, execution: QueueExecution): Promise<TResult> {\n\t\tconst outcome = attempt(() => this.#input(input))\n\t\tif (!outcome.success) return Promise.reject(outcome.error)\n\t\tif (!outcome.value) {\n\t\t\treturn Promise.reject(new Error('input did not satisfy input guard'))\n\t\t}\n\t\treturn dispatch(thread, input, execution, this.#result)\n\t}\n}\n","import type { WorkerInterface } from '@src/core'\nimport type { ContractShape, Infer } from '@orkestrel/contract'\nimport type { QueueStoreInterface } from '@orkestrel/queue'\nimport type { NodeWorkerOptions } from './types.js'\nimport { createJSONDriver } from '@orkestrel/database/server'\nimport { createDatabaseQueueStore } from '@orkestrel/queue'\nimport { NodeWorker } from './NodeWorker.js'\n\n/**\n * Create a persistent JSON-file {@link QueueStoreInterface} — the core\n * `createDatabaseQueueStore` over a server {@link createJSONDriver}.\n *\n * @remarks\n * A queue's durable state is just a database table, so JSON persistence reuses the\n * existing JSON-file driver rather than a bespoke store: the entries are written to\n * (and reloaded from) the file at `path`, surviving a process restart. There is no new\n * class — the store engine ({@link createDatabaseQueueStore}) is shared, and only the\n * driver changes where the bytes live. The `input` shape must be JSON-serializable\n * (the JSON driver round-trips it as JSON). Build a second store over the SAME `path` to\n * resume the outstanding entries a prior store persisted.\n *\n * @typeParam TInput - The contract shape of each entry's `input` payload\n * @param path - The JSON file the entries are loaded from and flushed to\n * @param input - The {@link ContractShape} for the work payload (the `input` column)\n * @returns A JSON-file-backed {@link QueueStoreInterface}, typed by `input`\n *\n * @example\n * ```ts\n * import { stringShape } from '@orkestrel/contract'\n * import { createJSONQueueStore } from '@orkestrel/worker/server'\n *\n * const store = createJSONQueueStore('data/queue.json', stringShape())\n * await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })\n * // A later process resumes the outstanding work:\n * const resumed = createJSONQueueStore('data/queue.json', stringShape())\n * const outstanding = await resumed.load()\n * ```\n */\nexport function createJSONQueueStore<TInput extends ContractShape>(\n\tpath: string,\n\tinput: TInput,\n): QueueStoreInterface<Infer<TInput>> {\n\treturn createDatabaseQueueStore(input, createJSONDriver(path))\n}\n\n/**\n * Create a CPU-parallel worker over `node:worker_threads` — a thin specialization of the\n * core `createWorker` whose pooled resource is a worker THREAD.\n *\n * @remarks\n * Composition, not reimplementation: all concurrency, retries, per-attempt timeout,\n * lifecycle, and durability are the core `Worker`'s (a `Queue` ⨉ `Pool`). This factory\n * supplies only the thread pairing — the pool `create`s a thread (via `spawnThread`),\n * `destroy`s it with `terminate()`, and `validate`s it by `alive && threadId > 0` (so an\n * evicted / crashed thread is dropped and replaced) — and an internal handler that\n * narrows the input through `options.input` (fail-fast before the structured-clone\n * boundary) then `dispatch`es the job to the leased thread, narrowing the reply through\n * `options.result`. Both generics INFER from the `input` / `result` guards, so call sites\n * need no explicit type arguments. The boundary is crossed with ZERO `as`: the guards\n * reconstruct `TInput` / `TResult` by validation (AGENTS §14). An `abort` / `timeout`\n * TERMINATES the in-flight thread (CPU-bound work can't honour a signal) and evicts it; a\n * subsequent job spawns a fresh thread. The worker script's module must call\n * `serveWorker`. Returns the plain {@link WorkerInterface} — its methods are the Worker's.\n *\n * @typeParam TInput - The work payload each job carries (inferred from `input`)\n * @typeParam TResult - The value a thread resolves for a job (inferred from `result`)\n * @param options - The `script` plus the `input` / `result` guards and optional\n * `workerData` / `concurrency` / `retries` / `timeout` / `store`\n * (see {@link NodeWorkerOptions})\n * @returns A working {@link WorkerInterface} backed by a thread pool\n *\n * @example\n * ```ts\n * import { createNodeWorker } from '@orkestrel/worker/server'\n *\n * const worker = createNodeWorker({\n * \tscript: new URL('./double.js', import.meta.url),\n * \tinput: (value): value is number => typeof value === 'number',\n * \tresult: (value): value is number => typeof value === 'number',\n * \tconcurrency: 4,\n * })\n *\n * const doubled = await worker.enqueue(21) // 42, computed on a worker thread\n * await worker.destroy() // terminates every thread\n * ```\n */\nexport function createNodeWorker<TInput, TResult>(\n\toptions: NodeWorkerOptions<TInput, TResult>,\n): WorkerInterface<TInput, TResult> {\n\treturn new NodeWorker(options).build()\n}\n"],"mappings":";;;;;;;;;;;;;;;AAWA,IAAa,SAAb,MAA0C;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,SAAS;CACT;CAEA,YAAY,QAAsB,YAAqB;EACtD,KAAKA,UAAU,IAAI,oBAAA,OAAa,QAAQ,EACvC,GAAI,eAAe,KAAA,IAAY,EAAE,WAAW,IAAI,CAAC,EAClD,CAAC;EACD,MAAM,YAAY,QAAQ,cAA0B;EACpD,KAAKC,WAAW,UAAU;EAC1B,KAAKC,WAAW,UAAU;EAC1B,KAAKC,UAAU,UAAU;EACzB,KAAKC,iBAAiB,KAAKK,QAAQ,KAAK,IAAI;EAC5C,KAAKJ,qBAAqB,KAAKK,YAAY,KAAK,IAAI;EACpD,KAAKJ,iBAAiB,KAAKK,QAAQ,KAAK,IAAI;EAC5C,KAAKJ,qBAAqB,KAAKK,YAAY,KAAK,IAAI;EACpD,KAAKJ,oBAAoB,KAAKK,WAAW,KAAK,IAAI;EAElD,KAAKb,QAAQ,GAAG,SAAS,KAAKI,cAAc;EAC5C,KAAKJ,QAAQ,GAAG,gBAAgB,KAAKI,cAAc;EACnD,KAAKJ,QAAQ,GAAG,QAAQ,KAAKK,kBAAkB;EAC/C,KAAKL,QAAQ,KAAK,UAAU,KAAKM,cAAc;EAC/C,KAAKN,QAAQ,KAAK,SAAS,KAAKO,kBAAkB;EAClD,KAAKP,QAAQ,KAAK,QAAQ,KAAKQ,iBAAiB;CACjD;CAEA,IAAI,SAAuB;EAC1B,OAAO,KAAKR;CACb;CAEA,IAAI,QAAiB;EACpB,OAAO,KAAKc;CACb;CAEA,IAAI,QAA2B;EAC9B,OAAO,KAAKC;CACb;CAEA,IAAI,UAA+B;EAClC,OAAO,KAAKd;CACb;CAEA,QAAc;EACb,KAAKa,SAAS;CACf;CAEA,QAAQ,OAAoB;EAC3B,KAAKA,SAAS;EACd,IAAI,KAAKC,WAAW,KAAA,GAAW,KAAKA,SAAS;CAC9C;CAEA,YAAY,MAAoB;EAC/B,KAAKD,SAAS;EACd,IAAI,KAAKC,WAAW,KAAA,GACnB,KAAKA,yBAAS,IAAI,MAAM,8BAA8B,OAAO,IAAI,EAAE,EAAE;CAEvE;CAEA,UAAgB;EACf,KAAKf,QAAQ,IAAI,SAAS,KAAKO,kBAAkB;EACjD,KAAKP,QAAQ,IAAI,QAAQ,KAAKQ,iBAAiB;EAC/C,KAAKN,SAAS,IAAI;CACnB;CAEA,YAAY,OAAoB;EAC/B,KAAKF,QAAQ,IAAI,UAAU,KAAKM,cAAc;EAC9C,KAAKN,QAAQ,IAAI,QAAQ,KAAKQ,iBAAiB;EAC/C,KAAKL,QAAQ,KAAK;CACnB;CAEA,WAAW,MAAoB;EAC9B,KAAKH,QAAQ,IAAI,UAAU,KAAKM,cAAc;EAC9C,KAAKN,QAAQ,IAAI,SAAS,KAAKO,kBAAkB;EACjD,KAAKJ,wBAAQ,IAAI,MAAM,mDAAmD,OAAO,IAAI,EAAE,EAAE,CAAC;CAC3F;AACD;;;;;;;;;;;;;;ACjFA,SAAgB,QAAQ,OAAgB,IAA4B;CACnE,MAAM,WAAA,GAAU,oBAAA,QAAA,OAAc;EAC7B,IAAI,EAAA,GAAC,oBAAA,SAAA,CAAS,KAAK,GAAG,OAAO;EAC7B,IAAI,MAAM,OAAO,IAAI,OAAO;EAC5B,IAAI,MAAM,OAAO,MAAM,OAAO,WAAW;EACzC,OAAO,MAAM,OAAO,SAAS,OAAO,MAAM,UAAU;CACrD,CAAC;CACD,OAAO,QAAQ,WAAW,QAAQ;AACnC;;;;;;;;;;;;ACLA,IAAa,WAAb,MAA+B;CAC9B;CACA;CACA;CACA;CACA;CACA,MAAe,OAAO,WAAW;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,WAAW;CAEX,YACC,QACA,OACA,WACA,QACC;EACD,KAAKa,UAAU;EACf,KAAKC,UAAU,OAAO;EACtB,KAAKC,SAAS;EACd,KAAKC,aAAa;EAClB,KAAKC,UAAU;EACf,MAAM,aAAa,QAAQ,cAAuB;EAClD,KAAKE,WAAW,WAAW;EAC3B,KAAKC,WAAW,WAAW;EAC3B,KAAKC,UAAU,WAAW;EAC1B,KAAKC,kBAAkB,KAAKK,SAAS,KAAK,IAAI;EAC9C,KAAKJ,uBAAuB,KAAKK,cAAc,KAAK,IAAI;EACxD,KAAKJ,gBAAgB,KAAKK,OAAO,KAAK,IAAI;EAC1C,KAAKJ,eAAe,KAAKK,MAAM,KAAK,IAAI;EACxC,KAAKJ,gBAAgB,KAAKK,OAAO,KAAK,IAAI;EAC1C,KAAKC,OAAO;CACb;CAEA,IAAI,UAA4B;EAC/B,OAAO,KAAKb;CACb;CAEA,SAAe;EACd,IAAI,KAAKN,QAAQ,UAAU,KAAA,KAAa,CAAC,KAAKA,QAAQ,OAAO;GAC5D,KAAKoB,MAAM,KAAKpB,QAAQ,yBAAS,IAAI,MAAM,uBAAuB,CAAC;GACnE;EACD;EACA,KAAKC,QAAQ,GAAG,WAAW,KAAKQ,eAAe;EAC/C,KAAKR,QAAQ,GAAG,gBAAgB,KAAKS,oBAAoB;EACzD,KAAKT,QAAQ,GAAG,SAAS,KAAKU,aAAa;EAC3C,KAAKV,QAAQ,GAAG,QAAQ,KAAKW,YAAY;EACzC,IAAI,KAAKT,WAAW,OAAO,SAAS;GACnC,KAAKe,OAAO;GACZ;EACD;EACA,KAAKf,WAAW,OAAO,iBAAiB,SAAS,KAAKU,eAAe,EAAE,MAAM,KAAK,CAAC;EACnF,IAAI;GACH,KAAKZ,QAAQ,YAAY;IAAE,IAAI,KAAKI;IAAK,SAAS;IAAO,OAAO,KAAKH;GAAO,CAAC;EAC9E,SAAS,OAAgB;GACxB,KAAKkB,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;EACrE;CACD;CAEA,SAAS,OAAsB;EAC9B,IAAI,EAAA,GAAC,oBAAA,SAAA,CAAS,KAAK,GAAG;EACtB,MAAM,MAAA,GAAK,oBAAA,QAAA,OAAc,MAAM,EAAE;EACjC,IAAI,CAAC,GAAG,WAAW,GAAG,UAAU,KAAKf,KAAK;EAC1C,IAAI,CAAC,QAAQ,OAAO,KAAKA,GAAG,GAAG;GAC9B,KAAKgB,2BAAW,IAAI,MAAM,4BAA4B,CAAC;GACvD;EACD;EACA,IAAI,MAAM,IAAI;GACb,MAAM,QAAQ,MAAM;GACpB,IAAI;IACH,IAAI,KAAKjB,QAAQ,KAAK,GAAG,KAAKkB,SAAS,KAAK;SACvC,KAAKF,sBAAM,IAAI,MAAM,oCAAoC,CAAC;GAChE,SAAS,OAAgB;IACxB,KAAKA,MAAM,KAAK;GACjB;GACA;EACD;EACA,KAAKA,MAAM,IAAI,MAAM,MAAM,KAAK,CAAC;CAClC;CAEA,cAAc,OAAoB;EACjC,KAAKC,WAAW,KAAK;CACtB;CAEA,OAAO,OAAoB;EAC1B,KAAKD,MAAM,KAAK;CACjB;CAEA,QAAc;EACb,KAAKA,MAAM,KAAKpB,QAAQ,yBAAS,IAAI,MAAM,sBAAsB,CAAC;CACnE;CAEA,SAAe;EACd,MAAM,eAA0B,CAAC;EACjC,IAAI;GACH,KAAKC,QAAQ,YAAY;IAAE,IAAI,KAAKI;IAAK,SAAS;GAAQ,CAAC;EAC5D,SAAS,OAAgB;GACxB,aAAa,KAAK,KAAK;EACxB;EACA,KAAKgB,WAAW,KAAKlB,WAAW,OAAO,QAAQ,YAAY;CAC5D;CAEA,WAAW,OAAgB,eAAmC,CAAC,GAAS;EACvE,IAAI,KAAKoB,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKC,QAAQ;EACb,IAAI,KAAKxB,mBAAmB,QAAQ,KAAKA,QAAQ,MAAM;EACvD,IAAI;EACJ,IAAI;GACH,cAAc,KAAKC,QAAQ,UAAU;EACtC,SAAS,OAAgB;GACxB,KAAKO,QAAQ,IAAI,eAAe;IAAC;IAAO,GAAG;IAAc;GAAK,GAAG,2BAA2B,CAAC;GAC7F;EACD;EACA,YAAiB,WACV;GACL,IAAI,aAAa,WAAW,GAAG,KAAKA,QAAQ,KAAK;QAEhD,KAAKA,QACJ,IAAI,eAAe,CAAC,OAAO,GAAG,YAAY,GAAG,kCAAkC,CAChF;EAEF,IACC,UACA,KAAKA,QACJ,IAAI,eAAe;GAAC;GAAO,GAAG;GAAc;EAAK,GAAG,2BAA2B,CAChF,CACF;CACD;CAEA,SAAS,OAAsB;EAC9B,IAAI,KAAKe,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKC,QAAQ;EACb,KAAKjB,SAAS,KAAK;CACpB;CAEA,MAAM,OAAsB;EAC3B,IAAI,KAAKgB,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKC,QAAQ;EACb,KAAKhB,QAAQ,KAAK;CACnB;CAEA,UAAgB;EACf,KAAKP,QAAQ,IAAI,WAAW,KAAKQ,eAAe;EAChD,KAAKR,QAAQ,IAAI,gBAAgB,KAAKS,oBAAoB;EAC1D,KAAKT,QAAQ,IAAI,SAAS,KAAKU,aAAa;EAC5C,KAAKV,QAAQ,IAAI,QAAQ,KAAKW,YAAY;EAC1C,KAAKT,WAAW,OAAO,oBAAoB,SAAS,KAAKU,aAAa;CACvE;AACD;;;;;;;;;;;;;;;;;;;;;;;;AC3IA,SAAgB,YAAY,QAAsB,YAA0C;CAC3F,OAAO,IAAI,OAAO,QAAQ,UAAU,CAAC,CAAC;AACvC;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,SACf,QACA,OACA,WACA,QACmB;CACnB,OAAO,IAAI,SAAS,QAAQ,OAAO,WAAW,MAAM,CAAC,CAAC;AACvD;;;AC3DA,SAAS,SAAS,OAAkD;CACnE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC3E;AAIA,SAAS,MAAM,OAA2E;CACzF,OACC,SAAS,KAAK,KAAK,OAAO,MAAM,OAAO,YAAY,MAAM,YAAY,SAAS,WAAW;AAE3F;AAGA,SAAS,QAAQ,OAAkD;CAClE,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,OAAO,YAAY,MAAM,YAAY;AAC7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,YAA6B,SAAoD;CAChG,MAAM,OAAO,oBAAA;CACb,IAAI,SAAS,MAAM;CACnB,MAAM,QAAQ,QAAQ;CACtB,MAAM,UAAU,QAAQ;CACxB,MAAM,8BAAc,IAAI,IAA6B;CACrD,KAAK,GAAG,YAAY,QAAiB;EACpC,IAAI,QAAQ,GAAG,GAAG;GACjB,YAAY,IAAI,IAAI,EAAE,CAAC,EAAE,MAAM;GAC/B;EACD;EACA,IAAI,CAAC,MAAM,GAAG,GAAG;EACjB,MAAM,KAAK,IAAI;EACf,MAAM,aAAa,IAAI,gBAAgB;EACvC,YAAY,IAAI,IAAI,UAAU;EAC9B,QAAa,QAAQ,CAAC,CACpB,WAAW;GACX,IAAI,CAAC,MAAM,IAAI,KAAK,GACnB,MAAM,IAAI,MAAM,mCAAmC;GAEpD,MAAM,QAAQ,IAAI;GAClB,OAAO,QAAQ,OAAO,EAAE,QAAQ,WAAW,OAAO,CAAC;EACpD,CAAC,CAAC,CACD,MAAM,UAAU;GAChB,YAAY,OAAO,EAAE;GACrB,KAAK,YAAY;IAAE;IAAI,IAAI;IAAM;GAAM,CAAC;EACzC,CAAC,CAAC,CACD,OAAO,UAAmB;GAC1B,YAAY,OAAO,EAAE;GACrB,IAAI,UAAU;GACd,IAAI;IACH,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAChE,QAAQ,CAAC;GACT,IAAI;IACH,KAAK,YAAY;KAAE;KAAI,IAAI;KAAO,OAAO;IAAQ,CAAC;GACnD,QAAQ;IACP,IAAI;KACH,KAAK,MAAM;IACZ,QAAQ,CAAC;GACV;EACD,CAAC;CACH,CAAC;AACF;;;;;;;;;;ACxFA,IAAa,aAAb,MAAyC;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,SAA6C;EACxD,KAAKY,UAAU,QAAQ;EACvB,KAAKC,SAAS,QAAQ;EACtB,KAAKC,UAAU,QAAQ;EACvB,KAAKC,cAAc,QAAQ;EAC3B,KAAKC,eAAe,QAAQ;EAC5B,KAAKC,WAAW,QAAQ;EACxB,KAAKC,WAAW,QAAQ;EACxB,KAAKC,SAAS,QAAQ;CACvB;CAEA,QAA0C;EACzC,QAAA,GAAO,UAAA,aAAA,CAA0C;GAChD,MAAM;IACL,QAAQ,KAAKC,QAAQ,KAAK,IAAI;IAC9B,SAAS,KAAKC,SAAS,KAAK,IAAI;IAChC,UAAU,KAAKC,UAAU,KAAK,IAAI;IAClC,GAAI,KAAKN,iBAAiB,KAAA,IAAY,EAAE,KAAK,KAAKA,aAAa,IAAI,CAAC;GACrE;GACA,SAAS,KAAKO,QAAQ,KAAK,IAAI;GAC/B,GAAI,KAAKP,iBAAiB,KAAA,IAAY,EAAE,aAAa,KAAKA,aAAa,IAAI,CAAC;GAC5E,GAAI,KAAKC,aAAa,KAAA,IAAY,EAAE,SAAS,KAAKA,SAAS,IAAI,CAAC;GAChE,GAAI,KAAKC,aAAa,KAAA,IAAY,EAAE,SAAS,KAAKA,SAAS,IAAI,CAAC;GAChE,GAAI,KAAKC,WAAW,KAAA,IAAY,EAAE,OAAO,KAAKA,OAAO,IAAI,CAAC;EAC3D,CAAC;CACF;CAEA,UAA+B;EAC9B,OAAO,YAAY,KAAKP,SAAS,KAAKG,WAAW;CAClD;CAEA,MAAMM,SAAS,QAAmC;EACjD,MAAM,OAAO,OAAO,UAAU;CAC/B;CAEA,UAAU,QAA6B;EACtC,OAAO,OAAO,SAAS,OAAO,OAAO,WAAW;CACjD;CAEA,QAAQ,OAAe,QAAoB,WAA6C;EACvF,MAAM,WAAA,GAAU,oBAAA,QAAA,OAAc,KAAKR,OAAO,KAAK,CAAC;EAChD,IAAI,CAAC,QAAQ,SAAS,OAAO,QAAQ,OAAO,QAAQ,KAAK;EACzD,IAAI,CAAC,QAAQ,OACZ,OAAO,QAAQ,uBAAO,IAAI,MAAM,mCAAmC,CAAC;EAErE,OAAO,SAAS,QAAQ,OAAO,WAAW,KAAKC,OAAO;CACvD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClCA,SAAgB,qBACf,MACA,OACqC;CACrC,QAAA,GAAO,iBAAA,yBAAA,CAAyB,QAAA,GAAO,2BAAA,iBAAA,CAAiB,IAAI,CAAC;AAC9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,iBACf,SACmC;CACnC,OAAO,IAAI,WAAW,OAAO,CAAC,CAAC,MAAM;AACtC"}
@@ -1,4 +1,5 @@
1
1
  import { ContractShape } from '@orkestrel/contract';
2
+ import { Guard } from '@orkestrel/contract';
2
3
  import { Infer } from '@orkestrel/contract';
3
4
  import { QueueExecution } from '@orkestrel/queue';
4
5
  import { QueueStoreInterface } from '@orkestrel/queue';
@@ -25,8 +26,8 @@ import { WorkerInterface } from '../core/index.ts';
25
26
  *
26
27
  * @example
27
28
  * ```ts
28
- * import { stringShape } from '@src/core'
29
- * import { createJSONQueueStore } from '@src/server'
29
+ * import { stringShape } from '@orkestrel/contract'
30
+ * import { createJSONQueueStore } from '@orkestrel/worker/server'
30
31
  *
31
32
  * const store = createJSONQueueStore('data/queue.json', stringShape())
32
33
  * await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })
@@ -65,7 +66,7 @@ export declare function createJSONQueueStore<TInput extends ContractShape>(path:
65
66
  *
66
67
  * @example
67
68
  * ```ts
68
- * import { createNodeWorker } from '@src/server'
69
+ * import { createNodeWorker } from '@orkestrel/worker/server'
69
70
  *
70
71
  * const worker = createNodeWorker({
71
72
  * script: new URL('./double.js', import.meta.url),
@@ -75,7 +76,7 @@ export declare function createJSONQueueStore<TInput extends ContractShape>(path:
75
76
  * })
76
77
  *
77
78
  * const doubled = await worker.enqueue(21) // 42, computed on a worker thread
78
- * worker.destroy() // terminates every thread
79
+ * await worker.destroy() // terminates every thread
79
80
  * ```
80
81
  */
81
82
  export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOptions<TInput, TResult>): WorkerInterface<TInput, TResult>;
@@ -88,15 +89,15 @@ export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOpt
88
89
  * that id: a success `value` is narrowed through `result` (a value that fails the guard
89
90
  * rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.
90
91
  * A thread that ALREADY died rejects synchronously at entry from the latched
91
- * {@link NodeThread.death} — its death events fired before this dispatch existed (under
92
- * load they arrive in one batched exit drain) and will never fire again, so waiting on
93
- * the listeners below would dangle forever; the latch makes the death total across every
94
- * event ordering. If the thread `error`s / `exit`s mid-flight it is marked dead and the
95
- * job rejects. On `execution.signal` abort it posts an `abort` envelope (cooperative) AND
96
- * evicts the thread `alive = false` + `terminate()` — because CPU-bound work cannot
97
- * honour the signal; the freed pool slot then gets a fresh thread. Every listener (the
98
- * thread's `message` / `error` / `exit` and the signal's `abort`) is removed on settle,
99
- * and a `settled` guard prevents a double-settle.
92
+ * {@link NodeThread.death} — its death events fired before this dispatch existed and will
93
+ * never fire again, so waiting on the listeners below would dangle forever; the latch makes
94
+ * death total across every event ordering. If the thread `error`s / `exit`s mid-flight it is
95
+ * marked dead and the
96
+ * job rejects. An inbound `messageerror` also evicts and terminates the thread before
97
+ * rejection. On `execution.signal` abort it contains the cooperative `abort` post,
98
+ * evicts the thread, and observes `terminate()` settlement because CPU-bound work cannot
99
+ * honour the signal; the freed pool slot then gets a fresh thread. Every per-job listener
100
+ * (`message` / `messageerror` / `error` / `exit` / `abort`) is removed on settle.
100
101
  *
101
102
  * @typeParam TResult - The reply type the `result` guard narrows to
102
103
  * @param thread - The leased thread to run the job on
@@ -107,36 +108,16 @@ export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOpt
107
108
  */
108
109
  export declare function dispatch<TResult>(thread: NodeThread, input: unknown, execution: QueueExecution, result: Guard<TResult>): Promise<TResult>;
109
110
 
110
- /**
111
- * A runtime type predicate used to narrow a wire payload with no assertion.
112
- *
113
- * @remarks
114
- * Mirrors the core `Guard<T>` (a total `(value: unknown) => value is T` predicate,
115
- * AGENTS §14) but is re-declared here so the server workers surface is self-describing
116
- * and the worker-side `serve.ts` — which may not import from `@src/core` (it loads as
117
- * raw `.ts` inside a spawned thread) — shares the same vocabulary. A `Guard` NEVER
118
- * throws; adversarial input returns `false`. It is the zero-`as` bridge across the
119
- * structured-clone boundary: the main side narrows each reply value through the
120
- * `result` guard and the input through the `input` guard, so a generic `TInput` /
121
- * `TResult` is reconstructed by validation rather than asserted.
122
- *
123
- * @typeParam T - The type a value is narrowed to when the predicate holds
124
- */
125
- export declare type Guard<T> = (value: unknown) => value is T;
126
-
127
111
  /**
128
112
  * Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.
129
113
  *
130
114
  * @remarks
131
- * A total {@link Guard}-style predicate (never throws): a record whose `id` matches and
132
- * whose `ok` discriminant is well-formed (a `true` carries any `value`; a `false` carries a
133
- * string `error`). Anything else — another job's reply, a malformed payload — is `false`, so
134
- * a {@link dispatch} listener ignores it (a thread that chatters on the channel can't corrupt
135
- * a job).
115
+ * A total predicate: a record whose `id` matches and whose `ok` discriminant is well-formed.
116
+ * Anything else is rejected so a dispatch listener can ignore foreign or malformed messages.
136
117
  *
137
118
  * @param value - The inbound message to narrow
138
119
  * @param id - The job id a matching reply must carry
139
- * @returns `true` (narrowing `value` to {@link Reply}) when it is this job's well-formed reply
120
+ * @returns `true` when the value is this job's well-formed reply
140
121
  */
141
122
  export declare function isReply(value: unknown, id: string): value is Reply;
142
123
 
@@ -145,22 +126,22 @@ export declare function isReply(value: unknown, id: string): value is Reply;
145
126
  * {@link createNodeWorker} leases per job.
146
127
  *
147
128
  * @remarks
148
- * `alive` starts `true` and flips to `false` the moment the thread `error`s, `exit`s, or
149
- * is evicted on abort; the pool's `validate` reads `alive && worker.threadId > 0`, so a
129
+ * `alive` starts `true` and flips to `false` when the thread `error`s, reports a
130
+ * `messageerror`, exits, or is evicted on abort; the pool's `validate` reads
131
+ * `alive && worker.threadId > 0`, so a
150
132
  * dead thread is destroyed and replaced rather than reused. `death` LATCHES the first
151
- * terminal event (`error`'s `Error`, or a synthesized one on `exit`) — the death-signal
133
+ * terminal event (`error` / `messageerror`, or a synthesized error on `exit`) — the death-signal
152
134
  * record a `dispatch` checks at entry, so a job dispatched AFTER the thread died (its
153
135
  * death events already fired and will never fire again) rejects immediately instead of
154
- * awaiting events that already happened. Under event-loop pressure Node delivers a dead
155
- * thread's `online` + `error` + `exit` in ONE synchronous exit-drain batch, starving the
156
- * microtask chain that attaches the dispatch listeners until after every death event —
157
- * the latch is what makes that ordering safe. `worker` is the underlying
136
+ * awaiting events that already happened. A thread can become terminal before the readiness
137
+ * promise continuation attaches dispatch listeners; the latch is what makes that ordering
138
+ * safe. `worker` is the underlying
158
139
  * `node:worker_threads` thread (its `postMessage` / `terminate` drive the protocol).
159
140
  */
160
141
  export declare interface NodeThread {
161
142
  readonly worker: Worker;
162
- alive: boolean;
163
- death: Error | undefined;
143
+ readonly alive: boolean;
144
+ readonly death: Error | undefined;
164
145
  }
165
146
 
166
147
  /**
@@ -168,8 +149,9 @@ export declare interface NodeThread {
168
149
  *
169
150
  * @remarks
170
151
  * - `script` — the worker module each pooled thread runs; its module must call
171
- * `serveWorker(...)`. A `.ts` script requires Node 23.6 (native type-stripping); on
172
- * older Node point this at a built `.js` / `.mjs`.
152
+ * `serveWorker(...)`. Raw TypeScript is unflagged on Node 22.18+ and Node 23.6+;
153
+ * Node 22.12–22.17 and Node 23.0–23.5 require `--experimental-strip-types`. A built
154
+ * `.js` / `.mjs` script remains an alternative across supported Node versions.
173
155
  * - `input` — narrows the work payload BEFORE it crosses the structured-clone boundary
174
156
  * (fail-fast) and supplies the `TInput` inference, so call sites need no type argument.
175
157
  * - `result` — narrows every reply value coming back from a thread; an invalid reply
@@ -177,7 +159,8 @@ export declare interface NodeThread {
177
159
  * - `workerData` — opaque data cloned to every thread once at spawn (read there via
178
160
  * `serveWorker`'s host `workerData`); must be structured-cloneable.
179
161
  * - `concurrency` — the maximum jobs in flight at once; the thread pool's `max` matches
180
- * it, so at most this many threads exist. Defaults to `1`. Floored at `1`.
162
+ * it, so at most this many threads exist. Defaults to `1` and must be a positive safe
163
+ * integer, as validated by the underlying queue.
181
164
  * - `retries` — the default extra attempts per job on failure / timeout; defaults to `0`.
182
165
  * - `timeout` — the default per-attempt deadline in milliseconds; defaults to none.
183
166
  * - `store` — durable backing for outstanding jobs (survives a restart; `restore()`
@@ -205,11 +188,11 @@ export declare interface NodeWorkerOptions<TInput, TResult> {
205
188
  * @remarks
206
189
  * Internal plumbing rather than public call surface, but centralized here per AGENTS §5 (an
207
190
  * impl file holds only its class / functions). A reply is a discriminated union on `ok`: a
208
- * `true` carries any opaque `value` (narrowed at the boundary by the `result` {@link Guard},
191
+ * `true` carries any opaque `value` (narrowed at the boundary by the `result` guard,
209
192
  * with no `as`); a `false` carries a string `error`. The worker-side `serve.ts` cannot import
210
193
  * this (it loads as raw source in a spawned thread, AGENTS §5 exception) and posts the same
211
- * shape structurally. The `id` ties a reply to its job, so a stray / foreign-id message is
212
- * ignored.
194
+ * shape structurally. The `id` ties a reply to its job: id-less / foreign-id chatter is ignored,
195
+ * while a matching-id malformed envelope taints the thread and causes dispatch to terminate it.
213
196
  */
214
197
  export declare type Reply = {
215
198
  readonly id: string;
@@ -229,7 +212,10 @@ export declare type Reply = {
229
212
  * run/abort protocol: a `run` message narrows its `input` through `options.input` (an
230
213
  * invalid payload replies with an error envelope, never running the handler), then runs
231
214
  * `options.handler(input, { signal })` and replies `{ id, ok: true, value }` on success or
232
- * `{ id, ok: false, error }` on throw. Each in-flight job has its own `AbortController`,
215
+ * `{ id, ok: false, error }` on throw. Input-guard throws use the same failure envelope. If a
216
+ * success value cannot be cloned, the post is retried as a clone-safe failure; if that post also
217
+ * fails, the parent port closes so the main side observes thread exit instead of waiting forever.
218
+ * Each in-flight job has its own `AbortController`,
233
219
  * so an `abort` message for that id fires the handler's `signal` (cooperative — the main
234
220
  * side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
235
221
  * Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
@@ -242,7 +228,7 @@ export declare type Reply = {
242
228
  * @example
243
229
  * ```ts
244
230
  * // double.ts — a worker script
245
- * import { serveWorker } from '@src/server'
231
+ * import { serveWorker } from '@orkestrel/worker/server'
246
232
  *
247
233
  * serveWorker<number, number>({
248
234
  * input: (value): value is number => typeof value === 'number',
@@ -284,11 +270,11 @@ export declare interface ServeWorkerOptions<TInput, TResult> {
284
270
  * listeners that flip `alive` to `false` AND latch the first terminal event on
285
271
  * {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via
286
272
  * its own listeners), to the pool's `validate` (via `alive`), and — crucially — to a
287
- * dispatch that attaches AFTER the death (via the latch). The latch closes a real race:
288
- * under event-loop pressure a dead thread's `online` + `error` + `exit` are delivered in
289
- * ONE synchronous exit-drain batch, so every death event fires before the microtask chain
290
- * resolving this spawn can hand the thread to `dispatch` without the latch that job
291
- * would await events that already fired, forever. The pool's `create` hook calls this.
273
+ * dispatch that attaches AFTER the death (via the latch). A `messageerror` is terminal too,
274
+ * so a thread whose inbound payload could not be deserialized is never reused. The latch
275
+ * closes a real race: a thread can become terminal before the readiness promise continuation
276
+ * hands it to `dispatch`, leaving no future death event for that dispatch to observe. Without
277
+ * the latch, that job would wait forever. The pool's `create` hook calls this.
292
278
  *
293
279
  * @param script - The worker module each thread runs (must call `serveWorker`)
294
280
  * @param workerData - Opaque, structured-cloneable data handed to the thread at spawn
@@ -1,4 +1,5 @@
1
1
  import { ContractShape } from '@orkestrel/contract';
2
+ import { Guard } from '@orkestrel/contract';
2
3
  import { Infer } from '@orkestrel/contract';
3
4
  import { QueueExecution } from '@orkestrel/queue';
4
5
  import { QueueStoreInterface } from '@orkestrel/queue';
@@ -25,8 +26,8 @@ import { WorkerInterface } from '../core/index.ts';
25
26
  *
26
27
  * @example
27
28
  * ```ts
28
- * import { stringShape } from '@src/core'
29
- * import { createJSONQueueStore } from '@src/server'
29
+ * import { stringShape } from '@orkestrel/contract'
30
+ * import { createJSONQueueStore } from '@orkestrel/worker/server'
30
31
  *
31
32
  * const store = createJSONQueueStore('data/queue.json', stringShape())
32
33
  * await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })
@@ -65,7 +66,7 @@ export declare function createJSONQueueStore<TInput extends ContractShape>(path:
65
66
  *
66
67
  * @example
67
68
  * ```ts
68
- * import { createNodeWorker } from '@src/server'
69
+ * import { createNodeWorker } from '@orkestrel/worker/server'
69
70
  *
70
71
  * const worker = createNodeWorker({
71
72
  * script: new URL('./double.js', import.meta.url),
@@ -75,7 +76,7 @@ export declare function createJSONQueueStore<TInput extends ContractShape>(path:
75
76
  * })
76
77
  *
77
78
  * const doubled = await worker.enqueue(21) // 42, computed on a worker thread
78
- * worker.destroy() // terminates every thread
79
+ * await worker.destroy() // terminates every thread
79
80
  * ```
80
81
  */
81
82
  export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOptions<TInput, TResult>): WorkerInterface<TInput, TResult>;
@@ -88,15 +89,15 @@ export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOpt
88
89
  * that id: a success `value` is narrowed through `result` (a value that fails the guard
89
90
  * rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.
90
91
  * A thread that ALREADY died rejects synchronously at entry from the latched
91
- * {@link NodeThread.death} — its death events fired before this dispatch existed (under
92
- * load they arrive in one batched exit drain) and will never fire again, so waiting on
93
- * the listeners below would dangle forever; the latch makes the death total across every
94
- * event ordering. If the thread `error`s / `exit`s mid-flight it is marked dead and the
95
- * job rejects. On `execution.signal` abort it posts an `abort` envelope (cooperative) AND
96
- * evicts the thread `alive = false` + `terminate()` — because CPU-bound work cannot
97
- * honour the signal; the freed pool slot then gets a fresh thread. Every listener (the
98
- * thread's `message` / `error` / `exit` and the signal's `abort`) is removed on settle,
99
- * and a `settled` guard prevents a double-settle.
92
+ * {@link NodeThread.death} — its death events fired before this dispatch existed and will
93
+ * never fire again, so waiting on the listeners below would dangle forever; the latch makes
94
+ * death total across every event ordering. If the thread `error`s / `exit`s mid-flight it is
95
+ * marked dead and the
96
+ * job rejects. An inbound `messageerror` also evicts and terminates the thread before
97
+ * rejection. On `execution.signal` abort it contains the cooperative `abort` post,
98
+ * evicts the thread, and observes `terminate()` settlement because CPU-bound work cannot
99
+ * honour the signal; the freed pool slot then gets a fresh thread. Every per-job listener
100
+ * (`message` / `messageerror` / `error` / `exit` / `abort`) is removed on settle.
100
101
  *
101
102
  * @typeParam TResult - The reply type the `result` guard narrows to
102
103
  * @param thread - The leased thread to run the job on
@@ -107,36 +108,16 @@ export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOpt
107
108
  */
108
109
  export declare function dispatch<TResult>(thread: NodeThread, input: unknown, execution: QueueExecution, result: Guard<TResult>): Promise<TResult>;
109
110
 
110
- /**
111
- * A runtime type predicate used to narrow a wire payload with no assertion.
112
- *
113
- * @remarks
114
- * Mirrors the core `Guard<T>` (a total `(value: unknown) => value is T` predicate,
115
- * AGENTS §14) but is re-declared here so the server workers surface is self-describing
116
- * and the worker-side `serve.ts` — which may not import from `@src/core` (it loads as
117
- * raw `.ts` inside a spawned thread) — shares the same vocabulary. A `Guard` NEVER
118
- * throws; adversarial input returns `false`. It is the zero-`as` bridge across the
119
- * structured-clone boundary: the main side narrows each reply value through the
120
- * `result` guard and the input through the `input` guard, so a generic `TInput` /
121
- * `TResult` is reconstructed by validation rather than asserted.
122
- *
123
- * @typeParam T - The type a value is narrowed to when the predicate holds
124
- */
125
- export declare type Guard<T> = (value: unknown) => value is T;
126
-
127
111
  /**
128
112
  * Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.
129
113
  *
130
114
  * @remarks
131
- * A total {@link Guard}-style predicate (never throws): a record whose `id` matches and
132
- * whose `ok` discriminant is well-formed (a `true` carries any `value`; a `false` carries a
133
- * string `error`). Anything else — another job's reply, a malformed payload — is `false`, so
134
- * a {@link dispatch} listener ignores it (a thread that chatters on the channel can't corrupt
135
- * a job).
115
+ * A total predicate: a record whose `id` matches and whose `ok` discriminant is well-formed.
116
+ * Anything else is rejected so a dispatch listener can ignore foreign or malformed messages.
136
117
  *
137
118
  * @param value - The inbound message to narrow
138
119
  * @param id - The job id a matching reply must carry
139
- * @returns `true` (narrowing `value` to {@link Reply}) when it is this job's well-formed reply
120
+ * @returns `true` when the value is this job's well-formed reply
140
121
  */
141
122
  export declare function isReply(value: unknown, id: string): value is Reply;
142
123
 
@@ -145,22 +126,22 @@ export declare function isReply(value: unknown, id: string): value is Reply;
145
126
  * {@link createNodeWorker} leases per job.
146
127
  *
147
128
  * @remarks
148
- * `alive` starts `true` and flips to `false` the moment the thread `error`s, `exit`s, or
149
- * is evicted on abort; the pool's `validate` reads `alive && worker.threadId > 0`, so a
129
+ * `alive` starts `true` and flips to `false` when the thread `error`s, reports a
130
+ * `messageerror`, exits, or is evicted on abort; the pool's `validate` reads
131
+ * `alive && worker.threadId > 0`, so a
150
132
  * dead thread is destroyed and replaced rather than reused. `death` LATCHES the first
151
- * terminal event (`error`'s `Error`, or a synthesized one on `exit`) — the death-signal
133
+ * terminal event (`error` / `messageerror`, or a synthesized error on `exit`) — the death-signal
152
134
  * record a `dispatch` checks at entry, so a job dispatched AFTER the thread died (its
153
135
  * death events already fired and will never fire again) rejects immediately instead of
154
- * awaiting events that already happened. Under event-loop pressure Node delivers a dead
155
- * thread's `online` + `error` + `exit` in ONE synchronous exit-drain batch, starving the
156
- * microtask chain that attaches the dispatch listeners until after every death event —
157
- * the latch is what makes that ordering safe. `worker` is the underlying
136
+ * awaiting events that already happened. A thread can become terminal before the readiness
137
+ * promise continuation attaches dispatch listeners; the latch is what makes that ordering
138
+ * safe. `worker` is the underlying
158
139
  * `node:worker_threads` thread (its `postMessage` / `terminate` drive the protocol).
159
140
  */
160
141
  export declare interface NodeThread {
161
142
  readonly worker: Worker;
162
- alive: boolean;
163
- death: Error | undefined;
143
+ readonly alive: boolean;
144
+ readonly death: Error | undefined;
164
145
  }
165
146
 
166
147
  /**
@@ -168,8 +149,9 @@ export declare interface NodeThread {
168
149
  *
169
150
  * @remarks
170
151
  * - `script` — the worker module each pooled thread runs; its module must call
171
- * `serveWorker(...)`. A `.ts` script requires Node 23.6 (native type-stripping); on
172
- * older Node point this at a built `.js` / `.mjs`.
152
+ * `serveWorker(...)`. Raw TypeScript is unflagged on Node 22.18+ and Node 23.6+;
153
+ * Node 22.12–22.17 and Node 23.0–23.5 require `--experimental-strip-types`. A built
154
+ * `.js` / `.mjs` script remains an alternative across supported Node versions.
173
155
  * - `input` — narrows the work payload BEFORE it crosses the structured-clone boundary
174
156
  * (fail-fast) and supplies the `TInput` inference, so call sites need no type argument.
175
157
  * - `result` — narrows every reply value coming back from a thread; an invalid reply
@@ -177,7 +159,8 @@ export declare interface NodeThread {
177
159
  * - `workerData` — opaque data cloned to every thread once at spawn (read there via
178
160
  * `serveWorker`'s host `workerData`); must be structured-cloneable.
179
161
  * - `concurrency` — the maximum jobs in flight at once; the thread pool's `max` matches
180
- * it, so at most this many threads exist. Defaults to `1`. Floored at `1`.
162
+ * it, so at most this many threads exist. Defaults to `1` and must be a positive safe
163
+ * integer, as validated by the underlying queue.
181
164
  * - `retries` — the default extra attempts per job on failure / timeout; defaults to `0`.
182
165
  * - `timeout` — the default per-attempt deadline in milliseconds; defaults to none.
183
166
  * - `store` — durable backing for outstanding jobs (survives a restart; `restore()`
@@ -205,11 +188,11 @@ export declare interface NodeWorkerOptions<TInput, TResult> {
205
188
  * @remarks
206
189
  * Internal plumbing rather than public call surface, but centralized here per AGENTS §5 (an
207
190
  * impl file holds only its class / functions). A reply is a discriminated union on `ok`: a
208
- * `true` carries any opaque `value` (narrowed at the boundary by the `result` {@link Guard},
191
+ * `true` carries any opaque `value` (narrowed at the boundary by the `result` guard,
209
192
  * with no `as`); a `false` carries a string `error`. The worker-side `serve.ts` cannot import
210
193
  * this (it loads as raw source in a spawned thread, AGENTS §5 exception) and posts the same
211
- * shape structurally. The `id` ties a reply to its job, so a stray / foreign-id message is
212
- * ignored.
194
+ * shape structurally. The `id` ties a reply to its job: id-less / foreign-id chatter is ignored,
195
+ * while a matching-id malformed envelope taints the thread and causes dispatch to terminate it.
213
196
  */
214
197
  export declare type Reply = {
215
198
  readonly id: string;
@@ -229,7 +212,10 @@ export declare type Reply = {
229
212
  * run/abort protocol: a `run` message narrows its `input` through `options.input` (an
230
213
  * invalid payload replies with an error envelope, never running the handler), then runs
231
214
  * `options.handler(input, { signal })` and replies `{ id, ok: true, value }` on success or
232
- * `{ id, ok: false, error }` on throw. Each in-flight job has its own `AbortController`,
215
+ * `{ id, ok: false, error }` on throw. Input-guard throws use the same failure envelope. If a
216
+ * success value cannot be cloned, the post is retried as a clone-safe failure; if that post also
217
+ * fails, the parent port closes so the main side observes thread exit instead of waiting forever.
218
+ * Each in-flight job has its own `AbortController`,
233
219
  * so an `abort` message for that id fires the handler's `signal` (cooperative — the main
234
220
  * side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
235
221
  * Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
@@ -242,7 +228,7 @@ export declare type Reply = {
242
228
  * @example
243
229
  * ```ts
244
230
  * // double.ts — a worker script
245
- * import { serveWorker } from '@src/server'
231
+ * import { serveWorker } from '@orkestrel/worker/server'
246
232
  *
247
233
  * serveWorker<number, number>({
248
234
  * input: (value): value is number => typeof value === 'number',
@@ -284,11 +270,11 @@ export declare interface ServeWorkerOptions<TInput, TResult> {
284
270
  * listeners that flip `alive` to `false` AND latch the first terminal event on
285
271
  * {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via
286
272
  * its own listeners), to the pool's `validate` (via `alive`), and — crucially — to a
287
- * dispatch that attaches AFTER the death (via the latch). The latch closes a real race:
288
- * under event-loop pressure a dead thread's `online` + `error` + `exit` are delivered in
289
- * ONE synchronous exit-drain batch, so every death event fires before the microtask chain
290
- * resolving this spawn can hand the thread to `dispatch` without the latch that job
291
- * would await events that already fired, forever. The pool's `create` hook calls this.
273
+ * dispatch that attaches AFTER the death (via the latch). A `messageerror` is terminal too,
274
+ * so a thread whose inbound payload could not be deserialized is never reused. The latch
275
+ * closes a real race: a thread can become terminal before the readiness promise continuation
276
+ * hands it to `dispatch`, leaving no future death event for that dispatch to observe. Without
277
+ * the latch, that job would wait forever. The pool's `create` hook calls this.
292
278
  *
293
279
  * @param script - The worker module each thread runs (must call `serveWorker`)
294
280
  * @param workerData - Opaque, structured-cloneable data handed to the thread at spawn