@graphrefly/graphrefly 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/{chunk-J7S54G7I.js → chunk-2L5J6RPM.js} +7 -2
  2. package/dist/chunk-2L5J6RPM.js.map +1 -0
  3. package/dist/{chunk-LB3RYLSC.js → chunk-3N2Y6PCR.js} +197 -42
  4. package/dist/chunk-3N2Y6PCR.js.map +1 -0
  5. package/dist/{chunk-KJGUP35I.js → chunk-5PSVTDNZ.js} +22 -9
  6. package/dist/chunk-5PSVTDNZ.js.map +1 -0
  7. package/dist/{chunk-76YPZQTW.js → chunk-BJAOEU4D.js} +34 -29
  8. package/dist/chunk-BJAOEU4D.js.map +1 -0
  9. package/dist/{chunk-UVWEKTYC.js → chunk-IAPLC4NR.js} +3 -3
  10. package/dist/{chunk-F6ORUNO7.js → chunk-OOA2UTXF.js} +58 -2
  11. package/dist/chunk-OOA2UTXF.js.map +1 -0
  12. package/dist/{chunk-TNKODJ6E.js → chunk-PGEU5MEH.js} +7 -3
  13. package/dist/{chunk-TNKODJ6E.js.map → chunk-PGEU5MEH.js.map} +1 -1
  14. package/dist/chunk-R2LPZIY2.js +111 -0
  15. package/dist/chunk-R2LPZIY2.js.map +1 -0
  16. package/dist/{chunk-BV3TPSBK.js → chunk-XYL3GLB3.js} +742 -757
  17. package/dist/chunk-XYL3GLB3.js.map +1 -0
  18. package/dist/compat/nestjs/index.cjs +967 -811
  19. package/dist/compat/nestjs/index.cjs.map +1 -1
  20. package/dist/compat/nestjs/index.d.cts +4 -4
  21. package/dist/compat/nestjs/index.d.ts +4 -4
  22. package/dist/compat/nestjs/index.js +7 -7
  23. package/dist/core/index.cjs +653 -666
  24. package/dist/core/index.cjs.map +1 -1
  25. package/dist/core/index.d.cts +2 -2
  26. package/dist/core/index.d.ts +2 -2
  27. package/dist/core/index.js +7 -3
  28. package/dist/extra/index.cjs +728 -688
  29. package/dist/extra/index.cjs.map +1 -1
  30. package/dist/extra/index.d.cts +4 -4
  31. package/dist/extra/index.d.ts +4 -4
  32. package/dist/extra/index.js +9 -5
  33. package/dist/graph/index.cjs +836 -808
  34. package/dist/graph/index.cjs.map +1 -1
  35. package/dist/graph/index.d.cts +3 -3
  36. package/dist/graph/index.d.ts +3 -3
  37. package/dist/graph/index.js +8 -8
  38. package/dist/{graph-gISB9n3n.d.ts → graph-KsTe57nI.d.cts} +82 -8
  39. package/dist/{graph-BYFlyNpX.d.cts → graph-mILUUqW8.d.ts} +82 -8
  40. package/dist/{index-CgKPpiu8.d.ts → index-8a605sg9.d.ts} +2 -2
  41. package/dist/{index-DKaB2x0T.d.ts → index-B2SvPEbc.d.ts} +6 -65
  42. package/dist/{index-EmzYk-TG.d.cts → index-BHfg_Ez3.d.ts} +123 -77
  43. package/dist/{index-B80mMeuf.d.ts → index-Bc_diYYJ.d.cts} +123 -77
  44. package/dist/{index-B43mC7uY.d.cts → index-BjtlNirP.d.cts} +3 -3
  45. package/dist/{index-CEDaJaYE.d.ts → index-CgSiUouz.d.ts} +3 -3
  46. package/dist/{index-7WnwgjMu.d.ts → index-DuN3bhtm.d.ts} +82 -32
  47. package/dist/{index-D_tUMcpz.d.cts → index-SFzE_KTa.d.cts} +82 -32
  48. package/dist/{index-Ci_vPaVm.d.cts → index-UudxGnzc.d.cts} +6 -65
  49. package/dist/{index-BqOWSFhr.d.cts → index-VHA43cGP.d.cts} +2 -2
  50. package/dist/index.cjs +5936 -5522
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +644 -379
  53. package/dist/index.d.ts +644 -379
  54. package/dist/index.js +4388 -4058
  55. package/dist/index.js.map +1 -1
  56. package/dist/{meta-npl5b97j.d.cts → meta-BnG7XAaE.d.cts} +394 -236
  57. package/dist/{meta-npl5b97j.d.ts → meta-BnG7XAaE.d.ts} +394 -236
  58. package/dist/{observable-DFBCBELR.d.cts → observable-C8Kx_O6k.d.cts} +1 -1
  59. package/dist/{observable-oAGygKvc.d.ts → observable-DcBwQY7t.d.ts} +1 -1
  60. package/dist/patterns/reactive-layout/index.cjs +865 -718
  61. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  62. package/dist/patterns/reactive-layout/index.d.cts +3 -3
  63. package/dist/patterns/reactive-layout/index.d.ts +3 -3
  64. package/dist/patterns/reactive-layout/index.js +4 -4
  65. package/package.json +2 -2
  66. package/dist/chunk-76YPZQTW.js.map +0 -1
  67. package/dist/chunk-BV3TPSBK.js.map +0 -1
  68. package/dist/chunk-F6ORUNO7.js.map +0 -1
  69. package/dist/chunk-FCLROC4Q.js +0 -231
  70. package/dist/chunk-FCLROC4Q.js.map +0 -1
  71. package/dist/chunk-J7S54G7I.js.map +0 -1
  72. package/dist/chunk-KJGUP35I.js.map +0 -1
  73. package/dist/chunk-LB3RYLSC.js.map +0 -1
  74. /package/dist/{chunk-UVWEKTYC.js.map → chunk-IAPLC4NR.js.map} +0 -0
@@ -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";
1075
1124
  }
1076
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();
1155
+ }
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,297 +1235,146 @@ 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;
1154
- }
1155
- if (this._fn) {
1156
- this._runFn();
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]]);
1157
1244
  }
1158
1245
  }
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();
1170
- }
1171
- _disconnectUpstream() {
1172
- if (!this._connected) return;
1173
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1174
- unsub();
1246
+ _onDepSettled(index) {
1247
+ if (!this._depDirtyMask.has(index)) {
1248
+ this._onDepDirty(index);
1175
1249
  }
1176
- this._connected = false;
1177
- this._depDirtyMask.reset();
1178
- this._depSettledMask.reset();
1179
- this._depCompleteMask.reset();
1180
- this._status = "disconnected";
1181
- }
1182
- };
1183
- function node(depsOrFn, fnOrOpts, optsArg) {
1184
- const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1185
- const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
1186
- let opts = {};
1187
- if (isNodeArray(depsOrFn)) {
1188
- opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
1189
- } else if (isNodeOptions(depsOrFn)) {
1190
- opts = depsOrFn;
1191
- } else {
1192
- opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
1193
- }
1194
- return new NodeImpl(deps, fn, opts);
1195
- }
1196
-
1197
- // src/core/sugar.ts
1198
- function state(initial, opts) {
1199
- return node([], { ...opts, initial });
1200
- }
1201
- function derived(deps, fn, opts) {
1202
- return node(deps, fn, { describeKind: "derived", ...opts });
1203
- }
1204
-
1205
- // src/core/dynamic-node.ts
1206
- var DynamicNodeImpl = class {
1207
- _optsName;
1208
- _registryName;
1209
- _describeKind;
1210
- meta;
1211
- _fn;
1212
- _equals;
1213
- _resubscribable;
1214
- _resetOnTeardown;
1215
- _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
- // Dynamic deps tracking
1237
- _deps = [];
1238
- _depUnsubs = [];
1239
- _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;
1246
- constructor(fn, opts) {
1247
- 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
- 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
- });
1250
+ this._depSettledMask.set(index);
1251
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1252
+ this._depDirtyMask.reset();
1253
+ this._depSettledMask.reset();
1254
+ this._runFn();
1266
1255
  }
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
1256
  }
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() };
1257
+ _maybeCompleteFromDeps() {
1258
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1259
+ this._downInternal([[COMPLETE]]);
1336
1260
  }
1337
- this._downInternal(messages);
1338
1261
  }
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;
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
+ }
1355
1278
  }
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]);
1279
+ if (allSame) {
1280
+ if (this._status === "dirty") {
1281
+ this._downInternal([[RESOLVED]]);
1282
+ }
1283
+ return;
1361
1284
  }
1362
- if (filtered.length > 0) {
1363
- downWithBatch(this._boundDownToSinks, filtered);
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);
1364
1298
  }
1365
1299
  return;
1366
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]]);
1367
1312
  }
1368
- downWithBatch(this._boundDownToSinks, sinkMessages);
1369
1313
  }
1370
- _canSkipDirty() {
1371
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
1314
+ };
1315
+ var isNodeArray = (value) => Array.isArray(value);
1316
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1317
+ function node(depsOrFn, fnOrOpts, optsArg) {
1318
+ const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1319
+ const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
1320
+ let opts = {};
1321
+ if (isNodeArray(depsOrFn)) {
1322
+ opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
1323
+ } else if (isNodeOptions(depsOrFn)) {
1324
+ opts = depsOrFn;
1325
+ } else {
1326
+ opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
1372
1327
  }
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
- };
1328
+ return new NodeImpl(deps, fn, opts);
1329
+ }
1330
+
1331
+ // src/core/sugar.ts
1332
+ function state(initial, opts) {
1333
+ return node([], { ...opts, initial });
1334
+ }
1335
+ function derived(deps, fn, opts) {
1336
+ return node(deps, fn, { describeKind: "derived", ...opts });
1337
+ }
1338
+
1339
+ // src/core/dynamic-node.ts
1340
+ var MAX_RERUN = 16;
1341
+ var DynamicNodeImpl = class extends NodeBase {
1342
+ _fn;
1343
+ _autoComplete;
1344
+ // Dynamic deps tracking
1345
+ /** @internal Read by `describeNode`. */
1346
+ _deps = [];
1347
+ _depUnsubs = [];
1348
+ _depIndexMap = /* @__PURE__ */ new Map();
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;
1358
+ constructor(fn, opts) {
1359
+ super(opts);
1360
+ this._fn = fn;
1361
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
1362
+ this.down = this.down.bind(this);
1363
+ this.up = this.up.bind(this);
1364
+ }
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
+ });
1426
1372
  }
1373
+ /** Versioning not supported on DynamicNodeImpl (override base). */
1374
+ get v() {
1375
+ return void 0;
1376
+ }
1377
+ // --- Up / unsubscribe ---
1427
1378
  up(messages, options) {
1428
1379
  if (this._deps.length === 0) return;
1429
1380
  if (!options?.internal && this._guard != null) {
@@ -1431,221 +1382,227 @@ var DynamicNodeImpl = class {
1431
1382
  if (!this._guard(actor, "write")) {
1432
1383
  throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1433
1384
  }
1434
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1385
+ this._recordMutation(actor);
1435
1386
  }
1436
1387
  for (const dep of this._deps) {
1437
1388
  dep.up?.(messages, options);
1438
1389
  }
1439
1390
  }
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
- }
1454
- }
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
- }
1488
- }
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 {
1495
- }
1391
+ _upInternal(messages) {
1392
+ for (const dep of this._deps) {
1393
+ dep.up?.(messages, { internal: true });
1496
1394
  }
1497
1395
  }
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;
1508
- }
1509
- if (unchanged) {
1510
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
1511
- return;
1512
- }
1513
- this._cached = value;
1514
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
1396
+ unsubscribe() {
1397
+ this._disconnect();
1515
1398
  }
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();
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__";
@@ -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}`;
@@ -2776,8 +2903,8 @@ var Graph = class _Graph {
2776
2903
  dirtyCount: 0,
2777
2904
  resolvedCount: 0,
2778
2905
  events: [],
2779
- completedCleanly: false,
2780
- errored: false
2906
+ anyCompletedCleanly: false,
2907
+ anyErrored: false
2781
2908
  };
2782
2909
  let lastTriggerDepIndex;
2783
2910
  let lastRunDepValues;
@@ -2821,8 +2948,8 @@ var Graph = class _Graph {
2821
2948
  } else if (minimal) {
2822
2949
  if (t === DIRTY) result.dirtyCount++;
2823
2950
  else if (t === RESOLVED) result.resolvedCount++;
2824
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
2825
- 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;
2826
2953
  } else if (t === DIRTY) {
2827
2954
  result.dirtyCount++;
2828
2955
  result.events.push({ type: "dirty", path, ...base });
@@ -2830,10 +2957,10 @@ var Graph = class _Graph {
2830
2957
  result.resolvedCount++;
2831
2958
  result.events.push({ type: "resolved", path, ...base, ...withCausal });
2832
2959
  } else if (t === COMPLETE) {
2833
- if (!result.errored) result.completedCleanly = true;
2960
+ if (!result.anyErrored) result.anyCompletedCleanly = true;
2834
2961
  result.events.push({ type: "complete", path, ...base });
2835
2962
  } else if (t === ERROR) {
2836
- result.errored = true;
2963
+ result.anyErrored = true;
2837
2964
  result.events.push({ type: "error", path, data: m[1], ...base });
2838
2965
  }
2839
2966
  }
@@ -2853,11 +2980,14 @@ var Graph = class _Graph {
2853
2980
  get events() {
2854
2981
  return result.events;
2855
2982
  },
2856
- get completedCleanly() {
2857
- return result.completedCleanly;
2983
+ get anyCompletedCleanly() {
2984
+ return result.anyCompletedCleanly;
2985
+ },
2986
+ get anyErrored() {
2987
+ return result.anyErrored;
2858
2988
  },
2859
- get errored() {
2860
- return result.errored;
2989
+ get completedWithoutErrors() {
2990
+ return result.anyCompletedCleanly && !result.anyErrored;
2861
2991
  },
2862
2992
  dispose() {
2863
2993
  unsub();
@@ -2893,9 +3023,10 @@ var Graph = class _Graph {
2893
3023
  dirtyCount: 0,
2894
3024
  resolvedCount: 0,
2895
3025
  events: [],
2896
- completedCleanly: false,
2897
- errored: false
3026
+ anyCompletedCleanly: false,
3027
+ anyErrored: false
2898
3028
  };
3029
+ const nodeErrored = /* @__PURE__ */ new Set();
2899
3030
  const actor = options.actor;
2900
3031
  const targets = [];
2901
3032
  this._collectObserveTargets("", targets);
@@ -2914,8 +3045,11 @@ var Graph = class _Graph {
2914
3045
  } else if (minimal) {
2915
3046
  if (t === DIRTY) result.dirtyCount++;
2916
3047
  else if (t === RESOLVED) result.resolvedCount++;
2917
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
2918
- 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
+ }
2919
3053
  } else if (t === DIRTY) {
2920
3054
  result.dirtyCount++;
2921
3055
  result.events.push({ type: "dirty", path, ...base });
@@ -2923,10 +3057,11 @@ var Graph = class _Graph {
2923
3057
  result.resolvedCount++;
2924
3058
  result.events.push({ type: "resolved", path, ...base });
2925
3059
  } else if (t === COMPLETE) {
2926
- if (!result.errored) result.completedCleanly = true;
3060
+ if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
2927
3061
  result.events.push({ type: "complete", path, ...base });
2928
3062
  } else if (t === ERROR) {
2929
- result.errored = true;
3063
+ result.anyErrored = true;
3064
+ nodeErrored.add(path);
2930
3065
  result.events.push({ type: "error", path, data: m[1], ...base });
2931
3066
  }
2932
3067
  }
@@ -2946,11 +3081,14 @@ var Graph = class _Graph {
2946
3081
  get events() {
2947
3082
  return result.events;
2948
3083
  },
2949
- get completedCleanly() {
2950
- return result.completedCleanly;
3084
+ get anyCompletedCleanly() {
3085
+ return result.anyCompletedCleanly;
3086
+ },
3087
+ get anyErrored() {
3088
+ return result.anyErrored;
2951
3089
  },
2952
- get errored() {
2953
- return result.errored;
3090
+ get completedWithoutErrors() {
3091
+ return result.anyCompletedCleanly && !result.anyErrored;
2954
3092
  },
2955
3093
  dispose() {
2956
3094
  for (const u of unsubs) u();
@@ -2982,8 +3120,8 @@ var Graph = class _Graph {
2982
3120
  dirtyCount: 0,
2983
3121
  resolvedCount: 0,
2984
3122
  events: [],
2985
- completedCleanly: false,
2986
- errored: false
3123
+ anyCompletedCleanly: false,
3124
+ anyErrored: false
2987
3125
  };
2988
3126
  const target = this.resolve(path);
2989
3127
  let batchSeq = 0;
@@ -3002,10 +3140,10 @@ var Graph = class _Graph {
3002
3140
  acc.resolvedCount++;
3003
3141
  acc.events.push({ type: "resolved", path, ...base });
3004
3142
  } else if (t === COMPLETE) {
3005
- if (!acc.errored) acc.completedCleanly = true;
3143
+ if (!acc.anyErrored) acc.anyCompletedCleanly = true;
3006
3144
  acc.events.push({ type: "complete", path, ...base });
3007
3145
  } else if (t === ERROR) {
3008
- acc.errored = true;
3146
+ acc.anyErrored = true;
3009
3147
  acc.events.push({ type: "error", path, data: m[1], ...base });
3010
3148
  }
3011
3149
  }
@@ -3023,11 +3161,14 @@ var Graph = class _Graph {
3023
3161
  get events() {
3024
3162
  return acc.events;
3025
3163
  },
3026
- get completedCleanly() {
3027
- return acc.completedCleanly;
3164
+ get anyCompletedCleanly() {
3165
+ return acc.anyCompletedCleanly;
3028
3166
  },
3029
- get errored() {
3030
- return acc.errored;
3167
+ get anyErrored() {
3168
+ return acc.anyErrored;
3169
+ },
3170
+ get completedWithoutErrors() {
3171
+ return acc.anyCompletedCleanly && !acc.anyErrored;
3031
3172
  },
3032
3173
  dispose() {
3033
3174
  unsub();
@@ -3048,9 +3189,10 @@ var Graph = class _Graph {
3048
3189
  dirtyCount: 0,
3049
3190
  resolvedCount: 0,
3050
3191
  events: [],
3051
- completedCleanly: false,
3052
- errored: false
3192
+ anyCompletedCleanly: false,
3193
+ anyErrored: false
3053
3194
  };
3195
+ const nodeErrored = /* @__PURE__ */ new Set();
3054
3196
  const targets = [];
3055
3197
  this._collectObserveTargets("", targets);
3056
3198
  targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
@@ -3072,10 +3214,11 @@ var Graph = class _Graph {
3072
3214
  acc.resolvedCount++;
3073
3215
  acc.events.push({ type: "resolved", path, ...base });
3074
3216
  } else if (t === COMPLETE) {
3075
- if (!acc.errored) acc.completedCleanly = true;
3217
+ if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
3076
3218
  acc.events.push({ type: "complete", path, ...base });
3077
3219
  } else if (t === ERROR) {
3078
- acc.errored = true;
3220
+ acc.anyErrored = true;
3221
+ nodeErrored.add(path);
3079
3222
  acc.events.push({ type: "error", path, data: m[1], ...base });
3080
3223
  }
3081
3224
  }
@@ -3094,11 +3237,14 @@ var Graph = class _Graph {
3094
3237
  get events() {
3095
3238
  return acc.events;
3096
3239
  },
3097
- get completedCleanly() {
3098
- return acc.completedCleanly;
3240
+ get anyCompletedCleanly() {
3241
+ return acc.anyCompletedCleanly;
3242
+ },
3243
+ get anyErrored() {
3244
+ return acc.anyErrored;
3099
3245
  },
3100
- get errored() {
3101
- return acc.errored;
3246
+ get completedWithoutErrors() {
3247
+ return acc.anyCompletedCleanly && !acc.anyErrored;
3102
3248
  },
3103
3249
  dispose() {
3104
3250
  for (const u of unsubs) u();
@@ -3424,8 +3570,9 @@ var Graph = class _Graph {
3424
3570
  /**
3425
3571
  * Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
3426
3572
  *
3427
- * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 2 messages
3428
- * 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`).
3429
3576
  */
3430
3577
  autoCheckpoint(adapter, options = {}) {
3431
3578
  const debounceMs = Math.max(0, options.debounceMs ?? 500);
@@ -3472,7 +3619,7 @@ var Graph = class _Graph {
3472
3619
  timer = setTimeout(flush, debounceMs);
3473
3620
  };
3474
3621
  const off = this.observe().subscribe((path, messages) => {
3475
- const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 2);
3622
+ const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
3476
3623
  if (!triggeredByTier) return;
3477
3624
  if (options.filter) {
3478
3625
  const nd = this.resolve(path);