@atolljs/core 0.1.13 → 0.1.15
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/contract/listSchema.js +1 -2
- package/dist/contract/memoryDevtools.js +2 -3
- package/dist/contract/msgpackrCodec.js +1 -2
- package/dist/contract/reef.js +1 -2
- package/dist/contract/sharedMemory.js +61 -62
- package/dist/contract/types.d.ts +24 -0
- package/dist/contract/zod.js +1 -2
- package/dist/devtools.js +32 -39
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -22
- package/dist/log.js +1 -2
- package/dist/observable.js +18 -19
- package/dist/pool/dedicatedWorker.d.ts +3 -0
- package/dist/pool/dedicatedWorker.js +72 -68
- package/dist/pool/devtoolsCommands.js +1 -2
- package/dist/pool/errors.js +1 -2
- package/dist/pool/memory.js +1 -2
- package/dist/pool/subWorkerClient.js +1 -2
- package/dist/pool/workerChannel.js +21 -22
- package/dist/pool/workerClient.d.ts +15 -1
- package/dist/pool/workerClient.js +14 -10
- package/dist/pool/workerPool.d.ts +3 -0
- package/dist/pool/workerPool.js +119 -121
- package/dist/reactive.js +1 -2
- package/dist/reactiveGraph.js +1 -2
- package/dist/service.js +1 -2
- package/dist/shared/sharedWorkerClient.js +2 -9
- package/dist/shared/sharedWorkerHost.js +22 -27
- package/dist/task.js +1 -2
- package/dist/testing/inProcessWorker.js +3 -8
- package/dist/userTiming.js +1 -2
- package/dist/worker/defineWorker.js +8 -9
- package/dist/worker/registry.js +1 -2
- package/dist/worker/workerBootstrap.js +40 -52
- package/package.json +11 -7
- package/src/contract/types.ts +26 -0
- package/src/index.ts +2 -0
- package/src/pool/dedicatedWorker.ts +19 -1
- package/src/pool/workerClient.ts +30 -1
- package/src/pool/workerPool.ts +11 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
var c =
|
|
1
|
+
import { TaskRegistry as e } from "./registry.js";
|
|
2
|
+
import { bindSharedMemories as t, getDefinedSharedMemoryCount as n } from "../contract/sharedMemory.js";
|
|
3
|
+
import { devtoolsEnabled as r, enableWorkerDevtoolsForwarding as i } from "../devtools.js";
|
|
4
|
+
import { fmtBytes as a, scoped as o } from "../log.js";
|
|
5
|
+
import { measureAtoll as s } from "../userTiming.js";
|
|
6
|
+
// src/worker/workerBootstrap.ts
|
|
7
|
+
var c = o("worker"), l = (e, t, n, r) => {
|
|
8
8
|
let i = performance.now();
|
|
9
|
-
|
|
9
|
+
s(`atoll run ${e}`, n, i, {
|
|
10
10
|
track: "tasks",
|
|
11
11
|
color: r ? "secondary" : "error",
|
|
12
12
|
properties: [["message", t], ["outcome", r ? "ok" : "error"]],
|
|
@@ -14,55 +14,43 @@ var c = r("worker"), l = (e, t, n, r) => {
|
|
|
14
14
|
});
|
|
15
15
|
}, u = !1, d = !1;
|
|
16
16
|
function f() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (o.type === "INIT") {
|
|
25
|
-
u = !0, o.devtools && t(), a() > 0 && c.warn("pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound"), c.info("initialized (message-only pool — no shared memory)");
|
|
17
|
+
d || (typeof self > "u" ? c.warn("workerBootstrap: no worker global found — skipping message wiring") : (d = !0, self.onmessage = async (o) => {
|
|
18
|
+
let s = o.data;
|
|
19
|
+
if (s.type === "INIT") u = !0, s.devtools && i(), n() > 0 && c.warn("pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound"), c.info("initialized (message-only pool — no shared memory)");
|
|
20
|
+
else if (s.type === "INIT_MEMORY") {
|
|
21
|
+
let e = s.memory?.buffer ?? s.buffer;
|
|
22
|
+
if (!e) {
|
|
23
|
+
c.warn("INIT_MEMORY arrived without a buffer — contracts stay unbound");
|
|
26
24
|
return;
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
t(e), u = !0, s.devtools && i(), c.info(`shared memory initialized (${a(e.byteLength)})`);
|
|
27
|
+
} else if (s.type === "EXECUTE_TASK") {
|
|
28
|
+
let { messageId: t, taskId: n, args: i } = s;
|
|
29
|
+
if (!u) {
|
|
30
|
+
c.warn(`rejected ${n} — shared memory not initialized`), self.postMessage({
|
|
31
|
+
messageId: t,
|
|
32
|
+
success: !1,
|
|
33
|
+
error: "Shared memory not initialized in worker."
|
|
34
|
+
});
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
messageId: t,
|
|
52
|
-
success: !0,
|
|
53
|
-
result: a
|
|
54
|
-
});
|
|
55
|
-
} catch (r) {
|
|
56
|
-
e() && l(n, t, i, !1), c.warn(`task ${n} failed: ${r.message || String(r)}`), self.postMessage({
|
|
57
|
-
messageId: t,
|
|
58
|
-
success: !1,
|
|
59
|
-
error: r.message || String(r)
|
|
60
|
-
});
|
|
61
|
-
}
|
|
37
|
+
let a = performance.now();
|
|
38
|
+
try {
|
|
39
|
+
let o = await e.execute(n, ...i || []);
|
|
40
|
+
r() && l(n, t, a, !0), c.debug(`task ${n} → ${(performance.now() - a).toFixed(1)}ms`), self.postMessage({
|
|
41
|
+
messageId: t,
|
|
42
|
+
success: !0,
|
|
43
|
+
result: o
|
|
44
|
+
});
|
|
45
|
+
} catch (e) {
|
|
46
|
+
r() && l(n, t, a, !1), c.warn(`task ${n} failed: ${e.message || String(e)}`), self.postMessage({
|
|
47
|
+
messageId: t,
|
|
48
|
+
success: !1,
|
|
49
|
+
error: e.message || String(e)
|
|
50
|
+
});
|
|
62
51
|
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
52
|
+
}
|
|
53
|
+
}));
|
|
65
54
|
}
|
|
66
55
|
queueMicrotask(f);
|
|
67
|
-
//#endregion
|
|
68
56
|
export { f as installWorkerListener };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolljs/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Typed shared-memory worker pools — defineWorker/connectWorker, SharedArrayBuffer contracts, cross-thread reactivity",
|
|
5
5
|
"homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
|
|
6
6
|
"type": "module",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"@types/react": "^19.3.0",
|
|
56
56
|
"@types/react-dom": "^19.3.0",
|
|
57
57
|
"@vitejs/plugin-vue": "^6.0.9",
|
|
58
|
-
"@vitest/coverage-v8": "^4.1.
|
|
58
|
+
"@vitest/coverage-v8": "^4.1.12",
|
|
59
59
|
"esbuild": "0.28.2",
|
|
60
60
|
"fast-json-stringify": "^7.0.1",
|
|
61
|
-
"happy-dom": "^
|
|
61
|
+
"happy-dom": "^21.0.4",
|
|
62
62
|
"htmlparser2": "^12.0.0",
|
|
63
63
|
"nx": "23.3.0",
|
|
64
64
|
"react": "^19.3.0",
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
"rxjs": "^7.8.2",
|
|
69
69
|
"svelte": "^5.57.1",
|
|
70
70
|
"typescript": "^7.0.2",
|
|
71
|
-
"vite": "^8.3.
|
|
72
|
-
"vitest": "^4.1.
|
|
71
|
+
"vite": "^8.3.5",
|
|
72
|
+
"vitest": "^4.1.12",
|
|
73
73
|
"vue": "^3.5.43",
|
|
74
|
-
"zod": "^4.6.
|
|
74
|
+
"zod": "^4.6.7"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"msgpackr": "^2.1.0",
|
|
@@ -79,7 +79,11 @@
|
|
|
79
79
|
},
|
|
80
80
|
"overrides": {
|
|
81
81
|
"nx": {
|
|
82
|
-
"undici": "7.
|
|
82
|
+
"undici": "7.30.1",
|
|
83
|
+
"minimatch": "11.0.4"
|
|
84
|
+
},
|
|
85
|
+
"@nx/devkit": {
|
|
86
|
+
"minimatch": "11.0.4"
|
|
83
87
|
}
|
|
84
88
|
},
|
|
85
89
|
"repository": {
|
package/src/contract/types.ts
CHANGED
|
@@ -41,6 +41,25 @@ export interface MemoryPersistence {
|
|
|
41
41
|
stop(): void | Promise<void>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A worker lifecycle transition, emitted by pools and dedicated workers
|
|
46
|
+
* through `onLifecycle`/`onWorkerEvent` — the seam error boundaries build
|
|
47
|
+
* on. `'error'` fires when a worker's error event lands (before respawn);
|
|
48
|
+
* `'respawn'` when the replacement is attached; `'terminate'` when the
|
|
49
|
+
* runner tears down deliberately.
|
|
50
|
+
*/
|
|
51
|
+
export interface WorkerLifecycleEvent {
|
|
52
|
+
type: 'error' | 'respawn' | 'terminate';
|
|
53
|
+
/** The emitting runner's devtools correlation id ('worker-N'/'pool-N'). */
|
|
54
|
+
poolId: string;
|
|
55
|
+
/** Worker slot in a pool; always 0 for a dedicated worker. */
|
|
56
|
+
slot?: number;
|
|
57
|
+
/** The crash/error message on 'error' events. */
|
|
58
|
+
message?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type WorkerLifecycleListener = (event: WorkerLifecycleEvent) => void;
|
|
62
|
+
|
|
44
63
|
export interface WorkerPoolConfig<S extends SharedSpec = SharedSpec, T extends TaskMap = TaskMap> {
|
|
45
64
|
/** Worker entry as a URL — e.g. `new URL('./worker.ts', import.meta.url)`. */
|
|
46
65
|
workerUrl?: URL;
|
|
@@ -85,6 +104,13 @@ export interface WorkerPoolConfig<S extends SharedSpec = SharedSpec, T extends T
|
|
|
85
104
|
maxQueue?: number;
|
|
86
105
|
/** Default true — a worker 'error' rejects its in-flight tasks and spawns a replacement. */
|
|
87
106
|
respawn?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Lifecycle observer — called on worker 'error' (before respawn),
|
|
109
|
+
* 'respawn', and 'terminate'. Island boundaries subscribe through
|
|
110
|
+
* `client.onWorkerEvent` to detect crashes; config-level listeners get
|
|
111
|
+
* the same events.
|
|
112
|
+
*/
|
|
113
|
+
onLifecycle?: WorkerLifecycleListener;
|
|
88
114
|
/**
|
|
89
115
|
* Default per-task timeout in ms (per-call `options.timeout` overrides).
|
|
90
116
|
* The timer starts at enqueue, so it bounds queue-wait + execution together.
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { MemoryManager } from './memory';
|
|
2
2
|
import { TaskAbortedError, TaskTimeoutError, WorkerCrashedError } from './errors';
|
|
3
3
|
import type { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
MemoryPersistence,
|
|
6
|
+
TaskContract,
|
|
7
|
+
TaskMap,
|
|
8
|
+
TaskResult,
|
|
9
|
+
WorkerLifecycleEvent,
|
|
10
|
+
WorkerLifecycleListener,
|
|
11
|
+
WorkerPoolConfig,
|
|
12
|
+
} from '../contract/types';
|
|
5
13
|
import { devtoolsEnabled, emitDevtools, estimateCloneBytes, nextDevtoolsId, type EmittedDevtoolsEvent } from '../devtools';
|
|
6
14
|
import { fmtBytes, scoped } from '../log';
|
|
7
15
|
import { measureTaskCall } from '../userTiming';
|
|
@@ -66,6 +74,7 @@ export class DedicatedWorker<S extends SharedSpec = SharedSpec> {
|
|
|
66
74
|
private readonly mem: ChannelMemory<S>;
|
|
67
75
|
private readonly spawn: () => Worker;
|
|
68
76
|
private readonly respawn: boolean;
|
|
77
|
+
private readonly onLifecycle?: WorkerLifecycleListener;
|
|
69
78
|
private readonly taskTimeout?: number;
|
|
70
79
|
private readonly label?: string;
|
|
71
80
|
private worker: Worker | null = null;
|
|
@@ -87,6 +96,7 @@ export class DedicatedWorker<S extends SharedSpec = SharedSpec> {
|
|
|
87
96
|
const url = config.workerUrl;
|
|
88
97
|
this.spawn = config.createWorker ?? (() => new Worker(url!, { type: 'module' }));
|
|
89
98
|
this.respawn = config.respawn ?? true;
|
|
99
|
+
this.onLifecycle = config.onLifecycle;
|
|
90
100
|
this.taskTimeout = config.taskTimeout;
|
|
91
101
|
this.label = config.name;
|
|
92
102
|
this.poolId = nextDevtoolsId('worker', config.name);
|
|
@@ -178,12 +188,18 @@ export class DedicatedWorker<S extends SharedSpec = SharedSpec> {
|
|
|
178
188
|
this.checkDrained();
|
|
179
189
|
}
|
|
180
190
|
|
|
191
|
+
/** Fan out a lifecycle transition — error boundaries subscribe through this. */
|
|
192
|
+
private lifecycle(type: WorkerLifecycleEvent['type'], message?: string): void {
|
|
193
|
+
this.onLifecycle?.({ type, poolId: this.poolId, slot: 0, message });
|
|
194
|
+
}
|
|
195
|
+
|
|
181
196
|
private handleError(event: unknown): void {
|
|
182
197
|
if (this.closed) return;
|
|
183
198
|
const e = event as { message?: string; error?: Error } | undefined;
|
|
184
199
|
const message = e?.message ?? (e?.error ? String(e.error) : 'worker error');
|
|
185
200
|
workerLog.warn(`worker error: ${message}`);
|
|
186
201
|
emitDevtools({ type: 'worker:error', poolId: this.poolId, slot: 0, message });
|
|
202
|
+
this.lifecycle('error', message);
|
|
187
203
|
this.rejectPending(message);
|
|
188
204
|
this.worker?.terminate();
|
|
189
205
|
this.worker = null;
|
|
@@ -191,6 +207,7 @@ export class DedicatedWorker<S extends SharedSpec = SharedSpec> {
|
|
|
191
207
|
workerLog.info('respawning crashed worker');
|
|
192
208
|
this.attach(this.spawn());
|
|
193
209
|
emitDevtools({ type: 'worker:respawn', poolId: this.poolId, slot: 0 });
|
|
210
|
+
this.lifecycle('respawn');
|
|
194
211
|
}
|
|
195
212
|
this.checkDrained();
|
|
196
213
|
}
|
|
@@ -320,6 +337,7 @@ export class DedicatedWorker<S extends SharedSpec = SharedSpec> {
|
|
|
320
337
|
public terminate(): void {
|
|
321
338
|
if (this.closed) return;
|
|
322
339
|
this.closed = true;
|
|
340
|
+
this.lifecycle('terminate');
|
|
323
341
|
this.devtoolsOff?.();
|
|
324
342
|
void this.persistence?.stop()?.catch?.((e: unknown) => workerLog.warn('memory persistence stop failed', e));
|
|
325
343
|
workerLog.info('terminating dedicated worker');
|
package/src/pool/workerClient.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ServiceMethod, TaskRunner } from '../service';
|
|
2
2
|
import type { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
MemoryConfig,
|
|
5
|
+
Prettify,
|
|
6
|
+
WorkerLifecycleListener,
|
|
7
|
+
} from '../contract/types';
|
|
4
8
|
import { WorkerPool, type RunOptions } from './workerPool';
|
|
5
9
|
import { DedicatedWorker } from './dedicatedWorker';
|
|
6
10
|
import type {
|
|
@@ -123,6 +127,13 @@ export interface ConnectWorkerConfig<S extends SharedSpec> {
|
|
|
123
127
|
maxQueue?: number;
|
|
124
128
|
/** Default true — a crashed worker is replaced and its in-flight calls reject. */
|
|
125
129
|
respawn?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Lifecycle observer — fires on worker 'error' (before respawn),
|
|
132
|
+
* 'respawn', and 'terminate'. For per-callsite subscriptions (island
|
|
133
|
+
* boundaries, telemetry) prefer `client.onWorkerEvent(fn)`, which can be
|
|
134
|
+
* attached after connect and supports multiple listeners.
|
|
135
|
+
*/
|
|
136
|
+
onLifecycle?: WorkerLifecycleListener;
|
|
126
137
|
/** Default per-call timeout ms (`client.with({ timeout })` overrides). */
|
|
127
138
|
taskTimeout?: number;
|
|
128
139
|
/** Default true: pool spawns on first method call (or start()). False spawns immediately. */
|
|
@@ -153,6 +164,13 @@ export type WorkerClient<W extends WorkerDefinition, S extends SharedSpec> = Wor
|
|
|
153
164
|
*/
|
|
154
165
|
readonly pool: WorkerRunner<S> | null;
|
|
155
166
|
readonly sharedMemory: ConnectWorkerConfig<S>['sharedMemory'] | undefined;
|
|
167
|
+
/**
|
|
168
|
+
* Subscribe to worker lifecycle transitions ('error' | 'respawn' |
|
|
169
|
+
* 'terminate') — the crash-detection seam error boundaries build on.
|
|
170
|
+
* Listeners attach lazily: subscribing before the pool spawns still sees
|
|
171
|
+
* every event. Returns the unsubscribe.
|
|
172
|
+
*/
|
|
173
|
+
onWorkerEvent(fn: WorkerLifecycleListener): () => void;
|
|
156
174
|
};
|
|
157
175
|
|
|
158
176
|
/**
|
|
@@ -181,6 +199,9 @@ export function connectWorker<
|
|
|
181
199
|
S extends SharedSpec = W extends WorkerDefinition<infer X> ? X : SharedSpec,
|
|
182
200
|
>(config: ConnectWorkerConfig<S>): WorkerClient<W, S> {
|
|
183
201
|
let pool: WorkerRunner<S> | null = null;
|
|
202
|
+
// Lifecycle subscribers register before/after spawn identically — the set
|
|
203
|
+
// outlives any single runner, and the runner's config fan-out reads it.
|
|
204
|
+
const lifecycleSubs = new Set<WorkerLifecycleListener>();
|
|
184
205
|
const spawn = (): WorkerRunner<S> => {
|
|
185
206
|
if (pool) return pool;
|
|
186
207
|
const count = resolveWorkerCount(config.workers ?? config.poolSize);
|
|
@@ -195,6 +216,10 @@ export function connectWorker<
|
|
|
195
216
|
memory: config.memory,
|
|
196
217
|
respawn: config.respawn,
|
|
197
218
|
taskTimeout: config.taskTimeout,
|
|
219
|
+
onLifecycle: (event: Parameters<WorkerLifecycleListener>[0]) => {
|
|
220
|
+
config.onLifecycle?.(event);
|
|
221
|
+
for (const fn of [...lifecycleSubs]) fn(event);
|
|
222
|
+
},
|
|
198
223
|
};
|
|
199
224
|
pool =
|
|
200
225
|
count === 1
|
|
@@ -224,6 +249,10 @@ export function connectWorker<
|
|
|
224
249
|
get sharedMemory() {
|
|
225
250
|
return config.sharedMemory;
|
|
226
251
|
},
|
|
252
|
+
onWorkerEvent: (fn: WorkerLifecycleListener): (() => void) => {
|
|
253
|
+
lifecycleSubs.add(fn);
|
|
254
|
+
return () => lifecycleSubs.delete(fn);
|
|
255
|
+
},
|
|
227
256
|
};
|
|
228
257
|
|
|
229
258
|
return makeClientProxy([], spawn, reserved) as WorkerClient<W, S>;
|
package/src/pool/workerPool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MemoryManager } from './memory';
|
|
2
2
|
import { PoolQueueFullError, TaskAbortedError, TaskTimeoutError, WorkerCrashedError } from './errors';
|
|
3
3
|
import { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
|
|
4
|
-
import { MemoryPersistence, PoolTasks, TaskContract, TaskMap, TaskResult, WorkerPoolConfig } from '../contract/types';
|
|
4
|
+
import { MemoryPersistence, PoolTasks, TaskContract, TaskMap, TaskResult, WorkerLifecycleEvent, WorkerLifecycleListener, WorkerPoolConfig } from '../contract/types';
|
|
5
5
|
import { devtoolsEnabled, emitDevtools, estimateCloneBytes, nextDevtoolsId, type EmittedDevtoolsEvent } from '../devtools';
|
|
6
6
|
import { measureTaskCall } from '../userTiming';
|
|
7
7
|
import { forwardWorkerDevtools, sendInit, setupChannelMemory, type ChannelMemory } from './workerChannel';
|
|
@@ -98,6 +98,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
98
98
|
private readonly concurrency: number;
|
|
99
99
|
private readonly maxQueue: number;
|
|
100
100
|
private readonly respawn: boolean;
|
|
101
|
+
private readonly onLifecycle?: WorkerLifecycleListener;
|
|
101
102
|
private readonly taskTimeout?: number;
|
|
102
103
|
private closed = false;
|
|
103
104
|
/** Dashboard fault injection (`pool.chaos`); null unless devtools set it. */
|
|
@@ -133,6 +134,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
133
134
|
this.concurrency = config.concurrency ?? 1;
|
|
134
135
|
this.maxQueue = config.maxQueue ?? Infinity;
|
|
135
136
|
this.respawn = config.respawn ?? true;
|
|
137
|
+
this.onLifecycle = config.onLifecycle;
|
|
136
138
|
this.taskTimeout = config.taskTimeout;
|
|
137
139
|
this.label = config.name;
|
|
138
140
|
this.poolId = nextDevtoolsId('pool', config.name);
|
|
@@ -264,6 +266,11 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
264
266
|
this.checkDrained();
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
/** Fan out a lifecycle transition — error boundaries subscribe through this. */
|
|
270
|
+
private lifecycle(type: WorkerLifecycleEvent['type'], slot?: number, message?: string): void {
|
|
271
|
+
this.onLifecycle?.({ type, poolId: this.poolId, slot, message });
|
|
272
|
+
}
|
|
273
|
+
|
|
267
274
|
private handleWorkerError(slot: WorkerSlot, event: unknown) {
|
|
268
275
|
if (this.closed) return; // pool terminated — a late error event can't respawn
|
|
269
276
|
const e = event as { message?: string; error?: Error } | undefined;
|
|
@@ -271,6 +278,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
271
278
|
poolLog.warn(`worker error: ${message}`);
|
|
272
279
|
const idx = this.slots.indexOf(slot);
|
|
273
280
|
emitDevtools({ type: 'worker:error', poolId: this.poolId, slot: idx, message });
|
|
281
|
+
this.lifecycle('error', idx >= 0 ? idx : undefined, message);
|
|
274
282
|
|
|
275
283
|
// Reject the dead worker's in-flight tasks. Orphaned (already
|
|
276
284
|
// aborted/timed-out) entries are dropped without re-counting.
|
|
@@ -293,6 +301,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
293
301
|
poolLog.info('respawning crashed worker');
|
|
294
302
|
this.addWorker(this.spawn());
|
|
295
303
|
emitDevtools({ type: 'worker:respawn', poolId: this.poolId, slot: this.slots.length - 1 });
|
|
304
|
+
this.lifecycle('respawn', this.slots.length - 1);
|
|
296
305
|
} else if (this.slots.length === 0) {
|
|
297
306
|
// No workers left to ever serve the queue.
|
|
298
307
|
for (const entry of this.queue.splice(0)) {
|
|
@@ -495,6 +504,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
495
504
|
|
|
496
505
|
public terminate(): void {
|
|
497
506
|
this.closed = true;
|
|
507
|
+
this.lifecycle('terminate');
|
|
498
508
|
this.devtoolsOff?.();
|
|
499
509
|
// Final flush + release — fire-and-forget; terminate() stays synchronous.
|
|
500
510
|
void this.persistence?.stop()?.catch?.((e: unknown) =>
|