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