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