@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.
- package/dist/{chunk-TNKODJ6E.js → chunk-AHRKWMNI.js} +7 -3
- package/dist/{chunk-TNKODJ6E.js.map → chunk-AHRKWMNI.js.map} +1 -1
- package/dist/{chunk-76YPZQTW.js → chunk-BER7UYLM.js} +27 -26
- package/dist/chunk-BER7UYLM.js.map +1 -0
- package/dist/{chunk-F6ORUNO7.js → chunk-IRZAGZUB.js} +34 -2
- package/dist/{chunk-F6ORUNO7.js.map → chunk-IRZAGZUB.js.map} +1 -1
- package/dist/{chunk-LB3RYLSC.js → chunk-JC2SN46B.js} +197 -42
- package/dist/chunk-JC2SN46B.js.map +1 -0
- package/dist/{chunk-KJGUP35I.js → chunk-OO5QOAXI.js} +4 -4
- package/dist/{chunk-UVWEKTYC.js → chunk-UW77D7SP.js} +3 -3
- package/dist/{chunk-J7S54G7I.js → chunk-XUOY3YKN.js} +7 -2
- package/dist/chunk-XUOY3YKN.js.map +1 -0
- package/dist/chunk-YLR5JUJZ.js +111 -0
- package/dist/chunk-YLR5JUJZ.js.map +1 -0
- package/dist/{chunk-BV3TPSBK.js → chunk-YXR3WW3Q.js} +740 -755
- package/dist/chunk-YXR3WW3Q.js.map +1 -0
- package/dist/compat/nestjs/index.cjs +931 -784
- package/dist/compat/nestjs/index.cjs.map +1 -1
- package/dist/compat/nestjs/index.d.cts +4 -4
- package/dist/compat/nestjs/index.d.ts +4 -4
- package/dist/compat/nestjs/index.js +7 -7
- package/dist/core/index.cjs +651 -664
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +7 -3
- package/dist/extra/index.cjs +686 -672
- package/dist/extra/index.cjs.map +1 -1
- package/dist/extra/index.d.cts +4 -4
- package/dist/extra/index.d.ts +4 -4
- package/dist/extra/index.js +5 -5
- package/dist/graph/index.cjs +836 -808
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +3 -3
- package/dist/graph/index.d.ts +3 -3
- package/dist/graph/index.js +8 -8
- package/dist/{graph-gISB9n3n.d.ts → graph-KsTe57nI.d.cts} +82 -8
- package/dist/{graph-BYFlyNpX.d.cts → graph-mILUUqW8.d.ts} +82 -8
- package/dist/{index-CgKPpiu8.d.ts → index-8a605sg9.d.ts} +2 -2
- package/dist/{index-DKaB2x0T.d.ts → index-B2SvPEbc.d.ts} +6 -65
- package/dist/{index-B80mMeuf.d.ts → index-BBUYZfJ1.d.cts} +122 -76
- package/dist/{index-D_tUMcpz.d.cts → index-Bjh5C1Tp.d.cts} +37 -32
- package/dist/{index-B43mC7uY.d.cts → index-BjtlNirP.d.cts} +3 -3
- package/dist/{index-7WnwgjMu.d.ts → index-BnkMgNNa.d.ts} +37 -32
- package/dist/{index-CEDaJaYE.d.ts → index-CgSiUouz.d.ts} +3 -3
- package/dist/{index-EmzYk-TG.d.cts → index-CvKzv0AW.d.ts} +122 -76
- package/dist/{index-Ci_vPaVm.d.cts → index-UudxGnzc.d.cts} +6 -65
- package/dist/{index-BqOWSFhr.d.cts → index-VHA43cGP.d.cts} +2 -2
- package/dist/index.cjs +5920 -5572
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +595 -399
- package/dist/index.d.ts +595 -399
- package/dist/index.js +4357 -4063
- package/dist/index.js.map +1 -1
- package/dist/{meta-npl5b97j.d.cts → meta-BnG7XAaE.d.cts} +394 -236
- package/dist/{meta-npl5b97j.d.ts → meta-BnG7XAaE.d.ts} +394 -236
- package/dist/{observable-DFBCBELR.d.cts → observable-C8Kx_O6k.d.cts} +1 -1
- package/dist/{observable-oAGygKvc.d.ts → observable-DcBwQY7t.d.ts} +1 -1
- package/dist/patterns/reactive-layout/index.cjs +865 -718
- package/dist/patterns/reactive-layout/index.cjs.map +1 -1
- package/dist/patterns/reactive-layout/index.d.cts +3 -3
- package/dist/patterns/reactive-layout/index.d.ts +3 -3
- package/dist/patterns/reactive-layout/index.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-76YPZQTW.js.map +0 -1
- package/dist/chunk-BV3TPSBK.js.map +0 -1
- package/dist/chunk-FCLROC4Q.js +0 -231
- package/dist/chunk-FCLROC4Q.js.map +0 -1
- package/dist/chunk-J7S54G7I.js.map +0 -1
- package/dist/chunk-LB3RYLSC.js.map +0 -1
- /package/dist/{chunk-KJGUP35I.js.map → chunk-OO5QOAXI.js.map} +0 -0
- /package/dist/{chunk-UVWEKTYC.js.map → chunk-UW77D7SP.js.map} +0 -0
|
@@ -317,6 +317,7 @@ var ImageSizeAdapter = class {
|
|
|
317
317
|
};
|
|
318
318
|
|
|
319
319
|
// src/core/messages.ts
|
|
320
|
+
var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
|
|
320
321
|
var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
|
|
321
322
|
var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
|
|
322
323
|
var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
|
|
@@ -326,13 +327,27 @@ var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
|
|
|
326
327
|
var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
|
|
327
328
|
var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
|
|
328
329
|
var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
|
|
330
|
+
var knownMessageTypes = [
|
|
331
|
+
START,
|
|
332
|
+
DATA,
|
|
333
|
+
DIRTY,
|
|
334
|
+
RESOLVED,
|
|
335
|
+
INVALIDATE,
|
|
336
|
+
PAUSE,
|
|
337
|
+
RESUME,
|
|
338
|
+
TEARDOWN,
|
|
339
|
+
COMPLETE,
|
|
340
|
+
ERROR
|
|
341
|
+
];
|
|
342
|
+
var knownMessageSet = new Set(knownMessageTypes);
|
|
329
343
|
function messageTier(t) {
|
|
330
|
-
if (t ===
|
|
331
|
-
if (t ===
|
|
332
|
-
if (t ===
|
|
333
|
-
if (t ===
|
|
334
|
-
if (t ===
|
|
335
|
-
return
|
|
344
|
+
if (t === START) return 0;
|
|
345
|
+
if (t === DIRTY || t === INVALIDATE) return 1;
|
|
346
|
+
if (t === PAUSE || t === RESUME) return 2;
|
|
347
|
+
if (t === DATA || t === RESOLVED) return 3;
|
|
348
|
+
if (t === COMPLETE || t === ERROR) return 4;
|
|
349
|
+
if (t === TEARDOWN) return 5;
|
|
350
|
+
return 1;
|
|
336
351
|
}
|
|
337
352
|
function isPhase2Message(msg) {
|
|
338
353
|
const t = msg[0];
|
|
@@ -420,14 +435,14 @@ function _downSequential(sink, messages, phase = 2) {
|
|
|
420
435
|
const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
|
|
421
436
|
for (const msg of messages) {
|
|
422
437
|
const tier = messageTier(msg[0]);
|
|
423
|
-
if (tier ===
|
|
438
|
+
if (tier === 3) {
|
|
424
439
|
if (isBatching()) {
|
|
425
440
|
const m = msg;
|
|
426
441
|
dataQueue.push(() => sink([m]));
|
|
427
442
|
} else {
|
|
428
443
|
sink([msg]);
|
|
429
444
|
}
|
|
430
|
-
} else if (tier >=
|
|
445
|
+
} else if (tier >= 4) {
|
|
431
446
|
if (isBatching()) {
|
|
432
447
|
const m = msg;
|
|
433
448
|
pendingPhase3.push(() => sink([m]));
|
|
@@ -546,10 +561,24 @@ function advanceVersion(info, newValue, hashFn) {
|
|
|
546
561
|
}
|
|
547
562
|
}
|
|
548
563
|
|
|
549
|
-
// src/core/node.ts
|
|
564
|
+
// src/core/node-base.ts
|
|
550
565
|
var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
|
|
551
566
|
var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
|
|
552
|
-
|
|
567
|
+
var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
|
|
568
|
+
var isCleanupFn = (value) => typeof value === "function";
|
|
569
|
+
function statusAfterMessage(status, msg) {
|
|
570
|
+
const t = msg[0];
|
|
571
|
+
if (t === DIRTY) return "dirty";
|
|
572
|
+
if (t === DATA) return "settled";
|
|
573
|
+
if (t === RESOLVED) return "resolved";
|
|
574
|
+
if (t === COMPLETE) return "completed";
|
|
575
|
+
if (t === ERROR) return "errored";
|
|
576
|
+
if (t === INVALIDATE) return "dirty";
|
|
577
|
+
if (t === TEARDOWN) return "disconnected";
|
|
578
|
+
return status;
|
|
579
|
+
}
|
|
580
|
+
function createIntBitSet(size) {
|
|
581
|
+
const fullMask = size >= 32 ? -1 : ~(-1 << size);
|
|
553
582
|
let bits = 0;
|
|
554
583
|
return {
|
|
555
584
|
set(i) {
|
|
@@ -562,7 +591,8 @@ function createIntBitSet() {
|
|
|
562
591
|
return (bits & 1 << i) !== 0;
|
|
563
592
|
},
|
|
564
593
|
covers(other) {
|
|
565
|
-
|
|
594
|
+
const otherBits = other._bits();
|
|
595
|
+
return (bits & otherBits) === otherBits;
|
|
566
596
|
},
|
|
567
597
|
any() {
|
|
568
598
|
return bits !== 0;
|
|
@@ -570,6 +600,9 @@ function createIntBitSet() {
|
|
|
570
600
|
reset() {
|
|
571
601
|
bits = 0;
|
|
572
602
|
},
|
|
603
|
+
setAll() {
|
|
604
|
+
bits = fullMask;
|
|
605
|
+
},
|
|
573
606
|
_bits() {
|
|
574
607
|
return bits;
|
|
575
608
|
}
|
|
@@ -577,6 +610,8 @@ function createIntBitSet() {
|
|
|
577
610
|
}
|
|
578
611
|
function createArrayBitSet(size) {
|
|
579
612
|
const words = new Uint32Array(Math.ceil(size / 32));
|
|
613
|
+
const lastBits = size % 32;
|
|
614
|
+
const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
|
|
580
615
|
return {
|
|
581
616
|
set(i) {
|
|
582
617
|
words[i >>> 5] |= 1 << (i & 31);
|
|
@@ -603,130 +638,103 @@ function createArrayBitSet(size) {
|
|
|
603
638
|
reset() {
|
|
604
639
|
words.fill(0);
|
|
605
640
|
},
|
|
641
|
+
setAll() {
|
|
642
|
+
for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
|
|
643
|
+
if (words.length > 0) words[words.length - 1] = lastWordMask;
|
|
644
|
+
},
|
|
606
645
|
_words: words
|
|
607
646
|
};
|
|
608
647
|
}
|
|
609
648
|
function createBitSet(size) {
|
|
610
|
-
return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
|
|
649
|
+
return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
|
|
611
650
|
}
|
|
612
|
-
var
|
|
613
|
-
|
|
614
|
-
var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
|
|
615
|
-
var isCleanupFn = (value) => typeof value === "function";
|
|
616
|
-
var statusAfterMessage = (status, msg) => {
|
|
617
|
-
const t = msg[0];
|
|
618
|
-
if (t === DIRTY) return "dirty";
|
|
619
|
-
if (t === DATA) return "settled";
|
|
620
|
-
if (t === RESOLVED) return "resolved";
|
|
621
|
-
if (t === COMPLETE) return "completed";
|
|
622
|
-
if (t === ERROR) return "errored";
|
|
623
|
-
if (t === INVALIDATE) return "dirty";
|
|
624
|
-
if (t === TEARDOWN) return "disconnected";
|
|
625
|
-
return status;
|
|
626
|
-
};
|
|
627
|
-
var NodeImpl = class {
|
|
628
|
-
// --- Configuration (set once, never reassigned) ---
|
|
651
|
+
var NodeBase = class {
|
|
652
|
+
// --- Identity (set once) ---
|
|
629
653
|
_optsName;
|
|
630
654
|
_registryName;
|
|
631
|
-
/** @internal
|
|
655
|
+
/** @internal Read by `describeNode` before inference. */
|
|
632
656
|
_describeKind;
|
|
633
657
|
meta;
|
|
634
|
-
|
|
635
|
-
_fn;
|
|
636
|
-
_opts;
|
|
658
|
+
// --- Options ---
|
|
637
659
|
_equals;
|
|
660
|
+
_resubscribable;
|
|
661
|
+
_resetOnTeardown;
|
|
662
|
+
_onResubscribe;
|
|
638
663
|
_onMessage;
|
|
639
|
-
/** @internal
|
|
664
|
+
/** @internal Read by `describeNode` for `accessHintForGuard`. */
|
|
640
665
|
_guard;
|
|
666
|
+
/** @internal Subclasses update this through {@link _recordMutation}. */
|
|
641
667
|
_lastMutation;
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
// ---
|
|
668
|
+
// --- Versioning ---
|
|
669
|
+
_hashFn;
|
|
670
|
+
_versioning;
|
|
671
|
+
// --- Lifecycle state ---
|
|
672
|
+
/** @internal Read by `describeNode` and `graph.ts`. */
|
|
646
673
|
_cached;
|
|
674
|
+
/** @internal Read externally via `get status()`. */
|
|
647
675
|
_status;
|
|
648
676
|
_terminal = false;
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
_manualEmitUsed = false;
|
|
677
|
+
_active = false;
|
|
678
|
+
// --- Sink storage ---
|
|
679
|
+
/** @internal Read by `graph/profile.ts` for subscriber counts. */
|
|
653
680
|
_sinkCount = 0;
|
|
654
681
|
_singleDepSinkCount = 0;
|
|
655
|
-
// --- Object/collection state ---
|
|
656
|
-
_depDirtyMask;
|
|
657
|
-
_depSettledMask;
|
|
658
|
-
_depCompleteMask;
|
|
659
|
-
_allDepsCompleteMask;
|
|
660
|
-
_lastDepValues;
|
|
661
|
-
_cleanup;
|
|
662
|
-
_sinks = null;
|
|
663
682
|
_singleDepSinks = /* @__PURE__ */ new WeakSet();
|
|
664
|
-
|
|
683
|
+
_sinks = null;
|
|
684
|
+
// --- Actions + bound helpers ---
|
|
665
685
|
_actions;
|
|
666
686
|
_boundDownToSinks;
|
|
687
|
+
// --- Inspector hook (Graph observability) ---
|
|
667
688
|
_inspectorHook;
|
|
668
|
-
|
|
669
|
-
_hashFn;
|
|
670
|
-
constructor(deps, fn, opts) {
|
|
671
|
-
this._deps = deps;
|
|
672
|
-
this._fn = fn;
|
|
673
|
-
this._opts = opts;
|
|
689
|
+
constructor(opts) {
|
|
674
690
|
this._optsName = opts.name;
|
|
675
691
|
this._describeKind = opts.describeKind;
|
|
676
692
|
this._equals = opts.equals ?? Object.is;
|
|
693
|
+
this._resubscribable = opts.resubscribable ?? false;
|
|
694
|
+
this._resetOnTeardown = opts.resetOnTeardown ?? false;
|
|
695
|
+
this._onResubscribe = opts.onResubscribe;
|
|
677
696
|
this._onMessage = opts.onMessage;
|
|
678
697
|
this._guard = opts.guard;
|
|
679
|
-
this._hasDeps = deps.length > 0;
|
|
680
|
-
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
681
|
-
this._isSingleDep = deps.length === 1 && fn != null;
|
|
682
698
|
this._cached = "initial" in opts ? opts.initial : NO_VALUE;
|
|
683
|
-
this._status =
|
|
699
|
+
this._status = "disconnected";
|
|
684
700
|
this._hashFn = opts.versioningHash ?? defaultHash;
|
|
685
701
|
this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
|
|
686
702
|
id: opts.versioningId,
|
|
687
703
|
hash: this._hashFn
|
|
688
704
|
}) : void 0;
|
|
689
|
-
this._depDirtyMask = createBitSet(deps.length);
|
|
690
|
-
this._depSettledMask = createBitSet(deps.length);
|
|
691
|
-
this._depCompleteMask = createBitSet(deps.length);
|
|
692
|
-
this._allDepsCompleteMask = createBitSet(deps.length);
|
|
693
|
-
for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
|
|
694
705
|
const meta = {};
|
|
695
706
|
for (const [k, v] of Object.entries(opts.meta ?? {})) {
|
|
696
|
-
meta[k] =
|
|
697
|
-
initial: v,
|
|
698
|
-
name: `${opts.name ?? "node"}:meta:${k}`,
|
|
699
|
-
describeKind: "state",
|
|
700
|
-
...opts.guard != null ? { guard: opts.guard } : {}
|
|
701
|
-
});
|
|
707
|
+
meta[k] = this._createMetaNode(k, v, opts);
|
|
702
708
|
}
|
|
703
709
|
Object.freeze(meta);
|
|
704
710
|
this.meta = meta;
|
|
705
711
|
const self = this;
|
|
706
712
|
this._actions = {
|
|
707
713
|
down(messages) {
|
|
708
|
-
self.
|
|
714
|
+
self._onManualEmit();
|
|
709
715
|
self._downInternal(messages);
|
|
710
716
|
},
|
|
711
717
|
emit(value) {
|
|
712
|
-
self.
|
|
718
|
+
self._onManualEmit();
|
|
713
719
|
self._downAutoValue(value);
|
|
714
720
|
},
|
|
715
721
|
up(messages) {
|
|
716
722
|
self._upInternal(messages);
|
|
717
723
|
}
|
|
718
724
|
};
|
|
719
|
-
this.down = this.down.bind(this);
|
|
720
|
-
this.up = this.up.bind(this);
|
|
721
725
|
this._boundDownToSinks = this._downToSinks.bind(this);
|
|
722
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
|
|
729
|
+
* {@link NodeImpl} overrides to set `_manualEmitUsed`.
|
|
730
|
+
*/
|
|
731
|
+
_onManualEmit() {
|
|
732
|
+
}
|
|
733
|
+
// --- Identity getters ---
|
|
723
734
|
get name() {
|
|
724
735
|
return this._registryName ?? this._optsName;
|
|
725
736
|
}
|
|
726
|
-
/**
|
|
727
|
-
* When a node is registered with {@link Graph.add} without an options `name`,
|
|
728
|
-
* the graph assigns the registry local name for introspection (parity with graphrefly-py).
|
|
729
|
-
*/
|
|
737
|
+
/** @internal Assigned by `Graph.add` when registered without an options `name`. */
|
|
730
738
|
_assignRegistryName(localName) {
|
|
731
739
|
if (this._optsName !== void 0 || this._registryName !== void 0) return;
|
|
732
740
|
this._registryName = localName;
|
|
@@ -744,7 +752,10 @@ var NodeImpl = class {
|
|
|
744
752
|
}
|
|
745
753
|
};
|
|
746
754
|
}
|
|
747
|
-
|
|
755
|
+
/** @internal Used by subclasses to surface inspector events. */
|
|
756
|
+
_emitInspectorHook(event) {
|
|
757
|
+
this._inspectorHook?.(event);
|
|
758
|
+
}
|
|
748
759
|
get status() {
|
|
749
760
|
return this._status;
|
|
750
761
|
}
|
|
@@ -754,15 +765,7 @@ var NodeImpl = class {
|
|
|
754
765
|
get v() {
|
|
755
766
|
return this._versioning;
|
|
756
767
|
}
|
|
757
|
-
/**
|
|
758
|
-
* Retroactively apply versioning to a node that was created without it.
|
|
759
|
-
* No-op if versioning is already enabled.
|
|
760
|
-
*
|
|
761
|
-
* Version starts at 0 regardless of prior DATA emissions — it tracks
|
|
762
|
-
* changes from the moment versioning is enabled, not historical ones.
|
|
763
|
-
*
|
|
764
|
-
* @internal — used by {@link Graph.setVersioning}.
|
|
765
|
-
*/
|
|
768
|
+
/** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
|
|
766
769
|
_applyVersioning(level, opts) {
|
|
767
770
|
if (this._versioning != null) return;
|
|
768
771
|
this._hashFn = opts?.hash ?? this._hashFn;
|
|
@@ -782,6 +785,7 @@ var NodeImpl = class {
|
|
|
782
785
|
if (this._guard == null) return true;
|
|
783
786
|
return this._guard(normalizeActor(actor), "observe");
|
|
784
787
|
}
|
|
788
|
+
// --- Public transport ---
|
|
785
789
|
get() {
|
|
786
790
|
return this._cached === NO_VALUE ? void 0 : this._cached;
|
|
787
791
|
}
|
|
@@ -794,43 +798,25 @@ var NodeImpl = class {
|
|
|
794
798
|
if (!this._guard(actor, action)) {
|
|
795
799
|
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
796
800
|
}
|
|
797
|
-
this.
|
|
801
|
+
this._recordMutation(actor);
|
|
798
802
|
}
|
|
799
803
|
this._downInternal(messages);
|
|
800
804
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
let sinkMessages = messages;
|
|
805
|
-
if (this._terminal && !this._opts.resubscribable) {
|
|
806
|
-
const terminalPassthrough = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
807
|
-
if (terminalPassthrough.length === 0) return;
|
|
808
|
-
lifecycleMessages = terminalPassthrough;
|
|
809
|
-
sinkMessages = terminalPassthrough;
|
|
810
|
-
}
|
|
811
|
-
this._handleLocalLifecycle(lifecycleMessages);
|
|
812
|
-
if (this._canSkipDirty()) {
|
|
813
|
-
let hasPhase2 = false;
|
|
814
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
815
|
-
const t = sinkMessages[i][0];
|
|
816
|
-
if (t === DATA || t === RESOLVED) {
|
|
817
|
-
hasPhase2 = true;
|
|
818
|
-
break;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
if (hasPhase2) {
|
|
822
|
-
const filtered = [];
|
|
823
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
824
|
-
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
825
|
-
}
|
|
826
|
-
if (filtered.length > 0) {
|
|
827
|
-
downWithBatch(this._boundDownToSinks, filtered);
|
|
828
|
-
}
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
805
|
+
/** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
|
|
806
|
+
_recordMutation(actor) {
|
|
807
|
+
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
833
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* At-most-once deactivation guard. Both TEARDOWN (eager) and
|
|
811
|
+
* unsubscribe-body (lazy) call this. The `_active` flag ensures
|
|
812
|
+
* `_doDeactivate` runs exactly once per activation cycle.
|
|
813
|
+
*/
|
|
814
|
+
_onDeactivate() {
|
|
815
|
+
if (!this._active) return;
|
|
816
|
+
this._active = false;
|
|
817
|
+
this._doDeactivate();
|
|
818
|
+
}
|
|
819
|
+
// --- Subscribe (uniform across node shapes) ---
|
|
834
820
|
subscribe(sink, hints) {
|
|
835
821
|
if (hints?.actor != null && this._guard != null) {
|
|
836
822
|
const actor = normalizeActor(hints.actor);
|
|
@@ -838,17 +824,21 @@ var NodeImpl = class {
|
|
|
838
824
|
throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
|
|
839
825
|
}
|
|
840
826
|
}
|
|
841
|
-
if (this._terminal && this.
|
|
827
|
+
if (this._terminal && this._resubscribable) {
|
|
842
828
|
this._terminal = false;
|
|
843
829
|
this._cached = NO_VALUE;
|
|
844
|
-
this._status =
|
|
845
|
-
this.
|
|
830
|
+
this._status = "disconnected";
|
|
831
|
+
this._onResubscribe?.();
|
|
846
832
|
}
|
|
847
833
|
this._sinkCount += 1;
|
|
848
834
|
if (hints?.singleDep) {
|
|
849
835
|
this._singleDepSinkCount += 1;
|
|
850
836
|
this._singleDepSinks.add(sink);
|
|
851
837
|
}
|
|
838
|
+
if (!this._terminal) {
|
|
839
|
+
const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
|
|
840
|
+
downWithBatch(sink, startMessages);
|
|
841
|
+
}
|
|
852
842
|
if (this._sinks == null) {
|
|
853
843
|
this._sinks = sink;
|
|
854
844
|
} else if (typeof this._sinks === "function") {
|
|
@@ -856,10 +846,12 @@ var NodeImpl = class {
|
|
|
856
846
|
} else {
|
|
857
847
|
this._sinks.add(sink);
|
|
858
848
|
}
|
|
859
|
-
if (this.
|
|
860
|
-
this.
|
|
861
|
-
|
|
862
|
-
|
|
849
|
+
if (this._sinkCount === 1 && !this._terminal) {
|
|
850
|
+
this._active = true;
|
|
851
|
+
this._onActivate();
|
|
852
|
+
}
|
|
853
|
+
if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
|
|
854
|
+
this._status = "pending";
|
|
863
855
|
}
|
|
864
856
|
let removed = false;
|
|
865
857
|
return () => {
|
|
@@ -883,39 +875,49 @@ var NodeImpl = class {
|
|
|
883
875
|
}
|
|
884
876
|
}
|
|
885
877
|
if (this._sinks == null) {
|
|
886
|
-
this.
|
|
887
|
-
this._stopProducer();
|
|
878
|
+
this._onDeactivate();
|
|
888
879
|
}
|
|
889
880
|
};
|
|
890
881
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
882
|
+
// --- Down pipeline ---
|
|
883
|
+
/**
|
|
884
|
+
* Core outgoing dispatch. Applies terminal filter + local lifecycle
|
|
885
|
+
* update, then hands messages to `downWithBatch` for tier-aware delivery.
|
|
886
|
+
*/
|
|
887
|
+
_downInternal(messages) {
|
|
888
|
+
if (messages.length === 0) return;
|
|
889
|
+
let sinkMessages = messages;
|
|
890
|
+
if (this._terminal && !this._resubscribable) {
|
|
891
|
+
const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
892
|
+
if (pass.length === 0) return;
|
|
893
|
+
sinkMessages = pass;
|
|
899
894
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
895
|
+
this._handleLocalLifecycle(sinkMessages);
|
|
896
|
+
if (this._canSkipDirty()) {
|
|
897
|
+
let hasPhase2 = false;
|
|
898
|
+
for (let i = 0; i < sinkMessages.length; i++) {
|
|
899
|
+
const t = sinkMessages[i][0];
|
|
900
|
+
if (t === DATA || t === RESOLVED) {
|
|
901
|
+
hasPhase2 = true;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (hasPhase2) {
|
|
906
|
+
const filtered = [];
|
|
907
|
+
for (let i = 0; i < sinkMessages.length; i++) {
|
|
908
|
+
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
909
|
+
}
|
|
910
|
+
if (filtered.length > 0) {
|
|
911
|
+
downWithBatch(this._boundDownToSinks, filtered);
|
|
912
|
+
}
|
|
913
|
+
return;
|
|
905
914
|
}
|
|
906
915
|
}
|
|
916
|
+
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
907
917
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
for (const dep of this._deps) {
|
|
911
|
-
dep.up?.(messages, { internal: true });
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
unsubscribe() {
|
|
915
|
-
if (!this._hasDeps) return;
|
|
916
|
-
this._disconnectUpstream();
|
|
918
|
+
_canSkipDirty() {
|
|
919
|
+
return this._sinkCount === 1 && this._singleDepSinkCount === 1;
|
|
917
920
|
}
|
|
918
|
-
// --- Private methods (prototype, _ prefix) ---
|
|
919
921
|
_downToSinks(messages) {
|
|
920
922
|
if (this._sinks == null) return;
|
|
921
923
|
if (typeof this._sinks === "function") {
|
|
@@ -927,6 +929,11 @@ var NodeImpl = class {
|
|
|
927
929
|
sink(messages);
|
|
928
930
|
}
|
|
929
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Update `_cached`, `_status`, `_terminal` from message batch before
|
|
934
|
+
* delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
|
|
935
|
+
* {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
|
|
936
|
+
*/
|
|
930
937
|
_handleLocalLifecycle(messages) {
|
|
931
938
|
for (const m of messages) {
|
|
932
939
|
const t = m[0];
|
|
@@ -940,28 +947,22 @@ var NodeImpl = class {
|
|
|
940
947
|
}
|
|
941
948
|
}
|
|
942
949
|
if (t === INVALIDATE) {
|
|
943
|
-
|
|
944
|
-
this._cleanup = void 0;
|
|
945
|
-
cleanupFn?.();
|
|
950
|
+
this._onInvalidate();
|
|
946
951
|
this._cached = NO_VALUE;
|
|
947
|
-
this._lastDepValues = void 0;
|
|
948
952
|
}
|
|
949
953
|
this._status = statusAfterMessage(this._status, m);
|
|
950
954
|
if (t === COMPLETE || t === ERROR) {
|
|
951
955
|
this._terminal = true;
|
|
952
956
|
}
|
|
953
957
|
if (t === TEARDOWN) {
|
|
954
|
-
if (this.
|
|
958
|
+
if (this._resetOnTeardown) {
|
|
955
959
|
this._cached = NO_VALUE;
|
|
956
960
|
}
|
|
957
|
-
|
|
958
|
-
this._cleanup = void 0;
|
|
959
|
-
teardownCleanup?.();
|
|
961
|
+
this._onTeardown();
|
|
960
962
|
try {
|
|
961
963
|
this._propagateToMeta(t);
|
|
962
964
|
} finally {
|
|
963
|
-
this.
|
|
964
|
-
this._stopProducer();
|
|
965
|
+
this._onDeactivate();
|
|
965
966
|
}
|
|
966
967
|
}
|
|
967
968
|
if (t !== TEARDOWN && propagatesToMeta(t)) {
|
|
@@ -969,7 +970,20 @@ var NodeImpl = class {
|
|
|
969
970
|
}
|
|
970
971
|
}
|
|
971
972
|
}
|
|
972
|
-
/**
|
|
973
|
+
/**
|
|
974
|
+
* Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
|
|
975
|
+
* cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
|
|
976
|
+
* drop `_lastDepValues` so the next wave re-runs fn.
|
|
977
|
+
*/
|
|
978
|
+
_onInvalidate() {
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
|
|
982
|
+
* {@link NodeImpl} uses this to run any pending cleanup fn.
|
|
983
|
+
*/
|
|
984
|
+
_onTeardown() {
|
|
985
|
+
}
|
|
986
|
+
/** Forward a signal to all companion meta nodes (best-effort). */
|
|
973
987
|
_propagateToMeta(t) {
|
|
974
988
|
for (const metaNode of Object.values(this.meta)) {
|
|
975
989
|
try {
|
|
@@ -978,9 +992,10 @@ var NodeImpl = class {
|
|
|
978
992
|
}
|
|
979
993
|
}
|
|
980
994
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
995
|
+
/**
|
|
996
|
+
* Frame a computed value into the right protocol messages and dispatch
|
|
997
|
+
* via `_downInternal`. Used by `_runFn` and `actions.emit`.
|
|
998
|
+
*/
|
|
984
999
|
_downAutoValue(value) {
|
|
985
1000
|
const wasDirty = this._status === "dirty";
|
|
986
1001
|
let unchanged;
|
|
@@ -988,7 +1003,9 @@ var NodeImpl = class {
|
|
|
988
1003
|
unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
|
|
989
1004
|
} catch (eqErr) {
|
|
990
1005
|
const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
|
|
991
|
-
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
|
|
1006
|
+
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
|
|
1007
|
+
cause: eqErr
|
|
1008
|
+
});
|
|
992
1009
|
this._downInternal([[ERROR, wrapped]]);
|
|
993
1010
|
return;
|
|
994
1011
|
}
|
|
@@ -998,89 +1015,173 @@ var NodeImpl = class {
|
|
|
998
1015
|
}
|
|
999
1016
|
this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
|
|
1000
1017
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
if ("value" in out) {
|
|
1036
|
-
this._downAutoValue(out.value);
|
|
1037
|
-
}
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
if (isCleanupFn(out)) {
|
|
1041
|
-
this._cleanup = out;
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
if (this._manualEmitUsed) return;
|
|
1045
|
-
if (out === void 0) return;
|
|
1046
|
-
this._downAutoValue(out);
|
|
1047
|
-
} catch (err) {
|
|
1048
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1049
|
-
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
|
|
1050
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
// src/core/node.ts
|
|
1021
|
+
var NodeImpl = class extends NodeBase {
|
|
1022
|
+
// --- Dep configuration (set once) ---
|
|
1023
|
+
_deps;
|
|
1024
|
+
_fn;
|
|
1025
|
+
_opts;
|
|
1026
|
+
_hasDeps;
|
|
1027
|
+
_isSingleDep;
|
|
1028
|
+
_autoComplete;
|
|
1029
|
+
// --- Wave tracking masks ---
|
|
1030
|
+
_depDirtyMask;
|
|
1031
|
+
_depSettledMask;
|
|
1032
|
+
_depCompleteMask;
|
|
1033
|
+
_allDepsCompleteMask;
|
|
1034
|
+
// --- Identity-skip optimization ---
|
|
1035
|
+
_lastDepValues;
|
|
1036
|
+
_cleanup;
|
|
1037
|
+
// --- Upstream bookkeeping ---
|
|
1038
|
+
_upstreamUnsubs = [];
|
|
1039
|
+
// --- Fn behavior flag ---
|
|
1040
|
+
/** @internal Read by `describeNode` to infer `"operator"` label. */
|
|
1041
|
+
_manualEmitUsed = false;
|
|
1042
|
+
constructor(deps, fn, opts) {
|
|
1043
|
+
super(opts);
|
|
1044
|
+
this._deps = deps;
|
|
1045
|
+
this._fn = fn;
|
|
1046
|
+
this._opts = opts;
|
|
1047
|
+
this._hasDeps = deps.length > 0;
|
|
1048
|
+
this._isSingleDep = deps.length === 1 && fn != null;
|
|
1049
|
+
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1050
|
+
if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
|
|
1051
|
+
this._status = "settled";
|
|
1051
1052
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
this.
|
|
1056
|
-
this.
|
|
1057
|
-
|
|
1058
|
-
|
|
1053
|
+
this._depDirtyMask = createBitSet(deps.length);
|
|
1054
|
+
this._depSettledMask = createBitSet(deps.length);
|
|
1055
|
+
this._depCompleteMask = createBitSet(deps.length);
|
|
1056
|
+
this._allDepsCompleteMask = createBitSet(deps.length);
|
|
1057
|
+
for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
|
|
1058
|
+
this.down = this.down.bind(this);
|
|
1059
|
+
this.up = this.up.bind(this);
|
|
1060
|
+
}
|
|
1061
|
+
// --- Meta node factory (called from base constructor) ---
|
|
1062
|
+
_createMetaNode(key, initialValue, opts) {
|
|
1063
|
+
return node({
|
|
1064
|
+
initial: initialValue,
|
|
1065
|
+
name: `${opts.name ?? "node"}:meta:${key}`,
|
|
1066
|
+
describeKind: "state",
|
|
1067
|
+
...opts.guard != null ? { guard: opts.guard } : {}
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
// --- Manual emit tracker (set by actions.down / actions.emit) ---
|
|
1071
|
+
_onManualEmit() {
|
|
1072
|
+
this._manualEmitUsed = true;
|
|
1073
|
+
}
|
|
1074
|
+
// --- Up / unsubscribe ---
|
|
1075
|
+
up(messages, options) {
|
|
1076
|
+
if (!this._hasDeps) return;
|
|
1077
|
+
if (!options?.internal && this._guard != null) {
|
|
1078
|
+
const actor = normalizeActor(options?.actor);
|
|
1079
|
+
if (!this._guard(actor, "write")) {
|
|
1080
|
+
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
1081
|
+
}
|
|
1082
|
+
this._recordMutation(actor);
|
|
1083
|
+
}
|
|
1084
|
+
for (const dep of this._deps) {
|
|
1085
|
+
if (options === void 0) {
|
|
1086
|
+
dep.up?.(messages);
|
|
1087
|
+
} else {
|
|
1088
|
+
dep.up?.(messages, options);
|
|
1089
|
+
}
|
|
1059
1090
|
}
|
|
1060
1091
|
}
|
|
1061
|
-
|
|
1062
|
-
if (!this.
|
|
1063
|
-
|
|
1092
|
+
_upInternal(messages) {
|
|
1093
|
+
if (!this._hasDeps) return;
|
|
1094
|
+
for (const dep of this._deps) {
|
|
1095
|
+
dep.up?.(messages, { internal: true });
|
|
1064
1096
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1097
|
+
}
|
|
1098
|
+
unsubscribe() {
|
|
1099
|
+
if (!this._hasDeps) return;
|
|
1100
|
+
this._disconnectUpstream();
|
|
1101
|
+
}
|
|
1102
|
+
// --- Activation (first-subscriber / last-subscriber hooks) ---
|
|
1103
|
+
_onActivate() {
|
|
1104
|
+
if (this._hasDeps) {
|
|
1105
|
+
this._connectUpstream();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (this._fn) {
|
|
1069
1109
|
this._runFn();
|
|
1110
|
+
return;
|
|
1070
1111
|
}
|
|
1071
1112
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1113
|
+
_doDeactivate() {
|
|
1114
|
+
this._disconnectUpstream();
|
|
1115
|
+
const cleanup = this._cleanup;
|
|
1116
|
+
this._cleanup = void 0;
|
|
1117
|
+
cleanup?.();
|
|
1118
|
+
if (this._fn != null) {
|
|
1119
|
+
this._cached = NO_VALUE;
|
|
1120
|
+
this._lastDepValues = void 0;
|
|
1121
|
+
}
|
|
1122
|
+
if (this._hasDeps || this._fn != null) {
|
|
1123
|
+
this._status = "disconnected";
|
|
1075
1124
|
}
|
|
1076
1125
|
}
|
|
1126
|
+
// --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
|
|
1127
|
+
_onInvalidate() {
|
|
1128
|
+
const cleanup = this._cleanup;
|
|
1129
|
+
this._cleanup = void 0;
|
|
1130
|
+
cleanup?.();
|
|
1131
|
+
this._lastDepValues = void 0;
|
|
1132
|
+
}
|
|
1133
|
+
_onTeardown() {
|
|
1134
|
+
const cleanup = this._cleanup;
|
|
1135
|
+
this._cleanup = void 0;
|
|
1136
|
+
cleanup?.();
|
|
1137
|
+
}
|
|
1138
|
+
// --- Upstream connect / disconnect ---
|
|
1139
|
+
_connectUpstream() {
|
|
1140
|
+
if (!this._hasDeps) return;
|
|
1141
|
+
if (this._upstreamUnsubs.length > 0) return;
|
|
1142
|
+
this._depDirtyMask.setAll();
|
|
1143
|
+
this._depSettledMask.reset();
|
|
1144
|
+
this._depCompleteMask.reset();
|
|
1145
|
+
const depValuesBefore = this._lastDepValues;
|
|
1146
|
+
const subHints = this._isSingleDep ? { singleDep: true } : void 0;
|
|
1147
|
+
for (let i = 0; i < this._deps.length; i += 1) {
|
|
1148
|
+
const dep = this._deps[i];
|
|
1149
|
+
this._upstreamUnsubs.push(
|
|
1150
|
+
dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
|
|
1154
|
+
this._runFn();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
_disconnectUpstream() {
|
|
1158
|
+
if (this._upstreamUnsubs.length === 0) return;
|
|
1159
|
+
for (const unsub of this._upstreamUnsubs.splice(0)) {
|
|
1160
|
+
unsub();
|
|
1161
|
+
}
|
|
1162
|
+
this._depDirtyMask.reset();
|
|
1163
|
+
this._depSettledMask.reset();
|
|
1164
|
+
this._depCompleteMask.reset();
|
|
1165
|
+
}
|
|
1166
|
+
// --- Wave handling ---
|
|
1077
1167
|
_handleDepMessages(index, messages) {
|
|
1078
1168
|
for (const msg of messages) {
|
|
1079
|
-
this.
|
|
1169
|
+
this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
|
|
1080
1170
|
const t = msg[0];
|
|
1081
1171
|
if (this._onMessage) {
|
|
1082
1172
|
try {
|
|
1083
|
-
|
|
1173
|
+
const consumed = this._onMessage(msg, index, this._actions);
|
|
1174
|
+
if (consumed) {
|
|
1175
|
+
if (t === START) {
|
|
1176
|
+
this._depDirtyMask.clear(index);
|
|
1177
|
+
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
1178
|
+
this._depDirtyMask.reset();
|
|
1179
|
+
this._depSettledMask.reset();
|
|
1180
|
+
this._runFn();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1084
1185
|
} catch (err) {
|
|
1085
1186
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1086
1187
|
const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
|
|
@@ -1090,6 +1191,7 @@ var NodeImpl = class {
|
|
|
1090
1191
|
return;
|
|
1091
1192
|
}
|
|
1092
1193
|
}
|
|
1194
|
+
if (messageTier(t) < 1) continue;
|
|
1093
1195
|
if (!this._fn) {
|
|
1094
1196
|
if (t === COMPLETE && this._deps.length > 1) {
|
|
1095
1197
|
this._depCompleteMask.set(index);
|
|
@@ -1133,297 +1235,146 @@ var NodeImpl = class {
|
|
|
1133
1235
|
this._downInternal([msg]);
|
|
1134
1236
|
}
|
|
1135
1237
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
this.
|
|
1139
|
-
this.
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
this._status = "settled";
|
|
1143
|
-
const subHints = this._isSingleDep ? { singleDep: true } : void 0;
|
|
1144
|
-
this._connecting = true;
|
|
1145
|
-
try {
|
|
1146
|
-
for (let i = 0; i < this._deps.length; i += 1) {
|
|
1147
|
-
const dep = this._deps[i];
|
|
1148
|
-
this._upstreamUnsubs.push(
|
|
1149
|
-
dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
|
|
1150
|
-
);
|
|
1151
|
-
}
|
|
1152
|
-
} finally {
|
|
1153
|
-
this._connecting = false;
|
|
1154
|
-
}
|
|
1155
|
-
if (this._fn) {
|
|
1156
|
-
this._runFn();
|
|
1238
|
+
_onDepDirty(index) {
|
|
1239
|
+
const wasDirty = this._depDirtyMask.has(index);
|
|
1240
|
+
this._depDirtyMask.set(index);
|
|
1241
|
+
this._depSettledMask.clear(index);
|
|
1242
|
+
if (!wasDirty) {
|
|
1243
|
+
this._downInternal([[DIRTY]]);
|
|
1157
1244
|
}
|
|
1158
1245
|
}
|
|
1159
|
-
|
|
1160
|
-
if (!this.
|
|
1161
|
-
|
|
1162
|
-
const producerCleanup = this._cleanup;
|
|
1163
|
-
this._cleanup = void 0;
|
|
1164
|
-
producerCleanup?.();
|
|
1165
|
-
}
|
|
1166
|
-
_startProducer() {
|
|
1167
|
-
if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
|
|
1168
|
-
this._producerStarted = true;
|
|
1169
|
-
this._runFn();
|
|
1170
|
-
}
|
|
1171
|
-
_disconnectUpstream() {
|
|
1172
|
-
if (!this._connected) return;
|
|
1173
|
-
for (const unsub of this._upstreamUnsubs.splice(0)) {
|
|
1174
|
-
unsub();
|
|
1246
|
+
_onDepSettled(index) {
|
|
1247
|
+
if (!this._depDirtyMask.has(index)) {
|
|
1248
|
+
this._onDepDirty(index);
|
|
1175
1249
|
}
|
|
1176
|
-
this.
|
|
1177
|
-
this._depDirtyMask.
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1182
|
-
};
|
|
1183
|
-
function node(depsOrFn, fnOrOpts, optsArg) {
|
|
1184
|
-
const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
|
|
1185
|
-
const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
|
|
1186
|
-
let opts = {};
|
|
1187
|
-
if (isNodeArray(depsOrFn)) {
|
|
1188
|
-
opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
|
|
1189
|
-
} else if (isNodeOptions(depsOrFn)) {
|
|
1190
|
-
opts = depsOrFn;
|
|
1191
|
-
} else {
|
|
1192
|
-
opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
|
|
1193
|
-
}
|
|
1194
|
-
return new NodeImpl(deps, fn, opts);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// src/core/sugar.ts
|
|
1198
|
-
function state(initial, opts) {
|
|
1199
|
-
return node([], { ...opts, initial });
|
|
1200
|
-
}
|
|
1201
|
-
function derived(deps, fn, opts) {
|
|
1202
|
-
return node(deps, fn, { describeKind: "derived", ...opts });
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// src/core/dynamic-node.ts
|
|
1206
|
-
var DynamicNodeImpl = class {
|
|
1207
|
-
_optsName;
|
|
1208
|
-
_registryName;
|
|
1209
|
-
_describeKind;
|
|
1210
|
-
meta;
|
|
1211
|
-
_fn;
|
|
1212
|
-
_equals;
|
|
1213
|
-
_resubscribable;
|
|
1214
|
-
_resetOnTeardown;
|
|
1215
|
-
_autoComplete;
|
|
1216
|
-
_onMessage;
|
|
1217
|
-
_onResubscribe;
|
|
1218
|
-
/** @internal — read by {@link describeNode} for `accessHintForGuard`. */
|
|
1219
|
-
_guard;
|
|
1220
|
-
_lastMutation;
|
|
1221
|
-
_inspectorHook;
|
|
1222
|
-
// Sink tracking
|
|
1223
|
-
_sinkCount = 0;
|
|
1224
|
-
_singleDepSinkCount = 0;
|
|
1225
|
-
_singleDepSinks = /* @__PURE__ */ new WeakSet();
|
|
1226
|
-
// Actions object (for onMessage handler)
|
|
1227
|
-
_actions;
|
|
1228
|
-
_boundDownToSinks;
|
|
1229
|
-
// Mutable state
|
|
1230
|
-
_cached = NO_VALUE;
|
|
1231
|
-
_status = "disconnected";
|
|
1232
|
-
_terminal = false;
|
|
1233
|
-
_connected = false;
|
|
1234
|
-
_rewiring = false;
|
|
1235
|
-
// re-entrancy guard
|
|
1236
|
-
// Dynamic deps tracking
|
|
1237
|
-
_deps = [];
|
|
1238
|
-
_depUnsubs = [];
|
|
1239
|
-
_depIndexMap = /* @__PURE__ */ new Map();
|
|
1240
|
-
// node → index in _deps
|
|
1241
|
-
_dirtyBits = /* @__PURE__ */ new Set();
|
|
1242
|
-
_settledBits = /* @__PURE__ */ new Set();
|
|
1243
|
-
_completeBits = /* @__PURE__ */ new Set();
|
|
1244
|
-
// Sinks
|
|
1245
|
-
_sinks = null;
|
|
1246
|
-
constructor(fn, opts) {
|
|
1247
|
-
this._fn = fn;
|
|
1248
|
-
this._optsName = opts.name;
|
|
1249
|
-
this._describeKind = opts.describeKind;
|
|
1250
|
-
this._equals = opts.equals ?? Object.is;
|
|
1251
|
-
this._resubscribable = opts.resubscribable ?? false;
|
|
1252
|
-
this._resetOnTeardown = opts.resetOnTeardown ?? false;
|
|
1253
|
-
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1254
|
-
this._onMessage = opts.onMessage;
|
|
1255
|
-
this._onResubscribe = opts.onResubscribe;
|
|
1256
|
-
this._guard = opts.guard;
|
|
1257
|
-
this._inspectorHook = void 0;
|
|
1258
|
-
const meta = {};
|
|
1259
|
-
for (const [k, v] of Object.entries(opts.meta ?? {})) {
|
|
1260
|
-
meta[k] = node({
|
|
1261
|
-
initial: v,
|
|
1262
|
-
name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
|
|
1263
|
-
describeKind: "state",
|
|
1264
|
-
...opts.guard != null ? { guard: opts.guard } : {}
|
|
1265
|
-
});
|
|
1250
|
+
this._depSettledMask.set(index);
|
|
1251
|
+
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
1252
|
+
this._depDirtyMask.reset();
|
|
1253
|
+
this._depSettledMask.reset();
|
|
1254
|
+
this._runFn();
|
|
1266
1255
|
}
|
|
1267
|
-
Object.freeze(meta);
|
|
1268
|
-
this.meta = meta;
|
|
1269
|
-
const self = this;
|
|
1270
|
-
this._actions = {
|
|
1271
|
-
down(messages) {
|
|
1272
|
-
self._downInternal(messages);
|
|
1273
|
-
},
|
|
1274
|
-
emit(value) {
|
|
1275
|
-
self._downAutoValue(value);
|
|
1276
|
-
},
|
|
1277
|
-
up(messages) {
|
|
1278
|
-
for (const dep of self._deps) {
|
|
1279
|
-
dep.up?.(messages, { internal: true });
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
};
|
|
1283
|
-
this._boundDownToSinks = this._downToSinks.bind(this);
|
|
1284
|
-
}
|
|
1285
|
-
get name() {
|
|
1286
|
-
return this._registryName ?? this._optsName;
|
|
1287
|
-
}
|
|
1288
|
-
/** @internal */
|
|
1289
|
-
_assignRegistryName(localName) {
|
|
1290
|
-
if (this._optsName !== void 0 || this._registryName !== void 0) return;
|
|
1291
|
-
this._registryName = localName;
|
|
1292
|
-
}
|
|
1293
|
-
/**
|
|
1294
|
-
* @internal Attach/remove inspector hook for graph-level observability.
|
|
1295
|
-
* Returns a disposer that restores the previous hook.
|
|
1296
|
-
*/
|
|
1297
|
-
_setInspectorHook(hook) {
|
|
1298
|
-
const prev = this._inspectorHook;
|
|
1299
|
-
this._inspectorHook = hook;
|
|
1300
|
-
return () => {
|
|
1301
|
-
if (this._inspectorHook === hook) {
|
|
1302
|
-
this._inspectorHook = prev;
|
|
1303
|
-
}
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
get status() {
|
|
1307
|
-
return this._status;
|
|
1308
|
-
}
|
|
1309
|
-
get lastMutation() {
|
|
1310
|
-
return this._lastMutation;
|
|
1311
1256
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
hasGuard() {
|
|
1317
|
-
return this._guard != null;
|
|
1318
|
-
}
|
|
1319
|
-
allowsObserve(actor) {
|
|
1320
|
-
if (this._guard == null) return true;
|
|
1321
|
-
return this._guard(normalizeActor(actor), "observe");
|
|
1322
|
-
}
|
|
1323
|
-
get() {
|
|
1324
|
-
return this._cached === NO_VALUE ? void 0 : this._cached;
|
|
1325
|
-
}
|
|
1326
|
-
down(messages, options) {
|
|
1327
|
-
if (messages.length === 0) return;
|
|
1328
|
-
if (!options?.internal && this._guard != null) {
|
|
1329
|
-
const actor = normalizeActor(options?.actor);
|
|
1330
|
-
const delivery = options?.delivery ?? "write";
|
|
1331
|
-
const action = delivery === "signal" ? "signal" : "write";
|
|
1332
|
-
if (!this._guard(actor, action)) {
|
|
1333
|
-
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
1334
|
-
}
|
|
1335
|
-
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
1257
|
+
_maybeCompleteFromDeps() {
|
|
1258
|
+
if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
|
|
1259
|
+
this._downInternal([[COMPLETE]]);
|
|
1336
1260
|
}
|
|
1337
|
-
this._downInternal(messages);
|
|
1338
1261
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
if (this._terminal && !this._resubscribable)
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1262
|
+
// --- Fn execution ---
|
|
1263
|
+
_runFn() {
|
|
1264
|
+
if (!this._fn) return;
|
|
1265
|
+
if (this._terminal && !this._resubscribable) return;
|
|
1266
|
+
try {
|
|
1267
|
+
const n = this._deps.length;
|
|
1268
|
+
const depValues = new Array(n);
|
|
1269
|
+
for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
|
|
1270
|
+
const prev = this._lastDepValues;
|
|
1271
|
+
if (n > 0 && prev != null && prev.length === n) {
|
|
1272
|
+
let allSame = true;
|
|
1273
|
+
for (let i = 0; i < n; i++) {
|
|
1274
|
+
if (!Object.is(depValues[i], prev[i])) {
|
|
1275
|
+
allSame = false;
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1355
1278
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1279
|
+
if (allSame) {
|
|
1280
|
+
if (this._status === "dirty") {
|
|
1281
|
+
this._downInternal([[RESOLVED]]);
|
|
1282
|
+
}
|
|
1283
|
+
return;
|
|
1361
1284
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1285
|
+
}
|
|
1286
|
+
const prevCleanup = this._cleanup;
|
|
1287
|
+
this._cleanup = void 0;
|
|
1288
|
+
prevCleanup?.();
|
|
1289
|
+
this._manualEmitUsed = false;
|
|
1290
|
+
this._lastDepValues = depValues;
|
|
1291
|
+
this._emitInspectorHook({ kind: "run", depValues });
|
|
1292
|
+
const out = this._fn(depValues, this._actions);
|
|
1293
|
+
if (isCleanupResult(out)) {
|
|
1294
|
+
this._cleanup = out.cleanup;
|
|
1295
|
+
if (this._manualEmitUsed) return;
|
|
1296
|
+
if ("value" in out) {
|
|
1297
|
+
this._downAutoValue(out.value);
|
|
1364
1298
|
}
|
|
1365
1299
|
return;
|
|
1366
1300
|
}
|
|
1301
|
+
if (isCleanupFn(out)) {
|
|
1302
|
+
this._cleanup = out;
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (this._manualEmitUsed) return;
|
|
1306
|
+
if (out === void 0) return;
|
|
1307
|
+
this._downAutoValue(out);
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1310
|
+
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
|
|
1311
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
1367
1312
|
}
|
|
1368
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
1369
1313
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1314
|
+
};
|
|
1315
|
+
var isNodeArray = (value) => Array.isArray(value);
|
|
1316
|
+
var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
|
|
1317
|
+
function node(depsOrFn, fnOrOpts, optsArg) {
|
|
1318
|
+
const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
|
|
1319
|
+
const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
|
|
1320
|
+
let opts = {};
|
|
1321
|
+
if (isNodeArray(depsOrFn)) {
|
|
1322
|
+
opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
|
|
1323
|
+
} else if (isNodeOptions(depsOrFn)) {
|
|
1324
|
+
opts = depsOrFn;
|
|
1325
|
+
} else {
|
|
1326
|
+
opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
|
|
1372
1327
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
this._sinks = only;
|
|
1418
|
-
} else if (this._sinks.size === 0) {
|
|
1419
|
-
this._sinks = null;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
if (this._sinks == null) {
|
|
1423
|
-
this._disconnect();
|
|
1424
|
-
}
|
|
1425
|
-
};
|
|
1328
|
+
return new NodeImpl(deps, fn, opts);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/core/sugar.ts
|
|
1332
|
+
function state(initial, opts) {
|
|
1333
|
+
return node([], { ...opts, initial });
|
|
1334
|
+
}
|
|
1335
|
+
function derived(deps, fn, opts) {
|
|
1336
|
+
return node(deps, fn, { describeKind: "derived", ...opts });
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/core/dynamic-node.ts
|
|
1340
|
+
var MAX_RERUN = 16;
|
|
1341
|
+
var DynamicNodeImpl = class extends NodeBase {
|
|
1342
|
+
_fn;
|
|
1343
|
+
_autoComplete;
|
|
1344
|
+
// Dynamic deps tracking
|
|
1345
|
+
/** @internal Read by `describeNode`. */
|
|
1346
|
+
_deps = [];
|
|
1347
|
+
_depUnsubs = [];
|
|
1348
|
+
_depIndexMap = /* @__PURE__ */ new Map();
|
|
1349
|
+
_depDirtyBits = /* @__PURE__ */ new Set();
|
|
1350
|
+
_depSettledBits = /* @__PURE__ */ new Set();
|
|
1351
|
+
_depCompleteBits = /* @__PURE__ */ new Set();
|
|
1352
|
+
// Execution state
|
|
1353
|
+
_running = false;
|
|
1354
|
+
_rewiring = false;
|
|
1355
|
+
_bufferedDepMessages = [];
|
|
1356
|
+
_trackedValues = /* @__PURE__ */ new Map();
|
|
1357
|
+
_rerunCount = 0;
|
|
1358
|
+
constructor(fn, opts) {
|
|
1359
|
+
super(opts);
|
|
1360
|
+
this._fn = fn;
|
|
1361
|
+
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1362
|
+
this.down = this.down.bind(this);
|
|
1363
|
+
this.up = this.up.bind(this);
|
|
1364
|
+
}
|
|
1365
|
+
_createMetaNode(key, initialValue, opts) {
|
|
1366
|
+
return node({
|
|
1367
|
+
initial: initialValue,
|
|
1368
|
+
name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
|
|
1369
|
+
describeKind: "state",
|
|
1370
|
+
...opts.guard != null ? { guard: opts.guard } : {}
|
|
1371
|
+
});
|
|
1426
1372
|
}
|
|
1373
|
+
/** Versioning not supported on DynamicNodeImpl (override base). */
|
|
1374
|
+
get v() {
|
|
1375
|
+
return void 0;
|
|
1376
|
+
}
|
|
1377
|
+
// --- Up / unsubscribe ---
|
|
1427
1378
|
up(messages, options) {
|
|
1428
1379
|
if (this._deps.length === 0) return;
|
|
1429
1380
|
if (!options?.internal && this._guard != null) {
|
|
@@ -1431,221 +1382,227 @@ var DynamicNodeImpl = class {
|
|
|
1431
1382
|
if (!this._guard(actor, "write")) {
|
|
1432
1383
|
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
1433
1384
|
}
|
|
1434
|
-
this.
|
|
1385
|
+
this._recordMutation(actor);
|
|
1435
1386
|
}
|
|
1436
1387
|
for (const dep of this._deps) {
|
|
1437
1388
|
dep.up?.(messages, options);
|
|
1438
1389
|
}
|
|
1439
1390
|
}
|
|
1440
|
-
|
|
1441
|
-
this.
|
|
1442
|
-
|
|
1443
|
-
// --- Private methods ---
|
|
1444
|
-
_downToSinks(messages) {
|
|
1445
|
-
if (this._sinks == null) return;
|
|
1446
|
-
if (typeof this._sinks === "function") {
|
|
1447
|
-
this._sinks(messages);
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
const snapshot = [...this._sinks];
|
|
1451
|
-
for (const sink of snapshot) {
|
|
1452
|
-
sink(messages);
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
_handleLocalLifecycle(messages) {
|
|
1456
|
-
for (const m of messages) {
|
|
1457
|
-
const t = m[0];
|
|
1458
|
-
if (t === DATA) this._cached = m[1];
|
|
1459
|
-
if (t === INVALIDATE) {
|
|
1460
|
-
this._cached = NO_VALUE;
|
|
1461
|
-
this._status = "dirty";
|
|
1462
|
-
}
|
|
1463
|
-
if (t === DATA) {
|
|
1464
|
-
this._status = "settled";
|
|
1465
|
-
} else if (t === RESOLVED) {
|
|
1466
|
-
this._status = "resolved";
|
|
1467
|
-
} else if (t === DIRTY) {
|
|
1468
|
-
this._status = "dirty";
|
|
1469
|
-
} else if (t === COMPLETE) {
|
|
1470
|
-
this._status = "completed";
|
|
1471
|
-
this._terminal = true;
|
|
1472
|
-
} else if (t === ERROR) {
|
|
1473
|
-
this._status = "errored";
|
|
1474
|
-
this._terminal = true;
|
|
1475
|
-
}
|
|
1476
|
-
if (t === TEARDOWN) {
|
|
1477
|
-
if (this._resetOnTeardown) this._cached = NO_VALUE;
|
|
1478
|
-
try {
|
|
1479
|
-
this._propagateToMeta(t);
|
|
1480
|
-
} finally {
|
|
1481
|
-
this._disconnect();
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
if (t !== TEARDOWN && propagatesToMeta(t)) {
|
|
1485
|
-
this._propagateToMeta(t);
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
/** Propagate a signal to all companion meta nodes (best-effort). */
|
|
1490
|
-
_propagateToMeta(t) {
|
|
1491
|
-
for (const metaNode of Object.values(this.meta)) {
|
|
1492
|
-
try {
|
|
1493
|
-
metaNode.down([[t]], { internal: true });
|
|
1494
|
-
} catch {
|
|
1495
|
-
}
|
|
1391
|
+
_upInternal(messages) {
|
|
1392
|
+
for (const dep of this._deps) {
|
|
1393
|
+
dep.up?.(messages, { internal: true });
|
|
1496
1394
|
}
|
|
1497
1395
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
let unchanged;
|
|
1501
|
-
try {
|
|
1502
|
-
unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
|
|
1503
|
-
} catch (eqErr) {
|
|
1504
|
-
const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
|
|
1505
|
-
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
|
|
1506
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
if (unchanged) {
|
|
1510
|
-
this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
|
|
1511
|
-
return;
|
|
1512
|
-
}
|
|
1513
|
-
this._cached = value;
|
|
1514
|
-
this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
|
|
1396
|
+
unsubscribe() {
|
|
1397
|
+
this._disconnect();
|
|
1515
1398
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
this._connected = true;
|
|
1519
|
-
this._status = "settled";
|
|
1520
|
-
this._dirtyBits.clear();
|
|
1521
|
-
this._settledBits.clear();
|
|
1522
|
-
this._completeBits.clear();
|
|
1399
|
+
// --- Activation hooks ---
|
|
1400
|
+
_onActivate() {
|
|
1523
1401
|
this._runFn();
|
|
1524
1402
|
}
|
|
1403
|
+
_doDeactivate() {
|
|
1404
|
+
this._disconnect();
|
|
1405
|
+
}
|
|
1525
1406
|
_disconnect() {
|
|
1526
|
-
if (!this._connected) return;
|
|
1527
1407
|
for (const unsub of this._depUnsubs) unsub();
|
|
1528
1408
|
this._depUnsubs = [];
|
|
1529
1409
|
this._deps = [];
|
|
1530
1410
|
this._depIndexMap.clear();
|
|
1531
|
-
this.
|
|
1532
|
-
this.
|
|
1533
|
-
this.
|
|
1534
|
-
this.
|
|
1411
|
+
this._depDirtyBits.clear();
|
|
1412
|
+
this._depSettledBits.clear();
|
|
1413
|
+
this._depCompleteBits.clear();
|
|
1414
|
+
this._cached = NO_VALUE;
|
|
1535
1415
|
this._status = "disconnected";
|
|
1536
1416
|
}
|
|
1417
|
+
// --- Fn execution with rewire buffer ---
|
|
1537
1418
|
_runFn() {
|
|
1538
1419
|
if (this._terminal && !this._resubscribable) return;
|
|
1539
|
-
if (this.
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
if (!trackedSet.has(dep)) {
|
|
1544
|
-
trackedSet.add(dep);
|
|
1545
|
-
trackedDeps.push(dep);
|
|
1546
|
-
}
|
|
1547
|
-
return dep.get();
|
|
1548
|
-
};
|
|
1420
|
+
if (this._running) return;
|
|
1421
|
+
this._running = true;
|
|
1422
|
+
this._rerunCount = 0;
|
|
1423
|
+
let result;
|
|
1549
1424
|
try {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1425
|
+
for (; ; ) {
|
|
1426
|
+
const trackedDeps = [];
|
|
1427
|
+
const trackedValuesMap = /* @__PURE__ */ new Map();
|
|
1428
|
+
const trackedSet = /* @__PURE__ */ new Set();
|
|
1429
|
+
const get = (dep) => {
|
|
1430
|
+
if (!trackedSet.has(dep)) {
|
|
1431
|
+
trackedSet.add(dep);
|
|
1432
|
+
trackedDeps.push(dep);
|
|
1433
|
+
trackedValuesMap.set(dep, dep.get());
|
|
1434
|
+
}
|
|
1435
|
+
return dep.get();
|
|
1436
|
+
};
|
|
1437
|
+
this._trackedValues = trackedValuesMap;
|
|
1438
|
+
const depValues = [];
|
|
1439
|
+
for (const dep of this._deps) depValues.push(dep.get());
|
|
1440
|
+
this._emitInspectorHook({ kind: "run", depValues });
|
|
1441
|
+
try {
|
|
1442
|
+
result = this._fn(get);
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1445
|
+
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
|
|
1446
|
+
cause: err
|
|
1447
|
+
});
|
|
1448
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
this._rewiring = true;
|
|
1452
|
+
this._bufferedDepMessages = [];
|
|
1453
|
+
try {
|
|
1454
|
+
this._rewire(trackedDeps);
|
|
1455
|
+
} finally {
|
|
1456
|
+
this._rewiring = false;
|
|
1457
|
+
}
|
|
1458
|
+
let needsRerun = false;
|
|
1459
|
+
for (const entry of this._bufferedDepMessages) {
|
|
1460
|
+
for (const msg of entry.msgs) {
|
|
1461
|
+
if (msg[0] === DATA) {
|
|
1462
|
+
const dep = this._deps[entry.index];
|
|
1463
|
+
const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
|
|
1464
|
+
const actualValue = msg[1];
|
|
1465
|
+
if (!this._equals(trackedValue, actualValue)) {
|
|
1466
|
+
needsRerun = true;
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (needsRerun) break;
|
|
1472
|
+
}
|
|
1473
|
+
if (needsRerun) {
|
|
1474
|
+
this._rerunCount += 1;
|
|
1475
|
+
if (this._rerunCount > MAX_RERUN) {
|
|
1476
|
+
this._bufferedDepMessages = [];
|
|
1477
|
+
this._downInternal([
|
|
1478
|
+
[
|
|
1479
|
+
ERROR,
|
|
1480
|
+
new Error(
|
|
1481
|
+
`dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
|
|
1482
|
+
)
|
|
1483
|
+
]
|
|
1484
|
+
]);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
this._bufferedDepMessages = [];
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
const drain = this._bufferedDepMessages;
|
|
1491
|
+
this._bufferedDepMessages = [];
|
|
1492
|
+
for (const entry of drain) {
|
|
1493
|
+
for (const msg of entry.msgs) {
|
|
1494
|
+
this._updateMasksForMessage(entry.index, msg);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
this._depDirtyBits.clear();
|
|
1498
|
+
this._depSettledBits.clear();
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
} finally {
|
|
1502
|
+
this._running = false;
|
|
1561
1503
|
}
|
|
1504
|
+
this._downAutoValue(result);
|
|
1562
1505
|
}
|
|
1563
1506
|
_rewire(newDeps) {
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
|
|
1580
|
-
newUnsubs.push(unsub);
|
|
1581
|
-
}
|
|
1507
|
+
const oldMap = this._depIndexMap;
|
|
1508
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1509
|
+
const newUnsubs = [];
|
|
1510
|
+
for (let i = 0; i < newDeps.length; i++) {
|
|
1511
|
+
const dep = newDeps[i];
|
|
1512
|
+
newMap.set(dep, i);
|
|
1513
|
+
const oldIdx = oldMap.get(dep);
|
|
1514
|
+
if (oldIdx !== void 0) {
|
|
1515
|
+
newUnsubs.push(this._depUnsubs[oldIdx]);
|
|
1516
|
+
this._depUnsubs[oldIdx] = () => {
|
|
1517
|
+
};
|
|
1518
|
+
} else {
|
|
1519
|
+
const idx = i;
|
|
1520
|
+
const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
|
|
1521
|
+
newUnsubs.push(unsub);
|
|
1582
1522
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1523
|
+
}
|
|
1524
|
+
for (const [dep, oldIdx] of oldMap) {
|
|
1525
|
+
if (!newMap.has(dep)) {
|
|
1526
|
+
this._depUnsubs[oldIdx]();
|
|
1587
1527
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1528
|
+
}
|
|
1529
|
+
this._deps = newDeps;
|
|
1530
|
+
this._depUnsubs = newUnsubs;
|
|
1531
|
+
this._depIndexMap = newMap;
|
|
1532
|
+
this._depDirtyBits.clear();
|
|
1533
|
+
this._depSettledBits.clear();
|
|
1534
|
+
const newCompleteBits = /* @__PURE__ */ new Set();
|
|
1535
|
+
for (const oldIdx of this._depCompleteBits) {
|
|
1536
|
+
for (const [dep, idx] of oldMap) {
|
|
1537
|
+
if (idx === oldIdx && newMap.has(dep)) {
|
|
1597
1538
|
newCompleteBits.add(newMap.get(dep));
|
|
1539
|
+
break;
|
|
1598
1540
|
}
|
|
1599
1541
|
}
|
|
1600
|
-
this._completeBits = newCompleteBits;
|
|
1601
|
-
} finally {
|
|
1602
|
-
this._rewiring = false;
|
|
1603
1542
|
}
|
|
1543
|
+
this._depCompleteBits = newCompleteBits;
|
|
1604
1544
|
}
|
|
1545
|
+
// --- Dep message handling ---
|
|
1605
1546
|
_handleDepMessages(index, messages) {
|
|
1606
|
-
if (this._rewiring)
|
|
1547
|
+
if (this._rewiring) {
|
|
1548
|
+
this._bufferedDepMessages.push({ index, msgs: messages });
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1607
1551
|
for (const msg of messages) {
|
|
1608
|
-
this.
|
|
1552
|
+
this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
|
|
1609
1553
|
const t = msg[0];
|
|
1610
1554
|
if (this._onMessage) {
|
|
1611
1555
|
try {
|
|
1612
1556
|
if (this._onMessage(msg, index, this._actions)) continue;
|
|
1613
1557
|
} catch (err) {
|
|
1614
|
-
|
|
1558
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1559
|
+
const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
|
|
1560
|
+
cause: err
|
|
1561
|
+
});
|
|
1562
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
1615
1563
|
return;
|
|
1616
1564
|
}
|
|
1617
1565
|
}
|
|
1566
|
+
if (messageTier(t) < 1) continue;
|
|
1618
1567
|
if (t === DIRTY) {
|
|
1619
|
-
this.
|
|
1620
|
-
this.
|
|
1621
|
-
|
|
1622
|
-
|
|
1568
|
+
const wasEmpty = this._depDirtyBits.size === 0;
|
|
1569
|
+
this._depDirtyBits.add(index);
|
|
1570
|
+
this._depSettledBits.delete(index);
|
|
1571
|
+
if (wasEmpty) {
|
|
1572
|
+
this._downInternal([[DIRTY]]);
|
|
1623
1573
|
}
|
|
1624
1574
|
continue;
|
|
1625
1575
|
}
|
|
1626
1576
|
if (t === DATA || t === RESOLVED) {
|
|
1627
|
-
if (!this.
|
|
1628
|
-
this.
|
|
1629
|
-
|
|
1577
|
+
if (!this._depDirtyBits.has(index)) {
|
|
1578
|
+
const wasEmpty = this._depDirtyBits.size === 0;
|
|
1579
|
+
this._depDirtyBits.add(index);
|
|
1580
|
+
if (wasEmpty) {
|
|
1581
|
+
this._downInternal([[DIRTY]]);
|
|
1582
|
+
}
|
|
1630
1583
|
}
|
|
1631
|
-
this.
|
|
1584
|
+
this._depSettledBits.add(index);
|
|
1632
1585
|
if (this._allDirtySettled()) {
|
|
1633
|
-
this.
|
|
1634
|
-
this.
|
|
1635
|
-
this.
|
|
1586
|
+
this._depDirtyBits.clear();
|
|
1587
|
+
this._depSettledBits.clear();
|
|
1588
|
+
if (!this._running) {
|
|
1589
|
+
if (this._depValuesDifferFromTracked()) {
|
|
1590
|
+
this._runFn();
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1636
1593
|
}
|
|
1637
1594
|
continue;
|
|
1638
1595
|
}
|
|
1639
1596
|
if (t === COMPLETE) {
|
|
1640
|
-
this.
|
|
1641
|
-
this.
|
|
1642
|
-
this.
|
|
1597
|
+
this._depCompleteBits.add(index);
|
|
1598
|
+
this._depDirtyBits.delete(index);
|
|
1599
|
+
this._depSettledBits.delete(index);
|
|
1643
1600
|
if (this._allDirtySettled()) {
|
|
1644
|
-
this.
|
|
1645
|
-
this.
|
|
1646
|
-
this._runFn();
|
|
1601
|
+
this._depDirtyBits.clear();
|
|
1602
|
+
this._depSettledBits.clear();
|
|
1603
|
+
if (!this._running) this._runFn();
|
|
1647
1604
|
}
|
|
1648
|
-
if (this._autoComplete && this.
|
|
1605
|
+
if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
|
|
1649
1606
|
this._downInternal([[COMPLETE]]);
|
|
1650
1607
|
}
|
|
1651
1608
|
continue;
|
|
@@ -1661,13 +1618,46 @@ var DynamicNodeImpl = class {
|
|
|
1661
1618
|
this._downInternal([msg]);
|
|
1662
1619
|
}
|
|
1663
1620
|
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Update dep masks for a message without triggering `_runFn` — used
|
|
1623
|
+
* during post-rewire drain so the wave state is consistent with the
|
|
1624
|
+
* buffered activation cascade without recursing.
|
|
1625
|
+
*/
|
|
1626
|
+
_updateMasksForMessage(index, msg) {
|
|
1627
|
+
const t = msg[0];
|
|
1628
|
+
if (t === DIRTY) {
|
|
1629
|
+
this._depDirtyBits.add(index);
|
|
1630
|
+
this._depSettledBits.delete(index);
|
|
1631
|
+
} else if (t === DATA || t === RESOLVED) {
|
|
1632
|
+
this._depDirtyBits.add(index);
|
|
1633
|
+
this._depSettledBits.add(index);
|
|
1634
|
+
} else if (t === COMPLETE) {
|
|
1635
|
+
this._depCompleteBits.add(index);
|
|
1636
|
+
this._depDirtyBits.delete(index);
|
|
1637
|
+
this._depSettledBits.delete(index);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1664
1640
|
_allDirtySettled() {
|
|
1665
|
-
if (this.
|
|
1666
|
-
for (const idx of this.
|
|
1667
|
-
if (!this.
|
|
1641
|
+
if (this._depDirtyBits.size === 0) return false;
|
|
1642
|
+
for (const idx of this._depDirtyBits) {
|
|
1643
|
+
if (!this._depSettledBits.has(idx)) return false;
|
|
1668
1644
|
}
|
|
1669
1645
|
return true;
|
|
1670
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* True if any current dep value differs from what the last `_runFn`
|
|
1649
|
+
* saw via `get()`. Used to suppress redundant re-runs when deferred
|
|
1650
|
+
* handshake messages arrive after `_rewire` for a dep whose value
|
|
1651
|
+
* already matches `_trackedValues`.
|
|
1652
|
+
*/
|
|
1653
|
+
_depValuesDifferFromTracked() {
|
|
1654
|
+
for (const dep of this._deps) {
|
|
1655
|
+
const current = dep.get();
|
|
1656
|
+
const tracked = this._trackedValues.get(dep);
|
|
1657
|
+
if (!this._equals(current, tracked)) return true;
|
|
1658
|
+
}
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1671
1661
|
};
|
|
1672
1662
|
|
|
1673
1663
|
// src/core/meta.ts
|
|
@@ -1737,6 +1727,10 @@ function describeNode(node2, includeFields) {
|
|
|
1737
1727
|
out.name = node2.name;
|
|
1738
1728
|
}
|
|
1739
1729
|
if (all || includeFields.has("value")) {
|
|
1730
|
+
const isSentinel = node2 instanceof NodeImpl && node2._cached === NO_VALUE || node2 instanceof DynamicNodeImpl && node2._cached === NO_VALUE;
|
|
1731
|
+
if (isSentinel) {
|
|
1732
|
+
out.sentinel = true;
|
|
1733
|
+
}
|
|
1740
1734
|
try {
|
|
1741
1735
|
out.value = node2.get();
|
|
1742
1736
|
} catch {
|
|
@@ -1763,6 +1757,129 @@ function describeNode(node2, includeFields) {
|
|
|
1763
1757
|
return out;
|
|
1764
1758
|
}
|
|
1765
1759
|
|
|
1760
|
+
// src/graph/sizeof.ts
|
|
1761
|
+
var OVERHEAD = {
|
|
1762
|
+
object: 56,
|
|
1763
|
+
array: 64,
|
|
1764
|
+
string: 40,
|
|
1765
|
+
// header; content added separately
|
|
1766
|
+
number: 8,
|
|
1767
|
+
boolean: 4,
|
|
1768
|
+
null: 0,
|
|
1769
|
+
undefined: 0,
|
|
1770
|
+
symbol: 40,
|
|
1771
|
+
bigint: 16,
|
|
1772
|
+
function: 120,
|
|
1773
|
+
map: 72,
|
|
1774
|
+
set: 72,
|
|
1775
|
+
mapEntry: 40,
|
|
1776
|
+
setEntry: 24
|
|
1777
|
+
};
|
|
1778
|
+
function sizeof(value) {
|
|
1779
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1780
|
+
return _sizeof(value, seen);
|
|
1781
|
+
}
|
|
1782
|
+
function _sizeof(value, seen) {
|
|
1783
|
+
if (value == null) return 0;
|
|
1784
|
+
const t = typeof value;
|
|
1785
|
+
switch (t) {
|
|
1786
|
+
case "number":
|
|
1787
|
+
return OVERHEAD.number;
|
|
1788
|
+
case "boolean":
|
|
1789
|
+
return OVERHEAD.boolean;
|
|
1790
|
+
case "string":
|
|
1791
|
+
return OVERHEAD.string + value.length * 2;
|
|
1792
|
+
// UTF-16
|
|
1793
|
+
case "bigint":
|
|
1794
|
+
return OVERHEAD.bigint;
|
|
1795
|
+
case "symbol":
|
|
1796
|
+
return OVERHEAD.symbol;
|
|
1797
|
+
case "function":
|
|
1798
|
+
if (seen.has(value)) return 0;
|
|
1799
|
+
seen.add(value);
|
|
1800
|
+
return OVERHEAD.function;
|
|
1801
|
+
case "undefined":
|
|
1802
|
+
return 0;
|
|
1803
|
+
}
|
|
1804
|
+
const obj = value;
|
|
1805
|
+
if (seen.has(obj)) return 0;
|
|
1806
|
+
seen.add(obj);
|
|
1807
|
+
if (obj instanceof Map) {
|
|
1808
|
+
let size2 = OVERHEAD.map;
|
|
1809
|
+
for (const [k, v] of obj) {
|
|
1810
|
+
size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
|
|
1811
|
+
}
|
|
1812
|
+
return size2;
|
|
1813
|
+
}
|
|
1814
|
+
if (obj instanceof Set) {
|
|
1815
|
+
let size2 = OVERHEAD.set;
|
|
1816
|
+
for (const v of obj) {
|
|
1817
|
+
size2 += OVERHEAD.setEntry + _sizeof(v, seen);
|
|
1818
|
+
}
|
|
1819
|
+
return size2;
|
|
1820
|
+
}
|
|
1821
|
+
if (Array.isArray(obj)) {
|
|
1822
|
+
let size2 = OVERHEAD.array + obj.length * 8;
|
|
1823
|
+
for (const item of obj) {
|
|
1824
|
+
size2 += _sizeof(item, seen);
|
|
1825
|
+
}
|
|
1826
|
+
return size2;
|
|
1827
|
+
}
|
|
1828
|
+
if (obj instanceof ArrayBuffer) return obj.byteLength;
|
|
1829
|
+
if (ArrayBuffer.isView(obj)) return obj.byteLength;
|
|
1830
|
+
let size = OVERHEAD.object;
|
|
1831
|
+
const keys = Object.keys(obj);
|
|
1832
|
+
for (const key of keys) {
|
|
1833
|
+
size += OVERHEAD.string + key.length * 2;
|
|
1834
|
+
size += _sizeof(obj[key], seen);
|
|
1835
|
+
}
|
|
1836
|
+
return size;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/graph/profile.ts
|
|
1840
|
+
function graphProfile(graph, opts) {
|
|
1841
|
+
const topN = opts?.topN ?? 10;
|
|
1842
|
+
const desc = graph.describe({ detail: "standard" });
|
|
1843
|
+
const targets = [];
|
|
1844
|
+
if (typeof graph._collectObserveTargets === "function") {
|
|
1845
|
+
graph._collectObserveTargets("", targets);
|
|
1846
|
+
}
|
|
1847
|
+
const pathToNode = /* @__PURE__ */ new Map();
|
|
1848
|
+
for (const [p, n] of targets) {
|
|
1849
|
+
pathToNode.set(p, n);
|
|
1850
|
+
}
|
|
1851
|
+
const profiles = [];
|
|
1852
|
+
for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
|
|
1853
|
+
const nd = pathToNode.get(path);
|
|
1854
|
+
const impl = nd instanceof NodeImpl ? nd : null;
|
|
1855
|
+
const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
|
|
1856
|
+
const subscriberCount = impl ? impl._sinkCount : 0;
|
|
1857
|
+
const depCount = nodeDesc.deps?.length ?? 0;
|
|
1858
|
+
const isOrphanEffect = nodeDesc.type === "effect" && subscriberCount === 0;
|
|
1859
|
+
profiles.push({
|
|
1860
|
+
path,
|
|
1861
|
+
type: nodeDesc.type,
|
|
1862
|
+
status: nodeDesc.status ?? "unknown",
|
|
1863
|
+
valueSizeBytes,
|
|
1864
|
+
subscriberCount,
|
|
1865
|
+
depCount,
|
|
1866
|
+
isOrphanEffect
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
|
|
1870
|
+
const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
|
|
1871
|
+
const orphanEffects = profiles.filter((p) => p.isOrphanEffect);
|
|
1872
|
+
return {
|
|
1873
|
+
nodeCount: profiles.length,
|
|
1874
|
+
edgeCount: desc.edges.length,
|
|
1875
|
+
subgraphCount: desc.subgraphs.length,
|
|
1876
|
+
nodes: profiles,
|
|
1877
|
+
totalValueSizeBytes,
|
|
1878
|
+
hotspots,
|
|
1879
|
+
orphanEffects
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1766
1883
|
// src/graph/graph.ts
|
|
1767
1884
|
var PATH_SEP = "::";
|
|
1768
1885
|
var GRAPH_META_SEGMENT = "__meta__";
|
|
@@ -2675,6 +2792,16 @@ var Graph = class _Graph {
|
|
|
2675
2792
|
}
|
|
2676
2793
|
return out;
|
|
2677
2794
|
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Snapshot-based resource profile: per-node stats, orphan effect detection,
|
|
2797
|
+
* memory hotspots. Zero runtime overhead — walks nodes on demand.
|
|
2798
|
+
*
|
|
2799
|
+
* @param opts - Optional `topN` for hotspot limit (default 10).
|
|
2800
|
+
* @returns Aggregate profile with per-node details, hotspots, and orphan effects.
|
|
2801
|
+
*/
|
|
2802
|
+
resourceProfile(opts) {
|
|
2803
|
+
return graphProfile(this, opts);
|
|
2804
|
+
}
|
|
2678
2805
|
_qualifyEdgeEndpoint(part, prefix) {
|
|
2679
2806
|
if (part.includes(PATH_SEP)) return part;
|
|
2680
2807
|
return prefix === "" ? part : `${prefix}${PATH_SEP}${part}`;
|
|
@@ -2776,8 +2903,8 @@ var Graph = class _Graph {
|
|
|
2776
2903
|
dirtyCount: 0,
|
|
2777
2904
|
resolvedCount: 0,
|
|
2778
2905
|
events: [],
|
|
2779
|
-
|
|
2780
|
-
|
|
2906
|
+
anyCompletedCleanly: false,
|
|
2907
|
+
anyErrored: false
|
|
2781
2908
|
};
|
|
2782
2909
|
let lastTriggerDepIndex;
|
|
2783
2910
|
let lastRunDepValues;
|
|
@@ -2821,8 +2948,8 @@ var Graph = class _Graph {
|
|
|
2821
2948
|
} else if (minimal) {
|
|
2822
2949
|
if (t === DIRTY) result.dirtyCount++;
|
|
2823
2950
|
else if (t === RESOLVED) result.resolvedCount++;
|
|
2824
|
-
else if (t === COMPLETE && !result.
|
|
2825
|
-
else if (t === ERROR) result.
|
|
2951
|
+
else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
|
|
2952
|
+
else if (t === ERROR) result.anyErrored = true;
|
|
2826
2953
|
} else if (t === DIRTY) {
|
|
2827
2954
|
result.dirtyCount++;
|
|
2828
2955
|
result.events.push({ type: "dirty", path, ...base });
|
|
@@ -2830,10 +2957,10 @@ var Graph = class _Graph {
|
|
|
2830
2957
|
result.resolvedCount++;
|
|
2831
2958
|
result.events.push({ type: "resolved", path, ...base, ...withCausal });
|
|
2832
2959
|
} else if (t === COMPLETE) {
|
|
2833
|
-
if (!result.
|
|
2960
|
+
if (!result.anyErrored) result.anyCompletedCleanly = true;
|
|
2834
2961
|
result.events.push({ type: "complete", path, ...base });
|
|
2835
2962
|
} else if (t === ERROR) {
|
|
2836
|
-
result.
|
|
2963
|
+
result.anyErrored = true;
|
|
2837
2964
|
result.events.push({ type: "error", path, data: m[1], ...base });
|
|
2838
2965
|
}
|
|
2839
2966
|
}
|
|
@@ -2853,11 +2980,14 @@ var Graph = class _Graph {
|
|
|
2853
2980
|
get events() {
|
|
2854
2981
|
return result.events;
|
|
2855
2982
|
},
|
|
2856
|
-
get
|
|
2857
|
-
return result.
|
|
2983
|
+
get anyCompletedCleanly() {
|
|
2984
|
+
return result.anyCompletedCleanly;
|
|
2985
|
+
},
|
|
2986
|
+
get anyErrored() {
|
|
2987
|
+
return result.anyErrored;
|
|
2858
2988
|
},
|
|
2859
|
-
get
|
|
2860
|
-
return result.
|
|
2989
|
+
get completedWithoutErrors() {
|
|
2990
|
+
return result.anyCompletedCleanly && !result.anyErrored;
|
|
2861
2991
|
},
|
|
2862
2992
|
dispose() {
|
|
2863
2993
|
unsub();
|
|
@@ -2893,9 +3023,10 @@ var Graph = class _Graph {
|
|
|
2893
3023
|
dirtyCount: 0,
|
|
2894
3024
|
resolvedCount: 0,
|
|
2895
3025
|
events: [],
|
|
2896
|
-
|
|
2897
|
-
|
|
3026
|
+
anyCompletedCleanly: false,
|
|
3027
|
+
anyErrored: false
|
|
2898
3028
|
};
|
|
3029
|
+
const nodeErrored = /* @__PURE__ */ new Set();
|
|
2899
3030
|
const actor = options.actor;
|
|
2900
3031
|
const targets = [];
|
|
2901
3032
|
this._collectObserveTargets("", targets);
|
|
@@ -2914,8 +3045,11 @@ var Graph = class _Graph {
|
|
|
2914
3045
|
} else if (minimal) {
|
|
2915
3046
|
if (t === DIRTY) result.dirtyCount++;
|
|
2916
3047
|
else if (t === RESOLVED) result.resolvedCount++;
|
|
2917
|
-
else if (t === COMPLETE && !
|
|
2918
|
-
else if (t === ERROR)
|
|
3048
|
+
else if (t === COMPLETE && !nodeErrored.has(path)) result.anyCompletedCleanly = true;
|
|
3049
|
+
else if (t === ERROR) {
|
|
3050
|
+
result.anyErrored = true;
|
|
3051
|
+
nodeErrored.add(path);
|
|
3052
|
+
}
|
|
2919
3053
|
} else if (t === DIRTY) {
|
|
2920
3054
|
result.dirtyCount++;
|
|
2921
3055
|
result.events.push({ type: "dirty", path, ...base });
|
|
@@ -2923,10 +3057,11 @@ var Graph = class _Graph {
|
|
|
2923
3057
|
result.resolvedCount++;
|
|
2924
3058
|
result.events.push({ type: "resolved", path, ...base });
|
|
2925
3059
|
} else if (t === COMPLETE) {
|
|
2926
|
-
if (!
|
|
3060
|
+
if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
|
|
2927
3061
|
result.events.push({ type: "complete", path, ...base });
|
|
2928
3062
|
} else if (t === ERROR) {
|
|
2929
|
-
result.
|
|
3063
|
+
result.anyErrored = true;
|
|
3064
|
+
nodeErrored.add(path);
|
|
2930
3065
|
result.events.push({ type: "error", path, data: m[1], ...base });
|
|
2931
3066
|
}
|
|
2932
3067
|
}
|
|
@@ -2946,11 +3081,14 @@ var Graph = class _Graph {
|
|
|
2946
3081
|
get events() {
|
|
2947
3082
|
return result.events;
|
|
2948
3083
|
},
|
|
2949
|
-
get
|
|
2950
|
-
return result.
|
|
3084
|
+
get anyCompletedCleanly() {
|
|
3085
|
+
return result.anyCompletedCleanly;
|
|
3086
|
+
},
|
|
3087
|
+
get anyErrored() {
|
|
3088
|
+
return result.anyErrored;
|
|
2951
3089
|
},
|
|
2952
|
-
get
|
|
2953
|
-
return result.
|
|
3090
|
+
get completedWithoutErrors() {
|
|
3091
|
+
return result.anyCompletedCleanly && !result.anyErrored;
|
|
2954
3092
|
},
|
|
2955
3093
|
dispose() {
|
|
2956
3094
|
for (const u of unsubs) u();
|
|
@@ -2982,8 +3120,8 @@ var Graph = class _Graph {
|
|
|
2982
3120
|
dirtyCount: 0,
|
|
2983
3121
|
resolvedCount: 0,
|
|
2984
3122
|
events: [],
|
|
2985
|
-
|
|
2986
|
-
|
|
3123
|
+
anyCompletedCleanly: false,
|
|
3124
|
+
anyErrored: false
|
|
2987
3125
|
};
|
|
2988
3126
|
const target = this.resolve(path);
|
|
2989
3127
|
let batchSeq = 0;
|
|
@@ -3002,10 +3140,10 @@ var Graph = class _Graph {
|
|
|
3002
3140
|
acc.resolvedCount++;
|
|
3003
3141
|
acc.events.push({ type: "resolved", path, ...base });
|
|
3004
3142
|
} else if (t === COMPLETE) {
|
|
3005
|
-
if (!acc.
|
|
3143
|
+
if (!acc.anyErrored) acc.anyCompletedCleanly = true;
|
|
3006
3144
|
acc.events.push({ type: "complete", path, ...base });
|
|
3007
3145
|
} else if (t === ERROR) {
|
|
3008
|
-
acc.
|
|
3146
|
+
acc.anyErrored = true;
|
|
3009
3147
|
acc.events.push({ type: "error", path, data: m[1], ...base });
|
|
3010
3148
|
}
|
|
3011
3149
|
}
|
|
@@ -3023,11 +3161,14 @@ var Graph = class _Graph {
|
|
|
3023
3161
|
get events() {
|
|
3024
3162
|
return acc.events;
|
|
3025
3163
|
},
|
|
3026
|
-
get
|
|
3027
|
-
return acc.
|
|
3164
|
+
get anyCompletedCleanly() {
|
|
3165
|
+
return acc.anyCompletedCleanly;
|
|
3028
3166
|
},
|
|
3029
|
-
get
|
|
3030
|
-
return acc.
|
|
3167
|
+
get anyErrored() {
|
|
3168
|
+
return acc.anyErrored;
|
|
3169
|
+
},
|
|
3170
|
+
get completedWithoutErrors() {
|
|
3171
|
+
return acc.anyCompletedCleanly && !acc.anyErrored;
|
|
3031
3172
|
},
|
|
3032
3173
|
dispose() {
|
|
3033
3174
|
unsub();
|
|
@@ -3048,9 +3189,10 @@ var Graph = class _Graph {
|
|
|
3048
3189
|
dirtyCount: 0,
|
|
3049
3190
|
resolvedCount: 0,
|
|
3050
3191
|
events: [],
|
|
3051
|
-
|
|
3052
|
-
|
|
3192
|
+
anyCompletedCleanly: false,
|
|
3193
|
+
anyErrored: false
|
|
3053
3194
|
};
|
|
3195
|
+
const nodeErrored = /* @__PURE__ */ new Set();
|
|
3054
3196
|
const targets = [];
|
|
3055
3197
|
this._collectObserveTargets("", targets);
|
|
3056
3198
|
targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
@@ -3072,10 +3214,11 @@ var Graph = class _Graph {
|
|
|
3072
3214
|
acc.resolvedCount++;
|
|
3073
3215
|
acc.events.push({ type: "resolved", path, ...base });
|
|
3074
3216
|
} else if (t === COMPLETE) {
|
|
3075
|
-
if (!
|
|
3217
|
+
if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
|
|
3076
3218
|
acc.events.push({ type: "complete", path, ...base });
|
|
3077
3219
|
} else if (t === ERROR) {
|
|
3078
|
-
acc.
|
|
3220
|
+
acc.anyErrored = true;
|
|
3221
|
+
nodeErrored.add(path);
|
|
3079
3222
|
acc.events.push({ type: "error", path, data: m[1], ...base });
|
|
3080
3223
|
}
|
|
3081
3224
|
}
|
|
@@ -3094,11 +3237,14 @@ var Graph = class _Graph {
|
|
|
3094
3237
|
get events() {
|
|
3095
3238
|
return acc.events;
|
|
3096
3239
|
},
|
|
3097
|
-
get
|
|
3098
|
-
return acc.
|
|
3240
|
+
get anyCompletedCleanly() {
|
|
3241
|
+
return acc.anyCompletedCleanly;
|
|
3242
|
+
},
|
|
3243
|
+
get anyErrored() {
|
|
3244
|
+
return acc.anyErrored;
|
|
3099
3245
|
},
|
|
3100
|
-
get
|
|
3101
|
-
return acc.
|
|
3246
|
+
get completedWithoutErrors() {
|
|
3247
|
+
return acc.anyCompletedCleanly && !acc.anyErrored;
|
|
3102
3248
|
},
|
|
3103
3249
|
dispose() {
|
|
3104
3250
|
for (const u of unsubs) u();
|
|
@@ -3424,8 +3570,9 @@ var Graph = class _Graph {
|
|
|
3424
3570
|
/**
|
|
3425
3571
|
* Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
|
|
3426
3572
|
*
|
|
3427
|
-
* Checkpoint trigger uses {@link messageTier}: only batches containing tier >=
|
|
3428
|
-
* schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1 control
|
|
3573
|
+
* Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 3 messages
|
|
3574
|
+
* schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1/2 control
|
|
3575
|
+
* waves (`START`/`DIRTY`/`INVALIDATE`/`PAUSE`/`RESUME`).
|
|
3429
3576
|
*/
|
|
3430
3577
|
autoCheckpoint(adapter, options = {}) {
|
|
3431
3578
|
const debounceMs = Math.max(0, options.debounceMs ?? 500);
|
|
@@ -3472,7 +3619,7 @@ var Graph = class _Graph {
|
|
|
3472
3619
|
timer = setTimeout(flush, debounceMs);
|
|
3473
3620
|
};
|
|
3474
3621
|
const off = this.observe().subscribe((path, messages) => {
|
|
3475
|
-
const triggeredByTier = messages.some((m) => messageTier(m[0]) >=
|
|
3622
|
+
const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
|
|
3476
3623
|
if (!triggeredByTier) return;
|
|
3477
3624
|
if (options.filter) {
|
|
3478
3625
|
const nd = this.resolve(path);
|