@graphrefly/graphrefly 0.18.0 → 0.20.0

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 (74) hide show
  1. package/dist/{chunk-J7S54G7I.js → chunk-2L5J6RPM.js} +7 -2
  2. package/dist/chunk-2L5J6RPM.js.map +1 -0
  3. package/dist/{chunk-LB3RYLSC.js → chunk-3N2Y6PCR.js} +197 -42
  4. package/dist/chunk-3N2Y6PCR.js.map +1 -0
  5. package/dist/{chunk-KJGUP35I.js → chunk-5PSVTDNZ.js} +22 -9
  6. package/dist/chunk-5PSVTDNZ.js.map +1 -0
  7. package/dist/{chunk-76YPZQTW.js → chunk-BJAOEU4D.js} +34 -29
  8. package/dist/chunk-BJAOEU4D.js.map +1 -0
  9. package/dist/{chunk-UVWEKTYC.js → chunk-IAPLC4NR.js} +3 -3
  10. package/dist/{chunk-F6ORUNO7.js → chunk-OOA2UTXF.js} +58 -2
  11. package/dist/chunk-OOA2UTXF.js.map +1 -0
  12. package/dist/{chunk-TNKODJ6E.js → chunk-PGEU5MEH.js} +7 -3
  13. package/dist/{chunk-TNKODJ6E.js.map → chunk-PGEU5MEH.js.map} +1 -1
  14. package/dist/chunk-R2LPZIY2.js +111 -0
  15. package/dist/chunk-R2LPZIY2.js.map +1 -0
  16. package/dist/{chunk-BV3TPSBK.js → chunk-XYL3GLB3.js} +742 -757
  17. package/dist/chunk-XYL3GLB3.js.map +1 -0
  18. package/dist/compat/nestjs/index.cjs +967 -811
  19. package/dist/compat/nestjs/index.cjs.map +1 -1
  20. package/dist/compat/nestjs/index.d.cts +4 -4
  21. package/dist/compat/nestjs/index.d.ts +4 -4
  22. package/dist/compat/nestjs/index.js +7 -7
  23. package/dist/core/index.cjs +653 -666
  24. package/dist/core/index.cjs.map +1 -1
  25. package/dist/core/index.d.cts +2 -2
  26. package/dist/core/index.d.ts +2 -2
  27. package/dist/core/index.js +7 -3
  28. package/dist/extra/index.cjs +728 -688
  29. package/dist/extra/index.cjs.map +1 -1
  30. package/dist/extra/index.d.cts +4 -4
  31. package/dist/extra/index.d.ts +4 -4
  32. package/dist/extra/index.js +9 -5
  33. package/dist/graph/index.cjs +836 -808
  34. package/dist/graph/index.cjs.map +1 -1
  35. package/dist/graph/index.d.cts +3 -3
  36. package/dist/graph/index.d.ts +3 -3
  37. package/dist/graph/index.js +8 -8
  38. package/dist/{graph-gISB9n3n.d.ts → graph-KsTe57nI.d.cts} +82 -8
  39. package/dist/{graph-BYFlyNpX.d.cts → graph-mILUUqW8.d.ts} +82 -8
  40. package/dist/{index-CgKPpiu8.d.ts → index-8a605sg9.d.ts} +2 -2
  41. package/dist/{index-DKaB2x0T.d.ts → index-B2SvPEbc.d.ts} +6 -65
  42. package/dist/{index-EmzYk-TG.d.cts → index-BHfg_Ez3.d.ts} +123 -77
  43. package/dist/{index-B80mMeuf.d.ts → index-Bc_diYYJ.d.cts} +123 -77
  44. package/dist/{index-B43mC7uY.d.cts → index-BjtlNirP.d.cts} +3 -3
  45. package/dist/{index-CEDaJaYE.d.ts → index-CgSiUouz.d.ts} +3 -3
  46. package/dist/{index-7WnwgjMu.d.ts → index-DuN3bhtm.d.ts} +82 -32
  47. package/dist/{index-D_tUMcpz.d.cts → index-SFzE_KTa.d.cts} +82 -32
  48. package/dist/{index-Ci_vPaVm.d.cts → index-UudxGnzc.d.cts} +6 -65
  49. package/dist/{index-BqOWSFhr.d.cts → index-VHA43cGP.d.cts} +2 -2
  50. package/dist/index.cjs +5936 -5522
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +644 -379
  53. package/dist/index.d.ts +644 -379
  54. package/dist/index.js +4388 -4058
  55. package/dist/index.js.map +1 -1
  56. package/dist/{meta-npl5b97j.d.cts → meta-BnG7XAaE.d.cts} +394 -236
  57. package/dist/{meta-npl5b97j.d.ts → meta-BnG7XAaE.d.ts} +394 -236
  58. package/dist/{observable-DFBCBELR.d.cts → observable-C8Kx_O6k.d.cts} +1 -1
  59. package/dist/{observable-oAGygKvc.d.ts → observable-DcBwQY7t.d.ts} +1 -1
  60. package/dist/patterns/reactive-layout/index.cjs +865 -718
  61. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  62. package/dist/patterns/reactive-layout/index.d.cts +3 -3
  63. package/dist/patterns/reactive-layout/index.d.ts +3 -3
  64. package/dist/patterns/reactive-layout/index.js +4 -4
  65. package/package.json +2 -2
  66. package/dist/chunk-76YPZQTW.js.map +0 -1
  67. package/dist/chunk-BV3TPSBK.js.map +0 -1
  68. package/dist/chunk-F6ORUNO7.js.map +0 -1
  69. package/dist/chunk-FCLROC4Q.js +0 -231
  70. package/dist/chunk-FCLROC4Q.js.map +0 -1
  71. package/dist/chunk-J7S54G7I.js.map +0 -1
  72. package/dist/chunk-KJGUP35I.js.map +0 -1
  73. package/dist/chunk-LB3RYLSC.js.map +0 -1
  74. /package/dist/{chunk-UVWEKTYC.js.map → chunk-IAPLC4NR.js.map} +0 -0
@@ -34,6 +34,7 @@ __export(core_exports, {
34
34
  RESOLVED: () => RESOLVED,
35
35
  RESUME: () => RESUME,
36
36
  ResettableTimer: () => ResettableTimer,
37
+ START: () => START,
37
38
  TEARDOWN: () => TEARDOWN,
38
39
  accessHintForGuard: () => accessHintForGuard,
39
40
  advanceVersion: () => advanceVersion,
@@ -48,6 +49,7 @@ __export(core_exports, {
48
49
  effect: () => effect,
49
50
  isBatching: () => isBatching,
50
51
  isKnownMessageType: () => isKnownMessageType,
52
+ isLocalOnly: () => isLocalOnly,
51
53
  isPhase2Message: () => isPhase2Message,
52
54
  isTerminalMessage: () => isTerminalMessage,
53
55
  isV1: () => isV1,
@@ -81,6 +83,7 @@ function normalizeActor(actor) {
81
83
  }
82
84
 
83
85
  // src/core/messages.ts
86
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
84
87
  var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
85
88
  var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
86
89
  var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
@@ -91,6 +94,7 @@ var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
91
94
  var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
92
95
  var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
93
96
  var knownMessageTypes = [
97
+ START,
94
98
  DATA,
95
99
  DIRTY,
96
100
  RESOLVED,
@@ -101,16 +105,18 @@ var knownMessageTypes = [
101
105
  COMPLETE,
102
106
  ERROR
103
107
  ];
108
+ var knownMessageSet = new Set(knownMessageTypes);
104
109
  function isKnownMessageType(t) {
105
- return knownMessageTypes.includes(t);
110
+ return knownMessageSet.has(t);
106
111
  }
107
112
  function messageTier(t) {
108
- if (t === DIRTY || t === INVALIDATE) return 0;
109
- if (t === PAUSE || t === RESUME) return 1;
110
- if (t === DATA || t === RESOLVED) return 2;
111
- if (t === COMPLETE || t === ERROR) return 3;
112
- if (t === TEARDOWN) return 4;
113
- return 0;
113
+ if (t === START) return 0;
114
+ if (t === DIRTY || t === INVALIDATE) return 1;
115
+ if (t === PAUSE || t === RESUME) return 2;
116
+ if (t === DATA || t === RESOLVED) return 3;
117
+ if (t === COMPLETE || t === ERROR) return 4;
118
+ if (t === TEARDOWN) return 5;
119
+ return 1;
114
120
  }
115
121
  function isPhase2Message(msg) {
116
122
  const t = msg[0];
@@ -119,6 +125,10 @@ function isPhase2Message(msg) {
119
125
  function isTerminalMessage(t) {
120
126
  return t === COMPLETE || t === ERROR;
121
127
  }
128
+ function isLocalOnly(t) {
129
+ if (!knownMessageSet.has(t)) return false;
130
+ return messageTier(t) < 3;
131
+ }
122
132
  function propagatesToMeta(t) {
123
133
  return t === TEARDOWN;
124
134
  }
@@ -279,14 +289,14 @@ function _downSequential(sink, messages, phase = 2) {
279
289
  const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
280
290
  for (const msg of messages) {
281
291
  const tier = messageTier(msg[0]);
282
- if (tier === 2) {
292
+ if (tier === 3) {
283
293
  if (isBatching()) {
284
294
  const m = msg;
285
295
  dataQueue.push(() => sink([m]));
286
296
  } else {
287
297
  sink([msg]);
288
298
  }
289
- } else if (tier >= 3) {
299
+ } else if (tier >= 4) {
290
300
  if (isBatching()) {
291
301
  const m = msg;
292
302
  pendingPhase3.push(() => sink([m]));
@@ -299,14 +309,6 @@ function _downSequential(sink, messages, phase = 2) {
299
309
  }
300
310
  }
301
311
 
302
- // src/core/clock.ts
303
- function monotonicNs() {
304
- return Math.trunc(performance.now() * 1e6);
305
- }
306
- function wallClockNs() {
307
- return Date.now() * 1e6;
308
- }
309
-
310
312
  // src/core/guard.ts
311
313
  var GuardDenied = class extends Error {
312
314
  actor;
@@ -405,6 +407,14 @@ function accessHintForGuard(guard) {
405
407
  return allowed.join("+");
406
408
  }
407
409
 
410
+ // src/core/clock.ts
411
+ function monotonicNs() {
412
+ return Math.trunc(performance.now() * 1e6);
413
+ }
414
+ function wallClockNs() {
415
+ return Date.now() * 1e6;
416
+ }
417
+
408
418
  // src/core/versioning.ts
409
419
  var import_node_crypto = require("crypto");
410
420
  function canonicalizeForHash(value) {
@@ -460,10 +470,29 @@ function isV1(info) {
460
470
  return "cid" in info;
461
471
  }
462
472
 
463
- // src/core/node.ts
473
+ // src/core/node-base.ts
464
474
  var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
465
475
  var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
466
- function createIntBitSet() {
476
+ function cleanupResult(cleanup, ...args) {
477
+ const r = { [CLEANUP_RESULT]: true, cleanup };
478
+ if (args.length > 0) r.value = args[0];
479
+ return r;
480
+ }
481
+ var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
482
+ var isCleanupFn = (value) => typeof value === "function";
483
+ function statusAfterMessage(status, msg) {
484
+ const t = msg[0];
485
+ if (t === DIRTY) return "dirty";
486
+ if (t === DATA) return "settled";
487
+ if (t === RESOLVED) return "resolved";
488
+ if (t === COMPLETE) return "completed";
489
+ if (t === ERROR) return "errored";
490
+ if (t === INVALIDATE) return "dirty";
491
+ if (t === TEARDOWN) return "disconnected";
492
+ return status;
493
+ }
494
+ function createIntBitSet(size) {
495
+ const fullMask = size >= 32 ? -1 : ~(-1 << size);
467
496
  let bits = 0;
468
497
  return {
469
498
  set(i) {
@@ -476,7 +505,8 @@ function createIntBitSet() {
476
505
  return (bits & 1 << i) !== 0;
477
506
  },
478
507
  covers(other) {
479
- return (bits & other._bits()) === other._bits();
508
+ const otherBits = other._bits();
509
+ return (bits & otherBits) === otherBits;
480
510
  },
481
511
  any() {
482
512
  return bits !== 0;
@@ -484,6 +514,9 @@ function createIntBitSet() {
484
514
  reset() {
485
515
  bits = 0;
486
516
  },
517
+ setAll() {
518
+ bits = fullMask;
519
+ },
487
520
  _bits() {
488
521
  return bits;
489
522
  }
@@ -491,6 +524,8 @@ function createIntBitSet() {
491
524
  }
492
525
  function createArrayBitSet(size) {
493
526
  const words = new Uint32Array(Math.ceil(size / 32));
527
+ const lastBits = size % 32;
528
+ const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
494
529
  return {
495
530
  set(i) {
496
531
  words[i >>> 5] |= 1 << (i & 31);
@@ -517,135 +552,103 @@ function createArrayBitSet(size) {
517
552
  reset() {
518
553
  words.fill(0);
519
554
  },
555
+ setAll() {
556
+ for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
557
+ if (words.length > 0) words[words.length - 1] = lastWordMask;
558
+ },
520
559
  _words: words
521
560
  };
522
561
  }
523
562
  function createBitSet(size) {
524
- return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
525
- }
526
- var isNodeArray = (value) => Array.isArray(value);
527
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
528
- function cleanupResult(cleanup, ...args) {
529
- const r = { [CLEANUP_RESULT]: true, cleanup };
530
- if (args.length > 0) r.value = args[0];
531
- return r;
563
+ return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
532
564
  }
533
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
534
- var isCleanupFn = (value) => typeof value === "function";
535
- var statusAfterMessage = (status, msg) => {
536
- const t = msg[0];
537
- if (t === DIRTY) return "dirty";
538
- if (t === DATA) return "settled";
539
- if (t === RESOLVED) return "resolved";
540
- if (t === COMPLETE) return "completed";
541
- if (t === ERROR) return "errored";
542
- if (t === INVALIDATE) return "dirty";
543
- if (t === TEARDOWN) return "disconnected";
544
- return status;
545
- };
546
- var NodeImpl = class {
547
- // --- Configuration (set once, never reassigned) ---
565
+ var NodeBase = class {
566
+ // --- Identity (set once) ---
548
567
  _optsName;
549
568
  _registryName;
550
- /** @internal read by {@link describeNode} before inference. */
569
+ /** @internal Read by `describeNode` before inference. */
551
570
  _describeKind;
552
571
  meta;
553
- _deps;
554
- _fn;
555
- _opts;
572
+ // --- Options ---
556
573
  _equals;
574
+ _resubscribable;
575
+ _resetOnTeardown;
576
+ _onResubscribe;
557
577
  _onMessage;
558
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
578
+ /** @internal Read by `describeNode` for `accessHintForGuard`. */
559
579
  _guard;
580
+ /** @internal Subclasses update this through {@link _recordMutation}. */
560
581
  _lastMutation;
561
- _hasDeps;
562
- _autoComplete;
563
- _isSingleDep;
564
- // --- Mutable state ---
582
+ // --- Versioning ---
583
+ _hashFn;
584
+ _versioning;
585
+ // --- Lifecycle state ---
586
+ /** @internal Read by `describeNode` and `graph.ts`. */
565
587
  _cached;
588
+ /** @internal Read externally via `get status()`. */
566
589
  _status;
567
590
  _terminal = false;
568
- _connected = false;
569
- _producerStarted = false;
570
- _connecting = false;
571
- _manualEmitUsed = false;
591
+ _active = false;
592
+ // --- Sink storage ---
593
+ /** @internal Read by `graph/profile.ts` for subscriber counts. */
572
594
  _sinkCount = 0;
573
595
  _singleDepSinkCount = 0;
574
- // --- Object/collection state ---
575
- _depDirtyMask;
576
- _depSettledMask;
577
- _depCompleteMask;
578
- _allDepsCompleteMask;
579
- _lastDepValues;
580
- _cleanup;
581
- _sinks = null;
582
596
  _singleDepSinks = /* @__PURE__ */ new WeakSet();
583
- _upstreamUnsubs = [];
597
+ _sinks = null;
598
+ // --- Actions + bound helpers ---
584
599
  _actions;
585
600
  _boundDownToSinks;
601
+ // --- Inspector hook (Graph observability) ---
586
602
  _inspectorHook;
587
- _versioning;
588
- _hashFn;
589
- constructor(deps, fn, opts) {
590
- this._deps = deps;
591
- this._fn = fn;
592
- this._opts = opts;
603
+ constructor(opts) {
593
604
  this._optsName = opts.name;
594
605
  this._describeKind = opts.describeKind;
595
606
  this._equals = opts.equals ?? Object.is;
607
+ this._resubscribable = opts.resubscribable ?? false;
608
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
609
+ this._onResubscribe = opts.onResubscribe;
596
610
  this._onMessage = opts.onMessage;
597
611
  this._guard = opts.guard;
598
- this._hasDeps = deps.length > 0;
599
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
600
- this._isSingleDep = deps.length === 1 && fn != null;
601
612
  this._cached = "initial" in opts ? opts.initial : NO_VALUE;
602
- this._status = this._hasDeps ? "disconnected" : "settled";
613
+ this._status = "disconnected";
603
614
  this._hashFn = opts.versioningHash ?? defaultHash;
604
615
  this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
605
616
  id: opts.versioningId,
606
617
  hash: this._hashFn
607
618
  }) : void 0;
608
- this._depDirtyMask = createBitSet(deps.length);
609
- this._depSettledMask = createBitSet(deps.length);
610
- this._depCompleteMask = createBitSet(deps.length);
611
- this._allDepsCompleteMask = createBitSet(deps.length);
612
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
613
619
  const meta = {};
614
620
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
615
- meta[k] = node({
616
- initial: v,
617
- name: `${opts.name ?? "node"}:meta:${k}`,
618
- describeKind: "state",
619
- ...opts.guard != null ? { guard: opts.guard } : {}
620
- });
621
+ meta[k] = this._createMetaNode(k, v, opts);
621
622
  }
622
623
  Object.freeze(meta);
623
624
  this.meta = meta;
624
625
  const self = this;
625
626
  this._actions = {
626
627
  down(messages) {
627
- self._manualEmitUsed = true;
628
+ self._onManualEmit();
628
629
  self._downInternal(messages);
629
630
  },
630
631
  emit(value) {
631
- self._manualEmitUsed = true;
632
+ self._onManualEmit();
632
633
  self._downAutoValue(value);
633
634
  },
634
635
  up(messages) {
635
636
  self._upInternal(messages);
636
637
  }
637
638
  };
638
- this.down = this.down.bind(this);
639
- this.up = this.up.bind(this);
640
639
  this._boundDownToSinks = this._downToSinks.bind(this);
641
640
  }
641
+ /**
642
+ * Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
643
+ * {@link NodeImpl} overrides to set `_manualEmitUsed`.
644
+ */
645
+ _onManualEmit() {
646
+ }
647
+ // --- Identity getters ---
642
648
  get name() {
643
649
  return this._registryName ?? this._optsName;
644
650
  }
645
- /**
646
- * When a node is registered with {@link Graph.add} without an options `name`,
647
- * the graph assigns the registry local name for introspection (parity with graphrefly-py).
648
- */
651
+ /** @internal Assigned by `Graph.add` when registered without an options `name`. */
649
652
  _assignRegistryName(localName) {
650
653
  if (this._optsName !== void 0 || this._registryName !== void 0) return;
651
654
  this._registryName = localName;
@@ -663,7 +666,10 @@ var NodeImpl = class {
663
666
  }
664
667
  };
665
668
  }
666
- // --- Public interface (Node<T>) ---
669
+ /** @internal Used by subclasses to surface inspector events. */
670
+ _emitInspectorHook(event) {
671
+ this._inspectorHook?.(event);
672
+ }
667
673
  get status() {
668
674
  return this._status;
669
675
  }
@@ -673,15 +679,7 @@ var NodeImpl = class {
673
679
  get v() {
674
680
  return this._versioning;
675
681
  }
676
- /**
677
- * Retroactively apply versioning to a node that was created without it.
678
- * No-op if versioning is already enabled.
679
- *
680
- * Version starts at 0 regardless of prior DATA emissions — it tracks
681
- * changes from the moment versioning is enabled, not historical ones.
682
- *
683
- * @internal — used by {@link Graph.setVersioning}.
684
- */
682
+ /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
685
683
  _applyVersioning(level, opts) {
686
684
  if (this._versioning != null) return;
687
685
  this._hashFn = opts?.hash ?? this._hashFn;
@@ -701,6 +699,7 @@ var NodeImpl = class {
701
699
  if (this._guard == null) return true;
702
700
  return this._guard(normalizeActor(actor), "observe");
703
701
  }
702
+ // --- Public transport ---
704
703
  get() {
705
704
  return this._cached === NO_VALUE ? void 0 : this._cached;
706
705
  }
@@ -713,43 +712,25 @@ var NodeImpl = class {
713
712
  if (!this._guard(actor, action)) {
714
713
  throw new GuardDenied({ actor, action, nodeName: this.name });
715
714
  }
716
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
715
+ this._recordMutation(actor);
717
716
  }
718
717
  this._downInternal(messages);
719
718
  }
720
- _downInternal(messages) {
721
- if (messages.length === 0) return;
722
- let lifecycleMessages = messages;
723
- let sinkMessages = messages;
724
- if (this._terminal && !this._opts.resubscribable) {
725
- const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
726
- if (terminalPassthrough.length === 0) return;
727
- lifecycleMessages = terminalPassthrough;
728
- sinkMessages = terminalPassthrough;
729
- }
730
- this._handleLocalLifecycle(lifecycleMessages);
731
- if (this._canSkipDirty()) {
732
- let hasPhase2 = false;
733
- for (let i = 0; i < sinkMessages.length; i++) {
734
- const t = sinkMessages[i][0];
735
- if (t === DATA || t === RESOLVED) {
736
- hasPhase2 = true;
737
- break;
738
- }
739
- }
740
- if (hasPhase2) {
741
- const filtered = [];
742
- for (let i = 0; i < sinkMessages.length; i++) {
743
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
744
- }
745
- if (filtered.length > 0) {
746
- downWithBatch(this._boundDownToSinks, filtered);
747
- }
748
- return;
749
- }
750
- }
751
- downWithBatch(this._boundDownToSinks, sinkMessages);
719
+ /** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
720
+ _recordMutation(actor) {
721
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
722
+ }
723
+ /**
724
+ * At-most-once deactivation guard. Both TEARDOWN (eager) and
725
+ * unsubscribe-body (lazy) call this. The `_active` flag ensures
726
+ * `_doDeactivate` runs exactly once per activation cycle.
727
+ */
728
+ _onDeactivate() {
729
+ if (!this._active) return;
730
+ this._active = false;
731
+ this._doDeactivate();
752
732
  }
733
+ // --- Subscribe (uniform across node shapes) ---
753
734
  subscribe(sink, hints) {
754
735
  if (hints?.actor != null && this._guard != null) {
755
736
  const actor = normalizeActor(hints.actor);
@@ -757,17 +738,21 @@ var NodeImpl = class {
757
738
  throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
758
739
  }
759
740
  }
760
- if (this._terminal && this._opts.resubscribable) {
741
+ if (this._terminal && this._resubscribable) {
761
742
  this._terminal = false;
762
743
  this._cached = NO_VALUE;
763
- this._status = this._hasDeps ? "disconnected" : "settled";
764
- this._opts.onResubscribe?.();
744
+ this._status = "disconnected";
745
+ this._onResubscribe?.();
765
746
  }
766
747
  this._sinkCount += 1;
767
748
  if (hints?.singleDep) {
768
749
  this._singleDepSinkCount += 1;
769
750
  this._singleDepSinks.add(sink);
770
751
  }
752
+ if (!this._terminal) {
753
+ const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
754
+ downWithBatch(sink, startMessages);
755
+ }
771
756
  if (this._sinks == null) {
772
757
  this._sinks = sink;
773
758
  } else if (typeof this._sinks === "function") {
@@ -775,10 +760,12 @@ var NodeImpl = class {
775
760
  } else {
776
761
  this._sinks.add(sink);
777
762
  }
778
- if (this._hasDeps) {
779
- this._connectUpstream();
780
- } else if (this._fn) {
781
- this._startProducer();
763
+ if (this._sinkCount === 1 && !this._terminal) {
764
+ this._active = true;
765
+ this._onActivate();
766
+ }
767
+ if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
768
+ this._status = "pending";
782
769
  }
783
770
  let removed = false;
784
771
  return () => {
@@ -802,39 +789,49 @@ var NodeImpl = class {
802
789
  }
803
790
  }
804
791
  if (this._sinks == null) {
805
- this._disconnectUpstream();
806
- this._stopProducer();
792
+ this._onDeactivate();
807
793
  }
808
794
  };
809
795
  }
810
- up(messages, options) {
811
- if (!this._hasDeps) return;
812
- if (!options?.internal && this._guard != null) {
813
- const actor = normalizeActor(options?.actor);
814
- if (!this._guard(actor, "write")) {
815
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
816
- }
817
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
796
+ // --- Down pipeline ---
797
+ /**
798
+ * Core outgoing dispatch. Applies terminal filter + local lifecycle
799
+ * update, then hands messages to `downWithBatch` for tier-aware delivery.
800
+ */
801
+ _downInternal(messages) {
802
+ if (messages.length === 0) return;
803
+ let sinkMessages = messages;
804
+ if (this._terminal && !this._resubscribable) {
805
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
806
+ if (pass.length === 0) return;
807
+ sinkMessages = pass;
818
808
  }
819
- for (const dep of this._deps) {
820
- if (options === void 0) {
821
- dep.up?.(messages);
822
- } else {
823
- dep.up?.(messages, options);
809
+ this._handleLocalLifecycle(sinkMessages);
810
+ if (this._canSkipDirty()) {
811
+ let hasPhase2 = false;
812
+ for (let i = 0; i < sinkMessages.length; i++) {
813
+ const t = sinkMessages[i][0];
814
+ if (t === DATA || t === RESOLVED) {
815
+ hasPhase2 = true;
816
+ break;
817
+ }
818
+ }
819
+ if (hasPhase2) {
820
+ const filtered = [];
821
+ for (let i = 0; i < sinkMessages.length; i++) {
822
+ if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
823
+ }
824
+ if (filtered.length > 0) {
825
+ downWithBatch(this._boundDownToSinks, filtered);
826
+ }
827
+ return;
824
828
  }
825
829
  }
830
+ downWithBatch(this._boundDownToSinks, sinkMessages);
826
831
  }
827
- _upInternal(messages) {
828
- if (!this._hasDeps) return;
829
- for (const dep of this._deps) {
830
- dep.up?.(messages, { internal: true });
831
- }
832
- }
833
- unsubscribe() {
834
- if (!this._hasDeps) return;
835
- this._disconnectUpstream();
832
+ _canSkipDirty() {
833
+ return this._sinkCount === 1 && this._singleDepSinkCount === 1;
836
834
  }
837
- // --- Private methods (prototype, _ prefix) ---
838
835
  _downToSinks(messages) {
839
836
  if (this._sinks == null) return;
840
837
  if (typeof this._sinks === "function") {
@@ -846,6 +843,11 @@ var NodeImpl = class {
846
843
  sink(messages);
847
844
  }
848
845
  }
846
+ /**
847
+ * Update `_cached`, `_status`, `_terminal` from message batch before
848
+ * delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
849
+ * {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
850
+ */
849
851
  _handleLocalLifecycle(messages) {
850
852
  for (const m of messages) {
851
853
  const t = m[0];
@@ -859,28 +861,22 @@ var NodeImpl = class {
859
861
  }
860
862
  }
861
863
  if (t === INVALIDATE) {
862
- const cleanupFn = this._cleanup;
863
- this._cleanup = void 0;
864
- cleanupFn?.();
864
+ this._onInvalidate();
865
865
  this._cached = NO_VALUE;
866
- this._lastDepValues = void 0;
867
866
  }
868
867
  this._status = statusAfterMessage(this._status, m);
869
868
  if (t === COMPLETE || t === ERROR) {
870
869
  this._terminal = true;
871
870
  }
872
871
  if (t === TEARDOWN) {
873
- if (this._opts.resetOnTeardown) {
872
+ if (this._resetOnTeardown) {
874
873
  this._cached = NO_VALUE;
875
874
  }
876
- const teardownCleanup = this._cleanup;
877
- this._cleanup = void 0;
878
- teardownCleanup?.();
875
+ this._onTeardown();
879
876
  try {
880
877
  this._propagateToMeta(t);
881
878
  } finally {
882
- this._disconnectUpstream();
883
- this._stopProducer();
879
+ this._onDeactivate();
884
880
  }
885
881
  }
886
882
  if (t !== TEARDOWN && propagatesToMeta(t)) {
@@ -888,7 +884,20 @@ var NodeImpl = class {
888
884
  }
889
885
  }
890
886
  }
891
- /** Propagate a signal to all companion meta nodes (best-effort). */
887
+ /**
888
+ * Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
889
+ * cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
890
+ * drop `_lastDepValues` so the next wave re-runs fn.
891
+ */
892
+ _onInvalidate() {
893
+ }
894
+ /**
895
+ * Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
896
+ * {@link NodeImpl} uses this to run any pending cleanup fn.
897
+ */
898
+ _onTeardown() {
899
+ }
900
+ /** Forward a signal to all companion meta nodes (best-effort). */
892
901
  _propagateToMeta(t) {
893
902
  for (const metaNode of Object.values(this.meta)) {
894
903
  try {
@@ -897,9 +906,10 @@ var NodeImpl = class {
897
906
  }
898
907
  }
899
908
  }
900
- _canSkipDirty() {
901
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
902
- }
909
+ /**
910
+ * Frame a computed value into the right protocol messages and dispatch
911
+ * via `_downInternal`. Used by `_runFn` and `actions.emit`.
912
+ */
903
913
  _downAutoValue(value) {
904
914
  const wasDirty = this._status === "dirty";
905
915
  let unchanged;
@@ -907,7 +917,9 @@ var NodeImpl = class {
907
917
  unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
908
918
  } catch (eqErr) {
909
919
  const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
910
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
920
+ const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
921
+ cause: eqErr
922
+ });
911
923
  this._downInternal([[ERROR, wrapped]]);
912
924
  return;
913
925
  }
@@ -917,89 +929,173 @@ var NodeImpl = class {
917
929
  }
918
930
  this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
919
931
  }
920
- _runFn() {
921
- if (!this._fn) return;
922
- if (this._terminal && !this._opts.resubscribable) return;
923
- if (this._connecting) return;
924
- try {
925
- const n = this._deps.length;
926
- const depValues = new Array(n);
927
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
928
- const prev = this._lastDepValues;
929
- if (n > 0 && prev != null && prev.length === n) {
930
- let allSame = true;
931
- for (let i = 0; i < n; i++) {
932
- if (!Object.is(depValues[i], prev[i])) {
933
- allSame = false;
934
- break;
935
- }
936
- }
937
- if (allSame) {
938
- if (this._status === "dirty") {
939
- this._downInternal([[RESOLVED]]);
940
- }
941
- return;
942
- }
943
- }
944
- const prevCleanup = this._cleanup;
945
- this._cleanup = void 0;
946
- prevCleanup?.();
947
- this._manualEmitUsed = false;
948
- this._lastDepValues = depValues;
949
- this._inspectorHook?.({ kind: "run", depValues });
950
- const out = this._fn(depValues, this._actions);
951
- if (isCleanupResult(out)) {
952
- this._cleanup = out.cleanup;
953
- if (this._manualEmitUsed) return;
954
- if ("value" in out) {
955
- this._downAutoValue(out.value);
956
- }
957
- return;
958
- }
959
- if (isCleanupFn(out)) {
960
- this._cleanup = out;
961
- return;
962
- }
963
- if (this._manualEmitUsed) return;
964
- if (out === void 0) return;
965
- this._downAutoValue(out);
966
- } catch (err) {
967
- const errMsg = err instanceof Error ? err.message : String(err);
968
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
969
- this._downInternal([[ERROR, wrapped]]);
970
- }
971
- }
972
- _onDepDirty(index) {
973
- const wasDirty = this._depDirtyMask.has(index);
974
- this._depDirtyMask.set(index);
975
- this._depSettledMask.clear(index);
976
- if (!wasDirty) {
977
- this._downInternal([[DIRTY]]);
932
+ };
933
+
934
+ // src/core/node.ts
935
+ var NodeImpl = class extends NodeBase {
936
+ // --- Dep configuration (set once) ---
937
+ _deps;
938
+ _fn;
939
+ _opts;
940
+ _hasDeps;
941
+ _isSingleDep;
942
+ _autoComplete;
943
+ // --- Wave tracking masks ---
944
+ _depDirtyMask;
945
+ _depSettledMask;
946
+ _depCompleteMask;
947
+ _allDepsCompleteMask;
948
+ // --- Identity-skip optimization ---
949
+ _lastDepValues;
950
+ _cleanup;
951
+ // --- Upstream bookkeeping ---
952
+ _upstreamUnsubs = [];
953
+ // --- Fn behavior flag ---
954
+ /** @internal Read by `describeNode` to infer `"operator"` label. */
955
+ _manualEmitUsed = false;
956
+ constructor(deps, fn, opts) {
957
+ super(opts);
958
+ this._deps = deps;
959
+ this._fn = fn;
960
+ this._opts = opts;
961
+ this._hasDeps = deps.length > 0;
962
+ this._isSingleDep = deps.length === 1 && fn != null;
963
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
964
+ if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
965
+ this._status = "settled";
978
966
  }
967
+ this._depDirtyMask = createBitSet(deps.length);
968
+ this._depSettledMask = createBitSet(deps.length);
969
+ this._depCompleteMask = createBitSet(deps.length);
970
+ this._allDepsCompleteMask = createBitSet(deps.length);
971
+ for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
972
+ this.down = this.down.bind(this);
973
+ this.up = this.up.bind(this);
979
974
  }
980
- _onDepSettled(index) {
981
- if (!this._depDirtyMask.has(index)) {
982
- this._onDepDirty(index);
975
+ // --- Meta node factory (called from base constructor) ---
976
+ _createMetaNode(key, initialValue, opts) {
977
+ return node({
978
+ initial: initialValue,
979
+ name: `${opts.name ?? "node"}:meta:${key}`,
980
+ describeKind: "state",
981
+ ...opts.guard != null ? { guard: opts.guard } : {}
982
+ });
983
+ }
984
+ // --- Manual emit tracker (set by actions.down / actions.emit) ---
985
+ _onManualEmit() {
986
+ this._manualEmitUsed = true;
987
+ }
988
+ // --- Up / unsubscribe ---
989
+ up(messages, options) {
990
+ if (!this._hasDeps) return;
991
+ if (!options?.internal && this._guard != null) {
992
+ const actor = normalizeActor(options?.actor);
993
+ if (!this._guard(actor, "write")) {
994
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
995
+ }
996
+ this._recordMutation(actor);
983
997
  }
984
- this._depSettledMask.set(index);
985
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
986
- this._depDirtyMask.reset();
987
- this._depSettledMask.reset();
998
+ for (const dep of this._deps) {
999
+ if (options === void 0) {
1000
+ dep.up?.(messages);
1001
+ } else {
1002
+ dep.up?.(messages, options);
1003
+ }
1004
+ }
1005
+ }
1006
+ _upInternal(messages) {
1007
+ if (!this._hasDeps) return;
1008
+ for (const dep of this._deps) {
1009
+ dep.up?.(messages, { internal: true });
1010
+ }
1011
+ }
1012
+ unsubscribe() {
1013
+ if (!this._hasDeps) return;
1014
+ this._disconnectUpstream();
1015
+ }
1016
+ // --- Activation (first-subscriber / last-subscriber hooks) ---
1017
+ _onActivate() {
1018
+ if (this._hasDeps) {
1019
+ this._connectUpstream();
1020
+ return;
1021
+ }
1022
+ if (this._fn) {
988
1023
  this._runFn();
1024
+ return;
989
1025
  }
990
1026
  }
991
- _maybeCompleteFromDeps() {
992
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
993
- this._downInternal([[COMPLETE]]);
1027
+ _doDeactivate() {
1028
+ this._disconnectUpstream();
1029
+ const cleanup = this._cleanup;
1030
+ this._cleanup = void 0;
1031
+ cleanup?.();
1032
+ if (this._fn != null) {
1033
+ this._cached = NO_VALUE;
1034
+ this._lastDepValues = void 0;
1035
+ }
1036
+ if (this._hasDeps || this._fn != null) {
1037
+ this._status = "disconnected";
1038
+ }
1039
+ }
1040
+ // --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
1041
+ _onInvalidate() {
1042
+ const cleanup = this._cleanup;
1043
+ this._cleanup = void 0;
1044
+ cleanup?.();
1045
+ this._lastDepValues = void 0;
1046
+ }
1047
+ _onTeardown() {
1048
+ const cleanup = this._cleanup;
1049
+ this._cleanup = void 0;
1050
+ cleanup?.();
1051
+ }
1052
+ // --- Upstream connect / disconnect ---
1053
+ _connectUpstream() {
1054
+ if (!this._hasDeps) return;
1055
+ if (this._upstreamUnsubs.length > 0) return;
1056
+ this._depDirtyMask.setAll();
1057
+ this._depSettledMask.reset();
1058
+ this._depCompleteMask.reset();
1059
+ const depValuesBefore = this._lastDepValues;
1060
+ const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1061
+ for (let i = 0; i < this._deps.length; i += 1) {
1062
+ const dep = this._deps[i];
1063
+ this._upstreamUnsubs.push(
1064
+ dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1065
+ );
1066
+ }
1067
+ if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
1068
+ this._runFn();
1069
+ }
1070
+ }
1071
+ _disconnectUpstream() {
1072
+ if (this._upstreamUnsubs.length === 0) return;
1073
+ for (const unsub of this._upstreamUnsubs.splice(0)) {
1074
+ unsub();
994
1075
  }
1076
+ this._depDirtyMask.reset();
1077
+ this._depSettledMask.reset();
1078
+ this._depCompleteMask.reset();
995
1079
  }
1080
+ // --- Wave handling ---
996
1081
  _handleDepMessages(index, messages) {
997
1082
  for (const msg of messages) {
998
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1083
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
999
1084
  const t = msg[0];
1000
1085
  if (this._onMessage) {
1001
1086
  try {
1002
- if (this._onMessage(msg, index, this._actions)) continue;
1087
+ const consumed = this._onMessage(msg, index, this._actions);
1088
+ if (consumed) {
1089
+ if (t === START) {
1090
+ this._depDirtyMask.clear(index);
1091
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1092
+ this._depDirtyMask.reset();
1093
+ this._depSettledMask.reset();
1094
+ this._runFn();
1095
+ }
1096
+ }
1097
+ continue;
1098
+ }
1003
1099
  } catch (err) {
1004
1100
  const errMsg = err instanceof Error ? err.message : String(err);
1005
1101
  const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
@@ -1009,6 +1105,7 @@ var NodeImpl = class {
1009
1105
  return;
1010
1106
  }
1011
1107
  }
1108
+ if (messageTier(t) < 1) continue;
1012
1109
  if (!this._fn) {
1013
1110
  if (t === COMPLETE && this._deps.length > 1) {
1014
1111
  this._depCompleteMask.set(index);
@@ -1052,53 +1149,85 @@ var NodeImpl = class {
1052
1149
  this._downInternal([msg]);
1053
1150
  }
1054
1151
  }
1055
- _connectUpstream() {
1056
- if (!this._hasDeps || this._connected) return;
1057
- this._connected = true;
1058
- this._depDirtyMask.reset();
1059
- this._depSettledMask.reset();
1060
- this._depCompleteMask.reset();
1061
- this._status = "settled";
1062
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1063
- this._connecting = true;
1064
- try {
1065
- for (let i = 0; i < this._deps.length; i += 1) {
1066
- const dep = this._deps[i];
1067
- this._upstreamUnsubs.push(
1068
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1069
- );
1070
- }
1071
- } finally {
1072
- this._connecting = false;
1152
+ _onDepDirty(index) {
1153
+ const wasDirty = this._depDirtyMask.has(index);
1154
+ this._depDirtyMask.set(index);
1155
+ this._depSettledMask.clear(index);
1156
+ if (!wasDirty) {
1157
+ this._downInternal([[DIRTY]]);
1073
1158
  }
1074
- if (this._fn) {
1159
+ }
1160
+ _onDepSettled(index) {
1161
+ if (!this._depDirtyMask.has(index)) {
1162
+ this._onDepDirty(index);
1163
+ }
1164
+ this._depSettledMask.set(index);
1165
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1166
+ this._depDirtyMask.reset();
1167
+ this._depSettledMask.reset();
1075
1168
  this._runFn();
1076
1169
  }
1077
1170
  }
1078
- _stopProducer() {
1079
- if (!this._producerStarted) return;
1080
- this._producerStarted = false;
1081
- const producerCleanup = this._cleanup;
1082
- this._cleanup = void 0;
1083
- producerCleanup?.();
1084
- }
1085
- _startProducer() {
1086
- if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
1087
- this._producerStarted = true;
1088
- this._runFn();
1171
+ _maybeCompleteFromDeps() {
1172
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1173
+ this._downInternal([[COMPLETE]]);
1174
+ }
1089
1175
  }
1090
- _disconnectUpstream() {
1091
- if (!this._connected) return;
1092
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1093
- unsub();
1176
+ // --- Fn execution ---
1177
+ _runFn() {
1178
+ if (!this._fn) return;
1179
+ if (this._terminal && !this._resubscribable) return;
1180
+ try {
1181
+ const n = this._deps.length;
1182
+ const depValues = new Array(n);
1183
+ for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1184
+ const prev = this._lastDepValues;
1185
+ if (n > 0 && prev != null && prev.length === n) {
1186
+ let allSame = true;
1187
+ for (let i = 0; i < n; i++) {
1188
+ if (!Object.is(depValues[i], prev[i])) {
1189
+ allSame = false;
1190
+ break;
1191
+ }
1192
+ }
1193
+ if (allSame) {
1194
+ if (this._status === "dirty") {
1195
+ this._downInternal([[RESOLVED]]);
1196
+ }
1197
+ return;
1198
+ }
1199
+ }
1200
+ const prevCleanup = this._cleanup;
1201
+ this._cleanup = void 0;
1202
+ prevCleanup?.();
1203
+ this._manualEmitUsed = false;
1204
+ this._lastDepValues = depValues;
1205
+ this._emitInspectorHook({ kind: "run", depValues });
1206
+ const out = this._fn(depValues, this._actions);
1207
+ if (isCleanupResult(out)) {
1208
+ this._cleanup = out.cleanup;
1209
+ if (this._manualEmitUsed) return;
1210
+ if ("value" in out) {
1211
+ this._downAutoValue(out.value);
1212
+ }
1213
+ return;
1214
+ }
1215
+ if (isCleanupFn(out)) {
1216
+ this._cleanup = out;
1217
+ return;
1218
+ }
1219
+ if (this._manualEmitUsed) return;
1220
+ if (out === void 0) return;
1221
+ this._downAutoValue(out);
1222
+ } catch (err) {
1223
+ const errMsg = err instanceof Error ? err.message : String(err);
1224
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1225
+ this._downInternal([[ERROR, wrapped]]);
1094
1226
  }
1095
- this._connected = false;
1096
- this._depDirtyMask.reset();
1097
- this._depSettledMask.reset();
1098
- this._depCompleteMask.reset();
1099
- this._status = "disconnected";
1100
1227
  }
1101
1228
  };
1229
+ var isNodeArray = (value) => Array.isArray(value);
1230
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1102
1231
  function node(depsOrFn, fnOrOpts, optsArg) {
1103
1232
  const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1104
1233
  const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
@@ -1165,230 +1294,47 @@ function bridge(from, to, opts) {
1165
1294
  }
1166
1295
 
1167
1296
  // src/core/dynamic-node.ts
1297
+ var MAX_RERUN = 16;
1168
1298
  function dynamicNode(fn, opts) {
1169
1299
  return new DynamicNodeImpl(fn, opts ?? {});
1170
1300
  }
1171
- var DynamicNodeImpl = class {
1172
- _optsName;
1173
- _registryName;
1174
- _describeKind;
1175
- meta;
1301
+ var DynamicNodeImpl = class extends NodeBase {
1176
1302
  _fn;
1177
- _equals;
1178
- _resubscribable;
1179
- _resetOnTeardown;
1180
1303
  _autoComplete;
1181
- _onMessage;
1182
- _onResubscribe;
1183
- /** @internal — read by {@link describeNode} for `accessHintForGuard`. */
1184
- _guard;
1185
- _lastMutation;
1186
- _inspectorHook;
1187
- // Sink tracking
1188
- _sinkCount = 0;
1189
- _singleDepSinkCount = 0;
1190
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
1191
- // Actions object (for onMessage handler)
1192
- _actions;
1193
- _boundDownToSinks;
1194
- // Mutable state
1195
- _cached = NO_VALUE;
1196
- _status = "disconnected";
1197
- _terminal = false;
1198
- _connected = false;
1199
- _rewiring = false;
1200
- // re-entrancy guard
1201
1304
  // Dynamic deps tracking
1305
+ /** @internal Read by `describeNode`. */
1202
1306
  _deps = [];
1203
1307
  _depUnsubs = [];
1204
1308
  _depIndexMap = /* @__PURE__ */ new Map();
1205
- // node index in _deps
1206
- _dirtyBits = /* @__PURE__ */ new Set();
1207
- _settledBits = /* @__PURE__ */ new Set();
1208
- _completeBits = /* @__PURE__ */ new Set();
1209
- // Sinks
1210
- _sinks = null;
1309
+ _depDirtyBits = /* @__PURE__ */ new Set();
1310
+ _depSettledBits = /* @__PURE__ */ new Set();
1311
+ _depCompleteBits = /* @__PURE__ */ new Set();
1312
+ // Execution state
1313
+ _running = false;
1314
+ _rewiring = false;
1315
+ _bufferedDepMessages = [];
1316
+ _trackedValues = /* @__PURE__ */ new Map();
1317
+ _rerunCount = 0;
1211
1318
  constructor(fn, opts) {
1319
+ super(opts);
1212
1320
  this._fn = fn;
1213
- this._optsName = opts.name;
1214
- this._describeKind = opts.describeKind;
1215
- this._equals = opts.equals ?? Object.is;
1216
- this._resubscribable = opts.resubscribable ?? false;
1217
- this._resetOnTeardown = opts.resetOnTeardown ?? false;
1218
1321
  this._autoComplete = opts.completeWhenDepsComplete ?? true;
1219
- this._onMessage = opts.onMessage;
1220
- this._onResubscribe = opts.onResubscribe;
1221
- this._guard = opts.guard;
1222
- this._inspectorHook = void 0;
1223
- const meta = {};
1224
- for (const [k, v] of Object.entries(opts.meta ?? {})) {
1225
- meta[k] = node({
1226
- initial: v,
1227
- name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
1228
- describeKind: "state",
1229
- ...opts.guard != null ? { guard: opts.guard } : {}
1230
- });
1231
- }
1232
- Object.freeze(meta);
1233
- this.meta = meta;
1234
- const self = this;
1235
- this._actions = {
1236
- down(messages) {
1237
- self._downInternal(messages);
1238
- },
1239
- emit(value) {
1240
- self._downAutoValue(value);
1241
- },
1242
- up(messages) {
1243
- for (const dep of self._deps) {
1244
- dep.up?.(messages, { internal: true });
1245
- }
1246
- }
1247
- };
1248
- this._boundDownToSinks = this._downToSinks.bind(this);
1249
- }
1250
- get name() {
1251
- return this._registryName ?? this._optsName;
1252
- }
1253
- /** @internal */
1254
- _assignRegistryName(localName) {
1255
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
1256
- this._registryName = localName;
1257
- }
1258
- /**
1259
- * @internal Attach/remove inspector hook for graph-level observability.
1260
- * Returns a disposer that restores the previous hook.
1261
- */
1262
- _setInspectorHook(hook) {
1263
- const prev = this._inspectorHook;
1264
- this._inspectorHook = hook;
1265
- return () => {
1266
- if (this._inspectorHook === hook) {
1267
- this._inspectorHook = prev;
1268
- }
1269
- };
1270
- }
1271
- get status() {
1272
- return this._status;
1322
+ this.down = this.down.bind(this);
1323
+ this.up = this.up.bind(this);
1273
1324
  }
1274
- get lastMutation() {
1275
- return this._lastMutation;
1325
+ _createMetaNode(key, initialValue, opts) {
1326
+ return node({
1327
+ initial: initialValue,
1328
+ name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
1329
+ describeKind: "state",
1330
+ ...opts.guard != null ? { guard: opts.guard } : {}
1331
+ });
1276
1332
  }
1277
- /** Versioning not yet supported on DynamicNodeImpl. */
1333
+ /** Versioning not supported on DynamicNodeImpl (override base). */
1278
1334
  get v() {
1279
1335
  return void 0;
1280
1336
  }
1281
- hasGuard() {
1282
- return this._guard != null;
1283
- }
1284
- allowsObserve(actor) {
1285
- if (this._guard == null) return true;
1286
- return this._guard(normalizeActor(actor), "observe");
1287
- }
1288
- get() {
1289
- return this._cached === NO_VALUE ? void 0 : this._cached;
1290
- }
1291
- down(messages, options) {
1292
- if (messages.length === 0) return;
1293
- if (!options?.internal && this._guard != null) {
1294
- const actor = normalizeActor(options?.actor);
1295
- const delivery = options?.delivery ?? "write";
1296
- const action = delivery === "signal" ? "signal" : "write";
1297
- if (!this._guard(actor, action)) {
1298
- throw new GuardDenied({ actor, action, nodeName: this.name });
1299
- }
1300
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1301
- }
1302
- this._downInternal(messages);
1303
- }
1304
- _downInternal(messages) {
1305
- if (messages.length === 0) return;
1306
- let sinkMessages = messages;
1307
- if (this._terminal && !this._resubscribable) {
1308
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
1309
- if (pass.length === 0) return;
1310
- sinkMessages = pass;
1311
- }
1312
- this._handleLocalLifecycle(sinkMessages);
1313
- if (this._canSkipDirty()) {
1314
- let hasPhase2 = false;
1315
- for (let i = 0; i < sinkMessages.length; i++) {
1316
- const t = sinkMessages[i][0];
1317
- if (t === DATA || t === RESOLVED) {
1318
- hasPhase2 = true;
1319
- break;
1320
- }
1321
- }
1322
- if (hasPhase2) {
1323
- const filtered = [];
1324
- for (let i = 0; i < sinkMessages.length; i++) {
1325
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
1326
- }
1327
- if (filtered.length > 0) {
1328
- downWithBatch(this._boundDownToSinks, filtered);
1329
- }
1330
- return;
1331
- }
1332
- }
1333
- downWithBatch(this._boundDownToSinks, sinkMessages);
1334
- }
1335
- _canSkipDirty() {
1336
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
1337
- }
1338
- subscribe(sink, hints) {
1339
- if (hints?.actor != null && this._guard != null) {
1340
- const actor = normalizeActor(hints.actor);
1341
- if (!this._guard(actor, "observe")) {
1342
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
1343
- }
1344
- }
1345
- if (this._terminal && this._resubscribable) {
1346
- this._terminal = false;
1347
- this._cached = NO_VALUE;
1348
- this._status = "disconnected";
1349
- this._onResubscribe?.();
1350
- }
1351
- this._sinkCount += 1;
1352
- if (hints?.singleDep) {
1353
- this._singleDepSinkCount += 1;
1354
- this._singleDepSinks.add(sink);
1355
- }
1356
- if (this._sinks == null) {
1357
- this._sinks = sink;
1358
- } else if (typeof this._sinks === "function") {
1359
- this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
1360
- } else {
1361
- this._sinks.add(sink);
1362
- }
1363
- if (!this._connected) {
1364
- this._connect();
1365
- }
1366
- let removed = false;
1367
- return () => {
1368
- if (removed) return;
1369
- removed = true;
1370
- this._sinkCount -= 1;
1371
- if (this._singleDepSinks.has(sink)) {
1372
- this._singleDepSinkCount -= 1;
1373
- this._singleDepSinks.delete(sink);
1374
- }
1375
- if (this._sinks == null) return;
1376
- if (typeof this._sinks === "function") {
1377
- if (this._sinks === sink) this._sinks = null;
1378
- } else {
1379
- this._sinks.delete(sink);
1380
- if (this._sinks.size === 1) {
1381
- const [only] = this._sinks;
1382
- this._sinks = only;
1383
- } else if (this._sinks.size === 0) {
1384
- this._sinks = null;
1385
- }
1386
- }
1387
- if (this._sinks == null) {
1388
- this._disconnect();
1389
- }
1390
- };
1391
- }
1337
+ // --- Up / unsubscribe ---
1392
1338
  up(messages, options) {
1393
1339
  if (this._deps.length === 0) return;
1394
1340
  if (!options?.internal && this._guard != null) {
@@ -1396,221 +1342,227 @@ var DynamicNodeImpl = class {
1396
1342
  if (!this._guard(actor, "write")) {
1397
1343
  throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1398
1344
  }
1399
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1345
+ this._recordMutation(actor);
1400
1346
  }
1401
1347
  for (const dep of this._deps) {
1402
1348
  dep.up?.(messages, options);
1403
1349
  }
1404
1350
  }
1405
- unsubscribe() {
1406
- this._disconnect();
1407
- }
1408
- // --- Private methods ---
1409
- _downToSinks(messages) {
1410
- if (this._sinks == null) return;
1411
- if (typeof this._sinks === "function") {
1412
- this._sinks(messages);
1413
- return;
1414
- }
1415
- const snapshot = [...this._sinks];
1416
- for (const sink of snapshot) {
1417
- sink(messages);
1418
- }
1419
- }
1420
- _handleLocalLifecycle(messages) {
1421
- for (const m of messages) {
1422
- const t = m[0];
1423
- if (t === DATA) this._cached = m[1];
1424
- if (t === INVALIDATE) {
1425
- this._cached = NO_VALUE;
1426
- this._status = "dirty";
1427
- }
1428
- if (t === DATA) {
1429
- this._status = "settled";
1430
- } else if (t === RESOLVED) {
1431
- this._status = "resolved";
1432
- } else if (t === DIRTY) {
1433
- this._status = "dirty";
1434
- } else if (t === COMPLETE) {
1435
- this._status = "completed";
1436
- this._terminal = true;
1437
- } else if (t === ERROR) {
1438
- this._status = "errored";
1439
- this._terminal = true;
1440
- }
1441
- if (t === TEARDOWN) {
1442
- if (this._resetOnTeardown) this._cached = NO_VALUE;
1443
- try {
1444
- this._propagateToMeta(t);
1445
- } finally {
1446
- this._disconnect();
1447
- }
1448
- }
1449
- if (t !== TEARDOWN && propagatesToMeta(t)) {
1450
- this._propagateToMeta(t);
1451
- }
1452
- }
1453
- }
1454
- /** Propagate a signal to all companion meta nodes (best-effort). */
1455
- _propagateToMeta(t) {
1456
- for (const metaNode of Object.values(this.meta)) {
1457
- try {
1458
- metaNode.down([[t]], { internal: true });
1459
- } catch {
1460
- }
1351
+ _upInternal(messages) {
1352
+ for (const dep of this._deps) {
1353
+ dep.up?.(messages, { internal: true });
1461
1354
  }
1462
1355
  }
1463
- _downAutoValue(value) {
1464
- const wasDirty = this._status === "dirty";
1465
- let unchanged;
1466
- try {
1467
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
1468
- } catch (eqErr) {
1469
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
1470
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
1471
- this._downInternal([[ERROR, wrapped]]);
1472
- return;
1473
- }
1474
- if (unchanged) {
1475
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
1476
- return;
1477
- }
1478
- this._cached = value;
1479
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
1356
+ unsubscribe() {
1357
+ this._disconnect();
1480
1358
  }
1481
- _connect() {
1482
- if (this._connected) return;
1483
- this._connected = true;
1484
- this._status = "settled";
1485
- this._dirtyBits.clear();
1486
- this._settledBits.clear();
1487
- this._completeBits.clear();
1359
+ // --- Activation hooks ---
1360
+ _onActivate() {
1488
1361
  this._runFn();
1489
1362
  }
1363
+ _doDeactivate() {
1364
+ this._disconnect();
1365
+ }
1490
1366
  _disconnect() {
1491
- if (!this._connected) return;
1492
1367
  for (const unsub of this._depUnsubs) unsub();
1493
1368
  this._depUnsubs = [];
1494
1369
  this._deps = [];
1495
1370
  this._depIndexMap.clear();
1496
- this._dirtyBits.clear();
1497
- this._settledBits.clear();
1498
- this._completeBits.clear();
1499
- this._connected = false;
1371
+ this._depDirtyBits.clear();
1372
+ this._depSettledBits.clear();
1373
+ this._depCompleteBits.clear();
1374
+ this._cached = NO_VALUE;
1500
1375
  this._status = "disconnected";
1501
1376
  }
1377
+ // --- Fn execution with rewire buffer ---
1502
1378
  _runFn() {
1503
1379
  if (this._terminal && !this._resubscribable) return;
1504
- if (this._rewiring) return;
1505
- const trackedDeps = [];
1506
- const trackedSet = /* @__PURE__ */ new Set();
1507
- const get = (dep) => {
1508
- if (!trackedSet.has(dep)) {
1509
- trackedSet.add(dep);
1510
- trackedDeps.push(dep);
1511
- }
1512
- return dep.get();
1513
- };
1380
+ if (this._running) return;
1381
+ this._running = true;
1382
+ this._rerunCount = 0;
1383
+ let result;
1514
1384
  try {
1515
- const depValues = [];
1516
- for (const dep of this._deps) {
1517
- depValues.push(dep.get());
1385
+ for (; ; ) {
1386
+ const trackedDeps = [];
1387
+ const trackedValuesMap = /* @__PURE__ */ new Map();
1388
+ const trackedSet = /* @__PURE__ */ new Set();
1389
+ const get = (dep) => {
1390
+ if (!trackedSet.has(dep)) {
1391
+ trackedSet.add(dep);
1392
+ trackedDeps.push(dep);
1393
+ trackedValuesMap.set(dep, dep.get());
1394
+ }
1395
+ return dep.get();
1396
+ };
1397
+ this._trackedValues = trackedValuesMap;
1398
+ const depValues = [];
1399
+ for (const dep of this._deps) depValues.push(dep.get());
1400
+ this._emitInspectorHook({ kind: "run", depValues });
1401
+ try {
1402
+ result = this._fn(get);
1403
+ } catch (err) {
1404
+ const errMsg = err instanceof Error ? err.message : String(err);
1405
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
1406
+ cause: err
1407
+ });
1408
+ this._downInternal([[ERROR, wrapped]]);
1409
+ return;
1410
+ }
1411
+ this._rewiring = true;
1412
+ this._bufferedDepMessages = [];
1413
+ try {
1414
+ this._rewire(trackedDeps);
1415
+ } finally {
1416
+ this._rewiring = false;
1417
+ }
1418
+ let needsRerun = false;
1419
+ for (const entry of this._bufferedDepMessages) {
1420
+ for (const msg of entry.msgs) {
1421
+ if (msg[0] === DATA) {
1422
+ const dep = this._deps[entry.index];
1423
+ const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
1424
+ const actualValue = msg[1];
1425
+ if (!this._equals(trackedValue, actualValue)) {
1426
+ needsRerun = true;
1427
+ break;
1428
+ }
1429
+ }
1430
+ }
1431
+ if (needsRerun) break;
1432
+ }
1433
+ if (needsRerun) {
1434
+ this._rerunCount += 1;
1435
+ if (this._rerunCount > MAX_RERUN) {
1436
+ this._bufferedDepMessages = [];
1437
+ this._downInternal([
1438
+ [
1439
+ ERROR,
1440
+ new Error(
1441
+ `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
1442
+ )
1443
+ ]
1444
+ ]);
1445
+ return;
1446
+ }
1447
+ this._bufferedDepMessages = [];
1448
+ continue;
1449
+ }
1450
+ const drain = this._bufferedDepMessages;
1451
+ this._bufferedDepMessages = [];
1452
+ for (const entry of drain) {
1453
+ for (const msg of entry.msgs) {
1454
+ this._updateMasksForMessage(entry.index, msg);
1455
+ }
1456
+ }
1457
+ this._depDirtyBits.clear();
1458
+ this._depSettledBits.clear();
1459
+ break;
1518
1460
  }
1519
- this._inspectorHook?.({ kind: "run", depValues });
1520
- const result = this._fn(get);
1521
- this._rewire(trackedDeps);
1522
- if (result === void 0) return;
1523
- this._downAutoValue(result);
1524
- } catch (err) {
1525
- this._downInternal([[ERROR, err]]);
1461
+ } finally {
1462
+ this._running = false;
1526
1463
  }
1464
+ this._downAutoValue(result);
1527
1465
  }
1528
1466
  _rewire(newDeps) {
1529
- this._rewiring = true;
1530
- try {
1531
- const oldMap = this._depIndexMap;
1532
- const newMap = /* @__PURE__ */ new Map();
1533
- const newUnsubs = [];
1534
- for (let i = 0; i < newDeps.length; i++) {
1535
- const dep = newDeps[i];
1536
- newMap.set(dep, i);
1537
- const oldIdx = oldMap.get(dep);
1538
- if (oldIdx !== void 0) {
1539
- newUnsubs.push(this._depUnsubs[oldIdx]);
1540
- this._depUnsubs[oldIdx] = () => {
1541
- };
1542
- } else {
1543
- const idx = i;
1544
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1545
- newUnsubs.push(unsub);
1546
- }
1467
+ const oldMap = this._depIndexMap;
1468
+ const newMap = /* @__PURE__ */ new Map();
1469
+ const newUnsubs = [];
1470
+ for (let i = 0; i < newDeps.length; i++) {
1471
+ const dep = newDeps[i];
1472
+ newMap.set(dep, i);
1473
+ const oldIdx = oldMap.get(dep);
1474
+ if (oldIdx !== void 0) {
1475
+ newUnsubs.push(this._depUnsubs[oldIdx]);
1476
+ this._depUnsubs[oldIdx] = () => {
1477
+ };
1478
+ } else {
1479
+ const idx = i;
1480
+ const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1481
+ newUnsubs.push(unsub);
1547
1482
  }
1548
- for (const [dep, oldIdx] of oldMap) {
1549
- if (!newMap.has(dep)) {
1550
- this._depUnsubs[oldIdx]();
1551
- }
1483
+ }
1484
+ for (const [dep, oldIdx] of oldMap) {
1485
+ if (!newMap.has(dep)) {
1486
+ this._depUnsubs[oldIdx]();
1552
1487
  }
1553
- this._deps = newDeps;
1554
- this._depUnsubs = newUnsubs;
1555
- this._depIndexMap = newMap;
1556
- this._dirtyBits.clear();
1557
- this._settledBits.clear();
1558
- const newCompleteBits = /* @__PURE__ */ new Set();
1559
- for (const oldIdx of this._completeBits) {
1560
- const dep = [...oldMap.entries()].find(([, idx]) => idx === oldIdx)?.[0];
1561
- if (dep && newMap.has(dep)) {
1488
+ }
1489
+ this._deps = newDeps;
1490
+ this._depUnsubs = newUnsubs;
1491
+ this._depIndexMap = newMap;
1492
+ this._depDirtyBits.clear();
1493
+ this._depSettledBits.clear();
1494
+ const newCompleteBits = /* @__PURE__ */ new Set();
1495
+ for (const oldIdx of this._depCompleteBits) {
1496
+ for (const [dep, idx] of oldMap) {
1497
+ if (idx === oldIdx && newMap.has(dep)) {
1562
1498
  newCompleteBits.add(newMap.get(dep));
1499
+ break;
1563
1500
  }
1564
1501
  }
1565
- this._completeBits = newCompleteBits;
1566
- } finally {
1567
- this._rewiring = false;
1568
1502
  }
1503
+ this._depCompleteBits = newCompleteBits;
1569
1504
  }
1505
+ // --- Dep message handling ---
1570
1506
  _handleDepMessages(index, messages) {
1571
- if (this._rewiring) return;
1507
+ if (this._rewiring) {
1508
+ this._bufferedDepMessages.push({ index, msgs: messages });
1509
+ return;
1510
+ }
1572
1511
  for (const msg of messages) {
1573
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1512
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1574
1513
  const t = msg[0];
1575
1514
  if (this._onMessage) {
1576
1515
  try {
1577
1516
  if (this._onMessage(msg, index, this._actions)) continue;
1578
1517
  } catch (err) {
1579
- this._downInternal([[ERROR, err]]);
1518
+ const errMsg = err instanceof Error ? err.message : String(err);
1519
+ const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
1520
+ cause: err
1521
+ });
1522
+ this._downInternal([[ERROR, wrapped]]);
1580
1523
  return;
1581
1524
  }
1582
1525
  }
1526
+ if (messageTier(t) < 1) continue;
1583
1527
  if (t === DIRTY) {
1584
- this._dirtyBits.add(index);
1585
- this._settledBits.delete(index);
1586
- if (this._dirtyBits.size === 1) {
1587
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
1528
+ const wasEmpty = this._depDirtyBits.size === 0;
1529
+ this._depDirtyBits.add(index);
1530
+ this._depSettledBits.delete(index);
1531
+ if (wasEmpty) {
1532
+ this._downInternal([[DIRTY]]);
1588
1533
  }
1589
1534
  continue;
1590
1535
  }
1591
1536
  if (t === DATA || t === RESOLVED) {
1592
- if (!this._dirtyBits.has(index)) {
1593
- this._dirtyBits.add(index);
1594
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
1537
+ if (!this._depDirtyBits.has(index)) {
1538
+ const wasEmpty = this._depDirtyBits.size === 0;
1539
+ this._depDirtyBits.add(index);
1540
+ if (wasEmpty) {
1541
+ this._downInternal([[DIRTY]]);
1542
+ }
1595
1543
  }
1596
- this._settledBits.add(index);
1544
+ this._depSettledBits.add(index);
1597
1545
  if (this._allDirtySettled()) {
1598
- this._dirtyBits.clear();
1599
- this._settledBits.clear();
1600
- this._runFn();
1546
+ this._depDirtyBits.clear();
1547
+ this._depSettledBits.clear();
1548
+ if (!this._running) {
1549
+ if (this._depValuesDifferFromTracked()) {
1550
+ this._runFn();
1551
+ }
1552
+ }
1601
1553
  }
1602
1554
  continue;
1603
1555
  }
1604
1556
  if (t === COMPLETE) {
1605
- this._completeBits.add(index);
1606
- this._dirtyBits.delete(index);
1607
- this._settledBits.delete(index);
1557
+ this._depCompleteBits.add(index);
1558
+ this._depDirtyBits.delete(index);
1559
+ this._depSettledBits.delete(index);
1608
1560
  if (this._allDirtySettled()) {
1609
- this._dirtyBits.clear();
1610
- this._settledBits.clear();
1611
- this._runFn();
1561
+ this._depDirtyBits.clear();
1562
+ this._depSettledBits.clear();
1563
+ if (!this._running) this._runFn();
1612
1564
  }
1613
- if (this._autoComplete && this._completeBits.size >= this._deps.length && this._deps.length > 0) {
1565
+ if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
1614
1566
  this._downInternal([[COMPLETE]]);
1615
1567
  }
1616
1568
  continue;
@@ -1626,13 +1578,46 @@ var DynamicNodeImpl = class {
1626
1578
  this._downInternal([msg]);
1627
1579
  }
1628
1580
  }
1581
+ /**
1582
+ * Update dep masks for a message without triggering `_runFn` — used
1583
+ * during post-rewire drain so the wave state is consistent with the
1584
+ * buffered activation cascade without recursing.
1585
+ */
1586
+ _updateMasksForMessage(index, msg) {
1587
+ const t = msg[0];
1588
+ if (t === DIRTY) {
1589
+ this._depDirtyBits.add(index);
1590
+ this._depSettledBits.delete(index);
1591
+ } else if (t === DATA || t === RESOLVED) {
1592
+ this._depDirtyBits.add(index);
1593
+ this._depSettledBits.add(index);
1594
+ } else if (t === COMPLETE) {
1595
+ this._depCompleteBits.add(index);
1596
+ this._depDirtyBits.delete(index);
1597
+ this._depSettledBits.delete(index);
1598
+ }
1599
+ }
1629
1600
  _allDirtySettled() {
1630
- if (this._dirtyBits.size === 0) return false;
1631
- for (const idx of this._dirtyBits) {
1632
- if (!this._settledBits.has(idx)) return false;
1601
+ if (this._depDirtyBits.size === 0) return false;
1602
+ for (const idx of this._depDirtyBits) {
1603
+ if (!this._depSettledBits.has(idx)) return false;
1633
1604
  }
1634
1605
  return true;
1635
1606
  }
1607
+ /**
1608
+ * True if any current dep value differs from what the last `_runFn`
1609
+ * saw via `get()`. Used to suppress redundant re-runs when deferred
1610
+ * handshake messages arrive after `_rewire` for a dep whose value
1611
+ * already matches `_trackedValues`.
1612
+ */
1613
+ _depValuesDifferFromTracked() {
1614
+ for (const dep of this._deps) {
1615
+ const current = dep.get();
1616
+ const tracked = this._trackedValues.get(dep);
1617
+ if (!this._equals(current, tracked)) return true;
1618
+ }
1619
+ return false;
1620
+ }
1636
1621
  };
1637
1622
 
1638
1623
  // src/core/meta.ts
@@ -1659,8 +1644,8 @@ function producer(fn, opts) {
1659
1644
  function derived(deps, fn, opts) {
1660
1645
  return node(deps, fn, { describeKind: "derived", ...opts });
1661
1646
  }
1662
- function effect(deps, fn) {
1663
- return node(deps, fn, { describeKind: "effect" });
1647
+ function effect(deps, fn, opts) {
1648
+ return node(deps, fn, { describeKind: "effect", ...opts });
1664
1649
  }
1665
1650
  function pipe(source, ...ops) {
1666
1651
  let current = source;
@@ -1713,6 +1698,7 @@ var ResettableTimer = class {
1713
1698
  RESOLVED,
1714
1699
  RESUME,
1715
1700
  ResettableTimer,
1701
+ START,
1716
1702
  TEARDOWN,
1717
1703
  accessHintForGuard,
1718
1704
  advanceVersion,
@@ -1727,6 +1713,7 @@ var ResettableTimer = class {
1727
1713
  effect,
1728
1714
  isBatching,
1729
1715
  isKnownMessageType,
1716
+ isLocalOnly,
1730
1717
  isPhase2Message,
1731
1718
  isTerminalMessage,
1732
1719
  isV1,