@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.
Files changed (40) hide show
  1. package/dist/contract/listSchema.js +1 -2
  2. package/dist/contract/memoryDevtools.js +2 -3
  3. package/dist/contract/msgpackrCodec.js +1 -2
  4. package/dist/contract/reef.js +1 -2
  5. package/dist/contract/sharedMemory.js +61 -62
  6. package/dist/contract/types.d.ts +24 -0
  7. package/dist/contract/zod.js +1 -2
  8. package/dist/devtools.js +32 -39
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +22 -22
  11. package/dist/log.js +1 -2
  12. package/dist/observable.js +18 -19
  13. package/dist/pool/dedicatedWorker.d.ts +3 -0
  14. package/dist/pool/dedicatedWorker.js +72 -68
  15. package/dist/pool/devtoolsCommands.js +1 -2
  16. package/dist/pool/errors.js +1 -2
  17. package/dist/pool/memory.js +1 -2
  18. package/dist/pool/subWorkerClient.js +1 -2
  19. package/dist/pool/workerChannel.js +21 -22
  20. package/dist/pool/workerClient.d.ts +15 -1
  21. package/dist/pool/workerClient.js +14 -10
  22. package/dist/pool/workerPool.d.ts +3 -0
  23. package/dist/pool/workerPool.js +119 -121
  24. package/dist/reactive.js +1 -2
  25. package/dist/reactiveGraph.js +1 -2
  26. package/dist/service.js +1 -2
  27. package/dist/shared/sharedWorkerClient.js +2 -9
  28. package/dist/shared/sharedWorkerHost.js +22 -27
  29. package/dist/task.js +1 -2
  30. package/dist/testing/inProcessWorker.js +3 -8
  31. package/dist/userTiming.js +1 -2
  32. package/dist/worker/defineWorker.js +8 -9
  33. package/dist/worker/registry.js +1 -2
  34. package/dist/worker/workerBootstrap.js +40 -52
  35. package/package.json +11 -7
  36. package/src/contract/types.ts +26 -0
  37. package/src/index.ts +2 -0
  38. package/src/pool/dedicatedWorker.ts +19 -1
  39. package/src/pool/workerClient.ts +30 -1
  40. package/src/pool/workerPool.ts +11 -1
@@ -1,11 +1,11 @@
1
- import { devtoolsEnabled as e, emitDevtools as t, estimateCloneBytes as n, nextDevtoolsId as r } from "../devtools.js";
2
- import { fmtBytes as i, scoped as a } from "../log.js";
3
- import { TaskAbortedError as o, TaskTimeoutError as s, WorkerCrashedError as c } from "./errors.js";
1
+ import { TaskAbortedError as e, TaskTimeoutError as t, WorkerCrashedError as n } from "./errors.js";
2
+ import { devtoolsEnabled as r, emitDevtools as i, estimateCloneBytes as a, nextDevtoolsId as o } from "../devtools.js";
3
+ import { fmtBytes as s, scoped as c } from "../log.js";
4
4
  import { measureTaskCall as l } from "../userTiming.js";
5
5
  import { forwardWorkerDevtools as u, sendInit as d, setupChannelMemory as f } from "./workerChannel.js";
6
6
  import { CHAOS_FAILURE as p, KILL_MESSAGE as m, applyChaos as h, registerDevtoolsRunner as g } from "./devtoolsCommands.js";
7
- //#region src/pool/dedicatedWorker.ts
8
- var _ = a("worker"), v = a("task"), y = () => ({
7
+ // src/pool/dedicatedWorker.ts
8
+ var _ = c("worker"), v = c("task"), y = () => ({
9
9
  count: 0,
10
10
  mean: 0,
11
11
  max: 0,
@@ -19,6 +19,7 @@ var _ = a("worker"), v = a("task"), y = () => ({
19
19
  mem;
20
20
  spawn;
21
21
  respawn;
22
+ onLifecycle;
22
23
  taskTimeout;
23
24
  label;
24
25
  worker = null;
@@ -35,20 +36,20 @@ var _ = a("worker"), v = a("task"), y = () => ({
35
36
  aborted: 0,
36
37
  runMs: y()
37
38
  };
38
- constructor(n) {
39
- if (this.mem = f(n, "DedicatedWorker"), !n.workerUrl && !n.createWorker) throw Error("DedicatedWorker requires either `workerUrl` or `createWorker` in its config.");
40
- let a = n.workerUrl;
41
- this.spawn = n.createWorker ?? (() => new Worker(a, { type: "module" })), this.respawn = n.respawn ?? !0, this.taskTimeout = n.taskTimeout, this.label = n.name, this.poolId = r("worker", n.name), this.sharedMemory = this.mem.sharedMemory, this.persistence = this.mem.persistence;
42
- let o = this.mem.sharedMemory?.totalBytes ?? this.mem.injectedBuffer?.byteLength;
43
- _.info(o === void 0 ? "spawning 1 dedicated worker, message-only" : `spawning 1 dedicated worker with ${i(o)} shared buffer`), t({
39
+ constructor(e) {
40
+ if (this.mem = f(e, "DedicatedWorker"), !e.workerUrl && !e.createWorker) throw Error("DedicatedWorker requires either `workerUrl` or `createWorker` in its config.");
41
+ let t = e.workerUrl;
42
+ this.spawn = e.createWorker ?? (() => new Worker(t, { type: "module" })), this.respawn = e.respawn ?? !0, this.onLifecycle = e.onLifecycle, this.taskTimeout = e.taskTimeout, this.label = e.name, this.poolId = o("worker", e.name), this.sharedMemory = this.mem.sharedMemory, this.persistence = this.mem.persistence;
43
+ let n = this.mem.sharedMemory?.totalBytes ?? this.mem.injectedBuffer?.byteLength;
44
+ _.info(n === void 0 ? "spawning 1 dedicated worker, message-only" : `spawning 1 dedicated worker with ${s(n)} shared buffer`), i({
44
45
  type: "pool:init",
45
46
  poolId: this.poolId,
46
47
  label: this.label,
47
48
  poolSize: 1,
48
49
  concurrency: 0,
49
- memoryBytes: o,
50
+ memoryBytes: n,
50
51
  dedicated: !0
51
- }), this.attach(this.spawn()), e() && (this.devtoolsOff = g({
52
+ }), this.attach(this.spawn()), r() && (this.devtoolsOff = g({
52
53
  poolId: this.poolId,
53
54
  label: this.label,
54
55
  dedicated: !0,
@@ -68,16 +69,12 @@ var _ = a("worker"), v = a("task"), y = () => ({
68
69
  this.worker = e, e.addEventListener("message", (t) => {
69
70
  if (this.worker !== e) return;
70
71
  let n = t.data;
71
- if (n.type === "ATOLL_DEVTOOLS" && n.event) {
72
- u(this.poolId, 0, n.event);
73
- return;
74
- }
75
- this.handleReply(n);
72
+ n.type === "ATOLL_DEVTOOLS" && n.event ? u(this.poolId, 0, n.event) : this.handleReply(n);
76
73
  }), e.addEventListener("error", (t) => {
77
74
  this.worker === e && this.handleError(t);
78
75
  });
79
- let n = d(e, this.mem);
80
- _.debug(`dedicated worker spawned, ${n} sent`), t({
76
+ let t = d(e, this.mem);
77
+ _.debug(`dedicated worker spawned, ${t} sent`), i({
81
78
  type: "worker:spawn",
82
79
  poolId: this.poolId,
83
80
  slot: 0
@@ -86,116 +83,124 @@ var _ = a("worker"), v = a("task"), y = () => ({
86
83
  detach(e) {
87
84
  e.timer && clearTimeout(e.timer), e.signal && e.onSignalAbort && e.signal.removeEventListener("abort", e.onSignalAbort);
88
85
  }
89
- handleReply(r) {
90
- let i = r.messageId === void 0 ? void 0 : this.pending.get(r.messageId);
91
- if (i) {
92
- if (this.pending.delete(r.messageId), this.detach(i), !i.done) {
93
- i.done = !0;
94
- let a = performance.now() - i.sentAt;
95
- this.statsAgg.runMs.add(a), r.success ? (this.statsAgg.completed++, v.debug(`← ${i.contract.taskId} ok`), e() && (t({
86
+ handleReply(e) {
87
+ let t = e.messageId === void 0 ? void 0 : this.pending.get(e.messageId);
88
+ if (t) {
89
+ if (this.pending.delete(e.messageId), this.detach(t), !t.done) {
90
+ t.done = !0;
91
+ let n = performance.now() - t.sentAt;
92
+ this.statsAgg.runMs.add(n), e.success ? (this.statsAgg.completed++, v.debug(`← ${t.contract.taskId} ok`), r() && (i({
96
93
  type: "task:settle",
97
94
  poolId: this.poolId,
98
- callId: i.callId,
99
- taskId: i.contract.taskId,
95
+ callId: t.callId,
96
+ taskId: t.contract.taskId,
100
97
  outcome: "ok",
101
- runMs: a,
102
- resultBytes: n(r.result)
103
- }), l(this.poolId, i.contract.taskId, i.callId, 0, i.sentAt, i.sentAt + a, !0)), i.resolve(r.result)) : (this.statsAgg.failed++, v.warn(`← ${i.contract.taskId} failed: ${r.error}`), e() && (t({
98
+ runMs: n,
99
+ resultBytes: a(e.result)
100
+ }), l(this.poolId, t.contract.taskId, t.callId, 0, t.sentAt, t.sentAt + n, !0)), t.resolve(e.result)) : (this.statsAgg.failed++, v.warn(`← ${t.contract.taskId} failed: ${e.error}`), r() && (i({
104
101
  type: "task:settle",
105
102
  poolId: this.poolId,
106
- callId: i.callId,
107
- taskId: i.contract.taskId,
103
+ callId: t.callId,
104
+ taskId: t.contract.taskId,
108
105
  outcome: "error",
109
- runMs: a,
110
- error: r.error
111
- }), l(this.poolId, i.contract.taskId, i.callId, 0, i.sentAt, i.sentAt + a, !1)), i.reject(Error(r.error)));
106
+ runMs: n,
107
+ error: e.error
108
+ }), l(this.poolId, t.contract.taskId, t.callId, 0, t.sentAt, t.sentAt + n, !1)), t.reject(Error(e.error)));
112
109
  }
113
110
  this.checkDrained();
114
111
  }
115
112
  }
113
+ lifecycle(e, t) {
114
+ this.onLifecycle?.({
115
+ type: e,
116
+ poolId: this.poolId,
117
+ slot: 0,
118
+ message: t
119
+ });
120
+ }
116
121
  handleError(e) {
117
122
  if (this.closed) return;
118
- let n = e, r = n?.message ?? (n?.error ? String(n.error) : "worker error");
119
- _.warn(`worker error: ${r}`), t({
123
+ let t = e, n = t?.message ?? (t?.error ? String(t.error) : "worker error");
124
+ _.warn(`worker error: ${n}`), i({
120
125
  type: "worker:error",
121
126
  poolId: this.poolId,
122
127
  slot: 0,
123
- message: r
124
- }), this.rejectPending(r), this.worker?.terminate(), this.worker = null, this.respawn && (_.info("respawning crashed worker"), this.attach(this.spawn()), t({
128
+ message: n
129
+ }), this.lifecycle("error", n), this.rejectPending(n), this.worker?.terminate(), this.worker = null, this.respawn && (_.info("respawning crashed worker"), this.attach(this.spawn()), i({
125
130
  type: "worker:respawn",
126
131
  poolId: this.poolId,
127
132
  slot: 0
128
- })), this.checkDrained();
133
+ }), this.lifecycle("respawn")), this.checkDrained();
129
134
  }
130
135
  rejectPending(e) {
131
- let n = new c(e);
132
- for (let r of this.pending.values()) this.detach(r), !r.done && (r.done = !0, this.statsAgg.failed++, t({
136
+ let t = new n(e);
137
+ for (let n of this.pending.values()) this.detach(n), n.done || (n.done = !0, this.statsAgg.failed++, i({
133
138
  type: "task:settle",
134
139
  poolId: this.poolId,
135
- callId: r.callId,
136
- taskId: r.contract.taskId,
140
+ callId: n.callId,
141
+ taskId: n.contract.taskId,
137
142
  outcome: "crashed",
138
143
  error: e
139
- }), r.reject(n));
144
+ }), n.reject(t));
140
145
  this.pending.clear();
141
146
  }
142
147
  cancel(e, n) {
143
- e.done || (e.done = !0, this.detach(e), this.statsAgg.aborted++, t({
148
+ e.done || (e.done = !0, this.detach(e), this.statsAgg.aborted++, i({
144
149
  type: "task:settle",
145
150
  poolId: this.poolId,
146
151
  callId: e.callId,
147
152
  taskId: e.contract.taskId,
148
- outcome: n instanceof s ? "timeout" : "aborted"
153
+ outcome: n instanceof t ? "timeout" : "aborted"
149
154
  }), e.reject(n));
150
155
  }
151
156
  checkDrained() {
152
157
  if (!(this.pending.size > 0)) for (let e of this.drainWaiters.splice(0)) e();
153
158
  }
154
- dispatch(r, i, a) {
159
+ dispatch(o, s, c) {
155
160
  let l = this.worker;
156
- return this.closed || l === null ? Promise.reject(new c("DedicatedWorker has been terminated — no worker available.")) : a?.signal?.aborted ? Promise.reject(new o()) : new Promise((c, u) => {
161
+ return this.closed || l === null ? Promise.reject(new n("DedicatedWorker has been terminated — no worker available.")) : c?.signal?.aborted ? Promise.reject(new e()) : new Promise((n, u) => {
157
162
  let d = {
158
- contract: r,
163
+ contract: o,
159
164
  callId: ++this.nextCallId,
160
165
  sentAt: performance.now(),
161
- resolve: c,
166
+ resolve: n,
162
167
  reject: u,
163
168
  done: !1,
164
- signal: a?.signal
169
+ signal: c?.signal
165
170
  };
166
- a?.signal && (d.onSignalAbort = () => this.cancel(d, new o()), a.signal.addEventListener("abort", d.onSignalAbort, { once: !0 }));
167
- let f = a?.timeout ?? this.taskTimeout;
168
- f !== void 0 && (d.timer = setTimeout(() => this.cancel(d, new s()), f));
171
+ c?.signal && (d.onSignalAbort = () => this.cancel(d, new e()), c.signal.addEventListener("abort", d.onSignalAbort, { once: !0 }));
172
+ let f = c?.timeout ?? this.taskTimeout;
173
+ f !== void 0 && (d.timer = setTimeout(() => this.cancel(d, new t()), f));
169
174
  let m = this.nextMessageId++;
170
- this.pending.set(m, d), v.debug(`→ ${r.taskId}`, { args: i }), t({
175
+ this.pending.set(m, d), v.debug(`→ ${o.taskId}`, { args: s }), i({
171
176
  type: "task:enqueue",
172
177
  poolId: this.poolId,
173
178
  callId: d.callId,
174
- taskId: r.taskId
175
- }), e() && t({
179
+ taskId: o.taskId
180
+ }), r() && i({
176
181
  type: "task:dispatch",
177
182
  poolId: this.poolId,
178
183
  callId: d.callId,
179
- taskId: r.taskId,
184
+ taskId: o.taskId,
180
185
  slot: 0,
181
186
  waitMs: 0,
182
- argBytes: n(i)
187
+ argBytes: a(s)
183
188
  });
184
189
  let g = {
185
190
  type: "EXECUTE_TASK",
186
191
  messageId: m,
187
- taskId: r.taskId,
188
- args: i
192
+ taskId: o.taskId,
193
+ args: s
189
194
  };
190
195
  this.chaos ? h(this.chaos, {
191
196
  live: () => this.pending.get(m) === d,
192
197
  fail: () => this.handleReply({
193
198
  messageId: m,
194
- taskId: r.taskId,
199
+ taskId: o.taskId,
195
200
  success: !1,
196
201
  error: p
197
202
  }),
198
- timeout: () => this.cancel(d, new s()),
203
+ timeout: () => this.cancel(d, new t()),
199
204
  post: () => l.postMessage(g)
200
205
  }) : l.postMessage(g);
201
206
  });
@@ -239,11 +244,10 @@ var _ = a("worker"), v = a("task"), y = () => ({
239
244
  });
240
245
  }
241
246
  terminate() {
242
- this.closed || (this.closed = !0, this.devtoolsOff?.(), this.persistence?.stop()?.catch?.((e) => _.warn("memory persistence stop failed", e)), _.info("terminating dedicated worker"), t({
247
+ this.closed || (this.closed = !0, this.lifecycle("terminate"), this.devtoolsOff?.(), this.persistence?.stop()?.catch?.((e) => _.warn("memory persistence stop failed", e)), _.info("terminating dedicated worker"), i({
243
248
  type: "pool:terminate",
244
249
  poolId: this.poolId
245
250
  }), this.rejectPending("worker terminated"), this.worker?.terminate(), this.worker = null, this.checkDrained());
246
251
  }
247
252
  };
248
- //#endregion
249
253
  export { b as DedicatedWorker };
@@ -1,5 +1,5 @@
1
1
  import { registerDevtoolsCommand as e } from "../devtools.js";
2
- //#region src/pool/devtoolsCommands.ts
2
+ // src/pool/devtoolsCommands.ts
3
3
  var t = "chaos: injected failure", n = "killed from devtools", r = /* @__PURE__ */ new Map(), i = null, a = (e) => {
4
4
  let t = e.poolId;
5
5
  if (typeof t != "string") throw Error("poolId (string) is required");
@@ -55,5 +55,4 @@ var t = "chaos: injected failure", n = "killed from devtools", r = /* @__PURE__
55
55
  }
56
56
  }, e.delayMs ?? 0);
57
57
  };
58
- //#endregion
59
58
  export { t as CHAOS_FAILURE, n as KILL_MESSAGE, d as applyChaos, s as parseChaos, u as registerDevtoolsRunner };
@@ -1,4 +1,4 @@
1
- //#region src/pool/errors.ts
1
+ // src/pool/errors.ts
2
2
  var e = class extends Error {
3
3
  constructor(e = "WorkerPool task queue is full") {
4
4
  super(e), this.name = "PoolQueueFullError";
@@ -16,5 +16,4 @@ var e = class extends Error {
16
16
  super(e), this.name = "WorkerCrashedError";
17
17
  }
18
18
  };
19
- //#endregion
20
19
  export { e as PoolQueueFullError, t as TaskAbortedError, n as TaskTimeoutError, r as WorkerCrashedError };
@@ -1,4 +1,4 @@
1
- //#region src/pool/memory.ts
1
+ // src/pool/memory.ts
2
2
  var e = class {
3
3
  memory;
4
4
  maxPages;
@@ -31,5 +31,4 @@ var e = class {
31
31
  return this.memory.buffer;
32
32
  }
33
33
  };
34
- //#endregion
35
34
  export { e as MemoryManager };
@@ -1,5 +1,5 @@
1
1
  import { connectWorker as e } from "./workerClient.js";
2
- //#region src/pool/subWorkerClient.ts
2
+ // src/pool/subWorkerClient.ts
3
3
  function t(t) {
4
4
  let n = { ...t };
5
5
  return typeof t.worker != "function" && (n.worker = () => {
@@ -7,5 +7,4 @@ function t(t) {
7
7
  return new Worker(t.worker, { type: "module" });
8
8
  }), e(n);
9
9
  }
10
- //#endregion
11
10
  export { t as connectSubWorker };
@@ -1,50 +1,49 @@
1
- import { devtoolsEnabled as e, forwardDevtools as t } from "../devtools.js";
2
- import { MemoryManager as n } from "./memory.js";
3
- //#region src/pool/workerChannel.ts
4
- function r(e, t) {
5
- if ((e.sharedMemory || e.sharedBuffer !== void 0) && (typeof SharedArrayBuffer > "u" || typeof crossOriginIsolated < "u" && !crossOriginIsolated)) throw Error(`${t} requires SharedArrayBuffer in a cross-origin isolated context — serve the page with COOP/COEP headers.`);
1
+ import { MemoryManager as e } from "./memory.js";
2
+ import { devtoolsEnabled as t, forwardDevtools as n } from "../devtools.js";
3
+ // src/pool/workerChannel.ts
4
+ function r(t, n) {
5
+ if ((t.sharedMemory || t.sharedBuffer !== void 0) && (typeof SharedArrayBuffer > "u" || typeof crossOriginIsolated < "u" && !crossOriginIsolated)) throw Error(`${n} requires SharedArrayBuffer in a cross-origin isolated context — serve the page with COOP/COEP headers.`);
6
6
  let r;
7
- if (e.sharedBuffer !== void 0 && (r = typeof e.sharedBuffer == "function" ? e.sharedBuffer() : e.sharedBuffer, !r)) throw Error(`${t}: \`sharedBuffer\` resolved to an empty buffer — the producing pool or contract has no bound memory yet.`);
8
- if (!e.sharedMemory) return {
7
+ if (t.sharedBuffer !== void 0 && (r = typeof t.sharedBuffer == "function" ? t.sharedBuffer() : t.sharedBuffer, !r)) throw Error(`${n}: \`sharedBuffer\` resolved to an empty buffer — the producing pool or contract has no bound memory yet.`);
8
+ if (!t.sharedMemory) return {
9
9
  sharedMemory: void 0,
10
10
  injectedBuffer: r,
11
11
  persistence: void 0
12
12
  };
13
- let i = e.sharedMemory, a;
14
- return r ? i.bind(r) : (a = new n(e.memory), a.ensureCapacity(i.totalBytes), i.bind(a.getBuffer())), {
13
+ let i = t.sharedMemory, a;
14
+ return r ? i.bind(r) : (a = new e(t.memory), a.ensureCapacity(i.totalBytes), i.bind(a.getBuffer())), {
15
15
  sharedMemory: i,
16
16
  memoryManager: a,
17
17
  injectedBuffer: r,
18
- persistence: e.persistence?.(i)
18
+ persistence: t.persistence?.(i)
19
19
  };
20
20
  }
21
- function i(t, n) {
22
- return n.memoryManager ? (t.postMessage({
21
+ function i(e, n) {
22
+ return n.memoryManager ? (e.postMessage({
23
23
  type: "INIT_MEMORY",
24
24
  memory: n.memoryManager.memory,
25
- devtools: e()
26
- }), "INIT_MEMORY") : n.injectedBuffer ? (t.postMessage({
25
+ devtools: t()
26
+ }), "INIT_MEMORY") : n.injectedBuffer ? (e.postMessage({
27
27
  type: "INIT_MEMORY",
28
28
  buffer: n.injectedBuffer,
29
- devtools: e()
30
- }), "INIT_MEMORY") : (t.postMessage({
29
+ devtools: t()
30
+ }), "INIT_MEMORY") : (e.postMessage({
31
31
  type: "INIT",
32
- devtools: e()
32
+ devtools: t()
33
33
  }), "INIT");
34
34
  }
35
- function a(e, n, r) {
36
- let i = (t) => `${e}#${n}~${t}`;
37
- t({
35
+ function a(e, t, r) {
36
+ let i = (n) => `${e}#${t}~${n}`;
37
+ n({
38
38
  ...r,
39
39
  ...r.poolId === void 0 ? {} : { poolId: i(r.poolId) },
40
40
  worker: r.worker === void 0 ? {
41
41
  poolId: e,
42
- slot: n
42
+ slot: t
43
43
  } : {
44
44
  poolId: i(r.worker.poolId),
45
45
  slot: r.worker.slot
46
46
  }
47
47
  });
48
48
  }
49
- //#endregion
50
49
  export { a as forwardWorkerDevtools, i as sendInit, r as setupChannelMemory };
@@ -1,6 +1,6 @@
1
1
  import type { ServiceMethod, TaskRunner } from '../service.js';
2
2
  import type { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory.js';
3
- import type { MemoryConfig, Prettify } from '../contract/types.js';
3
+ import type { MemoryConfig, Prettify, WorkerLifecycleListener } from '../contract/types.js';
4
4
  import { WorkerPool, type RunOptions } from './workerPool.js';
5
5
  import { DedicatedWorker } from './dedicatedWorker.js';
6
6
  import type { WorkerDefinition, WorkerMethodMap } from '../worker/defineWorker.js';
@@ -62,6 +62,13 @@ export interface ConnectWorkerConfig<S extends SharedSpec> {
62
62
  maxQueue?: number;
63
63
  /** Default true — a crashed worker is replaced and its in-flight calls reject. */
64
64
  respawn?: boolean;
65
+ /**
66
+ * Lifecycle observer — fires on worker 'error' (before respawn),
67
+ * 'respawn', and 'terminate'. For per-callsite subscriptions (island
68
+ * boundaries, telemetry) prefer `client.onWorkerEvent(fn)`, which can be
69
+ * attached after connect and supports multiple listeners.
70
+ */
71
+ onLifecycle?: WorkerLifecycleListener;
65
72
  /** Default per-call timeout ms (`client.with({ timeout })` overrides). */
66
73
  taskTimeout?: number;
67
74
  /** Default true: pool spawns on first method call (or start()). False spawns immediately. */
@@ -84,6 +91,13 @@ export type WorkerClient<W extends WorkerDefinition, S extends SharedSpec> = Wor
84
91
  */
85
92
  readonly pool: WorkerRunner<S> | null;
86
93
  readonly sharedMemory: ConnectWorkerConfig<S>['sharedMemory'] | undefined;
94
+ /**
95
+ * Subscribe to worker lifecycle transitions ('error' | 'respawn' |
96
+ * 'terminate') — the crash-detection seam error boundaries build on.
97
+ * Listeners attach lazily: subscribing before the pool spawns still sees
98
+ * every event. Returns the unsubscribe.
99
+ */
100
+ onWorkerEvent(fn: WorkerLifecycleListener): () => void;
87
101
  };
88
102
  /**
89
103
  * One-line worker hookup for the main thread — the WorkerPool boilerplate
@@ -1,6 +1,6 @@
1
1
  import { WorkerPool as e } from "./workerPool.js";
2
2
  import { DedicatedWorker as t } from "./dedicatedWorker.js";
3
- //#region src/pool/workerClient.ts
3
+ // src/pool/workerClient.ts
4
4
  var n = (e, t, r, i) => {
5
5
  let a = /* @__PURE__ */ new Map();
6
6
  return new Proxy(() => {}, {
@@ -23,27 +23,31 @@ function r(e) {
23
23
  }
24
24
  var i = (e) => e === "auto" ? typeof navigator < "u" ? navigator.hardwareConcurrency ?? 4 : 4 : Math.max(1, Math.floor(e ?? 1));
25
25
  function a(r) {
26
- let a = null, o = () => {
26
+ let a = null, o = /* @__PURE__ */ new Set(), s = () => {
27
27
  if (a) return a;
28
- let n = i(r.workers ?? r.poolSize), o = {
28
+ let n = i(r.workers ?? r.poolSize), s = {
29
29
  sharedMemory: r.sharedMemory,
30
30
  sharedBuffer: r.sharedBuffer,
31
31
  createWorker: typeof r.worker == "function" ? r.worker : () => new Worker(r.worker, { type: "module" }),
32
32
  name: r.name,
33
33
  memory: r.memory,
34
34
  respawn: r.respawn,
35
- taskTimeout: r.taskTimeout
35
+ taskTimeout: r.taskTimeout,
36
+ onLifecycle: (e) => {
37
+ r.onLifecycle?.(e);
38
+ for (let t of [...o]) t(e);
39
+ }
36
40
  };
37
- return a = n === 1 ? new t(o) : new e({
38
- ...o,
41
+ return a = n === 1 ? new t(s) : new e({
42
+ ...s,
39
43
  poolSize: n,
40
44
  concurrency: r.concurrency,
41
45
  maxQueue: r.maxQueue
42
46
  }), a;
43
47
  };
44
- return r.lazy === !1 && o(), n([], o, {
48
+ return r.lazy === !1 && s(), n([], s, {
45
49
  start: () => {
46
- o();
50
+ s();
47
51
  },
48
52
  terminate: () => {
49
53
  a?.terminate(), a = null;
@@ -53,8 +57,8 @@ function a(r) {
53
57
  },
54
58
  get sharedMemory() {
55
59
  return r.sharedMemory;
56
- }
60
+ },
61
+ onWorkerEvent: (e) => (o.add(e), () => o.delete(e))
57
62
  });
58
63
  }
59
- //#endregion
60
64
  export { a as connectWorker, i as resolveWorkerCount, r as workerClient };
@@ -51,6 +51,7 @@ declare class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
51
51
  private readonly concurrency;
52
52
  private readonly maxQueue;
53
53
  private readonly respawn;
54
+ private readonly onLifecycle?;
54
55
  private readonly taskTimeout?;
55
56
  private closed;
56
57
  /** Dashboard fault injection (`pool.chaos`); null unless devtools set it. */
@@ -72,6 +73,8 @@ declare class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
72
73
  /** Adds a spawned worker: one message + one error listener, then the INIT handshake. */
73
74
  private addWorker;
74
75
  private handleReply;
76
+ /** Fan out a lifecycle transition — error boundaries subscribe through this. */
77
+ private lifecycle;
75
78
  private handleWorkerError;
76
79
  /** Least-busy worker below its concurrency cap, if any. */
77
80
  private pickWorker;