@orkestrel/worker 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,266 @@
1
+ import { attempt, isRecord } from "@orkestrel/contract";
1
2
  import { Worker, parentPort } from "node:worker_threads";
2
- import { isRecord } from "@orkestrel/contract";
3
- import { createWorker } from "../core/index.js";
4
3
  import { createJSONDriver } from "@orkestrel/database/server";
5
4
  import { createDatabaseQueueStore } from "@orkestrel/queue";
5
+ import { createWorker } from "../core/index.js";
6
+ //#region src/server/Thread.ts
7
+ /**
8
+ * Internal mutable implementation of the readonly {@link NodeThread} observation contract.
9
+ *
10
+ * @remarks
11
+ * Liveness and the first terminal error live behind runtime-private fields. Thread `error`,
12
+ * `messageerror`, and `exit` all latch death, so pool validation cannot reuse a thread whose
13
+ * inbound message could not be deserialized.
14
+ */
15
+ var Thread = class {
16
+ #worker;
17
+ #promise;
18
+ #resolve;
19
+ #reject;
20
+ #recordHandler;
21
+ #recordExitHandler;
22
+ #onlineHandler;
23
+ #spawnErrorHandler;
24
+ #spawnExitHandler;
25
+ #alive = true;
26
+ #death;
27
+ constructor(script, workerData) {
28
+ this.#worker = new Worker(script, { ...workerData !== void 0 ? { workerData } : {} });
29
+ const readiness = Promise.withResolvers();
30
+ this.#promise = readiness.promise;
31
+ this.#resolve = readiness.resolve;
32
+ this.#reject = readiness.reject;
33
+ this.#recordHandler = this.#record.bind(this);
34
+ this.#recordExitHandler = this.#recordExit.bind(this);
35
+ this.#onlineHandler = this.#online.bind(this);
36
+ this.#spawnErrorHandler = this.#spawnError.bind(this);
37
+ this.#spawnExitHandler = this.#spawnExit.bind(this);
38
+ this.#worker.on("error", this.#recordHandler);
39
+ this.#worker.on("messageerror", this.#recordHandler);
40
+ this.#worker.on("exit", this.#recordExitHandler);
41
+ this.#worker.once("online", this.#onlineHandler);
42
+ this.#worker.once("error", this.#spawnErrorHandler);
43
+ this.#worker.once("exit", this.#spawnExitHandler);
44
+ }
45
+ get worker() {
46
+ return this.#worker;
47
+ }
48
+ get alive() {
49
+ return this.#alive;
50
+ }
51
+ get death() {
52
+ return this.#death;
53
+ }
54
+ get promise() {
55
+ return this.#promise;
56
+ }
57
+ evict() {
58
+ this.#alive = false;
59
+ }
60
+ #record(error) {
61
+ this.#alive = false;
62
+ if (this.#death === void 0) this.#death = error;
63
+ }
64
+ #recordExit(code) {
65
+ this.#alive = false;
66
+ if (this.#death === void 0) this.#death = /* @__PURE__ */ new Error(`worker thread exited (code ${String(code)})`);
67
+ }
68
+ #online() {
69
+ this.#worker.off("error", this.#spawnErrorHandler);
70
+ this.#worker.off("exit", this.#spawnExitHandler);
71
+ this.#resolve(this);
72
+ }
73
+ #spawnError(error) {
74
+ this.#worker.off("online", this.#onlineHandler);
75
+ this.#worker.off("exit", this.#spawnExitHandler);
76
+ this.#reject(error);
77
+ }
78
+ #spawnExit(code) {
79
+ this.#worker.off("online", this.#onlineHandler);
80
+ this.#worker.off("error", this.#spawnErrorHandler);
81
+ this.#reject(/* @__PURE__ */ new Error(`worker thread exited before coming online (code ${String(code)})`));
82
+ }
83
+ };
84
+ //#endregion
85
+ //#region src/server/validators.ts
86
+ /**
87
+ * Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.
88
+ *
89
+ * @remarks
90
+ * A total predicate: a record whose `id` matches and whose `ok` discriminant is well-formed.
91
+ * Anything else is rejected so a dispatch listener can ignore foreign or malformed messages.
92
+ *
93
+ * @param value - The inbound message to narrow
94
+ * @param id - The job id a matching reply must carry
95
+ * @returns `true` when the value is this job's well-formed reply
96
+ */
97
+ function isReply(value, id) {
98
+ const outcome = attempt(() => {
99
+ if (!isRecord(value)) return false;
100
+ if (value.id !== id) return false;
101
+ if (value.ok === true) return "value" in value;
102
+ return value.ok === false && typeof value.error === "string";
103
+ });
104
+ return outcome.success && outcome.value;
105
+ }
106
+ //#endregion
107
+ //#region src/server/Dispatch.ts
108
+ /**
109
+ * Internal lifecycle entity for one dispatched worker-thread job.
110
+ *
111
+ * @remarks
112
+ * Owns stable `message` / `messageerror` / death listener identities, settlement, result-guard
113
+ * containment, and abort eviction for one dispatch. Deserialization failure, a matching-id
114
+ * malformed reply, and abort each evict and terminate the thread before rejecting, with
115
+ * termination failure preserved. Non-record, id-less, hostile-id, and foreign-id chatter is ignored.
116
+ */
117
+ var Dispatch = class {
118
+ #thread;
119
+ #worker;
120
+ #input;
121
+ #execution;
122
+ #result;
123
+ #id = crypto.randomUUID();
124
+ #promise;
125
+ #fulfill;
126
+ #reject;
127
+ #messageHandler;
128
+ #messageErrorHandler;
129
+ #errorHandler;
130
+ #exitHandler;
131
+ #abortHandler;
132
+ #settled = false;
133
+ constructor(thread, input, execution, result) {
134
+ this.#thread = thread;
135
+ this.#worker = thread.worker;
136
+ this.#input = input;
137
+ this.#execution = execution;
138
+ this.#result = result;
139
+ const settlement = Promise.withResolvers();
140
+ this.#promise = settlement.promise;
141
+ this.#fulfill = settlement.resolve;
142
+ this.#reject = settlement.reject;
143
+ this.#messageHandler = this.#message.bind(this);
144
+ this.#messageErrorHandler = this.#messageError.bind(this);
145
+ this.#errorHandler = this.#error.bind(this);
146
+ this.#exitHandler = this.#exit.bind(this);
147
+ this.#abortHandler = this.#abort.bind(this);
148
+ this.#start();
149
+ }
150
+ get promise() {
151
+ return this.#promise;
152
+ }
153
+ #start() {
154
+ if (this.#thread.death !== void 0 || !this.#thread.alive) {
155
+ this.#fail(this.#thread.death ?? /* @__PURE__ */ new Error("worker thread is dead"));
156
+ return;
157
+ }
158
+ this.#worker.on("message", this.#messageHandler);
159
+ this.#worker.on("messageerror", this.#messageErrorHandler);
160
+ this.#worker.on("error", this.#errorHandler);
161
+ this.#worker.on("exit", this.#exitHandler);
162
+ if (this.#execution.signal.aborted) {
163
+ this.#abort();
164
+ return;
165
+ }
166
+ this.#execution.signal.addEventListener("abort", this.#abortHandler, { once: true });
167
+ try {
168
+ this.#worker.postMessage({
169
+ id: this.#id,
170
+ command: "run",
171
+ input: this.#input
172
+ });
173
+ } catch (error) {
174
+ this.#fail(error instanceof Error ? error : new Error(String(error)));
175
+ }
176
+ }
177
+ #message(value) {
178
+ if (!isRecord(value)) return;
179
+ const id = attempt(() => value.id);
180
+ if (!id.success || id.value !== this.#id) return;
181
+ if (!isReply(value, this.#id)) {
182
+ this.#terminate(/* @__PURE__ */ new Error("worker reply was malformed"));
183
+ return;
184
+ }
185
+ if (value.ok) {
186
+ const reply = value.value;
187
+ try {
188
+ if (this.#result(reply)) this.#succeed(reply);
189
+ else this.#fail(/* @__PURE__ */ new Error("reply did not satisfy result guard"));
190
+ } catch (error) {
191
+ this.#fail(error);
192
+ }
193
+ return;
194
+ }
195
+ this.#fail(new Error(value.error));
196
+ }
197
+ #messageError(error) {
198
+ this.#terminate(error);
199
+ }
200
+ #error(error) {
201
+ this.#fail(error);
202
+ }
203
+ #exit() {
204
+ this.#fail(this.#thread.death ?? /* @__PURE__ */ new Error("worker thread exited"));
205
+ }
206
+ #abort() {
207
+ const notification = [];
208
+ try {
209
+ this.#worker.postMessage({
210
+ id: this.#id,
211
+ command: "abort"
212
+ });
213
+ } catch (cause) {
214
+ notification.push(cause);
215
+ }
216
+ this.#terminate(this.#execution.signal.reason, notification);
217
+ }
218
+ #terminate(error, notification = []) {
219
+ if (this.#settled) return;
220
+ this.#settled = true;
221
+ this.#detach();
222
+ if (this.#thread instanceof Thread) this.#thread.evict();
223
+ let termination;
224
+ try {
225
+ termination = this.#worker.terminate();
226
+ } catch (cause) {
227
+ this.#reject(new AggregateError([
228
+ error,
229
+ ...notification,
230
+ cause
231
+ ], "worker termination failed"));
232
+ return;
233
+ }
234
+ termination.then(() => {
235
+ if (notification.length === 0) this.#reject(error);
236
+ else this.#reject(new AggregateError([error, ...notification], "worker abort notification failed"));
237
+ }, (cause) => this.#reject(new AggregateError([
238
+ error,
239
+ ...notification,
240
+ cause
241
+ ], "worker termination failed")));
242
+ }
243
+ #succeed(value) {
244
+ if (this.#settled) return;
245
+ this.#settled = true;
246
+ this.#detach();
247
+ this.#fulfill(value);
248
+ }
249
+ #fail(error) {
250
+ if (this.#settled) return;
251
+ this.#settled = true;
252
+ this.#detach();
253
+ this.#reject(error);
254
+ }
255
+ #detach() {
256
+ this.#worker.off("message", this.#messageHandler);
257
+ this.#worker.off("messageerror", this.#messageErrorHandler);
258
+ this.#worker.off("error", this.#errorHandler);
259
+ this.#worker.off("exit", this.#exitHandler);
260
+ this.#execution.signal.removeEventListener("abort", this.#abortHandler);
261
+ }
262
+ };
263
+ //#endregion
6
264
  //#region src/server/helpers.ts
7
265
  /**
8
266
  * Spawn one worker thread and resolve a live {@link NodeThread} once it comes online.
@@ -15,71 +273,18 @@ import { createDatabaseQueueStore } from "@orkestrel/queue";
15
273
  * listeners that flip `alive` to `false` AND latch the first terminal event on
16
274
  * {@link NodeThread.death}: a crash is observable to an in-flight {@link dispatch} (via
17
275
  * its own listeners), to the pool's `validate` (via `alive`), and — crucially — to a
18
- * dispatch that attaches AFTER the death (via the latch). The latch closes a real race:
19
- * under event-loop pressure a dead thread's `online` + `error` + `exit` are delivered in
20
- * ONE synchronous exit-drain batch, so every death event fires before the microtask chain
21
- * resolving this spawn can hand the thread to `dispatch` without the latch that job
22
- * would await events that already fired, forever. The pool's `create` hook calls this.
276
+ * dispatch that attaches AFTER the death (via the latch). A `messageerror` is terminal too,
277
+ * so a thread whose inbound payload could not be deserialized is never reused. The latch
278
+ * closes a real race: a thread can become terminal before the readiness promise continuation
279
+ * hands it to `dispatch`, leaving no future death event for that dispatch to observe. Without
280
+ * the latch, that job would wait forever. The pool's `create` hook calls this.
23
281
  *
24
282
  * @param script - The worker module each thread runs (must call `serveWorker`)
25
283
  * @param workerData - Opaque, structured-cloneable data handed to the thread at spawn
26
284
  * @returns A promise resolving the online {@link NodeThread}
27
285
  */
28
286
  function spawnThread(script, workerData) {
29
- const worker = new Worker(script, { workerData });
30
- const thread = {
31
- worker,
32
- alive: true,
33
- death: void 0
34
- };
35
- worker.on("error", (error) => {
36
- thread.alive = false;
37
- if (thread.death === void 0) thread.death = error;
38
- });
39
- worker.on("exit", (code) => {
40
- thread.alive = false;
41
- if (thread.death === void 0) thread.death = /* @__PURE__ */ new Error(`worker thread exited (code ${code})`);
42
- });
43
- return new Promise((resolve, reject) => {
44
- const onOnline = () => {
45
- worker.off("error", onError);
46
- worker.off("exit", onExit);
47
- resolve(thread);
48
- };
49
- const onError = (error) => {
50
- worker.off("online", onOnline);
51
- worker.off("exit", onExit);
52
- reject(error);
53
- };
54
- const onExit = (code) => {
55
- worker.off("online", onOnline);
56
- worker.off("error", onError);
57
- reject(/* @__PURE__ */ new Error(`worker thread exited before coming online (code ${code})`));
58
- };
59
- worker.once("online", onOnline);
60
- worker.once("error", onError);
61
- worker.once("exit", onExit);
62
- });
63
- }
64
- /**
65
- * Narrow an inbound `message` to a {@link Reply} for a given job `id` — no assertion.
66
- *
67
- * @remarks
68
- * A total {@link Guard}-style predicate (never throws): a record whose `id` matches and
69
- * whose `ok` discriminant is well-formed (a `true` carries any `value`; a `false` carries a
70
- * string `error`). Anything else — another job's reply, a malformed payload — is `false`, so
71
- * a {@link dispatch} listener ignores it (a thread that chatters on the channel can't corrupt
72
- * a job).
73
- *
74
- * @param value - The inbound message to narrow
75
- * @param id - The job id a matching reply must carry
76
- * @returns `true` (narrowing `value` to {@link Reply}) when it is this job's well-formed reply
77
- */
78
- function isReply(value, id) {
79
- if (!isRecord(value)) return false;
80
- if (value.id !== id) return false;
81
- if (value.ok === true) return true;
82
- return value.ok === false && typeof value.error === "string";
287
+ return new Thread(script, workerData).promise;
83
288
  }
84
289
  /**
85
290
  * Dispatch one job to a leased {@link NodeThread} and await its narrowed reply.
@@ -89,15 +294,15 @@ function isReply(value, id) {
89
294
  * that id: a success `value` is narrowed through `result` (a value that fails the guard
90
295
  * rejects — the zero-`as` type bridge), a failure rejects with the thread's error string.
91
296
  * A thread that ALREADY died rejects synchronously at entry from the latched
92
- * {@link NodeThread.death} — its death events fired before this dispatch existed (under
93
- * load they arrive in one batched exit drain) and will never fire again, so waiting on
94
- * the listeners below would dangle forever; the latch makes the death total across every
95
- * event ordering. If the thread `error`s / `exit`s mid-flight it is marked dead and the
96
- * job rejects. On `execution.signal` abort it posts an `abort` envelope (cooperative) AND
97
- * evicts the thread `alive = false` + `terminate()` — because CPU-bound work cannot
98
- * honour the signal; the freed pool slot then gets a fresh thread. Every listener (the
99
- * thread's `message` / `error` / `exit` and the signal's `abort`) is removed on settle,
100
- * and a `settled` guard prevents a double-settle.
297
+ * {@link NodeThread.death} — its death events fired before this dispatch existed and will
298
+ * never fire again, so waiting on the listeners below would dangle forever; the latch makes
299
+ * death total across every event ordering. If the thread `error`s / `exit`s mid-flight it is
300
+ * marked dead and the
301
+ * job rejects. An inbound `messageerror` also evicts and terminates the thread before
302
+ * rejection. On `execution.signal` abort it contains the cooperative `abort` post,
303
+ * evicts the thread, and observes `terminate()` settlement because CPU-bound work cannot
304
+ * honour the signal; the freed pool slot then gets a fresh thread. Every per-job listener
305
+ * (`message` / `messageerror` / `error` / `exit` / `abort`) is removed on settle.
101
306
  *
102
307
  * @typeParam TResult - The reply type the `result` guard narrows to
103
308
  * @param thread - The leased thread to run the job on
@@ -107,73 +312,7 @@ function isReply(value, id) {
107
312
  * @returns A promise resolving the narrowed `TResult`, or rejecting on error / abort
108
313
  */
109
314
  function dispatch(thread, input, execution, result) {
110
- const id = crypto.randomUUID();
111
- const worker = thread.worker;
112
- return new Promise((resolve, reject) => {
113
- if (thread.death !== void 0 || !thread.alive) {
114
- reject(thread.death ?? /* @__PURE__ */ new Error("worker thread is dead"));
115
- return;
116
- }
117
- let settled = false;
118
- let detach = () => {};
119
- const settle = (action) => {
120
- if (settled) return;
121
- settled = true;
122
- detach();
123
- action();
124
- };
125
- const onMessage = (value) => {
126
- if (!isReply(value, id)) return;
127
- if (value.ok) {
128
- const reply = value.value;
129
- if (result(reply)) settle(() => resolve(reply));
130
- else settle(() => reject(/* @__PURE__ */ new Error("reply did not satisfy result guard")));
131
- return;
132
- }
133
- const message = value.error;
134
- settle(() => reject(new Error(message)));
135
- };
136
- const onError = (error) => {
137
- thread.alive = false;
138
- settle(() => reject(error));
139
- };
140
- const onExit = () => {
141
- thread.alive = false;
142
- settle(() => reject(/* @__PURE__ */ new Error("worker thread exited")));
143
- };
144
- const onAbort = () => {
145
- worker.postMessage({
146
- id,
147
- command: "abort"
148
- });
149
- thread.alive = false;
150
- worker.terminate();
151
- settle(() => reject(/* @__PURE__ */ new Error("job aborted")));
152
- };
153
- detach = () => {
154
- worker.off("message", onMessage);
155
- worker.off("error", onError);
156
- worker.off("exit", onExit);
157
- execution.signal.removeEventListener("abort", onAbort);
158
- };
159
- worker.on("message", onMessage);
160
- worker.on("error", onError);
161
- worker.on("exit", onExit);
162
- if (execution.signal.aborted) {
163
- onAbort();
164
- return;
165
- }
166
- execution.signal.addEventListener("abort", onAbort, { once: true });
167
- try {
168
- worker.postMessage({
169
- id,
170
- command: "run",
171
- input
172
- });
173
- } catch (error) {
174
- settle(() => reject(error instanceof Error ? error : new Error(String(error))));
175
- }
176
- });
315
+ return new Dispatch(thread, input, execution, result).promise;
177
316
  }
178
317
  //#endregion
179
318
  //#region src/server/serve.ts
@@ -194,7 +333,10 @@ function isAbort(value) {
194
333
  * run/abort protocol: a `run` message narrows its `input` through `options.input` (an
195
334
  * invalid payload replies with an error envelope, never running the handler), then runs
196
335
  * `options.handler(input, { signal })` and replies `{ id, ok: true, value }` on success or
197
- * `{ id, ok: false, error }` on throw. Each in-flight job has its own `AbortController`,
336
+ * `{ id, ok: false, error }` on throw. Input-guard throws use the same failure envelope. If a
337
+ * success value cannot be cloned, the post is retried as a clone-safe failure; if that post also
338
+ * fails, the parent port closes so the main side observes thread exit instead of waiting forever.
339
+ * Each in-flight job has its own `AbortController`,
198
340
  * so an `abort` message for that id fires the handler's `signal` (cooperative — the main
199
341
  * side ALSO terminates the thread, so a handler that ignores its signal is still stopped).
200
342
  * Every inbound message is narrowed with the inlined guards — no `as`. On the main thread
@@ -207,7 +349,7 @@ function isAbort(value) {
207
349
  * @example
208
350
  * ```ts
209
351
  * // double.ts — a worker script
210
- * import { serveWorker } from '@src/server'
352
+ * import { serveWorker } from '@orkestrel/worker/server'
211
353
  *
212
354
  * serveWorker<number, number>({
213
355
  * input: (value): value is number => typeof value === 'number',
@@ -218,6 +360,8 @@ function isAbort(value) {
218
360
  function serveWorker(options) {
219
361
  const port = parentPort;
220
362
  if (port === null) return;
363
+ const input = options.input;
364
+ const handler = options.handler;
221
365
  const controllers = /* @__PURE__ */ new Map();
222
366
  port.on("message", (raw) => {
223
367
  if (isAbort(raw)) {
@@ -226,35 +370,99 @@ function serveWorker(options) {
226
370
  }
227
371
  if (!isRun(raw)) return;
228
372
  const id = raw.id;
229
- if (!options.input(raw.input)) {
230
- port.postMessage({
231
- id,
232
- ok: false,
233
- error: "input did not satisfy input guard"
234
- });
235
- return;
236
- }
237
- const input = raw.input;
238
373
  const controller = new AbortController();
239
374
  controllers.set(id, controller);
240
- Promise.resolve().then(() => options.handler(input, { signal: controller.signal })).then((value) => {
375
+ Promise.resolve().then(() => {
376
+ if (!input(raw.input)) throw new Error("input did not satisfy input guard");
377
+ const value = raw.input;
378
+ return handler(value, { signal: controller.signal });
379
+ }).then((value) => {
241
380
  controllers.delete(id);
242
381
  port.postMessage({
243
382
  id,
244
383
  ok: true,
245
384
  value
246
385
  });
247
- }, (error) => {
386
+ }).catch((error) => {
248
387
  controllers.delete(id);
249
- port.postMessage({
250
- id,
251
- ok: false,
252
- error: error instanceof Error ? error.message : String(error)
253
- });
388
+ let message = "worker operation failed";
389
+ try {
390
+ message = error instanceof Error ? error.message : String(error);
391
+ } catch {}
392
+ try {
393
+ port.postMessage({
394
+ id,
395
+ ok: false,
396
+ error: message
397
+ });
398
+ } catch {
399
+ try {
400
+ port.close();
401
+ } catch {}
402
+ }
254
403
  });
255
404
  });
256
405
  }
257
406
  //#endregion
407
+ //#region src/server/NodeWorker.ts
408
+ /**
409
+ * Internal composition entity backing {@link createNodeWorker}.
410
+ *
411
+ * @remarks
412
+ * Supplies bound Pool and Queue operations without nested function assignments. The resulting
413
+ * public entity remains the plain core {@link WorkerInterface}.
414
+ */
415
+ var NodeWorker = class {
416
+ #script;
417
+ #input;
418
+ #result;
419
+ #workerData;
420
+ #concurrency;
421
+ #retries;
422
+ #timeout;
423
+ #store;
424
+ constructor(options) {
425
+ this.#script = options.script;
426
+ this.#input = options.input;
427
+ this.#result = options.result;
428
+ this.#workerData = options.workerData;
429
+ this.#concurrency = options.concurrency;
430
+ this.#retries = options.retries;
431
+ this.#timeout = options.timeout;
432
+ this.#store = options.store;
433
+ }
434
+ build() {
435
+ return createWorker({
436
+ pool: {
437
+ create: this.#create.bind(this),
438
+ destroy: this.#destroy.bind(this),
439
+ validate: this.#validate.bind(this),
440
+ ...this.#concurrency !== void 0 ? { max: this.#concurrency } : {}
441
+ },
442
+ handler: this.#handle.bind(this),
443
+ ...this.#concurrency !== void 0 ? { concurrency: this.#concurrency } : {},
444
+ ...this.#retries !== void 0 ? { retries: this.#retries } : {},
445
+ ...this.#timeout !== void 0 ? { timeout: this.#timeout } : {},
446
+ ...this.#store !== void 0 ? { store: this.#store } : {}
447
+ });
448
+ }
449
+ #create() {
450
+ return spawnThread(this.#script, this.#workerData);
451
+ }
452
+ async #destroy(thread) {
453
+ await thread.worker.terminate();
454
+ }
455
+ #validate(thread) {
456
+ return thread.alive && thread.worker.threadId > 0;
457
+ }
458
+ #handle(input, thread, execution) {
459
+ const outcome = attempt(() => this.#input(input));
460
+ if (!outcome.success) return Promise.reject(outcome.error);
461
+ if (!outcome.value) return Promise.reject(/* @__PURE__ */ new Error("input did not satisfy input guard"));
462
+ return dispatch(thread, input, execution, this.#result);
463
+ }
464
+ };
465
+ //#endregion
258
466
  //#region src/server/factories.ts
259
467
  /**
260
468
  * Create a persistent JSON-file {@link QueueStoreInterface} — the core
@@ -276,8 +484,8 @@ function serveWorker(options) {
276
484
  *
277
485
  * @example
278
486
  * ```ts
279
- * import { stringShape } from '@src/core'
280
- * import { createJSONQueueStore } from '@src/server'
487
+ * import { stringShape } from '@orkestrel/contract'
488
+ * import { createJSONQueueStore } from '@orkestrel/worker/server'
281
489
  *
282
490
  * const store = createJSONQueueStore('data/queue.json', stringShape())
283
491
  * await store.save({ id: 'job-1', input: 'https://example.com', attempts: 0 })
@@ -317,7 +525,7 @@ function createJSONQueueStore(path, input) {
317
525
  *
318
526
  * @example
319
527
  * ```ts
320
- * import { createNodeWorker } from '@src/server'
528
+ * import { createNodeWorker } from '@orkestrel/worker/server'
321
529
  *
322
530
  * const worker = createNodeWorker({
323
531
  * script: new URL('./double.js', import.meta.url),
@@ -327,26 +535,11 @@ function createJSONQueueStore(path, input) {
327
535
  * })
328
536
  *
329
537
  * const doubled = await worker.enqueue(21) // 42, computed on a worker thread
330
- * worker.destroy() // terminates every thread
538
+ * await worker.destroy() // terminates every thread
331
539
  * ```
332
540
  */
333
541
  function createNodeWorker(options) {
334
- return createWorker({
335
- pool: {
336
- create: () => spawnThread(options.script, options.workerData),
337
- destroy: (thread) => thread.worker.terminate().then(() => {}),
338
- validate: (thread) => thread.alive && thread.worker.threadId > 0,
339
- max: options.concurrency
340
- },
341
- handler: (input, thread, execution) => {
342
- if (!options.input(input)) return Promise.reject(/* @__PURE__ */ new Error("input did not satisfy input guard"));
343
- return dispatch(thread, input, execution, options.result);
344
- },
345
- concurrency: options.concurrency,
346
- retries: options.retries,
347
- timeout: options.timeout,
348
- store: options.store
349
- });
542
+ return new NodeWorker(options).build();
350
543
  }
351
544
  //#endregion
352
545
  export { createJSONQueueStore, createNodeWorker, dispatch, isReply, serveWorker, spawnThread };