@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
@@ -65,6 +65,7 @@ __export(extra_exports, {
65
65
  find: () => find,
66
66
  first: () => first,
67
67
  firstValueFrom: () => firstValueFrom,
68
+ firstWhere: () => firstWhere,
68
69
  flatMap: () => flatMap,
69
70
  forEach: () => forEach,
70
71
  fromAny: () => fromAny,
@@ -111,8 +112,6 @@ __export(extra_exports, {
111
112
  mergeMap: () => mergeMap,
112
113
  nameToSignal: () => nameToSignal,
113
114
  never: () => never,
114
- observeGraph$: () => observeGraph$,
115
- observeNode$: () => observeNode$,
116
115
  of: () => of,
117
116
  pairwise: () => pairwise,
118
117
  parseCron: () => parseCron,
@@ -144,7 +143,6 @@ __export(extra_exports, {
144
143
  shareReplay: () => shareReplay,
145
144
  signalToName: () => signalToName,
146
145
  skip: () => skip,
147
- startWith: () => startWith,
148
146
  switchMap: () => switchMap,
149
147
  take: () => take,
150
148
  takeUntil: () => takeUntil,
@@ -161,7 +159,6 @@ __export(extra_exports, {
161
159
  toFile: () => toFile,
162
160
  toKafka: () => toKafka,
163
161
  toLoki: () => toLoki,
164
- toMessages$: () => toMessages$,
165
162
  toMongo: () => toMongo,
166
163
  toNATS: () => toNATS,
167
164
  toObservable: () => toObservable,
@@ -192,6 +189,7 @@ __export(extra_exports, {
192
189
  module.exports = __toCommonJS(extra_exports);
193
190
 
194
191
  // src/core/messages.ts
192
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
195
193
  var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
196
194
  var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
197
195
  var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
@@ -202,6 +200,7 @@ var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
202
200
  var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
203
201
  var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
204
202
  var knownMessageTypes = [
203
+ START,
205
204
  DATA,
206
205
  DIRTY,
207
206
  RESOLVED,
@@ -212,13 +211,15 @@ var knownMessageTypes = [
212
211
  COMPLETE,
213
212
  ERROR
214
213
  ];
214
+ var knownMessageSet = new Set(knownMessageTypes);
215
215
  function messageTier(t) {
216
- if (t === DIRTY || t === INVALIDATE) return 0;
217
- if (t === PAUSE || t === RESUME) return 1;
218
- if (t === DATA || t === RESOLVED) return 2;
219
- if (t === COMPLETE || t === ERROR) return 3;
220
- if (t === TEARDOWN) return 4;
221
- return 0;
216
+ if (t === START) return 0;
217
+ if (t === DIRTY || t === INVALIDATE) return 1;
218
+ if (t === PAUSE || t === RESUME) return 2;
219
+ if (t === DATA || t === RESOLVED) return 3;
220
+ if (t === COMPLETE || t === ERROR) return 4;
221
+ if (t === TEARDOWN) return 5;
222
+ return 1;
222
223
  }
223
224
  function isPhase2Message(msg) {
224
225
  const t = msg[0];
@@ -227,6 +228,10 @@ function isPhase2Message(msg) {
227
228
  function isTerminalMessage(t) {
228
229
  return t === COMPLETE || t === ERROR;
229
230
  }
231
+ function isLocalOnly(t) {
232
+ if (!knownMessageSet.has(t)) return false;
233
+ return messageTier(t) < 3;
234
+ }
230
235
  function propagatesToMeta(t) {
231
236
  return t === TEARDOWN;
232
237
  }
@@ -387,14 +392,14 @@ function _downSequential(sink, messages, phase = 2) {
387
392
  const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
388
393
  for (const msg of messages) {
389
394
  const tier = messageTier(msg[0]);
390
- if (tier === 2) {
395
+ if (tier === 3) {
391
396
  if (isBatching()) {
392
397
  const m = msg;
393
398
  dataQueue.push(() => sink([m]));
394
399
  } else {
395
400
  sink([msg]);
396
401
  }
397
- } else if (tier >= 3) {
402
+ } else if (tier >= 4) {
398
403
  if (isBatching()) {
399
404
  const m = msg;
400
405
  pendingPhase3.push(() => sink([m]));
@@ -503,10 +508,24 @@ function advanceVersion(info, newValue, hashFn) {
503
508
  }
504
509
  }
505
510
 
506
- // src/core/node.ts
511
+ // src/core/node-base.ts
507
512
  var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
508
513
  var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
509
- function createIntBitSet() {
514
+ var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
515
+ var isCleanupFn = (value) => typeof value === "function";
516
+ function statusAfterMessage(status, msg) {
517
+ const t = msg[0];
518
+ if (t === DIRTY) return "dirty";
519
+ if (t === DATA) return "settled";
520
+ if (t === RESOLVED) return "resolved";
521
+ if (t === COMPLETE) return "completed";
522
+ if (t === ERROR) return "errored";
523
+ if (t === INVALIDATE) return "dirty";
524
+ if (t === TEARDOWN) return "disconnected";
525
+ return status;
526
+ }
527
+ function createIntBitSet(size) {
528
+ const fullMask = size >= 32 ? -1 : ~(-1 << size);
510
529
  let bits = 0;
511
530
  return {
512
531
  set(i) {
@@ -519,7 +538,8 @@ function createIntBitSet() {
519
538
  return (bits & 1 << i) !== 0;
520
539
  },
521
540
  covers(other) {
522
- return (bits & other._bits()) === other._bits();
541
+ const otherBits = other._bits();
542
+ return (bits & otherBits) === otherBits;
523
543
  },
524
544
  any() {
525
545
  return bits !== 0;
@@ -527,6 +547,9 @@ function createIntBitSet() {
527
547
  reset() {
528
548
  bits = 0;
529
549
  },
550
+ setAll() {
551
+ bits = fullMask;
552
+ },
530
553
  _bits() {
531
554
  return bits;
532
555
  }
@@ -534,6 +557,8 @@ function createIntBitSet() {
534
557
  }
535
558
  function createArrayBitSet(size) {
536
559
  const words = new Uint32Array(Math.ceil(size / 32));
560
+ const lastBits = size % 32;
561
+ const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
537
562
  return {
538
563
  set(i) {
539
564
  words[i >>> 5] |= 1 << (i & 31);
@@ -560,130 +585,103 @@ function createArrayBitSet(size) {
560
585
  reset() {
561
586
  words.fill(0);
562
587
  },
588
+ setAll() {
589
+ for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
590
+ if (words.length > 0) words[words.length - 1] = lastWordMask;
591
+ },
563
592
  _words: words
564
593
  };
565
594
  }
566
595
  function createBitSet(size) {
567
- return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
596
+ return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
568
597
  }
569
- var isNodeArray = (value) => Array.isArray(value);
570
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
571
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
572
- var isCleanupFn = (value) => typeof value === "function";
573
- var statusAfterMessage = (status, msg) => {
574
- const t = msg[0];
575
- if (t === DIRTY) return "dirty";
576
- if (t === DATA) return "settled";
577
- if (t === RESOLVED) return "resolved";
578
- if (t === COMPLETE) return "completed";
579
- if (t === ERROR) return "errored";
580
- if (t === INVALIDATE) return "dirty";
581
- if (t === TEARDOWN) return "disconnected";
582
- return status;
583
- };
584
- var NodeImpl = class {
585
- // --- Configuration (set once, never reassigned) ---
598
+ var NodeBase = class {
599
+ // --- Identity (set once) ---
586
600
  _optsName;
587
601
  _registryName;
588
- /** @internal read by {@link describeNode} before inference. */
602
+ /** @internal Read by `describeNode` before inference. */
589
603
  _describeKind;
590
604
  meta;
591
- _deps;
592
- _fn;
593
- _opts;
605
+ // --- Options ---
594
606
  _equals;
607
+ _resubscribable;
608
+ _resetOnTeardown;
609
+ _onResubscribe;
595
610
  _onMessage;
596
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
611
+ /** @internal Read by `describeNode` for `accessHintForGuard`. */
597
612
  _guard;
613
+ /** @internal Subclasses update this through {@link _recordMutation}. */
598
614
  _lastMutation;
599
- _hasDeps;
600
- _autoComplete;
601
- _isSingleDep;
602
- // --- Mutable state ---
615
+ // --- Versioning ---
616
+ _hashFn;
617
+ _versioning;
618
+ // --- Lifecycle state ---
619
+ /** @internal Read by `describeNode` and `graph.ts`. */
603
620
  _cached;
621
+ /** @internal Read externally via `get status()`. */
604
622
  _status;
605
623
  _terminal = false;
606
- _connected = false;
607
- _producerStarted = false;
608
- _connecting = false;
609
- _manualEmitUsed = false;
624
+ _active = false;
625
+ // --- Sink storage ---
626
+ /** @internal Read by `graph/profile.ts` for subscriber counts. */
610
627
  _sinkCount = 0;
611
628
  _singleDepSinkCount = 0;
612
- // --- Object/collection state ---
613
- _depDirtyMask;
614
- _depSettledMask;
615
- _depCompleteMask;
616
- _allDepsCompleteMask;
617
- _lastDepValues;
618
- _cleanup;
619
- _sinks = null;
620
629
  _singleDepSinks = /* @__PURE__ */ new WeakSet();
621
- _upstreamUnsubs = [];
630
+ _sinks = null;
631
+ // --- Actions + bound helpers ---
622
632
  _actions;
623
633
  _boundDownToSinks;
634
+ // --- Inspector hook (Graph observability) ---
624
635
  _inspectorHook;
625
- _versioning;
626
- _hashFn;
627
- constructor(deps, fn, opts) {
628
- this._deps = deps;
629
- this._fn = fn;
630
- this._opts = opts;
636
+ constructor(opts) {
631
637
  this._optsName = opts.name;
632
638
  this._describeKind = opts.describeKind;
633
639
  this._equals = opts.equals ?? Object.is;
640
+ this._resubscribable = opts.resubscribable ?? false;
641
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
642
+ this._onResubscribe = opts.onResubscribe;
634
643
  this._onMessage = opts.onMessage;
635
644
  this._guard = opts.guard;
636
- this._hasDeps = deps.length > 0;
637
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
638
- this._isSingleDep = deps.length === 1 && fn != null;
639
645
  this._cached = "initial" in opts ? opts.initial : NO_VALUE;
640
- this._status = this._hasDeps ? "disconnected" : "settled";
646
+ this._status = "disconnected";
641
647
  this._hashFn = opts.versioningHash ?? defaultHash;
642
648
  this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
643
649
  id: opts.versioningId,
644
650
  hash: this._hashFn
645
651
  }) : void 0;
646
- this._depDirtyMask = createBitSet(deps.length);
647
- this._depSettledMask = createBitSet(deps.length);
648
- this._depCompleteMask = createBitSet(deps.length);
649
- this._allDepsCompleteMask = createBitSet(deps.length);
650
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
651
652
  const meta = {};
652
653
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
653
- meta[k] = node({
654
- initial: v,
655
- name: `${opts.name ?? "node"}:meta:${k}`,
656
- describeKind: "state",
657
- ...opts.guard != null ? { guard: opts.guard } : {}
658
- });
654
+ meta[k] = this._createMetaNode(k, v, opts);
659
655
  }
660
656
  Object.freeze(meta);
661
657
  this.meta = meta;
662
658
  const self = this;
663
659
  this._actions = {
664
660
  down(messages) {
665
- self._manualEmitUsed = true;
661
+ self._onManualEmit();
666
662
  self._downInternal(messages);
667
663
  },
668
664
  emit(value) {
669
- self._manualEmitUsed = true;
665
+ self._onManualEmit();
670
666
  self._downAutoValue(value);
671
667
  },
672
668
  up(messages) {
673
669
  self._upInternal(messages);
674
670
  }
675
671
  };
676
- this.down = this.down.bind(this);
677
- this.up = this.up.bind(this);
678
672
  this._boundDownToSinks = this._downToSinks.bind(this);
679
673
  }
674
+ /**
675
+ * Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
676
+ * {@link NodeImpl} overrides to set `_manualEmitUsed`.
677
+ */
678
+ _onManualEmit() {
679
+ }
680
+ // --- Identity getters ---
680
681
  get name() {
681
682
  return this._registryName ?? this._optsName;
682
683
  }
683
- /**
684
- * When a node is registered with {@link Graph.add} without an options `name`,
685
- * the graph assigns the registry local name for introspection (parity with graphrefly-py).
686
- */
684
+ /** @internal Assigned by `Graph.add` when registered without an options `name`. */
687
685
  _assignRegistryName(localName) {
688
686
  if (this._optsName !== void 0 || this._registryName !== void 0) return;
689
687
  this._registryName = localName;
@@ -701,7 +699,10 @@ var NodeImpl = class {
701
699
  }
702
700
  };
703
701
  }
704
- // --- Public interface (Node<T>) ---
702
+ /** @internal Used by subclasses to surface inspector events. */
703
+ _emitInspectorHook(event) {
704
+ this._inspectorHook?.(event);
705
+ }
705
706
  get status() {
706
707
  return this._status;
707
708
  }
@@ -711,15 +712,7 @@ var NodeImpl = class {
711
712
  get v() {
712
713
  return this._versioning;
713
714
  }
714
- /**
715
- * Retroactively apply versioning to a node that was created without it.
716
- * No-op if versioning is already enabled.
717
- *
718
- * Version starts at 0 regardless of prior DATA emissions — it tracks
719
- * changes from the moment versioning is enabled, not historical ones.
720
- *
721
- * @internal — used by {@link Graph.setVersioning}.
722
- */
715
+ /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
723
716
  _applyVersioning(level, opts) {
724
717
  if (this._versioning != null) return;
725
718
  this._hashFn = opts?.hash ?? this._hashFn;
@@ -739,6 +732,7 @@ var NodeImpl = class {
739
732
  if (this._guard == null) return true;
740
733
  return this._guard(normalizeActor(actor), "observe");
741
734
  }
735
+ // --- Public transport ---
742
736
  get() {
743
737
  return this._cached === NO_VALUE ? void 0 : this._cached;
744
738
  }
@@ -751,43 +745,25 @@ var NodeImpl = class {
751
745
  if (!this._guard(actor, action)) {
752
746
  throw new GuardDenied({ actor, action, nodeName: this.name });
753
747
  }
754
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
748
+ this._recordMutation(actor);
755
749
  }
756
750
  this._downInternal(messages);
757
751
  }
758
- _downInternal(messages) {
759
- if (messages.length === 0) return;
760
- let lifecycleMessages = messages;
761
- let sinkMessages = messages;
762
- if (this._terminal && !this._opts.resubscribable) {
763
- const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
764
- if (terminalPassthrough.length === 0) return;
765
- lifecycleMessages = terminalPassthrough;
766
- sinkMessages = terminalPassthrough;
767
- }
768
- this._handleLocalLifecycle(lifecycleMessages);
769
- if (this._canSkipDirty()) {
770
- let hasPhase2 = false;
771
- for (let i = 0; i < sinkMessages.length; i++) {
772
- const t = sinkMessages[i][0];
773
- if (t === DATA || t === RESOLVED) {
774
- hasPhase2 = true;
775
- break;
776
- }
777
- }
778
- if (hasPhase2) {
779
- const filtered = [];
780
- for (let i = 0; i < sinkMessages.length; i++) {
781
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
782
- }
783
- if (filtered.length > 0) {
784
- downWithBatch(this._boundDownToSinks, filtered);
785
- }
786
- return;
787
- }
788
- }
789
- downWithBatch(this._boundDownToSinks, sinkMessages);
752
+ /** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
753
+ _recordMutation(actor) {
754
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
790
755
  }
756
+ /**
757
+ * At-most-once deactivation guard. Both TEARDOWN (eager) and
758
+ * unsubscribe-body (lazy) call this. The `_active` flag ensures
759
+ * `_doDeactivate` runs exactly once per activation cycle.
760
+ */
761
+ _onDeactivate() {
762
+ if (!this._active) return;
763
+ this._active = false;
764
+ this._doDeactivate();
765
+ }
766
+ // --- Subscribe (uniform across node shapes) ---
791
767
  subscribe(sink, hints) {
792
768
  if (hints?.actor != null && this._guard != null) {
793
769
  const actor = normalizeActor(hints.actor);
@@ -795,17 +771,21 @@ var NodeImpl = class {
795
771
  throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
796
772
  }
797
773
  }
798
- if (this._terminal && this._opts.resubscribable) {
774
+ if (this._terminal && this._resubscribable) {
799
775
  this._terminal = false;
800
776
  this._cached = NO_VALUE;
801
- this._status = this._hasDeps ? "disconnected" : "settled";
802
- this._opts.onResubscribe?.();
777
+ this._status = "disconnected";
778
+ this._onResubscribe?.();
803
779
  }
804
780
  this._sinkCount += 1;
805
781
  if (hints?.singleDep) {
806
782
  this._singleDepSinkCount += 1;
807
783
  this._singleDepSinks.add(sink);
808
784
  }
785
+ if (!this._terminal) {
786
+ const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
787
+ downWithBatch(sink, startMessages);
788
+ }
809
789
  if (this._sinks == null) {
810
790
  this._sinks = sink;
811
791
  } else if (typeof this._sinks === "function") {
@@ -813,10 +793,12 @@ var NodeImpl = class {
813
793
  } else {
814
794
  this._sinks.add(sink);
815
795
  }
816
- if (this._hasDeps) {
817
- this._connectUpstream();
818
- } else if (this._fn) {
819
- this._startProducer();
796
+ if (this._sinkCount === 1 && !this._terminal) {
797
+ this._active = true;
798
+ this._onActivate();
799
+ }
800
+ if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
801
+ this._status = "pending";
820
802
  }
821
803
  let removed = false;
822
804
  return () => {
@@ -840,39 +822,49 @@ var NodeImpl = class {
840
822
  }
841
823
  }
842
824
  if (this._sinks == null) {
843
- this._disconnectUpstream();
844
- this._stopProducer();
825
+ this._onDeactivate();
845
826
  }
846
827
  };
847
828
  }
848
- up(messages, options) {
849
- if (!this._hasDeps) return;
850
- if (!options?.internal && this._guard != null) {
851
- const actor = normalizeActor(options?.actor);
852
- if (!this._guard(actor, "write")) {
853
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
854
- }
855
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
829
+ // --- Down pipeline ---
830
+ /**
831
+ * Core outgoing dispatch. Applies terminal filter + local lifecycle
832
+ * update, then hands messages to `downWithBatch` for tier-aware delivery.
833
+ */
834
+ _downInternal(messages) {
835
+ if (messages.length === 0) return;
836
+ let sinkMessages = messages;
837
+ if (this._terminal && !this._resubscribable) {
838
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
839
+ if (pass.length === 0) return;
840
+ sinkMessages = pass;
856
841
  }
857
- for (const dep of this._deps) {
858
- if (options === void 0) {
859
- dep.up?.(messages);
860
- } else {
861
- dep.up?.(messages, options);
842
+ this._handleLocalLifecycle(sinkMessages);
843
+ if (this._canSkipDirty()) {
844
+ let hasPhase2 = false;
845
+ for (let i = 0; i < sinkMessages.length; i++) {
846
+ const t = sinkMessages[i][0];
847
+ if (t === DATA || t === RESOLVED) {
848
+ hasPhase2 = true;
849
+ break;
850
+ }
851
+ }
852
+ if (hasPhase2) {
853
+ const filtered = [];
854
+ for (let i = 0; i < sinkMessages.length; i++) {
855
+ if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
856
+ }
857
+ if (filtered.length > 0) {
858
+ downWithBatch(this._boundDownToSinks, filtered);
859
+ }
860
+ return;
862
861
  }
863
862
  }
863
+ downWithBatch(this._boundDownToSinks, sinkMessages);
864
864
  }
865
- _upInternal(messages) {
866
- if (!this._hasDeps) return;
867
- for (const dep of this._deps) {
868
- dep.up?.(messages, { internal: true });
869
- }
870
- }
871
- unsubscribe() {
872
- if (!this._hasDeps) return;
873
- this._disconnectUpstream();
865
+ _canSkipDirty() {
866
+ return this._sinkCount === 1 && this._singleDepSinkCount === 1;
874
867
  }
875
- // --- Private methods (prototype, _ prefix) ---
876
868
  _downToSinks(messages) {
877
869
  if (this._sinks == null) return;
878
870
  if (typeof this._sinks === "function") {
@@ -884,6 +876,11 @@ var NodeImpl = class {
884
876
  sink(messages);
885
877
  }
886
878
  }
879
+ /**
880
+ * Update `_cached`, `_status`, `_terminal` from message batch before
881
+ * delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
882
+ * {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
883
+ */
887
884
  _handleLocalLifecycle(messages) {
888
885
  for (const m of messages) {
889
886
  const t = m[0];
@@ -897,28 +894,22 @@ var NodeImpl = class {
897
894
  }
898
895
  }
899
896
  if (t === INVALIDATE) {
900
- const cleanupFn = this._cleanup;
901
- this._cleanup = void 0;
902
- cleanupFn?.();
897
+ this._onInvalidate();
903
898
  this._cached = NO_VALUE;
904
- this._lastDepValues = void 0;
905
899
  }
906
900
  this._status = statusAfterMessage(this._status, m);
907
901
  if (t === COMPLETE || t === ERROR) {
908
902
  this._terminal = true;
909
903
  }
910
904
  if (t === TEARDOWN) {
911
- if (this._opts.resetOnTeardown) {
905
+ if (this._resetOnTeardown) {
912
906
  this._cached = NO_VALUE;
913
907
  }
914
- const teardownCleanup = this._cleanup;
915
- this._cleanup = void 0;
916
- teardownCleanup?.();
908
+ this._onTeardown();
917
909
  try {
918
910
  this._propagateToMeta(t);
919
911
  } finally {
920
- this._disconnectUpstream();
921
- this._stopProducer();
912
+ this._onDeactivate();
922
913
  }
923
914
  }
924
915
  if (t !== TEARDOWN && propagatesToMeta(t)) {
@@ -926,7 +917,20 @@ var NodeImpl = class {
926
917
  }
927
918
  }
928
919
  }
929
- /** Propagate a signal to all companion meta nodes (best-effort). */
920
+ /**
921
+ * Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
922
+ * cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
923
+ * drop `_lastDepValues` so the next wave re-runs fn.
924
+ */
925
+ _onInvalidate() {
926
+ }
927
+ /**
928
+ * Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
929
+ * {@link NodeImpl} uses this to run any pending cleanup fn.
930
+ */
931
+ _onTeardown() {
932
+ }
933
+ /** Forward a signal to all companion meta nodes (best-effort). */
930
934
  _propagateToMeta(t) {
931
935
  for (const metaNode of Object.values(this.meta)) {
932
936
  try {
@@ -935,9 +939,10 @@ var NodeImpl = class {
935
939
  }
936
940
  }
937
941
  }
938
- _canSkipDirty() {
939
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
940
- }
942
+ /**
943
+ * Frame a computed value into the right protocol messages and dispatch
944
+ * via `_downInternal`. Used by `_runFn` and `actions.emit`.
945
+ */
941
946
  _downAutoValue(value) {
942
947
  const wasDirty = this._status === "dirty";
943
948
  let unchanged;
@@ -945,7 +950,9 @@ var NodeImpl = class {
945
950
  unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
946
951
  } catch (eqErr) {
947
952
  const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
948
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
953
+ const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
954
+ cause: eqErr
955
+ });
949
956
  this._downInternal([[ERROR, wrapped]]);
950
957
  return;
951
958
  }
@@ -955,89 +962,173 @@ var NodeImpl = class {
955
962
  }
956
963
  this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
957
964
  }
958
- _runFn() {
959
- if (!this._fn) return;
960
- if (this._terminal && !this._opts.resubscribable) return;
961
- if (this._connecting) return;
962
- try {
963
- const n = this._deps.length;
964
- const depValues = new Array(n);
965
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
966
- const prev = this._lastDepValues;
967
- if (n > 0 && prev != null && prev.length === n) {
968
- let allSame = true;
969
- for (let i = 0; i < n; i++) {
970
- if (!Object.is(depValues[i], prev[i])) {
971
- allSame = false;
972
- break;
973
- }
974
- }
975
- if (allSame) {
976
- if (this._status === "dirty") {
977
- this._downInternal([[RESOLVED]]);
978
- }
979
- return;
980
- }
981
- }
982
- const prevCleanup = this._cleanup;
983
- this._cleanup = void 0;
984
- prevCleanup?.();
985
- this._manualEmitUsed = false;
986
- this._lastDepValues = depValues;
987
- this._inspectorHook?.({ kind: "run", depValues });
988
- const out = this._fn(depValues, this._actions);
989
- if (isCleanupResult(out)) {
990
- this._cleanup = out.cleanup;
991
- if (this._manualEmitUsed) return;
992
- if ("value" in out) {
993
- this._downAutoValue(out.value);
994
- }
995
- return;
965
+ };
966
+
967
+ // src/core/node.ts
968
+ var NodeImpl = class extends NodeBase {
969
+ // --- Dep configuration (set once) ---
970
+ _deps;
971
+ _fn;
972
+ _opts;
973
+ _hasDeps;
974
+ _isSingleDep;
975
+ _autoComplete;
976
+ // --- Wave tracking masks ---
977
+ _depDirtyMask;
978
+ _depSettledMask;
979
+ _depCompleteMask;
980
+ _allDepsCompleteMask;
981
+ // --- Identity-skip optimization ---
982
+ _lastDepValues;
983
+ _cleanup;
984
+ // --- Upstream bookkeeping ---
985
+ _upstreamUnsubs = [];
986
+ // --- Fn behavior flag ---
987
+ /** @internal Read by `describeNode` to infer `"operator"` label. */
988
+ _manualEmitUsed = false;
989
+ constructor(deps, fn, opts) {
990
+ super(opts);
991
+ this._deps = deps;
992
+ this._fn = fn;
993
+ this._opts = opts;
994
+ this._hasDeps = deps.length > 0;
995
+ this._isSingleDep = deps.length === 1 && fn != null;
996
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
997
+ if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
998
+ this._status = "settled";
999
+ }
1000
+ this._depDirtyMask = createBitSet(deps.length);
1001
+ this._depSettledMask = createBitSet(deps.length);
1002
+ this._depCompleteMask = createBitSet(deps.length);
1003
+ this._allDepsCompleteMask = createBitSet(deps.length);
1004
+ for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
1005
+ this.down = this.down.bind(this);
1006
+ this.up = this.up.bind(this);
1007
+ }
1008
+ // --- Meta node factory (called from base constructor) ---
1009
+ _createMetaNode(key, initialValue, opts) {
1010
+ return node({
1011
+ initial: initialValue,
1012
+ name: `${opts.name ?? "node"}:meta:${key}`,
1013
+ describeKind: "state",
1014
+ ...opts.guard != null ? { guard: opts.guard } : {}
1015
+ });
1016
+ }
1017
+ // --- Manual emit tracker (set by actions.down / actions.emit) ---
1018
+ _onManualEmit() {
1019
+ this._manualEmitUsed = true;
1020
+ }
1021
+ // --- Up / unsubscribe ---
1022
+ up(messages, options) {
1023
+ if (!this._hasDeps) return;
1024
+ if (!options?.internal && this._guard != null) {
1025
+ const actor = normalizeActor(options?.actor);
1026
+ if (!this._guard(actor, "write")) {
1027
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
996
1028
  }
997
- if (isCleanupFn(out)) {
998
- this._cleanup = out;
999
- return;
1029
+ this._recordMutation(actor);
1030
+ }
1031
+ for (const dep of this._deps) {
1032
+ if (options === void 0) {
1033
+ dep.up?.(messages);
1034
+ } else {
1035
+ dep.up?.(messages, options);
1000
1036
  }
1001
- if (this._manualEmitUsed) return;
1002
- if (out === void 0) return;
1003
- this._downAutoValue(out);
1004
- } catch (err) {
1005
- const errMsg = err instanceof Error ? err.message : String(err);
1006
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1007
- this._downInternal([[ERROR, wrapped]]);
1008
1037
  }
1009
1038
  }
1010
- _onDepDirty(index) {
1011
- const wasDirty = this._depDirtyMask.has(index);
1012
- this._depDirtyMask.set(index);
1013
- this._depSettledMask.clear(index);
1014
- if (!wasDirty) {
1015
- this._downInternal([[DIRTY]]);
1039
+ _upInternal(messages) {
1040
+ if (!this._hasDeps) return;
1041
+ for (const dep of this._deps) {
1042
+ dep.up?.(messages, { internal: true });
1016
1043
  }
1017
1044
  }
1018
- _onDepSettled(index) {
1019
- if (!this._depDirtyMask.has(index)) {
1020
- this._onDepDirty(index);
1045
+ unsubscribe() {
1046
+ if (!this._hasDeps) return;
1047
+ this._disconnectUpstream();
1048
+ }
1049
+ // --- Activation (first-subscriber / last-subscriber hooks) ---
1050
+ _onActivate() {
1051
+ if (this._hasDeps) {
1052
+ this._connectUpstream();
1053
+ return;
1021
1054
  }
1022
- this._depSettledMask.set(index);
1023
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1024
- this._depDirtyMask.reset();
1025
- this._depSettledMask.reset();
1055
+ if (this._fn) {
1026
1056
  this._runFn();
1057
+ return;
1027
1058
  }
1028
1059
  }
1029
- _maybeCompleteFromDeps() {
1030
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1031
- this._downInternal([[COMPLETE]]);
1060
+ _doDeactivate() {
1061
+ this._disconnectUpstream();
1062
+ const cleanup = this._cleanup;
1063
+ this._cleanup = void 0;
1064
+ cleanup?.();
1065
+ if (this._fn != null) {
1066
+ this._cached = NO_VALUE;
1067
+ this._lastDepValues = void 0;
1032
1068
  }
1069
+ if (this._hasDeps || this._fn != null) {
1070
+ this._status = "disconnected";
1071
+ }
1072
+ }
1073
+ // --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
1074
+ _onInvalidate() {
1075
+ const cleanup = this._cleanup;
1076
+ this._cleanup = void 0;
1077
+ cleanup?.();
1078
+ this._lastDepValues = void 0;
1033
1079
  }
1080
+ _onTeardown() {
1081
+ const cleanup = this._cleanup;
1082
+ this._cleanup = void 0;
1083
+ cleanup?.();
1084
+ }
1085
+ // --- Upstream connect / disconnect ---
1086
+ _connectUpstream() {
1087
+ if (!this._hasDeps) return;
1088
+ if (this._upstreamUnsubs.length > 0) return;
1089
+ this._depDirtyMask.setAll();
1090
+ this._depSettledMask.reset();
1091
+ this._depCompleteMask.reset();
1092
+ const depValuesBefore = this._lastDepValues;
1093
+ const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1094
+ for (let i = 0; i < this._deps.length; i += 1) {
1095
+ const dep = this._deps[i];
1096
+ this._upstreamUnsubs.push(
1097
+ dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1098
+ );
1099
+ }
1100
+ if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
1101
+ this._runFn();
1102
+ }
1103
+ }
1104
+ _disconnectUpstream() {
1105
+ if (this._upstreamUnsubs.length === 0) return;
1106
+ for (const unsub of this._upstreamUnsubs.splice(0)) {
1107
+ unsub();
1108
+ }
1109
+ this._depDirtyMask.reset();
1110
+ this._depSettledMask.reset();
1111
+ this._depCompleteMask.reset();
1112
+ }
1113
+ // --- Wave handling ---
1034
1114
  _handleDepMessages(index, messages) {
1035
1115
  for (const msg of messages) {
1036
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1116
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1037
1117
  const t = msg[0];
1038
1118
  if (this._onMessage) {
1039
1119
  try {
1040
- if (this._onMessage(msg, index, this._actions)) continue;
1120
+ const consumed = this._onMessage(msg, index, this._actions);
1121
+ if (consumed) {
1122
+ if (t === START) {
1123
+ this._depDirtyMask.clear(index);
1124
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1125
+ this._depDirtyMask.reset();
1126
+ this._depSettledMask.reset();
1127
+ this._runFn();
1128
+ }
1129
+ }
1130
+ continue;
1131
+ }
1041
1132
  } catch (err) {
1042
1133
  const errMsg = err instanceof Error ? err.message : String(err);
1043
1134
  const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
@@ -1047,6 +1138,7 @@ var NodeImpl = class {
1047
1138
  return;
1048
1139
  }
1049
1140
  }
1141
+ if (messageTier(t) < 1) continue;
1050
1142
  if (!this._fn) {
1051
1143
  if (t === COMPLETE && this._deps.length > 1) {
1052
1144
  this._depCompleteMask.set(index);
@@ -1090,53 +1182,85 @@ var NodeImpl = class {
1090
1182
  this._downInternal([msg]);
1091
1183
  }
1092
1184
  }
1093
- _connectUpstream() {
1094
- if (!this._hasDeps || this._connected) return;
1095
- this._connected = true;
1096
- this._depDirtyMask.reset();
1097
- this._depSettledMask.reset();
1098
- this._depCompleteMask.reset();
1099
- this._status = "settled";
1100
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1101
- this._connecting = true;
1102
- try {
1103
- for (let i = 0; i < this._deps.length; i += 1) {
1104
- const dep = this._deps[i];
1105
- this._upstreamUnsubs.push(
1106
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1107
- );
1108
- }
1109
- } finally {
1110
- this._connecting = false;
1185
+ _onDepDirty(index) {
1186
+ const wasDirty = this._depDirtyMask.has(index);
1187
+ this._depDirtyMask.set(index);
1188
+ this._depSettledMask.clear(index);
1189
+ if (!wasDirty) {
1190
+ this._downInternal([[DIRTY]]);
1111
1191
  }
1112
- if (this._fn) {
1192
+ }
1193
+ _onDepSettled(index) {
1194
+ if (!this._depDirtyMask.has(index)) {
1195
+ this._onDepDirty(index);
1196
+ }
1197
+ this._depSettledMask.set(index);
1198
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1199
+ this._depDirtyMask.reset();
1200
+ this._depSettledMask.reset();
1113
1201
  this._runFn();
1114
1202
  }
1115
1203
  }
1116
- _stopProducer() {
1117
- if (!this._producerStarted) return;
1118
- this._producerStarted = false;
1119
- const producerCleanup = this._cleanup;
1120
- this._cleanup = void 0;
1121
- producerCleanup?.();
1122
- }
1123
- _startProducer() {
1124
- if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
1125
- this._producerStarted = true;
1126
- this._runFn();
1204
+ _maybeCompleteFromDeps() {
1205
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1206
+ this._downInternal([[COMPLETE]]);
1207
+ }
1127
1208
  }
1128
- _disconnectUpstream() {
1129
- if (!this._connected) return;
1130
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1131
- unsub();
1209
+ // --- Fn execution ---
1210
+ _runFn() {
1211
+ if (!this._fn) return;
1212
+ if (this._terminal && !this._resubscribable) return;
1213
+ try {
1214
+ const n = this._deps.length;
1215
+ const depValues = new Array(n);
1216
+ for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1217
+ const prev = this._lastDepValues;
1218
+ if (n > 0 && prev != null && prev.length === n) {
1219
+ let allSame = true;
1220
+ for (let i = 0; i < n; i++) {
1221
+ if (!Object.is(depValues[i], prev[i])) {
1222
+ allSame = false;
1223
+ break;
1224
+ }
1225
+ }
1226
+ if (allSame) {
1227
+ if (this._status === "dirty") {
1228
+ this._downInternal([[RESOLVED]]);
1229
+ }
1230
+ return;
1231
+ }
1232
+ }
1233
+ const prevCleanup = this._cleanup;
1234
+ this._cleanup = void 0;
1235
+ prevCleanup?.();
1236
+ this._manualEmitUsed = false;
1237
+ this._lastDepValues = depValues;
1238
+ this._emitInspectorHook({ kind: "run", depValues });
1239
+ const out = this._fn(depValues, this._actions);
1240
+ if (isCleanupResult(out)) {
1241
+ this._cleanup = out.cleanup;
1242
+ if (this._manualEmitUsed) return;
1243
+ if ("value" in out) {
1244
+ this._downAutoValue(out.value);
1245
+ }
1246
+ return;
1247
+ }
1248
+ if (isCleanupFn(out)) {
1249
+ this._cleanup = out;
1250
+ return;
1251
+ }
1252
+ if (this._manualEmitUsed) return;
1253
+ if (out === void 0) return;
1254
+ this._downAutoValue(out);
1255
+ } catch (err) {
1256
+ const errMsg = err instanceof Error ? err.message : String(err);
1257
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1258
+ this._downInternal([[ERROR, wrapped]]);
1132
1259
  }
1133
- this._connected = false;
1134
- this._depDirtyMask.reset();
1135
- this._depSettledMask.reset();
1136
- this._depCompleteMask.reset();
1137
- this._status = "disconnected";
1138
1260
  }
1139
1261
  };
1262
+ var isNodeArray = (value) => Array.isArray(value);
1263
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1140
1264
  function node(depsOrFn, fnOrOpts, optsArg) {
1141
1265
  const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1142
1266
  const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
@@ -2272,7 +2396,38 @@ function firstValueFrom(source) {
2272
2396
  }
2273
2397
  if (m[0] === COMPLETE) {
2274
2398
  settled = true;
2275
- reject(new Error("completed without DATA"));
2399
+ reject(new Error("completed without DATA"));
2400
+ queueMicrotask(() => unsub());
2401
+ return;
2402
+ }
2403
+ }
2404
+ });
2405
+ });
2406
+ }
2407
+ function firstWhere(source, predicate) {
2408
+ return new Promise((resolve, reject) => {
2409
+ let settled = false;
2410
+ const unsub = source.subscribe((msgs) => {
2411
+ for (const m of msgs) {
2412
+ if (settled) return;
2413
+ if (m[0] === DATA) {
2414
+ const v = m[1];
2415
+ if (predicate(v)) {
2416
+ settled = true;
2417
+ resolve(v);
2418
+ queueMicrotask(() => unsub());
2419
+ return;
2420
+ }
2421
+ }
2422
+ if (m[0] === ERROR) {
2423
+ settled = true;
2424
+ reject(m[1]);
2425
+ queueMicrotask(() => unsub());
2426
+ return;
2427
+ }
2428
+ if (m[0] === COMPLETE) {
2429
+ settled = true;
2430
+ reject(new Error("completed without matching value"));
2276
2431
  queueMicrotask(() => unsub());
2277
2432
  return;
2278
2433
  }
@@ -2522,6 +2677,10 @@ function toSSE(source, opts) {
2522
2677
  unsub = source.subscribe((msgs) => {
2523
2678
  for (const msg of msgs) {
2524
2679
  const t = msg[0];
2680
+ if (isLocalOnly(t)) {
2681
+ if (t === DIRTY && includeDirty) {
2682
+ } else continue;
2683
+ }
2525
2684
  if (t === DATA) {
2526
2685
  write(dataEvent, serializeSseData(msg[1], serialize));
2527
2686
  continue;
@@ -2537,7 +2696,6 @@ function toSSE(source, opts) {
2537
2696
  return;
2538
2697
  }
2539
2698
  if (!includeResolved && t === RESOLVED) continue;
2540
- if (!includeDirty && t === DIRTY) continue;
2541
2699
  write(
2542
2700
  eventNameResolver(t),
2543
2701
  msg.length > 1 ? serializeSseData(msg[1], serialize) : void 0
@@ -5065,289 +5223,106 @@ function restoreGraphCheckpointIndexedDb(graph, spec) {
5065
5223
  }
5066
5224
  if (requestValue === void 0 || requestValue === null) {
5067
5225
  finishWith([[DATA, false], [COMPLETE]]);
5068
- return;
5069
- }
5070
- if (typeof requestValue !== "object" || Array.isArray(requestValue)) {
5071
- finishWith([[DATA, false], [COMPLETE]]);
5072
- return;
5073
- }
5074
- graph.restore(requestValue);
5075
- finishWith([[DATA, true], [COMPLETE]]);
5076
- };
5077
- const startRead = () => {
5078
- if (db === void 0 || reqUnsub !== void 0 || txUnsub !== void 0) return;
5079
- const tx = db.transaction(spec.storeName, "readonly");
5080
- const store = tx.objectStore(spec.storeName);
5081
- reqUnsub = fromIDBRequest(store.get(key)).subscribe((msgs) => {
5082
- for (const m of msgs) {
5083
- if (m[0] === DATA) requestValue = m[1];
5084
- if (m[0] === ERROR) requestError = m[1];
5085
- if (m[0] === COMPLETE || m[0] === ERROR) requestDone = true;
5086
- }
5087
- maybeEmitResult();
5088
- });
5089
- txUnsub = fromIDBTransaction(tx).subscribe((msgs) => {
5090
- for (const m of msgs) {
5091
- if (m[0] === ERROR) {
5092
- finishWith([[ERROR, m[1]]]);
5093
- return;
5094
- }
5095
- if (m[0] === COMPLETE) txDone = true;
5096
- }
5097
- maybeEmitResult();
5098
- });
5099
- };
5100
- const openUnsub = openIdbNode(spec.dbName, spec.storeName, spec.version ?? 1).subscribe(
5101
- (msgs) => {
5102
- for (const m of msgs) {
5103
- if (m[0] === DATA) {
5104
- db = m[1];
5105
- startRead();
5106
- continue;
5107
- }
5108
- if (m[0] === ERROR) {
5109
- finishWith([[ERROR, m[1]]]);
5110
- return;
5111
- }
5112
- }
5113
- }
5114
- );
5115
- return () => {
5116
- reqUnsub?.();
5117
- reqUnsub = void 0;
5118
- txUnsub?.();
5119
- txUnsub = void 0;
5120
- openUnsub();
5121
- close();
5122
- };
5123
- });
5124
- }
5125
-
5126
- // src/core/dynamic-node.ts
5127
- function dynamicNode(fn, opts) {
5128
- return new DynamicNodeImpl(fn, opts ?? {});
5129
- }
5130
- var DynamicNodeImpl = class {
5131
- _optsName;
5132
- _registryName;
5133
- _describeKind;
5134
- meta;
5135
- _fn;
5136
- _equals;
5137
- _resubscribable;
5138
- _resetOnTeardown;
5139
- _autoComplete;
5140
- _onMessage;
5141
- _onResubscribe;
5142
- /** @internal — read by {@link describeNode} for `accessHintForGuard`. */
5143
- _guard;
5144
- _lastMutation;
5145
- _inspectorHook;
5146
- // Sink tracking
5147
- _sinkCount = 0;
5148
- _singleDepSinkCount = 0;
5149
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
5150
- // Actions object (for onMessage handler)
5151
- _actions;
5152
- _boundDownToSinks;
5153
- // Mutable state
5154
- _cached = NO_VALUE;
5155
- _status = "disconnected";
5156
- _terminal = false;
5157
- _connected = false;
5158
- _rewiring = false;
5159
- // re-entrancy guard
5160
- // Dynamic deps tracking
5161
- _deps = [];
5162
- _depUnsubs = [];
5163
- _depIndexMap = /* @__PURE__ */ new Map();
5164
- // node → index in _deps
5165
- _dirtyBits = /* @__PURE__ */ new Set();
5166
- _settledBits = /* @__PURE__ */ new Set();
5167
- _completeBits = /* @__PURE__ */ new Set();
5168
- // Sinks
5169
- _sinks = null;
5170
- constructor(fn, opts) {
5171
- this._fn = fn;
5172
- this._optsName = opts.name;
5173
- this._describeKind = opts.describeKind;
5174
- this._equals = opts.equals ?? Object.is;
5175
- this._resubscribable = opts.resubscribable ?? false;
5176
- this._resetOnTeardown = opts.resetOnTeardown ?? false;
5177
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
5178
- this._onMessage = opts.onMessage;
5179
- this._onResubscribe = opts.onResubscribe;
5180
- this._guard = opts.guard;
5181
- this._inspectorHook = void 0;
5182
- const meta = {};
5183
- for (const [k, v] of Object.entries(opts.meta ?? {})) {
5184
- meta[k] = node({
5185
- initial: v,
5186
- name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
5187
- describeKind: "state",
5188
- ...opts.guard != null ? { guard: opts.guard } : {}
5189
- });
5190
- }
5191
- Object.freeze(meta);
5192
- this.meta = meta;
5193
- const self = this;
5194
- this._actions = {
5195
- down(messages) {
5196
- self._downInternal(messages);
5197
- },
5198
- emit(value) {
5199
- self._downAutoValue(value);
5200
- },
5201
- up(messages) {
5202
- for (const dep of self._deps) {
5203
- dep.up?.(messages, { internal: true });
5204
- }
5205
- }
5206
- };
5207
- this._boundDownToSinks = this._downToSinks.bind(this);
5208
- }
5209
- get name() {
5210
- return this._registryName ?? this._optsName;
5211
- }
5212
- /** @internal */
5213
- _assignRegistryName(localName) {
5214
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
5215
- this._registryName = localName;
5216
- }
5217
- /**
5218
- * @internal Attach/remove inspector hook for graph-level observability.
5219
- * Returns a disposer that restores the previous hook.
5220
- */
5221
- _setInspectorHook(hook) {
5222
- const prev = this._inspectorHook;
5223
- this._inspectorHook = hook;
5224
- return () => {
5225
- if (this._inspectorHook === hook) {
5226
- this._inspectorHook = prev;
5227
- }
5228
- };
5229
- }
5230
- get status() {
5231
- return this._status;
5232
- }
5233
- get lastMutation() {
5234
- return this._lastMutation;
5235
- }
5236
- /** Versioning not yet supported on DynamicNodeImpl. */
5237
- get v() {
5238
- return void 0;
5239
- }
5240
- hasGuard() {
5241
- return this._guard != null;
5242
- }
5243
- allowsObserve(actor) {
5244
- if (this._guard == null) return true;
5245
- return this._guard(normalizeActor(actor), "observe");
5246
- }
5247
- get() {
5248
- return this._cached === NO_VALUE ? void 0 : this._cached;
5249
- }
5250
- down(messages, options) {
5251
- if (messages.length === 0) return;
5252
- if (!options?.internal && this._guard != null) {
5253
- const actor = normalizeActor(options?.actor);
5254
- const delivery = options?.delivery ?? "write";
5255
- const action = delivery === "signal" ? "signal" : "write";
5256
- if (!this._guard(actor, action)) {
5257
- throw new GuardDenied({ actor, action, nodeName: this.name });
5258
- }
5259
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
5260
- }
5261
- this._downInternal(messages);
5262
- }
5263
- _downInternal(messages) {
5264
- if (messages.length === 0) return;
5265
- let sinkMessages = messages;
5266
- if (this._terminal && !this._resubscribable) {
5267
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
5268
- if (pass.length === 0) return;
5269
- sinkMessages = pass;
5270
- }
5271
- this._handleLocalLifecycle(sinkMessages);
5272
- if (this._canSkipDirty()) {
5273
- let hasPhase2 = false;
5274
- for (let i = 0; i < sinkMessages.length; i++) {
5275
- const t = sinkMessages[i][0];
5276
- if (t === DATA || t === RESOLVED) {
5277
- hasPhase2 = true;
5278
- break;
5279
- }
5280
- }
5281
- if (hasPhase2) {
5282
- const filtered = [];
5283
- for (let i = 0; i < sinkMessages.length; i++) {
5284
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
5285
- }
5286
- if (filtered.length > 0) {
5287
- downWithBatch(this._boundDownToSinks, filtered);
5288
- }
5289
- return;
5290
- }
5291
- }
5292
- downWithBatch(this._boundDownToSinks, sinkMessages);
5293
- }
5294
- _canSkipDirty() {
5295
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
5296
- }
5297
- subscribe(sink, hints) {
5298
- if (hints?.actor != null && this._guard != null) {
5299
- const actor = normalizeActor(hints.actor);
5300
- if (!this._guard(actor, "observe")) {
5301
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
5302
- }
5303
- }
5304
- if (this._terminal && this._resubscribable) {
5305
- this._terminal = false;
5306
- this._cached = NO_VALUE;
5307
- this._status = "disconnected";
5308
- this._onResubscribe?.();
5309
- }
5310
- this._sinkCount += 1;
5311
- if (hints?.singleDep) {
5312
- this._singleDepSinkCount += 1;
5313
- this._singleDepSinks.add(sink);
5314
- }
5315
- if (this._sinks == null) {
5316
- this._sinks = sink;
5317
- } else if (typeof this._sinks === "function") {
5318
- this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
5319
- } else {
5320
- this._sinks.add(sink);
5321
- }
5322
- if (!this._connected) {
5323
- this._connect();
5324
- }
5325
- let removed = false;
5326
- return () => {
5327
- if (removed) return;
5328
- removed = true;
5329
- this._sinkCount -= 1;
5330
- if (this._singleDepSinks.has(sink)) {
5331
- this._singleDepSinkCount -= 1;
5332
- this._singleDepSinks.delete(sink);
5333
- }
5334
- if (this._sinks == null) return;
5335
- if (typeof this._sinks === "function") {
5336
- if (this._sinks === sink) this._sinks = null;
5337
- } else {
5338
- this._sinks.delete(sink);
5339
- if (this._sinks.size === 1) {
5340
- const [only] = this._sinks;
5341
- this._sinks = only;
5342
- } else if (this._sinks.size === 0) {
5343
- this._sinks = null;
5344
- }
5226
+ return;
5345
5227
  }
5346
- if (this._sinks == null) {
5347
- this._disconnect();
5228
+ if (typeof requestValue !== "object" || Array.isArray(requestValue)) {
5229
+ finishWith([[DATA, false], [COMPLETE]]);
5230
+ return;
5231
+ }
5232
+ graph.restore(requestValue);
5233
+ finishWith([[DATA, true], [COMPLETE]]);
5234
+ };
5235
+ const startRead = () => {
5236
+ if (db === void 0 || reqUnsub !== void 0 || txUnsub !== void 0) return;
5237
+ const tx = db.transaction(spec.storeName, "readonly");
5238
+ const store = tx.objectStore(spec.storeName);
5239
+ reqUnsub = fromIDBRequest(store.get(key)).subscribe((msgs) => {
5240
+ for (const m of msgs) {
5241
+ if (m[0] === DATA) requestValue = m[1];
5242
+ if (m[0] === ERROR) requestError = m[1];
5243
+ if (m[0] === COMPLETE || m[0] === ERROR) requestDone = true;
5244
+ }
5245
+ maybeEmitResult();
5246
+ });
5247
+ txUnsub = fromIDBTransaction(tx).subscribe((msgs) => {
5248
+ for (const m of msgs) {
5249
+ if (m[0] === ERROR) {
5250
+ finishWith([[ERROR, m[1]]]);
5251
+ return;
5252
+ }
5253
+ if (m[0] === COMPLETE) txDone = true;
5254
+ }
5255
+ maybeEmitResult();
5256
+ });
5257
+ };
5258
+ const openUnsub = openIdbNode(spec.dbName, spec.storeName, spec.version ?? 1).subscribe(
5259
+ (msgs) => {
5260
+ for (const m of msgs) {
5261
+ if (m[0] === DATA) {
5262
+ db = m[1];
5263
+ startRead();
5264
+ continue;
5265
+ }
5266
+ if (m[0] === ERROR) {
5267
+ finishWith([[ERROR, m[1]]]);
5268
+ return;
5269
+ }
5270
+ }
5348
5271
  }
5272
+ );
5273
+ return () => {
5274
+ reqUnsub?.();
5275
+ reqUnsub = void 0;
5276
+ txUnsub?.();
5277
+ txUnsub = void 0;
5278
+ openUnsub();
5279
+ close();
5349
5280
  };
5281
+ });
5282
+ }
5283
+
5284
+ // src/core/dynamic-node.ts
5285
+ var MAX_RERUN = 16;
5286
+ function dynamicNode(fn, opts) {
5287
+ return new DynamicNodeImpl(fn, opts ?? {});
5288
+ }
5289
+ var DynamicNodeImpl = class extends NodeBase {
5290
+ _fn;
5291
+ _autoComplete;
5292
+ // Dynamic deps tracking
5293
+ /** @internal Read by `describeNode`. */
5294
+ _deps = [];
5295
+ _depUnsubs = [];
5296
+ _depIndexMap = /* @__PURE__ */ new Map();
5297
+ _depDirtyBits = /* @__PURE__ */ new Set();
5298
+ _depSettledBits = /* @__PURE__ */ new Set();
5299
+ _depCompleteBits = /* @__PURE__ */ new Set();
5300
+ // Execution state
5301
+ _running = false;
5302
+ _rewiring = false;
5303
+ _bufferedDepMessages = [];
5304
+ _trackedValues = /* @__PURE__ */ new Map();
5305
+ _rerunCount = 0;
5306
+ constructor(fn, opts) {
5307
+ super(opts);
5308
+ this._fn = fn;
5309
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
5310
+ this.down = this.down.bind(this);
5311
+ this.up = this.up.bind(this);
5350
5312
  }
5313
+ _createMetaNode(key, initialValue, opts) {
5314
+ return node({
5315
+ initial: initialValue,
5316
+ name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
5317
+ describeKind: "state",
5318
+ ...opts.guard != null ? { guard: opts.guard } : {}
5319
+ });
5320
+ }
5321
+ /** Versioning not supported on DynamicNodeImpl (override base). */
5322
+ get v() {
5323
+ return void 0;
5324
+ }
5325
+ // --- Up / unsubscribe ---
5351
5326
  up(messages, options) {
5352
5327
  if (this._deps.length === 0) return;
5353
5328
  if (!options?.internal && this._guard != null) {
@@ -5355,221 +5330,227 @@ var DynamicNodeImpl = class {
5355
5330
  if (!this._guard(actor, "write")) {
5356
5331
  throw new GuardDenied({ actor, action: "write", nodeName: this.name });
5357
5332
  }
5358
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
5333
+ this._recordMutation(actor);
5359
5334
  }
5360
5335
  for (const dep of this._deps) {
5361
5336
  dep.up?.(messages, options);
5362
5337
  }
5363
5338
  }
5364
- unsubscribe() {
5365
- this._disconnect();
5366
- }
5367
- // --- Private methods ---
5368
- _downToSinks(messages) {
5369
- if (this._sinks == null) return;
5370
- if (typeof this._sinks === "function") {
5371
- this._sinks(messages);
5372
- return;
5373
- }
5374
- const snapshot = [...this._sinks];
5375
- for (const sink of snapshot) {
5376
- sink(messages);
5377
- }
5378
- }
5379
- _handleLocalLifecycle(messages) {
5380
- for (const m of messages) {
5381
- const t = m[0];
5382
- if (t === DATA) this._cached = m[1];
5383
- if (t === INVALIDATE) {
5384
- this._cached = NO_VALUE;
5385
- this._status = "dirty";
5386
- }
5387
- if (t === DATA) {
5388
- this._status = "settled";
5389
- } else if (t === RESOLVED) {
5390
- this._status = "resolved";
5391
- } else if (t === DIRTY) {
5392
- this._status = "dirty";
5393
- } else if (t === COMPLETE) {
5394
- this._status = "completed";
5395
- this._terminal = true;
5396
- } else if (t === ERROR) {
5397
- this._status = "errored";
5398
- this._terminal = true;
5399
- }
5400
- if (t === TEARDOWN) {
5401
- if (this._resetOnTeardown) this._cached = NO_VALUE;
5402
- try {
5403
- this._propagateToMeta(t);
5404
- } finally {
5405
- this._disconnect();
5406
- }
5407
- }
5408
- if (t !== TEARDOWN && propagatesToMeta(t)) {
5409
- this._propagateToMeta(t);
5410
- }
5411
- }
5412
- }
5413
- /** Propagate a signal to all companion meta nodes (best-effort). */
5414
- _propagateToMeta(t) {
5415
- for (const metaNode of Object.values(this.meta)) {
5416
- try {
5417
- metaNode.down([[t]], { internal: true });
5418
- } catch {
5419
- }
5339
+ _upInternal(messages) {
5340
+ for (const dep of this._deps) {
5341
+ dep.up?.(messages, { internal: true });
5420
5342
  }
5421
5343
  }
5422
- _downAutoValue(value) {
5423
- const wasDirty = this._status === "dirty";
5424
- let unchanged;
5425
- try {
5426
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
5427
- } catch (eqErr) {
5428
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
5429
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
5430
- this._downInternal([[ERROR, wrapped]]);
5431
- return;
5432
- }
5433
- if (unchanged) {
5434
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
5435
- return;
5436
- }
5437
- this._cached = value;
5438
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
5344
+ unsubscribe() {
5345
+ this._disconnect();
5439
5346
  }
5440
- _connect() {
5441
- if (this._connected) return;
5442
- this._connected = true;
5443
- this._status = "settled";
5444
- this._dirtyBits.clear();
5445
- this._settledBits.clear();
5446
- this._completeBits.clear();
5347
+ // --- Activation hooks ---
5348
+ _onActivate() {
5447
5349
  this._runFn();
5448
5350
  }
5351
+ _doDeactivate() {
5352
+ this._disconnect();
5353
+ }
5449
5354
  _disconnect() {
5450
- if (!this._connected) return;
5451
5355
  for (const unsub of this._depUnsubs) unsub();
5452
5356
  this._depUnsubs = [];
5453
5357
  this._deps = [];
5454
5358
  this._depIndexMap.clear();
5455
- this._dirtyBits.clear();
5456
- this._settledBits.clear();
5457
- this._completeBits.clear();
5458
- this._connected = false;
5359
+ this._depDirtyBits.clear();
5360
+ this._depSettledBits.clear();
5361
+ this._depCompleteBits.clear();
5362
+ this._cached = NO_VALUE;
5459
5363
  this._status = "disconnected";
5460
5364
  }
5365
+ // --- Fn execution with rewire buffer ---
5461
5366
  _runFn() {
5462
5367
  if (this._terminal && !this._resubscribable) return;
5463
- if (this._rewiring) return;
5464
- const trackedDeps = [];
5465
- const trackedSet = /* @__PURE__ */ new Set();
5466
- const get = (dep) => {
5467
- if (!trackedSet.has(dep)) {
5468
- trackedSet.add(dep);
5469
- trackedDeps.push(dep);
5470
- }
5471
- return dep.get();
5472
- };
5368
+ if (this._running) return;
5369
+ this._running = true;
5370
+ this._rerunCount = 0;
5371
+ let result;
5473
5372
  try {
5474
- const depValues = [];
5475
- for (const dep of this._deps) {
5476
- depValues.push(dep.get());
5477
- }
5478
- this._inspectorHook?.({ kind: "run", depValues });
5479
- const result = this._fn(get);
5480
- this._rewire(trackedDeps);
5481
- if (result === void 0) return;
5482
- this._downAutoValue(result);
5483
- } catch (err) {
5484
- this._downInternal([[ERROR, err]]);
5373
+ for (; ; ) {
5374
+ const trackedDeps = [];
5375
+ const trackedValuesMap = /* @__PURE__ */ new Map();
5376
+ const trackedSet = /* @__PURE__ */ new Set();
5377
+ const get = (dep) => {
5378
+ if (!trackedSet.has(dep)) {
5379
+ trackedSet.add(dep);
5380
+ trackedDeps.push(dep);
5381
+ trackedValuesMap.set(dep, dep.get());
5382
+ }
5383
+ return dep.get();
5384
+ };
5385
+ this._trackedValues = trackedValuesMap;
5386
+ const depValues = [];
5387
+ for (const dep of this._deps) depValues.push(dep.get());
5388
+ this._emitInspectorHook({ kind: "run", depValues });
5389
+ try {
5390
+ result = this._fn(get);
5391
+ } catch (err) {
5392
+ const errMsg = err instanceof Error ? err.message : String(err);
5393
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
5394
+ cause: err
5395
+ });
5396
+ this._downInternal([[ERROR, wrapped]]);
5397
+ return;
5398
+ }
5399
+ this._rewiring = true;
5400
+ this._bufferedDepMessages = [];
5401
+ try {
5402
+ this._rewire(trackedDeps);
5403
+ } finally {
5404
+ this._rewiring = false;
5405
+ }
5406
+ let needsRerun = false;
5407
+ for (const entry of this._bufferedDepMessages) {
5408
+ for (const msg of entry.msgs) {
5409
+ if (msg[0] === DATA) {
5410
+ const dep = this._deps[entry.index];
5411
+ const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
5412
+ const actualValue = msg[1];
5413
+ if (!this._equals(trackedValue, actualValue)) {
5414
+ needsRerun = true;
5415
+ break;
5416
+ }
5417
+ }
5418
+ }
5419
+ if (needsRerun) break;
5420
+ }
5421
+ if (needsRerun) {
5422
+ this._rerunCount += 1;
5423
+ if (this._rerunCount > MAX_RERUN) {
5424
+ this._bufferedDepMessages = [];
5425
+ this._downInternal([
5426
+ [
5427
+ ERROR,
5428
+ new Error(
5429
+ `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
5430
+ )
5431
+ ]
5432
+ ]);
5433
+ return;
5434
+ }
5435
+ this._bufferedDepMessages = [];
5436
+ continue;
5437
+ }
5438
+ const drain = this._bufferedDepMessages;
5439
+ this._bufferedDepMessages = [];
5440
+ for (const entry of drain) {
5441
+ for (const msg of entry.msgs) {
5442
+ this._updateMasksForMessage(entry.index, msg);
5443
+ }
5444
+ }
5445
+ this._depDirtyBits.clear();
5446
+ this._depSettledBits.clear();
5447
+ break;
5448
+ }
5449
+ } finally {
5450
+ this._running = false;
5485
5451
  }
5452
+ this._downAutoValue(result);
5486
5453
  }
5487
5454
  _rewire(newDeps) {
5488
- this._rewiring = true;
5489
- try {
5490
- const oldMap = this._depIndexMap;
5491
- const newMap = /* @__PURE__ */ new Map();
5492
- const newUnsubs = [];
5493
- for (let i = 0; i < newDeps.length; i++) {
5494
- const dep = newDeps[i];
5495
- newMap.set(dep, i);
5496
- const oldIdx = oldMap.get(dep);
5497
- if (oldIdx !== void 0) {
5498
- newUnsubs.push(this._depUnsubs[oldIdx]);
5499
- this._depUnsubs[oldIdx] = () => {
5500
- };
5501
- } else {
5502
- const idx = i;
5503
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
5504
- newUnsubs.push(unsub);
5505
- }
5455
+ const oldMap = this._depIndexMap;
5456
+ const newMap = /* @__PURE__ */ new Map();
5457
+ const newUnsubs = [];
5458
+ for (let i = 0; i < newDeps.length; i++) {
5459
+ const dep = newDeps[i];
5460
+ newMap.set(dep, i);
5461
+ const oldIdx = oldMap.get(dep);
5462
+ if (oldIdx !== void 0) {
5463
+ newUnsubs.push(this._depUnsubs[oldIdx]);
5464
+ this._depUnsubs[oldIdx] = () => {
5465
+ };
5466
+ } else {
5467
+ const idx = i;
5468
+ const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
5469
+ newUnsubs.push(unsub);
5506
5470
  }
5507
- for (const [dep, oldIdx] of oldMap) {
5508
- if (!newMap.has(dep)) {
5509
- this._depUnsubs[oldIdx]();
5510
- }
5471
+ }
5472
+ for (const [dep, oldIdx] of oldMap) {
5473
+ if (!newMap.has(dep)) {
5474
+ this._depUnsubs[oldIdx]();
5511
5475
  }
5512
- this._deps = newDeps;
5513
- this._depUnsubs = newUnsubs;
5514
- this._depIndexMap = newMap;
5515
- this._dirtyBits.clear();
5516
- this._settledBits.clear();
5517
- const newCompleteBits = /* @__PURE__ */ new Set();
5518
- for (const oldIdx of this._completeBits) {
5519
- const dep = [...oldMap.entries()].find(([, idx]) => idx === oldIdx)?.[0];
5520
- if (dep && newMap.has(dep)) {
5476
+ }
5477
+ this._deps = newDeps;
5478
+ this._depUnsubs = newUnsubs;
5479
+ this._depIndexMap = newMap;
5480
+ this._depDirtyBits.clear();
5481
+ this._depSettledBits.clear();
5482
+ const newCompleteBits = /* @__PURE__ */ new Set();
5483
+ for (const oldIdx of this._depCompleteBits) {
5484
+ for (const [dep, idx] of oldMap) {
5485
+ if (idx === oldIdx && newMap.has(dep)) {
5521
5486
  newCompleteBits.add(newMap.get(dep));
5487
+ break;
5522
5488
  }
5523
5489
  }
5524
- this._completeBits = newCompleteBits;
5525
- } finally {
5526
- this._rewiring = false;
5527
5490
  }
5491
+ this._depCompleteBits = newCompleteBits;
5528
5492
  }
5493
+ // --- Dep message handling ---
5529
5494
  _handleDepMessages(index, messages) {
5530
- if (this._rewiring) return;
5495
+ if (this._rewiring) {
5496
+ this._bufferedDepMessages.push({ index, msgs: messages });
5497
+ return;
5498
+ }
5531
5499
  for (const msg of messages) {
5532
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
5500
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
5533
5501
  const t = msg[0];
5534
5502
  if (this._onMessage) {
5535
5503
  try {
5536
5504
  if (this._onMessage(msg, index, this._actions)) continue;
5537
5505
  } catch (err) {
5538
- this._downInternal([[ERROR, err]]);
5506
+ const errMsg = err instanceof Error ? err.message : String(err);
5507
+ const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
5508
+ cause: err
5509
+ });
5510
+ this._downInternal([[ERROR, wrapped]]);
5539
5511
  return;
5540
5512
  }
5541
5513
  }
5514
+ if (messageTier(t) < 1) continue;
5542
5515
  if (t === DIRTY) {
5543
- this._dirtyBits.add(index);
5544
- this._settledBits.delete(index);
5545
- if (this._dirtyBits.size === 1) {
5546
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
5516
+ const wasEmpty = this._depDirtyBits.size === 0;
5517
+ this._depDirtyBits.add(index);
5518
+ this._depSettledBits.delete(index);
5519
+ if (wasEmpty) {
5520
+ this._downInternal([[DIRTY]]);
5547
5521
  }
5548
5522
  continue;
5549
5523
  }
5550
5524
  if (t === DATA || t === RESOLVED) {
5551
- if (!this._dirtyBits.has(index)) {
5552
- this._dirtyBits.add(index);
5553
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
5525
+ if (!this._depDirtyBits.has(index)) {
5526
+ const wasEmpty = this._depDirtyBits.size === 0;
5527
+ this._depDirtyBits.add(index);
5528
+ if (wasEmpty) {
5529
+ this._downInternal([[DIRTY]]);
5530
+ }
5554
5531
  }
5555
- this._settledBits.add(index);
5532
+ this._depSettledBits.add(index);
5556
5533
  if (this._allDirtySettled()) {
5557
- this._dirtyBits.clear();
5558
- this._settledBits.clear();
5559
- this._runFn();
5534
+ this._depDirtyBits.clear();
5535
+ this._depSettledBits.clear();
5536
+ if (!this._running) {
5537
+ if (this._depValuesDifferFromTracked()) {
5538
+ this._runFn();
5539
+ }
5540
+ }
5560
5541
  }
5561
5542
  continue;
5562
5543
  }
5563
5544
  if (t === COMPLETE) {
5564
- this._completeBits.add(index);
5565
- this._dirtyBits.delete(index);
5566
- this._settledBits.delete(index);
5545
+ this._depCompleteBits.add(index);
5546
+ this._depDirtyBits.delete(index);
5547
+ this._depSettledBits.delete(index);
5567
5548
  if (this._allDirtySettled()) {
5568
- this._dirtyBits.clear();
5569
- this._settledBits.clear();
5570
- this._runFn();
5549
+ this._depDirtyBits.clear();
5550
+ this._depSettledBits.clear();
5551
+ if (!this._running) this._runFn();
5571
5552
  }
5572
- if (this._autoComplete && this._completeBits.size >= this._deps.length && this._deps.length > 0) {
5553
+ if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
5573
5554
  this._downInternal([[COMPLETE]]);
5574
5555
  }
5575
5556
  continue;
@@ -5585,13 +5566,46 @@ var DynamicNodeImpl = class {
5585
5566
  this._downInternal([msg]);
5586
5567
  }
5587
5568
  }
5569
+ /**
5570
+ * Update dep masks for a message without triggering `_runFn` — used
5571
+ * during post-rewire drain so the wave state is consistent with the
5572
+ * buffered activation cascade without recursing.
5573
+ */
5574
+ _updateMasksForMessage(index, msg) {
5575
+ const t = msg[0];
5576
+ if (t === DIRTY) {
5577
+ this._depDirtyBits.add(index);
5578
+ this._depSettledBits.delete(index);
5579
+ } else if (t === DATA || t === RESOLVED) {
5580
+ this._depDirtyBits.add(index);
5581
+ this._depSettledBits.add(index);
5582
+ } else if (t === COMPLETE) {
5583
+ this._depCompleteBits.add(index);
5584
+ this._depDirtyBits.delete(index);
5585
+ this._depSettledBits.delete(index);
5586
+ }
5587
+ }
5588
5588
  _allDirtySettled() {
5589
- if (this._dirtyBits.size === 0) return false;
5590
- for (const idx of this._dirtyBits) {
5591
- if (!this._settledBits.has(idx)) return false;
5589
+ if (this._depDirtyBits.size === 0) return false;
5590
+ for (const idx of this._depDirtyBits) {
5591
+ if (!this._depSettledBits.has(idx)) return false;
5592
5592
  }
5593
5593
  return true;
5594
5594
  }
5595
+ /**
5596
+ * True if any current dep value differs from what the last `_runFn`
5597
+ * saw via `get()`. Used to suppress redundant re-runs when deferred
5598
+ * handshake messages arrive after `_rewire` for a dep whose value
5599
+ * already matches `_trackedValues`.
5600
+ */
5601
+ _depValuesDifferFromTracked() {
5602
+ for (const dep of this._deps) {
5603
+ const current = dep.get();
5604
+ const tracked = this._trackedValues.get(dep);
5605
+ if (!this._equals(current, tracked)) return true;
5606
+ }
5607
+ return false;
5608
+ }
5595
5609
  };
5596
5610
 
5597
5611
  // src/extra/operators.ts
@@ -5654,9 +5668,12 @@ function reduce(source, reducer, seed, opts) {
5654
5668
  }
5655
5669
  function take(source, count, opts) {
5656
5670
  if (count <= 0) {
5671
+ let completed = false;
5657
5672
  return node(
5658
5673
  [source],
5659
5674
  (_d, a) => {
5675
+ if (completed) return void 0;
5676
+ completed = true;
5660
5677
  a.down([[COMPLETE]]);
5661
5678
  return void 0;
5662
5679
  },
@@ -5664,8 +5681,15 @@ function take(source, count, opts) {
5664
5681
  ...operatorOpts3(opts),
5665
5682
  completeWhenDepsComplete: false,
5666
5683
  onMessage(msg, _i, a) {
5667
- if (msg[0] === COMPLETE) {
5684
+ if (msg[0] === START && !completed) {
5685
+ completed = true;
5686
+ a.down([[COMPLETE]]);
5687
+ return true;
5688
+ }
5689
+ if (msg[0] === COMPLETE && !completed) {
5690
+ completed = true;
5668
5691
  a.down([[COMPLETE]]);
5692
+ return true;
5669
5693
  }
5670
5694
  return true;
5671
5695
  }
@@ -5847,21 +5871,6 @@ function find(source, predicate, opts) {
5847
5871
  function elementAt(source, index, opts) {
5848
5872
  return take(skip(source, index, opts), 1, opts);
5849
5873
  }
5850
- function startWith(source, initial, opts) {
5851
- let prepended = false;
5852
- return node(
5853
- [source],
5854
- ([v], a) => {
5855
- if (!prepended) {
5856
- prepended = true;
5857
- a.emit(initial);
5858
- }
5859
- a.emit(v);
5860
- return void 0;
5861
- },
5862
- operatorOpts3(opts)
5863
- );
5864
- }
5865
5874
  function tap(source, fnOrObserver, opts) {
5866
5875
  if (typeof fnOrObserver === "function") {
5867
5876
  return derived(
@@ -6197,6 +6206,7 @@ function forwardInner(inner, a, onInnerComplete) {
6197
6206
  let sawError = false;
6198
6207
  const out = [];
6199
6208
  for (const m of msgs) {
6209
+ if (messageTier(m[0]) < 1) continue;
6200
6210
  if (m[0] === DATA) emitted = true;
6201
6211
  if (m[0] === COMPLETE) sawComplete = true;
6202
6212
  else {
@@ -6681,7 +6691,7 @@ function sample(source, notifier, opts) {
6681
6691
  if (terminated) return true;
6682
6692
  const t = msg[0];
6683
6693
  const tier = messageTier(t);
6684
- if (tier >= 3) {
6694
+ if (tier >= 4) {
6685
6695
  if (t === ERROR) {
6686
6696
  terminated = true;
6687
6697
  a.down([msg]);
@@ -6697,6 +6707,7 @@ function sample(source, notifier, opts) {
6697
6707
  a.down([msg]);
6698
6708
  return true;
6699
6709
  }
6710
+ terminated = true;
6700
6711
  a.down([msg]);
6701
6712
  return true;
6702
6713
  }
@@ -7445,48 +7456,28 @@ function distill(source, extractFn, opts) {
7445
7456
 
7446
7457
  // src/extra/observable.ts
7447
7458
  var import_rxjs = require("rxjs");
7448
- function toObservable(node2) {
7449
- return new import_rxjs.Observable((subscriber) => {
7450
- const unsub = node2.subscribe((msgs) => {
7451
- for (const m of msgs) {
7459
+ function toObservable(node2, options) {
7460
+ if (options?.raw) {
7461
+ return new import_rxjs.Observable((subscriber) => {
7462
+ const unsub = node2.subscribe((msgs) => {
7452
7463
  if (subscriber.closed) return;
7453
- if (m[0] === DATA) {
7454
- subscriber.next(m[1]);
7455
- } else if (m[0] === ERROR) {
7456
- subscriber.error(m[1]);
7457
- return;
7458
- } else if (m[0] === COMPLETE) {
7459
- subscriber.complete();
7460
- return;
7464
+ subscriber.next(msgs);
7465
+ for (const m of msgs) {
7466
+ if (m[0] === ERROR) {
7467
+ subscriber.error(m[1]);
7468
+ return;
7469
+ }
7470
+ if (m[0] === COMPLETE) {
7471
+ subscriber.complete();
7472
+ return;
7473
+ }
7461
7474
  }
7462
- }
7475
+ });
7476
+ return unsub;
7463
7477
  });
7464
- return unsub;
7465
- });
7466
- }
7467
- function toMessages$(node2) {
7478
+ }
7468
7479
  return new import_rxjs.Observable((subscriber) => {
7469
7480
  const unsub = node2.subscribe((msgs) => {
7470
- if (subscriber.closed) return;
7471
- subscriber.next(msgs);
7472
- for (const m of msgs) {
7473
- if (m[0] === ERROR) {
7474
- subscriber.error(m[1]);
7475
- return;
7476
- }
7477
- if (m[0] === COMPLETE) {
7478
- subscriber.complete();
7479
- return;
7480
- }
7481
- }
7482
- });
7483
- return unsub;
7484
- });
7485
- }
7486
- function observeNode$(graph, path, options) {
7487
- return new import_rxjs.Observable((subscriber) => {
7488
- const handle = graph.observe(path, options);
7489
- const unsub = handle.subscribe((msgs) => {
7490
7481
  for (const m of msgs) {
7491
7482
  if (subscriber.closed) return;
7492
7483
  if (m[0] === DATA) {
@@ -7503,16 +7494,6 @@ function observeNode$(graph, path, options) {
7503
7494
  return unsub;
7504
7495
  });
7505
7496
  }
7506
- function observeGraph$(graph, options) {
7507
- return new import_rxjs.Observable((subscriber) => {
7508
- const handle = graph.observe(options);
7509
- const unsub = handle.subscribe((nodePath, messages) => {
7510
- if (subscriber.closed) return;
7511
- subscriber.next({ path: nodePath, messages });
7512
- });
7513
- return unsub;
7514
- });
7515
- }
7516
7497
 
7517
7498
  // src/extra/pubsub.ts
7518
7499
  var PubSubHubImpl = class {
@@ -8040,7 +8021,7 @@ function workerBridge(target, opts) {
8040
8021
  for (const m of msgs) {
8041
8022
  const type = m[0];
8042
8023
  if (type === DATA) continue;
8043
- if (knownMessageTypes.includes(type) && messageTier(type) < 2) continue;
8024
+ if (isLocalOnly(type)) continue;
8044
8025
  if (type === ERROR) {
8045
8026
  transport.post({
8046
8027
  t: "e",
@@ -8166,7 +8147,7 @@ function workerSelf(target, opts) {
8166
8147
  for (const m of msgs) {
8167
8148
  const type = m[0];
8168
8149
  if (type === DATA) continue;
8169
- if (knownMessageTypes.includes(type) && messageTier(type) < 2) continue;
8150
+ if (isLocalOnly(type)) continue;
8170
8151
  if (type === ERROR) {
8171
8152
  transport.post({
8172
8153
  t: "e",
@@ -8310,6 +8291,7 @@ function workerSelf(target, opts) {
8310
8291
  find,
8311
8292
  first,
8312
8293
  firstValueFrom,
8294
+ firstWhere,
8313
8295
  flatMap,
8314
8296
  forEach,
8315
8297
  fromAny,
@@ -8356,8 +8338,6 @@ function workerSelf(target, opts) {
8356
8338
  mergeMap,
8357
8339
  nameToSignal,
8358
8340
  never,
8359
- observeGraph$,
8360
- observeNode$,
8361
8341
  of,
8362
8342
  pairwise,
8363
8343
  parseCron,
@@ -8389,7 +8369,6 @@ function workerSelf(target, opts) {
8389
8369
  shareReplay,
8390
8370
  signalToName,
8391
8371
  skip,
8392
- startWith,
8393
8372
  switchMap,
8394
8373
  take,
8395
8374
  takeUntil,
@@ -8406,7 +8385,6 @@ function workerSelf(target, opts) {
8406
8385
  toFile,
8407
8386
  toKafka,
8408
8387
  toLoki,
8409
- toMessages$,
8410
8388
  toMongo,
8411
8389
  toNATS,
8412
8390
  toObservable,