@atolljs/node 0.1.6 → 0.1.8

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,6 +1,93 @@
1
+ // ../../src/devtools.ts
2
+ var THREAD = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope ? "worker" : "main";
3
+ var sink = null;
4
+ var emitDevtools = (event) => {
5
+ if (sink) sink({ ...event, at: performance.now(), thread: THREAD });
6
+ };
7
+ var forwardDevtools = (event) => {
8
+ if (sink) sink(event);
9
+ };
10
+ var devtoolsEnabled = () => sink !== null;
11
+ var previewValue = (value, max = 2048) => {
12
+ const seen = /* @__PURE__ */ new WeakSet();
13
+ let out;
14
+ try {
15
+ out = JSON.stringify(value, (_k, v) => {
16
+ if (typeof v === "function") return "[fn]";
17
+ if (typeof v === "bigint") return `${v}n`;
18
+ if (v instanceof ArrayBuffer) return `[ArrayBuffer ${v.byteLength}B]`;
19
+ if (ArrayBuffer.isView(v)) return `[${v.constructor.name} ${v.byteLength}B]`;
20
+ if (v instanceof Map) return { "[Map]": [...v.entries()].slice(0, 50) };
21
+ if (v instanceof Set) return { "[Set]": [...v].slice(0, 50) };
22
+ if (v && typeof v === "object") {
23
+ if (seen.has(v)) return "[cycle]";
24
+ seen.add(v);
25
+ }
26
+ return v;
27
+ }) ?? String(value);
28
+ } catch {
29
+ out = String(value);
30
+ }
31
+ return out.length > max ? `${out.slice(0, max)}\u2026` : out;
32
+ };
33
+ var estimateCloneBytes = (value, budget = 5e3) => {
34
+ let nodes = 0;
35
+ const seen = /* @__PURE__ */ new WeakSet();
36
+ const walk = (v) => {
37
+ if (++nodes > budget) return 0;
38
+ switch (typeof v) {
39
+ case "string":
40
+ return 4 + v.length * 2;
41
+ case "number":
42
+ return 8;
43
+ case "boolean":
44
+ return 4;
45
+ case "bigint":
46
+ return 16;
47
+ case "undefined":
48
+ return 1;
49
+ case "object": {
50
+ if (v === null) return 1;
51
+ if (v instanceof ArrayBuffer) return v.byteLength;
52
+ if (ArrayBuffer.isView(v)) return v.byteLength;
53
+ if (typeof SharedArrayBuffer !== "undefined" && v instanceof SharedArrayBuffer) return 8;
54
+ if (seen.has(v)) return 4;
55
+ seen.add(v);
56
+ let n = 8;
57
+ if (v instanceof Map) for (const [k, x] of v) n += walk(k) + walk(x);
58
+ else if (v instanceof Set) for (const x of v) n += walk(x);
59
+ else if (Array.isArray(v)) for (const x of v) n += walk(x);
60
+ else for (const k of Object.keys(v)) n += 4 + k.length * 2 + walk(v[k]);
61
+ return n;
62
+ }
63
+ default:
64
+ return 0;
65
+ }
66
+ };
67
+ return walk(value);
68
+ };
69
+ var commands = /* @__PURE__ */ new Map();
70
+ var registerDevtoolsCommand = (name, handler) => {
71
+ commands.set(name, handler);
72
+ return () => {
73
+ if (commands.get(name) === handler) commands.delete(name);
74
+ };
75
+ };
76
+ var BODY_PREVIEW = 8 * 1024;
77
+ var seq = 0;
78
+ var named = /* @__PURE__ */ new Map();
79
+ var nextDevtoolsId = (prefix, name) => {
80
+ const slug = name?.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
81
+ if (!slug) return `${prefix}-${++seq}`;
82
+ const base = `${slug}-${prefix === "pool" ? "p" : prefix === "worker" ? "w" : prefix}`;
83
+ const n = (named.get(base) ?? 0) + 1;
84
+ named.set(base, n);
85
+ return n === 1 ? base : `${base}${n}`;
86
+ };
87
+
1
88
  // ../../src/log.ts
2
89
  var LEVELS = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, off: 5 };
3
- var THREAD = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope ? "worker" : "main";
90
+ var THREAD2 = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope ? "worker" : "main";
4
91
  var consoleSink = ({ level, scope, message, data, thread, at }) => {
5
92
  const prefix = `[${(at / 1e3).toFixed(2)}s ${thread}:${scope}]`;
6
93
  const fn = level === "error" ? console.error : level === "warn" ? console.warn : level === "info" ? console.info : console.debug;
@@ -8,10 +95,25 @@ var consoleSink = ({ level, scope, message, data, thread, at }) => {
8
95
  else fn(`${prefix} ${message}`);
9
96
  };
10
97
  var minLevel = "info";
11
- var sink = consoleSink;
98
+ var sink2 = consoleSink;
99
+ var teeing = false;
12
100
  function log(level, scope, message, data) {
13
101
  if (LEVELS[level] < LEVELS[minLevel]) return;
14
- sink({ level, scope, message, data, thread: THREAD, at: performance.now() });
102
+ sink2({ level, scope, message, data, thread: THREAD2, at: performance.now() });
103
+ if (!teeing && devtoolsEnabled()) {
104
+ teeing = true;
105
+ try {
106
+ emitDevtools({
107
+ type: "log",
108
+ level,
109
+ scope,
110
+ message,
111
+ ...data !== void 0 ? { data: previewValue(data, 1024) } : {}
112
+ });
113
+ } finally {
114
+ teeing = false;
115
+ }
116
+ }
15
117
  }
16
118
  function scoped(scope) {
17
119
  return {
@@ -246,8 +348,11 @@ function listFactory(d, ctx, byteOffset) {
246
348
  writeAt,
247
349
  commit: () => {
248
350
  if (ctx.version) {
249
- Atomics.add(ctx.version.view, ctx.version.index, 1);
351
+ const prev = Atomics.add(ctx.version.view, ctx.version.index, 1);
250
352
  Atomics.notify(ctx.version.view, ctx.version.index);
353
+ if (ctx.path && devtoolsEnabled()) {
354
+ emitDevtools({ type: "memory:write", path: ctx.path, version: prev + 1 });
355
+ }
251
356
  }
252
357
  },
253
358
  read: () => Array.from({ length: count }, (_, i) => readAt(i)),
@@ -349,6 +454,177 @@ var WorkerCrashedError = class extends Error {
349
454
  }
350
455
  };
351
456
 
457
+ // ../../src/userTiming.ts
458
+ var measureAtoll = (name, start, end, t) => {
459
+ const perf = globalThis.performance;
460
+ if (typeof perf?.measure !== "function") return;
461
+ try {
462
+ perf.measure(name, {
463
+ start,
464
+ end: Math.max(end, start),
465
+ detail: {
466
+ devtools: {
467
+ dataType: "track-entry",
468
+ track: t.track,
469
+ trackGroup: "atoll",
470
+ color: t.color ?? "primary",
471
+ properties: t.properties,
472
+ tooltipText: t.tooltipText
473
+ }
474
+ }
475
+ });
476
+ perf.clearMeasures?.(name);
477
+ } catch {
478
+ }
479
+ };
480
+ var measureTaskCall = (runnerId, taskId, callId, slot, start, end, ok) => measureAtoll(`atoll task ${taskId}`, start, end, {
481
+ track: runnerId,
482
+ color: ok ? "primary" : "error",
483
+ properties: [["runner", runnerId], ["slot", slot], ["call", callId], ["outcome", ok ? "ok" : "error"]],
484
+ tooltipText: `${taskId} on ${runnerId}#${slot}: ${(end - start).toFixed(1)}ms`
485
+ });
486
+
487
+ // ../../src/pool/workerChannel.ts
488
+ function setupChannelMemory(config, owner) {
489
+ if (config.sharedMemory || config.sharedBuffer !== void 0) {
490
+ if (typeof SharedArrayBuffer === "undefined" || typeof crossOriginIsolated !== "undefined" && !crossOriginIsolated) {
491
+ throw new Error(
492
+ `${owner} requires SharedArrayBuffer in a cross-origin isolated context \u2014 serve the page with COOP/COEP headers.`
493
+ );
494
+ }
495
+ }
496
+ let injectedBuffer;
497
+ if (config.sharedBuffer !== void 0) {
498
+ injectedBuffer = typeof config.sharedBuffer === "function" ? config.sharedBuffer() : config.sharedBuffer;
499
+ if (!injectedBuffer) {
500
+ throw new Error(
501
+ `${owner}: \`sharedBuffer\` resolved to an empty buffer \u2014 the producing pool or contract has no bound memory yet.`
502
+ );
503
+ }
504
+ }
505
+ if (!config.sharedMemory) return { sharedMemory: void 0, injectedBuffer, persistence: void 0 };
506
+ const sharedMemory = config.sharedMemory;
507
+ let memoryManager;
508
+ if (injectedBuffer) {
509
+ sharedMemory.bind(injectedBuffer);
510
+ } else {
511
+ memoryManager = new MemoryManager(config.memory);
512
+ memoryManager.ensureCapacity(sharedMemory.totalBytes);
513
+ sharedMemory.bind(memoryManager.getBuffer());
514
+ }
515
+ return { sharedMemory, memoryManager, injectedBuffer, persistence: config.persistence?.(sharedMemory) };
516
+ }
517
+ function sendInit(worker, mem) {
518
+ if (mem.memoryManager) {
519
+ worker.postMessage({ type: "INIT_MEMORY", memory: mem.memoryManager.memory, devtools: devtoolsEnabled() });
520
+ return "INIT_MEMORY";
521
+ }
522
+ if (mem.injectedBuffer) {
523
+ worker.postMessage({ type: "INIT_MEMORY", buffer: mem.injectedBuffer, devtools: devtoolsEnabled() });
524
+ return "INIT_MEMORY";
525
+ }
526
+ worker.postMessage({ type: "INIT", devtools: devtoolsEnabled() });
527
+ return "INIT";
528
+ }
529
+ function forwardWorkerDevtools(runnerId, slot, ev) {
530
+ const qualify = (id) => `${runnerId}#${slot}~${id}`;
531
+ forwardDevtools({
532
+ ...ev,
533
+ ...ev.poolId !== void 0 ? { poolId: qualify(ev.poolId) } : {},
534
+ worker: ev.worker === void 0 ? { poolId: runnerId, slot } : { poolId: qualify(ev.worker.poolId), slot: ev.worker.slot }
535
+ });
536
+ }
537
+
538
+ // ../../src/pool/devtoolsCommands.ts
539
+ var CHAOS_FAILURE = "chaos: injected failure";
540
+ var KILL_MESSAGE = "killed from devtools";
541
+ var runners = /* @__PURE__ */ new Map();
542
+ var uninstall = null;
543
+ var runnerOf = (args) => {
544
+ const id = args.poolId;
545
+ if (typeof id !== "string") throw new Error("poolId (string) is required");
546
+ const r = runners.get(id);
547
+ if (!r) {
548
+ throw new Error(
549
+ id.includes("~") ? `runner '${id}' lives inside a worker: nested pools are not controllable from the dashboard` : `no live runner '${id}' on this thread`
550
+ );
551
+ }
552
+ return r;
553
+ };
554
+ var rate = (v, name) => {
555
+ if (v === void 0 || v === null) return void 0;
556
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1) {
557
+ throw new Error(`${name} must be a number in 0..1`);
558
+ }
559
+ return v || void 0;
560
+ };
561
+ var parseChaos = (args) => {
562
+ const { delayMs } = args;
563
+ if (delayMs !== void 0 && delayMs !== null && (typeof delayMs !== "number" || !Number.isFinite(delayMs) || delayMs < 0)) {
564
+ throw new Error("delayMs must be a non-negative number");
565
+ }
566
+ const failRate = rate(args.failRate, "failRate");
567
+ const timeoutRate = rate(args.timeoutRate, "timeoutRate");
568
+ if ((failRate ?? 0) + (timeoutRate ?? 0) > 1) throw new Error("failRate + timeoutRate must not exceed 1");
569
+ const chaos = {};
570
+ if (delayMs) chaos.delayMs = delayMs;
571
+ if (failRate) chaos.failRate = failRate;
572
+ if (timeoutRate) chaos.timeoutRate = timeoutRate;
573
+ return Object.keys(chaos).length ? chaos : null;
574
+ };
575
+ var listRunners = () => [...runners.values()].map((r) => ({
576
+ poolId: r.poolId,
577
+ label: r.label ?? r.poolId,
578
+ size: r.size(),
579
+ dedicated: r.dedicated,
580
+ chaos: r.getChaos()
581
+ }));
582
+ var install = () => {
583
+ const offs = [
584
+ registerDevtoolsCommand("pool.list", () => listRunners()),
585
+ registerDevtoolsCommand("pool.stats", (args) => runnerOf(args).stats()),
586
+ registerDevtoolsCommand("worker.kill", (args) => {
587
+ const r = runnerOf(args);
588
+ const slot = args.slot ?? 0;
589
+ if (typeof slot !== "number" || !Number.isInteger(slot) || slot < 0) {
590
+ throw new Error("slot must be a non-negative integer");
591
+ }
592
+ r.kill(slot);
593
+ return { poolId: r.poolId, slot, killed: true };
594
+ }),
595
+ registerDevtoolsCommand("pool.chaos", (args) => {
596
+ const r = runnerOf(args);
597
+ r.setChaos(parseChaos(args));
598
+ return r.getChaos();
599
+ })
600
+ ];
601
+ return () => offs.forEach((off) => off());
602
+ };
603
+ var registerDevtoolsRunner = (handle) => {
604
+ runners.set(handle.poolId, handle);
605
+ uninstall ??= install();
606
+ return () => {
607
+ if (runners.get(handle.poolId) !== handle) return;
608
+ runners.delete(handle.poolId);
609
+ if (runners.size === 0 && uninstall) {
610
+ uninstall();
611
+ uninstall = null;
612
+ }
613
+ };
614
+ };
615
+ var applyChaos = (chaos, call) => {
616
+ const r = Math.random();
617
+ const fail = r < (chaos.failRate ?? 0);
618
+ const timeout = !fail && r < (chaos.failRate ?? 0) + (chaos.timeoutRate ?? 0);
619
+ if (!chaos.delayMs && !fail && !timeout) return call.post();
620
+ setTimeout(() => {
621
+ if (!call.live()) return;
622
+ if (fail) return call.fail();
623
+ if (timeout) call.timeout();
624
+ call.post();
625
+ }, chaos.delayMs ?? 0);
626
+ };
627
+
352
628
  // ../../src/pool/workerPool.ts
353
629
  var poolLog = scoped("pool");
354
630
  var taskLog = scoped("task");
@@ -367,7 +643,10 @@ var WorkerPoolImpl = class {
367
643
  pending = /* @__PURE__ */ new Map();
368
644
  nextMessageId = 0;
369
645
  queue = [];
646
+ mem;
370
647
  memoryManager;
648
+ /** A caller-supplied buffer shipped via INIT_MEMORY (sub-worker / second-pool sharing). */
649
+ injectedBuffer;
371
650
  workerUrl;
372
651
  spawn;
373
652
  concurrency;
@@ -375,6 +654,9 @@ var WorkerPoolImpl = class {
375
654
  respawn;
376
655
  taskTimeout;
377
656
  closed = false;
657
+ /** Dashboard fault injection (`pool.chaos`); null unless devtools set it. */
658
+ chaos = null;
659
+ devtoolsOff;
378
660
  drainWaiters = [];
379
661
  statsAgg = {
380
662
  completed: 0,
@@ -384,17 +666,18 @@ var WorkerPoolImpl = class {
384
666
  waitMs: agg(),
385
667
  runMs: agg()
386
668
  };
669
+ /**
670
+ * Devtools correlation id, stamped on every event this pool emits:
671
+ * '<name>-p' when the config has a `name`, else 'pool-N'.
672
+ */
673
+ poolId;
674
+ label;
675
+ nextCallId = 0;
387
676
  sharedMemory;
388
677
  /** The attached memory-persistence adapter, if the config supplied one. */
389
678
  persistence;
390
679
  constructor(config) {
391
- if (config.sharedMemory) {
392
- if (typeof SharedArrayBuffer === "undefined" || typeof crossOriginIsolated !== "undefined" && !crossOriginIsolated) {
393
- throw new Error(
394
- "WorkerPool requires SharedArrayBuffer in a cross-origin isolated context \u2014 serve the page with COOP/COEP headers."
395
- );
396
- }
397
- }
680
+ const mem = setupChannelMemory(config, "WorkerPool");
398
681
  if (!config.workerUrl && !config.createWorker) {
399
682
  throw new Error("WorkerPool requires either `workerUrl` or `createWorker` in its config.");
400
683
  }
@@ -404,23 +687,49 @@ var WorkerPoolImpl = class {
404
687
  this.maxQueue = config.maxQueue ?? Infinity;
405
688
  this.respawn = config.respawn ?? true;
406
689
  this.taskTimeout = config.taskTimeout;
407
- if (config.sharedMemory) {
408
- this.sharedMemory = config.sharedMemory;
409
- this.memoryManager = new MemoryManager(config.memory);
410
- this.memoryManager.ensureCapacity(this.sharedMemory.totalBytes);
411
- this.sharedMemory.bind(this.memoryManager.getBuffer());
412
- this.persistence = config.persistence?.(config.sharedMemory);
413
- }
690
+ this.label = config.name;
691
+ this.poolId = nextDevtoolsId("pool", config.name);
692
+ this.mem = mem;
693
+ this.sharedMemory = mem.sharedMemory;
694
+ this.memoryManager = mem.memoryManager;
695
+ this.injectedBuffer = mem.injectedBuffer;
696
+ this.persistence = mem.persistence;
414
697
  const requestedSize = config.poolSize ?? "auto";
415
- const poolSize = requestedSize === "auto" ? navigator.hardwareConcurrency ?? 4 : requestedSize;
698
+ const poolSize = requestedSize === "auto" ? typeof navigator !== "undefined" ? navigator.hardwareConcurrency ?? 4 : 4 : requestedSize;
416
699
  poolLog.info(
417
- this.sharedMemory ? `spawning ${poolSize} worker(s) with ${fmtBytes(this.sharedMemory.totalBytes)} shared buffer` : `spawning ${poolSize} worker(s), message-only`,
700
+ this.sharedMemory ? `spawning ${poolSize} worker(s) with ${fmtBytes(this.sharedMemory.totalBytes)} shared buffer` : this.injectedBuffer ? `spawning ${poolSize} worker(s) with ${fmtBytes(this.injectedBuffer.byteLength)} inherited buffer` : `spawning ${poolSize} worker(s), message-only`,
418
701
  { workerUrl: config.workerUrl ? String(config.workerUrl) : "createWorker()" }
419
702
  );
420
703
  poolLog.debug(`tip: setLogLevel('trace') for per-task detail`);
704
+ emitDevtools({
705
+ type: "pool:init",
706
+ poolId: this.poolId,
707
+ label: this.label,
708
+ poolSize,
709
+ concurrency: this.concurrency,
710
+ memoryBytes: this.sharedMemory?.totalBytes ?? this.injectedBuffer?.byteLength
711
+ });
421
712
  for (let i = 0; i < poolSize; i++) {
422
713
  this.addWorker(this.spawn());
423
714
  }
715
+ if (devtoolsEnabled()) {
716
+ this.devtoolsOff = registerDevtoolsRunner({
717
+ poolId: this.poolId,
718
+ label: this.label,
719
+ dedicated: false,
720
+ size: () => this.slots.length,
721
+ stats: () => this.stats(),
722
+ kill: (slot) => {
723
+ const s = this.slots[slot];
724
+ if (!s) throw new Error(`${this.poolId} has no worker slot ${slot}`);
725
+ this.handleWorkerError(s, { message: KILL_MESSAGE });
726
+ },
727
+ getChaos: () => this.chaos,
728
+ setChaos: (chaos) => {
729
+ this.chaos = chaos;
730
+ }
731
+ });
732
+ }
424
733
  const tasks = config.tasks ?? {};
425
734
  for (const [name, contract] of Object.entries(tasks)) {
426
735
  if (name in this) {
@@ -437,18 +746,20 @@ var WorkerPoolImpl = class {
437
746
  addWorker(worker) {
438
747
  const slot = { worker, inFlight: 0 };
439
748
  worker.addEventListener("message", (event) => {
440
- this.handleReply(slot, event.data);
749
+ const data = event.data;
750
+ if (data.type === "ATOLL_DEVTOOLS" && data.event) {
751
+ forwardWorkerDevtools(this.poolId, this.slots.indexOf(slot), data.event);
752
+ return;
753
+ }
754
+ this.handleReply(slot, data);
441
755
  });
442
756
  worker.addEventListener("error", (event) => {
443
757
  this.handleWorkerError(slot, event);
444
758
  });
445
- if (this.memoryManager) {
446
- worker.postMessage({ type: "INIT_MEMORY", memory: this.memoryManager.memory });
447
- } else {
448
- worker.postMessage({ type: "INIT" });
449
- }
759
+ const init = sendInit(worker, this.mem);
450
760
  this.slots.push(slot);
451
- poolLog.debug(`worker spawned, ${this.memoryManager ? "INIT_MEMORY" : "INIT"} sent`);
761
+ poolLog.debug(`worker spawned, ${init} sent`);
762
+ emitDevtools({ type: "worker:spawn", poolId: this.poolId, slot: this.slots.indexOf(slot) });
452
763
  return slot;
453
764
  }
454
765
  handleReply(slot, data) {
@@ -459,14 +770,24 @@ var WorkerPoolImpl = class {
459
770
  this.detach(entry);
460
771
  if (!entry.done) {
461
772
  entry.done = true;
462
- this.statsAgg.runMs.add(performance.now() - (entry.sentAt ?? entry.enqueuedAt));
773
+ const start = entry.sentAt ?? entry.enqueuedAt;
774
+ const runMs = performance.now() - start;
775
+ this.statsAgg.runMs.add(runMs);
463
776
  if (data.success) {
464
777
  this.statsAgg.completed++;
465
778
  taskLog.debug(`\u2190 ${entry.contract.taskId} ok`);
779
+ if (devtoolsEnabled()) {
780
+ emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "ok", runMs, resultBytes: estimateCloneBytes(data.result) });
781
+ measureTaskCall(this.poolId, entry.contract.taskId, entry.callId, this.slots.indexOf(slot), start, start + runMs, true);
782
+ }
466
783
  entry.resolve(data.result);
467
784
  } else {
468
785
  this.statsAgg.failed++;
469
786
  taskLog.warn(`\u2190 ${entry.contract.taskId} failed: ${data.error}`);
787
+ if (devtoolsEnabled()) {
788
+ emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "error", runMs, error: data.error });
789
+ measureTaskCall(this.poolId, entry.contract.taskId, entry.callId, this.slots.indexOf(slot), start, start + runMs, false);
790
+ }
470
791
  entry.reject(new Error(data.error));
471
792
  }
472
793
  }
@@ -478,6 +799,8 @@ var WorkerPoolImpl = class {
478
799
  const e = event;
479
800
  const message = e?.message ?? (e?.error ? String(e.error) : "worker error");
480
801
  poolLog.warn(`worker error: ${message}`);
802
+ const idx = this.slots.indexOf(slot);
803
+ emitDevtools({ type: "worker:error", poolId: this.poolId, slot: idx, message });
481
804
  for (const [id, entry] of [...this.pending]) {
482
805
  if (entry.slot === slot) {
483
806
  this.pending.delete(id);
@@ -485,20 +808,22 @@ var WorkerPoolImpl = class {
485
808
  if (entry.done) continue;
486
809
  entry.done = true;
487
810
  this.statsAgg.failed++;
811
+ emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "crashed", error: message });
488
812
  entry.reject(new WorkerCrashedError(message));
489
813
  }
490
814
  }
491
- const idx = this.slots.indexOf(slot);
492
815
  if (idx >= 0) this.slots.splice(idx, 1);
493
816
  slot.worker.terminate();
494
817
  if (this.respawn) {
495
818
  poolLog.info("respawning crashed worker");
496
819
  this.addWorker(this.spawn());
820
+ emitDevtools({ type: "worker:respawn", poolId: this.poolId, slot: this.slots.length - 1 });
497
821
  } else if (this.slots.length === 0) {
498
822
  for (const entry of this.queue.splice(0)) {
499
823
  entry.done = true;
500
824
  this.detach(entry);
501
825
  this.statsAgg.failed++;
826
+ emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "crashed", error: message });
502
827
  entry.reject(new WorkerCrashedError(message));
503
828
  }
504
829
  }
@@ -527,16 +852,26 @@ var WorkerPoolImpl = class {
527
852
  entry.messageId = messageId;
528
853
  entry.slot = slot;
529
854
  entry.sentAt = performance.now();
530
- this.statsAgg.waitMs.add(performance.now() - entry.enqueuedAt);
855
+ const waitMs = entry.sentAt - entry.enqueuedAt;
856
+ this.statsAgg.waitMs.add(waitMs);
531
857
  slot.inFlight++;
532
858
  this.pending.set(messageId, entry);
533
- taskLog.debug(`\u2192 ${entry.contract.taskId}`, { worker: this.slots.indexOf(slot), args: entry.args });
534
- slot.worker.postMessage({
535
- type: "EXECUTE_TASK",
536
- messageId,
537
- taskId: entry.contract.taskId,
538
- args: entry.args
539
- });
859
+ const slotIndex = this.slots.indexOf(slot);
860
+ taskLog.debug(`\u2192 ${entry.contract.taskId}`, { worker: slotIndex, args: entry.args });
861
+ if (devtoolsEnabled()) {
862
+ emitDevtools({ type: "task:dispatch", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, slot: slotIndex, waitMs, argBytes: estimateCloneBytes(entry.args) });
863
+ }
864
+ const msg = { type: "EXECUTE_TASK", messageId, taskId: entry.contract.taskId, args: entry.args };
865
+ if (this.chaos) {
866
+ applyChaos(this.chaos, {
867
+ live: () => this.pending.get(messageId) === entry,
868
+ fail: () => this.handleReply(slot, { messageId, taskId: msg.taskId, success: false, error: CHAOS_FAILURE }),
869
+ timeout: () => this.cancel(entry, new TaskTimeoutError()),
870
+ post: () => slot.worker.postMessage(msg)
871
+ });
872
+ } else {
873
+ slot.worker.postMessage(msg);
874
+ }
540
875
  }
541
876
  /** Detaches signal/timeout listeners from a finalized entry. */
542
877
  detach(entry) {
@@ -557,6 +892,7 @@ var WorkerPoolImpl = class {
557
892
  entry.done = true;
558
893
  this.detach(entry);
559
894
  this.statsAgg.aborted++;
895
+ emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: err instanceof TaskTimeoutError ? "timeout" : "aborted" });
560
896
  entry.reject(err);
561
897
  this.drainQueue();
562
898
  this.checkDrained();
@@ -582,6 +918,7 @@ var WorkerPoolImpl = class {
582
918
  const entry = {
583
919
  contract,
584
920
  args,
921
+ callId: ++this.nextCallId,
585
922
  enqueuedAt: performance.now(),
586
923
  resolve,
587
924
  reject,
@@ -596,12 +933,14 @@ var WorkerPoolImpl = class {
596
933
  if (timeout !== void 0) {
597
934
  entry.timer = setTimeout(() => this.cancel(entry, new TaskTimeoutError()), timeout);
598
935
  }
936
+ emitDevtools({ type: "task:enqueue", poolId: this.poolId, callId: entry.callId, taskId: contract.taskId });
599
937
  const slot = this.pickWorker();
600
938
  if (slot) {
601
939
  this.send(slot, entry);
602
940
  } else if (this.queue.length >= this.maxQueue) {
603
941
  entry.done = true;
604
942
  this.detach(entry);
943
+ emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: contract.taskId, outcome: "queue-full" });
605
944
  reject(new PoolQueueFullError());
606
945
  return;
607
946
  } else {
@@ -622,7 +961,7 @@ var WorkerPoolImpl = class {
622
961
  * (e.g. a second pool of HTTP workers via withSharedBuffer).
623
962
  */
624
963
  get sharedBuffer() {
625
- return this.memoryManager?.getBuffer();
964
+ return this.memoryManager?.getBuffer() ?? this.injectedBuffer;
626
965
  }
627
966
  /**
628
967
  * Live snapshot of the pool's worker slots — for auxiliary messaging that
@@ -662,16 +1001,19 @@ var WorkerPoolImpl = class {
662
1001
  }
663
1002
  terminate() {
664
1003
  this.closed = true;
1004
+ this.devtoolsOff?.();
665
1005
  void this.persistence?.stop()?.catch?.(
666
1006
  (e) => poolLog.warn("memory persistence stop failed", e)
667
1007
  );
668
1008
  poolLog.info(`terminating ${this.slots.length} worker(s)`);
1009
+ emitDevtools({ type: "pool:terminate", poolId: this.poolId });
669
1010
  const crashed = new WorkerCrashedError("pool terminated");
670
1011
  for (const [, entry] of this.pending) {
671
1012
  this.detach(entry);
672
1013
  if (entry.done) continue;
673
1014
  entry.done = true;
674
1015
  this.statsAgg.failed++;
1016
+ emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "crashed", error: "pool terminated" });
675
1017
  entry.reject(crashed);
676
1018
  }
677
1019
  this.pending.clear();
@@ -679,6 +1021,7 @@ var WorkerPoolImpl = class {
679
1021
  entry.done = true;
680
1022
  this.detach(entry);
681
1023
  this.statsAgg.failed++;
1024
+ emitDevtools({ type: "task:settle", poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: "crashed", error: "pool terminated" });
682
1025
  entry.reject(crashed);
683
1026
  }
684
1027
  for (const slot of this.slots) {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  WorkerPool,
3
3
  bindSharedMemories
4
- } from "./chunk-QD4C3PWH.js";
4
+ } from "./chunk-VEJT6OVD.js";
5
5
 
6
6
  // src/worker.ts
7
7
  import { Worker as NodeWorker } from "node:worker_threads";
package/dist/redis.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scoped
3
- } from "./chunk-QD4C3PWH.js";
3
+ } from "./chunk-VEJT6OVD.js";
4
4
 
5
5
  // src/redis.ts
6
6
  var log = scoped("redis-mem");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolljs/node",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Node runtime adapter — node:worker_threads pools, no framework",
5
5
  "homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "registry": "https://registry.npmjs.org"
35
35
  },
36
36
  "peerDependencies": {
37
- "@atolljs/core": "0.1.6"
37
+ "@atolljs/core": "0.1.8"
38
38
  },
39
39
  "repository": {
40
40
  "type": "git",