@graphrefly/graphrefly 0.18.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 (72) hide show
  1. package/dist/{chunk-TNKODJ6E.js → chunk-AHRKWMNI.js} +7 -3
  2. package/dist/{chunk-TNKODJ6E.js.map → chunk-AHRKWMNI.js.map} +1 -1
  3. package/dist/{chunk-76YPZQTW.js → chunk-BER7UYLM.js} +27 -26
  4. package/dist/chunk-BER7UYLM.js.map +1 -0
  5. package/dist/{chunk-F6ORUNO7.js → chunk-IRZAGZUB.js} +34 -2
  6. package/dist/{chunk-F6ORUNO7.js.map → chunk-IRZAGZUB.js.map} +1 -1
  7. package/dist/{chunk-LB3RYLSC.js → chunk-JC2SN46B.js} +197 -42
  8. package/dist/chunk-JC2SN46B.js.map +1 -0
  9. package/dist/{chunk-KJGUP35I.js → chunk-OO5QOAXI.js} +4 -4
  10. package/dist/{chunk-UVWEKTYC.js → chunk-UW77D7SP.js} +3 -3
  11. package/dist/{chunk-J7S54G7I.js → chunk-XUOY3YKN.js} +7 -2
  12. package/dist/chunk-XUOY3YKN.js.map +1 -0
  13. package/dist/chunk-YLR5JUJZ.js +111 -0
  14. package/dist/chunk-YLR5JUJZ.js.map +1 -0
  15. package/dist/{chunk-BV3TPSBK.js → chunk-YXR3WW3Q.js} +740 -755
  16. package/dist/chunk-YXR3WW3Q.js.map +1 -0
  17. package/dist/compat/nestjs/index.cjs +931 -784
  18. package/dist/compat/nestjs/index.cjs.map +1 -1
  19. package/dist/compat/nestjs/index.d.cts +4 -4
  20. package/dist/compat/nestjs/index.d.ts +4 -4
  21. package/dist/compat/nestjs/index.js +7 -7
  22. package/dist/core/index.cjs +651 -664
  23. package/dist/core/index.cjs.map +1 -1
  24. package/dist/core/index.d.cts +2 -2
  25. package/dist/core/index.d.ts +2 -2
  26. package/dist/core/index.js +7 -3
  27. package/dist/extra/index.cjs +686 -672
  28. package/dist/extra/index.cjs.map +1 -1
  29. package/dist/extra/index.d.cts +4 -4
  30. package/dist/extra/index.d.ts +4 -4
  31. package/dist/extra/index.js +5 -5
  32. package/dist/graph/index.cjs +836 -808
  33. package/dist/graph/index.cjs.map +1 -1
  34. package/dist/graph/index.d.cts +3 -3
  35. package/dist/graph/index.d.ts +3 -3
  36. package/dist/graph/index.js +8 -8
  37. package/dist/{graph-gISB9n3n.d.ts → graph-KsTe57nI.d.cts} +82 -8
  38. package/dist/{graph-BYFlyNpX.d.cts → graph-mILUUqW8.d.ts} +82 -8
  39. package/dist/{index-CgKPpiu8.d.ts → index-8a605sg9.d.ts} +2 -2
  40. package/dist/{index-DKaB2x0T.d.ts → index-B2SvPEbc.d.ts} +6 -65
  41. package/dist/{index-B80mMeuf.d.ts → index-BBUYZfJ1.d.cts} +122 -76
  42. package/dist/{index-D_tUMcpz.d.cts → index-Bjh5C1Tp.d.cts} +37 -32
  43. package/dist/{index-B43mC7uY.d.cts → index-BjtlNirP.d.cts} +3 -3
  44. package/dist/{index-7WnwgjMu.d.ts → index-BnkMgNNa.d.ts} +37 -32
  45. package/dist/{index-CEDaJaYE.d.ts → index-CgSiUouz.d.ts} +3 -3
  46. package/dist/{index-EmzYk-TG.d.cts → index-CvKzv0AW.d.ts} +122 -76
  47. package/dist/{index-Ci_vPaVm.d.cts → index-UudxGnzc.d.cts} +6 -65
  48. package/dist/{index-BqOWSFhr.d.cts → index-VHA43cGP.d.cts} +2 -2
  49. package/dist/index.cjs +5920 -5572
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +595 -399
  52. package/dist/index.d.ts +595 -399
  53. package/dist/index.js +4357 -4063
  54. package/dist/index.js.map +1 -1
  55. package/dist/{meta-npl5b97j.d.cts → meta-BnG7XAaE.d.cts} +394 -236
  56. package/dist/{meta-npl5b97j.d.ts → meta-BnG7XAaE.d.ts} +394 -236
  57. package/dist/{observable-DFBCBELR.d.cts → observable-C8Kx_O6k.d.cts} +1 -1
  58. package/dist/{observable-oAGygKvc.d.ts → observable-DcBwQY7t.d.ts} +1 -1
  59. package/dist/patterns/reactive-layout/index.cjs +865 -718
  60. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  61. package/dist/patterns/reactive-layout/index.d.cts +3 -3
  62. package/dist/patterns/reactive-layout/index.d.ts +3 -3
  63. package/dist/patterns/reactive-layout/index.js +4 -4
  64. package/package.json +1 -1
  65. package/dist/chunk-76YPZQTW.js.map +0 -1
  66. package/dist/chunk-BV3TPSBK.js.map +0 -1
  67. package/dist/chunk-FCLROC4Q.js +0 -231
  68. package/dist/chunk-FCLROC4Q.js.map +0 -1
  69. package/dist/chunk-J7S54G7I.js.map +0 -1
  70. package/dist/chunk-LB3RYLSC.js.map +0 -1
  71. /package/dist/{chunk-KJGUP35I.js.map → chunk-OO5QOAXI.js.map} +0 -0
  72. /package/dist/{chunk-UVWEKTYC.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";
868
917
  }
869
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();
954
+ }
955
+ this._depDirtyMask.reset();
956
+ this._depSettledMask.reset();
957
+ this._depCompleteMask.reset();
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
1128
  _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
1129
  // Dynamic deps tracking
1130
+ /** @internal Read by `describeNode`. */
1022
1131
  _deps = [];
1023
1132
  _depUnsubs = [];
1024
1133
  _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;
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;
1031
1143
  constructor(fn, opts) {
1144
+ super(opts);
1032
1145
  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
1146
  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;
1147
+ this.down = this.down.bind(this);
1148
+ this.up = this.up.bind(this);
1093
1149
  }
1094
- get lastMutation() {
1095
- return this._lastMutation;
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
+ });
1096
1157
  }
1097
- /** Versioning not yet supported on DynamicNodeImpl. */
1158
+ /** Versioning not supported on DynamicNodeImpl (override base). */
1098
1159
  get v() {
1099
1160
  return void 0;
1100
1161
  }
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
- };
1211
- }
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__";
@@ -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}`;
@@ -2566,8 +2693,8 @@ var Graph = class _Graph {
2566
2693
  dirtyCount: 0,
2567
2694
  resolvedCount: 0,
2568
2695
  events: [],
2569
- completedCleanly: false,
2570
- errored: false
2696
+ anyCompletedCleanly: false,
2697
+ anyErrored: false
2571
2698
  };
2572
2699
  let lastTriggerDepIndex;
2573
2700
  let lastRunDepValues;
@@ -2611,8 +2738,8 @@ var Graph = class _Graph {
2611
2738
  } else if (minimal) {
2612
2739
  if (t === DIRTY) result.dirtyCount++;
2613
2740
  else if (t === RESOLVED) result.resolvedCount++;
2614
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
2615
- 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;
2616
2743
  } else if (t === DIRTY) {
2617
2744
  result.dirtyCount++;
2618
2745
  result.events.push({ type: "dirty", path, ...base });
@@ -2620,10 +2747,10 @@ var Graph = class _Graph {
2620
2747
  result.resolvedCount++;
2621
2748
  result.events.push({ type: "resolved", path, ...base, ...withCausal });
2622
2749
  } else if (t === COMPLETE) {
2623
- if (!result.errored) result.completedCleanly = true;
2750
+ if (!result.anyErrored) result.anyCompletedCleanly = true;
2624
2751
  result.events.push({ type: "complete", path, ...base });
2625
2752
  } else if (t === ERROR) {
2626
- result.errored = true;
2753
+ result.anyErrored = true;
2627
2754
  result.events.push({ type: "error", path, data: m[1], ...base });
2628
2755
  }
2629
2756
  }
@@ -2643,11 +2770,14 @@ var Graph = class _Graph {
2643
2770
  get events() {
2644
2771
  return result.events;
2645
2772
  },
2646
- get completedCleanly() {
2647
- return result.completedCleanly;
2773
+ get anyCompletedCleanly() {
2774
+ return result.anyCompletedCleanly;
2648
2775
  },
2649
- get errored() {
2650
- return result.errored;
2776
+ get anyErrored() {
2777
+ return result.anyErrored;
2778
+ },
2779
+ get completedWithoutErrors() {
2780
+ return result.anyCompletedCleanly && !result.anyErrored;
2651
2781
  },
2652
2782
  dispose() {
2653
2783
  unsub();
@@ -2683,9 +2813,10 @@ var Graph = class _Graph {
2683
2813
  dirtyCount: 0,
2684
2814
  resolvedCount: 0,
2685
2815
  events: [],
2686
- completedCleanly: false,
2687
- errored: false
2816
+ anyCompletedCleanly: false,
2817
+ anyErrored: false
2688
2818
  };
2819
+ const nodeErrored = /* @__PURE__ */ new Set();
2689
2820
  const actor = options.actor;
2690
2821
  const targets = [];
2691
2822
  this._collectObserveTargets("", targets);
@@ -2704,8 +2835,11 @@ var Graph = class _Graph {
2704
2835
  } else if (minimal) {
2705
2836
  if (t === DIRTY) result.dirtyCount++;
2706
2837
  else if (t === RESOLVED) result.resolvedCount++;
2707
- else if (t === COMPLETE && !result.errored) result.completedCleanly = true;
2708
- 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
+ }
2709
2843
  } else if (t === DIRTY) {
2710
2844
  result.dirtyCount++;
2711
2845
  result.events.push({ type: "dirty", path, ...base });
@@ -2713,10 +2847,11 @@ var Graph = class _Graph {
2713
2847
  result.resolvedCount++;
2714
2848
  result.events.push({ type: "resolved", path, ...base });
2715
2849
  } else if (t === COMPLETE) {
2716
- if (!result.errored) result.completedCleanly = true;
2850
+ if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
2717
2851
  result.events.push({ type: "complete", path, ...base });
2718
2852
  } else if (t === ERROR) {
2719
- result.errored = true;
2853
+ result.anyErrored = true;
2854
+ nodeErrored.add(path);
2720
2855
  result.events.push({ type: "error", path, data: m[1], ...base });
2721
2856
  }
2722
2857
  }
@@ -2736,11 +2871,14 @@ var Graph = class _Graph {
2736
2871
  get events() {
2737
2872
  return result.events;
2738
2873
  },
2739
- get completedCleanly() {
2740
- return result.completedCleanly;
2874
+ get anyCompletedCleanly() {
2875
+ return result.anyCompletedCleanly;
2741
2876
  },
2742
- get errored() {
2743
- return result.errored;
2877
+ get anyErrored() {
2878
+ return result.anyErrored;
2879
+ },
2880
+ get completedWithoutErrors() {
2881
+ return result.anyCompletedCleanly && !result.anyErrored;
2744
2882
  },
2745
2883
  dispose() {
2746
2884
  for (const u of unsubs) u();
@@ -2772,8 +2910,8 @@ var Graph = class _Graph {
2772
2910
  dirtyCount: 0,
2773
2911
  resolvedCount: 0,
2774
2912
  events: [],
2775
- completedCleanly: false,
2776
- errored: false
2913
+ anyCompletedCleanly: false,
2914
+ anyErrored: false
2777
2915
  };
2778
2916
  const target = this.resolve(path);
2779
2917
  let batchSeq = 0;
@@ -2792,10 +2930,10 @@ var Graph = class _Graph {
2792
2930
  acc.resolvedCount++;
2793
2931
  acc.events.push({ type: "resolved", path, ...base });
2794
2932
  } else if (t === COMPLETE) {
2795
- if (!acc.errored) acc.completedCleanly = true;
2933
+ if (!acc.anyErrored) acc.anyCompletedCleanly = true;
2796
2934
  acc.events.push({ type: "complete", path, ...base });
2797
2935
  } else if (t === ERROR) {
2798
- acc.errored = true;
2936
+ acc.anyErrored = true;
2799
2937
  acc.events.push({ type: "error", path, data: m[1], ...base });
2800
2938
  }
2801
2939
  }
@@ -2813,11 +2951,14 @@ var Graph = class _Graph {
2813
2951
  get events() {
2814
2952
  return acc.events;
2815
2953
  },
2816
- get completedCleanly() {
2817
- return acc.completedCleanly;
2954
+ get anyCompletedCleanly() {
2955
+ return acc.anyCompletedCleanly;
2956
+ },
2957
+ get anyErrored() {
2958
+ return acc.anyErrored;
2818
2959
  },
2819
- get errored() {
2820
- return acc.errored;
2960
+ get completedWithoutErrors() {
2961
+ return acc.anyCompletedCleanly && !acc.anyErrored;
2821
2962
  },
2822
2963
  dispose() {
2823
2964
  unsub();
@@ -2838,9 +2979,10 @@ var Graph = class _Graph {
2838
2979
  dirtyCount: 0,
2839
2980
  resolvedCount: 0,
2840
2981
  events: [],
2841
- completedCleanly: false,
2842
- errored: false
2982
+ anyCompletedCleanly: false,
2983
+ anyErrored: false
2843
2984
  };
2985
+ const nodeErrored = /* @__PURE__ */ new Set();
2844
2986
  const targets = [];
2845
2987
  this._collectObserveTargets("", targets);
2846
2988
  targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
@@ -2862,10 +3004,11 @@ var Graph = class _Graph {
2862
3004
  acc.resolvedCount++;
2863
3005
  acc.events.push({ type: "resolved", path, ...base });
2864
3006
  } else if (t === COMPLETE) {
2865
- if (!acc.errored) acc.completedCleanly = true;
3007
+ if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
2866
3008
  acc.events.push({ type: "complete", path, ...base });
2867
3009
  } else if (t === ERROR) {
2868
- acc.errored = true;
3010
+ acc.anyErrored = true;
3011
+ nodeErrored.add(path);
2869
3012
  acc.events.push({ type: "error", path, data: m[1], ...base });
2870
3013
  }
2871
3014
  }
@@ -2884,11 +3027,14 @@ var Graph = class _Graph {
2884
3027
  get events() {
2885
3028
  return acc.events;
2886
3029
  },
2887
- get completedCleanly() {
2888
- return acc.completedCleanly;
3030
+ get anyCompletedCleanly() {
3031
+ return acc.anyCompletedCleanly;
2889
3032
  },
2890
- get errored() {
2891
- return acc.errored;
3033
+ get anyErrored() {
3034
+ return acc.anyErrored;
3035
+ },
3036
+ get completedWithoutErrors() {
3037
+ return acc.anyCompletedCleanly && !acc.anyErrored;
2892
3038
  },
2893
3039
  dispose() {
2894
3040
  for (const u of unsubs) u();
@@ -3214,8 +3360,9 @@ var Graph = class _Graph {
3214
3360
  /**
3215
3361
  * Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
3216
3362
  *
3217
- * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 2 messages
3218
- * 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`).
3219
3366
  */
3220
3367
  autoCheckpoint(adapter, options = {}) {
3221
3368
  const debounceMs = Math.max(0, options.debounceMs ?? 500);
@@ -3262,7 +3409,7 @@ var Graph = class _Graph {
3262
3409
  timer = setTimeout(flush, debounceMs);
3263
3410
  };
3264
3411
  const off = this.observe().subscribe((path, messages) => {
3265
- const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 2);
3412
+ const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
3266
3413
  if (!triggeredByTier) return;
3267
3414
  if (options.filter) {
3268
3415
  const nd = this.resolve(path);
@@ -3488,125 +3635,6 @@ function reachable(described, from, direction, options = {}) {
3488
3635
  }
3489
3636
  return [...out].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3490
3637
  }
3491
-
3492
- // src/graph/sizeof.ts
3493
- var OVERHEAD = {
3494
- object: 56,
3495
- array: 64,
3496
- string: 40,
3497
- // header; content added separately
3498
- number: 8,
3499
- boolean: 4,
3500
- null: 0,
3501
- undefined: 0,
3502
- symbol: 40,
3503
- bigint: 16,
3504
- function: 120,
3505
- map: 72,
3506
- set: 72,
3507
- mapEntry: 40,
3508
- setEntry: 24
3509
- };
3510
- function sizeof(value) {
3511
- const seen = /* @__PURE__ */ new WeakSet();
3512
- return _sizeof(value, seen);
3513
- }
3514
- function _sizeof(value, seen) {
3515
- if (value == null) return 0;
3516
- const t = typeof value;
3517
- switch (t) {
3518
- case "number":
3519
- return OVERHEAD.number;
3520
- case "boolean":
3521
- return OVERHEAD.boolean;
3522
- case "string":
3523
- return OVERHEAD.string + value.length * 2;
3524
- // UTF-16
3525
- case "bigint":
3526
- return OVERHEAD.bigint;
3527
- case "symbol":
3528
- return OVERHEAD.symbol;
3529
- case "function":
3530
- if (seen.has(value)) return 0;
3531
- seen.add(value);
3532
- return OVERHEAD.function;
3533
- case "undefined":
3534
- return 0;
3535
- }
3536
- const obj = value;
3537
- if (seen.has(obj)) return 0;
3538
- seen.add(obj);
3539
- if (obj instanceof Map) {
3540
- let size2 = OVERHEAD.map;
3541
- for (const [k, v] of obj) {
3542
- size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
3543
- }
3544
- return size2;
3545
- }
3546
- if (obj instanceof Set) {
3547
- let size2 = OVERHEAD.set;
3548
- for (const v of obj) {
3549
- size2 += OVERHEAD.setEntry + _sizeof(v, seen);
3550
- }
3551
- return size2;
3552
- }
3553
- if (Array.isArray(obj)) {
3554
- let size2 = OVERHEAD.array + obj.length * 8;
3555
- for (const item of obj) {
3556
- size2 += _sizeof(item, seen);
3557
- }
3558
- return size2;
3559
- }
3560
- if (obj instanceof ArrayBuffer) return obj.byteLength;
3561
- if (ArrayBuffer.isView(obj)) return obj.byteLength;
3562
- let size = OVERHEAD.object;
3563
- const keys = Object.keys(obj);
3564
- for (const key of keys) {
3565
- size += OVERHEAD.string + key.length * 2;
3566
- size += _sizeof(obj[key], seen);
3567
- }
3568
- return size;
3569
- }
3570
-
3571
- // src/graph/profile.ts
3572
- function graphProfile(graph, opts) {
3573
- const topN = opts?.topN ?? 10;
3574
- const desc = graph.describe({ detail: "standard" });
3575
- const targets = [];
3576
- if (typeof graph._collectObserveTargets === "function") {
3577
- graph._collectObserveTargets("", targets);
3578
- }
3579
- const pathToNode = /* @__PURE__ */ new Map();
3580
- for (const [p, n] of targets) {
3581
- pathToNode.set(p, n);
3582
- }
3583
- const profiles = [];
3584
- for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
3585
- const nd = pathToNode.get(path);
3586
- const impl = nd instanceof NodeImpl ? nd : null;
3587
- const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
3588
- const subscriberCount = impl ? impl._sinkCount : 0;
3589
- const depCount = nodeDesc.deps?.length ?? 0;
3590
- profiles.push({
3591
- path,
3592
- type: nodeDesc.type,
3593
- status: nodeDesc.status ?? "unknown",
3594
- valueSizeBytes,
3595
- subscriberCount,
3596
- depCount
3597
- });
3598
- }
3599
- const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
3600
- const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
3601
- return {
3602
- nodeCount: profiles.length,
3603
- edgeCount: desc.edges.length,
3604
- subgraphCount: desc.subgraphs.length,
3605
- nodes: profiles,
3606
- totalValueSizeBytes,
3607
- hotspots
3608
- };
3609
- }
3610
3638
  // Annotate the CommonJS export names for ESM import in node:
3611
3639
  0 && (module.exports = {
3612
3640
  GRAPH_META_SEGMENT,