@orkestrel/worker 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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>;
@@ -84,19 +85,23 @@ export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOpt
84
85
  * Dispatch one job to a leased {@link NodeThread} and await its narrowed reply.
85
86
  *
86
87
  * @remarks
87
- * Mints a fresh `id`, posts a `run` envelope, and resolves when the thread replies for
88
- * that id: a success `value` is narrowed through `result` (a value that fails the guard
88
+ * Mints a fresh per-dispatch correlation `id`, posts it with `job: execution.id`, and
89
+ * resolves when the thread replies for that correlation id. The stable Queue job id reaches
90
+ * the worker handler for idempotency across retries and restore; it is not caller identity or
91
+ * authentication / authorization evidence. Per-job consumer context remains explicit,
92
+ * structured-cloneable `input`; ambient context is not worker-thread transport. A success
93
+ * `value` is narrowed through `result` (a value that fails the guard
89
94
  * rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.
90
95
  * 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.
96
+ * {@link NodeThread.death} — its death events fired before this dispatch existed and will
97
+ * never fire again, so waiting on the listeners below would dangle forever; the latch makes
98
+ * death total across every event ordering. If the thread `error`s / `exit`s mid-flight it is
99
+ * marked dead and the
100
+ * job rejects. An inbound `messageerror` also evicts and terminates the thread before
101
+ * rejection. On `execution.signal` abort it contains the cooperative `abort` post,
102
+ * evicts the thread, and observes `terminate()` settlement because CPU-bound work cannot
103
+ * honour the signal; the freed pool slot then gets a fresh thread. Every per-job listener
104
+ * (`message` / `messageerror` / `error` / `exit` / `abort`) is removed on settle.
100
105
  *
101
106
  * @typeParam TResult - The reply type the `result` guard narrows to
102
107
  * @param thread - The leased thread to run the job on
@@ -107,23 +112,6 @@ export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOpt
107
112
  */
108
113
  export declare function dispatch<TResult>(thread: NodeThread, input: unknown, execution: QueueExecution, result: Guard<TResult>): Promise<TResult>;
109
114
 
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
115
  /**
128
116
  * Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.
129
117
  *
@@ -142,16 +130,16 @@ export declare function isReply(value: unknown, id: string): value is Reply;
142
130
  * {@link createNodeWorker} leases per job.
143
131
  *
144
132
  * @remarks
145
- * `alive` starts `true` and flips to `false` the moment the thread `error`s, `exit`s, or
146
- * is evicted on abort; the pool's `validate` reads `alive && worker.threadId > 0`, so a
133
+ * `alive` starts `true` and flips to `false` when the thread `error`s, reports a
134
+ * `messageerror`, exits, or is evicted on abort; the pool's `validate` reads
135
+ * `alive && worker.threadId > 0`, so a
147
136
  * dead thread is destroyed and replaced rather than reused. `death` LATCHES the first
148
- * terminal event (`error`'s `Error`, or a synthesized one on `exit`) — the death-signal
137
+ * terminal event (`error` / `messageerror`, or a synthesized error on `exit`) — the death-signal
149
138
  * record a `dispatch` checks at entry, so a job dispatched AFTER the thread died (its
150
139
  * death events already fired and will never fire again) rejects immediately instead of
151
- * awaiting events that already happened. Under event-loop pressure Node delivers a dead
152
- * thread's `online` + `error` + `exit` in ONE synchronous exit-drain batch, starving the
153
- * microtask chain that attaches the dispatch listeners until after every death event —
154
- * the latch is what makes that ordering safe. `worker` is the underlying
140
+ * awaiting events that already happened. A thread can become terminal before the readiness
141
+ * promise continuation attaches dispatch listeners; the latch is what makes that ordering
142
+ * safe. `worker` is the underlying
155
143
  * `node:worker_threads` thread (its `postMessage` / `terminate` drive the protocol).
156
144
  */
157
145
  export declare interface NodeThread {
@@ -165,8 +153,9 @@ export declare interface NodeThread {
165
153
  *
166
154
  * @remarks
167
155
  * - `script` — the worker module each pooled thread runs; its module must call
168
- * `serveWorker(...)`. A `.ts` script requires Node 23.6 (native type-stripping); on
169
- * older Node point this at a built `.js` / `.mjs`.
156
+ * `serveWorker(...)`. Raw TypeScript is unflagged on Node 22.18+ and Node 23.6+;
157
+ * Node 22.12–22.17 and Node 23.0–23.5 require `--experimental-strip-types`. A built
158
+ * `.js` / `.mjs` script remains an alternative across supported Node versions.
170
159
  * - `input` — narrows the work payload BEFORE it crosses the structured-clone boundary
171
160
  * (fail-fast) and supplies the `TInput` inference, so call sites need no type argument.
172
161
  * - `result` — narrows every reply value coming back from a thread; an invalid reply
@@ -174,7 +163,8 @@ export declare interface NodeThread {
174
163
  * - `workerData` — opaque data cloned to every thread once at spawn (read there via
175
164
  * `serveWorker`'s host `workerData`); must be structured-cloneable.
176
165
  * - `concurrency` — the maximum jobs in flight at once; the thread pool's `max` matches
177
- * it, so at most this many threads exist. Defaults to `1`. Floored at `1`.
166
+ * it, so at most this many threads exist. Defaults to `1` and must be a positive safe
167
+ * integer, as validated by the underlying queue.
178
168
  * - `retries` — the default extra attempts per job on failure / timeout; defaults to `0`.
179
169
  * - `timeout` — the default per-attempt deadline in milliseconds; defaults to none.
180
170
  * - `store` — durable backing for outstanding jobs (survives a restart; `restore()`
@@ -202,11 +192,11 @@ export declare interface NodeWorkerOptions<TInput, TResult> {
202
192
  * @remarks
203
193
  * Internal plumbing rather than public call surface, but centralized here per AGENTS §5 (an
204
194
  * impl file holds only its class / functions). A reply is a discriminated union on `ok`: a
205
- * `true` carries any opaque `value` (narrowed at the boundary by the `result` {@link Guard},
195
+ * `true` carries any opaque `value` (narrowed at the boundary by the `result` guard,
206
196
  * with no `as`); a `false` carries a string `error`. The worker-side `serve.ts` cannot import
207
197
  * this (it loads as raw source in a spawned thread, AGENTS §5 exception) and posts the same
208
- * shape structurally. The `id` ties a reply to its job, so a stray / foreign-id message is
209
- * ignored.
198
+ * shape structurally. The `id` ties a reply to its job: id-less / foreign-id chatter is ignored,
199
+ * while a matching-id malformed envelope taints the thread and causes dispatch to terminate it.
210
200
  */
211
201
  export declare type Reply = {
212
202
  readonly id: string;
@@ -225,9 +215,15 @@ export declare type Reply = {
225
215
  * Must be the spawned thread's module entry. It listens on the parent port for the
226
216
  * run/abort protocol: a `run` message narrows its `input` through `options.input` (an
227
217
  * invalid payload replies with an error envelope, never running the handler), then runs
228
- * `options.handler(input, { signal })` and replies `{ id, ok: true, value }` on success or
229
- * `{ id, ok: false, error }` on throw. Each in-flight job has its own `AbortController`,
230
- * so an `abort` message for that id fires the handler's `signal` (cooperative the main
218
+ * `options.handler(input, { id: job, signal })` and replies `{ id, ok: true, value }` on success or
219
+ * `{ id, ok: false, error }` on throw. Input-guard throws use the same failure envelope. If a
220
+ * success value cannot be cloned, the post is retried as a clone-safe failure; if that post also
221
+ * fails, the parent port closes so the main side observes thread exit instead of waiting forever.
222
+ * The run envelope's `id` is fresh per dispatch and keys controllers, aborts, and replies;
223
+ * its `job` is the stable Queue idempotency key exposed as `execution.id` across retries
224
+ * and restore. That job id identifies work, not a caller, and is not authentication or
225
+ * authorization evidence. Each attempt has its own `AbortController`, so an `abort`
226
+ * message for the correlation id fires the handler's `signal` (cooperative — the main
231
227
  * side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
232
228
  * Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
233
229
  * (`parentPort === null`) it is a no-op.
@@ -239,7 +235,7 @@ export declare type Reply = {
239
235
  * @example
240
236
  * ```ts
241
237
  * // double.ts — a worker script
242
- * import { serveWorker } from '@src/server'
238
+ * import { serveWorker } from '@orkestrel/worker/server'
243
239
  *
244
240
  * serveWorker<number, number>({
245
241
  * input: (value): value is number => typeof value === 'number',
@@ -256,18 +252,19 @@ export declare function serveWorker<TInput, TResult>(options: ServeWorkerOptions
256
252
  * - `input` — narrows each inbound payload inside the thread; an invalid payload replies
257
253
  * with an error envelope rather than running the handler. Supplies the `TInput`
258
254
  * inference for the handler.
259
- * - `handler` — runs one job; receives the narrowed input and a `{ signal }` execution
260
- * whose `AbortSignal` fires when the main side aborts the job (cooperative). May be
261
- * async; its resolved value (which must be structured-cloneable) is the reply.
255
+ * - `handler` — runs one job; receives the narrowed input and the Queue's execution.
256
+ * `execution.id` is the stable Queue idempotency key across retries and crash restore;
257
+ * it identifies work, not a caller, and is not authentication or authorization evidence.
258
+ * `execution.signal` is per attempt and fires when the main side aborts that attempt
259
+ * (cooperative). The handler may be async; its resolved value (which must be
260
+ * structured-cloneable) is the reply.
262
261
  *
263
262
  * @typeParam TInput - The work payload (inferred from `input`)
264
263
  * @typeParam TResult - The value the handler resolves (the reply payload)
265
264
  */
266
265
  export declare interface ServeWorkerOptions<TInput, TResult> {
267
266
  readonly input: Guard<TInput>;
268
- readonly handler: (input: TInput, execution: {
269
- readonly signal: AbortSignal;
270
- }) => Promise<TResult> | TResult;
267
+ readonly handler: (input: TInput, execution: QueueExecution) => Promise<TResult> | TResult;
271
268
  }
272
269
 
273
270
  /**
@@ -281,11 +278,11 @@ export declare interface ServeWorkerOptions<TInput, TResult> {
281
278
  * listeners that flip `alive` to `false` AND latch the first terminal event on
282
279
  * {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via
283
280
  * its own listeners), to the pool's `validate` (via `alive`), and — crucially — to a
284
- * dispatch that attaches AFTER the death (via the latch). The latch closes a real race:
285
- * under event-loop pressure a dead thread's `online` + `error` + `exit` are delivered in
286
- * ONE synchronous exit-drain batch, so every death event fires before the microtask chain
287
- * resolving this spawn can hand the thread to `dispatch` without the latch that job
288
- * would await events that already fired, forever. The pool's `create` hook calls this.
281
+ * dispatch that attaches AFTER the death (via the latch). A `messageerror` is terminal too,
282
+ * so a thread whose inbound payload could not be deserialized is never reused. The latch
283
+ * closes a real race: a thread can become terminal before the readiness promise continuation
284
+ * hands it to `dispatch`, leaving no future death event for that dispatch to observe. Without
285
+ * the latch, that job would wait forever. The pool's `create` hook calls this.
289
286
  *
290
287
  * @param script - The worker module each thread runs (must call `serveWorker`)
291
288
  * @param workerData - Opaque, structured-cloneable data handed to the thread at spawn