@orkestrel/worker 0.0.1
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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/src/core/index.cjs +169 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +193 -0
- package/dist/src/core/index.d.ts +193 -0
- package/dist/src/core/index.js +167 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/server/index.cjs +360 -0
- package/dist/src/server/index.cjs.map +1 -0
- package/dist/src/server/index.d.cts +299 -0
- package/dist/src/server/index.d.ts +299 -0
- package/dist/src/server/index.js +354 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { ContractShape } from '@orkestrel/contract';
|
|
2
|
+
import { Infer } from '@orkestrel/contract';
|
|
3
|
+
import { QueueExecution } from '@orkestrel/queue';
|
|
4
|
+
import { QueueStoreInterface } from '@orkestrel/queue';
|
|
5
|
+
import { Worker } from 'node:worker_threads';
|
|
6
|
+
import { WorkerInterface } from '../core/index.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a persistent JSON-file {@link QueueStoreInterface} — the core
|
|
10
|
+
* `createDatabaseQueueStore` over a server {@link createJSONDriver}.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* A queue's durable state is just a database table, so JSON persistence reuses the
|
|
14
|
+
* existing JSON-file driver rather than a bespoke store: the entries are written to
|
|
15
|
+
* (and reloaded from) the file at `path`, surviving a process restart. There is no new
|
|
16
|
+
* class — the store engine ({@link createDatabaseQueueStore}) is shared, and only the
|
|
17
|
+
* driver changes where the bytes live. The `input` shape must be JSON-serializable
|
|
18
|
+
* (the JSON driver round-trips it as JSON). Build a second store over the SAME `path` to
|
|
19
|
+
* resume the outstanding entries a prior store persisted.
|
|
20
|
+
*
|
|
21
|
+
* @typeParam TInput - The contract shape of each entry's `input` payload
|
|
22
|
+
* @param path - The JSON file the entries are loaded from and flushed to
|
|
23
|
+
* @param input - The {@link ContractShape} for the work payload (the `input` column)
|
|
24
|
+
* @returns A JSON-file-backed {@link QueueStoreInterface}, typed by `input`
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { stringShape } from '@src/core'
|
|
29
|
+
* import { createJSONQueueStore } from '@src/server'
|
|
30
|
+
*
|
|
31
|
+
* const store = createJSONQueueStore('data/queue.json', stringShape())
|
|
32
|
+
* await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })
|
|
33
|
+
* // A later process resumes the outstanding work:
|
|
34
|
+
* const resumed = createJSONQueueStore('data/queue.json', stringShape())
|
|
35
|
+
* const outstanding = await resumed.load()
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function createJSONQueueStore<TInput extends ContractShape>(path: string, input: TInput): QueueStoreInterface<Infer<TInput>>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a CPU-parallel worker over `node:worker_threads` — a thin specialization of the
|
|
42
|
+
* core `createWorker` whose pooled resource is a worker THREAD.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Composition, not reimplementation: all concurrency, retries, per-attempt timeout,
|
|
46
|
+
* lifecycle, and durability are the core `Worker`'s (a `Queue` ⨉ `Pool`). This factory
|
|
47
|
+
* supplies only the thread pairing — the pool `create`s a thread (via `spawnThread`),
|
|
48
|
+
* `destroy`s it with `terminate()`, and `validate`s it by `alive && threadId > 0` (so an
|
|
49
|
+
* evicted / crashed thread is dropped and replaced) — and an internal handler that
|
|
50
|
+
* narrows the input through `options.input` (fail-fast before the structured-clone
|
|
51
|
+
* boundary) then `dispatch`es the job to the leased thread, narrowing the reply through
|
|
52
|
+
* `options.result`. Both generics INFER from the `input` / `result` guards, so call sites
|
|
53
|
+
* need no explicit type arguments. The boundary is crossed with ZERO `as`: the guards
|
|
54
|
+
* reconstruct `TInput` / `TResult` by validation (AGENTS §14). An `abort` / `timeout`
|
|
55
|
+
* TERMINATES the in-flight thread (CPU-bound work can't honour a signal) and evicts it; a
|
|
56
|
+
* subsequent job spawns a fresh thread. The worker script's module must call
|
|
57
|
+
* `serveWorker`. Returns the plain {@link WorkerInterface} — its methods are the Worker's.
|
|
58
|
+
*
|
|
59
|
+
* @typeParam TInput - The work payload each job carries (inferred from `input`)
|
|
60
|
+
* @typeParam TResult - The value a thread resolves for a job (inferred from `result`)
|
|
61
|
+
* @param options - The `script` plus the `input` / `result` guards and optional
|
|
62
|
+
* `workerData` / `concurrency` / `retries` / `timeout` / `store`
|
|
63
|
+
* (see {@link NodeWorkerOptions})
|
|
64
|
+
* @returns A working {@link WorkerInterface} backed by a thread pool
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* import { createNodeWorker } from '@src/server'
|
|
69
|
+
*
|
|
70
|
+
* const worker = createNodeWorker({
|
|
71
|
+
* script: new URL('./double.js', import.meta.url),
|
|
72
|
+
* input: (value): value is number => typeof value === 'number',
|
|
73
|
+
* result: (value): value is number => typeof value === 'number',
|
|
74
|
+
* concurrency: 4,
|
|
75
|
+
* })
|
|
76
|
+
*
|
|
77
|
+
* const doubled = await worker.enqueue(21) // 42, computed on a worker thread
|
|
78
|
+
* worker.destroy() // terminates every thread
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOptions<TInput, TResult>): WorkerInterface<TInput, TResult>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Dispatch one job to a leased {@link NodeThread} and await its narrowed reply.
|
|
85
|
+
*
|
|
86
|
+
* @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
|
|
89
|
+
* rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.
|
|
90
|
+
* 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.
|
|
100
|
+
*
|
|
101
|
+
* @typeParam TResult - The reply type the `result` guard narrows to
|
|
102
|
+
* @param thread - The leased thread to run the job on
|
|
103
|
+
* @param input - The work payload (structured-cloned to the thread)
|
|
104
|
+
* @param execution - The per-attempt handle; its `signal` aborts → terminate + evict
|
|
105
|
+
* @param result - The {@link Guard} narrowing the reply value with no assertion
|
|
106
|
+
* @returns A promise resolving the narrowed `TResult`, or rejecting on error / abort
|
|
107
|
+
*/
|
|
108
|
+
export declare function dispatch<TResult>(thread: NodeThread, input: unknown, execution: QueueExecution, result: Guard<TResult>): Promise<TResult>;
|
|
109
|
+
|
|
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
|
+
/**
|
|
128
|
+
* Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.
|
|
129
|
+
*
|
|
130
|
+
* @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).
|
|
136
|
+
*
|
|
137
|
+
* @param value - The inbound message to narrow
|
|
138
|
+
* @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
|
|
140
|
+
*/
|
|
141
|
+
export declare function isReply(value: unknown, id: string): value is Reply;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A live worker thread plus its latched liveness state — the pooled resource a
|
|
145
|
+
* {@link createNodeWorker} leases per job.
|
|
146
|
+
*
|
|
147
|
+
* @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
|
|
150
|
+
* 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
|
|
152
|
+
* record a `dispatch` checks at entry, so a job dispatched AFTER the thread died (its
|
|
153
|
+
* 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
|
|
158
|
+
* `node:worker_threads` thread (its `postMessage` / `terminate` drive the protocol).
|
|
159
|
+
*/
|
|
160
|
+
export declare interface NodeThread {
|
|
161
|
+
readonly worker: Worker;
|
|
162
|
+
alive: boolean;
|
|
163
|
+
death: Error | undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Options for `createNodeWorker` — a CPU-parallel worker over `node:worker_threads`.
|
|
168
|
+
*
|
|
169
|
+
* @remarks
|
|
170
|
+
* - `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`.
|
|
173
|
+
* - `input` — narrows the work payload BEFORE it crosses the structured-clone boundary
|
|
174
|
+
* (fail-fast) and supplies the `TInput` inference, so call sites need no type argument.
|
|
175
|
+
* - `result` — narrows every reply value coming back from a thread; an invalid reply
|
|
176
|
+
* rejects the job. This is the zero-`as` type bridge — `TResult` is inferred from it.
|
|
177
|
+
* - `workerData` — opaque data cloned to every thread once at spawn (read there via
|
|
178
|
+
* `serveWorker`'s host `workerData`); must be structured-cloneable.
|
|
179
|
+
* - `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`.
|
|
181
|
+
* - `retries` — the default extra attempts per job on failure / timeout; defaults to `0`.
|
|
182
|
+
* - `timeout` — the default per-attempt deadline in milliseconds; defaults to none.
|
|
183
|
+
* - `store` — durable backing for outstanding jobs (survives a restart; `restore()`
|
|
184
|
+
* re-runs them).
|
|
185
|
+
*
|
|
186
|
+
* @typeParam TInput - The work payload each job carries (inferred from `input`)
|
|
187
|
+
* @typeParam TResult - The value a thread resolves for a job (inferred from `result`)
|
|
188
|
+
*/
|
|
189
|
+
export declare interface NodeWorkerOptions<TInput, TResult> {
|
|
190
|
+
readonly script: string | URL;
|
|
191
|
+
readonly input: Guard<TInput>;
|
|
192
|
+
readonly result: Guard<TResult>;
|
|
193
|
+
readonly workerData?: unknown;
|
|
194
|
+
readonly concurrency?: number;
|
|
195
|
+
readonly retries?: number;
|
|
196
|
+
readonly timeout?: number;
|
|
197
|
+
readonly store?: QueueStoreInterface<TInput>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A thread→main reply envelope — a success carrying an opaque `value`, or a failure with a
|
|
202
|
+
* message — part of the internal wire protocol `createNodeWorker` posts and `serveWorker`
|
|
203
|
+
* answers.
|
|
204
|
+
*
|
|
205
|
+
* @remarks
|
|
206
|
+
* Internal plumbing rather than public call surface, but centralized here per AGENTS §5 (an
|
|
207
|
+
* 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},
|
|
209
|
+
* with no `as`); a `false` carries a string `error`. The worker-side `serve.ts` cannot import
|
|
210
|
+
* 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.
|
|
213
|
+
*/
|
|
214
|
+
export declare type Reply = {
|
|
215
|
+
readonly id: string;
|
|
216
|
+
readonly ok: true;
|
|
217
|
+
readonly value: unknown;
|
|
218
|
+
} | {
|
|
219
|
+
readonly id: string;
|
|
220
|
+
readonly ok: false;
|
|
221
|
+
readonly error: string;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Register a worker-thread handler — the worker-side half of {@link createNodeWorker}.
|
|
226
|
+
*
|
|
227
|
+
* @remarks
|
|
228
|
+
* Must be the spawned thread's module entry. It listens on the parent port for the
|
|
229
|
+
* run/abort protocol: a `run` message narrows its `input` through `options.input` (an
|
|
230
|
+
* invalid payload replies with an error envelope, never running the handler), then runs
|
|
231
|
+
* `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`,
|
|
233
|
+
* so an `abort` message for that id fires the handler's `signal` (cooperative — the main
|
|
234
|
+
* side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
|
|
235
|
+
* Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
|
|
236
|
+
* (`parentPort === null`) it is a no-op.
|
|
237
|
+
*
|
|
238
|
+
* @typeParam TInput - The work payload (inferred from `options.input`)
|
|
239
|
+
* @typeParam TResult - The value the handler resolves (the reply payload)
|
|
240
|
+
* @param options - The `input` guard and the `handler` (see {@link ServeWorkerOptions})
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* // double.ts — a worker script
|
|
245
|
+
* import { serveWorker } from '@src/server'
|
|
246
|
+
*
|
|
247
|
+
* serveWorker<number, number>({
|
|
248
|
+
* input: (value): value is number => typeof value === 'number',
|
|
249
|
+
* handler: (value) => value * 2,
|
|
250
|
+
* })
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
export declare function serveWorker<TInput, TResult>(options: ServeWorkerOptions<TInput, TResult>): void;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Options for `serveWorker` — the worker-side entry a thread script registers.
|
|
257
|
+
*
|
|
258
|
+
* @remarks
|
|
259
|
+
* - `input` — narrows each inbound payload inside the thread; an invalid payload replies
|
|
260
|
+
* with an error envelope rather than running the handler. Supplies the `TInput`
|
|
261
|
+
* inference for the handler.
|
|
262
|
+
* - `handler` — runs one job; receives the narrowed input and a `{ signal }` execution
|
|
263
|
+
* whose `AbortSignal` fires when the main side aborts the job (cooperative). May be
|
|
264
|
+
* async; its resolved value (which must be structured-cloneable) is the reply.
|
|
265
|
+
*
|
|
266
|
+
* @typeParam TInput - The work payload (inferred from `input`)
|
|
267
|
+
* @typeParam TResult - The value the handler resolves (the reply payload)
|
|
268
|
+
*/
|
|
269
|
+
export declare interface ServeWorkerOptions<TInput, TResult> {
|
|
270
|
+
readonly input: Guard<TInput>;
|
|
271
|
+
readonly handler: (input: TInput, execution: {
|
|
272
|
+
readonly signal: AbortSignal;
|
|
273
|
+
}) => Promise<TResult> | TResult;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Spawn one worker thread and resolve a live {@link NodeThread} once it comes online.
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
280
|
+
* Constructs the thread with the `script` module and the cloned `workerData`, then
|
|
281
|
+
* resolves on the thread's `online` event (rejecting on an early `error` OR an `exit`
|
|
282
|
+
* that arrives before `online`, so the spawn promise is total — it can never dangle on a
|
|
283
|
+
* thread that died without erroring). The wrapper attaches persistent `error` / `exit`
|
|
284
|
+
* listeners that flip `alive` to `false` AND latch the first terminal event on
|
|
285
|
+
* {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via
|
|
286
|
+
* 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.
|
|
292
|
+
*
|
|
293
|
+
* @param script - The worker module each thread runs (must call `serveWorker`)
|
|
294
|
+
* @param workerData - Opaque, structured-cloneable data handed to the thread at spawn
|
|
295
|
+
* @returns A promise resolving the online {@link NodeThread}
|
|
296
|
+
*/
|
|
297
|
+
export declare function spawnThread(script: string | URL, workerData: unknown): Promise<NodeThread>;
|
|
298
|
+
|
|
299
|
+
export { }
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { ContractShape } from '@orkestrel/contract';
|
|
2
|
+
import { Infer } from '@orkestrel/contract';
|
|
3
|
+
import { QueueExecution } from '@orkestrel/queue';
|
|
4
|
+
import { QueueStoreInterface } from '@orkestrel/queue';
|
|
5
|
+
import { Worker } from 'node:worker_threads';
|
|
6
|
+
import { WorkerInterface } from '../core/index.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a persistent JSON-file {@link QueueStoreInterface} — the core
|
|
10
|
+
* `createDatabaseQueueStore` over a server {@link createJSONDriver}.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* A queue's durable state is just a database table, so JSON persistence reuses the
|
|
14
|
+
* existing JSON-file driver rather than a bespoke store: the entries are written to
|
|
15
|
+
* (and reloaded from) the file at `path`, surviving a process restart. There is no new
|
|
16
|
+
* class — the store engine ({@link createDatabaseQueueStore}) is shared, and only the
|
|
17
|
+
* driver changes where the bytes live. The `input` shape must be JSON-serializable
|
|
18
|
+
* (the JSON driver round-trips it as JSON). Build a second store over the SAME `path` to
|
|
19
|
+
* resume the outstanding entries a prior store persisted.
|
|
20
|
+
*
|
|
21
|
+
* @typeParam TInput - The contract shape of each entry's `input` payload
|
|
22
|
+
* @param path - The JSON file the entries are loaded from and flushed to
|
|
23
|
+
* @param input - The {@link ContractShape} for the work payload (the `input` column)
|
|
24
|
+
* @returns A JSON-file-backed {@link QueueStoreInterface}, typed by `input`
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { stringShape } from '@src/core'
|
|
29
|
+
* import { createJSONQueueStore } from '@src/server'
|
|
30
|
+
*
|
|
31
|
+
* const store = createJSONQueueStore('data/queue.json', stringShape())
|
|
32
|
+
* await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })
|
|
33
|
+
* // A later process resumes the outstanding work:
|
|
34
|
+
* const resumed = createJSONQueueStore('data/queue.json', stringShape())
|
|
35
|
+
* const outstanding = await resumed.load()
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function createJSONQueueStore<TInput extends ContractShape>(path: string, input: TInput): QueueStoreInterface<Infer<TInput>>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a CPU-parallel worker over `node:worker_threads` — a thin specialization of the
|
|
42
|
+
* core `createWorker` whose pooled resource is a worker THREAD.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Composition, not reimplementation: all concurrency, retries, per-attempt timeout,
|
|
46
|
+
* lifecycle, and durability are the core `Worker`'s (a `Queue` ⨉ `Pool`). This factory
|
|
47
|
+
* supplies only the thread pairing — the pool `create`s a thread (via `spawnThread`),
|
|
48
|
+
* `destroy`s it with `terminate()`, and `validate`s it by `alive && threadId > 0` (so an
|
|
49
|
+
* evicted / crashed thread is dropped and replaced) — and an internal handler that
|
|
50
|
+
* narrows the input through `options.input` (fail-fast before the structured-clone
|
|
51
|
+
* boundary) then `dispatch`es the job to the leased thread, narrowing the reply through
|
|
52
|
+
* `options.result`. Both generics INFER from the `input` / `result` guards, so call sites
|
|
53
|
+
* need no explicit type arguments. The boundary is crossed with ZERO `as`: the guards
|
|
54
|
+
* reconstruct `TInput` / `TResult` by validation (AGENTS §14). An `abort` / `timeout`
|
|
55
|
+
* TERMINATES the in-flight thread (CPU-bound work can't honour a signal) and evicts it; a
|
|
56
|
+
* subsequent job spawns a fresh thread. The worker script's module must call
|
|
57
|
+
* `serveWorker`. Returns the plain {@link WorkerInterface} — its methods are the Worker's.
|
|
58
|
+
*
|
|
59
|
+
* @typeParam TInput - The work payload each job carries (inferred from `input`)
|
|
60
|
+
* @typeParam TResult - The value a thread resolves for a job (inferred from `result`)
|
|
61
|
+
* @param options - The `script` plus the `input` / `result` guards and optional
|
|
62
|
+
* `workerData` / `concurrency` / `retries` / `timeout` / `store`
|
|
63
|
+
* (see {@link NodeWorkerOptions})
|
|
64
|
+
* @returns A working {@link WorkerInterface} backed by a thread pool
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* import { createNodeWorker } from '@src/server'
|
|
69
|
+
*
|
|
70
|
+
* const worker = createNodeWorker({
|
|
71
|
+
* script: new URL('./double.js', import.meta.url),
|
|
72
|
+
* input: (value): value is number => typeof value === 'number',
|
|
73
|
+
* result: (value): value is number => typeof value === 'number',
|
|
74
|
+
* concurrency: 4,
|
|
75
|
+
* })
|
|
76
|
+
*
|
|
77
|
+
* const doubled = await worker.enqueue(21) // 42, computed on a worker thread
|
|
78
|
+
* worker.destroy() // terminates every thread
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function createNodeWorker<TInput, TResult>(options: NodeWorkerOptions<TInput, TResult>): WorkerInterface<TInput, TResult>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Dispatch one job to a leased {@link NodeThread} and await its narrowed reply.
|
|
85
|
+
*
|
|
86
|
+
* @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
|
|
89
|
+
* rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.
|
|
90
|
+
* 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.
|
|
100
|
+
*
|
|
101
|
+
* @typeParam TResult - The reply type the `result` guard narrows to
|
|
102
|
+
* @param thread - The leased thread to run the job on
|
|
103
|
+
* @param input - The work payload (structured-cloned to the thread)
|
|
104
|
+
* @param execution - The per-attempt handle; its `signal` aborts → terminate + evict
|
|
105
|
+
* @param result - The {@link Guard} narrowing the reply value with no assertion
|
|
106
|
+
* @returns A promise resolving the narrowed `TResult`, or rejecting on error / abort
|
|
107
|
+
*/
|
|
108
|
+
export declare function dispatch<TResult>(thread: NodeThread, input: unknown, execution: QueueExecution, result: Guard<TResult>): Promise<TResult>;
|
|
109
|
+
|
|
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
|
+
/**
|
|
128
|
+
* Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.
|
|
129
|
+
*
|
|
130
|
+
* @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).
|
|
136
|
+
*
|
|
137
|
+
* @param value - The inbound message to narrow
|
|
138
|
+
* @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
|
|
140
|
+
*/
|
|
141
|
+
export declare function isReply(value: unknown, id: string): value is Reply;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A live worker thread plus its latched liveness state — the pooled resource a
|
|
145
|
+
* {@link createNodeWorker} leases per job.
|
|
146
|
+
*
|
|
147
|
+
* @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
|
|
150
|
+
* 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
|
|
152
|
+
* record a `dispatch` checks at entry, so a job dispatched AFTER the thread died (its
|
|
153
|
+
* 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
|
|
158
|
+
* `node:worker_threads` thread (its `postMessage` / `terminate` drive the protocol).
|
|
159
|
+
*/
|
|
160
|
+
export declare interface NodeThread {
|
|
161
|
+
readonly worker: Worker;
|
|
162
|
+
alive: boolean;
|
|
163
|
+
death: Error | undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Options for `createNodeWorker` — a CPU-parallel worker over `node:worker_threads`.
|
|
168
|
+
*
|
|
169
|
+
* @remarks
|
|
170
|
+
* - `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`.
|
|
173
|
+
* - `input` — narrows the work payload BEFORE it crosses the structured-clone boundary
|
|
174
|
+
* (fail-fast) and supplies the `TInput` inference, so call sites need no type argument.
|
|
175
|
+
* - `result` — narrows every reply value coming back from a thread; an invalid reply
|
|
176
|
+
* rejects the job. This is the zero-`as` type bridge — `TResult` is inferred from it.
|
|
177
|
+
* - `workerData` — opaque data cloned to every thread once at spawn (read there via
|
|
178
|
+
* `serveWorker`'s host `workerData`); must be structured-cloneable.
|
|
179
|
+
* - `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`.
|
|
181
|
+
* - `retries` — the default extra attempts per job on failure / timeout; defaults to `0`.
|
|
182
|
+
* - `timeout` — the default per-attempt deadline in milliseconds; defaults to none.
|
|
183
|
+
* - `store` — durable backing for outstanding jobs (survives a restart; `restore()`
|
|
184
|
+
* re-runs them).
|
|
185
|
+
*
|
|
186
|
+
* @typeParam TInput - The work payload each job carries (inferred from `input`)
|
|
187
|
+
* @typeParam TResult - The value a thread resolves for a job (inferred from `result`)
|
|
188
|
+
*/
|
|
189
|
+
export declare interface NodeWorkerOptions<TInput, TResult> {
|
|
190
|
+
readonly script: string | URL;
|
|
191
|
+
readonly input: Guard<TInput>;
|
|
192
|
+
readonly result: Guard<TResult>;
|
|
193
|
+
readonly workerData?: unknown;
|
|
194
|
+
readonly concurrency?: number;
|
|
195
|
+
readonly retries?: number;
|
|
196
|
+
readonly timeout?: number;
|
|
197
|
+
readonly store?: QueueStoreInterface<TInput>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A thread→main reply envelope — a success carrying an opaque `value`, or a failure with a
|
|
202
|
+
* message — part of the internal wire protocol `createNodeWorker` posts and `serveWorker`
|
|
203
|
+
* answers.
|
|
204
|
+
*
|
|
205
|
+
* @remarks
|
|
206
|
+
* Internal plumbing rather than public call surface, but centralized here per AGENTS §5 (an
|
|
207
|
+
* 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},
|
|
209
|
+
* with no `as`); a `false` carries a string `error`. The worker-side `serve.ts` cannot import
|
|
210
|
+
* 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.
|
|
213
|
+
*/
|
|
214
|
+
export declare type Reply = {
|
|
215
|
+
readonly id: string;
|
|
216
|
+
readonly ok: true;
|
|
217
|
+
readonly value: unknown;
|
|
218
|
+
} | {
|
|
219
|
+
readonly id: string;
|
|
220
|
+
readonly ok: false;
|
|
221
|
+
readonly error: string;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Register a worker-thread handler — the worker-side half of {@link createNodeWorker}.
|
|
226
|
+
*
|
|
227
|
+
* @remarks
|
|
228
|
+
* Must be the spawned thread's module entry. It listens on the parent port for the
|
|
229
|
+
* run/abort protocol: a `run` message narrows its `input` through `options.input` (an
|
|
230
|
+
* invalid payload replies with an error envelope, never running the handler), then runs
|
|
231
|
+
* `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`,
|
|
233
|
+
* so an `abort` message for that id fires the handler's `signal` (cooperative — the main
|
|
234
|
+
* side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
|
|
235
|
+
* Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
|
|
236
|
+
* (`parentPort === null`) it is a no-op.
|
|
237
|
+
*
|
|
238
|
+
* @typeParam TInput - The work payload (inferred from `options.input`)
|
|
239
|
+
* @typeParam TResult - The value the handler resolves (the reply payload)
|
|
240
|
+
* @param options - The `input` guard and the `handler` (see {@link ServeWorkerOptions})
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* // double.ts — a worker script
|
|
245
|
+
* import { serveWorker } from '@src/server'
|
|
246
|
+
*
|
|
247
|
+
* serveWorker<number, number>({
|
|
248
|
+
* input: (value): value is number => typeof value === 'number',
|
|
249
|
+
* handler: (value) => value * 2,
|
|
250
|
+
* })
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
export declare function serveWorker<TInput, TResult>(options: ServeWorkerOptions<TInput, TResult>): void;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Options for `serveWorker` — the worker-side entry a thread script registers.
|
|
257
|
+
*
|
|
258
|
+
* @remarks
|
|
259
|
+
* - `input` — narrows each inbound payload inside the thread; an invalid payload replies
|
|
260
|
+
* with an error envelope rather than running the handler. Supplies the `TInput`
|
|
261
|
+
* inference for the handler.
|
|
262
|
+
* - `handler` — runs one job; receives the narrowed input and a `{ signal }` execution
|
|
263
|
+
* whose `AbortSignal` fires when the main side aborts the job (cooperative). May be
|
|
264
|
+
* async; its resolved value (which must be structured-cloneable) is the reply.
|
|
265
|
+
*
|
|
266
|
+
* @typeParam TInput - The work payload (inferred from `input`)
|
|
267
|
+
* @typeParam TResult - The value the handler resolves (the reply payload)
|
|
268
|
+
*/
|
|
269
|
+
export declare interface ServeWorkerOptions<TInput, TResult> {
|
|
270
|
+
readonly input: Guard<TInput>;
|
|
271
|
+
readonly handler: (input: TInput, execution: {
|
|
272
|
+
readonly signal: AbortSignal;
|
|
273
|
+
}) => Promise<TResult> | TResult;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Spawn one worker thread and resolve a live {@link NodeThread} once it comes online.
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
280
|
+
* Constructs the thread with the `script` module and the cloned `workerData`, then
|
|
281
|
+
* resolves on the thread's `online` event (rejecting on an early `error` OR an `exit`
|
|
282
|
+
* that arrives before `online`, so the spawn promise is total — it can never dangle on a
|
|
283
|
+
* thread that died without erroring). The wrapper attaches persistent `error` / `exit`
|
|
284
|
+
* listeners that flip `alive` to `false` AND latch the first terminal event on
|
|
285
|
+
* {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via
|
|
286
|
+
* 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.
|
|
292
|
+
*
|
|
293
|
+
* @param script - The worker module each thread runs (must call `serveWorker`)
|
|
294
|
+
* @param workerData - Opaque, structured-cloneable data handed to the thread at spawn
|
|
295
|
+
* @returns A promise resolving the online {@link NodeThread}
|
|
296
|
+
*/
|
|
297
|
+
export declare function spawnThread(script: string | URL, workerData: unknown): Promise<NodeThread>;
|
|
298
|
+
|
|
299
|
+
export { }
|