@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
@@ -110,6 +110,7 @@ function replayWAL(entries) {
110
110
  }
111
111
 
112
112
  // src/core/messages.ts
113
+ var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
113
114
  var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
114
115
  var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
115
116
  var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
@@ -119,13 +120,27 @@ var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
119
120
  var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
120
121
  var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
121
122
  var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
123
+ var knownMessageTypes = [
124
+ START,
125
+ DATA,
126
+ DIRTY,
127
+ RESOLVED,
128
+ INVALIDATE,
129
+ PAUSE,
130
+ RESUME,
131
+ TEARDOWN,
132
+ COMPLETE,
133
+ ERROR
134
+ ];
135
+ var knownMessageSet = new Set(knownMessageTypes);
122
136
  function messageTier(t) {
123
- if (t === DIRTY || t === INVALIDATE) return 0;
124
- if (t === PAUSE || t === RESUME) return 1;
125
- if (t === DATA || t === RESOLVED) return 2;
126
- if (t === COMPLETE || t === ERROR) return 3;
127
- if (t === TEARDOWN) return 4;
128
- return 0;
137
+ if (t === START) return 0;
138
+ if (t === DIRTY || t === INVALIDATE) return 1;
139
+ if (t === PAUSE || t === RESUME) return 2;
140
+ if (t === DATA || t === RESOLVED) return 3;
141
+ if (t === COMPLETE || t === ERROR) return 4;
142
+ if (t === TEARDOWN) return 5;
143
+ return 1;
129
144
  }
130
145
  function isPhase2Message(msg) {
131
146
  const t = msg[0];
@@ -213,14 +228,14 @@ function _downSequential(sink, messages, phase = 2) {
213
228
  const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
214
229
  for (const msg of messages) {
215
230
  const tier = messageTier(msg[0]);
216
- if (tier === 2) {
231
+ if (tier === 3) {
217
232
  if (isBatching()) {
218
233
  const m = msg;
219
234
  dataQueue.push(() => sink([m]));
220
235
  } else {
221
236
  sink([msg]);
222
237
  }
223
- } else if (tier >= 3) {
238
+ } else if (tier >= 4) {
224
239
  if (isBatching()) {
225
240
  const m = msg;
226
241
  pendingPhase3.push(() => sink([m]));
@@ -339,10 +354,24 @@ function advanceVersion(info, newValue, hashFn) {
339
354
  }
340
355
  }
341
356
 
342
- // src/core/node.ts
357
+ // src/core/node-base.ts
343
358
  var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
344
359
  var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
345
- function createIntBitSet() {
360
+ var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
361
+ var isCleanupFn = (value) => typeof value === "function";
362
+ function statusAfterMessage(status, msg) {
363
+ const t = msg[0];
364
+ if (t === DIRTY) return "dirty";
365
+ if (t === DATA) return "settled";
366
+ if (t === RESOLVED) return "resolved";
367
+ if (t === COMPLETE) return "completed";
368
+ if (t === ERROR) return "errored";
369
+ if (t === INVALIDATE) return "dirty";
370
+ if (t === TEARDOWN) return "disconnected";
371
+ return status;
372
+ }
373
+ function createIntBitSet(size) {
374
+ const fullMask = size >= 32 ? -1 : ~(-1 << size);
346
375
  let bits = 0;
347
376
  return {
348
377
  set(i) {
@@ -355,7 +384,8 @@ function createIntBitSet() {
355
384
  return (bits & 1 << i) !== 0;
356
385
  },
357
386
  covers(other) {
358
- return (bits & other._bits()) === other._bits();
387
+ const otherBits = other._bits();
388
+ return (bits & otherBits) === otherBits;
359
389
  },
360
390
  any() {
361
391
  return bits !== 0;
@@ -363,6 +393,9 @@ function createIntBitSet() {
363
393
  reset() {
364
394
  bits = 0;
365
395
  },
396
+ setAll() {
397
+ bits = fullMask;
398
+ },
366
399
  _bits() {
367
400
  return bits;
368
401
  }
@@ -370,6 +403,8 @@ function createIntBitSet() {
370
403
  }
371
404
  function createArrayBitSet(size) {
372
405
  const words = new Uint32Array(Math.ceil(size / 32));
406
+ const lastBits = size % 32;
407
+ const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
373
408
  return {
374
409
  set(i) {
375
410
  words[i >>> 5] |= 1 << (i & 31);
@@ -396,130 +431,103 @@ function createArrayBitSet(size) {
396
431
  reset() {
397
432
  words.fill(0);
398
433
  },
434
+ setAll() {
435
+ for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
436
+ if (words.length > 0) words[words.length - 1] = lastWordMask;
437
+ },
399
438
  _words: words
400
439
  };
401
440
  }
402
441
  function createBitSet(size) {
403
- return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
442
+ return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
404
443
  }
405
- var isNodeArray = (value) => Array.isArray(value);
406
- var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
407
- var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
408
- var isCleanupFn = (value) => typeof value === "function";
409
- var statusAfterMessage = (status, msg) => {
410
- const t = msg[0];
411
- if (t === DIRTY) return "dirty";
412
- if (t === DATA) return "settled";
413
- if (t === RESOLVED) return "resolved";
414
- if (t === COMPLETE) return "completed";
415
- if (t === ERROR) return "errored";
416
- if (t === INVALIDATE) return "dirty";
417
- if (t === TEARDOWN) return "disconnected";
418
- return status;
419
- };
420
- var NodeImpl = class {
421
- // --- Configuration (set once, never reassigned) ---
444
+ var NodeBase = class {
445
+ // --- Identity (set once) ---
422
446
  _optsName;
423
447
  _registryName;
424
- /** @internal read by {@link describeNode} before inference. */
448
+ /** @internal Read by `describeNode` before inference. */
425
449
  _describeKind;
426
450
  meta;
427
- _deps;
428
- _fn;
429
- _opts;
451
+ // --- Options ---
430
452
  _equals;
453
+ _resubscribable;
454
+ _resetOnTeardown;
455
+ _onResubscribe;
431
456
  _onMessage;
432
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
457
+ /** @internal Read by `describeNode` for `accessHintForGuard`. */
433
458
  _guard;
459
+ /** @internal Subclasses update this through {@link _recordMutation}. */
434
460
  _lastMutation;
435
- _hasDeps;
436
- _autoComplete;
437
- _isSingleDep;
438
- // --- Mutable state ---
461
+ // --- Versioning ---
462
+ _hashFn;
463
+ _versioning;
464
+ // --- Lifecycle state ---
465
+ /** @internal Read by `describeNode` and `graph.ts`. */
439
466
  _cached;
467
+ /** @internal Read externally via `get status()`. */
440
468
  _status;
441
469
  _terminal = false;
442
- _connected = false;
443
- _producerStarted = false;
444
- _connecting = false;
445
- _manualEmitUsed = false;
470
+ _active = false;
471
+ // --- Sink storage ---
472
+ /** @internal Read by `graph/profile.ts` for subscriber counts. */
446
473
  _sinkCount = 0;
447
474
  _singleDepSinkCount = 0;
448
- // --- Object/collection state ---
449
- _depDirtyMask;
450
- _depSettledMask;
451
- _depCompleteMask;
452
- _allDepsCompleteMask;
453
- _lastDepValues;
454
- _cleanup;
455
- _sinks = null;
456
475
  _singleDepSinks = /* @__PURE__ */ new WeakSet();
457
- _upstreamUnsubs = [];
476
+ _sinks = null;
477
+ // --- Actions + bound helpers ---
458
478
  _actions;
459
479
  _boundDownToSinks;
480
+ // --- Inspector hook (Graph observability) ---
460
481
  _inspectorHook;
461
- _versioning;
462
- _hashFn;
463
- constructor(deps, fn, opts) {
464
- this._deps = deps;
465
- this._fn = fn;
466
- this._opts = opts;
482
+ constructor(opts) {
467
483
  this._optsName = opts.name;
468
484
  this._describeKind = opts.describeKind;
469
485
  this._equals = opts.equals ?? Object.is;
486
+ this._resubscribable = opts.resubscribable ?? false;
487
+ this._resetOnTeardown = opts.resetOnTeardown ?? false;
488
+ this._onResubscribe = opts.onResubscribe;
470
489
  this._onMessage = opts.onMessage;
471
490
  this._guard = opts.guard;
472
- this._hasDeps = deps.length > 0;
473
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
474
- this._isSingleDep = deps.length === 1 && fn != null;
475
491
  this._cached = "initial" in opts ? opts.initial : NO_VALUE;
476
- this._status = this._hasDeps ? "disconnected" : "settled";
492
+ this._status = "disconnected";
477
493
  this._hashFn = opts.versioningHash ?? defaultHash;
478
494
  this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
479
495
  id: opts.versioningId,
480
496
  hash: this._hashFn
481
497
  }) : void 0;
482
- this._depDirtyMask = createBitSet(deps.length);
483
- this._depSettledMask = createBitSet(deps.length);
484
- this._depCompleteMask = createBitSet(deps.length);
485
- this._allDepsCompleteMask = createBitSet(deps.length);
486
- for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
487
498
  const meta = {};
488
499
  for (const [k, v] of Object.entries(opts.meta ?? {})) {
489
- meta[k] = node({
490
- initial: v,
491
- name: `${opts.name ?? "node"}:meta:${k}`,
492
- describeKind: "state",
493
- ...opts.guard != null ? { guard: opts.guard } : {}
494
- });
500
+ meta[k] = this._createMetaNode(k, v, opts);
495
501
  }
496
502
  Object.freeze(meta);
497
503
  this.meta = meta;
498
504
  const self = this;
499
505
  this._actions = {
500
506
  down(messages) {
501
- self._manualEmitUsed = true;
507
+ self._onManualEmit();
502
508
  self._downInternal(messages);
503
509
  },
504
510
  emit(value) {
505
- self._manualEmitUsed = true;
511
+ self._onManualEmit();
506
512
  self._downAutoValue(value);
507
513
  },
508
514
  up(messages) {
509
515
  self._upInternal(messages);
510
516
  }
511
517
  };
512
- this.down = this.down.bind(this);
513
- this.up = this.up.bind(this);
514
518
  this._boundDownToSinks = this._downToSinks.bind(this);
515
519
  }
520
+ /**
521
+ * Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
522
+ * {@link NodeImpl} overrides to set `_manualEmitUsed`.
523
+ */
524
+ _onManualEmit() {
525
+ }
526
+ // --- Identity getters ---
516
527
  get name() {
517
528
  return this._registryName ?? this._optsName;
518
529
  }
519
- /**
520
- * When a node is registered with {@link Graph.add} without an options `name`,
521
- * the graph assigns the registry local name for introspection (parity with graphrefly-py).
522
- */
530
+ /** @internal Assigned by `Graph.add` when registered without an options `name`. */
523
531
  _assignRegistryName(localName) {
524
532
  if (this._optsName !== void 0 || this._registryName !== void 0) return;
525
533
  this._registryName = localName;
@@ -537,7 +545,10 @@ var NodeImpl = class {
537
545
  }
538
546
  };
539
547
  }
540
- // --- Public interface (Node<T>) ---
548
+ /** @internal Used by subclasses to surface inspector events. */
549
+ _emitInspectorHook(event) {
550
+ this._inspectorHook?.(event);
551
+ }
541
552
  get status() {
542
553
  return this._status;
543
554
  }
@@ -547,15 +558,7 @@ var NodeImpl = class {
547
558
  get v() {
548
559
  return this._versioning;
549
560
  }
550
- /**
551
- * Retroactively apply versioning to a node that was created without it.
552
- * No-op if versioning is already enabled.
553
- *
554
- * Version starts at 0 regardless of prior DATA emissions — it tracks
555
- * changes from the moment versioning is enabled, not historical ones.
556
- *
557
- * @internal — used by {@link Graph.setVersioning}.
558
- */
561
+ /** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
559
562
  _applyVersioning(level, opts) {
560
563
  if (this._versioning != null) return;
561
564
  this._hashFn = opts?.hash ?? this._hashFn;
@@ -575,6 +578,7 @@ var NodeImpl = class {
575
578
  if (this._guard == null) return true;
576
579
  return this._guard(normalizeActor(actor), "observe");
577
580
  }
581
+ // --- Public transport ---
578
582
  get() {
579
583
  return this._cached === NO_VALUE ? void 0 : this._cached;
580
584
  }
@@ -587,43 +591,25 @@ var NodeImpl = class {
587
591
  if (!this._guard(actor, action)) {
588
592
  throw new GuardDenied({ actor, action, nodeName: this.name });
589
593
  }
590
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
594
+ this._recordMutation(actor);
591
595
  }
592
596
  this._downInternal(messages);
593
597
  }
594
- _downInternal(messages) {
595
- if (messages.length === 0) return;
596
- let lifecycleMessages = messages;
597
- let sinkMessages = messages;
598
- if (this._terminal && !this._opts.resubscribable) {
599
- const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
600
- if (terminalPassthrough.length === 0) return;
601
- lifecycleMessages = terminalPassthrough;
602
- sinkMessages = terminalPassthrough;
603
- }
604
- this._handleLocalLifecycle(lifecycleMessages);
605
- if (this._canSkipDirty()) {
606
- let hasPhase2 = false;
607
- for (let i = 0; i < sinkMessages.length; i++) {
608
- const t = sinkMessages[i][0];
609
- if (t === DATA || t === RESOLVED) {
610
- hasPhase2 = true;
611
- break;
612
- }
613
- }
614
- if (hasPhase2) {
615
- const filtered = [];
616
- for (let i = 0; i < sinkMessages.length; i++) {
617
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
618
- }
619
- if (filtered.length > 0) {
620
- downWithBatch(this._boundDownToSinks, filtered);
621
- }
622
- return;
623
- }
624
- }
625
- downWithBatch(this._boundDownToSinks, sinkMessages);
598
+ /** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
599
+ _recordMutation(actor) {
600
+ this._lastMutation = { actor, timestamp_ns: wallClockNs() };
601
+ }
602
+ /**
603
+ * At-most-once deactivation guard. Both TEARDOWN (eager) and
604
+ * unsubscribe-body (lazy) call this. The `_active` flag ensures
605
+ * `_doDeactivate` runs exactly once per activation cycle.
606
+ */
607
+ _onDeactivate() {
608
+ if (!this._active) return;
609
+ this._active = false;
610
+ this._doDeactivate();
626
611
  }
612
+ // --- Subscribe (uniform across node shapes) ---
627
613
  subscribe(sink, hints) {
628
614
  if (hints?.actor != null && this._guard != null) {
629
615
  const actor = normalizeActor(hints.actor);
@@ -631,17 +617,21 @@ var NodeImpl = class {
631
617
  throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
632
618
  }
633
619
  }
634
- if (this._terminal && this._opts.resubscribable) {
620
+ if (this._terminal && this._resubscribable) {
635
621
  this._terminal = false;
636
622
  this._cached = NO_VALUE;
637
- this._status = this._hasDeps ? "disconnected" : "settled";
638
- this._opts.onResubscribe?.();
623
+ this._status = "disconnected";
624
+ this._onResubscribe?.();
639
625
  }
640
626
  this._sinkCount += 1;
641
627
  if (hints?.singleDep) {
642
628
  this._singleDepSinkCount += 1;
643
629
  this._singleDepSinks.add(sink);
644
630
  }
631
+ if (!this._terminal) {
632
+ const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
633
+ downWithBatch(sink, startMessages);
634
+ }
645
635
  if (this._sinks == null) {
646
636
  this._sinks = sink;
647
637
  } else if (typeof this._sinks === "function") {
@@ -649,10 +639,12 @@ var NodeImpl = class {
649
639
  } else {
650
640
  this._sinks.add(sink);
651
641
  }
652
- if (this._hasDeps) {
653
- this._connectUpstream();
654
- } else if (this._fn) {
655
- this._startProducer();
642
+ if (this._sinkCount === 1 && !this._terminal) {
643
+ this._active = true;
644
+ this._onActivate();
645
+ }
646
+ if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
647
+ this._status = "pending";
656
648
  }
657
649
  let removed = false;
658
650
  return () => {
@@ -676,39 +668,49 @@ var NodeImpl = class {
676
668
  }
677
669
  }
678
670
  if (this._sinks == null) {
679
- this._disconnectUpstream();
680
- this._stopProducer();
671
+ this._onDeactivate();
681
672
  }
682
673
  };
683
674
  }
684
- up(messages, options) {
685
- if (!this._hasDeps) return;
686
- if (!options?.internal && this._guard != null) {
687
- const actor = normalizeActor(options?.actor);
688
- if (!this._guard(actor, "write")) {
689
- throw new GuardDenied({ actor, action: "write", nodeName: this.name });
690
- }
691
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
675
+ // --- Down pipeline ---
676
+ /**
677
+ * Core outgoing dispatch. Applies terminal filter + local lifecycle
678
+ * update, then hands messages to `downWithBatch` for tier-aware delivery.
679
+ */
680
+ _downInternal(messages) {
681
+ if (messages.length === 0) return;
682
+ let sinkMessages = messages;
683
+ if (this._terminal && !this._resubscribable) {
684
+ const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
685
+ if (pass.length === 0) return;
686
+ sinkMessages = pass;
692
687
  }
693
- for (const dep of this._deps) {
694
- if (options === void 0) {
695
- dep.up?.(messages);
696
- } else {
697
- dep.up?.(messages, options);
688
+ this._handleLocalLifecycle(sinkMessages);
689
+ if (this._canSkipDirty()) {
690
+ let hasPhase2 = false;
691
+ for (let i = 0; i < sinkMessages.length; i++) {
692
+ const t = sinkMessages[i][0];
693
+ if (t === DATA || t === RESOLVED) {
694
+ hasPhase2 = true;
695
+ break;
696
+ }
697
+ }
698
+ if (hasPhase2) {
699
+ const filtered = [];
700
+ for (let i = 0; i < sinkMessages.length; i++) {
701
+ if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
702
+ }
703
+ if (filtered.length > 0) {
704
+ downWithBatch(this._boundDownToSinks, filtered);
705
+ }
706
+ return;
698
707
  }
699
708
  }
709
+ downWithBatch(this._boundDownToSinks, sinkMessages);
700
710
  }
701
- _upInternal(messages) {
702
- if (!this._hasDeps) return;
703
- for (const dep of this._deps) {
704
- dep.up?.(messages, { internal: true });
705
- }
706
- }
707
- unsubscribe() {
708
- if (!this._hasDeps) return;
709
- this._disconnectUpstream();
711
+ _canSkipDirty() {
712
+ return this._sinkCount === 1 && this._singleDepSinkCount === 1;
710
713
  }
711
- // --- Private methods (prototype, _ prefix) ---
712
714
  _downToSinks(messages) {
713
715
  if (this._sinks == null) return;
714
716
  if (typeof this._sinks === "function") {
@@ -720,6 +722,11 @@ var NodeImpl = class {
720
722
  sink(messages);
721
723
  }
722
724
  }
725
+ /**
726
+ * Update `_cached`, `_status`, `_terminal` from message batch before
727
+ * delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
728
+ * {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
729
+ */
723
730
  _handleLocalLifecycle(messages) {
724
731
  for (const m of messages) {
725
732
  const t = m[0];
@@ -733,28 +740,22 @@ var NodeImpl = class {
733
740
  }
734
741
  }
735
742
  if (t === INVALIDATE) {
736
- const cleanupFn = this._cleanup;
737
- this._cleanup = void 0;
738
- cleanupFn?.();
743
+ this._onInvalidate();
739
744
  this._cached = NO_VALUE;
740
- this._lastDepValues = void 0;
741
745
  }
742
746
  this._status = statusAfterMessage(this._status, m);
743
747
  if (t === COMPLETE || t === ERROR) {
744
748
  this._terminal = true;
745
749
  }
746
750
  if (t === TEARDOWN) {
747
- if (this._opts.resetOnTeardown) {
751
+ if (this._resetOnTeardown) {
748
752
  this._cached = NO_VALUE;
749
753
  }
750
- const teardownCleanup = this._cleanup;
751
- this._cleanup = void 0;
752
- teardownCleanup?.();
754
+ this._onTeardown();
753
755
  try {
754
756
  this._propagateToMeta(t);
755
757
  } finally {
756
- this._disconnectUpstream();
757
- this._stopProducer();
758
+ this._onDeactivate();
758
759
  }
759
760
  }
760
761
  if (t !== TEARDOWN && propagatesToMeta(t)) {
@@ -762,7 +763,20 @@ var NodeImpl = class {
762
763
  }
763
764
  }
764
765
  }
765
- /** Propagate a signal to all companion meta nodes (best-effort). */
766
+ /**
767
+ * Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
768
+ * cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
769
+ * drop `_lastDepValues` so the next wave re-runs fn.
770
+ */
771
+ _onInvalidate() {
772
+ }
773
+ /**
774
+ * Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
775
+ * {@link NodeImpl} uses this to run any pending cleanup fn.
776
+ */
777
+ _onTeardown() {
778
+ }
779
+ /** Forward a signal to all companion meta nodes (best-effort). */
766
780
  _propagateToMeta(t) {
767
781
  for (const metaNode of Object.values(this.meta)) {
768
782
  try {
@@ -771,9 +785,10 @@ var NodeImpl = class {
771
785
  }
772
786
  }
773
787
  }
774
- _canSkipDirty() {
775
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
776
- }
788
+ /**
789
+ * Frame a computed value into the right protocol messages and dispatch
790
+ * via `_downInternal`. Used by `_runFn` and `actions.emit`.
791
+ */
777
792
  _downAutoValue(value) {
778
793
  const wasDirty = this._status === "dirty";
779
794
  let unchanged;
@@ -781,7 +796,9 @@ var NodeImpl = class {
781
796
  unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
782
797
  } catch (eqErr) {
783
798
  const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
784
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
799
+ const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
800
+ cause: eqErr
801
+ });
785
802
  this._downInternal([[ERROR, wrapped]]);
786
803
  return;
787
804
  }
@@ -791,89 +808,173 @@ var NodeImpl = class {
791
808
  }
792
809
  this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
793
810
  }
794
- _runFn() {
795
- if (!this._fn) return;
796
- if (this._terminal && !this._opts.resubscribable) return;
797
- if (this._connecting) return;
798
- try {
799
- const n = this._deps.length;
800
- const depValues = new Array(n);
801
- for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
802
- const prev = this._lastDepValues;
803
- if (n > 0 && prev != null && prev.length === n) {
804
- let allSame = true;
805
- for (let i = 0; i < n; i++) {
806
- if (!Object.is(depValues[i], prev[i])) {
807
- allSame = false;
808
- break;
809
- }
810
- }
811
- if (allSame) {
812
- if (this._status === "dirty") {
813
- this._downInternal([[RESOLVED]]);
814
- }
815
- return;
816
- }
817
- }
818
- const prevCleanup = this._cleanup;
819
- this._cleanup = void 0;
820
- prevCleanup?.();
821
- this._manualEmitUsed = false;
822
- this._lastDepValues = depValues;
823
- this._inspectorHook?.({ kind: "run", depValues });
824
- const out = this._fn(depValues, this._actions);
825
- if (isCleanupResult(out)) {
826
- this._cleanup = out.cleanup;
827
- if (this._manualEmitUsed) return;
828
- if ("value" in out) {
829
- this._downAutoValue(out.value);
830
- }
831
- return;
832
- }
833
- if (isCleanupFn(out)) {
834
- this._cleanup = out;
835
- return;
836
- }
837
- if (this._manualEmitUsed) return;
838
- if (out === void 0) return;
839
- this._downAutoValue(out);
840
- } catch (err) {
841
- const errMsg = err instanceof Error ? err.message : String(err);
842
- const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
843
- this._downInternal([[ERROR, wrapped]]);
811
+ };
812
+
813
+ // src/core/node.ts
814
+ var NodeImpl = class extends NodeBase {
815
+ // --- Dep configuration (set once) ---
816
+ _deps;
817
+ _fn;
818
+ _opts;
819
+ _hasDeps;
820
+ _isSingleDep;
821
+ _autoComplete;
822
+ // --- Wave tracking masks ---
823
+ _depDirtyMask;
824
+ _depSettledMask;
825
+ _depCompleteMask;
826
+ _allDepsCompleteMask;
827
+ // --- Identity-skip optimization ---
828
+ _lastDepValues;
829
+ _cleanup;
830
+ // --- Upstream bookkeeping ---
831
+ _upstreamUnsubs = [];
832
+ // --- Fn behavior flag ---
833
+ /** @internal Read by `describeNode` to infer `"operator"` label. */
834
+ _manualEmitUsed = false;
835
+ constructor(deps, fn, opts) {
836
+ super(opts);
837
+ this._deps = deps;
838
+ this._fn = fn;
839
+ this._opts = opts;
840
+ this._hasDeps = deps.length > 0;
841
+ this._isSingleDep = deps.length === 1 && fn != null;
842
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
843
+ if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
844
+ this._status = "settled";
844
845
  }
845
- }
846
- _onDepDirty(index) {
847
- const wasDirty = this._depDirtyMask.has(index);
848
- this._depDirtyMask.set(index);
849
- this._depSettledMask.clear(index);
850
- if (!wasDirty) {
851
- this._downInternal([[DIRTY]]);
846
+ this._depDirtyMask = createBitSet(deps.length);
847
+ this._depSettledMask = createBitSet(deps.length);
848
+ this._depCompleteMask = createBitSet(deps.length);
849
+ this._allDepsCompleteMask = createBitSet(deps.length);
850
+ for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
851
+ this.down = this.down.bind(this);
852
+ this.up = this.up.bind(this);
853
+ }
854
+ // --- Meta node factory (called from base constructor) ---
855
+ _createMetaNode(key, initialValue, opts) {
856
+ return node({
857
+ initial: initialValue,
858
+ name: `${opts.name ?? "node"}:meta:${key}`,
859
+ describeKind: "state",
860
+ ...opts.guard != null ? { guard: opts.guard } : {}
861
+ });
862
+ }
863
+ // --- Manual emit tracker (set by actions.down / actions.emit) ---
864
+ _onManualEmit() {
865
+ this._manualEmitUsed = true;
866
+ }
867
+ // --- Up / unsubscribe ---
868
+ up(messages, options) {
869
+ if (!this._hasDeps) return;
870
+ if (!options?.internal && this._guard != null) {
871
+ const actor = normalizeActor(options?.actor);
872
+ if (!this._guard(actor, "write")) {
873
+ throw new GuardDenied({ actor, action: "write", nodeName: this.name });
874
+ }
875
+ this._recordMutation(actor);
876
+ }
877
+ for (const dep of this._deps) {
878
+ if (options === void 0) {
879
+ dep.up?.(messages);
880
+ } else {
881
+ dep.up?.(messages, options);
882
+ }
852
883
  }
853
884
  }
854
- _onDepSettled(index) {
855
- if (!this._depDirtyMask.has(index)) {
856
- this._onDepDirty(index);
885
+ _upInternal(messages) {
886
+ if (!this._hasDeps) return;
887
+ for (const dep of this._deps) {
888
+ dep.up?.(messages, { internal: true });
857
889
  }
858
- this._depSettledMask.set(index);
859
- if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
860
- this._depDirtyMask.reset();
861
- this._depSettledMask.reset();
890
+ }
891
+ unsubscribe() {
892
+ if (!this._hasDeps) return;
893
+ this._disconnectUpstream();
894
+ }
895
+ // --- Activation (first-subscriber / last-subscriber hooks) ---
896
+ _onActivate() {
897
+ if (this._hasDeps) {
898
+ this._connectUpstream();
899
+ return;
900
+ }
901
+ if (this._fn) {
862
902
  this._runFn();
903
+ return;
863
904
  }
864
905
  }
865
- _maybeCompleteFromDeps() {
866
- if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
867
- this._downInternal([[COMPLETE]]);
906
+ _doDeactivate() {
907
+ this._disconnectUpstream();
908
+ const cleanup = this._cleanup;
909
+ this._cleanup = void 0;
910
+ cleanup?.();
911
+ if (this._fn != null) {
912
+ this._cached = NO_VALUE;
913
+ this._lastDepValues = void 0;
914
+ }
915
+ if (this._hasDeps || this._fn != null) {
916
+ this._status = "disconnected";
917
+ }
918
+ }
919
+ // --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
920
+ _onInvalidate() {
921
+ const cleanup = this._cleanup;
922
+ this._cleanup = void 0;
923
+ cleanup?.();
924
+ this._lastDepValues = void 0;
925
+ }
926
+ _onTeardown() {
927
+ const cleanup = this._cleanup;
928
+ this._cleanup = void 0;
929
+ cleanup?.();
930
+ }
931
+ // --- Upstream connect / disconnect ---
932
+ _connectUpstream() {
933
+ if (!this._hasDeps) return;
934
+ if (this._upstreamUnsubs.length > 0) return;
935
+ this._depDirtyMask.setAll();
936
+ this._depSettledMask.reset();
937
+ this._depCompleteMask.reset();
938
+ const depValuesBefore = this._lastDepValues;
939
+ const subHints = this._isSingleDep ? { singleDep: true } : void 0;
940
+ for (let i = 0; i < this._deps.length; i += 1) {
941
+ const dep = this._deps[i];
942
+ this._upstreamUnsubs.push(
943
+ dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
944
+ );
945
+ }
946
+ if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
947
+ this._runFn();
948
+ }
949
+ }
950
+ _disconnectUpstream() {
951
+ if (this._upstreamUnsubs.length === 0) return;
952
+ for (const unsub of this._upstreamUnsubs.splice(0)) {
953
+ unsub();
868
954
  }
955
+ this._depDirtyMask.reset();
956
+ this._depSettledMask.reset();
957
+ this._depCompleteMask.reset();
869
958
  }
959
+ // --- Wave handling ---
870
960
  _handleDepMessages(index, messages) {
871
961
  for (const msg of messages) {
872
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
962
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
873
963
  const t = msg[0];
874
964
  if (this._onMessage) {
875
965
  try {
876
- if (this._onMessage(msg, index, this._actions)) continue;
966
+ const consumed = this._onMessage(msg, index, this._actions);
967
+ if (consumed) {
968
+ if (t === START) {
969
+ this._depDirtyMask.clear(index);
970
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
971
+ this._depDirtyMask.reset();
972
+ this._depSettledMask.reset();
973
+ this._runFn();
974
+ }
975
+ }
976
+ continue;
977
+ }
877
978
  } catch (err) {
878
979
  const errMsg = err instanceof Error ? err.message : String(err);
879
980
  const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
@@ -883,6 +984,7 @@ var NodeImpl = class {
883
984
  return;
884
985
  }
885
986
  }
987
+ if (messageTier(t) < 1) continue;
886
988
  if (!this._fn) {
887
989
  if (t === COMPLETE && this._deps.length > 1) {
888
990
  this._depCompleteMask.set(index);
@@ -926,53 +1028,85 @@ var NodeImpl = class {
926
1028
  this._downInternal([msg]);
927
1029
  }
928
1030
  }
929
- _connectUpstream() {
930
- if (!this._hasDeps || this._connected) return;
931
- this._connected = true;
932
- this._depDirtyMask.reset();
933
- this._depSettledMask.reset();
934
- this._depCompleteMask.reset();
935
- this._status = "settled";
936
- const subHints = this._isSingleDep ? { singleDep: true } : void 0;
937
- this._connecting = true;
938
- try {
939
- for (let i = 0; i < this._deps.length; i += 1) {
940
- const dep = this._deps[i];
941
- this._upstreamUnsubs.push(
942
- dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
943
- );
944
- }
945
- } finally {
946
- this._connecting = false;
1031
+ _onDepDirty(index) {
1032
+ const wasDirty = this._depDirtyMask.has(index);
1033
+ this._depDirtyMask.set(index);
1034
+ this._depSettledMask.clear(index);
1035
+ if (!wasDirty) {
1036
+ this._downInternal([[DIRTY]]);
947
1037
  }
948
- if (this._fn) {
1038
+ }
1039
+ _onDepSettled(index) {
1040
+ if (!this._depDirtyMask.has(index)) {
1041
+ this._onDepDirty(index);
1042
+ }
1043
+ this._depSettledMask.set(index);
1044
+ if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
1045
+ this._depDirtyMask.reset();
1046
+ this._depSettledMask.reset();
949
1047
  this._runFn();
950
1048
  }
951
1049
  }
952
- _stopProducer() {
953
- if (!this._producerStarted) return;
954
- this._producerStarted = false;
955
- const producerCleanup = this._cleanup;
956
- this._cleanup = void 0;
957
- producerCleanup?.();
958
- }
959
- _startProducer() {
960
- if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
961
- this._producerStarted = true;
962
- this._runFn();
1050
+ _maybeCompleteFromDeps() {
1051
+ if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
1052
+ this._downInternal([[COMPLETE]]);
1053
+ }
963
1054
  }
964
- _disconnectUpstream() {
965
- if (!this._connected) return;
966
- for (const unsub of this._upstreamUnsubs.splice(0)) {
967
- unsub();
1055
+ // --- Fn execution ---
1056
+ _runFn() {
1057
+ if (!this._fn) return;
1058
+ if (this._terminal && !this._resubscribable) return;
1059
+ try {
1060
+ const n = this._deps.length;
1061
+ const depValues = new Array(n);
1062
+ for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
1063
+ const prev = this._lastDepValues;
1064
+ if (n > 0 && prev != null && prev.length === n) {
1065
+ let allSame = true;
1066
+ for (let i = 0; i < n; i++) {
1067
+ if (!Object.is(depValues[i], prev[i])) {
1068
+ allSame = false;
1069
+ break;
1070
+ }
1071
+ }
1072
+ if (allSame) {
1073
+ if (this._status === "dirty") {
1074
+ this._downInternal([[RESOLVED]]);
1075
+ }
1076
+ return;
1077
+ }
1078
+ }
1079
+ const prevCleanup = this._cleanup;
1080
+ this._cleanup = void 0;
1081
+ prevCleanup?.();
1082
+ this._manualEmitUsed = false;
1083
+ this._lastDepValues = depValues;
1084
+ this._emitInspectorHook({ kind: "run", depValues });
1085
+ const out = this._fn(depValues, this._actions);
1086
+ if (isCleanupResult(out)) {
1087
+ this._cleanup = out.cleanup;
1088
+ if (this._manualEmitUsed) return;
1089
+ if ("value" in out) {
1090
+ this._downAutoValue(out.value);
1091
+ }
1092
+ return;
1093
+ }
1094
+ if (isCleanupFn(out)) {
1095
+ this._cleanup = out;
1096
+ return;
1097
+ }
1098
+ if (this._manualEmitUsed) return;
1099
+ if (out === void 0) return;
1100
+ this._downAutoValue(out);
1101
+ } catch (err) {
1102
+ const errMsg = err instanceof Error ? err.message : String(err);
1103
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
1104
+ this._downInternal([[ERROR, wrapped]]);
968
1105
  }
969
- this._connected = false;
970
- this._depDirtyMask.reset();
971
- this._depSettledMask.reset();
972
- this._depCompleteMask.reset();
973
- this._status = "disconnected";
974
1106
  }
975
1107
  };
1108
+ var isNodeArray = (value) => Array.isArray(value);
1109
+ var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
976
1110
  function node(depsOrFn, fnOrOpts, optsArg) {
977
1111
  const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
978
1112
  const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
@@ -988,227 +1122,44 @@ function node(depsOrFn, fnOrOpts, optsArg) {
988
1122
  }
989
1123
 
990
1124
  // src/core/dynamic-node.ts
991
- var DynamicNodeImpl = class {
992
- _optsName;
993
- _registryName;
994
- _describeKind;
995
- meta;
1125
+ var MAX_RERUN = 16;
1126
+ var DynamicNodeImpl = class extends NodeBase {
996
1127
  _fn;
997
- _equals;
998
- _resubscribable;
999
- _resetOnTeardown;
1000
- _autoComplete;
1001
- _onMessage;
1002
- _onResubscribe;
1003
- /** @internal read by {@link describeNode} for `accessHintForGuard`. */
1004
- _guard;
1005
- _lastMutation;
1006
- _inspectorHook;
1007
- // Sink tracking
1008
- _sinkCount = 0;
1009
- _singleDepSinkCount = 0;
1010
- _singleDepSinks = /* @__PURE__ */ new WeakSet();
1011
- // Actions object (for onMessage handler)
1012
- _actions;
1013
- _boundDownToSinks;
1014
- // Mutable state
1015
- _cached = NO_VALUE;
1016
- _status = "disconnected";
1017
- _terminal = false;
1018
- _connected = false;
1019
- _rewiring = false;
1020
- // re-entrancy guard
1021
- // Dynamic deps tracking
1022
- _deps = [];
1023
- _depUnsubs = [];
1024
- _depIndexMap = /* @__PURE__ */ new Map();
1025
- // node → index in _deps
1026
- _dirtyBits = /* @__PURE__ */ new Set();
1027
- _settledBits = /* @__PURE__ */ new Set();
1028
- _completeBits = /* @__PURE__ */ new Set();
1029
- // Sinks
1030
- _sinks = null;
1031
- constructor(fn, opts) {
1032
- this._fn = fn;
1033
- this._optsName = opts.name;
1034
- this._describeKind = opts.describeKind;
1035
- this._equals = opts.equals ?? Object.is;
1036
- this._resubscribable = opts.resubscribable ?? false;
1037
- this._resetOnTeardown = opts.resetOnTeardown ?? false;
1038
- this._autoComplete = opts.completeWhenDepsComplete ?? true;
1039
- this._onMessage = opts.onMessage;
1040
- this._onResubscribe = opts.onResubscribe;
1041
- this._guard = opts.guard;
1042
- this._inspectorHook = void 0;
1043
- const meta = {};
1044
- for (const [k, v] of Object.entries(opts.meta ?? {})) {
1045
- meta[k] = node({
1046
- initial: v,
1047
- name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
1048
- describeKind: "state",
1049
- ...opts.guard != null ? { guard: opts.guard } : {}
1050
- });
1051
- }
1052
- Object.freeze(meta);
1053
- this.meta = meta;
1054
- const self = this;
1055
- this._actions = {
1056
- down(messages) {
1057
- self._downInternal(messages);
1058
- },
1059
- emit(value) {
1060
- self._downAutoValue(value);
1061
- },
1062
- up(messages) {
1063
- for (const dep of self._deps) {
1064
- dep.up?.(messages, { internal: true });
1065
- }
1066
- }
1067
- };
1068
- this._boundDownToSinks = this._downToSinks.bind(this);
1069
- }
1070
- get name() {
1071
- return this._registryName ?? this._optsName;
1072
- }
1073
- /** @internal */
1074
- _assignRegistryName(localName) {
1075
- if (this._optsName !== void 0 || this._registryName !== void 0) return;
1076
- this._registryName = localName;
1077
- }
1078
- /**
1079
- * @internal Attach/remove inspector hook for graph-level observability.
1080
- * Returns a disposer that restores the previous hook.
1081
- */
1082
- _setInspectorHook(hook) {
1083
- const prev = this._inspectorHook;
1084
- this._inspectorHook = hook;
1085
- return () => {
1086
- if (this._inspectorHook === hook) {
1087
- this._inspectorHook = prev;
1088
- }
1089
- };
1090
- }
1091
- get status() {
1092
- return this._status;
1093
- }
1094
- get lastMutation() {
1095
- return this._lastMutation;
1096
- }
1097
- /** Versioning not yet supported on DynamicNodeImpl. */
1098
- get v() {
1099
- return void 0;
1100
- }
1101
- hasGuard() {
1102
- return this._guard != null;
1103
- }
1104
- allowsObserve(actor) {
1105
- if (this._guard == null) return true;
1106
- return this._guard(normalizeActor(actor), "observe");
1107
- }
1108
- get() {
1109
- return this._cached === NO_VALUE ? void 0 : this._cached;
1110
- }
1111
- down(messages, options) {
1112
- if (messages.length === 0) return;
1113
- if (!options?.internal && this._guard != null) {
1114
- const actor = normalizeActor(options?.actor);
1115
- const delivery = options?.delivery ?? "write";
1116
- const action = delivery === "signal" ? "signal" : "write";
1117
- if (!this._guard(actor, action)) {
1118
- throw new GuardDenied({ actor, action, nodeName: this.name });
1119
- }
1120
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1121
- }
1122
- this._downInternal(messages);
1123
- }
1124
- _downInternal(messages) {
1125
- if (messages.length === 0) return;
1126
- let sinkMessages = messages;
1127
- if (this._terminal && !this._resubscribable) {
1128
- const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
1129
- if (pass.length === 0) return;
1130
- sinkMessages = pass;
1131
- }
1132
- this._handleLocalLifecycle(sinkMessages);
1133
- if (this._canSkipDirty()) {
1134
- let hasPhase2 = false;
1135
- for (let i = 0; i < sinkMessages.length; i++) {
1136
- const t = sinkMessages[i][0];
1137
- if (t === DATA || t === RESOLVED) {
1138
- hasPhase2 = true;
1139
- break;
1140
- }
1141
- }
1142
- if (hasPhase2) {
1143
- const filtered = [];
1144
- for (let i = 0; i < sinkMessages.length; i++) {
1145
- if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
1146
- }
1147
- if (filtered.length > 0) {
1148
- downWithBatch(this._boundDownToSinks, filtered);
1149
- }
1150
- return;
1151
- }
1152
- }
1153
- downWithBatch(this._boundDownToSinks, sinkMessages);
1154
- }
1155
- _canSkipDirty() {
1156
- return this._sinkCount === 1 && this._singleDepSinkCount === 1;
1157
- }
1158
- subscribe(sink, hints) {
1159
- if (hints?.actor != null && this._guard != null) {
1160
- const actor = normalizeActor(hints.actor);
1161
- if (!this._guard(actor, "observe")) {
1162
- throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
1163
- }
1164
- }
1165
- if (this._terminal && this._resubscribable) {
1166
- this._terminal = false;
1167
- this._cached = NO_VALUE;
1168
- this._status = "disconnected";
1169
- this._onResubscribe?.();
1170
- }
1171
- this._sinkCount += 1;
1172
- if (hints?.singleDep) {
1173
- this._singleDepSinkCount += 1;
1174
- this._singleDepSinks.add(sink);
1175
- }
1176
- if (this._sinks == null) {
1177
- this._sinks = sink;
1178
- } else if (typeof this._sinks === "function") {
1179
- this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
1180
- } else {
1181
- this._sinks.add(sink);
1182
- }
1183
- if (!this._connected) {
1184
- this._connect();
1185
- }
1186
- let removed = false;
1187
- return () => {
1188
- if (removed) return;
1189
- removed = true;
1190
- this._sinkCount -= 1;
1191
- if (this._singleDepSinks.has(sink)) {
1192
- this._singleDepSinkCount -= 1;
1193
- this._singleDepSinks.delete(sink);
1194
- }
1195
- if (this._sinks == null) return;
1196
- if (typeof this._sinks === "function") {
1197
- if (this._sinks === sink) this._sinks = null;
1198
- } else {
1199
- this._sinks.delete(sink);
1200
- if (this._sinks.size === 1) {
1201
- const [only] = this._sinks;
1202
- this._sinks = only;
1203
- } else if (this._sinks.size === 0) {
1204
- this._sinks = null;
1205
- }
1206
- }
1207
- if (this._sinks == null) {
1208
- this._disconnect();
1209
- }
1210
- };
1128
+ _autoComplete;
1129
+ // Dynamic deps tracking
1130
+ /** @internal Read by `describeNode`. */
1131
+ _deps = [];
1132
+ _depUnsubs = [];
1133
+ _depIndexMap = /* @__PURE__ */ new Map();
1134
+ _depDirtyBits = /* @__PURE__ */ new Set();
1135
+ _depSettledBits = /* @__PURE__ */ new Set();
1136
+ _depCompleteBits = /* @__PURE__ */ new Set();
1137
+ // Execution state
1138
+ _running = false;
1139
+ _rewiring = false;
1140
+ _bufferedDepMessages = [];
1141
+ _trackedValues = /* @__PURE__ */ new Map();
1142
+ _rerunCount = 0;
1143
+ constructor(fn, opts) {
1144
+ super(opts);
1145
+ this._fn = fn;
1146
+ this._autoComplete = opts.completeWhenDepsComplete ?? true;
1147
+ this.down = this.down.bind(this);
1148
+ this.up = this.up.bind(this);
1149
+ }
1150
+ _createMetaNode(key, initialValue, opts) {
1151
+ return node({
1152
+ initial: initialValue,
1153
+ name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
1154
+ describeKind: "state",
1155
+ ...opts.guard != null ? { guard: opts.guard } : {}
1156
+ });
1157
+ }
1158
+ /** Versioning not supported on DynamicNodeImpl (override base). */
1159
+ get v() {
1160
+ return void 0;
1211
1161
  }
1162
+ // --- Up / unsubscribe ---
1212
1163
  up(messages, options) {
1213
1164
  if (this._deps.length === 0) return;
1214
1165
  if (!options?.internal && this._guard != null) {
@@ -1216,221 +1167,227 @@ var DynamicNodeImpl = class {
1216
1167
  if (!this._guard(actor, "write")) {
1217
1168
  throw new GuardDenied({ actor, action: "write", nodeName: this.name });
1218
1169
  }
1219
- this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1170
+ this._recordMutation(actor);
1220
1171
  }
1221
1172
  for (const dep of this._deps) {
1222
1173
  dep.up?.(messages, options);
1223
1174
  }
1224
1175
  }
1225
- unsubscribe() {
1226
- this._disconnect();
1227
- }
1228
- // --- Private methods ---
1229
- _downToSinks(messages) {
1230
- if (this._sinks == null) return;
1231
- if (typeof this._sinks === "function") {
1232
- this._sinks(messages);
1233
- return;
1234
- }
1235
- const snapshot = [...this._sinks];
1236
- for (const sink of snapshot) {
1237
- sink(messages);
1238
- }
1239
- }
1240
- _handleLocalLifecycle(messages) {
1241
- for (const m of messages) {
1242
- const t = m[0];
1243
- if (t === DATA) this._cached = m[1];
1244
- if (t === INVALIDATE) {
1245
- this._cached = NO_VALUE;
1246
- this._status = "dirty";
1247
- }
1248
- if (t === DATA) {
1249
- this._status = "settled";
1250
- } else if (t === RESOLVED) {
1251
- this._status = "resolved";
1252
- } else if (t === DIRTY) {
1253
- this._status = "dirty";
1254
- } else if (t === COMPLETE) {
1255
- this._status = "completed";
1256
- this._terminal = true;
1257
- } else if (t === ERROR) {
1258
- this._status = "errored";
1259
- this._terminal = true;
1260
- }
1261
- if (t === TEARDOWN) {
1262
- if (this._resetOnTeardown) this._cached = NO_VALUE;
1263
- try {
1264
- this._propagateToMeta(t);
1265
- } finally {
1266
- this._disconnect();
1267
- }
1268
- }
1269
- if (t !== TEARDOWN && propagatesToMeta(t)) {
1270
- this._propagateToMeta(t);
1271
- }
1272
- }
1273
- }
1274
- /** Propagate a signal to all companion meta nodes (best-effort). */
1275
- _propagateToMeta(t) {
1276
- for (const metaNode of Object.values(this.meta)) {
1277
- try {
1278
- metaNode.down([[t]], { internal: true });
1279
- } catch {
1280
- }
1176
+ _upInternal(messages) {
1177
+ for (const dep of this._deps) {
1178
+ dep.up?.(messages, { internal: true });
1281
1179
  }
1282
1180
  }
1283
- _downAutoValue(value) {
1284
- const wasDirty = this._status === "dirty";
1285
- let unchanged;
1286
- try {
1287
- unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
1288
- } catch (eqErr) {
1289
- const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
1290
- const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
1291
- this._downInternal([[ERROR, wrapped]]);
1292
- return;
1293
- }
1294
- if (unchanged) {
1295
- this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
1296
- return;
1297
- }
1298
- this._cached = value;
1299
- this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
1181
+ unsubscribe() {
1182
+ this._disconnect();
1300
1183
  }
1301
- _connect() {
1302
- if (this._connected) return;
1303
- this._connected = true;
1304
- this._status = "settled";
1305
- this._dirtyBits.clear();
1306
- this._settledBits.clear();
1307
- this._completeBits.clear();
1184
+ // --- Activation hooks ---
1185
+ _onActivate() {
1308
1186
  this._runFn();
1309
1187
  }
1188
+ _doDeactivate() {
1189
+ this._disconnect();
1190
+ }
1310
1191
  _disconnect() {
1311
- if (!this._connected) return;
1312
1192
  for (const unsub of this._depUnsubs) unsub();
1313
1193
  this._depUnsubs = [];
1314
1194
  this._deps = [];
1315
1195
  this._depIndexMap.clear();
1316
- this._dirtyBits.clear();
1317
- this._settledBits.clear();
1318
- this._completeBits.clear();
1319
- this._connected = false;
1196
+ this._depDirtyBits.clear();
1197
+ this._depSettledBits.clear();
1198
+ this._depCompleteBits.clear();
1199
+ this._cached = NO_VALUE;
1320
1200
  this._status = "disconnected";
1321
1201
  }
1202
+ // --- Fn execution with rewire buffer ---
1322
1203
  _runFn() {
1323
1204
  if (this._terminal && !this._resubscribable) return;
1324
- if (this._rewiring) return;
1325
- const trackedDeps = [];
1326
- const trackedSet = /* @__PURE__ */ new Set();
1327
- const get = (dep) => {
1328
- if (!trackedSet.has(dep)) {
1329
- trackedSet.add(dep);
1330
- trackedDeps.push(dep);
1331
- }
1332
- return dep.get();
1333
- };
1205
+ if (this._running) return;
1206
+ this._running = true;
1207
+ this._rerunCount = 0;
1208
+ let result;
1334
1209
  try {
1335
- const depValues = [];
1336
- for (const dep of this._deps) {
1337
- depValues.push(dep.get());
1338
- }
1339
- this._inspectorHook?.({ kind: "run", depValues });
1340
- const result = this._fn(get);
1341
- this._rewire(trackedDeps);
1342
- if (result === void 0) return;
1343
- this._downAutoValue(result);
1344
- } catch (err) {
1345
- this._downInternal([[ERROR, err]]);
1210
+ for (; ; ) {
1211
+ const trackedDeps = [];
1212
+ const trackedValuesMap = /* @__PURE__ */ new Map();
1213
+ const trackedSet = /* @__PURE__ */ new Set();
1214
+ const get = (dep) => {
1215
+ if (!trackedSet.has(dep)) {
1216
+ trackedSet.add(dep);
1217
+ trackedDeps.push(dep);
1218
+ trackedValuesMap.set(dep, dep.get());
1219
+ }
1220
+ return dep.get();
1221
+ };
1222
+ this._trackedValues = trackedValuesMap;
1223
+ const depValues = [];
1224
+ for (const dep of this._deps) depValues.push(dep.get());
1225
+ this._emitInspectorHook({ kind: "run", depValues });
1226
+ try {
1227
+ result = this._fn(get);
1228
+ } catch (err) {
1229
+ const errMsg = err instanceof Error ? err.message : String(err);
1230
+ const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
1231
+ cause: err
1232
+ });
1233
+ this._downInternal([[ERROR, wrapped]]);
1234
+ return;
1235
+ }
1236
+ this._rewiring = true;
1237
+ this._bufferedDepMessages = [];
1238
+ try {
1239
+ this._rewire(trackedDeps);
1240
+ } finally {
1241
+ this._rewiring = false;
1242
+ }
1243
+ let needsRerun = false;
1244
+ for (const entry of this._bufferedDepMessages) {
1245
+ for (const msg of entry.msgs) {
1246
+ if (msg[0] === DATA) {
1247
+ const dep = this._deps[entry.index];
1248
+ const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
1249
+ const actualValue = msg[1];
1250
+ if (!this._equals(trackedValue, actualValue)) {
1251
+ needsRerun = true;
1252
+ break;
1253
+ }
1254
+ }
1255
+ }
1256
+ if (needsRerun) break;
1257
+ }
1258
+ if (needsRerun) {
1259
+ this._rerunCount += 1;
1260
+ if (this._rerunCount > MAX_RERUN) {
1261
+ this._bufferedDepMessages = [];
1262
+ this._downInternal([
1263
+ [
1264
+ ERROR,
1265
+ new Error(
1266
+ `dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
1267
+ )
1268
+ ]
1269
+ ]);
1270
+ return;
1271
+ }
1272
+ this._bufferedDepMessages = [];
1273
+ continue;
1274
+ }
1275
+ const drain = this._bufferedDepMessages;
1276
+ this._bufferedDepMessages = [];
1277
+ for (const entry of drain) {
1278
+ for (const msg of entry.msgs) {
1279
+ this._updateMasksForMessage(entry.index, msg);
1280
+ }
1281
+ }
1282
+ this._depDirtyBits.clear();
1283
+ this._depSettledBits.clear();
1284
+ break;
1285
+ }
1286
+ } finally {
1287
+ this._running = false;
1346
1288
  }
1289
+ this._downAutoValue(result);
1347
1290
  }
1348
1291
  _rewire(newDeps) {
1349
- this._rewiring = true;
1350
- try {
1351
- const oldMap = this._depIndexMap;
1352
- const newMap = /* @__PURE__ */ new Map();
1353
- const newUnsubs = [];
1354
- for (let i = 0; i < newDeps.length; i++) {
1355
- const dep = newDeps[i];
1356
- newMap.set(dep, i);
1357
- const oldIdx = oldMap.get(dep);
1358
- if (oldIdx !== void 0) {
1359
- newUnsubs.push(this._depUnsubs[oldIdx]);
1360
- this._depUnsubs[oldIdx] = () => {
1361
- };
1362
- } else {
1363
- const idx = i;
1364
- const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1365
- newUnsubs.push(unsub);
1366
- }
1292
+ const oldMap = this._depIndexMap;
1293
+ const newMap = /* @__PURE__ */ new Map();
1294
+ const newUnsubs = [];
1295
+ for (let i = 0; i < newDeps.length; i++) {
1296
+ const dep = newDeps[i];
1297
+ newMap.set(dep, i);
1298
+ const oldIdx = oldMap.get(dep);
1299
+ if (oldIdx !== void 0) {
1300
+ newUnsubs.push(this._depUnsubs[oldIdx]);
1301
+ this._depUnsubs[oldIdx] = () => {
1302
+ };
1303
+ } else {
1304
+ const idx = i;
1305
+ const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
1306
+ newUnsubs.push(unsub);
1367
1307
  }
1368
- for (const [dep, oldIdx] of oldMap) {
1369
- if (!newMap.has(dep)) {
1370
- this._depUnsubs[oldIdx]();
1371
- }
1308
+ }
1309
+ for (const [dep, oldIdx] of oldMap) {
1310
+ if (!newMap.has(dep)) {
1311
+ this._depUnsubs[oldIdx]();
1372
1312
  }
1373
- this._deps = newDeps;
1374
- this._depUnsubs = newUnsubs;
1375
- this._depIndexMap = newMap;
1376
- this._dirtyBits.clear();
1377
- this._settledBits.clear();
1378
- const newCompleteBits = /* @__PURE__ */ new Set();
1379
- for (const oldIdx of this._completeBits) {
1380
- const dep = [...oldMap.entries()].find(([, idx]) => idx === oldIdx)?.[0];
1381
- if (dep && newMap.has(dep)) {
1313
+ }
1314
+ this._deps = newDeps;
1315
+ this._depUnsubs = newUnsubs;
1316
+ this._depIndexMap = newMap;
1317
+ this._depDirtyBits.clear();
1318
+ this._depSettledBits.clear();
1319
+ const newCompleteBits = /* @__PURE__ */ new Set();
1320
+ for (const oldIdx of this._depCompleteBits) {
1321
+ for (const [dep, idx] of oldMap) {
1322
+ if (idx === oldIdx && newMap.has(dep)) {
1382
1323
  newCompleteBits.add(newMap.get(dep));
1324
+ break;
1383
1325
  }
1384
1326
  }
1385
- this._completeBits = newCompleteBits;
1386
- } finally {
1387
- this._rewiring = false;
1388
1327
  }
1328
+ this._depCompleteBits = newCompleteBits;
1389
1329
  }
1330
+ // --- Dep message handling ---
1390
1331
  _handleDepMessages(index, messages) {
1391
- if (this._rewiring) return;
1332
+ if (this._rewiring) {
1333
+ this._bufferedDepMessages.push({ index, msgs: messages });
1334
+ return;
1335
+ }
1392
1336
  for (const msg of messages) {
1393
- this._inspectorHook?.({ kind: "dep_message", depIndex: index, message: msg });
1337
+ this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
1394
1338
  const t = msg[0];
1395
1339
  if (this._onMessage) {
1396
1340
  try {
1397
1341
  if (this._onMessage(msg, index, this._actions)) continue;
1398
1342
  } catch (err) {
1399
- this._downInternal([[ERROR, err]]);
1343
+ const errMsg = err instanceof Error ? err.message : String(err);
1344
+ const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
1345
+ cause: err
1346
+ });
1347
+ this._downInternal([[ERROR, wrapped]]);
1400
1348
  return;
1401
1349
  }
1402
1350
  }
1351
+ if (messageTier(t) < 1) continue;
1403
1352
  if (t === DIRTY) {
1404
- this._dirtyBits.add(index);
1405
- this._settledBits.delete(index);
1406
- if (this._dirtyBits.size === 1) {
1407
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
1353
+ const wasEmpty = this._depDirtyBits.size === 0;
1354
+ this._depDirtyBits.add(index);
1355
+ this._depSettledBits.delete(index);
1356
+ if (wasEmpty) {
1357
+ this._downInternal([[DIRTY]]);
1408
1358
  }
1409
1359
  continue;
1410
1360
  }
1411
1361
  if (t === DATA || t === RESOLVED) {
1412
- if (!this._dirtyBits.has(index)) {
1413
- this._dirtyBits.add(index);
1414
- downWithBatch(this._boundDownToSinks, [[DIRTY]]);
1362
+ if (!this._depDirtyBits.has(index)) {
1363
+ const wasEmpty = this._depDirtyBits.size === 0;
1364
+ this._depDirtyBits.add(index);
1365
+ if (wasEmpty) {
1366
+ this._downInternal([[DIRTY]]);
1367
+ }
1415
1368
  }
1416
- this._settledBits.add(index);
1369
+ this._depSettledBits.add(index);
1417
1370
  if (this._allDirtySettled()) {
1418
- this._dirtyBits.clear();
1419
- this._settledBits.clear();
1420
- this._runFn();
1371
+ this._depDirtyBits.clear();
1372
+ this._depSettledBits.clear();
1373
+ if (!this._running) {
1374
+ if (this._depValuesDifferFromTracked()) {
1375
+ this._runFn();
1376
+ }
1377
+ }
1421
1378
  }
1422
1379
  continue;
1423
1380
  }
1424
1381
  if (t === COMPLETE) {
1425
- this._completeBits.add(index);
1426
- this._dirtyBits.delete(index);
1427
- this._settledBits.delete(index);
1382
+ this._depCompleteBits.add(index);
1383
+ this._depDirtyBits.delete(index);
1384
+ this._depSettledBits.delete(index);
1428
1385
  if (this._allDirtySettled()) {
1429
- this._dirtyBits.clear();
1430
- this._settledBits.clear();
1431
- this._runFn();
1386
+ this._depDirtyBits.clear();
1387
+ this._depSettledBits.clear();
1388
+ if (!this._running) this._runFn();
1432
1389
  }
1433
- if (this._autoComplete && this._completeBits.size >= this._deps.length && this._deps.length > 0) {
1390
+ if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
1434
1391
  this._downInternal([[COMPLETE]]);
1435
1392
  }
1436
1393
  continue;
@@ -1446,13 +1403,46 @@ var DynamicNodeImpl = class {
1446
1403
  this._downInternal([msg]);
1447
1404
  }
1448
1405
  }
1406
+ /**
1407
+ * Update dep masks for a message without triggering `_runFn` — used
1408
+ * during post-rewire drain so the wave state is consistent with the
1409
+ * buffered activation cascade without recursing.
1410
+ */
1411
+ _updateMasksForMessage(index, msg) {
1412
+ const t = msg[0];
1413
+ if (t === DIRTY) {
1414
+ this._depDirtyBits.add(index);
1415
+ this._depSettledBits.delete(index);
1416
+ } else if (t === DATA || t === RESOLVED) {
1417
+ this._depDirtyBits.add(index);
1418
+ this._depSettledBits.add(index);
1419
+ } else if (t === COMPLETE) {
1420
+ this._depCompleteBits.add(index);
1421
+ this._depDirtyBits.delete(index);
1422
+ this._depSettledBits.delete(index);
1423
+ }
1424
+ }
1449
1425
  _allDirtySettled() {
1450
- if (this._dirtyBits.size === 0) return false;
1451
- for (const idx of this._dirtyBits) {
1452
- if (!this._settledBits.has(idx)) return false;
1426
+ if (this._depDirtyBits.size === 0) return false;
1427
+ for (const idx of this._depDirtyBits) {
1428
+ if (!this._depSettledBits.has(idx)) return false;
1453
1429
  }
1454
1430
  return true;
1455
1431
  }
1432
+ /**
1433
+ * True if any current dep value differs from what the last `_runFn`
1434
+ * saw via `get()`. Used to suppress redundant re-runs when deferred
1435
+ * handshake messages arrive after `_rewire` for a dep whose value
1436
+ * already matches `_trackedValues`.
1437
+ */
1438
+ _depValuesDifferFromTracked() {
1439
+ for (const dep of this._deps) {
1440
+ const current = dep.get();
1441
+ const tracked = this._trackedValues.get(dep);
1442
+ if (!this._equals(current, tracked)) return true;
1443
+ }
1444
+ return false;
1445
+ }
1456
1446
  };
1457
1447
 
1458
1448
  // src/core/meta.ts
@@ -1522,6 +1512,10 @@ function describeNode(node2, includeFields) {
1522
1512
  out.name = node2.name;
1523
1513
  }
1524
1514
  if (all || includeFields.has("value")) {
1515
+ const isSentinel = node2 instanceof NodeImpl && node2._cached === NO_VALUE || node2 instanceof DynamicNodeImpl && node2._cached === NO_VALUE;
1516
+ if (isSentinel) {
1517
+ out.sentinel = true;
1518
+ }
1525
1519
  try {
1526
1520
  out.value = node2.get();
1527
1521
  } catch {
@@ -1553,6 +1547,129 @@ function state(initial, opts) {
1553
1547
  return node([], { ...opts, initial });
1554
1548
  }
1555
1549
 
1550
+ // src/graph/sizeof.ts
1551
+ var OVERHEAD = {
1552
+ object: 56,
1553
+ array: 64,
1554
+ string: 40,
1555
+ // header; content added separately
1556
+ number: 8,
1557
+ boolean: 4,
1558
+ null: 0,
1559
+ undefined: 0,
1560
+ symbol: 40,
1561
+ bigint: 16,
1562
+ function: 120,
1563
+ map: 72,
1564
+ set: 72,
1565
+ mapEntry: 40,
1566
+ setEntry: 24
1567
+ };
1568
+ function sizeof(value) {
1569
+ const seen = /* @__PURE__ */ new WeakSet();
1570
+ return _sizeof(value, seen);
1571
+ }
1572
+ function _sizeof(value, seen) {
1573
+ if (value == null) return 0;
1574
+ const t = typeof value;
1575
+ switch (t) {
1576
+ case "number":
1577
+ return OVERHEAD.number;
1578
+ case "boolean":
1579
+ return OVERHEAD.boolean;
1580
+ case "string":
1581
+ return OVERHEAD.string + value.length * 2;
1582
+ // UTF-16
1583
+ case "bigint":
1584
+ return OVERHEAD.bigint;
1585
+ case "symbol":
1586
+ return OVERHEAD.symbol;
1587
+ case "function":
1588
+ if (seen.has(value)) return 0;
1589
+ seen.add(value);
1590
+ return OVERHEAD.function;
1591
+ case "undefined":
1592
+ return 0;
1593
+ }
1594
+ const obj = value;
1595
+ if (seen.has(obj)) return 0;
1596
+ seen.add(obj);
1597
+ if (obj instanceof Map) {
1598
+ let size2 = OVERHEAD.map;
1599
+ for (const [k, v] of obj) {
1600
+ size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
1601
+ }
1602
+ return size2;
1603
+ }
1604
+ if (obj instanceof Set) {
1605
+ let size2 = OVERHEAD.set;
1606
+ for (const v of obj) {
1607
+ size2 += OVERHEAD.setEntry + _sizeof(v, seen);
1608
+ }
1609
+ return size2;
1610
+ }
1611
+ if (Array.isArray(obj)) {
1612
+ let size2 = OVERHEAD.array + obj.length * 8;
1613
+ for (const item of obj) {
1614
+ size2 += _sizeof(item, seen);
1615
+ }
1616
+ return size2;
1617
+ }
1618
+ if (obj instanceof ArrayBuffer) return obj.byteLength;
1619
+ if (ArrayBuffer.isView(obj)) return obj.byteLength;
1620
+ let size = OVERHEAD.object;
1621
+ const keys = Object.keys(obj);
1622
+ for (const key of keys) {
1623
+ size += OVERHEAD.string + key.length * 2;
1624
+ size += _sizeof(obj[key], seen);
1625
+ }
1626
+ return size;
1627
+ }
1628
+
1629
+ // src/graph/profile.ts
1630
+ function graphProfile(graph, opts) {
1631
+ const topN = opts?.topN ?? 10;
1632
+ const desc = graph.describe({ detail: "standard" });
1633
+ const targets = [];
1634
+ if (typeof graph._collectObserveTargets === "function") {
1635
+ graph._collectObserveTargets("", targets);
1636
+ }
1637
+ const pathToNode = /* @__PURE__ */ new Map();
1638
+ for (const [p, n] of targets) {
1639
+ pathToNode.set(p, n);
1640
+ }
1641
+ const profiles = [];
1642
+ for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
1643
+ const nd = pathToNode.get(path);
1644
+ const impl = nd instanceof NodeImpl ? nd : null;
1645
+ const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
1646
+ const subscriberCount = impl ? impl._sinkCount : 0;
1647
+ const depCount = nodeDesc.deps?.length ?? 0;
1648
+ const isOrphanEffect = nodeDesc.type === "effect" && subscriberCount === 0;
1649
+ profiles.push({
1650
+ path,
1651
+ type: nodeDesc.type,
1652
+ status: nodeDesc.status ?? "unknown",
1653
+ valueSizeBytes,
1654
+ subscriberCount,
1655
+ depCount,
1656
+ isOrphanEffect
1657
+ });
1658
+ }
1659
+ const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
1660
+ const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
1661
+ const orphanEffects = profiles.filter((p) => p.isOrphanEffect);
1662
+ return {
1663
+ nodeCount: profiles.length,
1664
+ edgeCount: desc.edges.length,
1665
+ subgraphCount: desc.subgraphs.length,
1666
+ nodes: profiles,
1667
+ totalValueSizeBytes,
1668
+ hotspots,
1669
+ orphanEffects
1670
+ };
1671
+ }
1672
+
1556
1673
  // src/graph/graph.ts
1557
1674
  var PATH_SEP = "::";
1558
1675
  var GRAPH_META_SEGMENT = "__meta__";
@@ -1695,7 +1812,7 @@ var RingBuffer = class {
1695
1812
  return result;
1696
1813
  }
1697
1814
  };
1698
- var SPY_ANSI_THEME = {
1815
+ var OBSERVE_ANSI_THEME = {
1699
1816
  data: "\x1B[32m",
1700
1817
  dirty: "\x1B[33m",
1701
1818
  resolved: "\x1B[36m",
@@ -1705,7 +1822,7 @@ var SPY_ANSI_THEME = {
1705
1822
  path: "\x1B[90m",
1706
1823
  reset: "\x1B[0m"
1707
1824
  };
1708
- var SPY_NO_COLOR_THEME = {
1825
+ var OBSERVE_NO_COLOR_THEME = {
1709
1826
  data: "",
1710
1827
  dirty: "",
1711
1828
  resolved: "",
@@ -1725,9 +1842,9 @@ function describeData(value) {
1725
1842
  return "[unserializable]";
1726
1843
  }
1727
1844
  }
1728
- function resolveSpyTheme(theme) {
1729
- if (theme === "none") return SPY_NO_COLOR_THEME;
1730
- if (theme === "ansi" || theme == null) return SPY_ANSI_THEME;
1845
+ function resolveObserveTheme(theme) {
1846
+ if (theme === "none") return OBSERVE_NO_COLOR_THEME;
1847
+ if (theme === "ansi" || theme == null) return OBSERVE_ANSI_THEME;
1731
1848
  return {
1732
1849
  data: theme.data ?? "",
1733
1850
  dirty: theme.dirty ?? "",
@@ -2465,6 +2582,16 @@ var Graph = class _Graph {
2465
2582
  }
2466
2583
  return out;
2467
2584
  }
2585
+ /**
2586
+ * Snapshot-based resource profile: per-node stats, orphan effect detection,
2587
+ * memory hotspots. Zero runtime overhead — walks nodes on demand.
2588
+ *
2589
+ * @param opts - Optional `topN` for hotspot limit (default 10).
2590
+ * @returns Aggregate profile with per-node details, hotspots, and orphan effects.
2591
+ */
2592
+ resourceProfile(opts) {
2593
+ return graphProfile(this, opts);
2594
+ }
2468
2595
  _qualifyEdgeEndpoint(part, prefix) {
2469
2596
  if (part.includes(PATH_SEP)) return part;
2470
2597
  return prefix === "" ? part : `${prefix}${PATH_SEP}${part}`;
@@ -2498,9 +2625,13 @@ var Graph = class _Graph {
2498
2625
  if (actor2 != null && !target.allowsObserve(actor2)) {
2499
2626
  throw new GuardDenied({ actor: actor2, action: "observe", nodeName: path });
2500
2627
  }
2501
- const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full";
2502
- if (wantsStructured2 && _Graph.inspectorEnabled) {
2503
- return this._createObserveResult(path, target, resolved);
2628
+ const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full" || resolved.format != null;
2629
+ if (wantsStructured2) {
2630
+ const result = _Graph.inspectorEnabled ? this._createObserveResult(path, target, resolved) : this._createFallbackObserveResult(path, resolved);
2631
+ if (resolved.format != null) {
2632
+ this._attachFormatLogger(result, resolved);
2633
+ }
2634
+ return result;
2504
2635
  }
2505
2636
  return {
2506
2637
  subscribe(sink) {
@@ -2518,9 +2649,13 @@ var Graph = class _Graph {
2518
2649
  }
2519
2650
  const opts = resolveObserveDetail(pathOrOpts);
2520
2651
  const actor = opts.actor;
2521
- const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full";
2522
- if (wantsStructured && _Graph.inspectorEnabled) {
2523
- return this._createObserveResultForAll(opts);
2652
+ const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full" || opts.format != null;
2653
+ if (wantsStructured) {
2654
+ const result = _Graph.inspectorEnabled ? this._createObserveResultForAll(opts) : this._createFallbackObserveResultForAll(opts);
2655
+ if (opts.format != null) {
2656
+ this._attachFormatLogger(result, opts);
2657
+ }
2658
+ return result;
2524
2659
  }
2525
2660
  return {
2526
2661
  subscribe: (sink) => {
@@ -2558,12 +2693,13 @@ var Graph = class _Graph {
2558
2693
  dirtyCount: 0,
2559
2694
  resolvedCount: 0,
2560
2695
  events: [],
2561
- completedCleanly: false,
2562
- errored: false
2696
+ anyCompletedCleanly: false,
2697
+ anyErrored: false
2563
2698
  };
2564
2699
  let lastTriggerDepIndex;
2565
2700
  let lastRunDepValues;
2566
2701
  let detachInspectorHook;
2702
+ let batchSeq = 0;
2567
2703
  if ((causal || derived) && target instanceof NodeImpl) {
2568
2704
  detachInspectorHook = target._setInspectorHook((event) => {
2569
2705
  if (event.kind === "dep_message") {
@@ -2576,15 +2712,16 @@ var Graph = class _Graph {
2576
2712
  type: "derived",
2577
2713
  path,
2578
2714
  dep_values: [...event.depValues],
2579
- ...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {}
2715
+ ...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {}
2580
2716
  });
2581
2717
  }
2582
2718
  });
2583
2719
  }
2584
2720
  const unsub = target.subscribe((msgs) => {
2721
+ batchSeq++;
2585
2722
  for (const m of msgs) {
2586
2723
  const t = m[0];
2587
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
2724
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
2588
2725
  const withCausal = causal && lastRunDepValues != null ? (() => {
2589
2726
  const triggerDep = lastTriggerDepIndex != null && lastTriggerDepIndex >= 0 && target instanceof NodeImpl ? target._deps[lastTriggerDepIndex] : void 0;
2590
2727
  const tv = triggerDep?.v;
@@ -2601,8 +2738,8 @@ var Graph = class _Graph {
2601
2738
  } else if (minimal) {
2602
2739
  if (t === DIRTY) result.dirtyCount++;
2603
2740
  else if (t === RESOLVED) result.resolvedCount++;
2604
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
2605
- else if (t === ERROR) result.errored = true;
2741
+ else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
2742
+ else if (t === ERROR) result.anyErrored = true;
2606
2743
  } else if (t === DIRTY) {
2607
2744
  result.dirtyCount++;
2608
2745
  result.events.push({ type: "dirty", path, ...base });
@@ -2610,10 +2747,10 @@ var Graph = class _Graph {
2610
2747
  result.resolvedCount++;
2611
2748
  result.events.push({ type: "resolved", path, ...base, ...withCausal });
2612
2749
  } else if (t === COMPLETE) {
2613
- if (!result.errored) result.completedCleanly = true;
2750
+ if (!result.anyErrored) result.anyCompletedCleanly = true;
2614
2751
  result.events.push({ type: "complete", path, ...base });
2615
2752
  } else if (t === ERROR) {
2616
- result.errored = true;
2753
+ result.anyErrored = true;
2617
2754
  result.events.push({ type: "error", path, data: m[1], ...base });
2618
2755
  }
2619
2756
  }
@@ -2633,11 +2770,14 @@ var Graph = class _Graph {
2633
2770
  get events() {
2634
2771
  return result.events;
2635
2772
  },
2636
- get completedCleanly() {
2637
- return result.completedCleanly;
2773
+ get anyCompletedCleanly() {
2774
+ return result.anyCompletedCleanly;
2775
+ },
2776
+ get anyErrored() {
2777
+ return result.anyErrored;
2638
2778
  },
2639
- get errored() {
2640
- return result.errored;
2779
+ get completedWithoutErrors() {
2780
+ return result.anyCompletedCleanly && !result.anyErrored;
2641
2781
  },
2642
2782
  dispose() {
2643
2783
  unsub();
@@ -2653,11 +2793,15 @@ var Graph = class _Graph {
2653
2793
  Object.assign(merged, extra);
2654
2794
  }
2655
2795
  const resolvedTarget = graph.resolve(basePath);
2656
- return graph._createObserveResult(
2796
+ const expanded = graph._createObserveResult(
2657
2797
  basePath,
2658
2798
  resolvedTarget,
2659
2799
  resolveObserveDetail(merged)
2660
2800
  );
2801
+ if (merged.format != null) {
2802
+ graph._attachFormatLogger(expanded, merged);
2803
+ }
2804
+ return expanded;
2661
2805
  }
2662
2806
  };
2663
2807
  }
@@ -2669,27 +2813,33 @@ var Graph = class _Graph {
2669
2813
  dirtyCount: 0,
2670
2814
  resolvedCount: 0,
2671
2815
  events: [],
2672
- completedCleanly: false,
2673
- errored: false
2816
+ anyCompletedCleanly: false,
2817
+ anyErrored: false
2674
2818
  };
2819
+ const nodeErrored = /* @__PURE__ */ new Set();
2675
2820
  const actor = options.actor;
2676
2821
  const targets = [];
2677
2822
  this._collectObserveTargets("", targets);
2678
2823
  targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
2679
2824
  const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
2825
+ let batchSeq = 0;
2680
2826
  const unsubs = picked.map(
2681
2827
  ([path, nd]) => nd.subscribe((msgs) => {
2828
+ batchSeq++;
2682
2829
  for (const m of msgs) {
2683
2830
  const t = m[0];
2684
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
2831
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
2685
2832
  if (t === DATA) {
2686
2833
  result.values[path] = m[1];
2687
2834
  result.events.push({ type: "data", path, data: m[1], ...base });
2688
2835
  } else if (minimal) {
2689
2836
  if (t === DIRTY) result.dirtyCount++;
2690
2837
  else if (t === RESOLVED) result.resolvedCount++;
2691
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
2692
- else if (t === ERROR) result.errored = true;
2838
+ else if (t === COMPLETE && !nodeErrored.has(path)) result.anyCompletedCleanly = true;
2839
+ else if (t === ERROR) {
2840
+ result.anyErrored = true;
2841
+ nodeErrored.add(path);
2842
+ }
2693
2843
  } else if (t === DIRTY) {
2694
2844
  result.dirtyCount++;
2695
2845
  result.events.push({ type: "dirty", path, ...base });
@@ -2697,10 +2847,11 @@ var Graph = class _Graph {
2697
2847
  result.resolvedCount++;
2698
2848
  result.events.push({ type: "resolved", path, ...base });
2699
2849
  } else if (t === COMPLETE) {
2700
- if (!result.errored) result.completedCleanly = true;
2850
+ if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
2701
2851
  result.events.push({ type: "complete", path, ...base });
2702
2852
  } else if (t === ERROR) {
2703
- result.errored = true;
2853
+ result.anyErrored = true;
2854
+ nodeErrored.add(path);
2704
2855
  result.events.push({ type: "error", path, data: m[1], ...base });
2705
2856
  }
2706
2857
  }
@@ -2720,11 +2871,14 @@ var Graph = class _Graph {
2720
2871
  get events() {
2721
2872
  return result.events;
2722
2873
  },
2723
- get completedCleanly() {
2724
- return result.completedCleanly;
2874
+ get anyCompletedCleanly() {
2875
+ return result.anyCompletedCleanly;
2725
2876
  },
2726
- get errored() {
2727
- return result.errored;
2877
+ get anyErrored() {
2878
+ return result.anyErrored;
2879
+ },
2880
+ get completedWithoutErrors() {
2881
+ return result.anyCompletedCleanly && !result.anyErrored;
2728
2882
  },
2729
2883
  dispose() {
2730
2884
  for (const u of unsubs) u();
@@ -2737,25 +2891,169 @@ var Graph = class _Graph {
2737
2891
  } else {
2738
2892
  Object.assign(merged, extra);
2739
2893
  }
2740
- return graph._createObserveResultForAll(resolveObserveDetail(merged));
2894
+ const expanded = graph._createObserveResultForAll(resolveObserveDetail(merged));
2895
+ if (merged.format != null) {
2896
+ graph._attachFormatLogger(expanded, merged);
2897
+ }
2898
+ return expanded;
2741
2899
  }
2742
2900
  };
2743
2901
  }
2744
2902
  /**
2745
- * Convenience live debugger over {@link Graph.observe}. Logs protocol events as they flow.
2746
- *
2747
- * Supports one-node (`path`) and graph-wide modes, event filtering, and JSON/pretty rendering.
2748
- * Color themes are built in (`ansi` / `none`) to avoid external dependencies.
2749
- *
2750
- * @param options - Spy configuration.
2751
- * @returns Disposable handle plus a structured observation accumulator.
2903
+ * Fallback ObserveResult for single-node when inspector is disabled but `format` is requested.
2904
+ * Subscribes to raw messages and accumulates events with timeline info.
2905
+ */
2906
+ _createFallbackObserveResult(path, options) {
2907
+ const timeline = options.timeline !== false;
2908
+ const acc = {
2909
+ values: {},
2910
+ dirtyCount: 0,
2911
+ resolvedCount: 0,
2912
+ events: [],
2913
+ anyCompletedCleanly: false,
2914
+ anyErrored: false
2915
+ };
2916
+ const target = this.resolve(path);
2917
+ let batchSeq = 0;
2918
+ const unsub = target.subscribe((msgs) => {
2919
+ batchSeq++;
2920
+ for (const m of msgs) {
2921
+ const t = m[0];
2922
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
2923
+ if (t === DATA) {
2924
+ acc.values[path] = m[1];
2925
+ acc.events.push({ type: "data", path, data: m[1], ...base });
2926
+ } else if (t === DIRTY) {
2927
+ acc.dirtyCount++;
2928
+ acc.events.push({ type: "dirty", path, ...base });
2929
+ } else if (t === RESOLVED) {
2930
+ acc.resolvedCount++;
2931
+ acc.events.push({ type: "resolved", path, ...base });
2932
+ } else if (t === COMPLETE) {
2933
+ if (!acc.anyErrored) acc.anyCompletedCleanly = true;
2934
+ acc.events.push({ type: "complete", path, ...base });
2935
+ } else if (t === ERROR) {
2936
+ acc.anyErrored = true;
2937
+ acc.events.push({ type: "error", path, data: m[1], ...base });
2938
+ }
2939
+ }
2940
+ });
2941
+ return {
2942
+ get values() {
2943
+ return acc.values;
2944
+ },
2945
+ get dirtyCount() {
2946
+ return acc.dirtyCount;
2947
+ },
2948
+ get resolvedCount() {
2949
+ return acc.resolvedCount;
2950
+ },
2951
+ get events() {
2952
+ return acc.events;
2953
+ },
2954
+ get anyCompletedCleanly() {
2955
+ return acc.anyCompletedCleanly;
2956
+ },
2957
+ get anyErrored() {
2958
+ return acc.anyErrored;
2959
+ },
2960
+ get completedWithoutErrors() {
2961
+ return acc.anyCompletedCleanly && !acc.anyErrored;
2962
+ },
2963
+ dispose() {
2964
+ unsub();
2965
+ },
2966
+ expand() {
2967
+ throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
2968
+ }
2969
+ };
2970
+ }
2971
+ /**
2972
+ * Fallback ObserveResult for graph-wide when inspector is disabled but `format` is requested.
2973
+ */
2974
+ _createFallbackObserveResultForAll(options) {
2975
+ const timeline = options.timeline !== false;
2976
+ const actor = options.actor;
2977
+ const acc = {
2978
+ values: {},
2979
+ dirtyCount: 0,
2980
+ resolvedCount: 0,
2981
+ events: [],
2982
+ anyCompletedCleanly: false,
2983
+ anyErrored: false
2984
+ };
2985
+ const nodeErrored = /* @__PURE__ */ new Set();
2986
+ const targets = [];
2987
+ this._collectObserveTargets("", targets);
2988
+ targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
2989
+ const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
2990
+ let batchSeq = 0;
2991
+ const unsubs = picked.map(
2992
+ ([path, nd]) => nd.subscribe((msgs) => {
2993
+ batchSeq++;
2994
+ for (const m of msgs) {
2995
+ const t = m[0];
2996
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
2997
+ if (t === DATA) {
2998
+ acc.values[path] = m[1];
2999
+ acc.events.push({ type: "data", path, data: m[1], ...base });
3000
+ } else if (t === DIRTY) {
3001
+ acc.dirtyCount++;
3002
+ acc.events.push({ type: "dirty", path, ...base });
3003
+ } else if (t === RESOLVED) {
3004
+ acc.resolvedCount++;
3005
+ acc.events.push({ type: "resolved", path, ...base });
3006
+ } else if (t === COMPLETE) {
3007
+ if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
3008
+ acc.events.push({ type: "complete", path, ...base });
3009
+ } else if (t === ERROR) {
3010
+ acc.anyErrored = true;
3011
+ nodeErrored.add(path);
3012
+ acc.events.push({ type: "error", path, data: m[1], ...base });
3013
+ }
3014
+ }
3015
+ })
3016
+ );
3017
+ return {
3018
+ get values() {
3019
+ return acc.values;
3020
+ },
3021
+ get dirtyCount() {
3022
+ return acc.dirtyCount;
3023
+ },
3024
+ get resolvedCount() {
3025
+ return acc.resolvedCount;
3026
+ },
3027
+ get events() {
3028
+ return acc.events;
3029
+ },
3030
+ get anyCompletedCleanly() {
3031
+ return acc.anyCompletedCleanly;
3032
+ },
3033
+ get anyErrored() {
3034
+ return acc.anyErrored;
3035
+ },
3036
+ get completedWithoutErrors() {
3037
+ return acc.anyCompletedCleanly && !acc.anyErrored;
3038
+ },
3039
+ dispose() {
3040
+ for (const u of unsubs) u();
3041
+ },
3042
+ expand() {
3043
+ throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
3044
+ }
3045
+ };
3046
+ }
3047
+ /**
3048
+ * Attaches a format logger to an ObserveResult, rendering events as they arrive.
3049
+ * Wraps the result's dispose to flush pending events.
2752
3050
  */
2753
- spy(options = {}) {
3051
+ _attachFormatLogger(result, options) {
3052
+ const format = options.format;
3053
+ const logger = options.logger ?? ((line) => console.log(line));
2754
3054
  const include = options.includeTypes ? new Set(options.includeTypes) : null;
2755
3055
  const exclude = options.excludeTypes ? new Set(options.excludeTypes) : null;
2756
- const theme = resolveSpyTheme(options.theme);
2757
- const format = options.format ?? "pretty";
2758
- const logger = options.logger ?? ((line) => console.log(line));
3056
+ const theme = resolveObserveTheme(options.theme);
2759
3057
  const shouldLog = (type) => {
2760
3058
  if (include?.has(type) === false) return false;
2761
3059
  if (exclude?.has(type) === true) return false;
@@ -2780,133 +3078,26 @@ var Graph = class _Graph {
2780
3078
  const batchPart = event.in_batch ? " [batch]" : "";
2781
3079
  return `${pathPart}${color}${event.type.toUpperCase()}${theme.reset}${dataPart}${triggerPart}${batchPart}`;
2782
3080
  };
2783
- if (!_Graph.inspectorEnabled) {
2784
- const timeline = options.timeline ?? true;
2785
- const acc = {
2786
- values: {},
2787
- dirtyCount: 0,
2788
- resolvedCount: 0,
2789
- events: [],
2790
- completedCleanly: false,
2791
- errored: false
2792
- };
2793
- let stop2 = () => {
2794
- };
2795
- const result2 = {
2796
- get values() {
2797
- return acc.values;
2798
- },
2799
- get dirtyCount() {
2800
- return acc.dirtyCount;
2801
- },
2802
- get resolvedCount() {
2803
- return acc.resolvedCount;
2804
- },
2805
- get events() {
2806
- return acc.events;
2807
- },
2808
- get completedCleanly() {
2809
- return acc.completedCleanly;
2810
- },
2811
- get errored() {
2812
- return acc.errored;
2813
- },
2814
- dispose() {
2815
- stop2();
2816
- },
2817
- expand() {
2818
- throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
2819
- }
2820
- };
2821
- const pushEvent = (path, message) => {
2822
- const t = message[0];
2823
- const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
2824
- let event;
2825
- if (t === DATA) {
2826
- if (path != null) acc.values[path] = message[1];
2827
- event = { type: "data", ...path != null ? { path } : {}, data: message[1], ...base };
2828
- } else if (t === DIRTY) {
2829
- acc.dirtyCount += 1;
2830
- event = { type: "dirty", ...path != null ? { path } : {}, ...base };
2831
- } else if (t === RESOLVED) {
2832
- acc.resolvedCount += 1;
2833
- event = { type: "resolved", ...path != null ? { path } : {}, ...base };
2834
- } else if (t === COMPLETE) {
2835
- if (!acc.errored) acc.completedCleanly = true;
2836
- event = { type: "complete", ...path != null ? { path } : {}, ...base };
2837
- } else if (t === ERROR) {
2838
- acc.errored = true;
2839
- event = {
2840
- type: "error",
2841
- ...path != null ? { path } : {},
2842
- data: message[1],
2843
- ...base
2844
- };
3081
+ let cursor = 0;
3082
+ const flush = () => {
3083
+ const events = result.events;
3084
+ while (cursor < events.length) {
3085
+ const event = events[cursor++];
3086
+ if (shouldLog(event.type)) {
3087
+ logger(renderEvent(event), event);
2845
3088
  }
2846
- if (!event) return;
2847
- acc.events.push(event);
2848
- if (!shouldLog(event.type)) return;
2849
- logger(renderEvent(event), event);
2850
- };
2851
- if (options.path != null) {
2852
- const stream2 = this.observe(options.path, {
2853
- actor: options.actor,
2854
- structured: false
2855
- });
2856
- stop2 = stream2.subscribe((messages) => {
2857
- for (const m of messages) {
2858
- pushEvent(options.path, m);
2859
- }
2860
- });
2861
- } else {
2862
- const stream2 = this.observe({ actor: options.actor, structured: false });
2863
- stop2 = stream2.subscribe((path, messages) => {
2864
- for (const m of messages) {
2865
- pushEvent(path, m);
2866
- }
2867
- });
2868
3089
  }
2869
- return {
2870
- result: result2,
2871
- dispose() {
2872
- result2.dispose();
2873
- }
2874
- };
2875
- }
2876
- const structuredObserveOptions = {
2877
- actor: options.actor,
2878
- structured: true,
2879
- ...options.timeline !== false ? { timeline: true } : {},
2880
- ...options.causal ? { causal: true } : {},
2881
- ...options.derived ? { derived: true } : {}
2882
3090
  };
2883
- const result = options.path != null ? this.observe(options.path, structuredObserveOptions) : this.observe(structuredObserveOptions);
2884
- let cursor = 0;
2885
- const flushNewEvents = () => {
2886
- const nextEvents = result.events.slice(cursor);
2887
- cursor = result.events.length;
2888
- for (const event of nextEvents) {
2889
- if (!shouldLog(event.type)) continue;
2890
- logger(renderEvent(event), event);
2891
- }
3091
+ const origPush = result.events.push;
3092
+ result.events.push = function(...items) {
3093
+ const ret = origPush.apply(this, items);
3094
+ flush();
3095
+ return ret;
2892
3096
  };
2893
- const stream = options.path != null ? this.observe(options.path, { actor: options.actor, structured: false }) : this.observe({ actor: options.actor, structured: false });
2894
- const stop = options.path != null ? stream.subscribe((messages) => {
2895
- if (messages.length > 0) {
2896
- flushNewEvents();
2897
- }
2898
- }) : stream.subscribe((_path, messages) => {
2899
- if (messages.length > 0) {
2900
- flushNewEvents();
2901
- }
2902
- });
2903
- return {
2904
- result,
2905
- dispose() {
2906
- stop();
2907
- flushNewEvents();
2908
- result.dispose();
2909
- }
3097
+ const origDispose = result.dispose.bind(result);
3098
+ result.dispose = () => {
3099
+ origDispose();
3100
+ flush();
2910
3101
  };
2911
3102
  }
2912
3103
  /**
@@ -3169,8 +3360,9 @@ var Graph = class _Graph {
3169
3360
  /**
3170
3361
  * Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
3171
3362
  *
3172
- * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 2 messages
3173
- * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1 control waves.
3363
+ * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 3 messages
3364
+ * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1/2 control
3365
+ * waves (`START`/`DIRTY`/`INVALIDATE`/`PAUSE`/`RESUME`).
3174
3366
  */
3175
3367
  autoCheckpoint(adapter, options = {}) {
3176
3368
  const debounceMs = Math.max(0, options.debounceMs ?? 500);
@@ -3217,7 +3409,7 @@ var Graph = class _Graph {
3217
3409
  timer = setTimeout(flush, debounceMs);
3218
3410
  };
3219
3411
  const off = this.observe().subscribe((path, messages) => {
3220
- const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 2);
3412
+ const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
3221
3413
  if (!triggeredByTier) return;
3222
3414
  if (options.filter) {
3223
3415
  const nd = this.resolve(path);
@@ -3301,33 +3493,21 @@ var Graph = class _Graph {
3301
3493
  // ——————————————————————————————————————————————————————————————
3302
3494
  /**
3303
3495
  * When `false`, structured observation options (`causal`, `timeline`),
3304
- * `annotate()`, and `traceLog()` are no-ops. Raw `observe()` always works.
3496
+ * and `trace()` writes are no-ops. Raw `observe()` always works.
3305
3497
  *
3306
3498
  * Default: `true` outside production (`process.env.NODE_ENV !== "production"`).
3307
3499
  */
3308
3500
  static inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
3309
3501
  _annotations = /* @__PURE__ */ new Map();
3310
3502
  _traceRing = new RingBuffer(1e3);
3311
- /**
3312
- * Attaches a reasoning annotation to a node — captures *why* an AI agent set a value.
3313
- *
3314
- * No-op when {@link Graph.inspectorEnabled} is `false`.
3315
- *
3316
- * @param path - Qualified node path.
3317
- * @param reason - Free-text note stored in the trace ring buffer.
3318
- */
3319
- annotate(path, reason) {
3320
- if (!_Graph.inspectorEnabled) return;
3321
- this.resolve(path);
3322
- this._annotations.set(path, reason);
3323
- this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
3324
- }
3325
- /**
3326
- * Returns a chronological log of all reasoning annotations (ring buffer).
3327
- *
3328
- * @returns `[]` when {@link Graph.inspectorEnabled} is `false`.
3329
- */
3330
- traceLog() {
3503
+ trace(path, reason) {
3504
+ if (path != null && reason != null) {
3505
+ if (!_Graph.inspectorEnabled) return;
3506
+ this.resolve(path);
3507
+ this._annotations.set(path, reason);
3508
+ this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
3509
+ return;
3510
+ }
3331
3511
  if (!_Graph.inspectorEnabled) return [];
3332
3512
  return this._traceRing.toArray();
3333
3513
  }
@@ -3455,125 +3635,6 @@ function reachable(described, from, direction, options = {}) {
3455
3635
  }
3456
3636
  return [...out].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3457
3637
  }
3458
-
3459
- // src/graph/sizeof.ts
3460
- var OVERHEAD = {
3461
- object: 56,
3462
- array: 64,
3463
- string: 40,
3464
- // header; content added separately
3465
- number: 8,
3466
- boolean: 4,
3467
- null: 0,
3468
- undefined: 0,
3469
- symbol: 40,
3470
- bigint: 16,
3471
- function: 120,
3472
- map: 72,
3473
- set: 72,
3474
- mapEntry: 40,
3475
- setEntry: 24
3476
- };
3477
- function sizeof(value) {
3478
- const seen = /* @__PURE__ */ new WeakSet();
3479
- return _sizeof(value, seen);
3480
- }
3481
- function _sizeof(value, seen) {
3482
- if (value == null) return 0;
3483
- const t = typeof value;
3484
- switch (t) {
3485
- case "number":
3486
- return OVERHEAD.number;
3487
- case "boolean":
3488
- return OVERHEAD.boolean;
3489
- case "string":
3490
- return OVERHEAD.string + value.length * 2;
3491
- // UTF-16
3492
- case "bigint":
3493
- return OVERHEAD.bigint;
3494
- case "symbol":
3495
- return OVERHEAD.symbol;
3496
- case "function":
3497
- if (seen.has(value)) return 0;
3498
- seen.add(value);
3499
- return OVERHEAD.function;
3500
- case "undefined":
3501
- return 0;
3502
- }
3503
- const obj = value;
3504
- if (seen.has(obj)) return 0;
3505
- seen.add(obj);
3506
- if (obj instanceof Map) {
3507
- let size2 = OVERHEAD.map;
3508
- for (const [k, v] of obj) {
3509
- size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
3510
- }
3511
- return size2;
3512
- }
3513
- if (obj instanceof Set) {
3514
- let size2 = OVERHEAD.set;
3515
- for (const v of obj) {
3516
- size2 += OVERHEAD.setEntry + _sizeof(v, seen);
3517
- }
3518
- return size2;
3519
- }
3520
- if (Array.isArray(obj)) {
3521
- let size2 = OVERHEAD.array + obj.length * 8;
3522
- for (const item of obj) {
3523
- size2 += _sizeof(item, seen);
3524
- }
3525
- return size2;
3526
- }
3527
- if (obj instanceof ArrayBuffer) return obj.byteLength;
3528
- if (ArrayBuffer.isView(obj)) return obj.byteLength;
3529
- let size = OVERHEAD.object;
3530
- const keys = Object.keys(obj);
3531
- for (const key of keys) {
3532
- size += OVERHEAD.string + key.length * 2;
3533
- size += _sizeof(obj[key], seen);
3534
- }
3535
- return size;
3536
- }
3537
-
3538
- // src/graph/profile.ts
3539
- function graphProfile(graph, opts) {
3540
- const topN = opts?.topN ?? 10;
3541
- const desc = graph.describe({ detail: "standard" });
3542
- const targets = [];
3543
- if (typeof graph._collectObserveTargets === "function") {
3544
- graph._collectObserveTargets("", targets);
3545
- }
3546
- const pathToNode = /* @__PURE__ */ new Map();
3547
- for (const [p, n] of targets) {
3548
- pathToNode.set(p, n);
3549
- }
3550
- const profiles = [];
3551
- for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
3552
- const nd = pathToNode.get(path);
3553
- const impl = nd instanceof NodeImpl ? nd : null;
3554
- const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
3555
- const subscriberCount = impl ? impl._sinkCount : 0;
3556
- const depCount = nodeDesc.deps?.length ?? 0;
3557
- profiles.push({
3558
- path,
3559
- type: nodeDesc.type,
3560
- status: nodeDesc.status ?? "unknown",
3561
- valueSizeBytes,
3562
- subscriberCount,
3563
- depCount
3564
- });
3565
- }
3566
- const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
3567
- const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
3568
- return {
3569
- nodeCount: profiles.length,
3570
- edgeCount: desc.edges.length,
3571
- subgraphCount: desc.subgraphs.length,
3572
- nodes: profiles,
3573
- totalValueSizeBytes,
3574
- hotspots
3575
- };
3576
- }
3577
3638
  // Annotate the CommonJS export names for ESM import in node:
3578
3639
  0 && (module.exports = {
3579
3640
  GRAPH_META_SEGMENT,