@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
@@ -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,
@@ -100,6 +101,7 @@ __export(extra_exports, {
100
101
  fromWebhook: () => fromWebhook,
101
102
  globToRegExp: () => globToRegExp,
102
103
  interval: () => interval,
104
+ keepalive: () => keepalive,
103
105
  last: () => last,
104
106
  linear: () => linear,
105
107
  logSlice: () => logSlice,
@@ -121,6 +123,7 @@ __export(extra_exports, {
121
123
  pubsub: () => pubsub,
122
124
  race: () => race,
123
125
  rateLimiter: () => rateLimiter,
126
+ reactiveCounter: () => reactiveCounter,
124
127
  reactiveIndex: () => reactiveIndex,
125
128
  reactiveList: () => reactiveList,
126
129
  reactiveLog: () => reactiveLog,
@@ -142,7 +145,6 @@ __export(extra_exports, {
142
145
  shareReplay: () => shareReplay,
143
146
  signalToName: () => signalToName,
144
147
  skip: () => skip,
145
- startWith: () => startWith,
146
148
  switchMap: () => switchMap,
147
149
  take: () => take,
148
150
  takeUntil: () => takeUntil,
@@ -189,6 +191,7 @@ __export(extra_exports, {
189
191
  module.exports = __toCommonJS(extra_exports);
190
192
 
191
193
  // src/core/messages.ts
194
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
192
195
  var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
193
196
  var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
194
197
  var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
@@ -199,6 +202,7 @@ var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
199
202
  var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
200
203
  var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
201
204
  var knownMessageTypes = [
205
+ START,
202
206
  DATA,
203
207
  DIRTY,
204
208
  RESOLVED,
@@ -209,13 +213,15 @@ var knownMessageTypes = [
209
213
  COMPLETE,
210
214
  ERROR
211
215
  ];
216
+ var knownMessageSet = new Set(knownMessageTypes);
212
217
  function messageTier(t) {
213
- if (t === DIRTY || t === INVALIDATE) return 0;
214
- if (t === PAUSE || t === RESUME) return 1;
215
- if (t === DATA || t === RESOLVED) return 2;
216
- if (t === COMPLETE || t === ERROR) return 3;
217
- if (t === TEARDOWN) return 4;
218
- return 0;
218
+ if (t === START) return 0;
219
+ if (t === DIRTY || t === INVALIDATE) return 1;
220
+ if (t === PAUSE || t === RESUME) return 2;
221
+ if (t === DATA || t === RESOLVED) return 3;
222
+ if (t === COMPLETE || t === ERROR) return 4;
223
+ if (t === TEARDOWN) return 5;
224
+ return 1;
219
225
  }
220
226
  function isPhase2Message(msg) {
221
227
  const t = msg[0];
@@ -224,6 +230,10 @@ function isPhase2Message(msg) {
224
230
  function isTerminalMessage(t) {
225
231
  return t === COMPLETE || t === ERROR;
226
232
  }
233
+ function isLocalOnly(t) {
234
+ if (!knownMessageSet.has(t)) return false;
235
+ return messageTier(t) < 3;
236
+ }
227
237
  function propagatesToMeta(t) {
228
238
  return t === TEARDOWN;
229
239
  }
@@ -384,14 +394,14 @@ function _downSequential(sink, messages, phase = 2) {
384
394
  const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
385
395
  for (const msg of messages) {
386
396
  const tier = messageTier(msg[0]);
387
- if (tier === 2) {
397
+ if (tier === 3) {
388
398
  if (isBatching()) {
389
399
  const m = msg;
390
400
  dataQueue.push(() => sink([m]));
391
401
  } else {
392
402
  sink([msg]);
393
403
  }
394
- } else if (tier >= 3) {
404
+ } else if (tier >= 4) {
395
405
  if (isBatching()) {
396
406
  const m = msg;
397
407
  pendingPhase3.push(() => sink([m]));
@@ -500,10 +510,24 @@ function advanceVersion(info, newValue, hashFn) {
500
510
  }
501
511
  }
502
512
 
503
- // src/core/node.ts
513
+ // src/core/node-base.ts
504
514
  var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
505
515
  var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
506
- function createIntBitSet() {
516
+ var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
517
+ var isCleanupFn = (value) => typeof value === "function";
518
+ function statusAfterMessage(status, msg) {
519
+ const t = msg[0];
520
+ if (t === DIRTY) return "dirty";
521
+ if (t === DATA) return "settled";
522
+ if (t === RESOLVED) return "resolved";
523
+ if (t === COMPLETE) return "completed";
524
+ if (t === ERROR) return "errored";
525
+ if (t === INVALIDATE) return "dirty";
526
+ if (t === TEARDOWN) return "disconnected";
527
+ return status;
528
+ }
529
+ function createIntBitSet(size) {
530
+ const fullMask = size >= 32 ? -1 : ~(-1 << size);
507
531
  let bits = 0;
508
532
  return {
509
533
  set(i) {
@@ -516,7 +540,8 @@ function createIntBitSet() {
516
540
  return (bits & 1 << i) !== 0;
517
541
  },
518
542
  covers(other) {
519
- return (bits & other._bits()) === other._bits();
543
+ const otherBits = other._bits();
544
+ return (bits & otherBits) === otherBits;
520
545
  },
521
546
  any() {
522
547
  return bits !== 0;
@@ -524,6 +549,9 @@ function createIntBitSet() {
524
549
  reset() {
525
550
  bits = 0;
526
551
  },
552
+ setAll() {
553
+ bits = fullMask;
554
+ },
527
555
  _bits() {
528
556
  return bits;
529
557
  }
@@ -531,6 +559,8 @@ function createIntBitSet() {
531
559
  }
532
560
  function createArrayBitSet(size) {
533
561
  const words = new Uint32Array(Math.ceil(size / 32));
562
+ const lastBits = size % 32;
563
+ const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
534
564
  return {
535
565
  set(i) {
536
566
  words[i >>> 5] |= 1 << (i & 31);
@@ -557,130 +587,103 @@ function createArrayBitSet(size) {
557
587
  reset() {
558
588
  words.fill(0);
559
589
  },
590
+ setAll() {
591
+ for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
592
+ if (words.length > 0) words[words.length - 1] = lastWordMask;
593
+ },
560
594
  _words: words
561
595
  };
562
596
  }
563
597
  function createBitSet(size) {
564
- return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
598
+ return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
565
599
  }
566
- var isNodeArray = (value) => Array.isArray(value);
567
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
568
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
569
- var isCleanupFn = (value) => typeof value === "function";
570
- var statusAfterMessage = (status, msg) => {
571
- const t = msg[0];
572
- if (t === DIRTY) return "dirty";
573
- if (t === DATA) return "settled";
574
- if (t === RESOLVED) return "resolved";
575
- if (t === COMPLETE) return "completed";
576
- if (t === ERROR) return "errored";
577
- if (t === INVALIDATE) return "dirty";
578
- if (t === TEARDOWN) return "disconnected";
579
- return status;
580
- };
581
- var NodeImpl = class {
582
- // --- Configuration (set once, never reassigned) ---
600
+ var NodeBase = class {
601
+ // --- Identity (set once) ---
583
602
  _optsName;
584
603
  _registryName;
585
- /** @internal read by {@link describeNode} before inference. */
604
+ /** @internal Read by `describeNode` before inference. */
586
605
  _describeKind;
587
606
  meta;
588
- _deps;
589
- _fn;
590
- _opts;
607
+ // --- Options ---
591
608
  _equals;
609
+ _resubscribable;
610
+ _resetOnTeardown;
611
+ _onResubscribe;
592
612
  _onMessage;
593
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
613
+ /** @internal Read by `describeNode` for `accessHintForGuard`. */
594
614
  _guard;
615
+ /** @internal Subclasses update this through {@link _recordMutation}. */
595
616
  _lastMutation;
596
- _hasDeps;
597
- _autoComplete;
598
- _isSingleDep;
599
- // --- Mutable state ---
617
+ // --- Versioning ---
618
+ _hashFn;
619
+ _versioning;
620
+ // --- Lifecycle state ---
621
+ /** @internal Read by `describeNode` and `graph.ts`. */
600
622
  _cached;
623
+ /** @internal Read externally via `get status()`. */
601
624
  _status;
602
625
  _terminal = false;
603
- _connected = false;
604
- _producerStarted = false;
605
- _connecting = false;
606
- _manualEmitUsed = false;
626
+ _active = false;
627
+ // --- Sink storage ---
628
+ /** @internal Read by `graph/profile.ts` for subscriber counts. */
607
629
  _sinkCount = 0;
608
630
  _singleDepSinkCount = 0;
609
- // --- Object/collection state ---
610
- _depDirtyMask;
611
- _depSettledMask;
612
- _depCompleteMask;
613
- _allDepsCompleteMask;
614
- _lastDepValues;
615
- _cleanup;
616
- _sinks = null;
617
631
  _singleDepSinks = /* @__PURE__ */ new WeakSet();
618
- _upstreamUnsubs = [];
632
+ _sinks = null;
633
+ // --- Actions + bound helpers ---
619
634
  _actions;
620
635
  _boundDownToSinks;
636
+ // --- Inspector hook (Graph observability) ---
621
637
  _inspectorHook;
622
- _versioning;
623
- _hashFn;
624
- constructor(deps, fn, opts) {
625
- this._deps = deps;
626
- this._fn = fn;
627
- this._opts = opts;
638
+ constructor(opts) {
628
639
  this._optsName = opts.name;
629
640
  this._describeKind = opts.describeKind;
630
641
  this._equals = opts.equals ?? Object.is;
642
+ this._resubscribable = opts.resubscribable ?? false;
643
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
644
+ this._onResubscribe = opts.onResubscribe;
631
645
  this._onMessage = opts.onMessage;
632
646
  this._guard = opts.guard;
633
- this._hasDeps = deps.length > 0;
634
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
635
- this._isSingleDep = deps.length === 1 && fn != null;
636
647
  this._cached = "initial" in opts ? opts.initial : NO_VALUE;
637
- this._status = this._hasDeps ? "disconnected" : "settled";
648
+ this._status = "disconnected";
638
649
  this._hashFn = opts.versioningHash ?? defaultHash;
639
650
  this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
640
651
  id: opts.versioningId,
641
652
  hash: this._hashFn
642
653
  }) : void 0;
643
- this._depDirtyMask = createBitSet(deps.length);
644
- this._depSettledMask = createBitSet(deps.length);
645
- this._depCompleteMask = createBitSet(deps.length);
646
- this._allDepsCompleteMask = createBitSet(deps.length);
647
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
648
654
  const meta = {};
649
655
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
650
- meta[k] = node({
651
- initial: v,
652
- name: `${opts.name ?? "node"}:meta:${k}`,
653
- describeKind: "state",
654
- ...opts.guard != null ? { guard: opts.guard } : {}
655
- });
656
+ meta[k] = this._createMetaNode(k, v, opts);
656
657
  }
657
658
  Object.freeze(meta);
658
659
  this.meta = meta;
659
660
  const self = this;
660
661
  this._actions = {
661
662
  down(messages) {
662
- self._manualEmitUsed = true;
663
+ self._onManualEmit();
663
664
  self._downInternal(messages);
664
665
  },
665
666
  emit(value) {
666
- self._manualEmitUsed = true;
667
+ self._onManualEmit();
667
668
  self._downAutoValue(value);
668
669
  },
669
670
  up(messages) {
670
671
  self._upInternal(messages);
671
672
  }
672
673
  };
673
- this.down = this.down.bind(this);
674
- this.up = this.up.bind(this);
675
674
  this._boundDownToSinks = this._downToSinks.bind(this);
676
675
  }
676
+ /**
677
+ * Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
678
+ * {@link NodeImpl} overrides to set `_manualEmitUsed`.
679
+ */
680
+ _onManualEmit() {
681
+ }
682
+ // --- Identity getters ---
677
683
  get name() {
678
684
  return this._registryName ?? this._optsName;
679
685
  }
680
- /**
681
- * When a node is registered with {@link Graph.add} without an options `name`,
682
- * the graph assigns the registry local name for introspection (parity with graphrefly-py).
683
- */
686
+ /** @internal Assigned by `Graph.add` when registered without an options `name`. */
684
687
  _assignRegistryName(localName) {
685
688
  if (this._optsName !== void 0 || this._registryName !== void 0) return;
686
689
  this._registryName = localName;
@@ -698,7 +701,10 @@ var NodeImpl = class {
698
701
  }
699
702
  };
700
703
  }
701
- // --- Public interface (Node<T>) ---
704
+ /** @internal Used by subclasses to surface inspector events. */
705
+ _emitInspectorHook(event) {
706
+ this._inspectorHook?.(event);
707
+ }
702
708
  get status() {
703
709
  return this._status;
704
710
  }
@@ -708,15 +714,7 @@ var NodeImpl = class {
708
714
  get v() {
709
715
  return this._versioning;
710
716
  }
711
- /**
712
- * Retroactively apply versioning to a node that was created without it.
713
- * No-op if versioning is already enabled.
714
- *
715
- * Version starts at 0 regardless of prior DATA emissions — it tracks
716
- * changes from the moment versioning is enabled, not historical ones.
717
- *
718
- * @internal — used by {@link Graph.setVersioning}.
719
- */
717
+ /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
720
718
  _applyVersioning(level, opts) {
721
719
  if (this._versioning != null) return;
722
720
  this._hashFn = opts?.hash ?? this._hashFn;
@@ -736,6 +734,7 @@ var NodeImpl = class {
736
734
  if (this._guard == null) return true;
737
735
  return this._guard(normalizeActor(actor), "observe");
738
736
  }
737
+ // --- Public transport ---
739
738
  get() {
740
739
  return this._cached === NO_VALUE ? void 0 : this._cached;
741
740
  }
@@ -748,43 +747,25 @@ var NodeImpl = class {
748
747
  if (!this._guard(actor, action)) {
749
748
  throw new GuardDenied({ actor, action, nodeName: this.name });
750
749
  }
751
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
750
+ this._recordMutation(actor);
752
751
  }
753
752
  this._downInternal(messages);
754
753
  }
755
- _downInternal(messages) {
756
- if (messages.length === 0) return;
757
- let lifecycleMessages = messages;
758
- let sinkMessages = messages;
759
- if (this._terminal && !this._opts.resubscribable) {
760
- const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
761
- if (terminalPassthrough.length === 0) return;
762
- lifecycleMessages = terminalPassthrough;
763
- sinkMessages = terminalPassthrough;
764
- }
765
- this._handleLocalLifecycle(lifecycleMessages);
766
- if (this._canSkipDirty()) {
767
- let hasPhase2 = false;
768
- for (let i = 0; i < sinkMessages.length; i++) {
769
- const t = sinkMessages[i][0];
770
- if (t === DATA || t === RESOLVED) {
771
- hasPhase2 = true;
772
- break;
773
- }
774
- }
775
- if (hasPhase2) {
776
- const filtered = [];
777
- for (let i = 0; i < sinkMessages.length; i++) {
778
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
779
- }
780
- if (filtered.length > 0) {
781
- downWithBatch(this._boundDownToSinks, filtered);
782
- }
783
- return;
784
- }
785
- }
786
- downWithBatch(this._boundDownToSinks, sinkMessages);
754
+ /** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
755
+ _recordMutation(actor) {
756
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
757
+ }
758
+ /**
759
+ * At-most-once deactivation guard. Both TEARDOWN (eager) and
760
+ * unsubscribe-body (lazy) call this. The `_active` flag ensures
761
+ * `_doDeactivate` runs exactly once per activation cycle.
762
+ */
763
+ _onDeactivate() {
764
+ if (!this._active) return;
765
+ this._active = false;
766
+ this._doDeactivate();
787
767
  }
768
+ // --- Subscribe (uniform across node shapes) ---
788
769
  subscribe(sink, hints) {
789
770
  if (hints?.actor != null && this._guard != null) {
790
771
  const actor = normalizeActor(hints.actor);
@@ -792,17 +773,21 @@ var NodeImpl = class {
792
773
  throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
793
774
  }
794
775
  }
795
- if (this._terminal && this._opts.resubscribable) {
776
+ if (this._terminal && this._resubscribable) {
796
777
  this._terminal = false;
797
778
  this._cached = NO_VALUE;
798
- this._status = this._hasDeps ? "disconnected" : "settled";
799
- this._opts.onResubscribe?.();
779
+ this._status = "disconnected";
780
+ this._onResubscribe?.();
800
781
  }
801
782
  this._sinkCount += 1;
802
783
  if (hints?.singleDep) {
803
784
  this._singleDepSinkCount += 1;
804
785
  this._singleDepSinks.add(sink);
805
786
  }
787
+ if (!this._terminal) {
788
+ const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
789
+ downWithBatch(sink, startMessages);
790
+ }
806
791
  if (this._sinks == null) {
807
792
  this._sinks = sink;
808
793
  } else if (typeof this._sinks === "function") {
@@ -810,10 +795,12 @@ var NodeImpl = class {
810
795
  } else {
811
796
  this._sinks.add(sink);
812
797
  }
813
- if (this._hasDeps) {
814
- this._connectUpstream();
815
- } else if (this._fn) {
816
- this._startProducer();
798
+ if (this._sinkCount === 1 && !this._terminal) {
799
+ this._active = true;
800
+ this._onActivate();
801
+ }
802
+ if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
803
+ this._status = "pending";
817
804
  }
818
805
  let removed = false;
819
806
  return () => {
@@ -837,39 +824,49 @@ var NodeImpl = class {
837
824
  }
838
825
  }
839
826
  if (this._sinks == null) {
840
- this._disconnectUpstream();
841
- this._stopProducer();
827
+ this._onDeactivate();
842
828
  }
843
829
  };
844
830
  }
845
- up(messages, options) {
846
- if (!this._hasDeps) return;
847
- if (!options?.internal && this._guard != null) {
848
- const actor = normalizeActor(options?.actor);
849
- if (!this._guard(actor, "write")) {
850
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
851
- }
852
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
831
+ // --- Down pipeline ---
832
+ /**
833
+ * Core outgoing dispatch. Applies terminal filter + local lifecycle
834
+ * update, then hands messages to `downWithBatch` for tier-aware delivery.
835
+ */
836
+ _downInternal(messages) {
837
+ if (messages.length === 0) return;
838
+ let sinkMessages = messages;
839
+ if (this._terminal && !this._resubscribable) {
840
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
841
+ if (pass.length === 0) return;
842
+ sinkMessages = pass;
853
843
  }
854
- for (const dep of this._deps) {
855
- if (options === void 0) {
856
- dep.up?.(messages);
857
- } else {
858
- dep.up?.(messages, options);
844
+ this._handleLocalLifecycle(sinkMessages);
845
+ if (this._canSkipDirty()) {
846
+ let hasPhase2 = false;
847
+ for (let i = 0; i < sinkMessages.length; i++) {
848
+ const t = sinkMessages[i][0];
849
+ if (t === DATA || t === RESOLVED) {
850
+ hasPhase2 = true;
851
+ break;
852
+ }
853
+ }
854
+ if (hasPhase2) {
855
+ const filtered = [];
856
+ for (let i = 0; i < sinkMessages.length; i++) {
857
+ if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
858
+ }
859
+ if (filtered.length > 0) {
860
+ downWithBatch(this._boundDownToSinks, filtered);
861
+ }
862
+ return;
859
863
  }
860
864
  }
865
+ downWithBatch(this._boundDownToSinks, sinkMessages);
861
866
  }
862
- _upInternal(messages) {
863
- if (!this._hasDeps) return;
864
- for (const dep of this._deps) {
865
- dep.up?.(messages, { internal: true });
866
- }
867
- }
868
- unsubscribe() {
869
- if (!this._hasDeps) return;
870
- this._disconnectUpstream();
867
+ _canSkipDirty() {
868
+ return this._sinkCount === 1 && this._singleDepSinkCount === 1;
871
869
  }
872
- // --- Private methods (prototype, _ prefix) ---
873
870
  _downToSinks(messages) {
874
871
  if (this._sinks == null) return;
875
872
  if (typeof this._sinks === "function") {
@@ -881,6 +878,11 @@ var NodeImpl = class {
881
878
  sink(messages);
882
879
  }
883
880
  }
881
+ /**
882
+ * Update `_cached`, `_status`, `_terminal` from message batch before
883
+ * delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
884
+ * {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
885
+ */
884
886
  _handleLocalLifecycle(messages) {
885
887
  for (const m of messages) {
886
888
  const t = m[0];
@@ -894,28 +896,22 @@ var NodeImpl = class {
894
896
  }
895
897
  }
896
898
  if (t === INVALIDATE) {
897
- const cleanupFn = this._cleanup;
898
- this._cleanup = void 0;
899
- cleanupFn?.();
899
+ this._onInvalidate();
900
900
  this._cached = NO_VALUE;
901
- this._lastDepValues = void 0;
902
901
  }
903
902
  this._status = statusAfterMessage(this._status, m);
904
903
  if (t === COMPLETE || t === ERROR) {
905
904
  this._terminal = true;
906
905
  }
907
906
  if (t === TEARDOWN) {
908
- if (this._opts.resetOnTeardown) {
907
+ if (this._resetOnTeardown) {
909
908
  this._cached = NO_VALUE;
910
909
  }
911
- const teardownCleanup = this._cleanup;
912
- this._cleanup = void 0;
913
- teardownCleanup?.();
910
+ this._onTeardown();
914
911
  try {
915
912
  this._propagateToMeta(t);
916
913
  } finally {
917
- this._disconnectUpstream();
918
- this._stopProducer();
914
+ this._onDeactivate();
919
915
  }
920
916
  }
921
917
  if (t !== TEARDOWN && propagatesToMeta(t)) {
@@ -923,7 +919,20 @@ var NodeImpl = class {
923
919
  }
924
920
  }
925
921
  }
926
- /** Propagate a signal to all companion meta nodes (best-effort). */
922
+ /**
923
+ * Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
924
+ * cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
925
+ * drop `_lastDepValues` so the next wave re-runs fn.
926
+ */
927
+ _onInvalidate() {
928
+ }
929
+ /**
930
+ * Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
931
+ * {@link NodeImpl} uses this to run any pending cleanup fn.
932
+ */
933
+ _onTeardown() {
934
+ }
935
+ /** Forward a signal to all companion meta nodes (best-effort). */
927
936
  _propagateToMeta(t) {
928
937
  for (const metaNode of Object.values(this.meta)) {
929
938
  try {
@@ -932,9 +941,10 @@ var NodeImpl = class {
932
941
  }
933
942
  }
934
943
  }
935
- _canSkipDirty() {
936
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
937
- }
944
+ /**
945
+ * Frame a computed value into the right protocol messages and dispatch
946
+ * via `_downInternal`. Used by `_runFn` and `actions.emit`.
947
+ */
938
948
  _downAutoValue(value) {
939
949
  const wasDirty = this._status === "dirty";
940
950
  let unchanged;
@@ -942,7 +952,9 @@ var NodeImpl = class {
942
952
  unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
943
953
  } catch (eqErr) {
944
954
  const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
945
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
955
+ const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
956
+ cause: eqErr
957
+ });
946
958
  this._downInternal([[ERROR, wrapped]]);
947
959
  return;
948
960
  }
@@ -952,89 +964,173 @@ var NodeImpl = class {
952
964
  }
953
965
  this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
954
966
  }
955
- _runFn() {
956
- if (!this._fn) return;
957
- if (this._terminal && !this._opts.resubscribable) return;
958
- if (this._connecting) return;
959
- try {
960
- const n = this._deps.length;
961
- const depValues = new Array(n);
962
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
963
- const prev = this._lastDepValues;
964
- if (n > 0 && prev != null && prev.length === n) {
965
- let allSame = true;
966
- for (let i = 0; i < n; i++) {
967
- if (!Object.is(depValues[i], prev[i])) {
968
- allSame = false;
969
- break;
970
- }
971
- }
972
- if (allSame) {
973
- if (this._status === "dirty") {
974
- this._downInternal([[RESOLVED]]);
975
- }
976
- return;
977
- }
978
- }
979
- const prevCleanup = this._cleanup;
980
- this._cleanup = void 0;
981
- prevCleanup?.();
982
- this._manualEmitUsed = false;
983
- this._lastDepValues = depValues;
984
- this._inspectorHook?.({ kind: "run", depValues });
985
- const out = this._fn(depValues, this._actions);
986
- if (isCleanupResult(out)) {
987
- this._cleanup = out.cleanup;
988
- if (this._manualEmitUsed) return;
989
- if ("value" in out) {
990
- this._downAutoValue(out.value);
991
- }
992
- return;
993
- }
994
- if (isCleanupFn(out)) {
995
- this._cleanup = out;
996
- return;
997
- }
998
- if (this._manualEmitUsed) return;
999
- if (out === void 0) return;
1000
- this._downAutoValue(out);
1001
- } catch (err) {
1002
- const errMsg = err instanceof Error ? err.message : String(err);
1003
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1004
- this._downInternal([[ERROR, wrapped]]);
967
+ };
968
+
969
+ // src/core/node.ts
970
+ var NodeImpl = class extends NodeBase {
971
+ // --- Dep configuration (set once) ---
972
+ _deps;
973
+ _fn;
974
+ _opts;
975
+ _hasDeps;
976
+ _isSingleDep;
977
+ _autoComplete;
978
+ // --- Wave tracking masks ---
979
+ _depDirtyMask;
980
+ _depSettledMask;
981
+ _depCompleteMask;
982
+ _allDepsCompleteMask;
983
+ // --- Identity-skip optimization ---
984
+ _lastDepValues;
985
+ _cleanup;
986
+ // --- Upstream bookkeeping ---
987
+ _upstreamUnsubs = [];
988
+ // --- Fn behavior flag ---
989
+ /** @internal Read by `describeNode` to infer `"operator"` label. */
990
+ _manualEmitUsed = false;
991
+ constructor(deps, fn, opts) {
992
+ super(opts);
993
+ this._deps = deps;
994
+ this._fn = fn;
995
+ this._opts = opts;
996
+ this._hasDeps = deps.length > 0;
997
+ this._isSingleDep = deps.length === 1 && fn != null;
998
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
999
+ if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
1000
+ this._status = "settled";
1005
1001
  }
1006
- }
1007
- _onDepDirty(index) {
1008
- const wasDirty = this._depDirtyMask.has(index);
1009
- this._depDirtyMask.set(index);
1010
- this._depSettledMask.clear(index);
1011
- if (!wasDirty) {
1012
- this._downInternal([[DIRTY]]);
1002
+ this._depDirtyMask = createBitSet(deps.length);
1003
+ this._depSettledMask = createBitSet(deps.length);
1004
+ this._depCompleteMask = createBitSet(deps.length);
1005
+ this._allDepsCompleteMask = createBitSet(deps.length);
1006
+ for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
1007
+ this.down = this.down.bind(this);
1008
+ this.up = this.up.bind(this);
1009
+ }
1010
+ // --- Meta node factory (called from base constructor) ---
1011
+ _createMetaNode(key, initialValue, opts) {
1012
+ return node({
1013
+ initial: initialValue,
1014
+ name: `${opts.name ?? "node"}:meta:${key}`,
1015
+ describeKind: "state",
1016
+ ...opts.guard != null ? { guard: opts.guard } : {}
1017
+ });
1018
+ }
1019
+ // --- Manual emit tracker (set by actions.down / actions.emit) ---
1020
+ _onManualEmit() {
1021
+ this._manualEmitUsed = true;
1022
+ }
1023
+ // --- Up / unsubscribe ---
1024
+ up(messages, options) {
1025
+ if (!this._hasDeps) return;
1026
+ if (!options?.internal && this._guard != null) {
1027
+ const actor = normalizeActor(options?.actor);
1028
+ if (!this._guard(actor, "write")) {
1029
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1030
+ }
1031
+ this._recordMutation(actor);
1032
+ }
1033
+ for (const dep of this._deps) {
1034
+ if (options === void 0) {
1035
+ dep.up?.(messages);
1036
+ } else {
1037
+ dep.up?.(messages, options);
1038
+ }
1013
1039
  }
1014
1040
  }
1015
- _onDepSettled(index) {
1016
- if (!this._depDirtyMask.has(index)) {
1017
- this._onDepDirty(index);
1041
+ _upInternal(messages) {
1042
+ if (!this._hasDeps) return;
1043
+ for (const dep of this._deps) {
1044
+ dep.up?.(messages, { internal: true });
1018
1045
  }
1019
- this._depSettledMask.set(index);
1020
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1021
- this._depDirtyMask.reset();
1022
- this._depSettledMask.reset();
1046
+ }
1047
+ unsubscribe() {
1048
+ if (!this._hasDeps) return;
1049
+ this._disconnectUpstream();
1050
+ }
1051
+ // --- Activation (first-subscriber / last-subscriber hooks) ---
1052
+ _onActivate() {
1053
+ if (this._hasDeps) {
1054
+ this._connectUpstream();
1055
+ return;
1056
+ }
1057
+ if (this._fn) {
1023
1058
  this._runFn();
1059
+ return;
1024
1060
  }
1025
1061
  }
1026
- _maybeCompleteFromDeps() {
1027
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1028
- this._downInternal([[COMPLETE]]);
1062
+ _doDeactivate() {
1063
+ this._disconnectUpstream();
1064
+ const cleanup = this._cleanup;
1065
+ this._cleanup = void 0;
1066
+ cleanup?.();
1067
+ if (this._fn != null) {
1068
+ this._cached = NO_VALUE;
1069
+ this._lastDepValues = void 0;
1070
+ }
1071
+ if (this._hasDeps || this._fn != null) {
1072
+ this._status = "disconnected";
1073
+ }
1074
+ }
1075
+ // --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
1076
+ _onInvalidate() {
1077
+ const cleanup = this._cleanup;
1078
+ this._cleanup = void 0;
1079
+ cleanup?.();
1080
+ this._lastDepValues = void 0;
1081
+ }
1082
+ _onTeardown() {
1083
+ const cleanup = this._cleanup;
1084
+ this._cleanup = void 0;
1085
+ cleanup?.();
1086
+ }
1087
+ // --- Upstream connect / disconnect ---
1088
+ _connectUpstream() {
1089
+ if (!this._hasDeps) return;
1090
+ if (this._upstreamUnsubs.length > 0) return;
1091
+ this._depDirtyMask.setAll();
1092
+ this._depSettledMask.reset();
1093
+ this._depCompleteMask.reset();
1094
+ const depValuesBefore = this._lastDepValues;
1095
+ const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1096
+ for (let i = 0; i < this._deps.length; i += 1) {
1097
+ const dep = this._deps[i];
1098
+ this._upstreamUnsubs.push(
1099
+ dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1100
+ );
1101
+ }
1102
+ if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
1103
+ this._runFn();
1029
1104
  }
1030
1105
  }
1106
+ _disconnectUpstream() {
1107
+ if (this._upstreamUnsubs.length === 0) return;
1108
+ for (const unsub of this._upstreamUnsubs.splice(0)) {
1109
+ unsub();
1110
+ }
1111
+ this._depDirtyMask.reset();
1112
+ this._depSettledMask.reset();
1113
+ this._depCompleteMask.reset();
1114
+ }
1115
+ // --- Wave handling ---
1031
1116
  _handleDepMessages(index, messages) {
1032
1117
  for (const msg of messages) {
1033
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1118
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1034
1119
  const t = msg[0];
1035
1120
  if (this._onMessage) {
1036
1121
  try {
1037
- if (this._onMessage(msg, index, this._actions)) continue;
1122
+ const consumed = this._onMessage(msg, index, this._actions);
1123
+ if (consumed) {
1124
+ if (t === START) {
1125
+ this._depDirtyMask.clear(index);
1126
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1127
+ this._depDirtyMask.reset();
1128
+ this._depSettledMask.reset();
1129
+ this._runFn();
1130
+ }
1131
+ }
1132
+ continue;
1133
+ }
1038
1134
  } catch (err) {
1039
1135
  const errMsg = err instanceof Error ? err.message : String(err);
1040
1136
  const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
@@ -1044,6 +1140,7 @@ var NodeImpl = class {
1044
1140
  return;
1045
1141
  }
1046
1142
  }
1143
+ if (messageTier(t) < 1) continue;
1047
1144
  if (!this._fn) {
1048
1145
  if (t === COMPLETE && this._deps.length > 1) {
1049
1146
  this._depCompleteMask.set(index);
@@ -1087,53 +1184,85 @@ var NodeImpl = class {
1087
1184
  this._downInternal([msg]);
1088
1185
  }
1089
1186
  }
1090
- _connectUpstream() {
1091
- if (!this._hasDeps || this._connected) return;
1092
- this._connected = true;
1093
- this._depDirtyMask.reset();
1094
- this._depSettledMask.reset();
1095
- this._depCompleteMask.reset();
1096
- this._status = "settled";
1097
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1098
- this._connecting = true;
1099
- try {
1100
- for (let i = 0; i < this._deps.length; i += 1) {
1101
- const dep = this._deps[i];
1102
- this._upstreamUnsubs.push(
1103
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1104
- );
1105
- }
1106
- } finally {
1107
- this._connecting = false;
1187
+ _onDepDirty(index) {
1188
+ const wasDirty = this._depDirtyMask.has(index);
1189
+ this._depDirtyMask.set(index);
1190
+ this._depSettledMask.clear(index);
1191
+ if (!wasDirty) {
1192
+ this._downInternal([[DIRTY]]);
1108
1193
  }
1109
- if (this._fn) {
1194
+ }
1195
+ _onDepSettled(index) {
1196
+ if (!this._depDirtyMask.has(index)) {
1197
+ this._onDepDirty(index);
1198
+ }
1199
+ this._depSettledMask.set(index);
1200
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1201
+ this._depDirtyMask.reset();
1202
+ this._depSettledMask.reset();
1110
1203
  this._runFn();
1111
1204
  }
1112
1205
  }
1113
- _stopProducer() {
1114
- if (!this._producerStarted) return;
1115
- this._producerStarted = false;
1116
- const producerCleanup = this._cleanup;
1117
- this._cleanup = void 0;
1118
- producerCleanup?.();
1119
- }
1120
- _startProducer() {
1121
- if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
1122
- this._producerStarted = true;
1123
- this._runFn();
1206
+ _maybeCompleteFromDeps() {
1207
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1208
+ this._downInternal([[COMPLETE]]);
1209
+ }
1124
1210
  }
1125
- _disconnectUpstream() {
1126
- if (!this._connected) return;
1127
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1128
- unsub();
1211
+ // --- Fn execution ---
1212
+ _runFn() {
1213
+ if (!this._fn) return;
1214
+ if (this._terminal && !this._resubscribable) return;
1215
+ try {
1216
+ const n = this._deps.length;
1217
+ const depValues = new Array(n);
1218
+ for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1219
+ const prev = this._lastDepValues;
1220
+ if (n > 0 && prev != null && prev.length === n) {
1221
+ let allSame = true;
1222
+ for (let i = 0; i < n; i++) {
1223
+ if (!Object.is(depValues[i], prev[i])) {
1224
+ allSame = false;
1225
+ break;
1226
+ }
1227
+ }
1228
+ if (allSame) {
1229
+ if (this._status === "dirty") {
1230
+ this._downInternal([[RESOLVED]]);
1231
+ }
1232
+ return;
1233
+ }
1234
+ }
1235
+ const prevCleanup = this._cleanup;
1236
+ this._cleanup = void 0;
1237
+ prevCleanup?.();
1238
+ this._manualEmitUsed = false;
1239
+ this._lastDepValues = depValues;
1240
+ this._emitInspectorHook({ kind: "run", depValues });
1241
+ const out = this._fn(depValues, this._actions);
1242
+ if (isCleanupResult(out)) {
1243
+ this._cleanup = out.cleanup;
1244
+ if (this._manualEmitUsed) return;
1245
+ if ("value" in out) {
1246
+ this._downAutoValue(out.value);
1247
+ }
1248
+ return;
1249
+ }
1250
+ if (isCleanupFn(out)) {
1251
+ this._cleanup = out;
1252
+ return;
1253
+ }
1254
+ if (this._manualEmitUsed) return;
1255
+ if (out === void 0) return;
1256
+ this._downAutoValue(out);
1257
+ } catch (err) {
1258
+ const errMsg = err instanceof Error ? err.message : String(err);
1259
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1260
+ this._downInternal([[ERROR, wrapped]]);
1129
1261
  }
1130
- this._connected = false;
1131
- this._depDirtyMask.reset();
1132
- this._depSettledMask.reset();
1133
- this._depCompleteMask.reset();
1134
- this._status = "disconnected";
1135
1262
  }
1136
1263
  };
1264
+ var isNodeArray = (value) => Array.isArray(value);
1265
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1137
1266
  function node(depsOrFn, fnOrOpts, optsArg) {
1138
1267
  const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1139
1268
  const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
@@ -1158,8 +1287,8 @@ function producer(fn, opts) {
1158
1287
  function derived(deps, fn, opts) {
1159
1288
  return node(deps, fn, { describeKind: "derived", ...opts });
1160
1289
  }
1161
- function effect(deps, fn) {
1162
- return node(deps, fn, { describeKind: "effect" });
1290
+ function effect(deps, fn, opts) {
1291
+ return node(deps, fn, { describeKind: "effect", ...opts });
1163
1292
  }
1164
1293
 
1165
1294
  // src/extra/backoff.ts
@@ -2277,7 +2406,60 @@ function firstValueFrom(source) {
2277
2406
  });
2278
2407
  });
2279
2408
  }
2409
+ function firstWhere(source, predicate) {
2410
+ return new Promise((resolve, reject) => {
2411
+ let settled = false;
2412
+ const unsub = source.subscribe((msgs) => {
2413
+ for (const m of msgs) {
2414
+ if (settled) return;
2415
+ if (m[0] === DATA) {
2416
+ const v = m[1];
2417
+ if (predicate(v)) {
2418
+ settled = true;
2419
+ resolve(v);
2420
+ queueMicrotask(() => unsub());
2421
+ return;
2422
+ }
2423
+ }
2424
+ if (m[0] === ERROR) {
2425
+ settled = true;
2426
+ reject(m[1]);
2427
+ queueMicrotask(() => unsub());
2428
+ return;
2429
+ }
2430
+ if (m[0] === COMPLETE) {
2431
+ settled = true;
2432
+ reject(new Error("completed without matching value"));
2433
+ queueMicrotask(() => unsub());
2434
+ return;
2435
+ }
2436
+ }
2437
+ });
2438
+ });
2439
+ }
2280
2440
  var shareReplay = replay;
2441
+ function keepalive(n) {
2442
+ return n.subscribe(() => {
2443
+ });
2444
+ }
2445
+ function reactiveCounter(cap) {
2446
+ const counter = state(0);
2447
+ return {
2448
+ node: counter,
2449
+ increment() {
2450
+ const current = counter.get() ?? 0;
2451
+ if (current >= cap) return false;
2452
+ counter.down([[DIRTY], [DATA, current + 1]]);
2453
+ return true;
2454
+ },
2455
+ get() {
2456
+ return counter.get() ?? 0;
2457
+ },
2458
+ atCap() {
2459
+ return (counter.get() ?? 0) >= cap;
2460
+ }
2461
+ };
2462
+ }
2281
2463
 
2282
2464
  // src/extra/adapters.ts
2283
2465
  function createSinkErrorHandler(userHandler) {
@@ -2519,6 +2701,10 @@ function toSSE(source, opts) {
2519
2701
  unsub = source.subscribe((msgs) => {
2520
2702
  for (const msg of msgs) {
2521
2703
  const t = msg[0];
2704
+ if (isLocalOnly(t)) {
2705
+ if (t === DIRTY && includeDirty) {
2706
+ } else continue;
2707
+ }
2522
2708
  if (t === DATA) {
2523
2709
  write(dataEvent, serializeSseData(msg[1], serialize));
2524
2710
  continue;
@@ -2534,7 +2720,6 @@ function toSSE(source, opts) {
2534
2720
  return;
2535
2721
  }
2536
2722
  if (!includeResolved && t === RESOLVED) continue;
2537
- if (!includeDirty && t === DIRTY) continue;
2538
2723
  write(
2539
2724
  eventNameResolver(t),
2540
2725
  msg.length > 1 ? serializeSseData(msg[1], serialize) : void 0
@@ -5121,452 +5306,275 @@ function restoreGraphCheckpointIndexedDb(graph, spec) {
5121
5306
  }
5122
5307
 
5123
5308
  // src/core/dynamic-node.ts
5309
+ var MAX_RERUN = 16;
5124
5310
  function dynamicNode(fn, opts) {
5125
5311
  return new DynamicNodeImpl(fn, opts ?? {});
5126
5312
  }
5127
- var DynamicNodeImpl = class {
5128
- _optsName;
5129
- _registryName;
5130
- _describeKind;
5131
- meta;
5313
+ var DynamicNodeImpl = class extends NodeBase {
5132
5314
  _fn;
5133
- _equals;
5134
- _resubscribable;
5135
- _resetOnTeardown;
5136
5315
  _autoComplete;
5137
- _onMessage;
5138
- _onResubscribe;
5139
- /** @internal — read by {@link describeNode} for `accessHintForGuard`. */
5140
- _guard;
5141
- _lastMutation;
5142
- _inspectorHook;
5143
- // Sink tracking
5144
- _sinkCount = 0;
5145
- _singleDepSinkCount = 0;
5146
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
5147
- // Actions object (for onMessage handler)
5148
- _actions;
5149
- _boundDownToSinks;
5150
- // Mutable state
5151
- _cached = NO_VALUE;
5152
- _status = "disconnected";
5153
- _terminal = false;
5154
- _connected = false;
5155
- _rewiring = false;
5156
- // re-entrancy guard
5157
5316
  // Dynamic deps tracking
5317
+ /** @internal Read by `describeNode`. */
5158
5318
  _deps = [];
5159
5319
  _depUnsubs = [];
5160
5320
  _depIndexMap = /* @__PURE__ */ new Map();
5161
- // node index in _deps
5162
- _dirtyBits = /* @__PURE__ */ new Set();
5163
- _settledBits = /* @__PURE__ */ new Set();
5164
- _completeBits = /* @__PURE__ */ new Set();
5165
- // Sinks
5166
- _sinks = null;
5321
+ _depDirtyBits = /* @__PURE__ */ new Set();
5322
+ _depSettledBits = /* @__PURE__ */ new Set();
5323
+ _depCompleteBits = /* @__PURE__ */ new Set();
5324
+ // Execution state
5325
+ _running = false;
5326
+ _rewiring = false;
5327
+ _bufferedDepMessages = [];
5328
+ _trackedValues = /* @__PURE__ */ new Map();
5329
+ _rerunCount = 0;
5167
5330
  constructor(fn, opts) {
5331
+ super(opts);
5168
5332
  this._fn = fn;
5169
- this._optsName = opts.name;
5170
- this._describeKind = opts.describeKind;
5171
- this._equals = opts.equals ?? Object.is;
5172
- this._resubscribable = opts.resubscribable ?? false;
5173
- this._resetOnTeardown = opts.resetOnTeardown ?? false;
5174
5333
  this._autoComplete = opts.completeWhenDepsComplete ?? true;
5175
- this._onMessage = opts.onMessage;
5176
- this._onResubscribe = opts.onResubscribe;
5177
- this._guard = opts.guard;
5178
- this._inspectorHook = void 0;
5179
- const meta = {};
5180
- for (const [k, v] of Object.entries(opts.meta ?? {})) {
5181
- meta[k] = node({
5182
- initial: v,
5183
- name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
5184
- describeKind: "state",
5185
- ...opts.guard != null ? { guard: opts.guard } : {}
5186
- });
5334
+ this.down = this.down.bind(this);
5335
+ this.up = this.up.bind(this);
5336
+ }
5337
+ _createMetaNode(key, initialValue, opts) {
5338
+ return node({
5339
+ initial: initialValue,
5340
+ name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
5341
+ describeKind: "state",
5342
+ ...opts.guard != null ? { guard: opts.guard } : {}
5343
+ });
5344
+ }
5345
+ /** Versioning not supported on DynamicNodeImpl (override base). */
5346
+ get v() {
5347
+ return void 0;
5348
+ }
5349
+ // --- Up / unsubscribe ---
5350
+ up(messages, options) {
5351
+ if (this._deps.length === 0) return;
5352
+ if (!options?.internal && this._guard != null) {
5353
+ const actor = normalizeActor(options?.actor);
5354
+ if (!this._guard(actor, "write")) {
5355
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
5356
+ }
5357
+ this._recordMutation(actor);
5187
5358
  }
5188
- Object.freeze(meta);
5189
- this.meta = meta;
5190
- const self = this;
5191
- this._actions = {
5192
- down(messages) {
5193
- self._downInternal(messages);
5194
- },
5195
- emit(value) {
5196
- self._downAutoValue(value);
5197
- },
5198
- up(messages) {
5199
- for (const dep of self._deps) {
5200
- dep.up?.(messages, { internal: true });
5201
- }
5202
- }
5203
- };
5204
- this._boundDownToSinks = this._downToSinks.bind(this);
5205
- }
5206
- get name() {
5207
- return this._registryName ?? this._optsName;
5208
- }
5209
- /** @internal */
5210
- _assignRegistryName(localName) {
5211
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
5212
- this._registryName = localName;
5213
- }
5214
- /**
5215
- * @internal Attach/remove inspector hook for graph-level observability.
5216
- * Returns a disposer that restores the previous hook.
5217
- */
5218
- _setInspectorHook(hook) {
5219
- const prev = this._inspectorHook;
5220
- this._inspectorHook = hook;
5221
- return () => {
5222
- if (this._inspectorHook === hook) {
5223
- this._inspectorHook = prev;
5224
- }
5225
- };
5226
- }
5227
- get status() {
5228
- return this._status;
5229
- }
5230
- get lastMutation() {
5231
- return this._lastMutation;
5232
- }
5233
- /** Versioning not yet supported on DynamicNodeImpl. */
5234
- get v() {
5235
- return void 0;
5236
- }
5237
- hasGuard() {
5238
- return this._guard != null;
5239
- }
5240
- allowsObserve(actor) {
5241
- if (this._guard == null) return true;
5242
- return this._guard(normalizeActor(actor), "observe");
5243
- }
5244
- get() {
5245
- return this._cached === NO_VALUE ? void 0 : this._cached;
5246
- }
5247
- down(messages, options) {
5248
- if (messages.length === 0) return;
5249
- if (!options?.internal && this._guard != null) {
5250
- const actor = normalizeActor(options?.actor);
5251
- const delivery = options?.delivery ?? "write";
5252
- const action = delivery === "signal" ? "signal" : "write";
5253
- if (!this._guard(actor, action)) {
5254
- throw new GuardDenied({ actor, action, nodeName: this.name });
5255
- }
5256
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
5257
- }
5258
- this._downInternal(messages);
5259
- }
5260
- _downInternal(messages) {
5261
- if (messages.length === 0) return;
5262
- let sinkMessages = messages;
5263
- if (this._terminal && !this._resubscribable) {
5264
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
5265
- if (pass.length === 0) return;
5266
- sinkMessages = pass;
5267
- }
5268
- this._handleLocalLifecycle(sinkMessages);
5269
- if (this._canSkipDirty()) {
5270
- let hasPhase2 = false;
5271
- for (let i = 0; i < sinkMessages.length; i++) {
5272
- const t = sinkMessages[i][0];
5273
- if (t === DATA || t === RESOLVED) {
5274
- hasPhase2 = true;
5275
- break;
5276
- }
5277
- }
5278
- if (hasPhase2) {
5279
- const filtered = [];
5280
- for (let i = 0; i < sinkMessages.length; i++) {
5281
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
5282
- }
5283
- if (filtered.length > 0) {
5284
- downWithBatch(this._boundDownToSinks, filtered);
5285
- }
5286
- return;
5287
- }
5359
+ for (const dep of this._deps) {
5360
+ dep.up?.(messages, options);
5288
5361
  }
5289
- downWithBatch(this._boundDownToSinks, sinkMessages);
5290
5362
  }
5291
- _canSkipDirty() {
5292
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
5293
- }
5294
- subscribe(sink, hints) {
5295
- if (hints?.actor != null && this._guard != null) {
5296
- const actor = normalizeActor(hints.actor);
5297
- if (!this._guard(actor, "observe")) {
5298
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
5299
- }
5300
- }
5301
- if (this._terminal && this._resubscribable) {
5302
- this._terminal = false;
5303
- this._cached = NO_VALUE;
5304
- this._status = "disconnected";
5305
- this._onResubscribe?.();
5306
- }
5307
- this._sinkCount += 1;
5308
- if (hints?.singleDep) {
5309
- this._singleDepSinkCount += 1;
5310
- this._singleDepSinks.add(sink);
5311
- }
5312
- if (this._sinks == null) {
5313
- this._sinks = sink;
5314
- } else if (typeof this._sinks === "function") {
5315
- this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
5316
- } else {
5317
- this._sinks.add(sink);
5318
- }
5319
- if (!this._connected) {
5320
- this._connect();
5321
- }
5322
- let removed = false;
5323
- return () => {
5324
- if (removed) return;
5325
- removed = true;
5326
- this._sinkCount -= 1;
5327
- if (this._singleDepSinks.has(sink)) {
5328
- this._singleDepSinkCount -= 1;
5329
- this._singleDepSinks.delete(sink);
5330
- }
5331
- if (this._sinks == null) return;
5332
- if (typeof this._sinks === "function") {
5333
- if (this._sinks === sink) this._sinks = null;
5334
- } else {
5335
- this._sinks.delete(sink);
5336
- if (this._sinks.size === 1) {
5337
- const [only] = this._sinks;
5338
- this._sinks = only;
5339
- } else if (this._sinks.size === 0) {
5340
- this._sinks = null;
5341
- }
5342
- }
5343
- if (this._sinks == null) {
5344
- this._disconnect();
5345
- }
5346
- };
5347
- }
5348
- up(messages, options) {
5349
- if (this._deps.length === 0) return;
5350
- if (!options?.internal && this._guard != null) {
5351
- const actor = normalizeActor(options?.actor);
5352
- if (!this._guard(actor, "write")) {
5353
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
5354
- }
5355
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
5356
- }
5363
+ _upInternal(messages) {
5357
5364
  for (const dep of this._deps) {
5358
- dep.up?.(messages, options);
5365
+ dep.up?.(messages, { internal: true });
5359
5366
  }
5360
5367
  }
5361
5368
  unsubscribe() {
5362
5369
  this._disconnect();
5363
5370
  }
5364
- // --- Private methods ---
5365
- _downToSinks(messages) {
5366
- if (this._sinks == null) return;
5367
- if (typeof this._sinks === "function") {
5368
- this._sinks(messages);
5369
- return;
5370
- }
5371
- const snapshot = [...this._sinks];
5372
- for (const sink of snapshot) {
5373
- sink(messages);
5374
- }
5375
- }
5376
- _handleLocalLifecycle(messages) {
5377
- for (const m of messages) {
5378
- const t = m[0];
5379
- if (t === DATA) this._cached = m[1];
5380
- if (t === INVALIDATE) {
5381
- this._cached = NO_VALUE;
5382
- this._status = "dirty";
5383
- }
5384
- if (t === DATA) {
5385
- this._status = "settled";
5386
- } else if (t === RESOLVED) {
5387
- this._status = "resolved";
5388
- } else if (t === DIRTY) {
5389
- this._status = "dirty";
5390
- } else if (t === COMPLETE) {
5391
- this._status = "completed";
5392
- this._terminal = true;
5393
- } else if (t === ERROR) {
5394
- this._status = "errored";
5395
- this._terminal = true;
5396
- }
5397
- if (t === TEARDOWN) {
5398
- if (this._resetOnTeardown) this._cached = NO_VALUE;
5399
- try {
5400
- this._propagateToMeta(t);
5401
- } finally {
5402
- this._disconnect();
5403
- }
5404
- }
5405
- if (t !== TEARDOWN && propagatesToMeta(t)) {
5406
- this._propagateToMeta(t);
5407
- }
5408
- }
5409
- }
5410
- /** Propagate a signal to all companion meta nodes (best-effort). */
5411
- _propagateToMeta(t) {
5412
- for (const metaNode of Object.values(this.meta)) {
5413
- try {
5414
- metaNode.down([[t]], { internal: true });
5415
- } catch {
5416
- }
5417
- }
5418
- }
5419
- _downAutoValue(value) {
5420
- const wasDirty = this._status === "dirty";
5421
- let unchanged;
5422
- try {
5423
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
5424
- } catch (eqErr) {
5425
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
5426
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
5427
- this._downInternal([[ERROR, wrapped]]);
5428
- return;
5429
- }
5430
- if (unchanged) {
5431
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
5432
- return;
5433
- }
5434
- this._cached = value;
5435
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
5436
- }
5437
- _connect() {
5438
- if (this._connected) return;
5439
- this._connected = true;
5440
- this._status = "settled";
5441
- this._dirtyBits.clear();
5442
- this._settledBits.clear();
5443
- this._completeBits.clear();
5371
+ // --- Activation hooks ---
5372
+ _onActivate() {
5444
5373
  this._runFn();
5445
5374
  }
5375
+ _doDeactivate() {
5376
+ this._disconnect();
5377
+ }
5446
5378
  _disconnect() {
5447
- if (!this._connected) return;
5448
5379
  for (const unsub of this._depUnsubs) unsub();
5449
5380
  this._depUnsubs = [];
5450
5381
  this._deps = [];
5451
5382
  this._depIndexMap.clear();
5452
- this._dirtyBits.clear();
5453
- this._settledBits.clear();
5454
- this._completeBits.clear();
5455
- this._connected = false;
5383
+ this._depDirtyBits.clear();
5384
+ this._depSettledBits.clear();
5385
+ this._depCompleteBits.clear();
5386
+ this._cached = NO_VALUE;
5456
5387
  this._status = "disconnected";
5457
5388
  }
5389
+ // --- Fn execution with rewire buffer ---
5458
5390
  _runFn() {
5459
5391
  if (this._terminal && !this._resubscribable) return;
5460
- if (this._rewiring) return;
5461
- const trackedDeps = [];
5462
- const trackedSet = /* @__PURE__ */ new Set();
5463
- const get = (dep) => {
5464
- if (!trackedSet.has(dep)) {
5465
- trackedSet.add(dep);
5466
- trackedDeps.push(dep);
5467
- }
5468
- return dep.get();
5469
- };
5392
+ if (this._running) return;
5393
+ this._running = true;
5394
+ this._rerunCount = 0;
5395
+ let result;
5470
5396
  try {
5471
- const depValues = [];
5472
- for (const dep of this._deps) {
5473
- depValues.push(dep.get());
5474
- }
5475
- this._inspectorHook?.({ kind: "run", depValues });
5476
- const result = this._fn(get);
5477
- this._rewire(trackedDeps);
5478
- if (result === void 0) return;
5479
- this._downAutoValue(result);
5480
- } catch (err) {
5481
- this._downInternal([[ERROR, err]]);
5397
+ for (; ; ) {
5398
+ const trackedDeps = [];
5399
+ const trackedValuesMap = /* @__PURE__ */ new Map();
5400
+ const trackedSet = /* @__PURE__ */ new Set();
5401
+ const get = (dep) => {
5402
+ if (!trackedSet.has(dep)) {
5403
+ trackedSet.add(dep);
5404
+ trackedDeps.push(dep);
5405
+ trackedValuesMap.set(dep, dep.get());
5406
+ }
5407
+ return dep.get();
5408
+ };
5409
+ this._trackedValues = trackedValuesMap;
5410
+ const depValues = [];
5411
+ for (const dep of this._deps) depValues.push(dep.get());
5412
+ this._emitInspectorHook({ kind: "run", depValues });
5413
+ try {
5414
+ result = this._fn(get);
5415
+ } catch (err) {
5416
+ const errMsg = err instanceof Error ? err.message : String(err);
5417
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
5418
+ cause: err
5419
+ });
5420
+ this._downInternal([[ERROR, wrapped]]);
5421
+ return;
5422
+ }
5423
+ this._rewiring = true;
5424
+ this._bufferedDepMessages = [];
5425
+ try {
5426
+ this._rewire(trackedDeps);
5427
+ } finally {
5428
+ this._rewiring = false;
5429
+ }
5430
+ let needsRerun = false;
5431
+ for (const entry of this._bufferedDepMessages) {
5432
+ for (const msg of entry.msgs) {
5433
+ if (msg[0] === DATA) {
5434
+ const dep = this._deps[entry.index];
5435
+ const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
5436
+ const actualValue = msg[1];
5437
+ if (!this._equals(trackedValue, actualValue)) {
5438
+ needsRerun = true;
5439
+ break;
5440
+ }
5441
+ }
5442
+ }
5443
+ if (needsRerun) break;
5444
+ }
5445
+ if (needsRerun) {
5446
+ this._rerunCount += 1;
5447
+ if (this._rerunCount > MAX_RERUN) {
5448
+ this._bufferedDepMessages = [];
5449
+ this._downInternal([
5450
+ [
5451
+ ERROR,
5452
+ new Error(
5453
+ `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
5454
+ )
5455
+ ]
5456
+ ]);
5457
+ return;
5458
+ }
5459
+ this._bufferedDepMessages = [];
5460
+ continue;
5461
+ }
5462
+ const drain = this._bufferedDepMessages;
5463
+ this._bufferedDepMessages = [];
5464
+ for (const entry of drain) {
5465
+ for (const msg of entry.msgs) {
5466
+ this._updateMasksForMessage(entry.index, msg);
5467
+ }
5468
+ }
5469
+ this._depDirtyBits.clear();
5470
+ this._depSettledBits.clear();
5471
+ break;
5472
+ }
5473
+ } finally {
5474
+ this._running = false;
5482
5475
  }
5476
+ this._downAutoValue(result);
5483
5477
  }
5484
5478
  _rewire(newDeps) {
5485
- this._rewiring = true;
5486
- try {
5487
- const oldMap = this._depIndexMap;
5488
- const newMap = /* @__PURE__ */ new Map();
5489
- const newUnsubs = [];
5490
- for (let i = 0; i < newDeps.length; i++) {
5491
- const dep = newDeps[i];
5492
- newMap.set(dep, i);
5493
- const oldIdx = oldMap.get(dep);
5494
- if (oldIdx !== void 0) {
5495
- newUnsubs.push(this._depUnsubs[oldIdx]);
5496
- this._depUnsubs[oldIdx] = () => {
5497
- };
5498
- } else {
5499
- const idx = i;
5500
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
5501
- newUnsubs.push(unsub);
5502
- }
5479
+ const oldMap = this._depIndexMap;
5480
+ const newMap = /* @__PURE__ */ new Map();
5481
+ const newUnsubs = [];
5482
+ for (let i = 0; i < newDeps.length; i++) {
5483
+ const dep = newDeps[i];
5484
+ newMap.set(dep, i);
5485
+ const oldIdx = oldMap.get(dep);
5486
+ if (oldIdx !== void 0) {
5487
+ newUnsubs.push(this._depUnsubs[oldIdx]);
5488
+ this._depUnsubs[oldIdx] = () => {
5489
+ };
5490
+ } else {
5491
+ const idx = i;
5492
+ const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
5493
+ newUnsubs.push(unsub);
5503
5494
  }
5504
- for (const [dep, oldIdx] of oldMap) {
5505
- if (!newMap.has(dep)) {
5506
- this._depUnsubs[oldIdx]();
5507
- }
5495
+ }
5496
+ for (const [dep, oldIdx] of oldMap) {
5497
+ if (!newMap.has(dep)) {
5498
+ this._depUnsubs[oldIdx]();
5508
5499
  }
5509
- this._deps = newDeps;
5510
- this._depUnsubs = newUnsubs;
5511
- this._depIndexMap = newMap;
5512
- this._dirtyBits.clear();
5513
- this._settledBits.clear();
5514
- const newCompleteBits = /* @__PURE__ */ new Set();
5515
- for (const oldIdx of this._completeBits) {
5516
- const dep = [...oldMap.entries()].find(([, idx]) => idx === oldIdx)?.[0];
5517
- if (dep && newMap.has(dep)) {
5500
+ }
5501
+ this._deps = newDeps;
5502
+ this._depUnsubs = newUnsubs;
5503
+ this._depIndexMap = newMap;
5504
+ this._depDirtyBits.clear();
5505
+ this._depSettledBits.clear();
5506
+ const newCompleteBits = /* @__PURE__ */ new Set();
5507
+ for (const oldIdx of this._depCompleteBits) {
5508
+ for (const [dep, idx] of oldMap) {
5509
+ if (idx === oldIdx && newMap.has(dep)) {
5518
5510
  newCompleteBits.add(newMap.get(dep));
5511
+ break;
5519
5512
  }
5520
5513
  }
5521
- this._completeBits = newCompleteBits;
5522
- } finally {
5523
- this._rewiring = false;
5524
5514
  }
5515
+ this._depCompleteBits = newCompleteBits;
5525
5516
  }
5517
+ // --- Dep message handling ---
5526
5518
  _handleDepMessages(index, messages) {
5527
- if (this._rewiring) return;
5519
+ if (this._rewiring) {
5520
+ this._bufferedDepMessages.push({ index, msgs: messages });
5521
+ return;
5522
+ }
5528
5523
  for (const msg of messages) {
5529
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
5524
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
5530
5525
  const t = msg[0];
5531
5526
  if (this._onMessage) {
5532
5527
  try {
5533
5528
  if (this._onMessage(msg, index, this._actions)) continue;
5534
5529
  } catch (err) {
5535
- this._downInternal([[ERROR, err]]);
5530
+ const errMsg = err instanceof Error ? err.message : String(err);
5531
+ const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
5532
+ cause: err
5533
+ });
5534
+ this._downInternal([[ERROR, wrapped]]);
5536
5535
  return;
5537
5536
  }
5538
5537
  }
5538
+ if (messageTier(t) < 1) continue;
5539
5539
  if (t === DIRTY) {
5540
- this._dirtyBits.add(index);
5541
- this._settledBits.delete(index);
5542
- if (this._dirtyBits.size === 1) {
5543
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
5540
+ const wasEmpty = this._depDirtyBits.size === 0;
5541
+ this._depDirtyBits.add(index);
5542
+ this._depSettledBits.delete(index);
5543
+ if (wasEmpty) {
5544
+ this._downInternal([[DIRTY]]);
5544
5545
  }
5545
5546
  continue;
5546
5547
  }
5547
5548
  if (t === DATA || t === RESOLVED) {
5548
- if (!this._dirtyBits.has(index)) {
5549
- this._dirtyBits.add(index);
5550
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
5549
+ if (!this._depDirtyBits.has(index)) {
5550
+ const wasEmpty = this._depDirtyBits.size === 0;
5551
+ this._depDirtyBits.add(index);
5552
+ if (wasEmpty) {
5553
+ this._downInternal([[DIRTY]]);
5554
+ }
5551
5555
  }
5552
- this._settledBits.add(index);
5556
+ this._depSettledBits.add(index);
5553
5557
  if (this._allDirtySettled()) {
5554
- this._dirtyBits.clear();
5555
- this._settledBits.clear();
5556
- this._runFn();
5558
+ this._depDirtyBits.clear();
5559
+ this._depSettledBits.clear();
5560
+ if (!this._running) {
5561
+ if (this._depValuesDifferFromTracked()) {
5562
+ this._runFn();
5563
+ }
5564
+ }
5557
5565
  }
5558
5566
  continue;
5559
5567
  }
5560
5568
  if (t === COMPLETE) {
5561
- this._completeBits.add(index);
5562
- this._dirtyBits.delete(index);
5563
- this._settledBits.delete(index);
5569
+ this._depCompleteBits.add(index);
5570
+ this._depDirtyBits.delete(index);
5571
+ this._depSettledBits.delete(index);
5564
5572
  if (this._allDirtySettled()) {
5565
- this._dirtyBits.clear();
5566
- this._settledBits.clear();
5567
- this._runFn();
5573
+ this._depDirtyBits.clear();
5574
+ this._depSettledBits.clear();
5575
+ if (!this._running) this._runFn();
5568
5576
  }
5569
- if (this._autoComplete && this._completeBits.size >= this._deps.length && this._deps.length > 0) {
5577
+ if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
5570
5578
  this._downInternal([[COMPLETE]]);
5571
5579
  }
5572
5580
  continue;
@@ -5582,13 +5590,46 @@ var DynamicNodeImpl = class {
5582
5590
  this._downInternal([msg]);
5583
5591
  }
5584
5592
  }
5593
+ /**
5594
+ * Update dep masks for a message without triggering `_runFn` — used
5595
+ * during post-rewire drain so the wave state is consistent with the
5596
+ * buffered activation cascade without recursing.
5597
+ */
5598
+ _updateMasksForMessage(index, msg) {
5599
+ const t = msg[0];
5600
+ if (t === DIRTY) {
5601
+ this._depDirtyBits.add(index);
5602
+ this._depSettledBits.delete(index);
5603
+ } else if (t === DATA || t === RESOLVED) {
5604
+ this._depDirtyBits.add(index);
5605
+ this._depSettledBits.add(index);
5606
+ } else if (t === COMPLETE) {
5607
+ this._depCompleteBits.add(index);
5608
+ this._depDirtyBits.delete(index);
5609
+ this._depSettledBits.delete(index);
5610
+ }
5611
+ }
5585
5612
  _allDirtySettled() {
5586
- if (this._dirtyBits.size === 0) return false;
5587
- for (const idx of this._dirtyBits) {
5588
- if (!this._settledBits.has(idx)) return false;
5613
+ if (this._depDirtyBits.size === 0) return false;
5614
+ for (const idx of this._depDirtyBits) {
5615
+ if (!this._depSettledBits.has(idx)) return false;
5589
5616
  }
5590
5617
  return true;
5591
5618
  }
5619
+ /**
5620
+ * True if any current dep value differs from what the last `_runFn`
5621
+ * saw via `get()`. Used to suppress redundant re-runs when deferred
5622
+ * handshake messages arrive after `_rewire` for a dep whose value
5623
+ * already matches `_trackedValues`.
5624
+ */
5625
+ _depValuesDifferFromTracked() {
5626
+ for (const dep of this._deps) {
5627
+ const current = dep.get();
5628
+ const tracked = this._trackedValues.get(dep);
5629
+ if (!this._equals(current, tracked)) return true;
5630
+ }
5631
+ return false;
5632
+ }
5592
5633
  };
5593
5634
 
5594
5635
  // src/extra/operators.ts
@@ -5651,9 +5692,12 @@ function reduce(source, reducer, seed, opts) {
5651
5692
  }
5652
5693
  function take(source, count, opts) {
5653
5694
  if (count <= 0) {
5695
+ let completed = false;
5654
5696
  return node(
5655
5697
  [source],
5656
5698
  (_d, a) => {
5699
+ if (completed) return void 0;
5700
+ completed = true;
5657
5701
  a.down([[COMPLETE]]);
5658
5702
  return void 0;
5659
5703
  },
@@ -5661,8 +5705,15 @@ function take(source, count, opts) {
5661
5705
  ...operatorOpts3(opts),
5662
5706
  completeWhenDepsComplete: false,
5663
5707
  onMessage(msg, _i, a) {
5664
- if (msg[0] === COMPLETE) {
5708
+ if (msg[0] === START && !completed) {
5709
+ completed = true;
5665
5710
  a.down([[COMPLETE]]);
5711
+ return true;
5712
+ }
5713
+ if (msg[0] === COMPLETE && !completed) {
5714
+ completed = true;
5715
+ a.down([[COMPLETE]]);
5716
+ return true;
5666
5717
  }
5667
5718
  return true;
5668
5719
  }
@@ -5844,21 +5895,6 @@ function find(source, predicate, opts) {
5844
5895
  function elementAt(source, index, opts) {
5845
5896
  return take(skip(source, index, opts), 1, opts);
5846
5897
  }
5847
- function startWith(source, initial, opts) {
5848
- let prepended = false;
5849
- return node(
5850
- [source],
5851
- ([v], a) => {
5852
- if (!prepended) {
5853
- prepended = true;
5854
- a.emit(initial);
5855
- }
5856
- a.emit(v);
5857
- return void 0;
5858
- },
5859
- operatorOpts3(opts)
5860
- );
5861
- }
5862
5898
  function tap(source, fnOrObserver, opts) {
5863
5899
  if (typeof fnOrObserver === "function") {
5864
5900
  return derived(
@@ -6194,6 +6230,7 @@ function forwardInner(inner, a, onInnerComplete) {
6194
6230
  let sawError = false;
6195
6231
  const out = [];
6196
6232
  for (const m of msgs) {
6233
+ if (messageTier(m[0]) < 1) continue;
6197
6234
  if (m[0] === DATA) emitted = true;
6198
6235
  if (m[0] === COMPLETE) sawComplete = true;
6199
6236
  else {
@@ -6678,7 +6715,7 @@ function sample(source, notifier, opts) {
6678
6715
  if (terminated) return true;
6679
6716
  const t = msg[0];
6680
6717
  const tier = messageTier(t);
6681
- if (tier >= 3) {
6718
+ if (tier >= 4) {
6682
6719
  if (t === ERROR) {
6683
6720
  terminated = true;
6684
6721
  a.down([msg]);
@@ -6694,6 +6731,7 @@ function sample(source, notifier, opts) {
6694
6731
  a.down([msg]);
6695
6732
  return true;
6696
6733
  }
6734
+ terminated = true;
6697
6735
  a.down([msg]);
6698
6736
  return true;
6699
6737
  }
@@ -7350,7 +7388,7 @@ function verifiable(source, verifyFn, opts) {
7350
7388
  }
7351
7389
  return { node: sourceNode, verified, trigger: triggerNode };
7352
7390
  }
7353
- function keepalive(node2) {
7391
+ function keepalive2(node2) {
7354
7392
  node2.subscribe(() => void 0);
7355
7393
  }
7356
7394
  function mapFromSnapshot(snapshot) {
@@ -7435,8 +7473,8 @@ function distill(source, extractFn, opts) {
7435
7473
  return packed;
7436
7474
  });
7437
7475
  const size = derived([store.entries], ([snapshot]) => mapFromSnapshot(snapshot).size);
7438
- keepalive(compact);
7439
- keepalive(size);
7476
+ keepalive2(compact);
7477
+ keepalive2(size);
7440
7478
  return { store, compact, size };
7441
7479
  }
7442
7480
 
@@ -8007,7 +8045,7 @@ function workerBridge(target, opts) {
8007
8045
  for (const m of msgs) {
8008
8046
  const type = m[0];
8009
8047
  if (type === DATA) continue;
8010
- if (knownMessageTypes.includes(type) && messageTier(type) < 2) continue;
8048
+ if (isLocalOnly(type)) continue;
8011
8049
  if (type === ERROR) {
8012
8050
  transport.post({
8013
8051
  t: "e",
@@ -8133,7 +8171,7 @@ function workerSelf(target, opts) {
8133
8171
  for (const m of msgs) {
8134
8172
  const type = m[0];
8135
8173
  if (type === DATA) continue;
8136
- if (knownMessageTypes.includes(type) && messageTier(type) < 2) continue;
8174
+ if (isLocalOnly(type)) continue;
8137
8175
  if (type === ERROR) {
8138
8176
  transport.post({
8139
8177
  t: "e",
@@ -8277,6 +8315,7 @@ function workerSelf(target, opts) {
8277
8315
  find,
8278
8316
  first,
8279
8317
  firstValueFrom,
8318
+ firstWhere,
8280
8319
  flatMap,
8281
8320
  forEach,
8282
8321
  fromAny,
@@ -8312,6 +8351,7 @@ function workerSelf(target, opts) {
8312
8351
  fromWebhook,
8313
8352
  globToRegExp,
8314
8353
  interval,
8354
+ keepalive,
8315
8355
  last,
8316
8356
  linear,
8317
8357
  logSlice,
@@ -8333,6 +8373,7 @@ function workerSelf(target, opts) {
8333
8373
  pubsub,
8334
8374
  race,
8335
8375
  rateLimiter,
8376
+ reactiveCounter,
8336
8377
  reactiveIndex,
8337
8378
  reactiveList,
8338
8379
  reactiveLog,
@@ -8354,7 +8395,6 @@ function workerSelf(target, opts) {
8354
8395
  shareReplay,
8355
8396
  signalToName,
8356
8397
  skip,
8357
- startWith,
8358
8398
  switchMap,
8359
8399
  take,
8360
8400
  takeUntil,