@orkestrel/worker 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/src/core/index.cjs +55 -24
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +38 -15
- package/dist/src/core/index.d.ts +38 -15
- package/dist/src/core/index.js +55 -24
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +170 -74
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +57 -60
- package/dist/src/server/index.d.ts +57 -60
- package/dist/src/server/index.js +170 -74
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -11
|
@@ -1,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 '@
|
|
29
|
-
* import { createJSONQueueStore } from '@
|
|
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 '@
|
|
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
|
|
88
|
-
*
|
|
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
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* job rejects.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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`
|
|
146
|
-
* is evicted on abort; the pool's `validate` reads
|
|
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`
|
|
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.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
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(...)`.
|
|
169
|
-
*
|
|
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
|
|
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`
|
|
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
|
|
209
|
-
*
|
|
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.
|
|
230
|
-
*
|
|
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 '@
|
|
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
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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).
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
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
|