@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
@@ -105,6 +105,7 @@ module.exports = __toCommonJS(nestjs_exports);
105
105
  var import_rxjs = require("rxjs");
106
106
 
107
107
  // src/core/messages.ts
108
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
108
109
  var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
109
110
  var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
110
111
  var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
@@ -114,13 +115,27 @@ var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
114
115
  var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
115
116
  var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
116
117
  var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
118
+ var knownMessageTypes = [
119
+ START,
120
+ DATA,
121
+ DIRTY,
122
+ RESOLVED,
123
+ INVALIDATE,
124
+ PAUSE,
125
+ RESUME,
126
+ TEARDOWN,
127
+ COMPLETE,
128
+ ERROR
129
+ ];
130
+ var knownMessageSet = new Set(knownMessageTypes);
117
131
  function messageTier(t) {
118
- if (t === DIRTY || t === INVALIDATE) return 0;
119
- if (t === PAUSE || t === RESUME) return 1;
120
- if (t === DATA || t === RESOLVED) return 2;
121
- if (t === COMPLETE || t === ERROR) return 3;
122
- if (t === TEARDOWN) return 4;
123
- return 0;
132
+ if (t === START) return 0;
133
+ if (t === DIRTY || t === INVALIDATE) return 1;
134
+ if (t === PAUSE || t === RESUME) return 2;
135
+ if (t === DATA || t === RESOLVED) return 3;
136
+ if (t === COMPLETE || t === ERROR) return 4;
137
+ if (t === TEARDOWN) return 5;
138
+ return 1;
124
139
  }
125
140
  function isPhase2Message(msg) {
126
141
  const t = msg[0];
@@ -306,6 +321,82 @@ function normalizeActor(actor) {
306
321
  };
307
322
  }
308
323
 
324
+ // src/core/guard.ts
325
+ var GuardDenied = class extends Error {
326
+ actor;
327
+ action;
328
+ nodeName;
329
+ /**
330
+ * @param details - Actor, action, and optional node name for the denial.
331
+ * @param message - Optional override for the default error message.
332
+ */
333
+ constructor(details, message) {
334
+ super(
335
+ message ?? `GuardDenied: action "${String(details.action)}" denied for actor type "${String(details.actor.type)}"`
336
+ );
337
+ this.name = "GuardDenied";
338
+ this.actor = details.actor;
339
+ this.action = details.action;
340
+ this.nodeName = details.nodeName;
341
+ }
342
+ /** Qualified registry path when known (roadmap diagnostics: same as {@link nodeName}). */
343
+ get node() {
344
+ return this.nodeName;
345
+ }
346
+ };
347
+ function normalizeActions(action) {
348
+ if (Array.isArray(action)) {
349
+ return [...action];
350
+ }
351
+ return [action];
352
+ }
353
+ function matchesActions(set, action) {
354
+ return set.has(action) || set.has("*");
355
+ }
356
+ function policy(build) {
357
+ const rules = [];
358
+ const allow = (action, opts) => {
359
+ rules.push({
360
+ kind: "allow",
361
+ actions: new Set(normalizeActions(action)),
362
+ where: opts?.where ?? (() => true)
363
+ });
364
+ };
365
+ const deny = (action, opts) => {
366
+ rules.push({
367
+ kind: "deny",
368
+ actions: new Set(normalizeActions(action)),
369
+ where: opts?.where ?? (() => true)
370
+ });
371
+ };
372
+ build(allow, deny);
373
+ return (actor, action) => {
374
+ let denied = false;
375
+ let allowed = false;
376
+ for (const r of rules) {
377
+ if (!matchesActions(r.actions, action)) continue;
378
+ if (!r.where(actor)) continue;
379
+ if (r.kind === "deny") {
380
+ denied = true;
381
+ } else {
382
+ allowed = true;
383
+ }
384
+ }
385
+ if (denied) return false;
386
+ return allowed;
387
+ };
388
+ }
389
+ var STANDARD_WRITE_TYPES = ["human", "llm", "wallet", "system"];
390
+ function accessHintForGuard(guard) {
391
+ const allowed = STANDARD_WRITE_TYPES.filter((t) => guard({ type: t, id: "" }, "write"));
392
+ if (allowed.length === 0) return "restricted";
393
+ if (allowed.includes("human") && allowed.includes("llm") && allowed.every((t) => t === "human" || t === "llm" || t === "system")) {
394
+ return "both";
395
+ }
396
+ if (allowed.length === 1) return allowed[0];
397
+ return allowed.join("+");
398
+ }
399
+
309
400
  // src/core/batch.ts
310
401
  var MAX_DRAIN_ITERATIONS = 1e3;
311
402
  var batchDepth = 0;
@@ -462,14 +553,14 @@ function _downSequential(sink, messages, phase = 2) {
462
553
  const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
463
554
  for (const msg of messages) {
464
555
  const tier = messageTier(msg[0]);
465
- if (tier === 2) {
556
+ if (tier === 3) {
466
557
  if (isBatching()) {
467
558
  const m = msg;
468
559
  dataQueue.push(() => sink([m]));
469
560
  } else {
470
561
  sink([msg]);
471
562
  }
472
- } else if (tier >= 3) {
563
+ } else if (tier >= 4) {
473
564
  if (isBatching()) {
474
565
  const m = msg;
475
566
  pendingPhase3.push(() => sink([m]));
@@ -482,82 +573,6 @@ function _downSequential(sink, messages, phase = 2) {
482
573
  }
483
574
  }
484
575
 
485
- // src/core/guard.ts
486
- var GuardDenied = class extends Error {
487
- actor;
488
- action;
489
- nodeName;
490
- /**
491
- * @param details - Actor, action, and optional node name for the denial.
492
- * @param message - Optional override for the default error message.
493
- */
494
- constructor(details, message) {
495
- super(
496
- message ?? `GuardDenied: action "${String(details.action)}" denied for actor type "${String(details.actor.type)}"`
497
- );
498
- this.name = "GuardDenied";
499
- this.actor = details.actor;
500
- this.action = details.action;
501
- this.nodeName = details.nodeName;
502
- }
503
- /** Qualified registry path when known (roadmap diagnostics: same as {@link nodeName}). */
504
- get node() {
505
- return this.nodeName;
506
- }
507
- };
508
- function normalizeActions(action) {
509
- if (Array.isArray(action)) {
510
- return [...action];
511
- }
512
- return [action];
513
- }
514
- function matchesActions(set, action) {
515
- return set.has(action) || set.has("*");
516
- }
517
- function policy(build) {
518
- const rules = [];
519
- const allow = (action, opts) => {
520
- rules.push({
521
- kind: "allow",
522
- actions: new Set(normalizeActions(action)),
523
- where: opts?.where ?? (() => true)
524
- });
525
- };
526
- const deny = (action, opts) => {
527
- rules.push({
528
- kind: "deny",
529
- actions: new Set(normalizeActions(action)),
530
- where: opts?.where ?? (() => true)
531
- });
532
- };
533
- build(allow, deny);
534
- return (actor, action) => {
535
- let denied = false;
536
- let allowed = false;
537
- for (const r of rules) {
538
- if (!matchesActions(r.actions, action)) continue;
539
- if (!r.where(actor)) continue;
540
- if (r.kind === "deny") {
541
- denied = true;
542
- } else {
543
- allowed = true;
544
- }
545
- }
546
- if (denied) return false;
547
- return allowed;
548
- };
549
- }
550
- var STANDARD_WRITE_TYPES = ["human", "llm", "wallet", "system"];
551
- function accessHintForGuard(guard) {
552
- const allowed = STANDARD_WRITE_TYPES.filter((t) => guard({ type: t, id: "" }, "write"));
553
- if (allowed.length === 0) return "restricted";
554
- if (allowed.includes("human") && allowed.includes("llm") && allowed.every((t) => t === "human" || t === "llm" || t === "system")) {
555
- return "both";
556
- }
557
- if (allowed.length === 1) return allowed[0];
558
- return allowed.join("+");
559
- }
560
-
561
576
  // src/core/versioning.ts
562
577
  var import_node_crypto = require("crypto");
563
578
  function canonicalizeForHash(value) {
@@ -610,10 +625,24 @@ function advanceVersion(info, newValue, hashFn) {
610
625
  }
611
626
  }
612
627
 
613
- // src/core/node.ts
628
+ // src/core/node-base.ts
614
629
  var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
615
630
  var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
616
- function createIntBitSet() {
631
+ var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
632
+ var isCleanupFn = (value) => typeof value === "function";
633
+ function statusAfterMessage(status, msg) {
634
+ const t = msg[0];
635
+ if (t === DIRTY) return "dirty";
636
+ if (t === DATA) return "settled";
637
+ if (t === RESOLVED) return "resolved";
638
+ if (t === COMPLETE) return "completed";
639
+ if (t === ERROR) return "errored";
640
+ if (t === INVALIDATE) return "dirty";
641
+ if (t === TEARDOWN) return "disconnected";
642
+ return status;
643
+ }
644
+ function createIntBitSet(size) {
645
+ const fullMask = size >= 32 ? -1 : ~(-1 << size);
617
646
  let bits = 0;
618
647
  return {
619
648
  set(i) {
@@ -626,7 +655,8 @@ function createIntBitSet() {
626
655
  return (bits & 1 << i) !== 0;
627
656
  },
628
657
  covers(other) {
629
- return (bits & other._bits()) === other._bits();
658
+ const otherBits = other._bits();
659
+ return (bits & otherBits) === otherBits;
630
660
  },
631
661
  any() {
632
662
  return bits !== 0;
@@ -634,6 +664,9 @@ function createIntBitSet() {
634
664
  reset() {
635
665
  bits = 0;
636
666
  },
667
+ setAll() {
668
+ bits = fullMask;
669
+ },
637
670
  _bits() {
638
671
  return bits;
639
672
  }
@@ -641,6 +674,8 @@ function createIntBitSet() {
641
674
  }
642
675
  function createArrayBitSet(size) {
643
676
  const words = new Uint32Array(Math.ceil(size / 32));
677
+ const lastBits = size % 32;
678
+ const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
644
679
  return {
645
680
  set(i) {
646
681
  words[i >>> 5] |= 1 << (i & 31);
@@ -667,130 +702,103 @@ function createArrayBitSet(size) {
667
702
  reset() {
668
703
  words.fill(0);
669
704
  },
705
+ setAll() {
706
+ for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
707
+ if (words.length > 0) words[words.length - 1] = lastWordMask;
708
+ },
670
709
  _words: words
671
710
  };
672
711
  }
673
712
  function createBitSet(size) {
674
- return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
713
+ return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
675
714
  }
676
- var isNodeArray = (value) => Array.isArray(value);
677
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
678
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
679
- var isCleanupFn = (value) => typeof value === "function";
680
- var statusAfterMessage = (status, msg) => {
681
- const t = msg[0];
682
- if (t === DIRTY) return "dirty";
683
- if (t === DATA) return "settled";
684
- if (t === RESOLVED) return "resolved";
685
- if (t === COMPLETE) return "completed";
686
- if (t === ERROR) return "errored";
687
- if (t === INVALIDATE) return "dirty";
688
- if (t === TEARDOWN) return "disconnected";
689
- return status;
690
- };
691
- var NodeImpl = class {
692
- // --- Configuration (set once, never reassigned) ---
715
+ var NodeBase = class {
716
+ // --- Identity (set once) ---
693
717
  _optsName;
694
718
  _registryName;
695
- /** @internal read by {@link describeNode} before inference. */
719
+ /** @internal Read by `describeNode` before inference. */
696
720
  _describeKind;
697
721
  meta;
698
- _deps;
699
- _fn;
700
- _opts;
722
+ // --- Options ---
701
723
  _equals;
724
+ _resubscribable;
725
+ _resetOnTeardown;
726
+ _onResubscribe;
702
727
  _onMessage;
703
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
728
+ /** @internal Read by `describeNode` for `accessHintForGuard`. */
704
729
  _guard;
730
+ /** @internal Subclasses update this through {@link _recordMutation}. */
705
731
  _lastMutation;
706
- _hasDeps;
707
- _autoComplete;
708
- _isSingleDep;
709
- // --- Mutable state ---
732
+ // --- Versioning ---
733
+ _hashFn;
734
+ _versioning;
735
+ // --- Lifecycle state ---
736
+ /** @internal Read by `describeNode` and `graph.ts`. */
710
737
  _cached;
738
+ /** @internal Read externally via `get status()`. */
711
739
  _status;
712
740
  _terminal = false;
713
- _connected = false;
714
- _producerStarted = false;
715
- _connecting = false;
716
- _manualEmitUsed = false;
741
+ _active = false;
742
+ // --- Sink storage ---
743
+ /** @internal Read by `graph/profile.ts` for subscriber counts. */
717
744
  _sinkCount = 0;
718
745
  _singleDepSinkCount = 0;
719
- // --- Object/collection state ---
720
- _depDirtyMask;
721
- _depSettledMask;
722
- _depCompleteMask;
723
- _allDepsCompleteMask;
724
- _lastDepValues;
725
- _cleanup;
726
- _sinks = null;
727
746
  _singleDepSinks = /* @__PURE__ */ new WeakSet();
728
- _upstreamUnsubs = [];
747
+ _sinks = null;
748
+ // --- Actions + bound helpers ---
729
749
  _actions;
730
750
  _boundDownToSinks;
751
+ // --- Inspector hook (Graph observability) ---
731
752
  _inspectorHook;
732
- _versioning;
733
- _hashFn;
734
- constructor(deps, fn, opts) {
735
- this._deps = deps;
736
- this._fn = fn;
737
- this._opts = opts;
753
+ constructor(opts) {
738
754
  this._optsName = opts.name;
739
755
  this._describeKind = opts.describeKind;
740
756
  this._equals = opts.equals ?? Object.is;
757
+ this._resubscribable = opts.resubscribable ?? false;
758
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
759
+ this._onResubscribe = opts.onResubscribe;
741
760
  this._onMessage = opts.onMessage;
742
761
  this._guard = opts.guard;
743
- this._hasDeps = deps.length > 0;
744
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
745
- this._isSingleDep = deps.length === 1 && fn != null;
746
762
  this._cached = "initial" in opts ? opts.initial : NO_VALUE;
747
- this._status = this._hasDeps ? "disconnected" : "settled";
763
+ this._status = "disconnected";
748
764
  this._hashFn = opts.versioningHash ?? defaultHash;
749
765
  this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
750
766
  id: opts.versioningId,
751
767
  hash: this._hashFn
752
768
  }) : void 0;
753
- this._depDirtyMask = createBitSet(deps.length);
754
- this._depSettledMask = createBitSet(deps.length);
755
- this._depCompleteMask = createBitSet(deps.length);
756
- this._allDepsCompleteMask = createBitSet(deps.length);
757
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
758
769
  const meta = {};
759
770
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
760
- meta[k] = node({
761
- initial: v,
762
- name: `${opts.name ?? "node"}:meta:${k}`,
763
- describeKind: "state",
764
- ...opts.guard != null ? { guard: opts.guard } : {}
765
- });
771
+ meta[k] = this._createMetaNode(k, v, opts);
766
772
  }
767
773
  Object.freeze(meta);
768
774
  this.meta = meta;
769
775
  const self = this;
770
776
  this._actions = {
771
777
  down(messages) {
772
- self._manualEmitUsed = true;
778
+ self._onManualEmit();
773
779
  self._downInternal(messages);
774
780
  },
775
781
  emit(value) {
776
- self._manualEmitUsed = true;
782
+ self._onManualEmit();
777
783
  self._downAutoValue(value);
778
784
  },
779
785
  up(messages) {
780
786
  self._upInternal(messages);
781
787
  }
782
788
  };
783
- this.down = this.down.bind(this);
784
- this.up = this.up.bind(this);
785
789
  this._boundDownToSinks = this._downToSinks.bind(this);
786
790
  }
791
+ /**
792
+ * Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
793
+ * {@link NodeImpl} overrides to set `_manualEmitUsed`.
794
+ */
795
+ _onManualEmit() {
796
+ }
797
+ // --- Identity getters ---
787
798
  get name() {
788
799
  return this._registryName ?? this._optsName;
789
800
  }
790
- /**
791
- * When a node is registered with {@link Graph.add} without an options `name`,
792
- * the graph assigns the registry local name for introspection (parity with graphrefly-py).
793
- */
801
+ /** @internal Assigned by `Graph.add` when registered without an options `name`. */
794
802
  _assignRegistryName(localName) {
795
803
  if (this._optsName !== void 0 || this._registryName !== void 0) return;
796
804
  this._registryName = localName;
@@ -808,7 +816,10 @@ var NodeImpl = class {
808
816
  }
809
817
  };
810
818
  }
811
- // --- Public interface (Node<T>) ---
819
+ /** @internal Used by subclasses to surface inspector events. */
820
+ _emitInspectorHook(event) {
821
+ this._inspectorHook?.(event);
822
+ }
812
823
  get status() {
813
824
  return this._status;
814
825
  }
@@ -818,15 +829,7 @@ var NodeImpl = class {
818
829
  get v() {
819
830
  return this._versioning;
820
831
  }
821
- /**
822
- * Retroactively apply versioning to a node that was created without it.
823
- * No-op if versioning is already enabled.
824
- *
825
- * Version starts at 0 regardless of prior DATA emissions — it tracks
826
- * changes from the moment versioning is enabled, not historical ones.
827
- *
828
- * @internal — used by {@link Graph.setVersioning}.
829
- */
832
+ /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
830
833
  _applyVersioning(level, opts) {
831
834
  if (this._versioning != null) return;
832
835
  this._hashFn = opts?.hash ?? this._hashFn;
@@ -846,6 +849,7 @@ var NodeImpl = class {
846
849
  if (this._guard == null) return true;
847
850
  return this._guard(normalizeActor(actor), "observe");
848
851
  }
852
+ // --- Public transport ---
849
853
  get() {
850
854
  return this._cached === NO_VALUE ? void 0 : this._cached;
851
855
  }
@@ -858,43 +862,25 @@ var NodeImpl = class {
858
862
  if (!this._guard(actor, action)) {
859
863
  throw new GuardDenied({ actor, action, nodeName: this.name });
860
864
  }
861
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
865
+ this._recordMutation(actor);
862
866
  }
863
867
  this._downInternal(messages);
864
868
  }
865
- _downInternal(messages) {
866
- if (messages.length === 0) return;
867
- let lifecycleMessages = messages;
868
- let sinkMessages = messages;
869
- if (this._terminal && !this._opts.resubscribable) {
870
- const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
871
- if (terminalPassthrough.length === 0) return;
872
- lifecycleMessages = terminalPassthrough;
873
- sinkMessages = terminalPassthrough;
874
- }
875
- this._handleLocalLifecycle(lifecycleMessages);
876
- if (this._canSkipDirty()) {
877
- let hasPhase2 = false;
878
- for (let i = 0; i < sinkMessages.length; i++) {
879
- const t = sinkMessages[i][0];
880
- if (t === DATA || t === RESOLVED) {
881
- hasPhase2 = true;
882
- break;
883
- }
884
- }
885
- if (hasPhase2) {
886
- const filtered = [];
887
- for (let i = 0; i < sinkMessages.length; i++) {
888
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
889
- }
890
- if (filtered.length > 0) {
891
- downWithBatch(this._boundDownToSinks, filtered);
892
- }
893
- return;
894
- }
895
- }
896
- downWithBatch(this._boundDownToSinks, sinkMessages);
869
+ /** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
870
+ _recordMutation(actor) {
871
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
872
+ }
873
+ /**
874
+ * At-most-once deactivation guard. Both TEARDOWN (eager) and
875
+ * unsubscribe-body (lazy) call this. The `_active` flag ensures
876
+ * `_doDeactivate` runs exactly once per activation cycle.
877
+ */
878
+ _onDeactivate() {
879
+ if (!this._active) return;
880
+ this._active = false;
881
+ this._doDeactivate();
897
882
  }
883
+ // --- Subscribe (uniform across node shapes) ---
898
884
  subscribe(sink, hints) {
899
885
  if (hints?.actor != null && this._guard != null) {
900
886
  const actor = normalizeActor(hints.actor);
@@ -902,17 +888,21 @@ var NodeImpl = class {
902
888
  throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
903
889
  }
904
890
  }
905
- if (this._terminal && this._opts.resubscribable) {
891
+ if (this._terminal && this._resubscribable) {
906
892
  this._terminal = false;
907
893
  this._cached = NO_VALUE;
908
- this._status = this._hasDeps ? "disconnected" : "settled";
909
- this._opts.onResubscribe?.();
894
+ this._status = "disconnected";
895
+ this._onResubscribe?.();
910
896
  }
911
897
  this._sinkCount += 1;
912
898
  if (hints?.singleDep) {
913
899
  this._singleDepSinkCount += 1;
914
900
  this._singleDepSinks.add(sink);
915
901
  }
902
+ if (!this._terminal) {
903
+ const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
904
+ downWithBatch(sink, startMessages);
905
+ }
916
906
  if (this._sinks == null) {
917
907
  this._sinks = sink;
918
908
  } else if (typeof this._sinks === "function") {
@@ -920,10 +910,12 @@ var NodeImpl = class {
920
910
  } else {
921
911
  this._sinks.add(sink);
922
912
  }
923
- if (this._hasDeps) {
924
- this._connectUpstream();
925
- } else if (this._fn) {
926
- this._startProducer();
913
+ if (this._sinkCount === 1 && !this._terminal) {
914
+ this._active = true;
915
+ this._onActivate();
916
+ }
917
+ if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
918
+ this._status = "pending";
927
919
  }
928
920
  let removed = false;
929
921
  return () => {
@@ -947,39 +939,49 @@ var NodeImpl = class {
947
939
  }
948
940
  }
949
941
  if (this._sinks == null) {
950
- this._disconnectUpstream();
951
- this._stopProducer();
942
+ this._onDeactivate();
952
943
  }
953
944
  };
954
945
  }
955
- up(messages, options) {
956
- if (!this._hasDeps) return;
957
- if (!options?.internal && this._guard != null) {
958
- const actor = normalizeActor(options?.actor);
959
- if (!this._guard(actor, "write")) {
960
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
961
- }
962
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
946
+ // --- Down pipeline ---
947
+ /**
948
+ * Core outgoing dispatch. Applies terminal filter + local lifecycle
949
+ * update, then hands messages to `downWithBatch` for tier-aware delivery.
950
+ */
951
+ _downInternal(messages) {
952
+ if (messages.length === 0) return;
953
+ let sinkMessages = messages;
954
+ if (this._terminal && !this._resubscribable) {
955
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
956
+ if (pass.length === 0) return;
957
+ sinkMessages = pass;
963
958
  }
964
- for (const dep of this._deps) {
965
- if (options === void 0) {
966
- dep.up?.(messages);
967
- } else {
968
- dep.up?.(messages, options);
959
+ this._handleLocalLifecycle(sinkMessages);
960
+ if (this._canSkipDirty()) {
961
+ let hasPhase2 = false;
962
+ for (let i = 0; i < sinkMessages.length; i++) {
963
+ const t = sinkMessages[i][0];
964
+ if (t === DATA || t === RESOLVED) {
965
+ hasPhase2 = true;
966
+ break;
967
+ }
968
+ }
969
+ if (hasPhase2) {
970
+ const filtered = [];
971
+ for (let i = 0; i < sinkMessages.length; i++) {
972
+ if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
973
+ }
974
+ if (filtered.length > 0) {
975
+ downWithBatch(this._boundDownToSinks, filtered);
976
+ }
977
+ return;
969
978
  }
970
979
  }
980
+ downWithBatch(this._boundDownToSinks, sinkMessages);
971
981
  }
972
- _upInternal(messages) {
973
- if (!this._hasDeps) return;
974
- for (const dep of this._deps) {
975
- dep.up?.(messages, { internal: true });
976
- }
977
- }
978
- unsubscribe() {
979
- if (!this._hasDeps) return;
980
- this._disconnectUpstream();
982
+ _canSkipDirty() {
983
+ return this._sinkCount === 1 && this._singleDepSinkCount === 1;
981
984
  }
982
- // --- Private methods (prototype, _ prefix) ---
983
985
  _downToSinks(messages) {
984
986
  if (this._sinks == null) return;
985
987
  if (typeof this._sinks === "function") {
@@ -991,6 +993,11 @@ var NodeImpl = class {
991
993
  sink(messages);
992
994
  }
993
995
  }
996
+ /**
997
+ * Update `_cached`, `_status`, `_terminal` from message batch before
998
+ * delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
999
+ * {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
1000
+ */
994
1001
  _handleLocalLifecycle(messages) {
995
1002
  for (const m of messages) {
996
1003
  const t = m[0];
@@ -1004,28 +1011,22 @@ var NodeImpl = class {
1004
1011
  }
1005
1012
  }
1006
1013
  if (t === INVALIDATE) {
1007
- const cleanupFn = this._cleanup;
1008
- this._cleanup = void 0;
1009
- cleanupFn?.();
1014
+ this._onInvalidate();
1010
1015
  this._cached = NO_VALUE;
1011
- this._lastDepValues = void 0;
1012
1016
  }
1013
1017
  this._status = statusAfterMessage(this._status, m);
1014
1018
  if (t === COMPLETE || t === ERROR) {
1015
1019
  this._terminal = true;
1016
1020
  }
1017
1021
  if (t === TEARDOWN) {
1018
- if (this._opts.resetOnTeardown) {
1022
+ if (this._resetOnTeardown) {
1019
1023
  this._cached = NO_VALUE;
1020
1024
  }
1021
- const teardownCleanup = this._cleanup;
1022
- this._cleanup = void 0;
1023
- teardownCleanup?.();
1025
+ this._onTeardown();
1024
1026
  try {
1025
1027
  this._propagateToMeta(t);
1026
1028
  } finally {
1027
- this._disconnectUpstream();
1028
- this._stopProducer();
1029
+ this._onDeactivate();
1029
1030
  }
1030
1031
  }
1031
1032
  if (t !== TEARDOWN && propagatesToMeta(t)) {
@@ -1033,7 +1034,20 @@ var NodeImpl = class {
1033
1034
  }
1034
1035
  }
1035
1036
  }
1036
- /** Propagate a signal to all companion meta nodes (best-effort). */
1037
+ /**
1038
+ * Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
1039
+ * cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
1040
+ * drop `_lastDepValues` so the next wave re-runs fn.
1041
+ */
1042
+ _onInvalidate() {
1043
+ }
1044
+ /**
1045
+ * Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
1046
+ * {@link NodeImpl} uses this to run any pending cleanup fn.
1047
+ */
1048
+ _onTeardown() {
1049
+ }
1050
+ /** Forward a signal to all companion meta nodes (best-effort). */
1037
1051
  _propagateToMeta(t) {
1038
1052
  for (const metaNode of Object.values(this.meta)) {
1039
1053
  try {
@@ -1042,9 +1056,10 @@ var NodeImpl = class {
1042
1056
  }
1043
1057
  }
1044
1058
  }
1045
- _canSkipDirty() {
1046
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
1047
- }
1059
+ /**
1060
+ * Frame a computed value into the right protocol messages and dispatch
1061
+ * via `_downInternal`. Used by `_runFn` and `actions.emit`.
1062
+ */
1048
1063
  _downAutoValue(value) {
1049
1064
  const wasDirty = this._status === "dirty";
1050
1065
  let unchanged;
@@ -1052,7 +1067,9 @@ var NodeImpl = class {
1052
1067
  unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
1053
1068
  } catch (eqErr) {
1054
1069
  const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
1055
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
1070
+ const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
1071
+ cause: eqErr
1072
+ });
1056
1073
  this._downInternal([[ERROR, wrapped]]);
1057
1074
  return;
1058
1075
  }
@@ -1062,89 +1079,173 @@ var NodeImpl = class {
1062
1079
  }
1063
1080
  this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
1064
1081
  }
1065
- _runFn() {
1066
- if (!this._fn) return;
1067
- if (this._terminal && !this._opts.resubscribable) return;
1068
- if (this._connecting) return;
1069
- try {
1070
- const n = this._deps.length;
1071
- const depValues = new Array(n);
1072
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1073
- const prev = this._lastDepValues;
1074
- if (n > 0 && prev != null && prev.length === n) {
1075
- let allSame = true;
1076
- for (let i = 0; i < n; i++) {
1077
- if (!Object.is(depValues[i], prev[i])) {
1078
- allSame = false;
1079
- break;
1080
- }
1081
- }
1082
- if (allSame) {
1083
- if (this._status === "dirty") {
1084
- this._downInternal([[RESOLVED]]);
1085
- }
1086
- return;
1087
- }
1088
- }
1089
- const prevCleanup = this._cleanup;
1090
- this._cleanup = void 0;
1091
- prevCleanup?.();
1092
- this._manualEmitUsed = false;
1093
- this._lastDepValues = depValues;
1094
- this._inspectorHook?.({ kind: "run", depValues });
1095
- const out = this._fn(depValues, this._actions);
1096
- if (isCleanupResult(out)) {
1097
- this._cleanup = out.cleanup;
1098
- if (this._manualEmitUsed) return;
1099
- if ("value" in out) {
1100
- this._downAutoValue(out.value);
1101
- }
1102
- return;
1082
+ };
1083
+
1084
+ // src/core/node.ts
1085
+ var NodeImpl = class extends NodeBase {
1086
+ // --- Dep configuration (set once) ---
1087
+ _deps;
1088
+ _fn;
1089
+ _opts;
1090
+ _hasDeps;
1091
+ _isSingleDep;
1092
+ _autoComplete;
1093
+ // --- Wave tracking masks ---
1094
+ _depDirtyMask;
1095
+ _depSettledMask;
1096
+ _depCompleteMask;
1097
+ _allDepsCompleteMask;
1098
+ // --- Identity-skip optimization ---
1099
+ _lastDepValues;
1100
+ _cleanup;
1101
+ // --- Upstream bookkeeping ---
1102
+ _upstreamUnsubs = [];
1103
+ // --- Fn behavior flag ---
1104
+ /** @internal Read by `describeNode` to infer `"operator"` label. */
1105
+ _manualEmitUsed = false;
1106
+ constructor(deps, fn, opts) {
1107
+ super(opts);
1108
+ this._deps = deps;
1109
+ this._fn = fn;
1110
+ this._opts = opts;
1111
+ this._hasDeps = deps.length > 0;
1112
+ this._isSingleDep = deps.length === 1 && fn != null;
1113
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
1114
+ if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
1115
+ this._status = "settled";
1116
+ }
1117
+ this._depDirtyMask = createBitSet(deps.length);
1118
+ this._depSettledMask = createBitSet(deps.length);
1119
+ this._depCompleteMask = createBitSet(deps.length);
1120
+ this._allDepsCompleteMask = createBitSet(deps.length);
1121
+ for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
1122
+ this.down = this.down.bind(this);
1123
+ this.up = this.up.bind(this);
1124
+ }
1125
+ // --- Meta node factory (called from base constructor) ---
1126
+ _createMetaNode(key, initialValue, opts) {
1127
+ return node({
1128
+ initial: initialValue,
1129
+ name: `${opts.name ?? "node"}:meta:${key}`,
1130
+ describeKind: "state",
1131
+ ...opts.guard != null ? { guard: opts.guard } : {}
1132
+ });
1133
+ }
1134
+ // --- Manual emit tracker (set by actions.down / actions.emit) ---
1135
+ _onManualEmit() {
1136
+ this._manualEmitUsed = true;
1137
+ }
1138
+ // --- Up / unsubscribe ---
1139
+ up(messages, options) {
1140
+ if (!this._hasDeps) return;
1141
+ if (!options?.internal && this._guard != null) {
1142
+ const actor = normalizeActor(options?.actor);
1143
+ if (!this._guard(actor, "write")) {
1144
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1103
1145
  }
1104
- if (isCleanupFn(out)) {
1105
- this._cleanup = out;
1106
- return;
1146
+ this._recordMutation(actor);
1147
+ }
1148
+ for (const dep of this._deps) {
1149
+ if (options === void 0) {
1150
+ dep.up?.(messages);
1151
+ } else {
1152
+ dep.up?.(messages, options);
1107
1153
  }
1108
- if (this._manualEmitUsed) return;
1109
- if (out === void 0) return;
1110
- this._downAutoValue(out);
1111
- } catch (err) {
1112
- const errMsg = err instanceof Error ? err.message : String(err);
1113
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1114
- this._downInternal([[ERROR, wrapped]]);
1115
1154
  }
1116
1155
  }
1117
- _onDepDirty(index) {
1118
- const wasDirty = this._depDirtyMask.has(index);
1119
- this._depDirtyMask.set(index);
1120
- this._depSettledMask.clear(index);
1121
- if (!wasDirty) {
1122
- this._downInternal([[DIRTY]]);
1156
+ _upInternal(messages) {
1157
+ if (!this._hasDeps) return;
1158
+ for (const dep of this._deps) {
1159
+ dep.up?.(messages, { internal: true });
1123
1160
  }
1124
1161
  }
1125
- _onDepSettled(index) {
1126
- if (!this._depDirtyMask.has(index)) {
1127
- this._onDepDirty(index);
1162
+ unsubscribe() {
1163
+ if (!this._hasDeps) return;
1164
+ this._disconnectUpstream();
1165
+ }
1166
+ // --- Activation (first-subscriber / last-subscriber hooks) ---
1167
+ _onActivate() {
1168
+ if (this._hasDeps) {
1169
+ this._connectUpstream();
1170
+ return;
1128
1171
  }
1129
- this._depSettledMask.set(index);
1130
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1131
- this._depDirtyMask.reset();
1132
- this._depSettledMask.reset();
1172
+ if (this._fn) {
1133
1173
  this._runFn();
1174
+ return;
1134
1175
  }
1135
1176
  }
1136
- _maybeCompleteFromDeps() {
1137
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1138
- this._downInternal([[COMPLETE]]);
1177
+ _doDeactivate() {
1178
+ this._disconnectUpstream();
1179
+ const cleanup = this._cleanup;
1180
+ this._cleanup = void 0;
1181
+ cleanup?.();
1182
+ if (this._fn != null) {
1183
+ this._cached = NO_VALUE;
1184
+ this._lastDepValues = void 0;
1185
+ }
1186
+ if (this._hasDeps || this._fn != null) {
1187
+ this._status = "disconnected";
1188
+ }
1189
+ }
1190
+ // --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
1191
+ _onInvalidate() {
1192
+ const cleanup = this._cleanup;
1193
+ this._cleanup = void 0;
1194
+ cleanup?.();
1195
+ this._lastDepValues = void 0;
1196
+ }
1197
+ _onTeardown() {
1198
+ const cleanup = this._cleanup;
1199
+ this._cleanup = void 0;
1200
+ cleanup?.();
1201
+ }
1202
+ // --- Upstream connect / disconnect ---
1203
+ _connectUpstream() {
1204
+ if (!this._hasDeps) return;
1205
+ if (this._upstreamUnsubs.length > 0) return;
1206
+ this._depDirtyMask.setAll();
1207
+ this._depSettledMask.reset();
1208
+ this._depCompleteMask.reset();
1209
+ const depValuesBefore = this._lastDepValues;
1210
+ const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1211
+ for (let i = 0; i < this._deps.length; i += 1) {
1212
+ const dep = this._deps[i];
1213
+ this._upstreamUnsubs.push(
1214
+ dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1215
+ );
1216
+ }
1217
+ if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
1218
+ this._runFn();
1139
1219
  }
1140
1220
  }
1221
+ _disconnectUpstream() {
1222
+ if (this._upstreamUnsubs.length === 0) return;
1223
+ for (const unsub of this._upstreamUnsubs.splice(0)) {
1224
+ unsub();
1225
+ }
1226
+ this._depDirtyMask.reset();
1227
+ this._depSettledMask.reset();
1228
+ this._depCompleteMask.reset();
1229
+ }
1230
+ // --- Wave handling ---
1141
1231
  _handleDepMessages(index, messages) {
1142
1232
  for (const msg of messages) {
1143
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1233
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1144
1234
  const t = msg[0];
1145
1235
  if (this._onMessage) {
1146
1236
  try {
1147
- if (this._onMessage(msg, index, this._actions)) continue;
1237
+ const consumed = this._onMessage(msg, index, this._actions);
1238
+ if (consumed) {
1239
+ if (t === START) {
1240
+ this._depDirtyMask.clear(index);
1241
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1242
+ this._depDirtyMask.reset();
1243
+ this._depSettledMask.reset();
1244
+ this._runFn();
1245
+ }
1246
+ }
1247
+ continue;
1248
+ }
1148
1249
  } catch (err) {
1149
1250
  const errMsg = err instanceof Error ? err.message : String(err);
1150
1251
  const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
@@ -1154,6 +1255,7 @@ var NodeImpl = class {
1154
1255
  return;
1155
1256
  }
1156
1257
  }
1258
+ if (messageTier(t) < 1) continue;
1157
1259
  if (!this._fn) {
1158
1260
  if (t === COMPLETE && this._deps.length > 1) {
1159
1261
  this._depCompleteMask.set(index);
@@ -1197,53 +1299,85 @@ var NodeImpl = class {
1197
1299
  this._downInternal([msg]);
1198
1300
  }
1199
1301
  }
1200
- _connectUpstream() {
1201
- if (!this._hasDeps || this._connected) return;
1202
- this._connected = true;
1203
- this._depDirtyMask.reset();
1204
- this._depSettledMask.reset();
1205
- this._depCompleteMask.reset();
1206
- this._status = "settled";
1207
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1208
- this._connecting = true;
1209
- try {
1210
- for (let i = 0; i < this._deps.length; i += 1) {
1211
- const dep = this._deps[i];
1212
- this._upstreamUnsubs.push(
1213
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1214
- );
1215
- }
1216
- } finally {
1217
- this._connecting = false;
1302
+ _onDepDirty(index) {
1303
+ const wasDirty = this._depDirtyMask.has(index);
1304
+ this._depDirtyMask.set(index);
1305
+ this._depSettledMask.clear(index);
1306
+ if (!wasDirty) {
1307
+ this._downInternal([[DIRTY]]);
1308
+ }
1309
+ }
1310
+ _onDepSettled(index) {
1311
+ if (!this._depDirtyMask.has(index)) {
1312
+ this._onDepDirty(index);
1218
1313
  }
1219
- if (this._fn) {
1314
+ this._depSettledMask.set(index);
1315
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1316
+ this._depDirtyMask.reset();
1317
+ this._depSettledMask.reset();
1220
1318
  this._runFn();
1221
1319
  }
1222
1320
  }
1223
- _stopProducer() {
1224
- if (!this._producerStarted) return;
1225
- this._producerStarted = false;
1226
- const producerCleanup = this._cleanup;
1227
- this._cleanup = void 0;
1228
- producerCleanup?.();
1229
- }
1230
- _startProducer() {
1231
- if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
1232
- this._producerStarted = true;
1233
- this._runFn();
1321
+ _maybeCompleteFromDeps() {
1322
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1323
+ this._downInternal([[COMPLETE]]);
1324
+ }
1234
1325
  }
1235
- _disconnectUpstream() {
1236
- if (!this._connected) return;
1237
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1238
- unsub();
1326
+ // --- Fn execution ---
1327
+ _runFn() {
1328
+ if (!this._fn) return;
1329
+ if (this._terminal && !this._resubscribable) return;
1330
+ try {
1331
+ const n = this._deps.length;
1332
+ const depValues = new Array(n);
1333
+ for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1334
+ const prev = this._lastDepValues;
1335
+ if (n > 0 && prev != null && prev.length === n) {
1336
+ let allSame = true;
1337
+ for (let i = 0; i < n; i++) {
1338
+ if (!Object.is(depValues[i], prev[i])) {
1339
+ allSame = false;
1340
+ break;
1341
+ }
1342
+ }
1343
+ if (allSame) {
1344
+ if (this._status === "dirty") {
1345
+ this._downInternal([[RESOLVED]]);
1346
+ }
1347
+ return;
1348
+ }
1349
+ }
1350
+ const prevCleanup = this._cleanup;
1351
+ this._cleanup = void 0;
1352
+ prevCleanup?.();
1353
+ this._manualEmitUsed = false;
1354
+ this._lastDepValues = depValues;
1355
+ this._emitInspectorHook({ kind: "run", depValues });
1356
+ const out = this._fn(depValues, this._actions);
1357
+ if (isCleanupResult(out)) {
1358
+ this._cleanup = out.cleanup;
1359
+ if (this._manualEmitUsed) return;
1360
+ if ("value" in out) {
1361
+ this._downAutoValue(out.value);
1362
+ }
1363
+ return;
1364
+ }
1365
+ if (isCleanupFn(out)) {
1366
+ this._cleanup = out;
1367
+ return;
1368
+ }
1369
+ if (this._manualEmitUsed) return;
1370
+ if (out === void 0) return;
1371
+ this._downAutoValue(out);
1372
+ } catch (err) {
1373
+ const errMsg = err instanceof Error ? err.message : String(err);
1374
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1375
+ this._downInternal([[ERROR, wrapped]]);
1239
1376
  }
1240
- this._connected = false;
1241
- this._depDirtyMask.reset();
1242
- this._depSettledMask.reset();
1243
- this._depCompleteMask.reset();
1244
- this._status = "disconnected";
1245
1377
  }
1246
1378
  };
1379
+ var isNodeArray = (value) => Array.isArray(value);
1380
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1247
1381
  function node(depsOrFn, fnOrOpts, optsArg) {
1248
1382
  const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1249
1383
  const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
@@ -1380,6 +1514,10 @@ function fromCron(expr, opts) {
1380
1514
  { ...sourceOpts(rest), name: rest.name ?? `cron:${expr}` }
1381
1515
  );
1382
1516
  }
1517
+ function keepalive(n) {
1518
+ return n.subscribe(() => {
1519
+ });
1520
+ }
1383
1521
 
1384
1522
  // src/compat/nestjs/explorer.ts
1385
1523
  var scheduleSeq = 0;
@@ -2058,261 +2196,78 @@ function fromHeader(headerName = "x-graphrefly-actor") {
2058
2196
  return (context) => {
2059
2197
  const req = context.switchToHttp().getRequest();
2060
2198
  const raw = req?.headers?.[headerName.toLowerCase()];
2061
- if (typeof raw !== "string" || raw.length === 0) return void 0;
2062
- try {
2063
- return JSON.parse(raw);
2064
- } catch {
2065
- return void 0;
2066
- }
2067
- };
2068
- }
2069
- function getActor(req) {
2070
- const actor = req?.[ACTOR_KEY];
2071
- return actor != null ? normalizeActor(actor) : DEFAULT_ACTOR;
2072
- }
2073
- var GraphReflyGuardImpl = class {
2074
- constructor(extractor) {
2075
- this.extractor = extractor;
2076
- }
2077
- canActivate(context) {
2078
- const actor = normalizeActor(this.extractor(context));
2079
- const req = context.switchToHttp().getRequest();
2080
- if (req != null) {
2081
- req[ACTOR_KEY] = actor;
2082
- }
2083
- return true;
2084
- }
2085
- };
2086
- function GraphReflyGuard(extractor) {
2087
- return new GraphReflyGuardImpl(extractor ?? fromJwtPayload());
2088
- }
2089
-
2090
- // src/compat/nestjs/module.ts
2091
- var import_common2 = require("@nestjs/common");
2092
- var import_core2 = require("@nestjs/core");
2093
-
2094
- // src/core/dynamic-node.ts
2095
- var DynamicNodeImpl = class {
2096
- _optsName;
2097
- _registryName;
2098
- _describeKind;
2099
- meta;
2100
- _fn;
2101
- _equals;
2102
- _resubscribable;
2103
- _resetOnTeardown;
2104
- _autoComplete;
2105
- _onMessage;
2106
- _onResubscribe;
2107
- /** @internal — read by {@link describeNode} for `accessHintForGuard`. */
2108
- _guard;
2109
- _lastMutation;
2110
- _inspectorHook;
2111
- // Sink tracking
2112
- _sinkCount = 0;
2113
- _singleDepSinkCount = 0;
2114
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
2115
- // Actions object (for onMessage handler)
2116
- _actions;
2117
- _boundDownToSinks;
2118
- // Mutable state
2119
- _cached = NO_VALUE;
2120
- _status = "disconnected";
2121
- _terminal = false;
2122
- _connected = false;
2123
- _rewiring = false;
2124
- // re-entrancy guard
2125
- // Dynamic deps tracking
2126
- _deps = [];
2127
- _depUnsubs = [];
2128
- _depIndexMap = /* @__PURE__ */ new Map();
2129
- // node → index in _deps
2130
- _dirtyBits = /* @__PURE__ */ new Set();
2131
- _settledBits = /* @__PURE__ */ new Set();
2132
- _completeBits = /* @__PURE__ */ new Set();
2133
- // Sinks
2134
- _sinks = null;
2135
- constructor(fn, opts) {
2136
- this._fn = fn;
2137
- this._optsName = opts.name;
2138
- this._describeKind = opts.describeKind;
2139
- this._equals = opts.equals ?? Object.is;
2140
- this._resubscribable = opts.resubscribable ?? false;
2141
- this._resetOnTeardown = opts.resetOnTeardown ?? false;
2142
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
2143
- this._onMessage = opts.onMessage;
2144
- this._onResubscribe = opts.onResubscribe;
2145
- this._guard = opts.guard;
2146
- this._inspectorHook = void 0;
2147
- const meta = {};
2148
- for (const [k, v] of Object.entries(opts.meta ?? {})) {
2149
- meta[k] = node({
2150
- initial: v,
2151
- name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
2152
- describeKind: "state",
2153
- ...opts.guard != null ? { guard: opts.guard } : {}
2154
- });
2155
- }
2156
- Object.freeze(meta);
2157
- this.meta = meta;
2158
- const self = this;
2159
- this._actions = {
2160
- down(messages) {
2161
- self._downInternal(messages);
2162
- },
2163
- emit(value) {
2164
- self._downAutoValue(value);
2165
- },
2166
- up(messages) {
2167
- for (const dep of self._deps) {
2168
- dep.up?.(messages, { internal: true });
2169
- }
2170
- }
2171
- };
2172
- this._boundDownToSinks = this._downToSinks.bind(this);
2173
- }
2174
- get name() {
2175
- return this._registryName ?? this._optsName;
2176
- }
2177
- /** @internal */
2178
- _assignRegistryName(localName) {
2179
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
2180
- this._registryName = localName;
2181
- }
2182
- /**
2183
- * @internal Attach/remove inspector hook for graph-level observability.
2184
- * Returns a disposer that restores the previous hook.
2185
- */
2186
- _setInspectorHook(hook) {
2187
- const prev = this._inspectorHook;
2188
- this._inspectorHook = hook;
2189
- return () => {
2190
- if (this._inspectorHook === hook) {
2191
- this._inspectorHook = prev;
2192
- }
2193
- };
2194
- }
2195
- get status() {
2196
- return this._status;
2197
- }
2198
- get lastMutation() {
2199
- return this._lastMutation;
2200
- }
2201
- /** Versioning not yet supported on DynamicNodeImpl. */
2202
- get v() {
2203
- return void 0;
2204
- }
2205
- hasGuard() {
2206
- return this._guard != null;
2207
- }
2208
- allowsObserve(actor) {
2209
- if (this._guard == null) return true;
2210
- return this._guard(normalizeActor(actor), "observe");
2211
- }
2212
- get() {
2213
- return this._cached === NO_VALUE ? void 0 : this._cached;
2214
- }
2215
- down(messages, options) {
2216
- if (messages.length === 0) return;
2217
- if (!options?.internal && this._guard != null) {
2218
- const actor = normalizeActor(options?.actor);
2219
- const delivery = options?.delivery ?? "write";
2220
- const action = delivery === "signal" ? "signal" : "write";
2221
- if (!this._guard(actor, action)) {
2222
- throw new GuardDenied({ actor, action, nodeName: this.name });
2223
- }
2224
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
2225
- }
2226
- this._downInternal(messages);
2227
- }
2228
- _downInternal(messages) {
2229
- if (messages.length === 0) return;
2230
- let sinkMessages = messages;
2231
- if (this._terminal && !this._resubscribable) {
2232
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
2233
- if (pass.length === 0) return;
2234
- sinkMessages = pass;
2235
- }
2236
- this._handleLocalLifecycle(sinkMessages);
2237
- if (this._canSkipDirty()) {
2238
- let hasPhase2 = false;
2239
- for (let i = 0; i < sinkMessages.length; i++) {
2240
- const t = sinkMessages[i][0];
2241
- if (t === DATA || t === RESOLVED) {
2242
- hasPhase2 = true;
2243
- break;
2244
- }
2245
- }
2246
- if (hasPhase2) {
2247
- const filtered = [];
2248
- for (let i = 0; i < sinkMessages.length; i++) {
2249
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
2250
- }
2251
- if (filtered.length > 0) {
2252
- downWithBatch(this._boundDownToSinks, filtered);
2253
- }
2254
- return;
2255
- }
2199
+ if (typeof raw !== "string" || raw.length === 0) return void 0;
2200
+ try {
2201
+ return JSON.parse(raw);
2202
+ } catch {
2203
+ return void 0;
2256
2204
  }
2257
- downWithBatch(this._boundDownToSinks, sinkMessages);
2258
- }
2259
- _canSkipDirty() {
2260
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
2205
+ };
2206
+ }
2207
+ function getActor(req) {
2208
+ const actor = req?.[ACTOR_KEY];
2209
+ return actor != null ? normalizeActor(actor) : DEFAULT_ACTOR;
2210
+ }
2211
+ var GraphReflyGuardImpl = class {
2212
+ constructor(extractor) {
2213
+ this.extractor = extractor;
2261
2214
  }
2262
- subscribe(sink, hints) {
2263
- if (hints?.actor != null && this._guard != null) {
2264
- const actor = normalizeActor(hints.actor);
2265
- if (!this._guard(actor, "observe")) {
2266
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
2267
- }
2268
- }
2269
- if (this._terminal && this._resubscribable) {
2270
- this._terminal = false;
2271
- this._cached = NO_VALUE;
2272
- this._status = "disconnected";
2273
- this._onResubscribe?.();
2274
- }
2275
- this._sinkCount += 1;
2276
- if (hints?.singleDep) {
2277
- this._singleDepSinkCount += 1;
2278
- this._singleDepSinks.add(sink);
2279
- }
2280
- if (this._sinks == null) {
2281
- this._sinks = sink;
2282
- } else if (typeof this._sinks === "function") {
2283
- this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
2284
- } else {
2285
- this._sinks.add(sink);
2286
- }
2287
- if (!this._connected) {
2288
- this._connect();
2215
+ canActivate(context) {
2216
+ const actor = normalizeActor(this.extractor(context));
2217
+ const req = context.switchToHttp().getRequest();
2218
+ if (req != null) {
2219
+ req[ACTOR_KEY] = actor;
2289
2220
  }
2290
- let removed = false;
2291
- return () => {
2292
- if (removed) return;
2293
- removed = true;
2294
- this._sinkCount -= 1;
2295
- if (this._singleDepSinks.has(sink)) {
2296
- this._singleDepSinkCount -= 1;
2297
- this._singleDepSinks.delete(sink);
2298
- }
2299
- if (this._sinks == null) return;
2300
- if (typeof this._sinks === "function") {
2301
- if (this._sinks === sink) this._sinks = null;
2302
- } else {
2303
- this._sinks.delete(sink);
2304
- if (this._sinks.size === 1) {
2305
- const [only] = this._sinks;
2306
- this._sinks = only;
2307
- } else if (this._sinks.size === 0) {
2308
- this._sinks = null;
2309
- }
2310
- }
2311
- if (this._sinks == null) {
2312
- this._disconnect();
2313
- }
2314
- };
2221
+ return true;
2222
+ }
2223
+ };
2224
+ function GraphReflyGuard(extractor) {
2225
+ return new GraphReflyGuardImpl(extractor ?? fromJwtPayload());
2226
+ }
2227
+
2228
+ // src/compat/nestjs/module.ts
2229
+ var import_common2 = require("@nestjs/common");
2230
+ var import_core2 = require("@nestjs/core");
2231
+
2232
+ // src/core/dynamic-node.ts
2233
+ var MAX_RERUN = 16;
2234
+ var DynamicNodeImpl = class extends NodeBase {
2235
+ _fn;
2236
+ _autoComplete;
2237
+ // Dynamic deps tracking
2238
+ /** @internal Read by `describeNode`. */
2239
+ _deps = [];
2240
+ _depUnsubs = [];
2241
+ _depIndexMap = /* @__PURE__ */ new Map();
2242
+ _depDirtyBits = /* @__PURE__ */ new Set();
2243
+ _depSettledBits = /* @__PURE__ */ new Set();
2244
+ _depCompleteBits = /* @__PURE__ */ new Set();
2245
+ // Execution state
2246
+ _running = false;
2247
+ _rewiring = false;
2248
+ _bufferedDepMessages = [];
2249
+ _trackedValues = /* @__PURE__ */ new Map();
2250
+ _rerunCount = 0;
2251
+ constructor(fn, opts) {
2252
+ super(opts);
2253
+ this._fn = fn;
2254
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
2255
+ this.down = this.down.bind(this);
2256
+ this.up = this.up.bind(this);
2315
2257
  }
2258
+ _createMetaNode(key, initialValue, opts) {
2259
+ return node({
2260
+ initial: initialValue,
2261
+ name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
2262
+ describeKind: "state",
2263
+ ...opts.guard != null ? { guard: opts.guard } : {}
2264
+ });
2265
+ }
2266
+ /** Versioning not supported on DynamicNodeImpl (override base). */
2267
+ get v() {
2268
+ return void 0;
2269
+ }
2270
+ // --- Up / unsubscribe ---
2316
2271
  up(messages, options) {
2317
2272
  if (this._deps.length === 0) return;
2318
2273
  if (!options?.internal && this._guard != null) {
@@ -2320,221 +2275,227 @@ var DynamicNodeImpl = class {
2320
2275
  if (!this._guard(actor, "write")) {
2321
2276
  throw new GuardDenied({ actor, action: "write", nodeName: this.name });
2322
2277
  }
2323
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
2278
+ this._recordMutation(actor);
2324
2279
  }
2325
2280
  for (const dep of this._deps) {
2326
2281
  dep.up?.(messages, options);
2327
2282
  }
2328
2283
  }
2329
- unsubscribe() {
2330
- this._disconnect();
2331
- }
2332
- // --- Private methods ---
2333
- _downToSinks(messages) {
2334
- if (this._sinks == null) return;
2335
- if (typeof this._sinks === "function") {
2336
- this._sinks(messages);
2337
- return;
2338
- }
2339
- const snapshot = [...this._sinks];
2340
- for (const sink of snapshot) {
2341
- sink(messages);
2342
- }
2343
- }
2344
- _handleLocalLifecycle(messages) {
2345
- for (const m of messages) {
2346
- const t = m[0];
2347
- if (t === DATA) this._cached = m[1];
2348
- if (t === INVALIDATE) {
2349
- this._cached = NO_VALUE;
2350
- this._status = "dirty";
2351
- }
2352
- if (t === DATA) {
2353
- this._status = "settled";
2354
- } else if (t === RESOLVED) {
2355
- this._status = "resolved";
2356
- } else if (t === DIRTY) {
2357
- this._status = "dirty";
2358
- } else if (t === COMPLETE) {
2359
- this._status = "completed";
2360
- this._terminal = true;
2361
- } else if (t === ERROR) {
2362
- this._status = "errored";
2363
- this._terminal = true;
2364
- }
2365
- if (t === TEARDOWN) {
2366
- if (this._resetOnTeardown) this._cached = NO_VALUE;
2367
- try {
2368
- this._propagateToMeta(t);
2369
- } finally {
2370
- this._disconnect();
2371
- }
2372
- }
2373
- if (t !== TEARDOWN && propagatesToMeta(t)) {
2374
- this._propagateToMeta(t);
2375
- }
2376
- }
2377
- }
2378
- /** Propagate a signal to all companion meta nodes (best-effort). */
2379
- _propagateToMeta(t) {
2380
- for (const metaNode of Object.values(this.meta)) {
2381
- try {
2382
- metaNode.down([[t]], { internal: true });
2383
- } catch {
2384
- }
2284
+ _upInternal(messages) {
2285
+ for (const dep of this._deps) {
2286
+ dep.up?.(messages, { internal: true });
2385
2287
  }
2386
2288
  }
2387
- _downAutoValue(value) {
2388
- const wasDirty = this._status === "dirty";
2389
- let unchanged;
2390
- try {
2391
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
2392
- } catch (eqErr) {
2393
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
2394
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
2395
- this._downInternal([[ERROR, wrapped]]);
2396
- return;
2397
- }
2398
- if (unchanged) {
2399
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
2400
- return;
2401
- }
2402
- this._cached = value;
2403
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
2289
+ unsubscribe() {
2290
+ this._disconnect();
2404
2291
  }
2405
- _connect() {
2406
- if (this._connected) return;
2407
- this._connected = true;
2408
- this._status = "settled";
2409
- this._dirtyBits.clear();
2410
- this._settledBits.clear();
2411
- this._completeBits.clear();
2292
+ // --- Activation hooks ---
2293
+ _onActivate() {
2412
2294
  this._runFn();
2413
2295
  }
2296
+ _doDeactivate() {
2297
+ this._disconnect();
2298
+ }
2414
2299
  _disconnect() {
2415
- if (!this._connected) return;
2416
2300
  for (const unsub of this._depUnsubs) unsub();
2417
2301
  this._depUnsubs = [];
2418
2302
  this._deps = [];
2419
2303
  this._depIndexMap.clear();
2420
- this._dirtyBits.clear();
2421
- this._settledBits.clear();
2422
- this._completeBits.clear();
2423
- this._connected = false;
2304
+ this._depDirtyBits.clear();
2305
+ this._depSettledBits.clear();
2306
+ this._depCompleteBits.clear();
2307
+ this._cached = NO_VALUE;
2424
2308
  this._status = "disconnected";
2425
2309
  }
2310
+ // --- Fn execution with rewire buffer ---
2426
2311
  _runFn() {
2427
2312
  if (this._terminal && !this._resubscribable) return;
2428
- if (this._rewiring) return;
2429
- const trackedDeps = [];
2430
- const trackedSet = /* @__PURE__ */ new Set();
2431
- const get = (dep) => {
2432
- if (!trackedSet.has(dep)) {
2433
- trackedSet.add(dep);
2434
- trackedDeps.push(dep);
2435
- }
2436
- return dep.get();
2437
- };
2313
+ if (this._running) return;
2314
+ this._running = true;
2315
+ this._rerunCount = 0;
2316
+ let result;
2438
2317
  try {
2439
- const depValues = [];
2440
- for (const dep of this._deps) {
2441
- depValues.push(dep.get());
2442
- }
2443
- this._inspectorHook?.({ kind: "run", depValues });
2444
- const result = this._fn(get);
2445
- this._rewire(trackedDeps);
2446
- if (result === void 0) return;
2447
- this._downAutoValue(result);
2448
- } catch (err) {
2449
- this._downInternal([[ERROR, err]]);
2318
+ for (; ; ) {
2319
+ const trackedDeps = [];
2320
+ const trackedValuesMap = /* @__PURE__ */ new Map();
2321
+ const trackedSet = /* @__PURE__ */ new Set();
2322
+ const get = (dep) => {
2323
+ if (!trackedSet.has(dep)) {
2324
+ trackedSet.add(dep);
2325
+ trackedDeps.push(dep);
2326
+ trackedValuesMap.set(dep, dep.get());
2327
+ }
2328
+ return dep.get();
2329
+ };
2330
+ this._trackedValues = trackedValuesMap;
2331
+ const depValues = [];
2332
+ for (const dep of this._deps) depValues.push(dep.get());
2333
+ this._emitInspectorHook({ kind: "run", depValues });
2334
+ try {
2335
+ result = this._fn(get);
2336
+ } catch (err) {
2337
+ const errMsg = err instanceof Error ? err.message : String(err);
2338
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
2339
+ cause: err
2340
+ });
2341
+ this._downInternal([[ERROR, wrapped]]);
2342
+ return;
2343
+ }
2344
+ this._rewiring = true;
2345
+ this._bufferedDepMessages = [];
2346
+ try {
2347
+ this._rewire(trackedDeps);
2348
+ } finally {
2349
+ this._rewiring = false;
2350
+ }
2351
+ let needsRerun = false;
2352
+ for (const entry of this._bufferedDepMessages) {
2353
+ for (const msg of entry.msgs) {
2354
+ if (msg[0] === DATA) {
2355
+ const dep = this._deps[entry.index];
2356
+ const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
2357
+ const actualValue = msg[1];
2358
+ if (!this._equals(trackedValue, actualValue)) {
2359
+ needsRerun = true;
2360
+ break;
2361
+ }
2362
+ }
2363
+ }
2364
+ if (needsRerun) break;
2365
+ }
2366
+ if (needsRerun) {
2367
+ this._rerunCount += 1;
2368
+ if (this._rerunCount > MAX_RERUN) {
2369
+ this._bufferedDepMessages = [];
2370
+ this._downInternal([
2371
+ [
2372
+ ERROR,
2373
+ new Error(
2374
+ `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
2375
+ )
2376
+ ]
2377
+ ]);
2378
+ return;
2379
+ }
2380
+ this._bufferedDepMessages = [];
2381
+ continue;
2382
+ }
2383
+ const drain = this._bufferedDepMessages;
2384
+ this._bufferedDepMessages = [];
2385
+ for (const entry of drain) {
2386
+ for (const msg of entry.msgs) {
2387
+ this._updateMasksForMessage(entry.index, msg);
2388
+ }
2389
+ }
2390
+ this._depDirtyBits.clear();
2391
+ this._depSettledBits.clear();
2392
+ break;
2393
+ }
2394
+ } finally {
2395
+ this._running = false;
2450
2396
  }
2397
+ this._downAutoValue(result);
2451
2398
  }
2452
2399
  _rewire(newDeps) {
2453
- this._rewiring = true;
2454
- try {
2455
- const oldMap = this._depIndexMap;
2456
- const newMap = /* @__PURE__ */ new Map();
2457
- const newUnsubs = [];
2458
- for (let i = 0; i < newDeps.length; i++) {
2459
- const dep = newDeps[i];
2460
- newMap.set(dep, i);
2461
- const oldIdx = oldMap.get(dep);
2462
- if (oldIdx !== void 0) {
2463
- newUnsubs.push(this._depUnsubs[oldIdx]);
2464
- this._depUnsubs[oldIdx] = () => {
2465
- };
2466
- } else {
2467
- const idx = i;
2468
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
2469
- newUnsubs.push(unsub);
2470
- }
2400
+ const oldMap = this._depIndexMap;
2401
+ const newMap = /* @__PURE__ */ new Map();
2402
+ const newUnsubs = [];
2403
+ for (let i = 0; i < newDeps.length; i++) {
2404
+ const dep = newDeps[i];
2405
+ newMap.set(dep, i);
2406
+ const oldIdx = oldMap.get(dep);
2407
+ if (oldIdx !== void 0) {
2408
+ newUnsubs.push(this._depUnsubs[oldIdx]);
2409
+ this._depUnsubs[oldIdx] = () => {
2410
+ };
2411
+ } else {
2412
+ const idx = i;
2413
+ const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
2414
+ newUnsubs.push(unsub);
2471
2415
  }
2472
- for (const [dep, oldIdx] of oldMap) {
2473
- if (!newMap.has(dep)) {
2474
- this._depUnsubs[oldIdx]();
2475
- }
2416
+ }
2417
+ for (const [dep, oldIdx] of oldMap) {
2418
+ if (!newMap.has(dep)) {
2419
+ this._depUnsubs[oldIdx]();
2476
2420
  }
2477
- this._deps = newDeps;
2478
- this._depUnsubs = newUnsubs;
2479
- this._depIndexMap = newMap;
2480
- this._dirtyBits.clear();
2481
- this._settledBits.clear();
2482
- const newCompleteBits = /* @__PURE__ */ new Set();
2483
- for (const oldIdx of this._completeBits) {
2484
- const dep = [...oldMap.entries()].find(([, idx]) => idx === oldIdx)?.[0];
2485
- if (dep && newMap.has(dep)) {
2421
+ }
2422
+ this._deps = newDeps;
2423
+ this._depUnsubs = newUnsubs;
2424
+ this._depIndexMap = newMap;
2425
+ this._depDirtyBits.clear();
2426
+ this._depSettledBits.clear();
2427
+ const newCompleteBits = /* @__PURE__ */ new Set();
2428
+ for (const oldIdx of this._depCompleteBits) {
2429
+ for (const [dep, idx] of oldMap) {
2430
+ if (idx === oldIdx && newMap.has(dep)) {
2486
2431
  newCompleteBits.add(newMap.get(dep));
2432
+ break;
2487
2433
  }
2488
2434
  }
2489
- this._completeBits = newCompleteBits;
2490
- } finally {
2491
- this._rewiring = false;
2492
2435
  }
2436
+ this._depCompleteBits = newCompleteBits;
2493
2437
  }
2438
+ // --- Dep message handling ---
2494
2439
  _handleDepMessages(index, messages) {
2495
- if (this._rewiring) return;
2440
+ if (this._rewiring) {
2441
+ this._bufferedDepMessages.push({ index, msgs: messages });
2442
+ return;
2443
+ }
2496
2444
  for (const msg of messages) {
2497
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
2445
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
2498
2446
  const t = msg[0];
2499
2447
  if (this._onMessage) {
2500
2448
  try {
2501
2449
  if (this._onMessage(msg, index, this._actions)) continue;
2502
2450
  } catch (err) {
2503
- this._downInternal([[ERROR, err]]);
2451
+ const errMsg = err instanceof Error ? err.message : String(err);
2452
+ const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
2453
+ cause: err
2454
+ });
2455
+ this._downInternal([[ERROR, wrapped]]);
2504
2456
  return;
2505
2457
  }
2506
2458
  }
2459
+ if (messageTier(t) < 1) continue;
2507
2460
  if (t === DIRTY) {
2508
- this._dirtyBits.add(index);
2509
- this._settledBits.delete(index);
2510
- if (this._dirtyBits.size === 1) {
2511
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
2461
+ const wasEmpty = this._depDirtyBits.size === 0;
2462
+ this._depDirtyBits.add(index);
2463
+ this._depSettledBits.delete(index);
2464
+ if (wasEmpty) {
2465
+ this._downInternal([[DIRTY]]);
2512
2466
  }
2513
2467
  continue;
2514
2468
  }
2515
2469
  if (t === DATA || t === RESOLVED) {
2516
- if (!this._dirtyBits.has(index)) {
2517
- this._dirtyBits.add(index);
2518
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
2470
+ if (!this._depDirtyBits.has(index)) {
2471
+ const wasEmpty = this._depDirtyBits.size === 0;
2472
+ this._depDirtyBits.add(index);
2473
+ if (wasEmpty) {
2474
+ this._downInternal([[DIRTY]]);
2475
+ }
2519
2476
  }
2520
- this._settledBits.add(index);
2477
+ this._depSettledBits.add(index);
2521
2478
  if (this._allDirtySettled()) {
2522
- this._dirtyBits.clear();
2523
- this._settledBits.clear();
2524
- this._runFn();
2479
+ this._depDirtyBits.clear();
2480
+ this._depSettledBits.clear();
2481
+ if (!this._running) {
2482
+ if (this._depValuesDifferFromTracked()) {
2483
+ this._runFn();
2484
+ }
2485
+ }
2525
2486
  }
2526
2487
  continue;
2527
2488
  }
2528
2489
  if (t === COMPLETE) {
2529
- this._completeBits.add(index);
2530
- this._dirtyBits.delete(index);
2531
- this._settledBits.delete(index);
2490
+ this._depCompleteBits.add(index);
2491
+ this._depDirtyBits.delete(index);
2492
+ this._depSettledBits.delete(index);
2532
2493
  if (this._allDirtySettled()) {
2533
- this._dirtyBits.clear();
2534
- this._settledBits.clear();
2535
- this._runFn();
2494
+ this._depDirtyBits.clear();
2495
+ this._depSettledBits.clear();
2496
+ if (!this._running) this._runFn();
2536
2497
  }
2537
- if (this._autoComplete && this._completeBits.size >= this._deps.length && this._deps.length > 0) {
2498
+ if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
2538
2499
  this._downInternal([[COMPLETE]]);
2539
2500
  }
2540
2501
  continue;
@@ -2550,13 +2511,46 @@ var DynamicNodeImpl = class {
2550
2511
  this._downInternal([msg]);
2551
2512
  }
2552
2513
  }
2514
+ /**
2515
+ * Update dep masks for a message without triggering `_runFn` — used
2516
+ * during post-rewire drain so the wave state is consistent with the
2517
+ * buffered activation cascade without recursing.
2518
+ */
2519
+ _updateMasksForMessage(index, msg) {
2520
+ const t = msg[0];
2521
+ if (t === DIRTY) {
2522
+ this._depDirtyBits.add(index);
2523
+ this._depSettledBits.delete(index);
2524
+ } else if (t === DATA || t === RESOLVED) {
2525
+ this._depDirtyBits.add(index);
2526
+ this._depSettledBits.add(index);
2527
+ } else if (t === COMPLETE) {
2528
+ this._depCompleteBits.add(index);
2529
+ this._depDirtyBits.delete(index);
2530
+ this._depSettledBits.delete(index);
2531
+ }
2532
+ }
2553
2533
  _allDirtySettled() {
2554
- if (this._dirtyBits.size === 0) return false;
2555
- for (const idx of this._dirtyBits) {
2556
- if (!this._settledBits.has(idx)) return false;
2534
+ if (this._depDirtyBits.size === 0) return false;
2535
+ for (const idx of this._depDirtyBits) {
2536
+ if (!this._depSettledBits.has(idx)) return false;
2557
2537
  }
2558
2538
  return true;
2559
2539
  }
2540
+ /**
2541
+ * True if any current dep value differs from what the last `_runFn`
2542
+ * saw via `get()`. Used to suppress redundant re-runs when deferred
2543
+ * handshake messages arrive after `_rewire` for a dep whose value
2544
+ * already matches `_trackedValues`.
2545
+ */
2546
+ _depValuesDifferFromTracked() {
2547
+ for (const dep of this._deps) {
2548
+ const current = dep.get();
2549
+ const tracked = this._trackedValues.get(dep);
2550
+ if (!this._equals(current, tracked)) return true;
2551
+ }
2552
+ return false;
2553
+ }
2560
2554
  };
2561
2555
 
2562
2556
  // src/core/meta.ts
@@ -2626,6 +2620,10 @@ function describeNode(node2, includeFields) {
2626
2620
  out.name = node2.name;
2627
2621
  }
2628
2622
  if (all || includeFields.has("value")) {
2623
+ const isSentinel = node2 instanceof NodeImpl && node2._cached === NO_VALUE || node2 instanceof DynamicNodeImpl && node2._cached === NO_VALUE;
2624
+ if (isSentinel) {
2625
+ out.sentinel = true;
2626
+ }
2629
2627
  try {
2630
2628
  out.value = node2.get();
2631
2629
  } catch {
@@ -2652,6 +2650,129 @@ function describeNode(node2, includeFields) {
2652
2650
  return out;
2653
2651
  }
2654
2652
 
2653
+ // src/graph/sizeof.ts
2654
+ var OVERHEAD = {
2655
+ object: 56,
2656
+ array: 64,
2657
+ string: 40,
2658
+ // header; content added separately
2659
+ number: 8,
2660
+ boolean: 4,
2661
+ null: 0,
2662
+ undefined: 0,
2663
+ symbol: 40,
2664
+ bigint: 16,
2665
+ function: 120,
2666
+ map: 72,
2667
+ set: 72,
2668
+ mapEntry: 40,
2669
+ setEntry: 24
2670
+ };
2671
+ function sizeof(value) {
2672
+ const seen = /* @__PURE__ */ new WeakSet();
2673
+ return _sizeof(value, seen);
2674
+ }
2675
+ function _sizeof(value, seen) {
2676
+ if (value == null) return 0;
2677
+ const t = typeof value;
2678
+ switch (t) {
2679
+ case "number":
2680
+ return OVERHEAD.number;
2681
+ case "boolean":
2682
+ return OVERHEAD.boolean;
2683
+ case "string":
2684
+ return OVERHEAD.string + value.length * 2;
2685
+ // UTF-16
2686
+ case "bigint":
2687
+ return OVERHEAD.bigint;
2688
+ case "symbol":
2689
+ return OVERHEAD.symbol;
2690
+ case "function":
2691
+ if (seen.has(value)) return 0;
2692
+ seen.add(value);
2693
+ return OVERHEAD.function;
2694
+ case "undefined":
2695
+ return 0;
2696
+ }
2697
+ const obj = value;
2698
+ if (seen.has(obj)) return 0;
2699
+ seen.add(obj);
2700
+ if (obj instanceof Map) {
2701
+ let size2 = OVERHEAD.map;
2702
+ for (const [k, v] of obj) {
2703
+ size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
2704
+ }
2705
+ return size2;
2706
+ }
2707
+ if (obj instanceof Set) {
2708
+ let size2 = OVERHEAD.set;
2709
+ for (const v of obj) {
2710
+ size2 += OVERHEAD.setEntry + _sizeof(v, seen);
2711
+ }
2712
+ return size2;
2713
+ }
2714
+ if (Array.isArray(obj)) {
2715
+ let size2 = OVERHEAD.array + obj.length * 8;
2716
+ for (const item of obj) {
2717
+ size2 += _sizeof(item, seen);
2718
+ }
2719
+ return size2;
2720
+ }
2721
+ if (obj instanceof ArrayBuffer) return obj.byteLength;
2722
+ if (ArrayBuffer.isView(obj)) return obj.byteLength;
2723
+ let size = OVERHEAD.object;
2724
+ const keys = Object.keys(obj);
2725
+ for (const key of keys) {
2726
+ size += OVERHEAD.string + key.length * 2;
2727
+ size += _sizeof(obj[key], seen);
2728
+ }
2729
+ return size;
2730
+ }
2731
+
2732
+ // src/graph/profile.ts
2733
+ function graphProfile(graph, opts) {
2734
+ const topN = opts?.topN ?? 10;
2735
+ const desc = graph.describe({ detail: "standard" });
2736
+ const targets = [];
2737
+ if (typeof graph._collectObserveTargets === "function") {
2738
+ graph._collectObserveTargets("", targets);
2739
+ }
2740
+ const pathToNode = /* @__PURE__ */ new Map();
2741
+ for (const [p, n] of targets) {
2742
+ pathToNode.set(p, n);
2743
+ }
2744
+ const profiles = [];
2745
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
2746
+ const nd = pathToNode.get(path);
2747
+ const impl = nd instanceof NodeImpl ? nd : null;
2748
+ const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
2749
+ const subscriberCount = impl ? impl._sinkCount : 0;
2750
+ const depCount = nodeDesc.deps?.length ?? 0;
2751
+ const isOrphanEffect = nodeDesc.type === "effect" && subscriberCount === 0;
2752
+ profiles.push({
2753
+ path,
2754
+ type: nodeDesc.type,
2755
+ status: nodeDesc.status ?? "unknown",
2756
+ valueSizeBytes,
2757
+ subscriberCount,
2758
+ depCount,
2759
+ isOrphanEffect
2760
+ });
2761
+ }
2762
+ const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
2763
+ const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
2764
+ const orphanEffects = profiles.filter((p) => p.isOrphanEffect);
2765
+ return {
2766
+ nodeCount: profiles.length,
2767
+ edgeCount: desc.edges.length,
2768
+ subgraphCount: desc.subgraphs.length,
2769
+ nodes: profiles,
2770
+ totalValueSizeBytes,
2771
+ hotspots,
2772
+ orphanEffects
2773
+ };
2774
+ }
2775
+
2655
2776
  // src/graph/graph.ts
2656
2777
  var PATH_SEP = "::";
2657
2778
  var GRAPH_META_SEGMENT = "__meta__";
@@ -3564,6 +3685,16 @@ var Graph = class _Graph {
3564
3685
  }
3565
3686
  return out;
3566
3687
  }
3688
+ /**
3689
+ * Snapshot-based resource profile: per-node stats, orphan effect detection,
3690
+ * memory hotspots. Zero runtime overhead — walks nodes on demand.
3691
+ *
3692
+ * @param opts - Optional `topN` for hotspot limit (default 10).
3693
+ * @returns Aggregate profile with per-node details, hotspots, and orphan effects.
3694
+ */
3695
+ resourceProfile(opts) {
3696
+ return graphProfile(this, opts);
3697
+ }
3567
3698
  _qualifyEdgeEndpoint(part, prefix) {
3568
3699
  if (part.includes(PATH_SEP)) return part;
3569
3700
  return prefix === "" ? part : `${prefix}${PATH_SEP}${part}`;
@@ -3665,8 +3796,8 @@ var Graph = class _Graph {
3665
3796
  dirtyCount: 0,
3666
3797
  resolvedCount: 0,
3667
3798
  events: [],
3668
- completedCleanly: false,
3669
- errored: false
3799
+ anyCompletedCleanly: false,
3800
+ anyErrored: false
3670
3801
  };
3671
3802
  let lastTriggerDepIndex;
3672
3803
  let lastRunDepValues;
@@ -3710,8 +3841,8 @@ var Graph = class _Graph {
3710
3841
  } else if (minimal) {
3711
3842
  if (t === DIRTY) result.dirtyCount++;
3712
3843
  else if (t === RESOLVED) result.resolvedCount++;
3713
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
3714
- else if (t === ERROR) result.errored = true;
3844
+ else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
3845
+ else if (t === ERROR) result.anyErrored = true;
3715
3846
  } else if (t === DIRTY) {
3716
3847
  result.dirtyCount++;
3717
3848
  result.events.push({ type: "dirty", path, ...base });
@@ -3719,10 +3850,10 @@ var Graph = class _Graph {
3719
3850
  result.resolvedCount++;
3720
3851
  result.events.push({ type: "resolved", path, ...base, ...withCausal });
3721
3852
  } else if (t === COMPLETE) {
3722
- if (!result.errored) result.completedCleanly = true;
3853
+ if (!result.anyErrored) result.anyCompletedCleanly = true;
3723
3854
  result.events.push({ type: "complete", path, ...base });
3724
3855
  } else if (t === ERROR) {
3725
- result.errored = true;
3856
+ result.anyErrored = true;
3726
3857
  result.events.push({ type: "error", path, data: m[1], ...base });
3727
3858
  }
3728
3859
  }
@@ -3742,11 +3873,14 @@ var Graph = class _Graph {
3742
3873
  get events() {
3743
3874
  return result.events;
3744
3875
  },
3745
- get completedCleanly() {
3746
- return result.completedCleanly;
3876
+ get anyCompletedCleanly() {
3877
+ return result.anyCompletedCleanly;
3878
+ },
3879
+ get anyErrored() {
3880
+ return result.anyErrored;
3747
3881
  },
3748
- get errored() {
3749
- return result.errored;
3882
+ get completedWithoutErrors() {
3883
+ return result.anyCompletedCleanly && !result.anyErrored;
3750
3884
  },
3751
3885
  dispose() {
3752
3886
  unsub();
@@ -3782,9 +3916,10 @@ var Graph = class _Graph {
3782
3916
  dirtyCount: 0,
3783
3917
  resolvedCount: 0,
3784
3918
  events: [],
3785
- completedCleanly: false,
3786
- errored: false
3919
+ anyCompletedCleanly: false,
3920
+ anyErrored: false
3787
3921
  };
3922
+ const nodeErrored = /* @__PURE__ */ new Set();
3788
3923
  const actor = options.actor;
3789
3924
  const targets = [];
3790
3925
  this._collectObserveTargets("", targets);
@@ -3803,8 +3938,11 @@ var Graph = class _Graph {
3803
3938
  } else if (minimal) {
3804
3939
  if (t === DIRTY) result.dirtyCount++;
3805
3940
  else if (t === RESOLVED) result.resolvedCount++;
3806
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
3807
- else if (t === ERROR) result.errored = true;
3941
+ else if (t === COMPLETE && !nodeErrored.has(path)) result.anyCompletedCleanly = true;
3942
+ else if (t === ERROR) {
3943
+ result.anyErrored = true;
3944
+ nodeErrored.add(path);
3945
+ }
3808
3946
  } else if (t === DIRTY) {
3809
3947
  result.dirtyCount++;
3810
3948
  result.events.push({ type: "dirty", path, ...base });
@@ -3812,10 +3950,11 @@ var Graph = class _Graph {
3812
3950
  result.resolvedCount++;
3813
3951
  result.events.push({ type: "resolved", path, ...base });
3814
3952
  } else if (t === COMPLETE) {
3815
- if (!result.errored) result.completedCleanly = true;
3953
+ if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
3816
3954
  result.events.push({ type: "complete", path, ...base });
3817
3955
  } else if (t === ERROR) {
3818
- result.errored = true;
3956
+ result.anyErrored = true;
3957
+ nodeErrored.add(path);
3819
3958
  result.events.push({ type: "error", path, data: m[1], ...base });
3820
3959
  }
3821
3960
  }
@@ -3835,11 +3974,14 @@ var Graph = class _Graph {
3835
3974
  get events() {
3836
3975
  return result.events;
3837
3976
  },
3838
- get completedCleanly() {
3839
- return result.completedCleanly;
3977
+ get anyCompletedCleanly() {
3978
+ return result.anyCompletedCleanly;
3979
+ },
3980
+ get anyErrored() {
3981
+ return result.anyErrored;
3840
3982
  },
3841
- get errored() {
3842
- return result.errored;
3983
+ get completedWithoutErrors() {
3984
+ return result.anyCompletedCleanly && !result.anyErrored;
3843
3985
  },
3844
3986
  dispose() {
3845
3987
  for (const u of unsubs) u();
@@ -3871,8 +4013,8 @@ var Graph = class _Graph {
3871
4013
  dirtyCount: 0,
3872
4014
  resolvedCount: 0,
3873
4015
  events: [],
3874
- completedCleanly: false,
3875
- errored: false
4016
+ anyCompletedCleanly: false,
4017
+ anyErrored: false
3876
4018
  };
3877
4019
  const target = this.resolve(path);
3878
4020
  let batchSeq = 0;
@@ -3891,10 +4033,10 @@ var Graph = class _Graph {
3891
4033
  acc.resolvedCount++;
3892
4034
  acc.events.push({ type: "resolved", path, ...base });
3893
4035
  } else if (t === COMPLETE) {
3894
- if (!acc.errored) acc.completedCleanly = true;
4036
+ if (!acc.anyErrored) acc.anyCompletedCleanly = true;
3895
4037
  acc.events.push({ type: "complete", path, ...base });
3896
4038
  } else if (t === ERROR) {
3897
- acc.errored = true;
4039
+ acc.anyErrored = true;
3898
4040
  acc.events.push({ type: "error", path, data: m[1], ...base });
3899
4041
  }
3900
4042
  }
@@ -3912,11 +4054,14 @@ var Graph = class _Graph {
3912
4054
  get events() {
3913
4055
  return acc.events;
3914
4056
  },
3915
- get completedCleanly() {
3916
- return acc.completedCleanly;
4057
+ get anyCompletedCleanly() {
4058
+ return acc.anyCompletedCleanly;
3917
4059
  },
3918
- get errored() {
3919
- return acc.errored;
4060
+ get anyErrored() {
4061
+ return acc.anyErrored;
4062
+ },
4063
+ get completedWithoutErrors() {
4064
+ return acc.anyCompletedCleanly && !acc.anyErrored;
3920
4065
  },
3921
4066
  dispose() {
3922
4067
  unsub();
@@ -3937,9 +4082,10 @@ var Graph = class _Graph {
3937
4082
  dirtyCount: 0,
3938
4083
  resolvedCount: 0,
3939
4084
  events: [],
3940
- completedCleanly: false,
3941
- errored: false
4085
+ anyCompletedCleanly: false,
4086
+ anyErrored: false
3942
4087
  };
4088
+ const nodeErrored = /* @__PURE__ */ new Set();
3943
4089
  const targets = [];
3944
4090
  this._collectObserveTargets("", targets);
3945
4091
  targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
@@ -3961,10 +4107,11 @@ var Graph = class _Graph {
3961
4107
  acc.resolvedCount++;
3962
4108
  acc.events.push({ type: "resolved", path, ...base });
3963
4109
  } else if (t === COMPLETE) {
3964
- if (!acc.errored) acc.completedCleanly = true;
4110
+ if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
3965
4111
  acc.events.push({ type: "complete", path, ...base });
3966
4112
  } else if (t === ERROR) {
3967
- acc.errored = true;
4113
+ acc.anyErrored = true;
4114
+ nodeErrored.add(path);
3968
4115
  acc.events.push({ type: "error", path, data: m[1], ...base });
3969
4116
  }
3970
4117
  }
@@ -3983,11 +4130,14 @@ var Graph = class _Graph {
3983
4130
  get events() {
3984
4131
  return acc.events;
3985
4132
  },
3986
- get completedCleanly() {
3987
- return acc.completedCleanly;
4133
+ get anyCompletedCleanly() {
4134
+ return acc.anyCompletedCleanly;
4135
+ },
4136
+ get anyErrored() {
4137
+ return acc.anyErrored;
3988
4138
  },
3989
- get errored() {
3990
- return acc.errored;
4139
+ get completedWithoutErrors() {
4140
+ return acc.anyCompletedCleanly && !acc.anyErrored;
3991
4141
  },
3992
4142
  dispose() {
3993
4143
  for (const u of unsubs) u();
@@ -4313,8 +4463,9 @@ var Graph = class _Graph {
4313
4463
  /**
4314
4464
  * Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
4315
4465
  *
4316
- * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 2 messages
4317
- * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1 control waves.
4466
+ * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 3 messages
4467
+ * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1/2 control
4468
+ * waves (`START`/`DIRTY`/`INVALIDATE`/`PAUSE`/`RESUME`).
4318
4469
  */
4319
4470
  autoCheckpoint(adapter, options = {}) {
4320
4471
  const debounceMs = Math.max(0, options.debounceMs ?? 500);
@@ -4361,7 +4512,7 @@ var Graph = class _Graph {
4361
4512
  timer = setTimeout(flush, debounceMs);
4362
4513
  };
4363
4514
  const off = this.observe().subscribe((path, messages) => {
4364
- const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 2);
4515
+ const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
4365
4516
  if (!triggeredByTier) return;
4366
4517
  if (options.filter) {
4367
4518
  const nd = this.resolve(path);
@@ -4604,6 +4755,15 @@ function reactiveLog(initial, options = {}) {
4604
4755
  return bundle;
4605
4756
  }
4606
4757
 
4758
+ // src/patterns/_internal.ts
4759
+ function domainMeta(domain, kind, extra) {
4760
+ return {
4761
+ [domain]: true,
4762
+ [`${domain}_type`]: kind,
4763
+ ...extra ?? {}
4764
+ };
4765
+ }
4766
+
4607
4767
  // src/patterns/cqrs.ts
4608
4768
  var COMMAND_GUARD = policy((allow, deny) => {
4609
4769
  allow("write");
@@ -4621,11 +4781,7 @@ var EVENT_GUARD = policy((allow, deny) => {
4621
4781
  deny("write");
4622
4782
  });
4623
4783
  function cqrsMeta(kind, extra) {
4624
- return { cqrs: true, cqrs_type: kind, ...extra ?? {} };
4625
- }
4626
- function keepalive(n) {
4627
- return n.subscribe(() => {
4628
- });
4784
+ return domainMeta("cqrs", kind, extra);
4629
4785
  }
4630
4786
  var CqrsGraph = class extends Graph {
4631
4787
  _eventLogs = /* @__PURE__ */ new Map();