@graphrefly/graphrefly 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/{chunk-R6OHUUYB.js → chunk-AHRKWMNI.js} +7 -7
  2. package/dist/chunk-AHRKWMNI.js.map +1 -0
  3. package/dist/{chunk-2PORF4RP.js → chunk-BER7UYLM.js} +27 -32
  4. package/dist/chunk-BER7UYLM.js.map +1 -0
  5. package/dist/{chunk-646OG3PO.js → chunk-IRZAGZUB.js} +51 -52
  6. package/dist/chunk-IRZAGZUB.js.map +1 -0
  7. package/dist/{chunk-IHJHBADD.js → chunk-JC2SN46B.js} +385 -197
  8. package/dist/chunk-JC2SN46B.js.map +1 -0
  9. package/dist/{chunk-XJ6EMQ22.js → chunk-OO5QOAXI.js} +4 -10
  10. package/dist/chunk-OO5QOAXI.js.map +1 -0
  11. package/dist/{chunk-YXROQFXZ.js → chunk-UW77D7SP.js} +3 -3
  12. package/dist/{chunk-F2ULI3Q3.js → chunk-XUOY3YKN.js} +7 -3
  13. package/dist/chunk-XUOY3YKN.js.map +1 -0
  14. package/dist/chunk-YLR5JUJZ.js +111 -0
  15. package/dist/chunk-YLR5JUJZ.js.map +1 -0
  16. package/dist/{chunk-BV3TPSBK.js → chunk-YXR3WW3Q.js} +740 -755
  17. package/dist/chunk-YXR3WW3Q.js.map +1 -0
  18. package/dist/compat/nestjs/index.cjs +1127 -983
  19. package/dist/compat/nestjs/index.cjs.map +1 -1
  20. package/dist/compat/nestjs/index.d.cts +4 -4
  21. package/dist/compat/nestjs/index.d.ts +4 -4
  22. package/dist/compat/nestjs/index.js +7 -13
  23. package/dist/core/index.cjs +653 -749
  24. package/dist/core/index.cjs.map +1 -1
  25. package/dist/core/index.d.cts +2 -2
  26. package/dist/core/index.d.ts +2 -2
  27. package/dist/core/index.js +7 -7
  28. package/dist/extra/index.cjs +773 -795
  29. package/dist/extra/index.cjs.map +1 -1
  30. package/dist/extra/index.d.cts +4 -4
  31. package/dist/extra/index.d.ts +4 -4
  32. package/dist/extra/index.js +5 -11
  33. package/dist/graph/index.cjs +1036 -975
  34. package/dist/graph/index.cjs.map +1 -1
  35. package/dist/graph/index.d.cts +3 -3
  36. package/dist/graph/index.d.ts +3 -3
  37. package/dist/graph/index.js +8 -8
  38. package/dist/{graph-fCsaaVIa.d.cts → graph-KsTe57nI.d.cts} +127 -51
  39. package/dist/{graph-Dc-P9BVm.d.ts → graph-mILUUqW8.d.ts} +127 -51
  40. package/dist/{index-DhXznWyH.d.ts → index-8a605sg9.d.ts} +2 -2
  41. package/dist/{index-D7y9Q8W4.d.ts → index-B2SvPEbc.d.ts} +8 -69
  42. package/dist/{index-YlOH1Gw6.d.cts → index-BBUYZfJ1.d.cts} +122 -78
  43. package/dist/{index-ClaKZFPl.d.cts → index-Bjh5C1Tp.d.cts} +38 -35
  44. package/dist/{index-DWq0P9T6.d.ts → index-BjtlNirP.d.cts} +5 -7
  45. package/dist/{index-N704txAA.d.ts → index-BnkMgNNa.d.ts} +38 -35
  46. package/dist/{index-BBVBYPxr.d.cts → index-CgSiUouz.d.ts} +5 -7
  47. package/dist/{index-BmoUvOGN.d.ts → index-CvKzv0AW.d.ts} +122 -78
  48. package/dist/{index-4OIX-q0C.d.cts → index-UudxGnzc.d.cts} +8 -69
  49. package/dist/{index-DlGMf_Qe.d.cts → index-VHA43cGP.d.cts} +2 -2
  50. package/dist/index.cjs +6146 -5725
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +617 -383
  53. package/dist/index.d.ts +617 -383
  54. package/dist/index.js +4401 -4028
  55. package/dist/index.js.map +1 -1
  56. package/dist/{meta-BV4pj9ML.d.cts → meta-BnG7XAaE.d.cts} +395 -289
  57. package/dist/{meta-BV4pj9ML.d.ts → meta-BnG7XAaE.d.ts} +395 -289
  58. package/dist/observable-C8Kx_O6k.d.cts +36 -0
  59. package/dist/observable-DcBwQY7t.d.ts +36 -0
  60. package/dist/patterns/reactive-layout/index.cjs +1037 -857
  61. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  62. package/dist/patterns/reactive-layout/index.d.cts +3 -3
  63. package/dist/patterns/reactive-layout/index.d.ts +3 -3
  64. package/dist/patterns/reactive-layout/index.js +4 -4
  65. package/package.json +1 -1
  66. package/dist/chunk-2PORF4RP.js.map +0 -1
  67. package/dist/chunk-646OG3PO.js.map +0 -1
  68. package/dist/chunk-BV3TPSBK.js.map +0 -1
  69. package/dist/chunk-EBNKJULL.js +0 -231
  70. package/dist/chunk-EBNKJULL.js.map +0 -1
  71. package/dist/chunk-F2ULI3Q3.js.map +0 -1
  72. package/dist/chunk-IHJHBADD.js.map +0 -1
  73. package/dist/chunk-R6OHUUYB.js.map +0 -1
  74. package/dist/chunk-XJ6EMQ22.js.map +0 -1
  75. package/dist/observable-Cz-AWhwR.d.cts +0 -42
  76. package/dist/observable-DCqlwGyl.d.ts +0 -42
  77. /package/dist/{chunk-YXROQFXZ.js.map → chunk-UW77D7SP.js.map} +0 -0
@@ -95,11 +95,8 @@ __export(nestjs_exports, {
95
95
  getActor: () => getActor,
96
96
  getGraphToken: () => getGraphToken,
97
97
  getNodeToken: () => getNodeToken,
98
- observeGraph$: () => observeGraph$,
99
- observeNode$: () => observeNode$,
100
98
  observeSSE: () => observeSSE,
101
99
  observeSubscription: () => observeSubscription,
102
- toMessages$: () => toMessages$,
103
100
  toObservable: () => toObservable
104
101
  });
105
102
  module.exports = __toCommonJS(nestjs_exports);
@@ -108,6 +105,7 @@ module.exports = __toCommonJS(nestjs_exports);
108
105
  var import_rxjs = require("rxjs");
109
106
 
110
107
  // src/core/messages.ts
108
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
111
109
  var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
112
110
  var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
113
111
  var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
@@ -117,13 +115,27 @@ var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
117
115
  var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
118
116
  var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
119
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);
120
131
  function messageTier(t) {
121
- if (t === DIRTY || t === INVALIDATE) return 0;
122
- if (t === PAUSE || t === RESUME) return 1;
123
- if (t === DATA || t === RESOLVED) return 2;
124
- if (t === COMPLETE || t === ERROR) return 3;
125
- if (t === TEARDOWN) return 4;
126
- 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;
127
139
  }
128
140
  function isPhase2Message(msg) {
129
141
  const t = msg[0];
@@ -137,48 +149,28 @@ function propagatesToMeta(t) {
137
149
  }
138
150
 
139
151
  // src/extra/observable.ts
140
- function toObservable(node2) {
141
- return new import_rxjs.Observable((subscriber) => {
142
- const unsub = node2.subscribe((msgs) => {
143
- for (const m of msgs) {
152
+ function toObservable(node2, options) {
153
+ if (options?.raw) {
154
+ return new import_rxjs.Observable((subscriber) => {
155
+ const unsub = node2.subscribe((msgs) => {
144
156
  if (subscriber.closed) return;
145
- if (m[0] === DATA) {
146
- subscriber.next(m[1]);
147
- } else if (m[0] === ERROR) {
148
- subscriber.error(m[1]);
149
- return;
150
- } else if (m[0] === COMPLETE) {
151
- subscriber.complete();
152
- return;
157
+ subscriber.next(msgs);
158
+ for (const m of msgs) {
159
+ if (m[0] === ERROR) {
160
+ subscriber.error(m[1]);
161
+ return;
162
+ }
163
+ if (m[0] === COMPLETE) {
164
+ subscriber.complete();
165
+ return;
166
+ }
153
167
  }
154
- }
168
+ });
169
+ return unsub;
155
170
  });
156
- return unsub;
157
- });
158
- }
159
- function toMessages$(node2) {
171
+ }
160
172
  return new import_rxjs.Observable((subscriber) => {
161
173
  const unsub = node2.subscribe((msgs) => {
162
- if (subscriber.closed) return;
163
- subscriber.next(msgs);
164
- for (const m of msgs) {
165
- if (m[0] === ERROR) {
166
- subscriber.error(m[1]);
167
- return;
168
- }
169
- if (m[0] === COMPLETE) {
170
- subscriber.complete();
171
- return;
172
- }
173
- }
174
- });
175
- return unsub;
176
- });
177
- }
178
- function observeNode$(graph, path, options) {
179
- return new import_rxjs.Observable((subscriber) => {
180
- const handle = graph.observe(path, options);
181
- const unsub = handle.subscribe((msgs) => {
182
174
  for (const m of msgs) {
183
175
  if (subscriber.closed) return;
184
176
  if (m[0] === DATA) {
@@ -195,16 +187,6 @@ function observeNode$(graph, path, options) {
195
187
  return unsub;
196
188
  });
197
189
  }
198
- function observeGraph$(graph, options) {
199
- return new import_rxjs.Observable((subscriber) => {
200
- const handle = graph.observe(options);
201
- const unsub = handle.subscribe((nodePath, messages) => {
202
- if (subscriber.closed) return;
203
- subscriber.next({ path: nodePath, messages });
204
- });
205
- return unsub;
206
- });
207
- }
208
190
 
209
191
  // src/compat/nestjs/decorators.ts
210
192
  var import_common = require("@nestjs/common");
@@ -339,6 +321,82 @@ function normalizeActor(actor) {
339
321
  };
340
322
  }
341
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
+
342
400
  // src/core/batch.ts
343
401
  var MAX_DRAIN_ITERATIONS = 1e3;
344
402
  var batchDepth = 0;
@@ -495,14 +553,14 @@ function _downSequential(sink, messages, phase = 2) {
495
553
  const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
496
554
  for (const msg of messages) {
497
555
  const tier = messageTier(msg[0]);
498
- if (tier === 2) {
556
+ if (tier === 3) {
499
557
  if (isBatching()) {
500
558
  const m = msg;
501
559
  dataQueue.push(() => sink([m]));
502
560
  } else {
503
561
  sink([msg]);
504
562
  }
505
- } else if (tier >= 3) {
563
+ } else if (tier >= 4) {
506
564
  if (isBatching()) {
507
565
  const m = msg;
508
566
  pendingPhase3.push(() => sink([m]));
@@ -515,82 +573,6 @@ function _downSequential(sink, messages, phase = 2) {
515
573
  }
516
574
  }
517
575
 
518
- // src/core/guard.ts
519
- var GuardDenied = class extends Error {
520
- actor;
521
- action;
522
- nodeName;
523
- /**
524
- * @param details - Actor, action, and optional node name for the denial.
525
- * @param message - Optional override for the default error message.
526
- */
527
- constructor(details, message) {
528
- super(
529
- message ?? `GuardDenied: action "${String(details.action)}" denied for actor type "${String(details.actor.type)}"`
530
- );
531
- this.name = "GuardDenied";
532
- this.actor = details.actor;
533
- this.action = details.action;
534
- this.nodeName = details.nodeName;
535
- }
536
- /** Qualified registry path when known (roadmap diagnostics: same as {@link nodeName}). */
537
- get node() {
538
- return this.nodeName;
539
- }
540
- };
541
- function normalizeActions(action) {
542
- if (Array.isArray(action)) {
543
- return [...action];
544
- }
545
- return [action];
546
- }
547
- function matchesActions(set, action) {
548
- return set.has(action) || set.has("*");
549
- }
550
- function policy(build) {
551
- const rules = [];
552
- const allow = (action, opts) => {
553
- rules.push({
554
- kind: "allow",
555
- actions: new Set(normalizeActions(action)),
556
- where: opts?.where ?? (() => true)
557
- });
558
- };
559
- const deny = (action, opts) => {
560
- rules.push({
561
- kind: "deny",
562
- actions: new Set(normalizeActions(action)),
563
- where: opts?.where ?? (() => true)
564
- });
565
- };
566
- build(allow, deny);
567
- return (actor, action) => {
568
- let denied = false;
569
- let allowed = false;
570
- for (const r of rules) {
571
- if (!matchesActions(r.actions, action)) continue;
572
- if (!r.where(actor)) continue;
573
- if (r.kind === "deny") {
574
- denied = true;
575
- } else {
576
- allowed = true;
577
- }
578
- }
579
- if (denied) return false;
580
- return allowed;
581
- };
582
- }
583
- var STANDARD_WRITE_TYPES = ["human", "llm", "wallet", "system"];
584
- function accessHintForGuard(guard) {
585
- const allowed = STANDARD_WRITE_TYPES.filter((t) => guard({ type: t, id: "" }, "write"));
586
- if (allowed.length === 0) return "restricted";
587
- if (allowed.includes("human") && allowed.includes("llm") && allowed.every((t) => t === "human" || t === "llm" || t === "system")) {
588
- return "both";
589
- }
590
- if (allowed.length === 1) return allowed[0];
591
- return allowed.join("+");
592
- }
593
-
594
576
  // src/core/versioning.ts
595
577
  var import_node_crypto = require("crypto");
596
578
  function canonicalizeForHash(value) {
@@ -643,10 +625,24 @@ function advanceVersion(info, newValue, hashFn) {
643
625
  }
644
626
  }
645
627
 
646
- // src/core/node.ts
628
+ // src/core/node-base.ts
647
629
  var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
648
630
  var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
649
- 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);
650
646
  let bits = 0;
651
647
  return {
652
648
  set(i) {
@@ -659,7 +655,8 @@ function createIntBitSet() {
659
655
  return (bits & 1 << i) !== 0;
660
656
  },
661
657
  covers(other) {
662
- return (bits & other._bits()) === other._bits();
658
+ const otherBits = other._bits();
659
+ return (bits & otherBits) === otherBits;
663
660
  },
664
661
  any() {
665
662
  return bits !== 0;
@@ -667,6 +664,9 @@ function createIntBitSet() {
667
664
  reset() {
668
665
  bits = 0;
669
666
  },
667
+ setAll() {
668
+ bits = fullMask;
669
+ },
670
670
  _bits() {
671
671
  return bits;
672
672
  }
@@ -674,6 +674,8 @@ function createIntBitSet() {
674
674
  }
675
675
  function createArrayBitSet(size) {
676
676
  const words = new Uint32Array(Math.ceil(size / 32));
677
+ const lastBits = size % 32;
678
+ const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
677
679
  return {
678
680
  set(i) {
679
681
  words[i >>> 5] |= 1 << (i & 31);
@@ -700,130 +702,103 @@ function createArrayBitSet(size) {
700
702
  reset() {
701
703
  words.fill(0);
702
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
+ },
703
709
  _words: words
704
710
  };
705
711
  }
706
712
  function createBitSet(size) {
707
- return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
713
+ return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
708
714
  }
709
- var isNodeArray = (value) => Array.isArray(value);
710
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
711
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
712
- var isCleanupFn = (value) => typeof value === "function";
713
- var statusAfterMessage = (status, msg) => {
714
- const t = msg[0];
715
- if (t === DIRTY) return "dirty";
716
- if (t === DATA) return "settled";
717
- if (t === RESOLVED) return "resolved";
718
- if (t === COMPLETE) return "completed";
719
- if (t === ERROR) return "errored";
720
- if (t === INVALIDATE) return "dirty";
721
- if (t === TEARDOWN) return "disconnected";
722
- return status;
723
- };
724
- var NodeImpl = class {
725
- // --- Configuration (set once, never reassigned) ---
715
+ var NodeBase = class {
716
+ // --- Identity (set once) ---
726
717
  _optsName;
727
718
  _registryName;
728
- /** @internal read by {@link describeNode} before inference. */
719
+ /** @internal Read by `describeNode` before inference. */
729
720
  _describeKind;
730
721
  meta;
731
- _deps;
732
- _fn;
733
- _opts;
722
+ // --- Options ---
734
723
  _equals;
724
+ _resubscribable;
725
+ _resetOnTeardown;
726
+ _onResubscribe;
735
727
  _onMessage;
736
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
728
+ /** @internal Read by `describeNode` for `accessHintForGuard`. */
737
729
  _guard;
730
+ /** @internal Subclasses update this through {@link _recordMutation}. */
738
731
  _lastMutation;
739
- _hasDeps;
740
- _autoComplete;
741
- _isSingleDep;
742
- // --- Mutable state ---
732
+ // --- Versioning ---
733
+ _hashFn;
734
+ _versioning;
735
+ // --- Lifecycle state ---
736
+ /** @internal Read by `describeNode` and `graph.ts`. */
743
737
  _cached;
738
+ /** @internal Read externally via `get status()`. */
744
739
  _status;
745
740
  _terminal = false;
746
- _connected = false;
747
- _producerStarted = false;
748
- _connecting = false;
749
- _manualEmitUsed = false;
741
+ _active = false;
742
+ // --- Sink storage ---
743
+ /** @internal Read by `graph/profile.ts` for subscriber counts. */
750
744
  _sinkCount = 0;
751
745
  _singleDepSinkCount = 0;
752
- // --- Object/collection state ---
753
- _depDirtyMask;
754
- _depSettledMask;
755
- _depCompleteMask;
756
- _allDepsCompleteMask;
757
- _lastDepValues;
758
- _cleanup;
759
- _sinks = null;
760
746
  _singleDepSinks = /* @__PURE__ */ new WeakSet();
761
- _upstreamUnsubs = [];
747
+ _sinks = null;
748
+ // --- Actions + bound helpers ---
762
749
  _actions;
763
750
  _boundDownToSinks;
751
+ // --- Inspector hook (Graph observability) ---
764
752
  _inspectorHook;
765
- _versioning;
766
- _hashFn;
767
- constructor(deps, fn, opts) {
768
- this._deps = deps;
769
- this._fn = fn;
770
- this._opts = opts;
753
+ constructor(opts) {
771
754
  this._optsName = opts.name;
772
755
  this._describeKind = opts.describeKind;
773
756
  this._equals = opts.equals ?? Object.is;
757
+ this._resubscribable = opts.resubscribable ?? false;
758
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
759
+ this._onResubscribe = opts.onResubscribe;
774
760
  this._onMessage = opts.onMessage;
775
761
  this._guard = opts.guard;
776
- this._hasDeps = deps.length > 0;
777
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
778
- this._isSingleDep = deps.length === 1 && fn != null;
779
762
  this._cached = "initial" in opts ? opts.initial : NO_VALUE;
780
- this._status = this._hasDeps ? "disconnected" : "settled";
763
+ this._status = "disconnected";
781
764
  this._hashFn = opts.versioningHash ?? defaultHash;
782
765
  this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
783
766
  id: opts.versioningId,
784
767
  hash: this._hashFn
785
768
  }) : void 0;
786
- this._depDirtyMask = createBitSet(deps.length);
787
- this._depSettledMask = createBitSet(deps.length);
788
- this._depCompleteMask = createBitSet(deps.length);
789
- this._allDepsCompleteMask = createBitSet(deps.length);
790
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
791
769
  const meta = {};
792
770
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
793
- meta[k] = node({
794
- initial: v,
795
- name: `${opts.name ?? "node"}:meta:${k}`,
796
- describeKind: "state",
797
- ...opts.guard != null ? { guard: opts.guard } : {}
798
- });
771
+ meta[k] = this._createMetaNode(k, v, opts);
799
772
  }
800
773
  Object.freeze(meta);
801
774
  this.meta = meta;
802
775
  const self = this;
803
776
  this._actions = {
804
777
  down(messages) {
805
- self._manualEmitUsed = true;
778
+ self._onManualEmit();
806
779
  self._downInternal(messages);
807
780
  },
808
781
  emit(value) {
809
- self._manualEmitUsed = true;
782
+ self._onManualEmit();
810
783
  self._downAutoValue(value);
811
784
  },
812
785
  up(messages) {
813
786
  self._upInternal(messages);
814
787
  }
815
788
  };
816
- this.down = this.down.bind(this);
817
- this.up = this.up.bind(this);
818
789
  this._boundDownToSinks = this._downToSinks.bind(this);
819
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 ---
820
798
  get name() {
821
799
  return this._registryName ?? this._optsName;
822
800
  }
823
- /**
824
- * When a node is registered with {@link Graph.add} without an options `name`,
825
- * the graph assigns the registry local name for introspection (parity with graphrefly-py).
826
- */
801
+ /** @internal Assigned by `Graph.add` when registered without an options `name`. */
827
802
  _assignRegistryName(localName) {
828
803
  if (this._optsName !== void 0 || this._registryName !== void 0) return;
829
804
  this._registryName = localName;
@@ -841,7 +816,10 @@ var NodeImpl = class {
841
816
  }
842
817
  };
843
818
  }
844
- // --- Public interface (Node<T>) ---
819
+ /** @internal Used by subclasses to surface inspector events. */
820
+ _emitInspectorHook(event) {
821
+ this._inspectorHook?.(event);
822
+ }
845
823
  get status() {
846
824
  return this._status;
847
825
  }
@@ -851,15 +829,7 @@ var NodeImpl = class {
851
829
  get v() {
852
830
  return this._versioning;
853
831
  }
854
- /**
855
- * Retroactively apply versioning to a node that was created without it.
856
- * No-op if versioning is already enabled.
857
- *
858
- * Version starts at 0 regardless of prior DATA emissions — it tracks
859
- * changes from the moment versioning is enabled, not historical ones.
860
- *
861
- * @internal — used by {@link Graph.setVersioning}.
862
- */
832
+ /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
863
833
  _applyVersioning(level, opts) {
864
834
  if (this._versioning != null) return;
865
835
  this._hashFn = opts?.hash ?? this._hashFn;
@@ -879,6 +849,7 @@ var NodeImpl = class {
879
849
  if (this._guard == null) return true;
880
850
  return this._guard(normalizeActor(actor), "observe");
881
851
  }
852
+ // --- Public transport ---
882
853
  get() {
883
854
  return this._cached === NO_VALUE ? void 0 : this._cached;
884
855
  }
@@ -891,43 +862,25 @@ var NodeImpl = class {
891
862
  if (!this._guard(actor, action)) {
892
863
  throw new GuardDenied({ actor, action, nodeName: this.name });
893
864
  }
894
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
865
+ this._recordMutation(actor);
895
866
  }
896
867
  this._downInternal(messages);
897
868
  }
898
- _downInternal(messages) {
899
- if (messages.length === 0) return;
900
- let lifecycleMessages = messages;
901
- let sinkMessages = messages;
902
- if (this._terminal && !this._opts.resubscribable) {
903
- const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
904
- if (terminalPassthrough.length === 0) return;
905
- lifecycleMessages = terminalPassthrough;
906
- sinkMessages = terminalPassthrough;
907
- }
908
- this._handleLocalLifecycle(lifecycleMessages);
909
- if (this._canSkipDirty()) {
910
- let hasPhase2 = false;
911
- for (let i = 0; i < sinkMessages.length; i++) {
912
- const t = sinkMessages[i][0];
913
- if (t === DATA || t === RESOLVED) {
914
- hasPhase2 = true;
915
- break;
916
- }
917
- }
918
- if (hasPhase2) {
919
- const filtered = [];
920
- for (let i = 0; i < sinkMessages.length; i++) {
921
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
922
- }
923
- if (filtered.length > 0) {
924
- downWithBatch(this._boundDownToSinks, filtered);
925
- }
926
- return;
927
- }
928
- }
929
- 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();
930
882
  }
883
+ // --- Subscribe (uniform across node shapes) ---
931
884
  subscribe(sink, hints) {
932
885
  if (hints?.actor != null && this._guard != null) {
933
886
  const actor = normalizeActor(hints.actor);
@@ -935,17 +888,21 @@ var NodeImpl = class {
935
888
  throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
936
889
  }
937
890
  }
938
- if (this._terminal && this._opts.resubscribable) {
891
+ if (this._terminal && this._resubscribable) {
939
892
  this._terminal = false;
940
893
  this._cached = NO_VALUE;
941
- this._status = this._hasDeps ? "disconnected" : "settled";
942
- this._opts.onResubscribe?.();
894
+ this._status = "disconnected";
895
+ this._onResubscribe?.();
943
896
  }
944
897
  this._sinkCount += 1;
945
898
  if (hints?.singleDep) {
946
899
  this._singleDepSinkCount += 1;
947
900
  this._singleDepSinks.add(sink);
948
901
  }
902
+ if (!this._terminal) {
903
+ const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
904
+ downWithBatch(sink, startMessages);
905
+ }
949
906
  if (this._sinks == null) {
950
907
  this._sinks = sink;
951
908
  } else if (typeof this._sinks === "function") {
@@ -953,10 +910,12 @@ var NodeImpl = class {
953
910
  } else {
954
911
  this._sinks.add(sink);
955
912
  }
956
- if (this._hasDeps) {
957
- this._connectUpstream();
958
- } else if (this._fn) {
959
- 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";
960
919
  }
961
920
  let removed = false;
962
921
  return () => {
@@ -980,39 +939,49 @@ var NodeImpl = class {
980
939
  }
981
940
  }
982
941
  if (this._sinks == null) {
983
- this._disconnectUpstream();
984
- this._stopProducer();
942
+ this._onDeactivate();
985
943
  }
986
944
  };
987
945
  }
988
- up(messages, options) {
989
- if (!this._hasDeps) return;
990
- if (!options?.internal && this._guard != null) {
991
- const actor = normalizeActor(options?.actor);
992
- if (!this._guard(actor, "write")) {
993
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
994
- }
995
- 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;
996
958
  }
997
- for (const dep of this._deps) {
998
- if (options === void 0) {
999
- dep.up?.(messages);
1000
- } else {
1001
- 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;
1002
978
  }
1003
979
  }
980
+ downWithBatch(this._boundDownToSinks, sinkMessages);
1004
981
  }
1005
- _upInternal(messages) {
1006
- if (!this._hasDeps) return;
1007
- for (const dep of this._deps) {
1008
- dep.up?.(messages, { internal: true });
1009
- }
1010
- }
1011
- unsubscribe() {
1012
- if (!this._hasDeps) return;
1013
- this._disconnectUpstream();
982
+ _canSkipDirty() {
983
+ return this._sinkCount === 1 && this._singleDepSinkCount === 1;
1014
984
  }
1015
- // --- Private methods (prototype, _ prefix) ---
1016
985
  _downToSinks(messages) {
1017
986
  if (this._sinks == null) return;
1018
987
  if (typeof this._sinks === "function") {
@@ -1024,6 +993,11 @@ var NodeImpl = class {
1024
993
  sink(messages);
1025
994
  }
1026
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
+ */
1027
1001
  _handleLocalLifecycle(messages) {
1028
1002
  for (const m of messages) {
1029
1003
  const t = m[0];
@@ -1037,28 +1011,22 @@ var NodeImpl = class {
1037
1011
  }
1038
1012
  }
1039
1013
  if (t === INVALIDATE) {
1040
- const cleanupFn = this._cleanup;
1041
- this._cleanup = void 0;
1042
- cleanupFn?.();
1014
+ this._onInvalidate();
1043
1015
  this._cached = NO_VALUE;
1044
- this._lastDepValues = void 0;
1045
1016
  }
1046
1017
  this._status = statusAfterMessage(this._status, m);
1047
1018
  if (t === COMPLETE || t === ERROR) {
1048
1019
  this._terminal = true;
1049
1020
  }
1050
1021
  if (t === TEARDOWN) {
1051
- if (this._opts.resetOnTeardown) {
1022
+ if (this._resetOnTeardown) {
1052
1023
  this._cached = NO_VALUE;
1053
1024
  }
1054
- const teardownCleanup = this._cleanup;
1055
- this._cleanup = void 0;
1056
- teardownCleanup?.();
1025
+ this._onTeardown();
1057
1026
  try {
1058
1027
  this._propagateToMeta(t);
1059
1028
  } finally {
1060
- this._disconnectUpstream();
1061
- this._stopProducer();
1029
+ this._onDeactivate();
1062
1030
  }
1063
1031
  }
1064
1032
  if (t !== TEARDOWN && propagatesToMeta(t)) {
@@ -1066,7 +1034,20 @@ var NodeImpl = class {
1066
1034
  }
1067
1035
  }
1068
1036
  }
1069
- /** 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). */
1070
1051
  _propagateToMeta(t) {
1071
1052
  for (const metaNode of Object.values(this.meta)) {
1072
1053
  try {
@@ -1075,9 +1056,10 @@ var NodeImpl = class {
1075
1056
  }
1076
1057
  }
1077
1058
  }
1078
- _canSkipDirty() {
1079
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
1080
- }
1059
+ /**
1060
+ * Frame a computed value into the right protocol messages and dispatch
1061
+ * via `_downInternal`. Used by `_runFn` and `actions.emit`.
1062
+ */
1081
1063
  _downAutoValue(value) {
1082
1064
  const wasDirty = this._status === "dirty";
1083
1065
  let unchanged;
@@ -1085,7 +1067,9 @@ var NodeImpl = class {
1085
1067
  unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
1086
1068
  } catch (eqErr) {
1087
1069
  const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
1088
- 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
+ });
1089
1073
  this._downInternal([[ERROR, wrapped]]);
1090
1074
  return;
1091
1075
  }
@@ -1095,89 +1079,173 @@ var NodeImpl = class {
1095
1079
  }
1096
1080
  this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
1097
1081
  }
1098
- _runFn() {
1099
- if (!this._fn) return;
1100
- if (this._terminal && !this._opts.resubscribable) return;
1101
- if (this._connecting) return;
1102
- try {
1103
- const n = this._deps.length;
1104
- const depValues = new Array(n);
1105
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1106
- const prev = this._lastDepValues;
1107
- if (n > 0 && prev != null && prev.length === n) {
1108
- let allSame = true;
1109
- for (let i = 0; i < n; i++) {
1110
- if (!Object.is(depValues[i], prev[i])) {
1111
- allSame = false;
1112
- break;
1113
- }
1114
- }
1115
- if (allSame) {
1116
- if (this._status === "dirty") {
1117
- this._downInternal([[RESOLVED]]);
1118
- }
1119
- return;
1120
- }
1121
- }
1122
- const prevCleanup = this._cleanup;
1123
- this._cleanup = void 0;
1124
- prevCleanup?.();
1125
- this._manualEmitUsed = false;
1126
- this._lastDepValues = depValues;
1127
- this._inspectorHook?.({ kind: "run", depValues });
1128
- const out = this._fn(depValues, this._actions);
1129
- if (isCleanupResult(out)) {
1130
- this._cleanup = out.cleanup;
1131
- if (this._manualEmitUsed) return;
1132
- if ("value" in out) {
1133
- this._downAutoValue(out.value);
1134
- }
1135
- 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 });
1136
1145
  }
1137
- if (isCleanupFn(out)) {
1138
- this._cleanup = out;
1139
- 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);
1140
1153
  }
1141
- if (this._manualEmitUsed) return;
1142
- if (out === void 0) return;
1143
- this._downAutoValue(out);
1144
- } catch (err) {
1145
- const errMsg = err instanceof Error ? err.message : String(err);
1146
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1147
- this._downInternal([[ERROR, wrapped]]);
1148
1154
  }
1149
1155
  }
1150
- _onDepDirty(index) {
1151
- const wasDirty = this._depDirtyMask.has(index);
1152
- this._depDirtyMask.set(index);
1153
- this._depSettledMask.clear(index);
1154
- if (!wasDirty) {
1155
- this._downInternal([[DIRTY]]);
1156
+ _upInternal(messages) {
1157
+ if (!this._hasDeps) return;
1158
+ for (const dep of this._deps) {
1159
+ dep.up?.(messages, { internal: true });
1156
1160
  }
1157
1161
  }
1158
- _onDepSettled(index) {
1159
- if (!this._depDirtyMask.has(index)) {
1160
- 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;
1161
1171
  }
1162
- this._depSettledMask.set(index);
1163
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1164
- this._depDirtyMask.reset();
1165
- this._depSettledMask.reset();
1172
+ if (this._fn) {
1166
1173
  this._runFn();
1174
+ return;
1167
1175
  }
1168
1176
  }
1169
- _maybeCompleteFromDeps() {
1170
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1171
- 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;
1172
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?.();
1173
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();
1219
+ }
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 ---
1174
1231
  _handleDepMessages(index, messages) {
1175
1232
  for (const msg of messages) {
1176
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1233
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1177
1234
  const t = msg[0];
1178
1235
  if (this._onMessage) {
1179
1236
  try {
1180
- 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
+ }
1181
1249
  } catch (err) {
1182
1250
  const errMsg = err instanceof Error ? err.message : String(err);
1183
1251
  const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
@@ -1187,6 +1255,7 @@ var NodeImpl = class {
1187
1255
  return;
1188
1256
  }
1189
1257
  }
1258
+ if (messageTier(t) < 1) continue;
1190
1259
  if (!this._fn) {
1191
1260
  if (t === COMPLETE && this._deps.length > 1) {
1192
1261
  this._depCompleteMask.set(index);
@@ -1230,53 +1299,85 @@ var NodeImpl = class {
1230
1299
  this._downInternal([msg]);
1231
1300
  }
1232
1301
  }
1233
- _connectUpstream() {
1234
- if (!this._hasDeps || this._connected) return;
1235
- this._connected = true;
1236
- this._depDirtyMask.reset();
1237
- this._depSettledMask.reset();
1238
- this._depCompleteMask.reset();
1239
- this._status = "settled";
1240
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
1241
- this._connecting = true;
1242
- try {
1243
- for (let i = 0; i < this._deps.length; i += 1) {
1244
- const dep = this._deps[i];
1245
- this._upstreamUnsubs.push(
1246
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
1247
- );
1248
- }
1249
- } finally {
1250
- 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]]);
1251
1308
  }
1252
- if (this._fn) {
1309
+ }
1310
+ _onDepSettled(index) {
1311
+ if (!this._depDirtyMask.has(index)) {
1312
+ this._onDepDirty(index);
1313
+ }
1314
+ this._depSettledMask.set(index);
1315
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1316
+ this._depDirtyMask.reset();
1317
+ this._depSettledMask.reset();
1253
1318
  this._runFn();
1254
1319
  }
1255
1320
  }
1256
- _stopProducer() {
1257
- if (!this._producerStarted) return;
1258
- this._producerStarted = false;
1259
- const producerCleanup = this._cleanup;
1260
- this._cleanup = void 0;
1261
- producerCleanup?.();
1262
- }
1263
- _startProducer() {
1264
- if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
1265
- this._producerStarted = true;
1266
- this._runFn();
1321
+ _maybeCompleteFromDeps() {
1322
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1323
+ this._downInternal([[COMPLETE]]);
1324
+ }
1267
1325
  }
1268
- _disconnectUpstream() {
1269
- if (!this._connected) return;
1270
- for (const unsub of this._upstreamUnsubs.splice(0)) {
1271
- 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]]);
1272
1376
  }
1273
- this._connected = false;
1274
- this._depDirtyMask.reset();
1275
- this._depSettledMask.reset();
1276
- this._depCompleteMask.reset();
1277
- this._status = "disconnected";
1278
1377
  }
1279
1378
  };
1379
+ var isNodeArray = (value) => Array.isArray(value);
1380
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
1280
1381
  function node(depsOrFn, fnOrOpts, optsArg) {
1281
1382
  const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
1282
1383
  const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
@@ -2125,449 +2226,272 @@ var import_common2 = require("@nestjs/common");
2125
2226
  var import_core2 = require("@nestjs/core");
2126
2227
 
2127
2228
  // src/core/dynamic-node.ts
2128
- var DynamicNodeImpl = class {
2129
- _optsName;
2130
- _registryName;
2131
- _describeKind;
2132
- meta;
2229
+ var MAX_RERUN = 16;
2230
+ var DynamicNodeImpl = class extends NodeBase {
2133
2231
  _fn;
2134
- _equals;
2135
- _resubscribable;
2136
- _resetOnTeardown;
2137
2232
  _autoComplete;
2138
- _onMessage;
2139
- _onResubscribe;
2140
- /** @internal — read by {@link describeNode} for `accessHintForGuard`. */
2141
- _guard;
2142
- _lastMutation;
2143
- _inspectorHook;
2144
- // Sink tracking
2145
- _sinkCount = 0;
2146
- _singleDepSinkCount = 0;
2147
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
2148
- // Actions object (for onMessage handler)
2149
- _actions;
2150
- _boundDownToSinks;
2151
- // Mutable state
2152
- _cached = NO_VALUE;
2153
- _status = "disconnected";
2154
- _terminal = false;
2155
- _connected = false;
2156
- _rewiring = false;
2157
- // re-entrancy guard
2158
2233
  // Dynamic deps tracking
2234
+ /** @internal Read by `describeNode`. */
2159
2235
  _deps = [];
2160
2236
  _depUnsubs = [];
2161
2237
  _depIndexMap = /* @__PURE__ */ new Map();
2162
- // node index in _deps
2163
- _dirtyBits = /* @__PURE__ */ new Set();
2164
- _settledBits = /* @__PURE__ */ new Set();
2165
- _completeBits = /* @__PURE__ */ new Set();
2166
- // Sinks
2167
- _sinks = null;
2238
+ _depDirtyBits = /* @__PURE__ */ new Set();
2239
+ _depSettledBits = /* @__PURE__ */ new Set();
2240
+ _depCompleteBits = /* @__PURE__ */ new Set();
2241
+ // Execution state
2242
+ _running = false;
2243
+ _rewiring = false;
2244
+ _bufferedDepMessages = [];
2245
+ _trackedValues = /* @__PURE__ */ new Map();
2246
+ _rerunCount = 0;
2168
2247
  constructor(fn, opts) {
2248
+ super(opts);
2169
2249
  this._fn = fn;
2170
- this._optsName = opts.name;
2171
- this._describeKind = opts.describeKind;
2172
- this._equals = opts.equals ?? Object.is;
2173
- this._resubscribable = opts.resubscribable ?? false;
2174
- this._resetOnTeardown = opts.resetOnTeardown ?? false;
2175
2250
  this._autoComplete = opts.completeWhenDepsComplete ?? true;
2176
- this._onMessage = opts.onMessage;
2177
- this._onResubscribe = opts.onResubscribe;
2178
- this._guard = opts.guard;
2179
- this._inspectorHook = void 0;
2180
- const meta = {};
2181
- for (const [k, v] of Object.entries(opts.meta ?? {})) {
2182
- meta[k] = node({
2183
- initial: v,
2184
- name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
2185
- describeKind: "state",
2186
- ...opts.guard != null ? { guard: opts.guard } : {}
2187
- });
2188
- }
2189
- Object.freeze(meta);
2190
- this.meta = meta;
2191
- const self = this;
2192
- this._actions = {
2193
- down(messages) {
2194
- self._downInternal(messages);
2195
- },
2196
- emit(value) {
2197
- self._downAutoValue(value);
2198
- },
2199
- up(messages) {
2200
- for (const dep of self._deps) {
2201
- dep.up?.(messages, { internal: true });
2202
- }
2203
- }
2204
- };
2205
- this._boundDownToSinks = this._downToSinks.bind(this);
2206
- }
2207
- get name() {
2208
- return this._registryName ?? this._optsName;
2209
- }
2210
- /** @internal */
2211
- _assignRegistryName(localName) {
2212
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
2213
- this._registryName = localName;
2214
- }
2215
- /**
2216
- * @internal Attach/remove inspector hook for graph-level observability.
2217
- * Returns a disposer that restores the previous hook.
2218
- */
2219
- _setInspectorHook(hook) {
2220
- const prev = this._inspectorHook;
2221
- this._inspectorHook = hook;
2222
- return () => {
2223
- if (this._inspectorHook === hook) {
2224
- this._inspectorHook = prev;
2225
- }
2226
- };
2227
- }
2228
- get status() {
2229
- return this._status;
2230
- }
2231
- get lastMutation() {
2232
- return this._lastMutation;
2233
- }
2234
- /** Versioning not yet supported on DynamicNodeImpl. */
2235
- get v() {
2236
- return void 0;
2237
- }
2238
- hasGuard() {
2239
- return this._guard != null;
2240
- }
2241
- allowsObserve(actor) {
2242
- if (this._guard == null) return true;
2243
- return this._guard(normalizeActor(actor), "observe");
2244
- }
2245
- get() {
2246
- return this._cached === NO_VALUE ? void 0 : this._cached;
2247
- }
2248
- down(messages, options) {
2249
- if (messages.length === 0) return;
2250
- if (!options?.internal && this._guard != null) {
2251
- const actor = normalizeActor(options?.actor);
2252
- const delivery = options?.delivery ?? "write";
2253
- const action = delivery === "signal" ? "signal" : "write";
2254
- if (!this._guard(actor, action)) {
2255
- throw new GuardDenied({ actor, action, nodeName: this.name });
2256
- }
2257
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
2258
- }
2259
- this._downInternal(messages);
2260
- }
2261
- _downInternal(messages) {
2262
- if (messages.length === 0) return;
2263
- let sinkMessages = messages;
2264
- if (this._terminal && !this._resubscribable) {
2265
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
2266
- if (pass.length === 0) return;
2267
- sinkMessages = pass;
2268
- }
2269
- this._handleLocalLifecycle(sinkMessages);
2270
- if (this._canSkipDirty()) {
2271
- let hasPhase2 = false;
2272
- for (let i = 0; i < sinkMessages.length; i++) {
2273
- const t = sinkMessages[i][0];
2274
- if (t === DATA || t === RESOLVED) {
2275
- hasPhase2 = true;
2276
- break;
2277
- }
2278
- }
2279
- if (hasPhase2) {
2280
- const filtered = [];
2281
- for (let i = 0; i < sinkMessages.length; i++) {
2282
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
2283
- }
2284
- if (filtered.length > 0) {
2285
- downWithBatch(this._boundDownToSinks, filtered);
2286
- }
2287
- return;
2288
- }
2289
- }
2290
- downWithBatch(this._boundDownToSinks, sinkMessages);
2291
- }
2292
- _canSkipDirty() {
2293
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
2294
- }
2295
- subscribe(sink, hints) {
2296
- if (hints?.actor != null && this._guard != null) {
2297
- const actor = normalizeActor(hints.actor);
2298
- if (!this._guard(actor, "observe")) {
2299
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
2300
- }
2301
- }
2302
- if (this._terminal && this._resubscribable) {
2303
- this._terminal = false;
2304
- this._cached = NO_VALUE;
2305
- this._status = "disconnected";
2306
- this._onResubscribe?.();
2307
- }
2308
- this._sinkCount += 1;
2309
- if (hints?.singleDep) {
2310
- this._singleDepSinkCount += 1;
2311
- this._singleDepSinks.add(sink);
2312
- }
2313
- if (this._sinks == null) {
2314
- this._sinks = sink;
2315
- } else if (typeof this._sinks === "function") {
2316
- this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
2317
- } else {
2318
- this._sinks.add(sink);
2319
- }
2320
- if (!this._connected) {
2321
- this._connect();
2322
- }
2323
- let removed = false;
2324
- return () => {
2325
- if (removed) return;
2326
- removed = true;
2327
- this._sinkCount -= 1;
2328
- if (this._singleDepSinks.has(sink)) {
2329
- this._singleDepSinkCount -= 1;
2330
- this._singleDepSinks.delete(sink);
2331
- }
2332
- if (this._sinks == null) return;
2333
- if (typeof this._sinks === "function") {
2334
- if (this._sinks === sink) this._sinks = null;
2335
- } else {
2336
- this._sinks.delete(sink);
2337
- if (this._sinks.size === 1) {
2338
- const [only] = this._sinks;
2339
- this._sinks = only;
2340
- } else if (this._sinks.size === 0) {
2341
- this._sinks = null;
2342
- }
2343
- }
2344
- if (this._sinks == null) {
2345
- this._disconnect();
2346
- }
2347
- };
2348
- }
2349
- up(messages, options) {
2350
- if (this._deps.length === 0) return;
2351
- if (!options?.internal && this._guard != null) {
2352
- const actor = normalizeActor(options?.actor);
2353
- if (!this._guard(actor, "write")) {
2354
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
2355
- }
2356
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
2357
- }
2358
- for (const dep of this._deps) {
2359
- dep.up?.(messages, options);
2360
- }
2361
- }
2362
- unsubscribe() {
2363
- this._disconnect();
2364
- }
2365
- // --- Private methods ---
2366
- _downToSinks(messages) {
2367
- if (this._sinks == null) return;
2368
- if (typeof this._sinks === "function") {
2369
- this._sinks(messages);
2370
- return;
2371
- }
2372
- const snapshot = [...this._sinks];
2373
- for (const sink of snapshot) {
2374
- sink(messages);
2375
- }
2251
+ this.down = this.down.bind(this);
2252
+ this.up = this.up.bind(this);
2376
2253
  }
2377
- _handleLocalLifecycle(messages) {
2378
- for (const m of messages) {
2379
- const t = m[0];
2380
- if (t === DATA) this._cached = m[1];
2381
- if (t === INVALIDATE) {
2382
- this._cached = NO_VALUE;
2383
- this._status = "dirty";
2384
- }
2385
- if (t === DATA) {
2386
- this._status = "settled";
2387
- } else if (t === RESOLVED) {
2388
- this._status = "resolved";
2389
- } else if (t === DIRTY) {
2390
- this._status = "dirty";
2391
- } else if (t === COMPLETE) {
2392
- this._status = "completed";
2393
- this._terminal = true;
2394
- } else if (t === ERROR) {
2395
- this._status = "errored";
2396
- this._terminal = true;
2397
- }
2398
- if (t === TEARDOWN) {
2399
- if (this._resetOnTeardown) this._cached = NO_VALUE;
2400
- try {
2401
- this._propagateToMeta(t);
2402
- } finally {
2403
- this._disconnect();
2404
- }
2405
- }
2406
- if (t !== TEARDOWN && propagatesToMeta(t)) {
2407
- this._propagateToMeta(t);
2408
- }
2409
- }
2254
+ _createMetaNode(key, initialValue, opts) {
2255
+ return node({
2256
+ initial: initialValue,
2257
+ name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
2258
+ describeKind: "state",
2259
+ ...opts.guard != null ? { guard: opts.guard } : {}
2260
+ });
2410
2261
  }
2411
- /** Propagate a signal to all companion meta nodes (best-effort). */
2412
- _propagateToMeta(t) {
2413
- for (const metaNode of Object.values(this.meta)) {
2414
- try {
2415
- metaNode.down([[t]], { internal: true });
2416
- } catch {
2262
+ /** Versioning not supported on DynamicNodeImpl (override base). */
2263
+ get v() {
2264
+ return void 0;
2265
+ }
2266
+ // --- Up / unsubscribe ---
2267
+ up(messages, options) {
2268
+ if (this._deps.length === 0) return;
2269
+ if (!options?.internal && this._guard != null) {
2270
+ const actor = normalizeActor(options?.actor);
2271
+ if (!this._guard(actor, "write")) {
2272
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
2417
2273
  }
2274
+ this._recordMutation(actor);
2418
2275
  }
2419
- }
2420
- _downAutoValue(value) {
2421
- const wasDirty = this._status === "dirty";
2422
- let unchanged;
2423
- try {
2424
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
2425
- } catch (eqErr) {
2426
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
2427
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
2428
- this._downInternal([[ERROR, wrapped]]);
2429
- return;
2276
+ for (const dep of this._deps) {
2277
+ dep.up?.(messages, options);
2430
2278
  }
2431
- if (unchanged) {
2432
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
2433
- return;
2279
+ }
2280
+ _upInternal(messages) {
2281
+ for (const dep of this._deps) {
2282
+ dep.up?.(messages, { internal: true });
2434
2283
  }
2435
- this._cached = value;
2436
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
2437
2284
  }
2438
- _connect() {
2439
- if (this._connected) return;
2440
- this._connected = true;
2441
- this._status = "settled";
2442
- this._dirtyBits.clear();
2443
- this._settledBits.clear();
2444
- this._completeBits.clear();
2285
+ unsubscribe() {
2286
+ this._disconnect();
2287
+ }
2288
+ // --- Activation hooks ---
2289
+ _onActivate() {
2445
2290
  this._runFn();
2446
2291
  }
2292
+ _doDeactivate() {
2293
+ this._disconnect();
2294
+ }
2447
2295
  _disconnect() {
2448
- if (!this._connected) return;
2449
2296
  for (const unsub of this._depUnsubs) unsub();
2450
2297
  this._depUnsubs = [];
2451
2298
  this._deps = [];
2452
2299
  this._depIndexMap.clear();
2453
- this._dirtyBits.clear();
2454
- this._settledBits.clear();
2455
- this._completeBits.clear();
2456
- this._connected = false;
2300
+ this._depDirtyBits.clear();
2301
+ this._depSettledBits.clear();
2302
+ this._depCompleteBits.clear();
2303
+ this._cached = NO_VALUE;
2457
2304
  this._status = "disconnected";
2458
2305
  }
2306
+ // --- Fn execution with rewire buffer ---
2459
2307
  _runFn() {
2460
2308
  if (this._terminal && !this._resubscribable) return;
2461
- if (this._rewiring) return;
2462
- const trackedDeps = [];
2463
- const trackedSet = /* @__PURE__ */ new Set();
2464
- const get = (dep) => {
2465
- if (!trackedSet.has(dep)) {
2466
- trackedSet.add(dep);
2467
- trackedDeps.push(dep);
2468
- }
2469
- return dep.get();
2470
- };
2309
+ if (this._running) return;
2310
+ this._running = true;
2311
+ this._rerunCount = 0;
2312
+ let result;
2471
2313
  try {
2472
- const depValues = [];
2473
- for (const dep of this._deps) {
2474
- depValues.push(dep.get());
2475
- }
2476
- this._inspectorHook?.({ kind: "run", depValues });
2477
- const result = this._fn(get);
2478
- this._rewire(trackedDeps);
2479
- if (result === void 0) return;
2480
- this._downAutoValue(result);
2481
- } catch (err) {
2482
- this._downInternal([[ERROR, err]]);
2314
+ for (; ; ) {
2315
+ const trackedDeps = [];
2316
+ const trackedValuesMap = /* @__PURE__ */ new Map();
2317
+ const trackedSet = /* @__PURE__ */ new Set();
2318
+ const get = (dep) => {
2319
+ if (!trackedSet.has(dep)) {
2320
+ trackedSet.add(dep);
2321
+ trackedDeps.push(dep);
2322
+ trackedValuesMap.set(dep, dep.get());
2323
+ }
2324
+ return dep.get();
2325
+ };
2326
+ this._trackedValues = trackedValuesMap;
2327
+ const depValues = [];
2328
+ for (const dep of this._deps) depValues.push(dep.get());
2329
+ this._emitInspectorHook({ kind: "run", depValues });
2330
+ try {
2331
+ result = this._fn(get);
2332
+ } catch (err) {
2333
+ const errMsg = err instanceof Error ? err.message : String(err);
2334
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
2335
+ cause: err
2336
+ });
2337
+ this._downInternal([[ERROR, wrapped]]);
2338
+ return;
2339
+ }
2340
+ this._rewiring = true;
2341
+ this._bufferedDepMessages = [];
2342
+ try {
2343
+ this._rewire(trackedDeps);
2344
+ } finally {
2345
+ this._rewiring = false;
2346
+ }
2347
+ let needsRerun = false;
2348
+ for (const entry of this._bufferedDepMessages) {
2349
+ for (const msg of entry.msgs) {
2350
+ if (msg[0] === DATA) {
2351
+ const dep = this._deps[entry.index];
2352
+ const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
2353
+ const actualValue = msg[1];
2354
+ if (!this._equals(trackedValue, actualValue)) {
2355
+ needsRerun = true;
2356
+ break;
2357
+ }
2358
+ }
2359
+ }
2360
+ if (needsRerun) break;
2361
+ }
2362
+ if (needsRerun) {
2363
+ this._rerunCount += 1;
2364
+ if (this._rerunCount > MAX_RERUN) {
2365
+ this._bufferedDepMessages = [];
2366
+ this._downInternal([
2367
+ [
2368
+ ERROR,
2369
+ new Error(
2370
+ `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
2371
+ )
2372
+ ]
2373
+ ]);
2374
+ return;
2375
+ }
2376
+ this._bufferedDepMessages = [];
2377
+ continue;
2378
+ }
2379
+ const drain = this._bufferedDepMessages;
2380
+ this._bufferedDepMessages = [];
2381
+ for (const entry of drain) {
2382
+ for (const msg of entry.msgs) {
2383
+ this._updateMasksForMessage(entry.index, msg);
2384
+ }
2385
+ }
2386
+ this._depDirtyBits.clear();
2387
+ this._depSettledBits.clear();
2388
+ break;
2389
+ }
2390
+ } finally {
2391
+ this._running = false;
2483
2392
  }
2393
+ this._downAutoValue(result);
2484
2394
  }
2485
2395
  _rewire(newDeps) {
2486
- this._rewiring = true;
2487
- try {
2488
- const oldMap = this._depIndexMap;
2489
- const newMap = /* @__PURE__ */ new Map();
2490
- const newUnsubs = [];
2491
- for (let i = 0; i < newDeps.length; i++) {
2492
- const dep = newDeps[i];
2493
- newMap.set(dep, i);
2494
- const oldIdx = oldMap.get(dep);
2495
- if (oldIdx !== void 0) {
2496
- newUnsubs.push(this._depUnsubs[oldIdx]);
2497
- this._depUnsubs[oldIdx] = () => {
2498
- };
2499
- } else {
2500
- const idx = i;
2501
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
2502
- newUnsubs.push(unsub);
2503
- }
2396
+ const oldMap = this._depIndexMap;
2397
+ const newMap = /* @__PURE__ */ new Map();
2398
+ const newUnsubs = [];
2399
+ for (let i = 0; i < newDeps.length; i++) {
2400
+ const dep = newDeps[i];
2401
+ newMap.set(dep, i);
2402
+ const oldIdx = oldMap.get(dep);
2403
+ if (oldIdx !== void 0) {
2404
+ newUnsubs.push(this._depUnsubs[oldIdx]);
2405
+ this._depUnsubs[oldIdx] = () => {
2406
+ };
2407
+ } else {
2408
+ const idx = i;
2409
+ const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
2410
+ newUnsubs.push(unsub);
2504
2411
  }
2505
- for (const [dep, oldIdx] of oldMap) {
2506
- if (!newMap.has(dep)) {
2507
- this._depUnsubs[oldIdx]();
2508
- }
2412
+ }
2413
+ for (const [dep, oldIdx] of oldMap) {
2414
+ if (!newMap.has(dep)) {
2415
+ this._depUnsubs[oldIdx]();
2509
2416
  }
2510
- this._deps = newDeps;
2511
- this._depUnsubs = newUnsubs;
2512
- this._depIndexMap = newMap;
2513
- this._dirtyBits.clear();
2514
- this._settledBits.clear();
2515
- const newCompleteBits = /* @__PURE__ */ new Set();
2516
- for (const oldIdx of this._completeBits) {
2517
- const dep = [...oldMap.entries()].find(([, idx]) => idx === oldIdx)?.[0];
2518
- if (dep && newMap.has(dep)) {
2417
+ }
2418
+ this._deps = newDeps;
2419
+ this._depUnsubs = newUnsubs;
2420
+ this._depIndexMap = newMap;
2421
+ this._depDirtyBits.clear();
2422
+ this._depSettledBits.clear();
2423
+ const newCompleteBits = /* @__PURE__ */ new Set();
2424
+ for (const oldIdx of this._depCompleteBits) {
2425
+ for (const [dep, idx] of oldMap) {
2426
+ if (idx === oldIdx && newMap.has(dep)) {
2519
2427
  newCompleteBits.add(newMap.get(dep));
2428
+ break;
2520
2429
  }
2521
2430
  }
2522
- this._completeBits = newCompleteBits;
2523
- } finally {
2524
- this._rewiring = false;
2525
2431
  }
2432
+ this._depCompleteBits = newCompleteBits;
2526
2433
  }
2434
+ // --- Dep message handling ---
2527
2435
  _handleDepMessages(index, messages) {
2528
- if (this._rewiring) return;
2436
+ if (this._rewiring) {
2437
+ this._bufferedDepMessages.push({ index, msgs: messages });
2438
+ return;
2439
+ }
2529
2440
  for (const msg of messages) {
2530
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
2441
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
2531
2442
  const t = msg[0];
2532
2443
  if (this._onMessage) {
2533
2444
  try {
2534
2445
  if (this._onMessage(msg, index, this._actions)) continue;
2535
2446
  } catch (err) {
2536
- this._downInternal([[ERROR, err]]);
2447
+ const errMsg = err instanceof Error ? err.message : String(err);
2448
+ const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
2449
+ cause: err
2450
+ });
2451
+ this._downInternal([[ERROR, wrapped]]);
2537
2452
  return;
2538
2453
  }
2539
2454
  }
2455
+ if (messageTier(t) < 1) continue;
2540
2456
  if (t === DIRTY) {
2541
- this._dirtyBits.add(index);
2542
- this._settledBits.delete(index);
2543
- if (this._dirtyBits.size === 1) {
2544
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
2457
+ const wasEmpty = this._depDirtyBits.size === 0;
2458
+ this._depDirtyBits.add(index);
2459
+ this._depSettledBits.delete(index);
2460
+ if (wasEmpty) {
2461
+ this._downInternal([[DIRTY]]);
2545
2462
  }
2546
2463
  continue;
2547
2464
  }
2548
2465
  if (t === DATA || t === RESOLVED) {
2549
- if (!this._dirtyBits.has(index)) {
2550
- this._dirtyBits.add(index);
2551
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
2466
+ if (!this._depDirtyBits.has(index)) {
2467
+ const wasEmpty = this._depDirtyBits.size === 0;
2468
+ this._depDirtyBits.add(index);
2469
+ if (wasEmpty) {
2470
+ this._downInternal([[DIRTY]]);
2471
+ }
2552
2472
  }
2553
- this._settledBits.add(index);
2473
+ this._depSettledBits.add(index);
2554
2474
  if (this._allDirtySettled()) {
2555
- this._dirtyBits.clear();
2556
- this._settledBits.clear();
2557
- this._runFn();
2475
+ this._depDirtyBits.clear();
2476
+ this._depSettledBits.clear();
2477
+ if (!this._running) {
2478
+ if (this._depValuesDifferFromTracked()) {
2479
+ this._runFn();
2480
+ }
2481
+ }
2558
2482
  }
2559
2483
  continue;
2560
2484
  }
2561
2485
  if (t === COMPLETE) {
2562
- this._completeBits.add(index);
2563
- this._dirtyBits.delete(index);
2564
- this._settledBits.delete(index);
2486
+ this._depCompleteBits.add(index);
2487
+ this._depDirtyBits.delete(index);
2488
+ this._depSettledBits.delete(index);
2565
2489
  if (this._allDirtySettled()) {
2566
- this._dirtyBits.clear();
2567
- this._settledBits.clear();
2568
- this._runFn();
2490
+ this._depDirtyBits.clear();
2491
+ this._depSettledBits.clear();
2492
+ if (!this._running) this._runFn();
2569
2493
  }
2570
- if (this._autoComplete && this._completeBits.size >= this._deps.length && this._deps.length > 0) {
2494
+ if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
2571
2495
  this._downInternal([[COMPLETE]]);
2572
2496
  }
2573
2497
  continue;
@@ -2583,13 +2507,46 @@ var DynamicNodeImpl = class {
2583
2507
  this._downInternal([msg]);
2584
2508
  }
2585
2509
  }
2510
+ /**
2511
+ * Update dep masks for a message without triggering `_runFn` — used
2512
+ * during post-rewire drain so the wave state is consistent with the
2513
+ * buffered activation cascade without recursing.
2514
+ */
2515
+ _updateMasksForMessage(index, msg) {
2516
+ const t = msg[0];
2517
+ if (t === DIRTY) {
2518
+ this._depDirtyBits.add(index);
2519
+ this._depSettledBits.delete(index);
2520
+ } else if (t === DATA || t === RESOLVED) {
2521
+ this._depDirtyBits.add(index);
2522
+ this._depSettledBits.add(index);
2523
+ } else if (t === COMPLETE) {
2524
+ this._depCompleteBits.add(index);
2525
+ this._depDirtyBits.delete(index);
2526
+ this._depSettledBits.delete(index);
2527
+ }
2528
+ }
2586
2529
  _allDirtySettled() {
2587
- if (this._dirtyBits.size === 0) return false;
2588
- for (const idx of this._dirtyBits) {
2589
- if (!this._settledBits.has(idx)) return false;
2530
+ if (this._depDirtyBits.size === 0) return false;
2531
+ for (const idx of this._depDirtyBits) {
2532
+ if (!this._depSettledBits.has(idx)) return false;
2590
2533
  }
2591
2534
  return true;
2592
2535
  }
2536
+ /**
2537
+ * True if any current dep value differs from what the last `_runFn`
2538
+ * saw via `get()`. Used to suppress redundant re-runs when deferred
2539
+ * handshake messages arrive after `_rewire` for a dep whose value
2540
+ * already matches `_trackedValues`.
2541
+ */
2542
+ _depValuesDifferFromTracked() {
2543
+ for (const dep of this._deps) {
2544
+ const current = dep.get();
2545
+ const tracked = this._trackedValues.get(dep);
2546
+ if (!this._equals(current, tracked)) return true;
2547
+ }
2548
+ return false;
2549
+ }
2593
2550
  };
2594
2551
 
2595
2552
  // src/core/meta.ts
@@ -2659,6 +2616,10 @@ function describeNode(node2, includeFields) {
2659
2616
  out.name = node2.name;
2660
2617
  }
2661
2618
  if (all || includeFields.has("value")) {
2619
+ const isSentinel = node2 instanceof NodeImpl && node2._cached === NO_VALUE || node2 instanceof DynamicNodeImpl && node2._cached === NO_VALUE;
2620
+ if (isSentinel) {
2621
+ out.sentinel = true;
2622
+ }
2662
2623
  try {
2663
2624
  out.value = node2.get();
2664
2625
  } catch {
@@ -2685,6 +2646,129 @@ function describeNode(node2, includeFields) {
2685
2646
  return out;
2686
2647
  }
2687
2648
 
2649
+ // src/graph/sizeof.ts
2650
+ var OVERHEAD = {
2651
+ object: 56,
2652
+ array: 64,
2653
+ string: 40,
2654
+ // header; content added separately
2655
+ number: 8,
2656
+ boolean: 4,
2657
+ null: 0,
2658
+ undefined: 0,
2659
+ symbol: 40,
2660
+ bigint: 16,
2661
+ function: 120,
2662
+ map: 72,
2663
+ set: 72,
2664
+ mapEntry: 40,
2665
+ setEntry: 24
2666
+ };
2667
+ function sizeof(value) {
2668
+ const seen = /* @__PURE__ */ new WeakSet();
2669
+ return _sizeof(value, seen);
2670
+ }
2671
+ function _sizeof(value, seen) {
2672
+ if (value == null) return 0;
2673
+ const t = typeof value;
2674
+ switch (t) {
2675
+ case "number":
2676
+ return OVERHEAD.number;
2677
+ case "boolean":
2678
+ return OVERHEAD.boolean;
2679
+ case "string":
2680
+ return OVERHEAD.string + value.length * 2;
2681
+ // UTF-16
2682
+ case "bigint":
2683
+ return OVERHEAD.bigint;
2684
+ case "symbol":
2685
+ return OVERHEAD.symbol;
2686
+ case "function":
2687
+ if (seen.has(value)) return 0;
2688
+ seen.add(value);
2689
+ return OVERHEAD.function;
2690
+ case "undefined":
2691
+ return 0;
2692
+ }
2693
+ const obj = value;
2694
+ if (seen.has(obj)) return 0;
2695
+ seen.add(obj);
2696
+ if (obj instanceof Map) {
2697
+ let size2 = OVERHEAD.map;
2698
+ for (const [k, v] of obj) {
2699
+ size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
2700
+ }
2701
+ return size2;
2702
+ }
2703
+ if (obj instanceof Set) {
2704
+ let size2 = OVERHEAD.set;
2705
+ for (const v of obj) {
2706
+ size2 += OVERHEAD.setEntry + _sizeof(v, seen);
2707
+ }
2708
+ return size2;
2709
+ }
2710
+ if (Array.isArray(obj)) {
2711
+ let size2 = OVERHEAD.array + obj.length * 8;
2712
+ for (const item of obj) {
2713
+ size2 += _sizeof(item, seen);
2714
+ }
2715
+ return size2;
2716
+ }
2717
+ if (obj instanceof ArrayBuffer) return obj.byteLength;
2718
+ if (ArrayBuffer.isView(obj)) return obj.byteLength;
2719
+ let size = OVERHEAD.object;
2720
+ const keys = Object.keys(obj);
2721
+ for (const key of keys) {
2722
+ size += OVERHEAD.string + key.length * 2;
2723
+ size += _sizeof(obj[key], seen);
2724
+ }
2725
+ return size;
2726
+ }
2727
+
2728
+ // src/graph/profile.ts
2729
+ function graphProfile(graph, opts) {
2730
+ const topN = opts?.topN ?? 10;
2731
+ const desc = graph.describe({ detail: "standard" });
2732
+ const targets = [];
2733
+ if (typeof graph._collectObserveTargets === "function") {
2734
+ graph._collectObserveTargets("", targets);
2735
+ }
2736
+ const pathToNode = /* @__PURE__ */ new Map();
2737
+ for (const [p, n] of targets) {
2738
+ pathToNode.set(p, n);
2739
+ }
2740
+ const profiles = [];
2741
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
2742
+ const nd = pathToNode.get(path);
2743
+ const impl = nd instanceof NodeImpl ? nd : null;
2744
+ const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
2745
+ const subscriberCount = impl ? impl._sinkCount : 0;
2746
+ const depCount = nodeDesc.deps?.length ?? 0;
2747
+ const isOrphanEffect = nodeDesc.type === "effect" && subscriberCount === 0;
2748
+ profiles.push({
2749
+ path,
2750
+ type: nodeDesc.type,
2751
+ status: nodeDesc.status ?? "unknown",
2752
+ valueSizeBytes,
2753
+ subscriberCount,
2754
+ depCount,
2755
+ isOrphanEffect
2756
+ });
2757
+ }
2758
+ const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
2759
+ const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
2760
+ const orphanEffects = profiles.filter((p) => p.isOrphanEffect);
2761
+ return {
2762
+ nodeCount: profiles.length,
2763
+ edgeCount: desc.edges.length,
2764
+ subgraphCount: desc.subgraphs.length,
2765
+ nodes: profiles,
2766
+ totalValueSizeBytes,
2767
+ hotspots,
2768
+ orphanEffects
2769
+ };
2770
+ }
2771
+
2688
2772
  // src/graph/graph.ts
2689
2773
  var PATH_SEP = "::";
2690
2774
  var GRAPH_META_SEGMENT = "__meta__";
@@ -2827,7 +2911,7 @@ var RingBuffer = class {
2827
2911
  return result;
2828
2912
  }
2829
2913
  };
2830
- var SPY_ANSI_THEME = {
2914
+ var OBSERVE_ANSI_THEME = {
2831
2915
  data: "\x1B[32m",
2832
2916
  dirty: "\x1B[33m",
2833
2917
  resolved: "\x1B[36m",
@@ -2837,7 +2921,7 @@ var SPY_ANSI_THEME = {
2837
2921
  path: "\x1B[90m",
2838
2922
  reset: "\x1B[0m"
2839
2923
  };
2840
- var SPY_NO_COLOR_THEME = {
2924
+ var OBSERVE_NO_COLOR_THEME = {
2841
2925
  data: "",
2842
2926
  dirty: "",
2843
2927
  resolved: "",
@@ -2857,9 +2941,9 @@ function describeData(value) {
2857
2941
  return "[unserializable]";
2858
2942
  }
2859
2943
  }
2860
- function resolveSpyTheme(theme) {
2861
- if (theme === "none") return SPY_NO_COLOR_THEME;
2862
- if (theme === "ansi" || theme == null) return SPY_ANSI_THEME;
2944
+ function resolveObserveTheme(theme) {
2945
+ if (theme === "none") return OBSERVE_NO_COLOR_THEME;
2946
+ if (theme === "ansi" || theme == null) return OBSERVE_ANSI_THEME;
2863
2947
  return {
2864
2948
  data: theme.data ?? "",
2865
2949
  dirty: theme.dirty ?? "",
@@ -3597,6 +3681,16 @@ var Graph = class _Graph {
3597
3681
  }
3598
3682
  return out;
3599
3683
  }
3684
+ /**
3685
+ * Snapshot-based resource profile: per-node stats, orphan effect detection,
3686
+ * memory hotspots. Zero runtime overhead — walks nodes on demand.
3687
+ *
3688
+ * @param opts - Optional `topN` for hotspot limit (default 10).
3689
+ * @returns Aggregate profile with per-node details, hotspots, and orphan effects.
3690
+ */
3691
+ resourceProfile(opts) {
3692
+ return graphProfile(this, opts);
3693
+ }
3600
3694
  _qualifyEdgeEndpoint(part, prefix) {
3601
3695
  if (part.includes(PATH_SEP)) return part;
3602
3696
  return prefix === "" ? part : `${prefix}${PATH_SEP}${part}`;
@@ -3630,9 +3724,13 @@ var Graph = class _Graph {
3630
3724
  if (actor2 != null && !target.allowsObserve(actor2)) {
3631
3725
  throw new GuardDenied({ actor: actor2, action: "observe", nodeName: path });
3632
3726
  }
3633
- const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full";
3634
- if (wantsStructured2 && _Graph.inspectorEnabled) {
3635
- return this._createObserveResult(path, target, resolved);
3727
+ const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full" || resolved.format != null;
3728
+ if (wantsStructured2) {
3729
+ const result = _Graph.inspectorEnabled ? this._createObserveResult(path, target, resolved) : this._createFallbackObserveResult(path, resolved);
3730
+ if (resolved.format != null) {
3731
+ this._attachFormatLogger(result, resolved);
3732
+ }
3733
+ return result;
3636
3734
  }
3637
3735
  return {
3638
3736
  subscribe(sink) {
@@ -3650,9 +3748,13 @@ var Graph = class _Graph {
3650
3748
  }
3651
3749
  const opts = resolveObserveDetail(pathOrOpts);
3652
3750
  const actor = opts.actor;
3653
- const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full";
3654
- if (wantsStructured && _Graph.inspectorEnabled) {
3655
- return this._createObserveResultForAll(opts);
3751
+ const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full" || opts.format != null;
3752
+ if (wantsStructured) {
3753
+ const result = _Graph.inspectorEnabled ? this._createObserveResultForAll(opts) : this._createFallbackObserveResultForAll(opts);
3754
+ if (opts.format != null) {
3755
+ this._attachFormatLogger(result, opts);
3756
+ }
3757
+ return result;
3656
3758
  }
3657
3759
  return {
3658
3760
  subscribe: (sink) => {
@@ -3690,12 +3792,13 @@ var Graph = class _Graph {
3690
3792
  dirtyCount: 0,
3691
3793
  resolvedCount: 0,
3692
3794
  events: [],
3693
- completedCleanly: false,
3694
- errored: false
3795
+ anyCompletedCleanly: false,
3796
+ anyErrored: false
3695
3797
  };
3696
3798
  let lastTriggerDepIndex;
3697
3799
  let lastRunDepValues;
3698
3800
  let detachInspectorHook;
3801
+ let batchSeq = 0;
3699
3802
  if ((causal || derived2) && target instanceof NodeImpl) {
3700
3803
  detachInspectorHook = target._setInspectorHook((event) => {
3701
3804
  if (event.kind === "dep_message") {
@@ -3708,15 +3811,16 @@ var Graph = class _Graph {
3708
3811
  type: "derived",
3709
3812
  path,
3710
3813
  dep_values: [...event.depValues],
3711
- ...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {}
3814
+ ...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {}
3712
3815
  });
3713
3816
  }
3714
3817
  });
3715
3818
  }
3716
3819
  const unsub = target.subscribe((msgs) => {
3820
+ batchSeq++;
3717
3821
  for (const m of msgs) {
3718
3822
  const t = m[0];
3719
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
3823
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
3720
3824
  const withCausal = causal && lastRunDepValues != null ? (() => {
3721
3825
  const triggerDep = lastTriggerDepIndex != null && lastTriggerDepIndex >= 0 && target instanceof NodeImpl ? target._deps[lastTriggerDepIndex] : void 0;
3722
3826
  const tv = triggerDep?.v;
@@ -3733,8 +3837,8 @@ var Graph = class _Graph {
3733
3837
  } else if (minimal) {
3734
3838
  if (t === DIRTY) result.dirtyCount++;
3735
3839
  else if (t === RESOLVED) result.resolvedCount++;
3736
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
3737
- else if (t === ERROR) result.errored = true;
3840
+ else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
3841
+ else if (t === ERROR) result.anyErrored = true;
3738
3842
  } else if (t === DIRTY) {
3739
3843
  result.dirtyCount++;
3740
3844
  result.events.push({ type: "dirty", path, ...base });
@@ -3742,10 +3846,10 @@ var Graph = class _Graph {
3742
3846
  result.resolvedCount++;
3743
3847
  result.events.push({ type: "resolved", path, ...base, ...withCausal });
3744
3848
  } else if (t === COMPLETE) {
3745
- if (!result.errored) result.completedCleanly = true;
3849
+ if (!result.anyErrored) result.anyCompletedCleanly = true;
3746
3850
  result.events.push({ type: "complete", path, ...base });
3747
3851
  } else if (t === ERROR) {
3748
- result.errored = true;
3852
+ result.anyErrored = true;
3749
3853
  result.events.push({ type: "error", path, data: m[1], ...base });
3750
3854
  }
3751
3855
  }
@@ -3765,11 +3869,14 @@ var Graph = class _Graph {
3765
3869
  get events() {
3766
3870
  return result.events;
3767
3871
  },
3768
- get completedCleanly() {
3769
- return result.completedCleanly;
3872
+ get anyCompletedCleanly() {
3873
+ return result.anyCompletedCleanly;
3874
+ },
3875
+ get anyErrored() {
3876
+ return result.anyErrored;
3770
3877
  },
3771
- get errored() {
3772
- return result.errored;
3878
+ get completedWithoutErrors() {
3879
+ return result.anyCompletedCleanly && !result.anyErrored;
3773
3880
  },
3774
3881
  dispose() {
3775
3882
  unsub();
@@ -3785,11 +3892,15 @@ var Graph = class _Graph {
3785
3892
  Object.assign(merged, extra);
3786
3893
  }
3787
3894
  const resolvedTarget = graph.resolve(basePath);
3788
- return graph._createObserveResult(
3895
+ const expanded = graph._createObserveResult(
3789
3896
  basePath,
3790
3897
  resolvedTarget,
3791
3898
  resolveObserveDetail(merged)
3792
3899
  );
3900
+ if (merged.format != null) {
3901
+ graph._attachFormatLogger(expanded, merged);
3902
+ }
3903
+ return expanded;
3793
3904
  }
3794
3905
  };
3795
3906
  }
@@ -3801,27 +3912,33 @@ var Graph = class _Graph {
3801
3912
  dirtyCount: 0,
3802
3913
  resolvedCount: 0,
3803
3914
  events: [],
3804
- completedCleanly: false,
3805
- errored: false
3915
+ anyCompletedCleanly: false,
3916
+ anyErrored: false
3806
3917
  };
3918
+ const nodeErrored = /* @__PURE__ */ new Set();
3807
3919
  const actor = options.actor;
3808
3920
  const targets = [];
3809
3921
  this._collectObserveTargets("", targets);
3810
3922
  targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
3811
3923
  const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
3924
+ let batchSeq = 0;
3812
3925
  const unsubs = picked.map(
3813
3926
  ([path, nd]) => nd.subscribe((msgs) => {
3927
+ batchSeq++;
3814
3928
  for (const m of msgs) {
3815
3929
  const t = m[0];
3816
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
3930
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
3817
3931
  if (t === DATA) {
3818
3932
  result.values[path] = m[1];
3819
3933
  result.events.push({ type: "data", path, data: m[1], ...base });
3820
3934
  } else if (minimal) {
3821
3935
  if (t === DIRTY) result.dirtyCount++;
3822
3936
  else if (t === RESOLVED) result.resolvedCount++;
3823
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
3824
- else if (t === ERROR) result.errored = true;
3937
+ else if (t === COMPLETE && !nodeErrored.has(path)) result.anyCompletedCleanly = true;
3938
+ else if (t === ERROR) {
3939
+ result.anyErrored = true;
3940
+ nodeErrored.add(path);
3941
+ }
3825
3942
  } else if (t === DIRTY) {
3826
3943
  result.dirtyCount++;
3827
3944
  result.events.push({ type: "dirty", path, ...base });
@@ -3829,10 +3946,11 @@ var Graph = class _Graph {
3829
3946
  result.resolvedCount++;
3830
3947
  result.events.push({ type: "resolved", path, ...base });
3831
3948
  } else if (t === COMPLETE) {
3832
- if (!result.errored) result.completedCleanly = true;
3949
+ if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
3833
3950
  result.events.push({ type: "complete", path, ...base });
3834
3951
  } else if (t === ERROR) {
3835
- result.errored = true;
3952
+ result.anyErrored = true;
3953
+ nodeErrored.add(path);
3836
3954
  result.events.push({ type: "error", path, data: m[1], ...base });
3837
3955
  }
3838
3956
  }
@@ -3852,11 +3970,14 @@ var Graph = class _Graph {
3852
3970
  get events() {
3853
3971
  return result.events;
3854
3972
  },
3855
- get completedCleanly() {
3856
- return result.completedCleanly;
3973
+ get anyCompletedCleanly() {
3974
+ return result.anyCompletedCleanly;
3857
3975
  },
3858
- get errored() {
3859
- return result.errored;
3976
+ get anyErrored() {
3977
+ return result.anyErrored;
3978
+ },
3979
+ get completedWithoutErrors() {
3980
+ return result.anyCompletedCleanly && !result.anyErrored;
3860
3981
  },
3861
3982
  dispose() {
3862
3983
  for (const u of unsubs) u();
@@ -3869,25 +3990,169 @@ var Graph = class _Graph {
3869
3990
  } else {
3870
3991
  Object.assign(merged, extra);
3871
3992
  }
3872
- return graph._createObserveResultForAll(resolveObserveDetail(merged));
3993
+ const expanded = graph._createObserveResultForAll(resolveObserveDetail(merged));
3994
+ if (merged.format != null) {
3995
+ graph._attachFormatLogger(expanded, merged);
3996
+ }
3997
+ return expanded;
3873
3998
  }
3874
3999
  };
3875
4000
  }
3876
4001
  /**
3877
- * Convenience live debugger over {@link Graph.observe}. Logs protocol events as they flow.
3878
- *
3879
- * Supports one-node (`path`) and graph-wide modes, event filtering, and JSON/pretty rendering.
3880
- * Color themes are built in (`ansi` / `none`) to avoid external dependencies.
3881
- *
3882
- * @param options - Spy configuration.
3883
- * @returns Disposable handle plus a structured observation accumulator.
4002
+ * Fallback ObserveResult for single-node when inspector is disabled but `format` is requested.
4003
+ * Subscribes to raw messages and accumulates events with timeline info.
4004
+ */
4005
+ _createFallbackObserveResult(path, options) {
4006
+ const timeline = options.timeline !== false;
4007
+ const acc = {
4008
+ values: {},
4009
+ dirtyCount: 0,
4010
+ resolvedCount: 0,
4011
+ events: [],
4012
+ anyCompletedCleanly: false,
4013
+ anyErrored: false
4014
+ };
4015
+ const target = this.resolve(path);
4016
+ let batchSeq = 0;
4017
+ const unsub = target.subscribe((msgs) => {
4018
+ batchSeq++;
4019
+ for (const m of msgs) {
4020
+ const t = m[0];
4021
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
4022
+ if (t === DATA) {
4023
+ acc.values[path] = m[1];
4024
+ acc.events.push({ type: "data", path, data: m[1], ...base });
4025
+ } else if (t === DIRTY) {
4026
+ acc.dirtyCount++;
4027
+ acc.events.push({ type: "dirty", path, ...base });
4028
+ } else if (t === RESOLVED) {
4029
+ acc.resolvedCount++;
4030
+ acc.events.push({ type: "resolved", path, ...base });
4031
+ } else if (t === COMPLETE) {
4032
+ if (!acc.anyErrored) acc.anyCompletedCleanly = true;
4033
+ acc.events.push({ type: "complete", path, ...base });
4034
+ } else if (t === ERROR) {
4035
+ acc.anyErrored = true;
4036
+ acc.events.push({ type: "error", path, data: m[1], ...base });
4037
+ }
4038
+ }
4039
+ });
4040
+ return {
4041
+ get values() {
4042
+ return acc.values;
4043
+ },
4044
+ get dirtyCount() {
4045
+ return acc.dirtyCount;
4046
+ },
4047
+ get resolvedCount() {
4048
+ return acc.resolvedCount;
4049
+ },
4050
+ get events() {
4051
+ return acc.events;
4052
+ },
4053
+ get anyCompletedCleanly() {
4054
+ return acc.anyCompletedCleanly;
4055
+ },
4056
+ get anyErrored() {
4057
+ return acc.anyErrored;
4058
+ },
4059
+ get completedWithoutErrors() {
4060
+ return acc.anyCompletedCleanly && !acc.anyErrored;
4061
+ },
4062
+ dispose() {
4063
+ unsub();
4064
+ },
4065
+ expand() {
4066
+ throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
4067
+ }
4068
+ };
4069
+ }
4070
+ /**
4071
+ * Fallback ObserveResult for graph-wide when inspector is disabled but `format` is requested.
4072
+ */
4073
+ _createFallbackObserveResultForAll(options) {
4074
+ const timeline = options.timeline !== false;
4075
+ const actor = options.actor;
4076
+ const acc = {
4077
+ values: {},
4078
+ dirtyCount: 0,
4079
+ resolvedCount: 0,
4080
+ events: [],
4081
+ anyCompletedCleanly: false,
4082
+ anyErrored: false
4083
+ };
4084
+ const nodeErrored = /* @__PURE__ */ new Set();
4085
+ const targets = [];
4086
+ this._collectObserveTargets("", targets);
4087
+ targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
4088
+ const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
4089
+ let batchSeq = 0;
4090
+ const unsubs = picked.map(
4091
+ ([path, nd]) => nd.subscribe((msgs) => {
4092
+ batchSeq++;
4093
+ for (const m of msgs) {
4094
+ const t = m[0];
4095
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
4096
+ if (t === DATA) {
4097
+ acc.values[path] = m[1];
4098
+ acc.events.push({ type: "data", path, data: m[1], ...base });
4099
+ } else if (t === DIRTY) {
4100
+ acc.dirtyCount++;
4101
+ acc.events.push({ type: "dirty", path, ...base });
4102
+ } else if (t === RESOLVED) {
4103
+ acc.resolvedCount++;
4104
+ acc.events.push({ type: "resolved", path, ...base });
4105
+ } else if (t === COMPLETE) {
4106
+ if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
4107
+ acc.events.push({ type: "complete", path, ...base });
4108
+ } else if (t === ERROR) {
4109
+ acc.anyErrored = true;
4110
+ nodeErrored.add(path);
4111
+ acc.events.push({ type: "error", path, data: m[1], ...base });
4112
+ }
4113
+ }
4114
+ })
4115
+ );
4116
+ return {
4117
+ get values() {
4118
+ return acc.values;
4119
+ },
4120
+ get dirtyCount() {
4121
+ return acc.dirtyCount;
4122
+ },
4123
+ get resolvedCount() {
4124
+ return acc.resolvedCount;
4125
+ },
4126
+ get events() {
4127
+ return acc.events;
4128
+ },
4129
+ get anyCompletedCleanly() {
4130
+ return acc.anyCompletedCleanly;
4131
+ },
4132
+ get anyErrored() {
4133
+ return acc.anyErrored;
4134
+ },
4135
+ get completedWithoutErrors() {
4136
+ return acc.anyCompletedCleanly && !acc.anyErrored;
4137
+ },
4138
+ dispose() {
4139
+ for (const u of unsubs) u();
4140
+ },
4141
+ expand() {
4142
+ throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
4143
+ }
4144
+ };
4145
+ }
4146
+ /**
4147
+ * Attaches a format logger to an ObserveResult, rendering events as they arrive.
4148
+ * Wraps the result's dispose to flush pending events.
3884
4149
  */
3885
- spy(options = {}) {
4150
+ _attachFormatLogger(result, options) {
4151
+ const format = options.format;
4152
+ const logger = options.logger ?? ((line) => console.log(line));
3886
4153
  const include = options.includeTypes ? new Set(options.includeTypes) : null;
3887
4154
  const exclude = options.excludeTypes ? new Set(options.excludeTypes) : null;
3888
- const theme = resolveSpyTheme(options.theme);
3889
- const format = options.format ?? "pretty";
3890
- const logger = options.logger ?? ((line) => console.log(line));
4155
+ const theme = resolveObserveTheme(options.theme);
3891
4156
  const shouldLog = (type) => {
3892
4157
  if (include?.has(type) === false) return false;
3893
4158
  if (exclude?.has(type) === true) return false;
@@ -3912,133 +4177,26 @@ var Graph = class _Graph {
3912
4177
  const batchPart = event.in_batch ? " [batch]" : "";
3913
4178
  return `${pathPart}${color}${event.type.toUpperCase()}${theme.reset}${dataPart}${triggerPart}${batchPart}`;
3914
4179
  };
3915
- if (!_Graph.inspectorEnabled) {
3916
- const timeline = options.timeline ?? true;
3917
- const acc = {
3918
- values: {},
3919
- dirtyCount: 0,
3920
- resolvedCount: 0,
3921
- events: [],
3922
- completedCleanly: false,
3923
- errored: false
3924
- };
3925
- let stop2 = () => {
3926
- };
3927
- const result2 = {
3928
- get values() {
3929
- return acc.values;
3930
- },
3931
- get dirtyCount() {
3932
- return acc.dirtyCount;
3933
- },
3934
- get resolvedCount() {
3935
- return acc.resolvedCount;
3936
- },
3937
- get events() {
3938
- return acc.events;
3939
- },
3940
- get completedCleanly() {
3941
- return acc.completedCleanly;
3942
- },
3943
- get errored() {
3944
- return acc.errored;
3945
- },
3946
- dispose() {
3947
- stop2();
3948
- },
3949
- expand() {
3950
- throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
3951
- }
3952
- };
3953
- const pushEvent = (path, message) => {
3954
- const t = message[0];
3955
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
3956
- let event;
3957
- if (t === DATA) {
3958
- if (path != null) acc.values[path] = message[1];
3959
- event = { type: "data", ...path != null ? { path } : {}, data: message[1], ...base };
3960
- } else if (t === DIRTY) {
3961
- acc.dirtyCount += 1;
3962
- event = { type: "dirty", ...path != null ? { path } : {}, ...base };
3963
- } else if (t === RESOLVED) {
3964
- acc.resolvedCount += 1;
3965
- event = { type: "resolved", ...path != null ? { path } : {}, ...base };
3966
- } else if (t === COMPLETE) {
3967
- if (!acc.errored) acc.completedCleanly = true;
3968
- event = { type: "complete", ...path != null ? { path } : {}, ...base };
3969
- } else if (t === ERROR) {
3970
- acc.errored = true;
3971
- event = {
3972
- type: "error",
3973
- ...path != null ? { path } : {},
3974
- data: message[1],
3975
- ...base
3976
- };
4180
+ let cursor = 0;
4181
+ const flush = () => {
4182
+ const events = result.events;
4183
+ while (cursor < events.length) {
4184
+ const event = events[cursor++];
4185
+ if (shouldLog(event.type)) {
4186
+ logger(renderEvent(event), event);
3977
4187
  }
3978
- if (!event) return;
3979
- acc.events.push(event);
3980
- if (!shouldLog(event.type)) return;
3981
- logger(renderEvent(event), event);
3982
- };
3983
- if (options.path != null) {
3984
- const stream2 = this.observe(options.path, {
3985
- actor: options.actor,
3986
- structured: false
3987
- });
3988
- stop2 = stream2.subscribe((messages) => {
3989
- for (const m of messages) {
3990
- pushEvent(options.path, m);
3991
- }
3992
- });
3993
- } else {
3994
- const stream2 = this.observe({ actor: options.actor, structured: false });
3995
- stop2 = stream2.subscribe((path, messages) => {
3996
- for (const m of messages) {
3997
- pushEvent(path, m);
3998
- }
3999
- });
4000
4188
  }
4001
- return {
4002
- result: result2,
4003
- dispose() {
4004
- result2.dispose();
4005
- }
4006
- };
4007
- }
4008
- const structuredObserveOptions = {
4009
- actor: options.actor,
4010
- structured: true,
4011
- ...options.timeline !== false ? { timeline: true } : {},
4012
- ...options.causal ? { causal: true } : {},
4013
- ...options.derived ? { derived: true } : {}
4014
4189
  };
4015
- const result = options.path != null ? this.observe(options.path, structuredObserveOptions) : this.observe(structuredObserveOptions);
4016
- let cursor = 0;
4017
- const flushNewEvents = () => {
4018
- const nextEvents = result.events.slice(cursor);
4019
- cursor = result.events.length;
4020
- for (const event of nextEvents) {
4021
- if (!shouldLog(event.type)) continue;
4022
- logger(renderEvent(event), event);
4023
- }
4190
+ const origPush = result.events.push;
4191
+ result.events.push = function(...items) {
4192
+ const ret = origPush.apply(this, items);
4193
+ flush();
4194
+ return ret;
4024
4195
  };
4025
- const stream = options.path != null ? this.observe(options.path, { actor: options.actor, structured: false }) : this.observe({ actor: options.actor, structured: false });
4026
- const stop = options.path != null ? stream.subscribe((messages) => {
4027
- if (messages.length > 0) {
4028
- flushNewEvents();
4029
- }
4030
- }) : stream.subscribe((_path, messages) => {
4031
- if (messages.length > 0) {
4032
- flushNewEvents();
4033
- }
4034
- });
4035
- return {
4036
- result,
4037
- dispose() {
4038
- stop();
4039
- flushNewEvents();
4040
- result.dispose();
4041
- }
4196
+ const origDispose = result.dispose.bind(result);
4197
+ result.dispose = () => {
4198
+ origDispose();
4199
+ flush();
4042
4200
  };
4043
4201
  }
4044
4202
  /**
@@ -4301,8 +4459,9 @@ var Graph = class _Graph {
4301
4459
  /**
4302
4460
  * Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
4303
4461
  *
4304
- * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 2 messages
4305
- * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1 control waves.
4462
+ * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 3 messages
4463
+ * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1/2 control
4464
+ * waves (`START`/`DIRTY`/`INVALIDATE`/`PAUSE`/`RESUME`).
4306
4465
  */
4307
4466
  autoCheckpoint(adapter, options = {}) {
4308
4467
  const debounceMs = Math.max(0, options.debounceMs ?? 500);
@@ -4349,7 +4508,7 @@ var Graph = class _Graph {
4349
4508
  timer = setTimeout(flush, debounceMs);
4350
4509
  };
4351
4510
  const off = this.observe().subscribe((path, messages) => {
4352
- const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 2);
4511
+ const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
4353
4512
  if (!triggeredByTier) return;
4354
4513
  if (options.filter) {
4355
4514
  const nd = this.resolve(path);
@@ -4433,33 +4592,21 @@ var Graph = class _Graph {
4433
4592
  // ——————————————————————————————————————————————————————————————
4434
4593
  /**
4435
4594
  * When `false`, structured observation options (`causal`, `timeline`),
4436
- * `annotate()`, and `traceLog()` are no-ops. Raw `observe()` always works.
4595
+ * and `trace()` writes are no-ops. Raw `observe()` always works.
4437
4596
  *
4438
4597
  * Default: `true` outside production (`process.env.NODE_ENV !== "production"`).
4439
4598
  */
4440
4599
  static inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
4441
4600
  _annotations = /* @__PURE__ */ new Map();
4442
4601
  _traceRing = new RingBuffer(1e3);
4443
- /**
4444
- * Attaches a reasoning annotation to a node — captures *why* an AI agent set a value.
4445
- *
4446
- * No-op when {@link Graph.inspectorEnabled} is `false`.
4447
- *
4448
- * @param path - Qualified node path.
4449
- * @param reason - Free-text note stored in the trace ring buffer.
4450
- */
4451
- annotate(path, reason) {
4452
- if (!_Graph.inspectorEnabled) return;
4453
- this.resolve(path);
4454
- this._annotations.set(path, reason);
4455
- this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
4456
- }
4457
- /**
4458
- * Returns a chronological log of all reasoning annotations (ring buffer).
4459
- *
4460
- * @returns `[]` when {@link Graph.inspectorEnabled} is `false`.
4461
- */
4462
- traceLog() {
4602
+ trace(path, reason) {
4603
+ if (path != null && reason != null) {
4604
+ if (!_Graph.inspectorEnabled) return;
4605
+ this.resolve(path);
4606
+ this._annotations.set(path, reason);
4607
+ this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
4608
+ return;
4609
+ }
4463
4610
  if (!_Graph.inspectorEnabled) return [];
4464
4611
  return this._traceRing.toArray();
4465
4612
  }
@@ -5084,11 +5231,8 @@ var GraphReflyModule = _GraphReflyModule;
5084
5231
  getActor,
5085
5232
  getGraphToken,
5086
5233
  getNodeToken,
5087
- observeGraph$,
5088
- observeNode$,
5089
5234
  observeSSE,
5090
5235
  observeSubscription,
5091
- toMessages$,
5092
5236
  toObservable
5093
5237
  });
5094
5238
  //# sourceMappingURL=index.cjs.map