@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
@@ -317,6 +317,7 @@ var ImageSizeAdapter = class {
317
317
  };
318
318
 
319
319
  // src/core/messages.ts
320
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
320
321
  var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
321
322
  var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
322
323
  var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
@@ -326,13 +327,27 @@ var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
326
327
  var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
327
328
  var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
328
329
  var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
330
+ var knownMessageTypes = [
331
+ START,
332
+ DATA,
333
+ DIRTY,
334
+ RESOLVED,
335
+ INVALIDATE,
336
+ PAUSE,
337
+ RESUME,
338
+ TEARDOWN,
339
+ COMPLETE,
340
+ ERROR
341
+ ];
342
+ var knownMessageSet = new Set(knownMessageTypes);
329
343
  function messageTier(t) {
330
- if (t === DIRTY || t === INVALIDATE) return 0;
331
- if (t === PAUSE || t === RESUME) return 1;
332
- if (t === DATA || t === RESOLVED) return 2;
333
- if (t === COMPLETE || t === ERROR) return 3;
334
- if (t === TEARDOWN) return 4;
335
- return 0;
344
+ if (t === START) return 0;
345
+ if (t === DIRTY || t === INVALIDATE) return 1;
346
+ if (t === PAUSE || t === RESUME) return 2;
347
+ if (t === DATA || t === RESOLVED) return 3;
348
+ if (t === COMPLETE || t === ERROR) return 4;
349
+ if (t === TEARDOWN) return 5;
350
+ return 1;
336
351
  }
337
352
  function isPhase2Message(msg) {
338
353
  const t = msg[0];
@@ -420,14 +435,14 @@ function _downSequential(sink, messages, phase = 2) {
420
435
  const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
421
436
  for (const msg of messages) {
422
437
  const tier = messageTier(msg[0]);
423
- if (tier === 2) {
438
+ if (tier === 3) {
424
439
  if (isBatching()) {
425
440
  const m = msg;
426
441
  dataQueue.push(() => sink([m]));
427
442
  } else {
428
443
  sink([msg]);
429
444
  }
430
- } else if (tier >= 3) {
445
+ } else if (tier >= 4) {
431
446
  if (isBatching()) {
432
447
  const m = msg;
433
448
  pendingPhase3.push(() => sink([m]));
@@ -546,10 +561,24 @@ function advanceVersion(info, newValue, hashFn) {
546
561
  }
547
562
  }
548
563
 
549
- // src/core/node.ts
564
+ // src/core/node-base.ts
550
565
  var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
551
566
  var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
552
- function createIntBitSet() {
567
+ var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
568
+ var isCleanupFn = (value) => typeof value === "function";
569
+ function statusAfterMessage(status, msg) {
570
+ const t = msg[0];
571
+ if (t === DIRTY) return "dirty";
572
+ if (t === DATA) return "settled";
573
+ if (t === RESOLVED) return "resolved";
574
+ if (t === COMPLETE) return "completed";
575
+ if (t === ERROR) return "errored";
576
+ if (t === INVALIDATE) return "dirty";
577
+ if (t === TEARDOWN) return "disconnected";
578
+ return status;
579
+ }
580
+ function createIntBitSet(size) {
581
+ const fullMask = size >= 32 ? -1 : ~(-1 << size);
553
582
  let bits = 0;
554
583
  return {
555
584
  set(i) {
@@ -562,7 +591,8 @@ function createIntBitSet() {
562
591
  return (bits & 1 << i) !== 0;
563
592
  },
564
593
  covers(other) {
565
- return (bits & other._bits()) === other._bits();
594
+ const otherBits = other._bits();
595
+ return (bits & otherBits) === otherBits;
566
596
  },
567
597
  any() {
568
598
  return bits !== 0;
@@ -570,6 +600,9 @@ function createIntBitSet() {
570
600
  reset() {
571
601
  bits = 0;
572
602
  },
603
+ setAll() {
604
+ bits = fullMask;
605
+ },
573
606
  _bits() {
574
607
  return bits;
575
608
  }
@@ -577,6 +610,8 @@ function createIntBitSet() {
577
610
  }
578
611
  function createArrayBitSet(size) {
579
612
  const words = new Uint32Array(Math.ceil(size / 32));
613
+ const lastBits = size % 32;
614
+ const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
580
615
  return {
581
616
  set(i) {
582
617
  words[i >>> 5] |= 1 << (i & 31);
@@ -603,130 +638,103 @@ function createArrayBitSet(size) {
603
638
  reset() {
604
639
  words.fill(0);
605
640
  },
641
+ setAll() {
642
+ for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
643
+ if (words.length > 0) words[words.length - 1] = lastWordMask;
644
+ },
606
645
  _words: words
607
646
  };
608
647
  }
609
648
  function createBitSet(size) {
610
- return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
649
+ return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
611
650
  }
612
- var isNodeArray = (value) => Array.isArray(value);
613
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
614
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
615
- var isCleanupFn = (value) => typeof value === "function";
616
- var statusAfterMessage = (status, msg) => {
617
- const t = msg[0];
618
- if (t === DIRTY) return "dirty";
619
- if (t === DATA) return "settled";
620
- if (t === RESOLVED) return "resolved";
621
- if (t === COMPLETE) return "completed";
622
- if (t === ERROR) return "errored";
623
- if (t === INVALIDATE) return "dirty";
624
- if (t === TEARDOWN) return "disconnected";
625
- return status;
626
- };
627
- var NodeImpl = class {
628
- // --- Configuration (set once, never reassigned) ---
651
+ var NodeBase = class {
652
+ // --- Identity (set once) ---
629
653
  _optsName;
630
654
  _registryName;
631
- /** @internal read by {@link describeNode} before inference. */
655
+ /** @internal Read by `describeNode` before inference. */
632
656
  _describeKind;
633
657
  meta;
634
- _deps;
635
- _fn;
636
- _opts;
658
+ // --- Options ---
637
659
  _equals;
660
+ _resubscribable;
661
+ _resetOnTeardown;
662
+ _onResubscribe;
638
663
  _onMessage;
639
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
664
+ /** @internal Read by `describeNode` for `accessHintForGuard`. */
640
665
  _guard;
666
+ /** @internal Subclasses update this through {@link _recordMutation}. */
641
667
  _lastMutation;
642
- _hasDeps;
643
- _autoComplete;
644
- _isSingleDep;
645
- // --- Mutable state ---
668
+ // --- Versioning ---
669
+ _hashFn;
670
+ _versioning;
671
+ // --- Lifecycle state ---
672
+ /** @internal Read by `describeNode` and `graph.ts`. */
646
673
  _cached;
674
+ /** @internal Read externally via `get status()`. */
647
675
  _status;
648
676
  _terminal = false;
649
- _connected = false;
650
- _producerStarted = false;
651
- _connecting = false;
652
- _manualEmitUsed = false;
677
+ _active = false;
678
+ // --- Sink storage ---
679
+ /** @internal Read by `graph/profile.ts` for subscriber counts. */
653
680
  _sinkCount = 0;
654
681
  _singleDepSinkCount = 0;
655
- // --- Object/collection state ---
656
- _depDirtyMask;
657
- _depSettledMask;
658
- _depCompleteMask;
659
- _allDepsCompleteMask;
660
- _lastDepValues;
661
- _cleanup;
662
- _sinks = null;
663
682
  _singleDepSinks = /* @__PURE__ */ new WeakSet();
664
- _upstreamUnsubs = [];
683
+ _sinks = null;
684
+ // --- Actions + bound helpers ---
665
685
  _actions;
666
686
  _boundDownToSinks;
687
+ // --- Inspector hook (Graph observability) ---
667
688
  _inspectorHook;
668
- _versioning;
669
- _hashFn;
670
- constructor(deps, fn, opts) {
671
- this._deps = deps;
672
- this._fn = fn;
673
- this._opts = opts;
689
+ constructor(opts) {
674
690
  this._optsName = opts.name;
675
691
  this._describeKind = opts.describeKind;
676
692
  this._equals = opts.equals ?? Object.is;
693
+ this._resubscribable = opts.resubscribable ?? false;
694
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
695
+ this._onResubscribe = opts.onResubscribe;
677
696
  this._onMessage = opts.onMessage;
678
697
  this._guard = opts.guard;
679
- this._hasDeps = deps.length > 0;
680
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
681
- this._isSingleDep = deps.length === 1 && fn != null;
682
698
  this._cached = "initial" in opts ? opts.initial : NO_VALUE;
683
- this._status = this._hasDeps ? "disconnected" : "settled";
699
+ this._status = "disconnected";
684
700
  this._hashFn = opts.versioningHash ?? defaultHash;
685
701
  this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
686
702
  id: opts.versioningId,
687
703
  hash: this._hashFn
688
704
  }) : void 0;
689
- this._depDirtyMask = createBitSet(deps.length);
690
- this._depSettledMask = createBitSet(deps.length);
691
- this._depCompleteMask = createBitSet(deps.length);
692
- this._allDepsCompleteMask = createBitSet(deps.length);
693
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
694
705
  const meta = {};
695
706
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
696
- meta[k] = node({
697
- initial: v,
698
- name: `${opts.name ?? "node"}:meta:${k}`,
699
- describeKind: "state",
700
- ...opts.guard != null ? { guard: opts.guard } : {}
701
- });
707
+ meta[k] = this._createMetaNode(k, v, opts);
702
708
  }
703
709
  Object.freeze(meta);
704
710
  this.meta = meta;
705
711
  const self = this;
706
712
  this._actions = {
707
713
  down(messages) {
708
- self._manualEmitUsed = true;
714
+ self._onManualEmit();
709
715
  self._downInternal(messages);
710
716
  },
711
717
  emit(value) {
712
- self._manualEmitUsed = true;
718
+ self._onManualEmit();
713
719
  self._downAutoValue(value);
714
720
  },
715
721
  up(messages) {
716
722
  self._upInternal(messages);
717
723
  }
718
724
  };
719
- this.down = this.down.bind(this);
720
- this.up = this.up.bind(this);
721
725
  this._boundDownToSinks = this._downToSinks.bind(this);
722
726
  }
727
+ /**
728
+ * Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
729
+ * {@link NodeImpl} overrides to set `_manualEmitUsed`.
730
+ */
731
+ _onManualEmit() {
732
+ }
733
+ // --- Identity getters ---
723
734
  get name() {
724
735
  return this._registryName ?? this._optsName;
725
736
  }
726
- /**
727
- * When a node is registered with {@link Graph.add} without an options `name`,
728
- * the graph assigns the registry local name for introspection (parity with graphrefly-py).
729
- */
737
+ /** @internal Assigned by `Graph.add` when registered without an options `name`. */
730
738
  _assignRegistryName(localName) {
731
739
  if (this._optsName !== void 0 || this._registryName !== void 0) return;
732
740
  this._registryName = localName;
@@ -744,7 +752,10 @@ var NodeImpl = class {
744
752
  }
745
753
  };
746
754
  }
747
- // --- Public interface (Node<T>) ---
755
+ /** @internal Used by subclasses to surface inspector events. */
756
+ _emitInspectorHook(event) {
757
+ this._inspectorHook?.(event);
758
+ }
748
759
  get status() {
749
760
  return this._status;
750
761
  }
@@ -754,15 +765,7 @@ var NodeImpl = class {
754
765
  get v() {
755
766
  return this._versioning;
756
767
  }
757
- /**
758
- * Retroactively apply versioning to a node that was created without it.
759
- * No-op if versioning is already enabled.
760
- *
761
- * Version starts at 0 regardless of prior DATA emissions — it tracks
762
- * changes from the moment versioning is enabled, not historical ones.
763
- *
764
- * @internal — used by {@link Graph.setVersioning}.
765
- */
768
+ /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
766
769
  _applyVersioning(level, opts) {
767
770
  if (this._versioning != null) return;
768
771
  this._hashFn = opts?.hash ?? this._hashFn;
@@ -782,6 +785,7 @@ var NodeImpl = class {
782
785
  if (this._guard == null) return true;
783
786
  return this._guard(normalizeActor(actor), "observe");
784
787
  }
788
+ // --- Public transport ---
785
789
  get() {
786
790
  return this._cached === NO_VALUE ? void 0 : this._cached;
787
791
  }
@@ -794,43 +798,25 @@ var NodeImpl = class {
794
798
  if (!this._guard(actor, action)) {
795
799
  throw new GuardDenied({ actor, action, nodeName: this.name });
796
800
  }
797
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
801
+ this._recordMutation(actor);
798
802
  }
799
803
  this._downInternal(messages);
800
804
  }
801
- _downInternal(messages) {
802
- if (messages.length === 0) return;
803
- let lifecycleMessages = messages;
804
- let sinkMessages = messages;
805
- if (this._terminal && !this._opts.resubscribable) {
806
- const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
807
- if (terminalPassthrough.length === 0) return;
808
- lifecycleMessages = terminalPassthrough;
809
- sinkMessages = terminalPassthrough;
810
- }
811
- this._handleLocalLifecycle(lifecycleMessages);
812
- if (this._canSkipDirty()) {
813
- let hasPhase2 = false;
814
- for (let i = 0; i < sinkMessages.length; i++) {
815
- const t = sinkMessages[i][0];
816
- if (t === DATA || t === RESOLVED) {
817
- hasPhase2 = true;
818
- break;
819
- }
820
- }
821
- if (hasPhase2) {
822
- const filtered = [];
823
- for (let i = 0; i < sinkMessages.length; i++) {
824
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
825
- }
826
- if (filtered.length > 0) {
827
- downWithBatch(this._boundDownToSinks, filtered);
828
- }
829
- return;
830
- }
831
- }
832
- downWithBatch(this._boundDownToSinks, sinkMessages);
805
+ /** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
806
+ _recordMutation(actor) {
807
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
833
808
  }
809
+ /**
810
+ * At-most-once deactivation guard. Both TEARDOWN (eager) and
811
+ * unsubscribe-body (lazy) call this. The `_active` flag ensures
812
+ * `_doDeactivate` runs exactly once per activation cycle.
813
+ */
814
+ _onDeactivate() {
815
+ if (!this._active) return;
816
+ this._active = false;
817
+ this._doDeactivate();
818
+ }
819
+ // --- Subscribe (uniform across node shapes) ---
834
820
  subscribe(sink, hints) {
835
821
  if (hints?.actor != null && this._guard != null) {
836
822
  const actor = normalizeActor(hints.actor);
@@ -838,17 +824,21 @@ var NodeImpl = class {
838
824
  throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
839
825
  }
840
826
  }
841
- if (this._terminal && this._opts.resubscribable) {
827
+ if (this._terminal && this._resubscribable) {
842
828
  this._terminal = false;
843
829
  this._cached = NO_VALUE;
844
- this._status = this._hasDeps ? "disconnected" : "settled";
845
- this._opts.onResubscribe?.();
830
+ this._status = "disconnected";
831
+ this._onResubscribe?.();
846
832
  }
847
833
  this._sinkCount += 1;
848
834
  if (hints?.singleDep) {
849
835
  this._singleDepSinkCount += 1;
850
836
  this._singleDepSinks.add(sink);
851
837
  }
838
+ if (!this._terminal) {
839
+ const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
840
+ downWithBatch(sink, startMessages);
841
+ }
852
842
  if (this._sinks == null) {
853
843
  this._sinks = sink;
854
844
  } else if (typeof this._sinks === "function") {
@@ -856,10 +846,12 @@ var NodeImpl = class {
856
846
  } else {
857
847
  this._sinks.add(sink);
858
848
  }
859
- if (this._hasDeps) {
860
- this._connectUpstream();
861
- } else if (this._fn) {
862
- this._startProducer();
849
+ if (this._sinkCount === 1 && !this._terminal) {
850
+ this._active = true;
851
+ this._onActivate();
852
+ }
853
+ if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
854
+ this._status = "pending";
863
855
  }
864
856
  let removed = false;
865
857
  return () => {
@@ -883,39 +875,49 @@ var NodeImpl = class {
883
875
  }
884
876
  }
885
877
  if (this._sinks == null) {
886
- this._disconnectUpstream();
887
- this._stopProducer();
878
+ this._onDeactivate();
888
879
  }
889
880
  };
890
881
  }
891
- up(messages, options) {
892
- if (!this._hasDeps) return;
893
- if (!options?.internal && this._guard != null) {
894
- const actor = normalizeActor(options?.actor);
895
- if (!this._guard(actor, "write")) {
896
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
897
- }
898
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
882
+ // --- Down pipeline ---
883
+ /**
884
+ * Core outgoing dispatch. Applies terminal filter + local lifecycle
885
+ * update, then hands messages to `downWithBatch` for tier-aware delivery.
886
+ */
887
+ _downInternal(messages) {
888
+ if (messages.length === 0) return;
889
+ let sinkMessages = messages;
890
+ if (this._terminal && !this._resubscribable) {
891
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
892
+ if (pass.length === 0) return;
893
+ sinkMessages = pass;
899
894
  }
900
- for (const dep of this._deps) {
901
- if (options === void 0) {
902
- dep.up?.(messages);
903
- } else {
904
- dep.up?.(messages, options);
895
+ this._handleLocalLifecycle(sinkMessages);
896
+ if (this._canSkipDirty()) {
897
+ let hasPhase2 = false;
898
+ for (let i = 0; i < sinkMessages.length; i++) {
899
+ const t = sinkMessages[i][0];
900
+ if (t === DATA || t === RESOLVED) {
901
+ hasPhase2 = true;
902
+ break;
903
+ }
904
+ }
905
+ if (hasPhase2) {
906
+ const filtered = [];
907
+ for (let i = 0; i < sinkMessages.length; i++) {
908
+ if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
909
+ }
910
+ if (filtered.length > 0) {
911
+ downWithBatch(this._boundDownToSinks, filtered);
912
+ }
913
+ return;
905
914
  }
906
915
  }
916
+ downWithBatch(this._boundDownToSinks, sinkMessages);
907
917
  }
908
- _upInternal(messages) {
909
- if (!this._hasDeps) return;
910
- for (const dep of this._deps) {
911
- dep.up?.(messages, { internal: true });
912
- }
913
- }
914
- unsubscribe() {
915
- if (!this._hasDeps) return;
916
- this._disconnectUpstream();
918
+ _canSkipDirty() {
919
+ return this._sinkCount === 1 && this._singleDepSinkCount === 1;
917
920
  }
918
- // --- Private methods (prototype, _ prefix) ---
919
921
  _downToSinks(messages) {
920
922
  if (this._sinks == null) return;
921
923
  if (typeof this._sinks === "function") {
@@ -927,6 +929,11 @@ var NodeImpl = class {
927
929
  sink(messages);
928
930
  }
929
931
  }
932
+ /**
933
+ * Update `_cached`, `_status`, `_terminal` from message batch before
934
+ * delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
935
+ * {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
936
+ */
930
937
  _handleLocalLifecycle(messages) {
931
938
  for (const m of messages) {
932
939
  const t = m[0];
@@ -940,28 +947,22 @@ var NodeImpl = class {
940
947
  }
941
948
  }
942
949
  if (t === INVALIDATE) {
943
- const cleanupFn = this._cleanup;
944
- this._cleanup = void 0;
945
- cleanupFn?.();
950
+ this._onInvalidate();
946
951
  this._cached = NO_VALUE;
947
- this._lastDepValues = void 0;
948
952
  }
949
953
  this._status = statusAfterMessage(this._status, m);
950
954
  if (t === COMPLETE || t === ERROR) {
951
955
  this._terminal = true;
952
956
  }
953
957
  if (t === TEARDOWN) {
954
- if (this._opts.resetOnTeardown) {
958
+ if (this._resetOnTeardown) {
955
959
  this._cached = NO_VALUE;
956
960
  }
957
- const teardownCleanup = this._cleanup;
958
- this._cleanup = void 0;
959
- teardownCleanup?.();
961
+ this._onTeardown();
960
962
  try {
961
963
  this._propagateToMeta(t);
962
964
  } finally {
963
- this._disconnectUpstream();
964
- this._stopProducer();
965
+ this._onDeactivate();
965
966
  }
966
967
  }
967
968
  if (t !== TEARDOWN && propagatesToMeta(t)) {
@@ -969,7 +970,20 @@ var NodeImpl = class {
969
970
  }
970
971
  }
971
972
  }
972
- /** Propagate a signal to all companion meta nodes (best-effort). */
973
+ /**
974
+ * Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
975
+ * cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
976
+ * drop `_lastDepValues` so the next wave re-runs fn.
977
+ */
978
+ _onInvalidate() {
979
+ }
980
+ /**
981
+ * Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
982
+ * {@link NodeImpl} uses this to run any pending cleanup fn.
983
+ */
984
+ _onTeardown() {
985
+ }
986
+ /** Forward a signal to all companion meta nodes (best-effort). */
973
987
  _propagateToMeta(t) {
974
988
  for (const metaNode of Object.values(this.meta)) {
975
989
  try {
@@ -978,9 +992,10 @@ var NodeImpl = class {
978
992
  }
979
993
  }
980
994
  }
981
- _canSkipDirty() {
982
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
983
- }
995
+ /**
996
+ * Frame a computed value into the right protocol messages and dispatch
997
+ * via `_downInternal`. Used by `_runFn` and `actions.emit`.
998
+ */
984
999
  _downAutoValue(value) {
985
1000
  const wasDirty = this._status === "dirty";
986
1001
  let unchanged;
@@ -988,7 +1003,9 @@ var NodeImpl = class {
988
1003
  unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
989
1004
  } catch (eqErr) {
990
1005
  const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
991
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
1006
+ const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
1007
+ cause: eqErr
1008
+ });
992
1009
  this._downInternal([[ERROR, wrapped]]);
993
1010
  return;
994
1011
  }
@@ -998,89 +1015,173 @@ var NodeImpl = class {
998
1015
  }
999
1016
  this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
1000
1017
  }
1001
- _runFn() {
1002
- if (!this._fn) return;
1003
- if (this._terminal && !this._opts.resubscribable) return;
1004
- if (this._connecting) return;
1005
- try {
1006
- const n = this._deps.length;
1007
- const depValues = new Array(n);
1008
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1009
- const prev = this._lastDepValues;
1010
- if (n > 0 && prev != null && prev.length === n) {
1011
- let allSame = true;
1012
- for (let i = 0; i < n; i++) {
1013
- if (!Object.is(depValues[i], prev[i])) {
1014
- allSame = false;
1015
- break;
1016
- }
1017
- }
1018
- if (allSame) {
1019
- if (this._status === "dirty") {
1020
- this._downInternal([[RESOLVED]]);
1021
- }
1022
- return;
1023
- }
1024
- }
1025
- const prevCleanup = this._cleanup;
1026
- this._cleanup = void 0;
1027
- prevCleanup?.();
1028
- this._manualEmitUsed = false;
1029
- this._lastDepValues = depValues;
1030
- this._inspectorHook?.({ kind: "run", depValues });
1031
- const out = this._fn(depValues, this._actions);
1032
- if (isCleanupResult(out)) {
1033
- this._cleanup = out.cleanup;
1034
- if (this._manualEmitUsed) return;
1035
- if ("value" in out) {
1036
- this._downAutoValue(out.value);
1037
- }
1038
- return;
1039
- }
1040
- if (isCleanupFn(out)) {
1041
- this._cleanup = out;
1042
- return;
1043
- }
1044
- if (this._manualEmitUsed) return;
1045
- if (out === void 0) return;
1046
- this._downAutoValue(out);
1047
- } catch (err) {
1048
- const errMsg = err instanceof Error ? err.message : String(err);
1049
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1050
- this._downInternal([[ERROR, wrapped]]);
1018
+ };
1019
+
1020
+ // src/core/node.ts
1021
+ var NodeImpl = class extends NodeBase {
1022
+ // --- Dep configuration (set once) ---
1023
+ _deps;
1024
+ _fn;
1025
+ _opts;
1026
+ _hasDeps;
1027
+ _isSingleDep;
1028
+ _autoComplete;
1029
+ // --- Wave tracking masks ---
1030
+ _depDirtyMask;
1031
+ _depSettledMask;
1032
+ _depCompleteMask;
1033
+ _allDepsCompleteMask;
1034
+ // --- Identity-skip optimization ---
1035
+ _lastDepValues;
1036
+ _cleanup;
1037
+ // --- Upstream bookkeeping ---
1038
+ _upstreamUnsubs = [];
1039
+ // --- Fn behavior flag ---
1040
+ /** @internal Read by `describeNode` to infer `"operator"` label. */
1041
+ _manualEmitUsed = false;
1042
+ constructor(deps, fn, opts) {
1043
+ super(opts);
1044
+ this._deps = deps;
1045
+ this._fn = fn;
1046
+ this._opts = opts;
1047
+ this._hasDeps = deps.length > 0;
1048
+ this._isSingleDep = deps.length === 1 && fn != null;
1049
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
1050
+ if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
1051
+ this._status = "settled";
1051
1052
  }
1052
- }
1053
- _onDepDirty(index) {
1054
- const wasDirty = this._depDirtyMask.has(index);
1055
- this._depDirtyMask.set(index);
1056
- this._depSettledMask.clear(index);
1057
- if (!wasDirty) {
1058
- this._downInternal([[DIRTY]]);
1053
+ this._depDirtyMask = createBitSet(deps.length);
1054
+ this._depSettledMask = createBitSet(deps.length);
1055
+ this._depCompleteMask = createBitSet(deps.length);
1056
+ this._allDepsCompleteMask = createBitSet(deps.length);
1057
+ for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
1058
+ this.down = this.down.bind(this);
1059
+ this.up = this.up.bind(this);
1060
+ }
1061
+ // --- Meta node factory (called from base constructor) ---
1062
+ _createMetaNode(key, initialValue, opts) {
1063
+ return node({
1064
+ initial: initialValue,
1065
+ name: `${opts.name ?? "node"}:meta:${key}`,
1066
+ describeKind: "state",
1067
+ ...opts.guard != null ? { guard: opts.guard } : {}
1068
+ });
1069
+ }
1070
+ // --- Manual emit tracker (set by actions.down / actions.emit) ---
1071
+ _onManualEmit() {
1072
+ this._manualEmitUsed = true;
1073
+ }
1074
+ // --- Up / unsubscribe ---
1075
+ up(messages, options) {
1076
+ if (!this._hasDeps) return;
1077
+ if (!options?.internal && this._guard != null) {
1078
+ const actor = normalizeActor(options?.actor);
1079
+ if (!this._guard(actor, "write")) {
1080
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1081
+ }
1082
+ this._recordMutation(actor);
1083
+ }
1084
+ for (const dep of this._deps) {
1085
+ if (options === void 0) {
1086
+ dep.up?.(messages);
1087
+ } else {
1088
+ dep.up?.(messages, options);
1089
+ }
1059
1090
  }
1060
1091
  }
1061
- _onDepSettled(index) {
1062
- if (!this._depDirtyMask.has(index)) {
1063
- this._onDepDirty(index);
1092
+ _upInternal(messages) {
1093
+ if (!this._hasDeps) return;
1094
+ for (const dep of this._deps) {
1095
+ dep.up?.(messages, { internal: true });
1064
1096
  }
1065
- this._depSettledMask.set(index);
1066
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1067
- this._depDirtyMask.reset();
1068
- this._depSettledMask.reset();
1097
+ }
1098
+ unsubscribe() {
1099
+ if (!this._hasDeps) return;
1100
+ this._disconnectUpstream();
1101
+ }
1102
+ // --- Activation (first-subscriber / last-subscriber hooks) ---
1103
+ _onActivate() {
1104
+ if (this._hasDeps) {
1105
+ this._connectUpstream();
1106
+ return;
1107
+ }
1108
+ if (this._fn) {
1069
1109
  this._runFn();
1110
+ return;
1070
1111
  }
1071
1112
  }
1072
- _maybeCompleteFromDeps() {
1073
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1074
- this._downInternal([[COMPLETE]]);
1113
+ _doDeactivate() {
1114
+ this._disconnectUpstream();
1115
+ const cleanup = this._cleanup;
1116
+ this._cleanup = void 0;
1117
+ cleanup?.();
1118
+ if (this._fn != null) {
1119
+ this._cached = NO_VALUE;
1120
+ this._lastDepValues = void 0;
1121
+ }
1122
+ if (this._hasDeps || this._fn != null) {
1123
+ this._status = "disconnected";
1124
+ }
1125
+ }
1126
+ // --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
1127
+ _onInvalidate() {
1128
+ const cleanup = this._cleanup;
1129
+ this._cleanup = void 0;
1130
+ cleanup?.();
1131
+ this._lastDepValues = void 0;
1132
+ }
1133
+ _onTeardown() {
1134
+ const cleanup = this._cleanup;
1135
+ this._cleanup = void 0;
1136
+ cleanup?.();
1137
+ }
1138
+ // --- Upstream connect / disconnect ---
1139
+ _connectUpstream() {
1140
+ if (!this._hasDeps) return;
1141
+ if (this._upstreamUnsubs.length > 0) return;
1142
+ this._depDirtyMask.setAll();
1143
+ this._depSettledMask.reset();
1144
+ this._depCompleteMask.reset();
1145
+ const depValuesBefore = this._lastDepValues;
1146
+ const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1147
+ for (let i = 0; i < this._deps.length; i += 1) {
1148
+ const dep = this._deps[i];
1149
+ this._upstreamUnsubs.push(
1150
+ dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1151
+ );
1152
+ }
1153
+ if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
1154
+ this._runFn();
1075
1155
  }
1076
1156
  }
1157
+ _disconnectUpstream() {
1158
+ if (this._upstreamUnsubs.length === 0) return;
1159
+ for (const unsub of this._upstreamUnsubs.splice(0)) {
1160
+ unsub();
1161
+ }
1162
+ this._depDirtyMask.reset();
1163
+ this._depSettledMask.reset();
1164
+ this._depCompleteMask.reset();
1165
+ }
1166
+ // --- Wave handling ---
1077
1167
  _handleDepMessages(index, messages) {
1078
1168
  for (const msg of messages) {
1079
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1169
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1080
1170
  const t = msg[0];
1081
1171
  if (this._onMessage) {
1082
1172
  try {
1083
- if (this._onMessage(msg, index, this._actions)) continue;
1173
+ const consumed = this._onMessage(msg, index, this._actions);
1174
+ if (consumed) {
1175
+ if (t === START) {
1176
+ this._depDirtyMask.clear(index);
1177
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1178
+ this._depDirtyMask.reset();
1179
+ this._depSettledMask.reset();
1180
+ this._runFn();
1181
+ }
1182
+ }
1183
+ continue;
1184
+ }
1084
1185
  } catch (err) {
1085
1186
  const errMsg = err instanceof Error ? err.message : String(err);
1086
1187
  const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
@@ -1090,6 +1191,7 @@ var NodeImpl = class {
1090
1191
  return;
1091
1192
  }
1092
1193
  }
1194
+ if (messageTier(t) < 1) continue;
1093
1195
  if (!this._fn) {
1094
1196
  if (t === COMPLETE && this._deps.length > 1) {
1095
1197
  this._depCompleteMask.set(index);
@@ -1133,53 +1235,85 @@ var NodeImpl = class {
1133
1235
  this._downInternal([msg]);
1134
1236
  }
1135
1237
  }
1136
- _connectUpstream() {
1137
- if (!this._hasDeps || this._connected) return;
1138
- this._connected = true;
1139
- this._depDirtyMask.reset();
1140
- this._depSettledMask.reset();
1141
- this._depCompleteMask.reset();
1142
- this._status = "settled";
1143
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1144
- this._connecting = true;
1145
- try {
1146
- for (let i = 0; i < this._deps.length; i += 1) {
1147
- const dep = this._deps[i];
1148
- this._upstreamUnsubs.push(
1149
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1150
- );
1151
- }
1152
- } finally {
1153
- this._connecting = false;
1238
+ _onDepDirty(index) {
1239
+ const wasDirty = this._depDirtyMask.has(index);
1240
+ this._depDirtyMask.set(index);
1241
+ this._depSettledMask.clear(index);
1242
+ if (!wasDirty) {
1243
+ this._downInternal([[DIRTY]]);
1154
1244
  }
1155
- if (this._fn) {
1245
+ }
1246
+ _onDepSettled(index) {
1247
+ if (!this._depDirtyMask.has(index)) {
1248
+ this._onDepDirty(index);
1249
+ }
1250
+ this._depSettledMask.set(index);
1251
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1252
+ this._depDirtyMask.reset();
1253
+ this._depSettledMask.reset();
1156
1254
  this._runFn();
1157
1255
  }
1158
1256
  }
1159
- _stopProducer() {
1160
- if (!this._producerStarted) return;
1161
- this._producerStarted = false;
1162
- const producerCleanup = this._cleanup;
1163
- this._cleanup = void 0;
1164
- producerCleanup?.();
1165
- }
1166
- _startProducer() {
1167
- if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
1168
- this._producerStarted = true;
1169
- this._runFn();
1257
+ _maybeCompleteFromDeps() {
1258
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1259
+ this._downInternal([[COMPLETE]]);
1260
+ }
1170
1261
  }
1171
- _disconnectUpstream() {
1172
- if (!this._connected) return;
1173
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1174
- unsub();
1262
+ // --- Fn execution ---
1263
+ _runFn() {
1264
+ if (!this._fn) return;
1265
+ if (this._terminal && !this._resubscribable) return;
1266
+ try {
1267
+ const n = this._deps.length;
1268
+ const depValues = new Array(n);
1269
+ for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1270
+ const prev = this._lastDepValues;
1271
+ if (n > 0 && prev != null && prev.length === n) {
1272
+ let allSame = true;
1273
+ for (let i = 0; i < n; i++) {
1274
+ if (!Object.is(depValues[i], prev[i])) {
1275
+ allSame = false;
1276
+ break;
1277
+ }
1278
+ }
1279
+ if (allSame) {
1280
+ if (this._status === "dirty") {
1281
+ this._downInternal([[RESOLVED]]);
1282
+ }
1283
+ return;
1284
+ }
1285
+ }
1286
+ const prevCleanup = this._cleanup;
1287
+ this._cleanup = void 0;
1288
+ prevCleanup?.();
1289
+ this._manualEmitUsed = false;
1290
+ this._lastDepValues = depValues;
1291
+ this._emitInspectorHook({ kind: "run", depValues });
1292
+ const out = this._fn(depValues, this._actions);
1293
+ if (isCleanupResult(out)) {
1294
+ this._cleanup = out.cleanup;
1295
+ if (this._manualEmitUsed) return;
1296
+ if ("value" in out) {
1297
+ this._downAutoValue(out.value);
1298
+ }
1299
+ return;
1300
+ }
1301
+ if (isCleanupFn(out)) {
1302
+ this._cleanup = out;
1303
+ return;
1304
+ }
1305
+ if (this._manualEmitUsed) return;
1306
+ if (out === void 0) return;
1307
+ this._downAutoValue(out);
1308
+ } catch (err) {
1309
+ const errMsg = err instanceof Error ? err.message : String(err);
1310
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1311
+ this._downInternal([[ERROR, wrapped]]);
1175
1312
  }
1176
- this._connected = false;
1177
- this._depDirtyMask.reset();
1178
- this._depSettledMask.reset();
1179
- this._depCompleteMask.reset();
1180
- this._status = "disconnected";
1181
1313
  }
1182
1314
  };
1315
+ var isNodeArray = (value) => Array.isArray(value);
1316
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1183
1317
  function node(depsOrFn, fnOrOpts, optsArg) {
1184
1318
  const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1185
1319
  const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
@@ -1203,449 +1337,272 @@ function derived(deps, fn, opts) {
1203
1337
  }
1204
1338
 
1205
1339
  // src/core/dynamic-node.ts
1206
- var DynamicNodeImpl = class {
1207
- _optsName;
1208
- _registryName;
1209
- _describeKind;
1210
- meta;
1340
+ var MAX_RERUN = 16;
1341
+ var DynamicNodeImpl = class extends NodeBase {
1211
1342
  _fn;
1212
- _equals;
1213
- _resubscribable;
1214
- _resetOnTeardown;
1215
1343
  _autoComplete;
1216
- _onMessage;
1217
- _onResubscribe;
1218
- /** @internal — read by {@link describeNode} for `accessHintForGuard`. */
1219
- _guard;
1220
- _lastMutation;
1221
- _inspectorHook;
1222
- // Sink tracking
1223
- _sinkCount = 0;
1224
- _singleDepSinkCount = 0;
1225
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
1226
- // Actions object (for onMessage handler)
1227
- _actions;
1228
- _boundDownToSinks;
1229
- // Mutable state
1230
- _cached = NO_VALUE;
1231
- _status = "disconnected";
1232
- _terminal = false;
1233
- _connected = false;
1234
- _rewiring = false;
1235
- // re-entrancy guard
1236
1344
  // Dynamic deps tracking
1345
+ /** @internal Read by `describeNode`. */
1237
1346
  _deps = [];
1238
1347
  _depUnsubs = [];
1239
1348
  _depIndexMap = /* @__PURE__ */ new Map();
1240
- // node index in _deps
1241
- _dirtyBits = /* @__PURE__ */ new Set();
1242
- _settledBits = /* @__PURE__ */ new Set();
1243
- _completeBits = /* @__PURE__ */ new Set();
1244
- // Sinks
1245
- _sinks = null;
1349
+ _depDirtyBits = /* @__PURE__ */ new Set();
1350
+ _depSettledBits = /* @__PURE__ */ new Set();
1351
+ _depCompleteBits = /* @__PURE__ */ new Set();
1352
+ // Execution state
1353
+ _running = false;
1354
+ _rewiring = false;
1355
+ _bufferedDepMessages = [];
1356
+ _trackedValues = /* @__PURE__ */ new Map();
1357
+ _rerunCount = 0;
1246
1358
  constructor(fn, opts) {
1359
+ super(opts);
1247
1360
  this._fn = fn;
1248
- this._optsName = opts.name;
1249
- this._describeKind = opts.describeKind;
1250
- this._equals = opts.equals ?? Object.is;
1251
- this._resubscribable = opts.resubscribable ?? false;
1252
- this._resetOnTeardown = opts.resetOnTeardown ?? false;
1253
1361
  this._autoComplete = opts.completeWhenDepsComplete ?? true;
1254
- this._onMessage = opts.onMessage;
1255
- this._onResubscribe = opts.onResubscribe;
1256
- this._guard = opts.guard;
1257
- this._inspectorHook = void 0;
1258
- const meta = {};
1259
- for (const [k, v] of Object.entries(opts.meta ?? {})) {
1260
- meta[k] = node({
1261
- initial: v,
1262
- name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
1263
- describeKind: "state",
1264
- ...opts.guard != null ? { guard: opts.guard } : {}
1265
- });
1266
- }
1267
- Object.freeze(meta);
1268
- this.meta = meta;
1269
- const self = this;
1270
- this._actions = {
1271
- down(messages) {
1272
- self._downInternal(messages);
1273
- },
1274
- emit(value) {
1275
- self._downAutoValue(value);
1276
- },
1277
- up(messages) {
1278
- for (const dep of self._deps) {
1279
- dep.up?.(messages, { internal: true });
1280
- }
1281
- }
1282
- };
1283
- this._boundDownToSinks = this._downToSinks.bind(this);
1284
- }
1285
- get name() {
1286
- return this._registryName ?? this._optsName;
1287
- }
1288
- /** @internal */
1289
- _assignRegistryName(localName) {
1290
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
1291
- this._registryName = localName;
1292
- }
1293
- /**
1294
- * @internal Attach/remove inspector hook for graph-level observability.
1295
- * Returns a disposer that restores the previous hook.
1296
- */
1297
- _setInspectorHook(hook) {
1298
- const prev = this._inspectorHook;
1299
- this._inspectorHook = hook;
1300
- return () => {
1301
- if (this._inspectorHook === hook) {
1302
- this._inspectorHook = prev;
1303
- }
1304
- };
1305
- }
1306
- get status() {
1307
- return this._status;
1308
- }
1309
- get lastMutation() {
1310
- return this._lastMutation;
1311
- }
1312
- /** Versioning not yet supported on DynamicNodeImpl. */
1313
- get v() {
1314
- return void 0;
1315
- }
1316
- hasGuard() {
1317
- return this._guard != null;
1318
- }
1319
- allowsObserve(actor) {
1320
- if (this._guard == null) return true;
1321
- return this._guard(normalizeActor(actor), "observe");
1322
- }
1323
- get() {
1324
- return this._cached === NO_VALUE ? void 0 : this._cached;
1325
- }
1326
- down(messages, options) {
1327
- if (messages.length === 0) return;
1328
- if (!options?.internal && this._guard != null) {
1329
- const actor = normalizeActor(options?.actor);
1330
- const delivery = options?.delivery ?? "write";
1331
- const action = delivery === "signal" ? "signal" : "write";
1332
- if (!this._guard(actor, action)) {
1333
- throw new GuardDenied({ actor, action, nodeName: this.name });
1334
- }
1335
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1336
- }
1337
- this._downInternal(messages);
1338
- }
1339
- _downInternal(messages) {
1340
- if (messages.length === 0) return;
1341
- let sinkMessages = messages;
1342
- if (this._terminal && !this._resubscribable) {
1343
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
1344
- if (pass.length === 0) return;
1345
- sinkMessages = pass;
1346
- }
1347
- this._handleLocalLifecycle(sinkMessages);
1348
- if (this._canSkipDirty()) {
1349
- let hasPhase2 = false;
1350
- for (let i = 0; i < sinkMessages.length; i++) {
1351
- const t = sinkMessages[i][0];
1352
- if (t === DATA || t === RESOLVED) {
1353
- hasPhase2 = true;
1354
- break;
1355
- }
1356
- }
1357
- if (hasPhase2) {
1358
- const filtered = [];
1359
- for (let i = 0; i < sinkMessages.length; i++) {
1360
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
1361
- }
1362
- if (filtered.length > 0) {
1363
- downWithBatch(this._boundDownToSinks, filtered);
1364
- }
1365
- return;
1366
- }
1367
- }
1368
- downWithBatch(this._boundDownToSinks, sinkMessages);
1369
- }
1370
- _canSkipDirty() {
1371
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
1372
- }
1373
- subscribe(sink, hints) {
1374
- if (hints?.actor != null && this._guard != null) {
1375
- const actor = normalizeActor(hints.actor);
1376
- if (!this._guard(actor, "observe")) {
1377
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
1378
- }
1379
- }
1380
- if (this._terminal && this._resubscribable) {
1381
- this._terminal = false;
1382
- this._cached = NO_VALUE;
1383
- this._status = "disconnected";
1384
- this._onResubscribe?.();
1385
- }
1386
- this._sinkCount += 1;
1387
- if (hints?.singleDep) {
1388
- this._singleDepSinkCount += 1;
1389
- this._singleDepSinks.add(sink);
1390
- }
1391
- if (this._sinks == null) {
1392
- this._sinks = sink;
1393
- } else if (typeof this._sinks === "function") {
1394
- this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
1395
- } else {
1396
- this._sinks.add(sink);
1397
- }
1398
- if (!this._connected) {
1399
- this._connect();
1400
- }
1401
- let removed = false;
1402
- return () => {
1403
- if (removed) return;
1404
- removed = true;
1405
- this._sinkCount -= 1;
1406
- if (this._singleDepSinks.has(sink)) {
1407
- this._singleDepSinkCount -= 1;
1408
- this._singleDepSinks.delete(sink);
1409
- }
1410
- if (this._sinks == null) return;
1411
- if (typeof this._sinks === "function") {
1412
- if (this._sinks === sink) this._sinks = null;
1413
- } else {
1414
- this._sinks.delete(sink);
1415
- if (this._sinks.size === 1) {
1416
- const [only] = this._sinks;
1417
- this._sinks = only;
1418
- } else if (this._sinks.size === 0) {
1419
- this._sinks = null;
1420
- }
1421
- }
1422
- if (this._sinks == null) {
1423
- this._disconnect();
1424
- }
1425
- };
1426
- }
1427
- up(messages, options) {
1428
- if (this._deps.length === 0) return;
1429
- if (!options?.internal && this._guard != null) {
1430
- const actor = normalizeActor(options?.actor);
1431
- if (!this._guard(actor, "write")) {
1432
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1433
- }
1434
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1435
- }
1436
- for (const dep of this._deps) {
1437
- dep.up?.(messages, options);
1438
- }
1439
- }
1440
- unsubscribe() {
1441
- this._disconnect();
1442
- }
1443
- // --- Private methods ---
1444
- _downToSinks(messages) {
1445
- if (this._sinks == null) return;
1446
- if (typeof this._sinks === "function") {
1447
- this._sinks(messages);
1448
- return;
1449
- }
1450
- const snapshot = [...this._sinks];
1451
- for (const sink of snapshot) {
1452
- sink(messages);
1453
- }
1362
+ this.down = this.down.bind(this);
1363
+ this.up = this.up.bind(this);
1454
1364
  }
1455
- _handleLocalLifecycle(messages) {
1456
- for (const m of messages) {
1457
- const t = m[0];
1458
- if (t === DATA) this._cached = m[1];
1459
- if (t === INVALIDATE) {
1460
- this._cached = NO_VALUE;
1461
- this._status = "dirty";
1462
- }
1463
- if (t === DATA) {
1464
- this._status = "settled";
1465
- } else if (t === RESOLVED) {
1466
- this._status = "resolved";
1467
- } else if (t === DIRTY) {
1468
- this._status = "dirty";
1469
- } else if (t === COMPLETE) {
1470
- this._status = "completed";
1471
- this._terminal = true;
1472
- } else if (t === ERROR) {
1473
- this._status = "errored";
1474
- this._terminal = true;
1475
- }
1476
- if (t === TEARDOWN) {
1477
- if (this._resetOnTeardown) this._cached = NO_VALUE;
1478
- try {
1479
- this._propagateToMeta(t);
1480
- } finally {
1481
- this._disconnect();
1482
- }
1483
- }
1484
- if (t !== TEARDOWN && propagatesToMeta(t)) {
1485
- this._propagateToMeta(t);
1486
- }
1487
- }
1365
+ _createMetaNode(key, initialValue, opts) {
1366
+ return node({
1367
+ initial: initialValue,
1368
+ name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
1369
+ describeKind: "state",
1370
+ ...opts.guard != null ? { guard: opts.guard } : {}
1371
+ });
1488
1372
  }
1489
- /** Propagate a signal to all companion meta nodes (best-effort). */
1490
- _propagateToMeta(t) {
1491
- for (const metaNode of Object.values(this.meta)) {
1492
- try {
1493
- metaNode.down([[t]], { internal: true });
1494
- } catch {
1373
+ /** Versioning not supported on DynamicNodeImpl (override base). */
1374
+ get v() {
1375
+ return void 0;
1376
+ }
1377
+ // --- Up / unsubscribe ---
1378
+ up(messages, options) {
1379
+ if (this._deps.length === 0) return;
1380
+ if (!options?.internal && this._guard != null) {
1381
+ const actor = normalizeActor(options?.actor);
1382
+ if (!this._guard(actor, "write")) {
1383
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1495
1384
  }
1385
+ this._recordMutation(actor);
1496
1386
  }
1497
- }
1498
- _downAutoValue(value) {
1499
- const wasDirty = this._status === "dirty";
1500
- let unchanged;
1501
- try {
1502
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
1503
- } catch (eqErr) {
1504
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
1505
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
1506
- this._downInternal([[ERROR, wrapped]]);
1507
- return;
1387
+ for (const dep of this._deps) {
1388
+ dep.up?.(messages, options);
1508
1389
  }
1509
- if (unchanged) {
1510
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
1511
- return;
1390
+ }
1391
+ _upInternal(messages) {
1392
+ for (const dep of this._deps) {
1393
+ dep.up?.(messages, { internal: true });
1512
1394
  }
1513
- this._cached = value;
1514
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
1515
1395
  }
1516
- _connect() {
1517
- if (this._connected) return;
1518
- this._connected = true;
1519
- this._status = "settled";
1520
- this._dirtyBits.clear();
1521
- this._settledBits.clear();
1522
- this._completeBits.clear();
1396
+ unsubscribe() {
1397
+ this._disconnect();
1398
+ }
1399
+ // --- Activation hooks ---
1400
+ _onActivate() {
1523
1401
  this._runFn();
1524
1402
  }
1403
+ _doDeactivate() {
1404
+ this._disconnect();
1405
+ }
1525
1406
  _disconnect() {
1526
- if (!this._connected) return;
1527
1407
  for (const unsub of this._depUnsubs) unsub();
1528
1408
  this._depUnsubs = [];
1529
1409
  this._deps = [];
1530
1410
  this._depIndexMap.clear();
1531
- this._dirtyBits.clear();
1532
- this._settledBits.clear();
1533
- this._completeBits.clear();
1534
- this._connected = false;
1411
+ this._depDirtyBits.clear();
1412
+ this._depSettledBits.clear();
1413
+ this._depCompleteBits.clear();
1414
+ this._cached = NO_VALUE;
1535
1415
  this._status = "disconnected";
1536
1416
  }
1417
+ // --- Fn execution with rewire buffer ---
1537
1418
  _runFn() {
1538
1419
  if (this._terminal && !this._resubscribable) return;
1539
- if (this._rewiring) return;
1540
- const trackedDeps = [];
1541
- const trackedSet = /* @__PURE__ */ new Set();
1542
- const get = (dep) => {
1543
- if (!trackedSet.has(dep)) {
1544
- trackedSet.add(dep);
1545
- trackedDeps.push(dep);
1546
- }
1547
- return dep.get();
1548
- };
1420
+ if (this._running) return;
1421
+ this._running = true;
1422
+ this._rerunCount = 0;
1423
+ let result;
1549
1424
  try {
1550
- const depValues = [];
1551
- for (const dep of this._deps) {
1552
- depValues.push(dep.get());
1553
- }
1554
- this._inspectorHook?.({ kind: "run", depValues });
1555
- const result = this._fn(get);
1556
- this._rewire(trackedDeps);
1557
- if (result === void 0) return;
1558
- this._downAutoValue(result);
1559
- } catch (err) {
1560
- this._downInternal([[ERROR, err]]);
1425
+ for (; ; ) {
1426
+ const trackedDeps = [];
1427
+ const trackedValuesMap = /* @__PURE__ */ new Map();
1428
+ const trackedSet = /* @__PURE__ */ new Set();
1429
+ const get = (dep) => {
1430
+ if (!trackedSet.has(dep)) {
1431
+ trackedSet.add(dep);
1432
+ trackedDeps.push(dep);
1433
+ trackedValuesMap.set(dep, dep.get());
1434
+ }
1435
+ return dep.get();
1436
+ };
1437
+ this._trackedValues = trackedValuesMap;
1438
+ const depValues = [];
1439
+ for (const dep of this._deps) depValues.push(dep.get());
1440
+ this._emitInspectorHook({ kind: "run", depValues });
1441
+ try {
1442
+ result = this._fn(get);
1443
+ } catch (err) {
1444
+ const errMsg = err instanceof Error ? err.message : String(err);
1445
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
1446
+ cause: err
1447
+ });
1448
+ this._downInternal([[ERROR, wrapped]]);
1449
+ return;
1450
+ }
1451
+ this._rewiring = true;
1452
+ this._bufferedDepMessages = [];
1453
+ try {
1454
+ this._rewire(trackedDeps);
1455
+ } finally {
1456
+ this._rewiring = false;
1457
+ }
1458
+ let needsRerun = false;
1459
+ for (const entry of this._bufferedDepMessages) {
1460
+ for (const msg of entry.msgs) {
1461
+ if (msg[0] === DATA) {
1462
+ const dep = this._deps[entry.index];
1463
+ const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
1464
+ const actualValue = msg[1];
1465
+ if (!this._equals(trackedValue, actualValue)) {
1466
+ needsRerun = true;
1467
+ break;
1468
+ }
1469
+ }
1470
+ }
1471
+ if (needsRerun) break;
1472
+ }
1473
+ if (needsRerun) {
1474
+ this._rerunCount += 1;
1475
+ if (this._rerunCount > MAX_RERUN) {
1476
+ this._bufferedDepMessages = [];
1477
+ this._downInternal([
1478
+ [
1479
+ ERROR,
1480
+ new Error(
1481
+ `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
1482
+ )
1483
+ ]
1484
+ ]);
1485
+ return;
1486
+ }
1487
+ this._bufferedDepMessages = [];
1488
+ continue;
1489
+ }
1490
+ const drain = this._bufferedDepMessages;
1491
+ this._bufferedDepMessages = [];
1492
+ for (const entry of drain) {
1493
+ for (const msg of entry.msgs) {
1494
+ this._updateMasksForMessage(entry.index, msg);
1495
+ }
1496
+ }
1497
+ this._depDirtyBits.clear();
1498
+ this._depSettledBits.clear();
1499
+ break;
1500
+ }
1501
+ } finally {
1502
+ this._running = false;
1561
1503
  }
1504
+ this._downAutoValue(result);
1562
1505
  }
1563
1506
  _rewire(newDeps) {
1564
- this._rewiring = true;
1565
- try {
1566
- const oldMap = this._depIndexMap;
1567
- const newMap = /* @__PURE__ */ new Map();
1568
- const newUnsubs = [];
1569
- for (let i = 0; i < newDeps.length; i++) {
1570
- const dep = newDeps[i];
1571
- newMap.set(dep, i);
1572
- const oldIdx = oldMap.get(dep);
1573
- if (oldIdx !== void 0) {
1574
- newUnsubs.push(this._depUnsubs[oldIdx]);
1575
- this._depUnsubs[oldIdx] = () => {
1576
- };
1577
- } else {
1578
- const idx = i;
1579
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1580
- newUnsubs.push(unsub);
1581
- }
1507
+ const oldMap = this._depIndexMap;
1508
+ const newMap = /* @__PURE__ */ new Map();
1509
+ const newUnsubs = [];
1510
+ for (let i = 0; i < newDeps.length; i++) {
1511
+ const dep = newDeps[i];
1512
+ newMap.set(dep, i);
1513
+ const oldIdx = oldMap.get(dep);
1514
+ if (oldIdx !== void 0) {
1515
+ newUnsubs.push(this._depUnsubs[oldIdx]);
1516
+ this._depUnsubs[oldIdx] = () => {
1517
+ };
1518
+ } else {
1519
+ const idx = i;
1520
+ const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1521
+ newUnsubs.push(unsub);
1582
1522
  }
1583
- for (const [dep, oldIdx] of oldMap) {
1584
- if (!newMap.has(dep)) {
1585
- this._depUnsubs[oldIdx]();
1586
- }
1523
+ }
1524
+ for (const [dep, oldIdx] of oldMap) {
1525
+ if (!newMap.has(dep)) {
1526
+ this._depUnsubs[oldIdx]();
1587
1527
  }
1588
- this._deps = newDeps;
1589
- this._depUnsubs = newUnsubs;
1590
- this._depIndexMap = newMap;
1591
- this._dirtyBits.clear();
1592
- this._settledBits.clear();
1593
- const newCompleteBits = /* @__PURE__ */ new Set();
1594
- for (const oldIdx of this._completeBits) {
1595
- const dep = [...oldMap.entries()].find(([, idx]) => idx === oldIdx)?.[0];
1596
- if (dep && newMap.has(dep)) {
1528
+ }
1529
+ this._deps = newDeps;
1530
+ this._depUnsubs = newUnsubs;
1531
+ this._depIndexMap = newMap;
1532
+ this._depDirtyBits.clear();
1533
+ this._depSettledBits.clear();
1534
+ const newCompleteBits = /* @__PURE__ */ new Set();
1535
+ for (const oldIdx of this._depCompleteBits) {
1536
+ for (const [dep, idx] of oldMap) {
1537
+ if (idx === oldIdx && newMap.has(dep)) {
1597
1538
  newCompleteBits.add(newMap.get(dep));
1539
+ break;
1598
1540
  }
1599
1541
  }
1600
- this._completeBits = newCompleteBits;
1601
- } finally {
1602
- this._rewiring = false;
1603
1542
  }
1543
+ this._depCompleteBits = newCompleteBits;
1604
1544
  }
1545
+ // --- Dep message handling ---
1605
1546
  _handleDepMessages(index, messages) {
1606
- if (this._rewiring) return;
1547
+ if (this._rewiring) {
1548
+ this._bufferedDepMessages.push({ index, msgs: messages });
1549
+ return;
1550
+ }
1607
1551
  for (const msg of messages) {
1608
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1552
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1609
1553
  const t = msg[0];
1610
1554
  if (this._onMessage) {
1611
1555
  try {
1612
1556
  if (this._onMessage(msg, index, this._actions)) continue;
1613
1557
  } catch (err) {
1614
- this._downInternal([[ERROR, err]]);
1558
+ const errMsg = err instanceof Error ? err.message : String(err);
1559
+ const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
1560
+ cause: err
1561
+ });
1562
+ this._downInternal([[ERROR, wrapped]]);
1615
1563
  return;
1616
1564
  }
1617
1565
  }
1566
+ if (messageTier(t) < 1) continue;
1618
1567
  if (t === DIRTY) {
1619
- this._dirtyBits.add(index);
1620
- this._settledBits.delete(index);
1621
- if (this._dirtyBits.size === 1) {
1622
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
1568
+ const wasEmpty = this._depDirtyBits.size === 0;
1569
+ this._depDirtyBits.add(index);
1570
+ this._depSettledBits.delete(index);
1571
+ if (wasEmpty) {
1572
+ this._downInternal([[DIRTY]]);
1623
1573
  }
1624
1574
  continue;
1625
1575
  }
1626
1576
  if (t === DATA || t === RESOLVED) {
1627
- if (!this._dirtyBits.has(index)) {
1628
- this._dirtyBits.add(index);
1629
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
1577
+ if (!this._depDirtyBits.has(index)) {
1578
+ const wasEmpty = this._depDirtyBits.size === 0;
1579
+ this._depDirtyBits.add(index);
1580
+ if (wasEmpty) {
1581
+ this._downInternal([[DIRTY]]);
1582
+ }
1630
1583
  }
1631
- this._settledBits.add(index);
1584
+ this._depSettledBits.add(index);
1632
1585
  if (this._allDirtySettled()) {
1633
- this._dirtyBits.clear();
1634
- this._settledBits.clear();
1635
- this._runFn();
1586
+ this._depDirtyBits.clear();
1587
+ this._depSettledBits.clear();
1588
+ if (!this._running) {
1589
+ if (this._depValuesDifferFromTracked()) {
1590
+ this._runFn();
1591
+ }
1592
+ }
1636
1593
  }
1637
1594
  continue;
1638
1595
  }
1639
1596
  if (t === COMPLETE) {
1640
- this._completeBits.add(index);
1641
- this._dirtyBits.delete(index);
1642
- this._settledBits.delete(index);
1597
+ this._depCompleteBits.add(index);
1598
+ this._depDirtyBits.delete(index);
1599
+ this._depSettledBits.delete(index);
1643
1600
  if (this._allDirtySettled()) {
1644
- this._dirtyBits.clear();
1645
- this._settledBits.clear();
1646
- this._runFn();
1601
+ this._depDirtyBits.clear();
1602
+ this._depSettledBits.clear();
1603
+ if (!this._running) this._runFn();
1647
1604
  }
1648
- if (this._autoComplete && this._completeBits.size >= this._deps.length && this._deps.length > 0) {
1605
+ if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
1649
1606
  this._downInternal([[COMPLETE]]);
1650
1607
  }
1651
1608
  continue;
@@ -1661,13 +1618,46 @@ var DynamicNodeImpl = class {
1661
1618
  this._downInternal([msg]);
1662
1619
  }
1663
1620
  }
1621
+ /**
1622
+ * Update dep masks for a message without triggering `_runFn` — used
1623
+ * during post-rewire drain so the wave state is consistent with the
1624
+ * buffered activation cascade without recursing.
1625
+ */
1626
+ _updateMasksForMessage(index, msg) {
1627
+ const t = msg[0];
1628
+ if (t === DIRTY) {
1629
+ this._depDirtyBits.add(index);
1630
+ this._depSettledBits.delete(index);
1631
+ } else if (t === DATA || t === RESOLVED) {
1632
+ this._depDirtyBits.add(index);
1633
+ this._depSettledBits.add(index);
1634
+ } else if (t === COMPLETE) {
1635
+ this._depCompleteBits.add(index);
1636
+ this._depDirtyBits.delete(index);
1637
+ this._depSettledBits.delete(index);
1638
+ }
1639
+ }
1664
1640
  _allDirtySettled() {
1665
- if (this._dirtyBits.size === 0) return false;
1666
- for (const idx of this._dirtyBits) {
1667
- if (!this._settledBits.has(idx)) return false;
1641
+ if (this._depDirtyBits.size === 0) return false;
1642
+ for (const idx of this._depDirtyBits) {
1643
+ if (!this._depSettledBits.has(idx)) return false;
1668
1644
  }
1669
1645
  return true;
1670
1646
  }
1647
+ /**
1648
+ * True if any current dep value differs from what the last `_runFn`
1649
+ * saw via `get()`. Used to suppress redundant re-runs when deferred
1650
+ * handshake messages arrive after `_rewire` for a dep whose value
1651
+ * already matches `_trackedValues`.
1652
+ */
1653
+ _depValuesDifferFromTracked() {
1654
+ for (const dep of this._deps) {
1655
+ const current = dep.get();
1656
+ const tracked = this._trackedValues.get(dep);
1657
+ if (!this._equals(current, tracked)) return true;
1658
+ }
1659
+ return false;
1660
+ }
1671
1661
  };
1672
1662
 
1673
1663
  // src/core/meta.ts
@@ -1737,6 +1727,10 @@ function describeNode(node2, includeFields) {
1737
1727
  out.name = node2.name;
1738
1728
  }
1739
1729
  if (all || includeFields.has("value")) {
1730
+ const isSentinel = node2 instanceof NodeImpl && node2._cached === NO_VALUE || node2 instanceof DynamicNodeImpl && node2._cached === NO_VALUE;
1731
+ if (isSentinel) {
1732
+ out.sentinel = true;
1733
+ }
1740
1734
  try {
1741
1735
  out.value = node2.get();
1742
1736
  } catch {
@@ -1763,6 +1757,129 @@ function describeNode(node2, includeFields) {
1763
1757
  return out;
1764
1758
  }
1765
1759
 
1760
+ // src/graph/sizeof.ts
1761
+ var OVERHEAD = {
1762
+ object: 56,
1763
+ array: 64,
1764
+ string: 40,
1765
+ // header; content added separately
1766
+ number: 8,
1767
+ boolean: 4,
1768
+ null: 0,
1769
+ undefined: 0,
1770
+ symbol: 40,
1771
+ bigint: 16,
1772
+ function: 120,
1773
+ map: 72,
1774
+ set: 72,
1775
+ mapEntry: 40,
1776
+ setEntry: 24
1777
+ };
1778
+ function sizeof(value) {
1779
+ const seen = /* @__PURE__ */ new WeakSet();
1780
+ return _sizeof(value, seen);
1781
+ }
1782
+ function _sizeof(value, seen) {
1783
+ if (value == null) return 0;
1784
+ const t = typeof value;
1785
+ switch (t) {
1786
+ case "number":
1787
+ return OVERHEAD.number;
1788
+ case "boolean":
1789
+ return OVERHEAD.boolean;
1790
+ case "string":
1791
+ return OVERHEAD.string + value.length * 2;
1792
+ // UTF-16
1793
+ case "bigint":
1794
+ return OVERHEAD.bigint;
1795
+ case "symbol":
1796
+ return OVERHEAD.symbol;
1797
+ case "function":
1798
+ if (seen.has(value)) return 0;
1799
+ seen.add(value);
1800
+ return OVERHEAD.function;
1801
+ case "undefined":
1802
+ return 0;
1803
+ }
1804
+ const obj = value;
1805
+ if (seen.has(obj)) return 0;
1806
+ seen.add(obj);
1807
+ if (obj instanceof Map) {
1808
+ let size2 = OVERHEAD.map;
1809
+ for (const [k, v] of obj) {
1810
+ size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
1811
+ }
1812
+ return size2;
1813
+ }
1814
+ if (obj instanceof Set) {
1815
+ let size2 = OVERHEAD.set;
1816
+ for (const v of obj) {
1817
+ size2 += OVERHEAD.setEntry + _sizeof(v, seen);
1818
+ }
1819
+ return size2;
1820
+ }
1821
+ if (Array.isArray(obj)) {
1822
+ let size2 = OVERHEAD.array + obj.length * 8;
1823
+ for (const item of obj) {
1824
+ size2 += _sizeof(item, seen);
1825
+ }
1826
+ return size2;
1827
+ }
1828
+ if (obj instanceof ArrayBuffer) return obj.byteLength;
1829
+ if (ArrayBuffer.isView(obj)) return obj.byteLength;
1830
+ let size = OVERHEAD.object;
1831
+ const keys = Object.keys(obj);
1832
+ for (const key of keys) {
1833
+ size += OVERHEAD.string + key.length * 2;
1834
+ size += _sizeof(obj[key], seen);
1835
+ }
1836
+ return size;
1837
+ }
1838
+
1839
+ // src/graph/profile.ts
1840
+ function graphProfile(graph, opts) {
1841
+ const topN = opts?.topN ?? 10;
1842
+ const desc = graph.describe({ detail: "standard" });
1843
+ const targets = [];
1844
+ if (typeof graph._collectObserveTargets === "function") {
1845
+ graph._collectObserveTargets("", targets);
1846
+ }
1847
+ const pathToNode = /* @__PURE__ */ new Map();
1848
+ for (const [p, n] of targets) {
1849
+ pathToNode.set(p, n);
1850
+ }
1851
+ const profiles = [];
1852
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
1853
+ const nd = pathToNode.get(path);
1854
+ const impl = nd instanceof NodeImpl ? nd : null;
1855
+ const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
1856
+ const subscriberCount = impl ? impl._sinkCount : 0;
1857
+ const depCount = nodeDesc.deps?.length ?? 0;
1858
+ const isOrphanEffect = nodeDesc.type === "effect" && subscriberCount === 0;
1859
+ profiles.push({
1860
+ path,
1861
+ type: nodeDesc.type,
1862
+ status: nodeDesc.status ?? "unknown",
1863
+ valueSizeBytes,
1864
+ subscriberCount,
1865
+ depCount,
1866
+ isOrphanEffect
1867
+ });
1868
+ }
1869
+ const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
1870
+ const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
1871
+ const orphanEffects = profiles.filter((p) => p.isOrphanEffect);
1872
+ return {
1873
+ nodeCount: profiles.length,
1874
+ edgeCount: desc.edges.length,
1875
+ subgraphCount: desc.subgraphs.length,
1876
+ nodes: profiles,
1877
+ totalValueSizeBytes,
1878
+ hotspots,
1879
+ orphanEffects
1880
+ };
1881
+ }
1882
+
1766
1883
  // src/graph/graph.ts
1767
1884
  var PATH_SEP = "::";
1768
1885
  var GRAPH_META_SEGMENT = "__meta__";
@@ -1905,7 +2022,7 @@ var RingBuffer = class {
1905
2022
  return result;
1906
2023
  }
1907
2024
  };
1908
- var SPY_ANSI_THEME = {
2025
+ var OBSERVE_ANSI_THEME = {
1909
2026
  data: "\x1B[32m",
1910
2027
  dirty: "\x1B[33m",
1911
2028
  resolved: "\x1B[36m",
@@ -1915,7 +2032,7 @@ var SPY_ANSI_THEME = {
1915
2032
  path: "\x1B[90m",
1916
2033
  reset: "\x1B[0m"
1917
2034
  };
1918
- var SPY_NO_COLOR_THEME = {
2035
+ var OBSERVE_NO_COLOR_THEME = {
1919
2036
  data: "",
1920
2037
  dirty: "",
1921
2038
  resolved: "",
@@ -1935,9 +2052,9 @@ function describeData(value) {
1935
2052
  return "[unserializable]";
1936
2053
  }
1937
2054
  }
1938
- function resolveSpyTheme(theme) {
1939
- if (theme === "none") return SPY_NO_COLOR_THEME;
1940
- if (theme === "ansi" || theme == null) return SPY_ANSI_THEME;
2055
+ function resolveObserveTheme(theme) {
2056
+ if (theme === "none") return OBSERVE_NO_COLOR_THEME;
2057
+ if (theme === "ansi" || theme == null) return OBSERVE_ANSI_THEME;
1941
2058
  return {
1942
2059
  data: theme.data ?? "",
1943
2060
  dirty: theme.dirty ?? "",
@@ -2675,6 +2792,16 @@ var Graph = class _Graph {
2675
2792
  }
2676
2793
  return out;
2677
2794
  }
2795
+ /**
2796
+ * Snapshot-based resource profile: per-node stats, orphan effect detection,
2797
+ * memory hotspots. Zero runtime overhead — walks nodes on demand.
2798
+ *
2799
+ * @param opts - Optional `topN` for hotspot limit (default 10).
2800
+ * @returns Aggregate profile with per-node details, hotspots, and orphan effects.
2801
+ */
2802
+ resourceProfile(opts) {
2803
+ return graphProfile(this, opts);
2804
+ }
2678
2805
  _qualifyEdgeEndpoint(part, prefix) {
2679
2806
  if (part.includes(PATH_SEP)) return part;
2680
2807
  return prefix === "" ? part : `${prefix}${PATH_SEP}${part}`;
@@ -2708,9 +2835,13 @@ var Graph = class _Graph {
2708
2835
  if (actor2 != null && !target.allowsObserve(actor2)) {
2709
2836
  throw new GuardDenied({ actor: actor2, action: "observe", nodeName: path });
2710
2837
  }
2711
- const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full";
2712
- if (wantsStructured2 && _Graph.inspectorEnabled) {
2713
- return this._createObserveResult(path, target, resolved);
2838
+ const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full" || resolved.format != null;
2839
+ if (wantsStructured2) {
2840
+ const result = _Graph.inspectorEnabled ? this._createObserveResult(path, target, resolved) : this._createFallbackObserveResult(path, resolved);
2841
+ if (resolved.format != null) {
2842
+ this._attachFormatLogger(result, resolved);
2843
+ }
2844
+ return result;
2714
2845
  }
2715
2846
  return {
2716
2847
  subscribe(sink) {
@@ -2728,9 +2859,13 @@ var Graph = class _Graph {
2728
2859
  }
2729
2860
  const opts = resolveObserveDetail(pathOrOpts);
2730
2861
  const actor = opts.actor;
2731
- const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full";
2732
- if (wantsStructured && _Graph.inspectorEnabled) {
2733
- return this._createObserveResultForAll(opts);
2862
+ const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full" || opts.format != null;
2863
+ if (wantsStructured) {
2864
+ const result = _Graph.inspectorEnabled ? this._createObserveResultForAll(opts) : this._createFallbackObserveResultForAll(opts);
2865
+ if (opts.format != null) {
2866
+ this._attachFormatLogger(result, opts);
2867
+ }
2868
+ return result;
2734
2869
  }
2735
2870
  return {
2736
2871
  subscribe: (sink) => {
@@ -2768,12 +2903,13 @@ var Graph = class _Graph {
2768
2903
  dirtyCount: 0,
2769
2904
  resolvedCount: 0,
2770
2905
  events: [],
2771
- completedCleanly: false,
2772
- errored: false
2906
+ anyCompletedCleanly: false,
2907
+ anyErrored: false
2773
2908
  };
2774
2909
  let lastTriggerDepIndex;
2775
2910
  let lastRunDepValues;
2776
2911
  let detachInspectorHook;
2912
+ let batchSeq = 0;
2777
2913
  if ((causal || derived2) && target instanceof NodeImpl) {
2778
2914
  detachInspectorHook = target._setInspectorHook((event) => {
2779
2915
  if (event.kind === "dep_message") {
@@ -2786,15 +2922,16 @@ var Graph = class _Graph {
2786
2922
  type: "derived",
2787
2923
  path,
2788
2924
  dep_values: [...event.depValues],
2789
- ...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {}
2925
+ ...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {}
2790
2926
  });
2791
2927
  }
2792
2928
  });
2793
2929
  }
2794
2930
  const unsub = target.subscribe((msgs) => {
2931
+ batchSeq++;
2795
2932
  for (const m of msgs) {
2796
2933
  const t = m[0];
2797
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
2934
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
2798
2935
  const withCausal = causal && lastRunDepValues != null ? (() => {
2799
2936
  const triggerDep = lastTriggerDepIndex != null && lastTriggerDepIndex >= 0 && target instanceof NodeImpl ? target._deps[lastTriggerDepIndex] : void 0;
2800
2937
  const tv = triggerDep?.v;
@@ -2811,8 +2948,8 @@ var Graph = class _Graph {
2811
2948
  } else if (minimal) {
2812
2949
  if (t === DIRTY) result.dirtyCount++;
2813
2950
  else if (t === RESOLVED) result.resolvedCount++;
2814
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
2815
- else if (t === ERROR) result.errored = true;
2951
+ else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
2952
+ else if (t === ERROR) result.anyErrored = true;
2816
2953
  } else if (t === DIRTY) {
2817
2954
  result.dirtyCount++;
2818
2955
  result.events.push({ type: "dirty", path, ...base });
@@ -2820,10 +2957,10 @@ var Graph = class _Graph {
2820
2957
  result.resolvedCount++;
2821
2958
  result.events.push({ type: "resolved", path, ...base, ...withCausal });
2822
2959
  } else if (t === COMPLETE) {
2823
- if (!result.errored) result.completedCleanly = true;
2960
+ if (!result.anyErrored) result.anyCompletedCleanly = true;
2824
2961
  result.events.push({ type: "complete", path, ...base });
2825
2962
  } else if (t === ERROR) {
2826
- result.errored = true;
2963
+ result.anyErrored = true;
2827
2964
  result.events.push({ type: "error", path, data: m[1], ...base });
2828
2965
  }
2829
2966
  }
@@ -2843,11 +2980,14 @@ var Graph = class _Graph {
2843
2980
  get events() {
2844
2981
  return result.events;
2845
2982
  },
2846
- get completedCleanly() {
2847
- return result.completedCleanly;
2983
+ get anyCompletedCleanly() {
2984
+ return result.anyCompletedCleanly;
2985
+ },
2986
+ get anyErrored() {
2987
+ return result.anyErrored;
2848
2988
  },
2849
- get errored() {
2850
- return result.errored;
2989
+ get completedWithoutErrors() {
2990
+ return result.anyCompletedCleanly && !result.anyErrored;
2851
2991
  },
2852
2992
  dispose() {
2853
2993
  unsub();
@@ -2863,11 +3003,15 @@ var Graph = class _Graph {
2863
3003
  Object.assign(merged, extra);
2864
3004
  }
2865
3005
  const resolvedTarget = graph.resolve(basePath);
2866
- return graph._createObserveResult(
3006
+ const expanded = graph._createObserveResult(
2867
3007
  basePath,
2868
3008
  resolvedTarget,
2869
3009
  resolveObserveDetail(merged)
2870
3010
  );
3011
+ if (merged.format != null) {
3012
+ graph._attachFormatLogger(expanded, merged);
3013
+ }
3014
+ return expanded;
2871
3015
  }
2872
3016
  };
2873
3017
  }
@@ -2879,27 +3023,33 @@ var Graph = class _Graph {
2879
3023
  dirtyCount: 0,
2880
3024
  resolvedCount: 0,
2881
3025
  events: [],
2882
- completedCleanly: false,
2883
- errored: false
3026
+ anyCompletedCleanly: false,
3027
+ anyErrored: false
2884
3028
  };
3029
+ const nodeErrored = /* @__PURE__ */ new Set();
2885
3030
  const actor = options.actor;
2886
3031
  const targets = [];
2887
3032
  this._collectObserveTargets("", targets);
2888
3033
  targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
2889
3034
  const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
3035
+ let batchSeq = 0;
2890
3036
  const unsubs = picked.map(
2891
3037
  ([path, nd]) => nd.subscribe((msgs) => {
3038
+ batchSeq++;
2892
3039
  for (const m of msgs) {
2893
3040
  const t = m[0];
2894
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
3041
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
2895
3042
  if (t === DATA) {
2896
3043
  result.values[path] = m[1];
2897
3044
  result.events.push({ type: "data", path, data: m[1], ...base });
2898
3045
  } else if (minimal) {
2899
3046
  if (t === DIRTY) result.dirtyCount++;
2900
3047
  else if (t === RESOLVED) result.resolvedCount++;
2901
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
2902
- else if (t === ERROR) result.errored = true;
3048
+ else if (t === COMPLETE && !nodeErrored.has(path)) result.anyCompletedCleanly = true;
3049
+ else if (t === ERROR) {
3050
+ result.anyErrored = true;
3051
+ nodeErrored.add(path);
3052
+ }
2903
3053
  } else if (t === DIRTY) {
2904
3054
  result.dirtyCount++;
2905
3055
  result.events.push({ type: "dirty", path, ...base });
@@ -2907,10 +3057,11 @@ var Graph = class _Graph {
2907
3057
  result.resolvedCount++;
2908
3058
  result.events.push({ type: "resolved", path, ...base });
2909
3059
  } else if (t === COMPLETE) {
2910
- if (!result.errored) result.completedCleanly = true;
3060
+ if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
2911
3061
  result.events.push({ type: "complete", path, ...base });
2912
3062
  } else if (t === ERROR) {
2913
- result.errored = true;
3063
+ result.anyErrored = true;
3064
+ nodeErrored.add(path);
2914
3065
  result.events.push({ type: "error", path, data: m[1], ...base });
2915
3066
  }
2916
3067
  }
@@ -2930,11 +3081,14 @@ var Graph = class _Graph {
2930
3081
  get events() {
2931
3082
  return result.events;
2932
3083
  },
2933
- get completedCleanly() {
2934
- return result.completedCleanly;
3084
+ get anyCompletedCleanly() {
3085
+ return result.anyCompletedCleanly;
2935
3086
  },
2936
- get errored() {
2937
- return result.errored;
3087
+ get anyErrored() {
3088
+ return result.anyErrored;
3089
+ },
3090
+ get completedWithoutErrors() {
3091
+ return result.anyCompletedCleanly && !result.anyErrored;
2938
3092
  },
2939
3093
  dispose() {
2940
3094
  for (const u of unsubs) u();
@@ -2947,25 +3101,169 @@ var Graph = class _Graph {
2947
3101
  } else {
2948
3102
  Object.assign(merged, extra);
2949
3103
  }
2950
- return graph._createObserveResultForAll(resolveObserveDetail(merged));
3104
+ const expanded = graph._createObserveResultForAll(resolveObserveDetail(merged));
3105
+ if (merged.format != null) {
3106
+ graph._attachFormatLogger(expanded, merged);
3107
+ }
3108
+ return expanded;
2951
3109
  }
2952
3110
  };
2953
3111
  }
2954
3112
  /**
2955
- * Convenience live debugger over {@link Graph.observe}. Logs protocol events as they flow.
2956
- *
2957
- * Supports one-node (`path`) and graph-wide modes, event filtering, and JSON/pretty rendering.
2958
- * Color themes are built in (`ansi` / `none`) to avoid external dependencies.
2959
- *
2960
- * @param options - Spy configuration.
2961
- * @returns Disposable handle plus a structured observation accumulator.
3113
+ * Fallback ObserveResult for single-node when inspector is disabled but `format` is requested.
3114
+ * Subscribes to raw messages and accumulates events with timeline info.
3115
+ */
3116
+ _createFallbackObserveResult(path, options) {
3117
+ const timeline = options.timeline !== false;
3118
+ const acc = {
3119
+ values: {},
3120
+ dirtyCount: 0,
3121
+ resolvedCount: 0,
3122
+ events: [],
3123
+ anyCompletedCleanly: false,
3124
+ anyErrored: false
3125
+ };
3126
+ const target = this.resolve(path);
3127
+ let batchSeq = 0;
3128
+ const unsub = target.subscribe((msgs) => {
3129
+ batchSeq++;
3130
+ for (const m of msgs) {
3131
+ const t = m[0];
3132
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
3133
+ if (t === DATA) {
3134
+ acc.values[path] = m[1];
3135
+ acc.events.push({ type: "data", path, data: m[1], ...base });
3136
+ } else if (t === DIRTY) {
3137
+ acc.dirtyCount++;
3138
+ acc.events.push({ type: "dirty", path, ...base });
3139
+ } else if (t === RESOLVED) {
3140
+ acc.resolvedCount++;
3141
+ acc.events.push({ type: "resolved", path, ...base });
3142
+ } else if (t === COMPLETE) {
3143
+ if (!acc.anyErrored) acc.anyCompletedCleanly = true;
3144
+ acc.events.push({ type: "complete", path, ...base });
3145
+ } else if (t === ERROR) {
3146
+ acc.anyErrored = true;
3147
+ acc.events.push({ type: "error", path, data: m[1], ...base });
3148
+ }
3149
+ }
3150
+ });
3151
+ return {
3152
+ get values() {
3153
+ return acc.values;
3154
+ },
3155
+ get dirtyCount() {
3156
+ return acc.dirtyCount;
3157
+ },
3158
+ get resolvedCount() {
3159
+ return acc.resolvedCount;
3160
+ },
3161
+ get events() {
3162
+ return acc.events;
3163
+ },
3164
+ get anyCompletedCleanly() {
3165
+ return acc.anyCompletedCleanly;
3166
+ },
3167
+ get anyErrored() {
3168
+ return acc.anyErrored;
3169
+ },
3170
+ get completedWithoutErrors() {
3171
+ return acc.anyCompletedCleanly && !acc.anyErrored;
3172
+ },
3173
+ dispose() {
3174
+ unsub();
3175
+ },
3176
+ expand() {
3177
+ throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
3178
+ }
3179
+ };
3180
+ }
3181
+ /**
3182
+ * Fallback ObserveResult for graph-wide when inspector is disabled but `format` is requested.
3183
+ */
3184
+ _createFallbackObserveResultForAll(options) {
3185
+ const timeline = options.timeline !== false;
3186
+ const actor = options.actor;
3187
+ const acc = {
3188
+ values: {},
3189
+ dirtyCount: 0,
3190
+ resolvedCount: 0,
3191
+ events: [],
3192
+ anyCompletedCleanly: false,
3193
+ anyErrored: false
3194
+ };
3195
+ const nodeErrored = /* @__PURE__ */ new Set();
3196
+ const targets = [];
3197
+ this._collectObserveTargets("", targets);
3198
+ targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
3199
+ const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
3200
+ let batchSeq = 0;
3201
+ const unsubs = picked.map(
3202
+ ([path, nd]) => nd.subscribe((msgs) => {
3203
+ batchSeq++;
3204
+ for (const m of msgs) {
3205
+ const t = m[0];
3206
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
3207
+ if (t === DATA) {
3208
+ acc.values[path] = m[1];
3209
+ acc.events.push({ type: "data", path, data: m[1], ...base });
3210
+ } else if (t === DIRTY) {
3211
+ acc.dirtyCount++;
3212
+ acc.events.push({ type: "dirty", path, ...base });
3213
+ } else if (t === RESOLVED) {
3214
+ acc.resolvedCount++;
3215
+ acc.events.push({ type: "resolved", path, ...base });
3216
+ } else if (t === COMPLETE) {
3217
+ if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
3218
+ acc.events.push({ type: "complete", path, ...base });
3219
+ } else if (t === ERROR) {
3220
+ acc.anyErrored = true;
3221
+ nodeErrored.add(path);
3222
+ acc.events.push({ type: "error", path, data: m[1], ...base });
3223
+ }
3224
+ }
3225
+ })
3226
+ );
3227
+ return {
3228
+ get values() {
3229
+ return acc.values;
3230
+ },
3231
+ get dirtyCount() {
3232
+ return acc.dirtyCount;
3233
+ },
3234
+ get resolvedCount() {
3235
+ return acc.resolvedCount;
3236
+ },
3237
+ get events() {
3238
+ return acc.events;
3239
+ },
3240
+ get anyCompletedCleanly() {
3241
+ return acc.anyCompletedCleanly;
3242
+ },
3243
+ get anyErrored() {
3244
+ return acc.anyErrored;
3245
+ },
3246
+ get completedWithoutErrors() {
3247
+ return acc.anyCompletedCleanly && !acc.anyErrored;
3248
+ },
3249
+ dispose() {
3250
+ for (const u of unsubs) u();
3251
+ },
3252
+ expand() {
3253
+ throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
3254
+ }
3255
+ };
3256
+ }
3257
+ /**
3258
+ * Attaches a format logger to an ObserveResult, rendering events as they arrive.
3259
+ * Wraps the result's dispose to flush pending events.
2962
3260
  */
2963
- spy(options = {}) {
3261
+ _attachFormatLogger(result, options) {
3262
+ const format = options.format;
3263
+ const logger = options.logger ?? ((line) => console.log(line));
2964
3264
  const include = options.includeTypes ? new Set(options.includeTypes) : null;
2965
3265
  const exclude = options.excludeTypes ? new Set(options.excludeTypes) : null;
2966
- const theme = resolveSpyTheme(options.theme);
2967
- const format = options.format ?? "pretty";
2968
- const logger = options.logger ?? ((line) => console.log(line));
3266
+ const theme = resolveObserveTheme(options.theme);
2969
3267
  const shouldLog = (type) => {
2970
3268
  if (include?.has(type) === false) return false;
2971
3269
  if (exclude?.has(type) === true) return false;
@@ -2990,133 +3288,26 @@ var Graph = class _Graph {
2990
3288
  const batchPart = event.in_batch ? " [batch]" : "";
2991
3289
  return `${pathPart}${color}${event.type.toUpperCase()}${theme.reset}${dataPart}${triggerPart}${batchPart}`;
2992
3290
  };
2993
- if (!_Graph.inspectorEnabled) {
2994
- const timeline = options.timeline ?? true;
2995
- const acc = {
2996
- values: {},
2997
- dirtyCount: 0,
2998
- resolvedCount: 0,
2999
- events: [],
3000
- completedCleanly: false,
3001
- errored: false
3002
- };
3003
- let stop2 = () => {
3004
- };
3005
- const result2 = {
3006
- get values() {
3007
- return acc.values;
3008
- },
3009
- get dirtyCount() {
3010
- return acc.dirtyCount;
3011
- },
3012
- get resolvedCount() {
3013
- return acc.resolvedCount;
3014
- },
3015
- get events() {
3016
- return acc.events;
3017
- },
3018
- get completedCleanly() {
3019
- return acc.completedCleanly;
3020
- },
3021
- get errored() {
3022
- return acc.errored;
3023
- },
3024
- dispose() {
3025
- stop2();
3026
- },
3027
- expand() {
3028
- throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
3029
- }
3030
- };
3031
- const pushEvent = (path, message) => {
3032
- const t = message[0];
3033
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
3034
- let event;
3035
- if (t === DATA) {
3036
- if (path != null) acc.values[path] = message[1];
3037
- event = { type: "data", ...path != null ? { path } : {}, data: message[1], ...base };
3038
- } else if (t === DIRTY) {
3039
- acc.dirtyCount += 1;
3040
- event = { type: "dirty", ...path != null ? { path } : {}, ...base };
3041
- } else if (t === RESOLVED) {
3042
- acc.resolvedCount += 1;
3043
- event = { type: "resolved", ...path != null ? { path } : {}, ...base };
3044
- } else if (t === COMPLETE) {
3045
- if (!acc.errored) acc.completedCleanly = true;
3046
- event = { type: "complete", ...path != null ? { path } : {}, ...base };
3047
- } else if (t === ERROR) {
3048
- acc.errored = true;
3049
- event = {
3050
- type: "error",
3051
- ...path != null ? { path } : {},
3052
- data: message[1],
3053
- ...base
3054
- };
3291
+ let cursor = 0;
3292
+ const flush = () => {
3293
+ const events = result.events;
3294
+ while (cursor < events.length) {
3295
+ const event = events[cursor++];
3296
+ if (shouldLog(event.type)) {
3297
+ logger(renderEvent(event), event);
3055
3298
  }
3056
- if (!event) return;
3057
- acc.events.push(event);
3058
- if (!shouldLog(event.type)) return;
3059
- logger(renderEvent(event), event);
3060
- };
3061
- if (options.path != null) {
3062
- const stream2 = this.observe(options.path, {
3063
- actor: options.actor,
3064
- structured: false
3065
- });
3066
- stop2 = stream2.subscribe((messages) => {
3067
- for (const m of messages) {
3068
- pushEvent(options.path, m);
3069
- }
3070
- });
3071
- } else {
3072
- const stream2 = this.observe({ actor: options.actor, structured: false });
3073
- stop2 = stream2.subscribe((path, messages) => {
3074
- for (const m of messages) {
3075
- pushEvent(path, m);
3076
- }
3077
- });
3078
3299
  }
3079
- return {
3080
- result: result2,
3081
- dispose() {
3082
- result2.dispose();
3083
- }
3084
- };
3085
- }
3086
- const structuredObserveOptions = {
3087
- actor: options.actor,
3088
- structured: true,
3089
- ...options.timeline !== false ? { timeline: true } : {},
3090
- ...options.causal ? { causal: true } : {},
3091
- ...options.derived ? { derived: true } : {}
3092
3300
  };
3093
- const result = options.path != null ? this.observe(options.path, structuredObserveOptions) : this.observe(structuredObserveOptions);
3094
- let cursor = 0;
3095
- const flushNewEvents = () => {
3096
- const nextEvents = result.events.slice(cursor);
3097
- cursor = result.events.length;
3098
- for (const event of nextEvents) {
3099
- if (!shouldLog(event.type)) continue;
3100
- logger(renderEvent(event), event);
3101
- }
3301
+ const origPush = result.events.push;
3302
+ result.events.push = function(...items) {
3303
+ const ret = origPush.apply(this, items);
3304
+ flush();
3305
+ return ret;
3102
3306
  };
3103
- const stream = options.path != null ? this.observe(options.path, { actor: options.actor, structured: false }) : this.observe({ actor: options.actor, structured: false });
3104
- const stop = options.path != null ? stream.subscribe((messages) => {
3105
- if (messages.length > 0) {
3106
- flushNewEvents();
3107
- }
3108
- }) : stream.subscribe((_path, messages) => {
3109
- if (messages.length > 0) {
3110
- flushNewEvents();
3111
- }
3112
- });
3113
- return {
3114
- result,
3115
- dispose() {
3116
- stop();
3117
- flushNewEvents();
3118
- result.dispose();
3119
- }
3307
+ const origDispose = result.dispose.bind(result);
3308
+ result.dispose = () => {
3309
+ origDispose();
3310
+ flush();
3120
3311
  };
3121
3312
  }
3122
3313
  /**
@@ -3379,8 +3570,9 @@ var Graph = class _Graph {
3379
3570
  /**
3380
3571
  * Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
3381
3572
  *
3382
- * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 2 messages
3383
- * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1 control waves.
3573
+ * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 3 messages
3574
+ * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1/2 control
3575
+ * waves (`START`/`DIRTY`/`INVALIDATE`/`PAUSE`/`RESUME`).
3384
3576
  */
3385
3577
  autoCheckpoint(adapter, options = {}) {
3386
3578
  const debounceMs = Math.max(0, options.debounceMs ?? 500);
@@ -3427,7 +3619,7 @@ var Graph = class _Graph {
3427
3619
  timer = setTimeout(flush, debounceMs);
3428
3620
  };
3429
3621
  const off = this.observe().subscribe((path, messages) => {
3430
- const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 2);
3622
+ const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
3431
3623
  if (!triggeredByTier) return;
3432
3624
  if (options.filter) {
3433
3625
  const nd = this.resolve(path);
@@ -3511,33 +3703,21 @@ var Graph = class _Graph {
3511
3703
  // ——————————————————————————————————————————————————————————————
3512
3704
  /**
3513
3705
  * When `false`, structured observation options (`causal`, `timeline`),
3514
- * `annotate()`, and `traceLog()` are no-ops. Raw `observe()` always works.
3706
+ * and `trace()` writes are no-ops. Raw `observe()` always works.
3515
3707
  *
3516
3708
  * Default: `true` outside production (`process.env.NODE_ENV !== "production"`).
3517
3709
  */
3518
3710
  static inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
3519
3711
  _annotations = /* @__PURE__ */ new Map();
3520
3712
  _traceRing = new RingBuffer(1e3);
3521
- /**
3522
- * Attaches a reasoning annotation to a node — captures *why* an AI agent set a value.
3523
- *
3524
- * No-op when {@link Graph.inspectorEnabled} is `false`.
3525
- *
3526
- * @param path - Qualified node path.
3527
- * @param reason - Free-text note stored in the trace ring buffer.
3528
- */
3529
- annotate(path, reason) {
3530
- if (!_Graph.inspectorEnabled) return;
3531
- this.resolve(path);
3532
- this._annotations.set(path, reason);
3533
- this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
3534
- }
3535
- /**
3536
- * Returns a chronological log of all reasoning annotations (ring buffer).
3537
- *
3538
- * @returns `[]` when {@link Graph.inspectorEnabled} is `false`.
3539
- */
3540
- traceLog() {
3713
+ trace(path, reason) {
3714
+ if (path != null && reason != null) {
3715
+ if (!_Graph.inspectorEnabled) return;
3716
+ this.resolve(path);
3717
+ this._annotations.set(path, reason);
3718
+ this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
3719
+ return;
3720
+ }
3541
3721
  if (!_Graph.inspectorEnabled) return [];
3542
3722
  return this._traceRing.toArray();
3543
3723
  }