@orkestrel/scaffold 0.0.67 → 0.0.68
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/dist/bin/main.js +67 -44
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/templates/brief.md +9 -0
- package/dist/host/claude/agents/orkestrel.md +4 -4
- package/dist/host/claude/rules/names.md +15 -0
- package/dist/host/claude/rules/tests.md +33 -4
- package/dist/host/claude/rules/workspace.md +14 -2
- package/dist/host/dotfiles/prettierignore +3 -0
- package/dist/host/guides/README.md +65 -0
- package/dist/host/guides/abort.md +169 -0
- package/dist/host/guides/agent.md +1509 -0
- package/dist/host/guides/brief.md +1266 -0
- package/dist/host/guides/browser.md +2200 -0
- package/dist/host/guides/budget.md +196 -0
- package/dist/host/guides/codec.md +519 -0
- package/dist/host/guides/console.md +785 -0
- package/dist/host/guides/contract.md +1193 -0
- package/dist/host/guides/csv.md +541 -0
- package/dist/host/guides/database.md +2518 -0
- package/dist/host/guides/emitter.md +233 -0
- package/dist/host/guides/form.md +1791 -0
- package/dist/host/guides/html.md +717 -0
- package/dist/host/guides/indexeddb.md +505 -0
- package/dist/host/guides/interpret.md +1029 -0
- package/dist/host/guides/lsp.md +515 -0
- package/dist/host/guides/markdown.md +964 -0
- package/dist/host/guides/mcp.md +5554 -0
- package/dist/host/guides/middleware.md +927 -0
- package/dist/host/guides/msg.md +440 -0
- package/dist/host/guides/ndjson.md +120 -0
- package/dist/host/guides/ollama.md +380 -0
- package/dist/host/guides/pool.md +280 -0
- package/dist/host/guides/probe.md +1210 -0
- package/dist/host/guides/process.md +1620 -0
- package/dist/host/guides/program.md +1110 -0
- package/dist/host/guides/qualifier.md +854 -0
- package/dist/host/guides/queue.md +370 -0
- package/dist/host/guides/rater.md +330 -0
- package/dist/host/guides/reason.md +1122 -0
- package/dist/host/guides/relation.md +373 -0
- package/dist/host/guides/router.md +753 -0
- package/dist/host/guides/scaffold.md +192 -31
- package/dist/host/guides/sea.md +383 -0
- package/dist/host/guides/server.md +752 -0
- package/dist/host/guides/sqlite.md +330 -0
- package/dist/host/guides/sse.md +187 -0
- package/dist/host/guides/supervisor.md +4890 -0
- package/dist/host/guides/table.md +1556 -0
- package/dist/host/guides/template.md +280 -0
- package/dist/host/guides/terminal.md +1145 -0
- package/dist/host/guides/test.md +2969 -0
- package/dist/host/guides/timeout.md +252 -0
- package/dist/host/guides/tool.md +311 -0
- package/dist/host/guides/toolbox.md +1038 -0
- package/dist/host/guides/websocket.md +282 -0
- package/dist/host/guides/worker.md +615 -0
- package/dist/host/guides/workflow.md +1507 -0
- package/dist/host/guides/workspace.md +595 -0
- package/dist/host/manifest.json +1218 -10
- package/dist/host/tests/policy.test.ts +279 -2
- package/dist/host/tests/setupPolicy.ts +437 -6
- package/dist/src/core/index.cjs +38 -16
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +33 -9
- package/dist/src/core/index.d.ts +33 -9
- package/dist/src/core/index.js +37 -17
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1750 -1567
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +106 -24
- package/dist/src/server/index.d.ts +106 -24
- package/dist/src/server/index.js +1751 -1570
- package/dist/src/server/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
# Worker
|
|
2
|
+
|
|
3
|
+
> A resource-backed job worker: a thin facade composing a `Queue` (`@orkestrel/queue`) with
|
|
4
|
+
> a `Pool` (`@orkestrel/pool`), where each job's handler runs against an automatically
|
|
5
|
+
> acquired pooled resource released when the job settles.
|
|
6
|
+
|
|
7
|
+
All concurrency, retries, per-attempt timeout, and lifecycle are the Queue's, and all
|
|
8
|
+
resource lifecycle (idle reuse, `max` backpressure, FIFO waiting) is the Pool's; the facade
|
|
9
|
+
adds only the resource pairing, and it reimplements neither primitive. At most one resource
|
|
10
|
+
exists per in-flight job by default, and idle resources are reused across jobs. Each job
|
|
11
|
+
acquires over the attempt's `context.signal`, so an abort or a timeout while waiting for a
|
|
12
|
+
resource rejects the acquire cleanly (no token to release). For what construction captures,
|
|
13
|
+
when each value is validated, and which validator a runtime `null` reaches, see
|
|
14
|
+
`## Contract`.
|
|
15
|
+
|
|
16
|
+
The worker is observable through its own `emitter` — see [Observing](#observing). For CPU
|
|
17
|
+
parallelism, `createNodeWorker` (`@orkestrel/worker/server`) specializes
|
|
18
|
+
`createWorker` over a pool of `node:worker_threads`, with `serveWorker` as the worker-side
|
|
19
|
+
entry; the structured-clone boundary is narrowed by `input` / `result` guards with no
|
|
20
|
+
`as`. Source: [`src/core`](../src/core) (the `Worker` facade) and [`src/server`](../src/server)
|
|
21
|
+
(the thread pool and the worker-side entry). Surfaced through the `@orkestrel/worker` and
|
|
22
|
+
`@orkestrel/worker/server` exports.
|
|
23
|
+
|
|
24
|
+
## Surface
|
|
25
|
+
|
|
26
|
+
Create a worker over a resource lifecycle and a handler, then `enqueue` inputs and await
|
|
27
|
+
their results:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createWorker } from '@orkestrel/worker'
|
|
31
|
+
|
|
32
|
+
const worker = createWorker<Query, Connection, Rows>({
|
|
33
|
+
pool: { create: () => connect(), destroy: (connection) => connection.close() },
|
|
34
|
+
handler: (query, connection, { signal }) => connection.run(query, signal),
|
|
35
|
+
concurrency: 4, // up to four jobs in flight; the pool defaults its `max` to match
|
|
36
|
+
retries: 1,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const rows = await worker.enqueue(query)
|
|
40
|
+
await worker.destroy() // awaits queue cleanup, then pool cleanup, then emitter teardown
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Factories
|
|
44
|
+
|
|
45
|
+
The package publishes these factories. `createWorker` is the `@orkestrel/worker` export;
|
|
46
|
+
`createJSONQueueStore`, `createNodeWorker`, and `serveWorker` are the
|
|
47
|
+
`@orkestrel/worker/server` exports.
|
|
48
|
+
|
|
49
|
+
| API | Kind | Summary |
|
|
50
|
+
| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `createWorker` | function | Creates a resource-backed job worker — a `Queue` (`@orkestrel/queue`) composed with a `Pool` (`@orkestrel/pool`), where each enqueued input runs through the handler against an automatically acquired pooled resource released when the job settles. |
|
|
52
|
+
| `createJSONQueueStore` | function | Creates a persistent JSON-file `QueueStoreInterface` — the core `createDatabaseQueueStore` over a server `createJSONDriver`. |
|
|
53
|
+
| `createNodeWorker` | function | Creates a CPU-parallel worker over `node:worker_threads` — a thin specialization of the core `createWorker` whose pooled resource is a worker thread. |
|
|
54
|
+
| `serveWorker` | function | Registers a worker-thread handler — the worker-side half of `createNodeWorker`. |
|
|
55
|
+
|
|
56
|
+
### Threads
|
|
57
|
+
|
|
58
|
+
The lower-level `node:worker_threads` machinery `createNodeWorker` composes over
|
|
59
|
+
(`@orkestrel/worker/server`) — the thread-pool lifecycle hooks behind the public factory. Use
|
|
60
|
+
these to drive one thread yourself; `createNodeWorker` is the entry point for pooled, queued
|
|
61
|
+
work. Driving a thread by hand:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createThread, Dispatch, isReply } from '@orkestrel/worker/server'
|
|
65
|
+
|
|
66
|
+
const isNumber = (value: unknown): value is number => typeof value === 'number'
|
|
67
|
+
|
|
68
|
+
const thread = await createThread(new URL('./double.ts', import.meta.url))
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
try {
|
|
71
|
+
const job = new Dispatch(thread, 21, { id: 'job-1', signal: controller.signal }, isNumber)
|
|
72
|
+
console.log(await job.promise) // 42
|
|
73
|
+
console.log(isReply({ id: 'reply-1', ok: true, value: 42 }, 'reply-1')) // true
|
|
74
|
+
console.log(isReply({ id: 'reply-1', ok: true, value: 42 }, 'other')) // false
|
|
75
|
+
} finally {
|
|
76
|
+
await thread.worker.terminate()
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The thread-level functions behind `createNodeWorker`:
|
|
81
|
+
|
|
82
|
+
| API | Kind | Summary |
|
|
83
|
+
| -------------- | -------- | --------------------------------------------------------------------------------------- |
|
|
84
|
+
| `createThread` | function | Creates one live worker thread and resolves it as a `NodeThread` after it comes online. |
|
|
85
|
+
| `isReply` | function | Narrows an inbound `message` to a `Reply` for a given correlation `id` — no assertion. |
|
|
86
|
+
|
|
87
|
+
### Classes
|
|
88
|
+
|
|
89
|
+
The classes the core and server faces export:
|
|
90
|
+
|
|
91
|
+
| API | Kind | Summary |
|
|
92
|
+
| ---------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `Dispatch` | class | Represents one dispatched worker-thread job — the lifecycle entity behind a job posted to a leased `NodeThread`, whose `promise` settles with the narrowed reply. |
|
|
94
|
+
| `Worker` | class | Represents a resource-backed job worker — a thin facade composing a `Queue` (`@orkestrel/queue`) with a `Pool` (`@orkestrel/pool`). |
|
|
95
|
+
|
|
96
|
+
### Types
|
|
97
|
+
|
|
98
|
+
A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
|
|
99
|
+
|
|
100
|
+
Each type and interface the core and server faces publish:
|
|
101
|
+
|
|
102
|
+
| Type | Kind | Shape | Summary |
|
|
103
|
+
| -------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| `WorkerHandler` | type | `(input: TInput, resource: TResource, context: QueueContext) => Promise<TResult> \| TResult` | Runs one worker job with a leased pool resource. |
|
|
105
|
+
| `WorkerOptions` | interface | `{ on?, error?, handler, pool, concurrency?, retries?, timeout?, store? }` | Configures `createWorker`. |
|
|
106
|
+
| `WorkerInterface` | interface | `{ emitter, count, active, paused, stopped } plus enqueue, restore, start, stop, pause, resume, abort, clear, destroy` | Represents the job-worker contract a consumer holds — a `Queue` whose handler runs each job against a pooled resource. |
|
|
107
|
+
| `WorkerEventMap` | type | `{ enqueue, start, retry, success, failure, abort, drain }` | Represents the push observation surface of a `WorkerInterface` — the job lifecycle a fire-and-forget observer subscribes to. |
|
|
108
|
+
| `NodeWorkerOptions` | interface | `{ on?, error?, script, input, result, workerData?, concurrency?, retries?, timeout?, store? }` | Configures `createNodeWorker` — a CPU-parallel worker over `node:worker_threads`. |
|
|
109
|
+
| `ServeWorkerOptions` | interface | `{ input, handler }` | Configures `serveWorker` — the worker-side entry a thread script registers. |
|
|
110
|
+
| `NodeThread` | interface | `{ worker, alive, death }` | Represents a live worker thread plus its latched liveness state — the pooled resource a `createNodeWorker` leases per job. |
|
|
111
|
+
| `Reply` | type | `{ id, ok, value } \| { id, ok, error }` | Represents a thread→main reply envelope — a success carrying an opaque `value`, or a failure with a message — the reply half of the wire protocol `createNodeWorker` posts and `serveWorker` answers. |
|
|
112
|
+
|
|
113
|
+
The `emitter` / `count` / `active` / `paused` / `stopped` members of `WorkerInterface` are
|
|
114
|
+
`readonly` data members (Surface rows, earlier) — `emitter` is the typed push observation
|
|
115
|
+
surface (see [Observing](#observing)); the call-signature methods are documented under
|
|
116
|
+
[Methods](#methods). `Queue` / `Pool` themselves — and their own options, event maps, and
|
|
117
|
+
stores — are documented in their own packages: [queue.md](queue.md) / [pool.md](pool.md).
|
|
118
|
+
|
|
119
|
+
## Methods
|
|
120
|
+
|
|
121
|
+
The public methods of `WorkerInterface` — every call-signature member listed (its
|
|
122
|
+
`readonly` data members stay Surface rows). `Worker` implements `WorkerInterface`
|
|
123
|
+
exactly, so this doubles as the class's instance-method surface.
|
|
124
|
+
|
|
125
|
+
#### `WorkerInterface`
|
|
126
|
+
|
|
127
|
+
| Method | Returns | Summary |
|
|
128
|
+
| --------- | ------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
|
129
|
+
| `enqueue` | `Promise<TResult>` | Submits one job in FIFO order; the handler runs against an acquired resource, released when the job settles. |
|
|
130
|
+
| `restore` | `Promise<void>` | Re-enqueues the store's outstanding entries through the underlying queue; no-op without a store. |
|
|
131
|
+
| `start` | `void` | Starts or restarts the underlying queue's worker loops. |
|
|
132
|
+
| `stop` | `Promise<void>` | Stops the queue, rejects pending work, and awaits current-loop and durable cleanup quiescence. |
|
|
133
|
+
| `pause` | `void` | Suspends dequeuing through the underlying queue, leaving in-flight jobs untouched. |
|
|
134
|
+
| `resume` | `void` | Continues a paused worker through the underlying queue. |
|
|
135
|
+
| `abort` | `Promise<void>` | Cancels in-flight work, rejects pending work, and awaits queue-owned cleanup; an aborted attempt is never retried. |
|
|
136
|
+
| `clear` | `Promise<void>` | Drops pending jobs and awaits their durable cleanup, leaving in-flight jobs untouched. |
|
|
137
|
+
| `destroy` | `Promise<void>` | Tears down the queue, then the pool, and finally the worker emitter, behind one stable barrier. |
|
|
138
|
+
|
|
139
|
+
`stop`, `abort`, and `clear` return the exact cleanup promises supplied by the underlying
|
|
140
|
+
queue. `destroy` is its own stable barrier: every call, including a call reentered
|
|
141
|
+
synchronously from the queue's `abort` event, returns the same promise. Queue cleanup always
|
|
142
|
+
settles before pool cleanup begins, and the worker emitter is destroyed only after both have
|
|
143
|
+
settled. No cleanup failure prevents the next owned layer from being attempted. One failure
|
|
144
|
+
rejects the barrier with that exact value; failures from both queue and pool reject with a
|
|
145
|
+
native `AggregateError` whose errors are ordered queue first, pool second.
|
|
146
|
+
|
|
147
|
+
## Contract
|
|
148
|
+
|
|
149
|
+
These invariants hold across `src/core` ↔ `worker.md`:
|
|
150
|
+
|
|
151
|
+
1. **Doc ↔ source bijection.** Every `function` / `class` / `interface` / `type` row in
|
|
152
|
+
the `## Surface` tables is a real export of the worker module, and every export
|
|
153
|
+
appears as a Surface row — exhaustive, both directions.
|
|
154
|
+
2. **Composition, not reimplementation.** A `Worker` does not reimplement concurrency /
|
|
155
|
+
retry / lifecycle — it composes a `Queue` (`@orkestrel/queue`) with a `Pool`
|
|
156
|
+
(`@orkestrel/pool`). Its queue handler `acquire`s a resource over the attempt's
|
|
157
|
+
`context.signal`, runs the caller's handler against it, and `release`s it in a
|
|
158
|
+
`finally` (so a throwing or aborted handler still frees the resource). Because the
|
|
159
|
+
`finally` runs only when the handler actually settles, a handler that ignores its
|
|
160
|
+
`context.signal` keeps its leased resource until it returns — so on a timeout /
|
|
161
|
+
abort the resource can outlive the freed queue slot; a cooperative handler that
|
|
162
|
+
honours the signal releases promptly. Construction snapshots every caller-owned
|
|
163
|
+
top-level option once. Only `undefined` defaults `concurrency` or pool `max`; runtime
|
|
164
|
+
`null` reaches Queue or Pool validation. Queue's integer timeout contract is preserved:
|
|
165
|
+
`timeout` must be in `0..2_147_483_647` milliseconds, and `0` disables the deadline.
|
|
166
|
+
Queue is constructed successfully before the
|
|
167
|
+
pool option is read, and every declared pool member (`max`, `on`, `error`, `create`,
|
|
168
|
+
`destroy`, `validate`) is then captured once by direct access, preserving inherited and
|
|
169
|
+
non-enumerable structural options. Pool `max` still defaults to concurrency, so resources
|
|
170
|
+
match the jobs in flight by default. `stop` / `abort` / `clear` return the
|
|
171
|
+
queue's exact cleanup barriers. `destroy` installs one stable barrier before it invokes
|
|
172
|
+
queue teardown (including synchronous abort-event reentry), awaits queue then pool
|
|
173
|
+
settlement serially, aggregates the queue's failure and the pool's in that order, and
|
|
174
|
+
destroys the worker emitter last.
|
|
175
|
+
3. **Observable — the queue lifecycle re-exposed.** The `Worker` owns a typed `Emitter`
|
|
176
|
+
exposed as `readonly emitter` carrying `WorkerEventMap<TResult>`
|
|
177
|
+
(`enqueue` / `start` / `retry` / `success` / `failure` / `abort` / `drain`), bridged
|
|
178
|
+
from the underlying queue's own emitter at construction — each bridge listener
|
|
179
|
+
re-emits directly on the worker's emitter and never throws, so the inner queue's own
|
|
180
|
+
emit stays balanced regardless of what a worker observer does. **Emitting is
|
|
181
|
+
observation-only** — the emitter isolates a listener throw (routing it to the `error`
|
|
182
|
+
handler, never a domain event) and every event sits strictly after the relevant
|
|
183
|
+
queue transition, so a buggy observer can never corrupt the queue or pool. The pool's
|
|
184
|
+
own `create` / `acquire` / `release` / `destroy` events stay the pool's internal
|
|
185
|
+
concern — observe a `Pool` directly for those.
|
|
186
|
+
4. **Doc ↔ source method bijection.** The `## Methods` table lists exactly the public
|
|
187
|
+
methods of `WorkerInterface` — exhaustive, both directions — and `Worker` exposes
|
|
188
|
+
the same public methods as its interface, no more.
|
|
189
|
+
5. **`createNodeWorker` is a thread specialization of `createWorker` (`@orkestrel/worker/server`).**
|
|
190
|
+
It does not reimplement concurrency / retry / timeout / lifecycle — it calls
|
|
191
|
+
`createWorker` with a `Pool` whose resource is a `node:worker_threads` thread
|
|
192
|
+
(`create` = the spawn `createThread` publishes, `destroy` = `terminate()`,
|
|
193
|
+
`validate` = `alive && threadId > 0`) and an internal handler that narrows the input
|
|
194
|
+
through `options.input` then runs a `Dispatch` against the leased thread. `TInput` and
|
|
195
|
+
`TResult` infer from the `input` and `result` guards, so a call site needs no type
|
|
196
|
+
argument. The structured-clone boundary is crossed with no `as`: a `Dispatch` narrows each reply
|
|
197
|
+
value through `options.result` (a value that fails it rejects with `'reply did not
|
|
198
|
+
satisfy result guard'`), and the worker side narrows each payload through
|
|
199
|
+
`options.input` (a bad input replies `'input did not satisfy input guard'`) —
|
|
200
|
+
`TInput` / `TResult` are reconstructed by validation, never asserted. A throwing
|
|
201
|
+
caller guard is contained and rejects only that job; the pooled worker remains usable.
|
|
202
|
+
The run/abort/reply protocol is published as `Reply` and `isReply`: main → thread posts
|
|
203
|
+
`{ id, job, command: 'run', input }` / `{ id, command: 'abort' }`; thread → main posts
|
|
204
|
+
`{ id, ok: true, value }` / `{ id, ok: false, error }`. The run `id` is a fresh
|
|
205
|
+
per-dispatch correlation key used exclusively for replies, aborts, controllers, and
|
|
206
|
+
listeners; `job` is the Queue entry's stable `QueueContext.id`, preserved across retry
|
|
207
|
+
attempts and crash restore and exposed to the thread handler as `context.id`. A run
|
|
208
|
+
without a string `job` fails closed: the handler is not invoked and no reply is sent.
|
|
209
|
+
6. **Abort terminates and evicts the thread without losing its cause.** Because CPU-bound
|
|
210
|
+
work cannot honour an `AbortSignal`, an `abort` / `timeout` posts the cooperative
|
|
211
|
+
`abort`, flips `alive = false` for a thread this package produced, and observes
|
|
212
|
+
`terminate()` settlement. Successful
|
|
213
|
+
termination rejects with the exact `context.signal.reason`. If the cooperative post
|
|
214
|
+
fails, that reason remains first in `AggregateError.errors`, followed by the notification
|
|
215
|
+
failure, with message `worker abort notification failed`. A termination failure rejects
|
|
216
|
+
with `AggregateError.errors` ordered as reason, optional notification failure, then
|
|
217
|
+
termination failure, with message `worker termination failed`. Neither failure escapes
|
|
218
|
+
its event callback. The freed pool slot spawns a fresh thread on the next job.
|
|
219
|
+
7. **A thread death settles its job under every event ordering — the `death` latch.**
|
|
220
|
+
Every spawned thread carries persistent `error` / `messageerror` / `exit` listeners that
|
|
221
|
+
flip `alive = false`
|
|
222
|
+
and latch the first terminal event on `NodeThread.death`; a `Dispatch` checks that latch
|
|
223
|
+
synchronously at construction, so a job dispatched onto a thread that already died rejects
|
|
224
|
+
immediately. A thread can become terminal before the readiness promise continuation
|
|
225
|
+
attaches dispatch listeners, and those events will never fire again; without the latch
|
|
226
|
+
the job would wait forever. A death mid-flight still rejects through the dispatch's own
|
|
227
|
+
`error` / `exit` listeners; exit rejects with that exact already-latched `death` object,
|
|
228
|
+
whose message retains the exit code (for example, `worker thread exited (code 1)`). An
|
|
229
|
+
inbound `messageerror` also evicts and terminates the thread, rejects the
|
|
230
|
+
dispatch, and is detached with the other per-job listeners. A death before `online`
|
|
231
|
+
rejects the spawn itself.
|
|
232
|
+
8. **`serveWorker` captures registration options once.** After the main-thread
|
|
233
|
+
`parentPort === null` no-op, registration reads `options.input` and then
|
|
234
|
+
`options.handler` exactly once. Getter failure is therefore fail-fast during registration,
|
|
235
|
+
the handler getter is not read after an input-getter failure, and every later job retains
|
|
236
|
+
the initially captured guard and handler identities. The main-thread no-op reads neither.
|
|
237
|
+
9. **Structured-clone constraints.** `TInput` / `TResult` / `workerData` must be
|
|
238
|
+
structured-cloneable (no functions / Promises / `AbortSignal`). A non-cloneable result
|
|
239
|
+
cannot strand a dispatch: `serveWorker` replaces the failed success post with a clone-safe
|
|
240
|
+
failure envelope, and closes the parent port if even that fallback cannot be posted so the
|
|
241
|
+
main side observes exit. A matching-id malformed reply similarly rejects the dispatch,
|
|
242
|
+
terminates and evicts the tainted thread, and leaves id-less / foreign-id chatter ignored.
|
|
243
|
+
Raw TypeScript is unflagged on Node 22.18+ and Node 23.6+; Node 22.12–22.17 and Node
|
|
244
|
+
23.0–23.5 require `--experimental-strip-types`. A built `.js` / `.mjs` script is an
|
|
245
|
+
alternative across supported Node versions. The worker script's
|
|
246
|
+
module must call `serveWorker` — the worker-side entry
|
|
247
|
+
that runs the handler and answers the protocol; it is self-contained and imports only
|
|
248
|
+
`node:worker_threads` at runtime,
|
|
249
|
+
so it loads as a raw module in a spawned thread. `createNodeWorker` returns the plain
|
|
250
|
+
`WorkerInterface`, so it inherits the Worker's `emitter` unchanged (clause 3).
|
|
251
|
+
10. **`createJSONQueueStore` is `@orkestrel/queue`'s `createDatabaseQueueStore` over a
|
|
252
|
+
server JSON driver.** A queue's durable state is a `@orkestrel/database` table,
|
|
253
|
+
so JSON persistence reuses the existing driver rather than a bespoke store — see
|
|
254
|
+
[queue.md](queue.md) for the `QueueStoreInterface` contract itself.
|
|
255
|
+
|
|
256
|
+
## NodeWorker
|
|
257
|
+
|
|
258
|
+
`createNodeWorker` (`@orkestrel/worker/server`) runs jobs on a pool of `node:worker_threads` threads —
|
|
259
|
+
true CPU parallelism for work that would otherwise block the event loop. It is a thin
|
|
260
|
+
specialization of [`createWorker`](#surface): the pooled resource is a worker thread, and
|
|
261
|
+
it returns the plain `WorkerInterface`, so its methods, lifecycle, concurrency, retries,
|
|
262
|
+
timeout, and durability are exactly the Worker's (see [Methods](#methods)). The only
|
|
263
|
+
additions are the thread pairing and the zero-`as` wire bridge.
|
|
264
|
+
|
|
265
|
+
The `input` and `result` guards define the boundary and the inference: `input` narrows each
|
|
266
|
+
payload (and fail-fasts a bad one before it crosses to a thread), and `result` narrows each
|
|
267
|
+
reply value. `TInput` and `TResult` infer from these, so call sites pass no type arguments:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { createNodeWorker } from '@orkestrel/worker/server'
|
|
271
|
+
|
|
272
|
+
const isNumber = (value: unknown): value is number => typeof value === 'number'
|
|
273
|
+
|
|
274
|
+
const worker = createNodeWorker({
|
|
275
|
+
script: new URL('./double.js', import.meta.url), // built JavaScript works across supported Node
|
|
276
|
+
input: isNumber, // narrows + infers TInput; fail-fasts a bad input before posting
|
|
277
|
+
result: isNumber, // narrows each reply — the zero-`as` type bridge; infers TResult
|
|
278
|
+
concurrency: 4, // up to four threads run jobs in parallel
|
|
279
|
+
retries: 1,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const doubled = await worker.enqueue(21) // 42, computed on a worker thread
|
|
283
|
+
await worker.destroy() // awaits termination of every thread
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The worker script registers its handler with `serveWorker`, which must be the thread
|
|
287
|
+
module's entry. On a worker thread, registration captures `input` then `handler` exactly
|
|
288
|
+
once and retains those identities for every job; on the main thread it reads neither.
|
|
289
|
+
It receives the narrowed input and the Queue's `{ id, signal }` context. `id` is the stable
|
|
290
|
+
Queue idempotency key across retries and crash restore; it identifies the work, not its caller,
|
|
291
|
+
and is not authentication or authorization evidence. `signal` is per attempt and fires on
|
|
292
|
+
a cooperative abort. The handler's resolved value is the reply:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// double.ts — the worker script
|
|
296
|
+
import { serveWorker } from '@orkestrel/worker/server'
|
|
297
|
+
|
|
298
|
+
serveWorker<number, number>({
|
|
299
|
+
input: (value): value is number => typeof value === 'number',
|
|
300
|
+
handler: (value, { id, signal }) => {
|
|
301
|
+
console.log(`running ${id}`)
|
|
302
|
+
if (signal.aborted) throw signal.reason
|
|
303
|
+
return value * 2
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Per-job consumer context is explicit, structured-cloneable `TInput`. Ambient main-thread
|
|
309
|
+
state, including `AsyncLocalStorage`, does not cross a worker-thread structured-clone boundary;
|
|
310
|
+
there is no implicit caller transport.
|
|
311
|
+
|
|
312
|
+
Because CPU-bound work cannot honour its signal, an `abort` or a per-attempt `timeout`
|
|
313
|
+
**terminates** the in-flight thread and evicts it from the pool — the next job spawns a
|
|
314
|
+
fresh one. Successful termination preserves the exact signal reason; notification and
|
|
315
|
+
termination failures aggregate after that primary cause in protocol order. A thread that
|
|
316
|
+
dies (a crash, a script that fails to load or even to resolve) always settles its job with
|
|
317
|
+
the exact latched `NodeThread.death`, including its exit-code message, so even a job
|
|
318
|
+
dispatched after the death — its events already fired — rejects immediately rather than
|
|
319
|
+
waiting on a reply that never comes. `workerData`, the input, and the result must all be
|
|
320
|
+
structured-cloneable (no functions, Promises, or `AbortSignal`s cross the boundary). The
|
|
321
|
+
`workerData` key mirrors the `node:worker_threads` `Worker` constructor option of the same
|
|
322
|
+
name, and the thread reads it back from `node:worker_threads`.
|
|
323
|
+
|
|
324
|
+
A thread worker takes the same `on` and `error` hooks as the core worker: pass `on` to wire
|
|
325
|
+
initial `WorkerEventMap` listeners at construction, and `error` to receive a listener's throw
|
|
326
|
+
(see [Observing](#observing)).
|
|
327
|
+
|
|
328
|
+
## Persistence
|
|
329
|
+
|
|
330
|
+
`createJSONQueueStore` (`@orkestrel/worker/server`) builds a `QueueStoreInterface` over a JSON file —
|
|
331
|
+
`@orkestrel/queue`'s `createDatabaseQueueStore` composed with `@orkestrel/database`'s
|
|
332
|
+
`createJSONDriver` — so a fresh store over the same path resumes a prior process's
|
|
333
|
+
outstanding work:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { stringShape } from '@orkestrel/contract'
|
|
337
|
+
import { createJSONQueueStore } from '@orkestrel/worker/server'
|
|
338
|
+
|
|
339
|
+
const store = createJSONQueueStore('data/worker.json', stringShape())
|
|
340
|
+
await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })
|
|
341
|
+
|
|
342
|
+
// A later process — the entries persisted to the file are still outstanding:
|
|
343
|
+
const resumed = createJSONQueueStore('data/worker.json', stringShape())
|
|
344
|
+
const work = await resumed.load() // [{ id: 'job-1', input: 'https://example.com', attempts: 0 }]
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Pass the resulting `store` to `createWorker`'s `store` option to wire it into a worker's
|
|
348
|
+
underlying queue — see [queue.md](queue.md) for the `QueueStoreInterface` contract, the
|
|
349
|
+
`save` / `remove` / `load` / `clear` semantics, and what a store must persist across restarts.
|
|
350
|
+
|
|
351
|
+
## Observing
|
|
352
|
+
|
|
353
|
+
The `Worker` exposes a typed `emitter` carrying the job lifecycle it
|
|
354
|
+
re-exposes from its underlying queue — logging, metrics, tracing. Subscribe through
|
|
355
|
+
`worker.emitter.on(...)`, or wire initial listeners through the reserved `on?` option;
|
|
356
|
+
supply an `error?` handler to receive a listener's throw.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { createWorker } from '@orkestrel/worker'
|
|
360
|
+
|
|
361
|
+
const worker = createWorker({
|
|
362
|
+
pool: { create: () => connect() },
|
|
363
|
+
handler: (job, connection) => run(job, connection),
|
|
364
|
+
on: { drain: () => console.log('worker idle') }, // initial listener at construction
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
worker.emitter.on('success', (id, result) => metrics.record(id, result))
|
|
368
|
+
worker.emitter.on('failure', (id, error) => log.warn(`job ${id} failed`, error))
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The events the worker's emitter carries:
|
|
372
|
+
|
|
373
|
+
| Event map | Events |
|
|
374
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
375
|
+
| `WorkerEventMap<TResult>` | `enqueue(id)` · `start(id)` · `retry(id, attempt)` · `success(id, result)` · `failure(id, error)` · `abort(reason)` · `drain()` |
|
|
376
|
+
|
|
377
|
+
The worker forwards queue event payloads unchanged. In particular, `abort(reason)` carries
|
|
378
|
+
the queue's coded `QueueError` with code `aborted`; its `cause` retains the caller-supplied
|
|
379
|
+
abort reason.
|
|
380
|
+
|
|
381
|
+
See [queue.md](queue.md) / [pool.md](pool.md) for the underlying
|
|
382
|
+
`Queue` / `Pool` event vocabulary and their listener isolation, which holds here too: one
|
|
383
|
+
listener's throw never prevents a sibling listener, and the throw reaches the emitter's
|
|
384
|
+
`error` handler, so a buggy worker observer leaves the inner queue and pool intact.
|
|
385
|
+
|
|
386
|
+
## Patterns
|
|
387
|
+
|
|
388
|
+
### A resource-backed worker
|
|
389
|
+
|
|
390
|
+
Pair a resource lifecycle with a handler and let the queue drive both:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { createWorker } from '@orkestrel/worker'
|
|
394
|
+
|
|
395
|
+
// A Queue whose handler runs each job against a pooled resource (acquired before the
|
|
396
|
+
// handler, released after it — even on throw). The pool's `max` defaults to `concurrency`.
|
|
397
|
+
const worker = createWorker<Query, Connection, Rows>({
|
|
398
|
+
pool: { create: () => connect(), destroy: (connection) => connection.close() },
|
|
399
|
+
handler: (query, connection, { signal }) => connection.run(query, signal),
|
|
400
|
+
concurrency: 4,
|
|
401
|
+
retries: 1,
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const rows = await worker.enqueue(query)
|
|
405
|
+
await worker.destroy() // awaits queue cleanup, pool cleanup, then emitter teardown
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### CPU-parallel jobs over threads
|
|
409
|
+
|
|
410
|
+
Run the same job shape across a pool of worker threads:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
import { createNodeWorker } from '@orkestrel/worker/server'
|
|
414
|
+
|
|
415
|
+
const isNumber = (value: unknown): value is number => typeof value === 'number'
|
|
416
|
+
|
|
417
|
+
const worker = createNodeWorker({
|
|
418
|
+
script: new URL('./double.ts', import.meta.url),
|
|
419
|
+
input: isNumber,
|
|
420
|
+
result: isNumber,
|
|
421
|
+
concurrency: 4,
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const doubled = await worker.enqueue(21) // 42
|
|
425
|
+
await worker.destroy()
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Durable jobs across restarts
|
|
429
|
+
|
|
430
|
+
Back the queue with a store so outstanding work survives a restart:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
import { stringShape } from '@orkestrel/contract'
|
|
434
|
+
import { createWorker } from '@orkestrel/worker'
|
|
435
|
+
import { createJSONQueueStore } from '@orkestrel/worker/server'
|
|
436
|
+
|
|
437
|
+
const store = createJSONQueueStore('data/worker.json', stringShape())
|
|
438
|
+
const worker = createWorker({ store, pool: { create: () => connect() }, handler: run })
|
|
439
|
+
|
|
440
|
+
await worker.enqueue('https://example.com') // durably saved before it runs
|
|
441
|
+
|
|
442
|
+
// …after a crash + restart, a fresh worker over the same store resumes the work:
|
|
443
|
+
const resumed = createWorker({ store, pool: { create: () => connect() }, handler: run })
|
|
444
|
+
await resumed.restore() // re-enqueues every still-outstanding entry, then runs it
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Pause, drain, and shut down
|
|
448
|
+
|
|
449
|
+
Enqueue work, wait for the `drain` event, then drive the lifecycle and close the worker down:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { createWorker } from '@orkestrel/worker'
|
|
453
|
+
|
|
454
|
+
const idle = Promise.withResolvers<void>()
|
|
455
|
+
const worker = createWorker({
|
|
456
|
+
pool: { create: () => connect() },
|
|
457
|
+
handler: run,
|
|
458
|
+
on: { drain: () => idle.resolve() }, // fires when nothing is pending and nothing in flight
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
await worker.enqueue('https://example.com')
|
|
462
|
+
await idle.promise // the queue has drained
|
|
463
|
+
|
|
464
|
+
worker.pause() // suspends dequeuing; jobs already in flight keep running
|
|
465
|
+
worker.resume() // continues where the pause left off
|
|
466
|
+
|
|
467
|
+
await worker.clear() // drops pending jobs and awaits their durable cleanup
|
|
468
|
+
await worker.stop() // rejects pending work and awaits cleanup quiescence
|
|
469
|
+
worker.start() // restarts the loops a stop halted
|
|
470
|
+
|
|
471
|
+
await worker.abort('shutting down') // cancels in-flight jobs; `start` cannot revive the worker
|
|
472
|
+
await worker.destroy() // queue cleanup, then pool cleanup, then emitter teardown
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Practices
|
|
476
|
+
|
|
477
|
+
Follow these practices when you run a worker in production:
|
|
478
|
+
|
|
479
|
+
- **Honour `context.signal`** — pass it through to the resource's operation and bail
|
|
480
|
+
out when it fires, so timeouts and aborts actually stop work rather than abandoning
|
|
481
|
+
its result.
|
|
482
|
+
- **Size the pool through `concurrency`** — the pool's `max` defaults to it; override
|
|
483
|
+
`pool.max` explicitly only when the resource cap must diverge from the job cap.
|
|
484
|
+
Concurrency must be a positive safe integer; invalid values are rejected by Queue and
|
|
485
|
+
are never normalized.
|
|
486
|
+
- **`abort` is terminal** — a worker-level abort cancels in-flight work and stops the
|
|
487
|
+
underlying queue; await its cleanup barrier, then create a new worker to start over.
|
|
488
|
+
- **Await lifecycle cleanup** — `stop`, `abort`, `clear`, and `destroy` are completion
|
|
489
|
+
barriers. `destroy` is stable across repeated/reentrant calls and completes only after
|
|
490
|
+
serial queue then pool cleanup and emitter teardown.
|
|
491
|
+
- **Observe, don't drive** — subscribe to `worker.emitter` for lifecycle moments (see
|
|
492
|
+
[Observing](#observing)); emitting is a pure side-channel.
|
|
493
|
+
- **CPU-parallel work needs `createNodeWorker`** — the core `createWorker` runs its
|
|
494
|
+
handler on the same event loop (a resource pool, not a thread pool); reach for
|
|
495
|
+
`createNodeWorker` only when the work is genuinely CPU-bound and would otherwise
|
|
496
|
+
block.
|
|
497
|
+
|
|
498
|
+
## Tests
|
|
499
|
+
|
|
500
|
+
These tests pin the behaviour this guide documents:
|
|
501
|
+
|
|
502
|
+
- [`tests/guides.test.ts`](../tests/guides.test.ts) — the
|
|
503
|
+
`## Surface` ↔ `src/core` / `src/server` bijection (value + type exports), the
|
|
504
|
+
`WorkerInterface` ↔ `Worker` method bijection, and the equality gate: every `Summary`
|
|
505
|
+
cell against its declaration's description paragraph, the titled
|
|
506
|
+
`A resource-backed worker` fence against the `@example` block of that title (pinned so
|
|
507
|
+
the titled pair cannot be retired silently), and the README pitch against this guide's
|
|
508
|
+
tagline. It also transcribes the Threads, NodeWorker, Persistence, CPU-parallel, and
|
|
509
|
+
lifecycle fences, running each against the real exports: it checks every trailing comment
|
|
510
|
+
that names a returned value against that value, and checks the lifecycle fence's claims —
|
|
511
|
+
the `drain` hook, dequeuing suspended while in-flight work runs, the loops restarted after
|
|
512
|
+
`stop`, and the worker left terminal by `abort`.
|
|
513
|
+
- [`tests/policy.test.ts`](../tests/policy.test.ts) — the fleet placement sweep over
|
|
514
|
+
`src`: every module function sits in a function-kind file, every centralized declaration
|
|
515
|
+
is exported, types sit in `types.ts`, classes match their file, and every module test
|
|
516
|
+
under `tests/src` mirrors a real source module.
|
|
517
|
+
- [`tests/config.test.ts`](../tests/config.test.ts) — the root configuration resolves every
|
|
518
|
+
declared alias to a real entry, registers every workspace project with its fixed include
|
|
519
|
+
and setup files, and builds each face to its declared output.
|
|
520
|
+
- [`tests/src/core/Worker.test.ts`](../tests/src/core/Worker.test.ts) — the handler
|
|
521
|
+
runs against a pooled resource; resources are reused across jobs and never exceed the
|
|
522
|
+
pool max; the resource is released even when the handler throws (a later job reuses
|
|
523
|
+
it); the lifecycle (`pause` / `resume` / `abort` / `stop` / `clear` / `destroy`)
|
|
524
|
+
returns the queue's cleanup barriers; constructor options and every declared pool option are
|
|
525
|
+
captured once (including inherited / non-enumerable structural options), only explicit
|
|
526
|
+
`undefined` defaults, runtime `null` reaches the Queue/Pool diagnostic,
|
|
527
|
+
and invalid Queue concurrency wins before the pool option is read; strict concurrency
|
|
528
|
+
rejects zero, negative, fractional, `NaN`, and infinite values instead of normalizing
|
|
529
|
+
them; `destroy` keeps one
|
|
530
|
+
promise identity across repeated and abort-event-reentrant calls, waits for serial queue
|
|
531
|
+
then pool cleanup, destroys the worker emitter last, and preserves a sole pool failure;
|
|
532
|
+
and durability passthrough — a `store` persists a job and `restore()` re-runs it against
|
|
533
|
+
a fresh resource (delegated to the queue), a no-op without a store.
|
|
534
|
+
- [`tests/src/core/factories.test.ts`](../tests/src/core/factories.test.ts) —
|
|
535
|
+
`createWorker` returns a working, typed instance end to end and honours its options +
|
|
536
|
+
status surface.
|
|
537
|
+
- [`tests/src/server/factories.test.ts`](../tests/src/server/factories.test.ts) —
|
|
538
|
+
`createJSONQueueStore` over a real temp file: entries persist across store instances on
|
|
539
|
+
the same path (a second store `load`s the first's work), a nested-object input survives
|
|
540
|
+
the JSON round-trip, and a `remove` is reflected across a reopen; `createThread` resolving
|
|
541
|
+
a live thread that clones its `workerData` across, spawning with that argument omitted, and
|
|
542
|
+
rejecting a script that dies before `online`; plus a `createNodeWorker` round-trip smoke (a
|
|
543
|
+
job over a real thread, then teardown), the `on` hooks wired at construction with a
|
|
544
|
+
listener throw routed to `error`, and the option snapshot retained across jobs.
|
|
545
|
+
- [`tests/src/server/helpers.test.ts`](../tests/src/server/helpers.test.ts) — the
|
|
546
|
+
main-side worker-thread machinery (`createThread` / `Dispatch`), driven
|
|
547
|
+
through `createNodeWorker` over real worker threads (no mocking): a round-trip and a
|
|
548
|
+
batch over a small pool; the concurrency cap and the live-thread cap + idle reuse; a
|
|
549
|
+
throwing handler rejecting with its error, and re-running under `retries`; explicit enqueue
|
|
550
|
+
ids reaching the thread handler, retry attempts retaining one stable job id while using fresh
|
|
551
|
+
correlation ids, and a real pre-populated `MemoryQueueStore` restore retaining its stored id; a
|
|
552
|
+
per-attempt `timeout` rejecting and terminating the uncooperative thread, with a later
|
|
553
|
+
job served on a fresh thread; a direct in-flight abort preserving the caller's exact
|
|
554
|
+
reason object; a code-1 crash rejecting with the exact latched `NodeThread.death` and
|
|
555
|
+
exit-code message; each terminal path evicting its thread; `workerData` cloned through
|
|
556
|
+
to the worker side (and a non-cloneable `workerData` surfacing a clear error, never a
|
|
557
|
+
hang); a large array
|
|
558
|
+
input + result round-trip; a broken worker script rejecting the job cleanly with the
|
|
559
|
+
pool recovering across retries + a fresh worker; stray / foreign-id chatter ignored while
|
|
560
|
+
the correct reply still resolves; a matching-id malformed reply rejecting, evicting its
|
|
561
|
+
tainted thread, and allowing a later job on a replacement; an already-aborted enqueue signal
|
|
562
|
+
short-circuiting; a non-cloneable result rejecting without a hang and a later job on the
|
|
563
|
+
same concurrency-1 worker succeeding; false and throwing `input` / `result` guards
|
|
564
|
+
rejecting without wedging the worker; a consumer-supplied `NodeThread` whose job the abort
|
|
565
|
+
rejects and whose `worker` it terminates while the implementer's own `alive` stays
|
|
566
|
+
untouched; `messageerror` listener attachment and settlement
|
|
567
|
+
cleanup; `destroy` with multiple threads mid-job terminating
|
|
568
|
+
all; rapid enqueue/abort churn settling every job with no thread leak; and `destroy`
|
|
569
|
+
terminating every thread so the process exits. Plus the total `isReply` predicate over
|
|
570
|
+
valid success/failure envelopes, foreign ids, malformed or incomplete envelopes,
|
|
571
|
+
non-records, hostile getters, and stray messages.
|
|
572
|
+
- [`tests/src/server/handlers.test.ts`](../tests/src/server/handlers.test.ts) —
|
|
573
|
+
`serveWorker` driven manually over a raw `node:worker_threads` thread (post a run/abort
|
|
574
|
+
envelope, await the reply): reply correlation remaining distinct from the stable job id,
|
|
575
|
+
missing / non-string job ids plus revoked proxies and throwing job getters invoking no handler
|
|
576
|
+
and producing no reply, abort routing by correlation id when the stable job id differs, the
|
|
577
|
+
success envelope, false and throwing input-guard
|
|
578
|
+
rejection envelopes, a non-cloneable success falling back to a clone-safe error while a
|
|
579
|
+
later job succeeds on the same thread, a handler-throw error envelope (a synchronous throw and an
|
|
580
|
+
asynchronous rejection both reported as `{ ok: false }`), a `{ command: 'abort' }` firing the
|
|
581
|
+
handler's signal, an abort for an unknown id
|
|
582
|
+
being a no-op, an unknown message `command` and a malformed (no-`id`) message both
|
|
583
|
+
ignored without crashing the thread, and object / array / null / boolean result shapes
|
|
584
|
+
round-tripping through the `{ ok: true, value }` envelope; registration reading `input`
|
|
585
|
+
then `handler` once across successive real jobs, failing before the handler read when the input
|
|
586
|
+
getter throws, and the main-thread no-op reading neither option.
|
|
587
|
+
- The worker fixtures under
|
|
588
|
+
[`tests/src/server/fixtures`](../tests/src/server/fixtures) (`double` / `fail` /
|
|
589
|
+
`slow` / `bad-result` / `abortable` / `crash` / `identify` / `execution` / `identity` /
|
|
590
|
+
`echo-data` / `sum` /
|
|
591
|
+
`stray` / `malformed` / `throw-async` / `load-throw` / `echo` / `noncloneable-result` /
|
|
592
|
+
`serve-options`) are
|
|
593
|
+
real `.ts` worker scripts. The Vitest projects supply no type-stripping flag, so the
|
|
594
|
+
`src:server` and `guides` suites load the fixtures through Node's unflagged type stripping
|
|
595
|
+
and run on Node 22.18+ and Node 23.6+ — a narrower floor than the `>=22.12.0` engine range
|
|
596
|
+
in `package.json`. Ordinary fixtures import `serveWorker` by
|
|
597
|
+
relative-to-source path; `malformed` and `identity` speak the internal envelope protocol
|
|
598
|
+
directly. `malformed` keeps a tainted thread alive after its invalid reply; `identity` observes
|
|
599
|
+
the fresh correlation id and stable job id across a retry. Every fixture contains no diagnostic
|
|
600
|
+
suppression and is included in the root TypeScript project while remaining outside test
|
|
601
|
+
discovery (not a `*.test.ts`).
|
|
602
|
+
|
|
603
|
+
## See also
|
|
604
|
+
|
|
605
|
+
Read these guides next:
|
|
606
|
+
|
|
607
|
+
- [`queue.md`](queue.md) — the `Queue` engine a `Worker` composes (cooperative wake-park
|
|
608
|
+
loop, retries, timeout, durability) — the `QueueStoreInterface` contract for
|
|
609
|
+
`createJSONQueueStore` / the `store` option.
|
|
610
|
+
- [`pool.md`](pool.md) — the `Pool` engine a `Worker` composes (idle reuse, `max`
|
|
611
|
+
backpressure, FIFO abort-able wait).
|
|
612
|
+
- [`contract.md`](contract.md) — the `Guard<T>` / shape vocabulary threaded through the
|
|
613
|
+
structured-clone boundary (`input` / `result` on `createNodeWorker`).
|
|
614
|
+
- [`database.md`](database.md) — the storage layer `createJSONQueueStore` builds on.
|
|
615
|
+
- [`README.md`](README.md) — the guides index.
|