@graphrefly/graphrefly 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-R6OHUUYB.js → chunk-AHRKWMNI.js} +7 -7
- package/dist/chunk-AHRKWMNI.js.map +1 -0
- package/dist/{chunk-2PORF4RP.js → chunk-BER7UYLM.js} +27 -32
- package/dist/chunk-BER7UYLM.js.map +1 -0
- package/dist/{chunk-646OG3PO.js → chunk-IRZAGZUB.js} +51 -52
- package/dist/chunk-IRZAGZUB.js.map +1 -0
- package/dist/{chunk-IHJHBADD.js → chunk-JC2SN46B.js} +385 -197
- package/dist/chunk-JC2SN46B.js.map +1 -0
- package/dist/{chunk-XJ6EMQ22.js → chunk-OO5QOAXI.js} +4 -10
- package/dist/chunk-OO5QOAXI.js.map +1 -0
- package/dist/{chunk-YXROQFXZ.js → chunk-UW77D7SP.js} +3 -3
- package/dist/{chunk-F2ULI3Q3.js → chunk-XUOY3YKN.js} +7 -3
- 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 +1127 -983
- 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 -13
- package/dist/core/index.cjs +653 -749
- 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 -7
- package/dist/extra/index.cjs +773 -795
- 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 -11
- package/dist/graph/index.cjs +1036 -975
- 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-fCsaaVIa.d.cts → graph-KsTe57nI.d.cts} +127 -51
- package/dist/{graph-Dc-P9BVm.d.ts → graph-mILUUqW8.d.ts} +127 -51
- package/dist/{index-DhXznWyH.d.ts → index-8a605sg9.d.ts} +2 -2
- package/dist/{index-D7y9Q8W4.d.ts → index-B2SvPEbc.d.ts} +8 -69
- package/dist/{index-YlOH1Gw6.d.cts → index-BBUYZfJ1.d.cts} +122 -78
- package/dist/{index-ClaKZFPl.d.cts → index-Bjh5C1Tp.d.cts} +38 -35
- package/dist/{index-DWq0P9T6.d.ts → index-BjtlNirP.d.cts} +5 -7
- package/dist/{index-N704txAA.d.ts → index-BnkMgNNa.d.ts} +38 -35
- package/dist/{index-BBVBYPxr.d.cts → index-CgSiUouz.d.ts} +5 -7
- package/dist/{index-BmoUvOGN.d.ts → index-CvKzv0AW.d.ts} +122 -78
- package/dist/{index-4OIX-q0C.d.cts → index-UudxGnzc.d.cts} +8 -69
- package/dist/{index-DlGMf_Qe.d.cts → index-VHA43cGP.d.cts} +2 -2
- package/dist/index.cjs +6146 -5725
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +617 -383
- package/dist/index.d.ts +617 -383
- package/dist/index.js +4401 -4028
- package/dist/index.js.map +1 -1
- package/dist/{meta-BV4pj9ML.d.cts → meta-BnG7XAaE.d.cts} +395 -289
- package/dist/{meta-BV4pj9ML.d.ts → meta-BnG7XAaE.d.ts} +395 -289
- package/dist/observable-C8Kx_O6k.d.cts +36 -0
- package/dist/observable-DcBwQY7t.d.ts +36 -0
- package/dist/patterns/reactive-layout/index.cjs +1037 -857
- 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-2PORF4RP.js.map +0 -1
- package/dist/chunk-646OG3PO.js.map +0 -1
- package/dist/chunk-BV3TPSBK.js.map +0 -1
- package/dist/chunk-EBNKJULL.js +0 -231
- package/dist/chunk-EBNKJULL.js.map +0 -1
- package/dist/chunk-F2ULI3Q3.js.map +0 -1
- package/dist/chunk-IHJHBADD.js.map +0 -1
- package/dist/chunk-R6OHUUYB.js.map +0 -1
- package/dist/chunk-XJ6EMQ22.js.map +0 -1
- package/dist/observable-Cz-AWhwR.d.cts +0 -42
- package/dist/observable-DCqlwGyl.d.ts +0 -42
- /package/dist/{chunk-YXROQFXZ.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";
|
|
1124
|
+
}
|
|
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();
|
|
1075
1155
|
}
|
|
1076
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,53 +1235,85 @@ 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;
|
|
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]]);
|
|
1154
1244
|
}
|
|
1155
|
-
|
|
1245
|
+
}
|
|
1246
|
+
_onDepSettled(index) {
|
|
1247
|
+
if (!this._depDirtyMask.has(index)) {
|
|
1248
|
+
this._onDepDirty(index);
|
|
1249
|
+
}
|
|
1250
|
+
this._depSettledMask.set(index);
|
|
1251
|
+
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
1252
|
+
this._depDirtyMask.reset();
|
|
1253
|
+
this._depSettledMask.reset();
|
|
1156
1254
|
this._runFn();
|
|
1157
1255
|
}
|
|
1158
1256
|
}
|
|
1159
|
-
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
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();
|
|
1257
|
+
_maybeCompleteFromDeps() {
|
|
1258
|
+
if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
|
|
1259
|
+
this._downInternal([[COMPLETE]]);
|
|
1260
|
+
}
|
|
1170
1261
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (allSame) {
|
|
1280
|
+
if (this._status === "dirty") {
|
|
1281
|
+
this._downInternal([[RESOLVED]]);
|
|
1282
|
+
}
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
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);
|
|
1298
|
+
}
|
|
1299
|
+
return;
|
|
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]]);
|
|
1175
1312
|
}
|
|
1176
|
-
this._connected = false;
|
|
1177
|
-
this._depDirtyMask.reset();
|
|
1178
|
-
this._depSettledMask.reset();
|
|
1179
|
-
this._depCompleteMask.reset();
|
|
1180
|
-
this._status = "disconnected";
|
|
1181
1313
|
}
|
|
1182
1314
|
};
|
|
1315
|
+
var isNodeArray = (value) => Array.isArray(value);
|
|
1316
|
+
var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
|
|
1183
1317
|
function node(depsOrFn, fnOrOpts, optsArg) {
|
|
1184
1318
|
const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
|
|
1185
1319
|
const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
|
|
@@ -1203,449 +1337,272 @@ function derived(deps, fn, opts) {
|
|
|
1203
1337
|
}
|
|
1204
1338
|
|
|
1205
1339
|
// src/core/dynamic-node.ts
|
|
1206
|
-
var
|
|
1207
|
-
|
|
1208
|
-
_registryName;
|
|
1209
|
-
_describeKind;
|
|
1210
|
-
meta;
|
|
1340
|
+
var MAX_RERUN = 16;
|
|
1341
|
+
var DynamicNodeImpl = class extends NodeBase {
|
|
1211
1342
|
_fn;
|
|
1212
|
-
_equals;
|
|
1213
|
-
_resubscribable;
|
|
1214
|
-
_resetOnTeardown;
|
|
1215
1343
|
_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
1344
|
// Dynamic deps tracking
|
|
1345
|
+
/** @internal Read by `describeNode`. */
|
|
1237
1346
|
_deps = [];
|
|
1238
1347
|
_depUnsubs = [];
|
|
1239
1348
|
_depIndexMap = /* @__PURE__ */ new Map();
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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;
|
|
1246
1358
|
constructor(fn, opts) {
|
|
1359
|
+
super(opts);
|
|
1247
1360
|
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
1361
|
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1254
|
-
this.
|
|
1255
|
-
this.
|
|
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
|
-
});
|
|
1266
|
-
}
|
|
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
|
-
}
|
|
1312
|
-
/** Versioning not yet supported on DynamicNodeImpl. */
|
|
1313
|
-
get v() {
|
|
1314
|
-
return void 0;
|
|
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() };
|
|
1336
|
-
}
|
|
1337
|
-
this._downInternal(messages);
|
|
1338
|
-
}
|
|
1339
|
-
_downInternal(messages) {
|
|
1340
|
-
if (messages.length === 0) return;
|
|
1341
|
-
let sinkMessages = messages;
|
|
1342
|
-
if (this._terminal && !this._resubscribable) {
|
|
1343
|
-
const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
1344
|
-
if (pass.length === 0) return;
|
|
1345
|
-
sinkMessages = pass;
|
|
1346
|
-
}
|
|
1347
|
-
this._handleLocalLifecycle(sinkMessages);
|
|
1348
|
-
if (this._canSkipDirty()) {
|
|
1349
|
-
let hasPhase2 = false;
|
|
1350
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
1351
|
-
const t = sinkMessages[i][0];
|
|
1352
|
-
if (t === DATA || t === RESOLVED) {
|
|
1353
|
-
hasPhase2 = true;
|
|
1354
|
-
break;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
if (hasPhase2) {
|
|
1358
|
-
const filtered = [];
|
|
1359
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
1360
|
-
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
1361
|
-
}
|
|
1362
|
-
if (filtered.length > 0) {
|
|
1363
|
-
downWithBatch(this._boundDownToSinks, filtered);
|
|
1364
|
-
}
|
|
1365
|
-
return;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
1369
|
-
}
|
|
1370
|
-
_canSkipDirty() {
|
|
1371
|
-
return this._sinkCount === 1 && this._singleDepSinkCount === 1;
|
|
1372
|
-
}
|
|
1373
|
-
subscribe(sink, hints) {
|
|
1374
|
-
if (hints?.actor != null && this._guard != null) {
|
|
1375
|
-
const actor = normalizeActor(hints.actor);
|
|
1376
|
-
if (!this._guard(actor, "observe")) {
|
|
1377
|
-
throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
if (this._terminal && this._resubscribable) {
|
|
1381
|
-
this._terminal = false;
|
|
1382
|
-
this._cached = NO_VALUE;
|
|
1383
|
-
this._status = "disconnected";
|
|
1384
|
-
this._onResubscribe?.();
|
|
1385
|
-
}
|
|
1386
|
-
this._sinkCount += 1;
|
|
1387
|
-
if (hints?.singleDep) {
|
|
1388
|
-
this._singleDepSinkCount += 1;
|
|
1389
|
-
this._singleDepSinks.add(sink);
|
|
1390
|
-
}
|
|
1391
|
-
if (this._sinks == null) {
|
|
1392
|
-
this._sinks = sink;
|
|
1393
|
-
} else if (typeof this._sinks === "function") {
|
|
1394
|
-
this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
|
|
1395
|
-
} else {
|
|
1396
|
-
this._sinks.add(sink);
|
|
1397
|
-
}
|
|
1398
|
-
if (!this._connected) {
|
|
1399
|
-
this._connect();
|
|
1400
|
-
}
|
|
1401
|
-
let removed = false;
|
|
1402
|
-
return () => {
|
|
1403
|
-
if (removed) return;
|
|
1404
|
-
removed = true;
|
|
1405
|
-
this._sinkCount -= 1;
|
|
1406
|
-
if (this._singleDepSinks.has(sink)) {
|
|
1407
|
-
this._singleDepSinkCount -= 1;
|
|
1408
|
-
this._singleDepSinks.delete(sink);
|
|
1409
|
-
}
|
|
1410
|
-
if (this._sinks == null) return;
|
|
1411
|
-
if (typeof this._sinks === "function") {
|
|
1412
|
-
if (this._sinks === sink) this._sinks = null;
|
|
1413
|
-
} else {
|
|
1414
|
-
this._sinks.delete(sink);
|
|
1415
|
-
if (this._sinks.size === 1) {
|
|
1416
|
-
const [only] = this._sinks;
|
|
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
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
up(messages, options) {
|
|
1428
|
-
if (this._deps.length === 0) return;
|
|
1429
|
-
if (!options?.internal && this._guard != null) {
|
|
1430
|
-
const actor = normalizeActor(options?.actor);
|
|
1431
|
-
if (!this._guard(actor, "write")) {
|
|
1432
|
-
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
1433
|
-
}
|
|
1434
|
-
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
1435
|
-
}
|
|
1436
|
-
for (const dep of this._deps) {
|
|
1437
|
-
dep.up?.(messages, options);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
unsubscribe() {
|
|
1441
|
-
this._disconnect();
|
|
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
|
-
}
|
|
1362
|
+
this.down = this.down.bind(this);
|
|
1363
|
+
this.up = this.up.bind(this);
|
|
1454
1364
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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
|
-
}
|
|
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
|
+
});
|
|
1488
1372
|
}
|
|
1489
|
-
/**
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1373
|
+
/** Versioning not supported on DynamicNodeImpl (override base). */
|
|
1374
|
+
get v() {
|
|
1375
|
+
return void 0;
|
|
1376
|
+
}
|
|
1377
|
+
// --- Up / unsubscribe ---
|
|
1378
|
+
up(messages, options) {
|
|
1379
|
+
if (this._deps.length === 0) return;
|
|
1380
|
+
if (!options?.internal && this._guard != null) {
|
|
1381
|
+
const actor = normalizeActor(options?.actor);
|
|
1382
|
+
if (!this._guard(actor, "write")) {
|
|
1383
|
+
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
1495
1384
|
}
|
|
1385
|
+
this._recordMutation(actor);
|
|
1496
1386
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
const wasDirty = this._status === "dirty";
|
|
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;
|
|
1387
|
+
for (const dep of this._deps) {
|
|
1388
|
+
dep.up?.(messages, options);
|
|
1508
1389
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1390
|
+
}
|
|
1391
|
+
_upInternal(messages) {
|
|
1392
|
+
for (const dep of this._deps) {
|
|
1393
|
+
dep.up?.(messages, { internal: true });
|
|
1512
1394
|
}
|
|
1513
|
-
this._cached = value;
|
|
1514
|
-
this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
|
|
1515
1395
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
this._settledBits.clear();
|
|
1522
|
-
this._completeBits.clear();
|
|
1396
|
+
unsubscribe() {
|
|
1397
|
+
this._disconnect();
|
|
1398
|
+
}
|
|
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__";
|
|
@@ -1905,7 +2022,7 @@ var RingBuffer = class {
|
|
|
1905
2022
|
return result;
|
|
1906
2023
|
}
|
|
1907
2024
|
};
|
|
1908
|
-
var
|
|
2025
|
+
var OBSERVE_ANSI_THEME = {
|
|
1909
2026
|
data: "\x1B[32m",
|
|
1910
2027
|
dirty: "\x1B[33m",
|
|
1911
2028
|
resolved: "\x1B[36m",
|
|
@@ -1915,7 +2032,7 @@ var SPY_ANSI_THEME = {
|
|
|
1915
2032
|
path: "\x1B[90m",
|
|
1916
2033
|
reset: "\x1B[0m"
|
|
1917
2034
|
};
|
|
1918
|
-
var
|
|
2035
|
+
var OBSERVE_NO_COLOR_THEME = {
|
|
1919
2036
|
data: "",
|
|
1920
2037
|
dirty: "",
|
|
1921
2038
|
resolved: "",
|
|
@@ -1935,9 +2052,9 @@ function describeData(value) {
|
|
|
1935
2052
|
return "[unserializable]";
|
|
1936
2053
|
}
|
|
1937
2054
|
}
|
|
1938
|
-
function
|
|
1939
|
-
if (theme === "none") return
|
|
1940
|
-
if (theme === "ansi" || theme == null) return
|
|
2055
|
+
function resolveObserveTheme(theme) {
|
|
2056
|
+
if (theme === "none") return OBSERVE_NO_COLOR_THEME;
|
|
2057
|
+
if (theme === "ansi" || theme == null) return OBSERVE_ANSI_THEME;
|
|
1941
2058
|
return {
|
|
1942
2059
|
data: theme.data ?? "",
|
|
1943
2060
|
dirty: theme.dirty ?? "",
|
|
@@ -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}`;
|
|
@@ -2708,9 +2835,13 @@ var Graph = class _Graph {
|
|
|
2708
2835
|
if (actor2 != null && !target.allowsObserve(actor2)) {
|
|
2709
2836
|
throw new GuardDenied({ actor: actor2, action: "observe", nodeName: path });
|
|
2710
2837
|
}
|
|
2711
|
-
const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full";
|
|
2712
|
-
if (wantsStructured2
|
|
2713
|
-
|
|
2838
|
+
const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full" || resolved.format != null;
|
|
2839
|
+
if (wantsStructured2) {
|
|
2840
|
+
const result = _Graph.inspectorEnabled ? this._createObserveResult(path, target, resolved) : this._createFallbackObserveResult(path, resolved);
|
|
2841
|
+
if (resolved.format != null) {
|
|
2842
|
+
this._attachFormatLogger(result, resolved);
|
|
2843
|
+
}
|
|
2844
|
+
return result;
|
|
2714
2845
|
}
|
|
2715
2846
|
return {
|
|
2716
2847
|
subscribe(sink) {
|
|
@@ -2728,9 +2859,13 @@ var Graph = class _Graph {
|
|
|
2728
2859
|
}
|
|
2729
2860
|
const opts = resolveObserveDetail(pathOrOpts);
|
|
2730
2861
|
const actor = opts.actor;
|
|
2731
|
-
const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full";
|
|
2732
|
-
if (wantsStructured
|
|
2733
|
-
|
|
2862
|
+
const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full" || opts.format != null;
|
|
2863
|
+
if (wantsStructured) {
|
|
2864
|
+
const result = _Graph.inspectorEnabled ? this._createObserveResultForAll(opts) : this._createFallbackObserveResultForAll(opts);
|
|
2865
|
+
if (opts.format != null) {
|
|
2866
|
+
this._attachFormatLogger(result, opts);
|
|
2867
|
+
}
|
|
2868
|
+
return result;
|
|
2734
2869
|
}
|
|
2735
2870
|
return {
|
|
2736
2871
|
subscribe: (sink) => {
|
|
@@ -2768,12 +2903,13 @@ var Graph = class _Graph {
|
|
|
2768
2903
|
dirtyCount: 0,
|
|
2769
2904
|
resolvedCount: 0,
|
|
2770
2905
|
events: [],
|
|
2771
|
-
|
|
2772
|
-
|
|
2906
|
+
anyCompletedCleanly: false,
|
|
2907
|
+
anyErrored: false
|
|
2773
2908
|
};
|
|
2774
2909
|
let lastTriggerDepIndex;
|
|
2775
2910
|
let lastRunDepValues;
|
|
2776
2911
|
let detachInspectorHook;
|
|
2912
|
+
let batchSeq = 0;
|
|
2777
2913
|
if ((causal || derived2) && target instanceof NodeImpl) {
|
|
2778
2914
|
detachInspectorHook = target._setInspectorHook((event) => {
|
|
2779
2915
|
if (event.kind === "dep_message") {
|
|
@@ -2786,15 +2922,16 @@ var Graph = class _Graph {
|
|
|
2786
2922
|
type: "derived",
|
|
2787
2923
|
path,
|
|
2788
2924
|
dep_values: [...event.depValues],
|
|
2789
|
-
...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {}
|
|
2925
|
+
...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {}
|
|
2790
2926
|
});
|
|
2791
2927
|
}
|
|
2792
2928
|
});
|
|
2793
2929
|
}
|
|
2794
2930
|
const unsub = target.subscribe((msgs) => {
|
|
2931
|
+
batchSeq++;
|
|
2795
2932
|
for (const m of msgs) {
|
|
2796
2933
|
const t = m[0];
|
|
2797
|
-
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
|
|
2934
|
+
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
|
|
2798
2935
|
const withCausal = causal && lastRunDepValues != null ? (() => {
|
|
2799
2936
|
const triggerDep = lastTriggerDepIndex != null && lastTriggerDepIndex >= 0 && target instanceof NodeImpl ? target._deps[lastTriggerDepIndex] : void 0;
|
|
2800
2937
|
const tv = triggerDep?.v;
|
|
@@ -2811,8 +2948,8 @@ var Graph = class _Graph {
|
|
|
2811
2948
|
} else if (minimal) {
|
|
2812
2949
|
if (t === DIRTY) result.dirtyCount++;
|
|
2813
2950
|
else if (t === RESOLVED) result.resolvedCount++;
|
|
2814
|
-
else if (t === COMPLETE && !result.
|
|
2815
|
-
else if (t === ERROR) result.
|
|
2951
|
+
else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
|
|
2952
|
+
else if (t === ERROR) result.anyErrored = true;
|
|
2816
2953
|
} else if (t === DIRTY) {
|
|
2817
2954
|
result.dirtyCount++;
|
|
2818
2955
|
result.events.push({ type: "dirty", path, ...base });
|
|
@@ -2820,10 +2957,10 @@ var Graph = class _Graph {
|
|
|
2820
2957
|
result.resolvedCount++;
|
|
2821
2958
|
result.events.push({ type: "resolved", path, ...base, ...withCausal });
|
|
2822
2959
|
} else if (t === COMPLETE) {
|
|
2823
|
-
if (!result.
|
|
2960
|
+
if (!result.anyErrored) result.anyCompletedCleanly = true;
|
|
2824
2961
|
result.events.push({ type: "complete", path, ...base });
|
|
2825
2962
|
} else if (t === ERROR) {
|
|
2826
|
-
result.
|
|
2963
|
+
result.anyErrored = true;
|
|
2827
2964
|
result.events.push({ type: "error", path, data: m[1], ...base });
|
|
2828
2965
|
}
|
|
2829
2966
|
}
|
|
@@ -2843,11 +2980,14 @@ var Graph = class _Graph {
|
|
|
2843
2980
|
get events() {
|
|
2844
2981
|
return result.events;
|
|
2845
2982
|
},
|
|
2846
|
-
get
|
|
2847
|
-
return result.
|
|
2983
|
+
get anyCompletedCleanly() {
|
|
2984
|
+
return result.anyCompletedCleanly;
|
|
2985
|
+
},
|
|
2986
|
+
get anyErrored() {
|
|
2987
|
+
return result.anyErrored;
|
|
2848
2988
|
},
|
|
2849
|
-
get
|
|
2850
|
-
return result.
|
|
2989
|
+
get completedWithoutErrors() {
|
|
2990
|
+
return result.anyCompletedCleanly && !result.anyErrored;
|
|
2851
2991
|
},
|
|
2852
2992
|
dispose() {
|
|
2853
2993
|
unsub();
|
|
@@ -2863,11 +3003,15 @@ var Graph = class _Graph {
|
|
|
2863
3003
|
Object.assign(merged, extra);
|
|
2864
3004
|
}
|
|
2865
3005
|
const resolvedTarget = graph.resolve(basePath);
|
|
2866
|
-
|
|
3006
|
+
const expanded = graph._createObserveResult(
|
|
2867
3007
|
basePath,
|
|
2868
3008
|
resolvedTarget,
|
|
2869
3009
|
resolveObserveDetail(merged)
|
|
2870
3010
|
);
|
|
3011
|
+
if (merged.format != null) {
|
|
3012
|
+
graph._attachFormatLogger(expanded, merged);
|
|
3013
|
+
}
|
|
3014
|
+
return expanded;
|
|
2871
3015
|
}
|
|
2872
3016
|
};
|
|
2873
3017
|
}
|
|
@@ -2879,27 +3023,33 @@ var Graph = class _Graph {
|
|
|
2879
3023
|
dirtyCount: 0,
|
|
2880
3024
|
resolvedCount: 0,
|
|
2881
3025
|
events: [],
|
|
2882
|
-
|
|
2883
|
-
|
|
3026
|
+
anyCompletedCleanly: false,
|
|
3027
|
+
anyErrored: false
|
|
2884
3028
|
};
|
|
3029
|
+
const nodeErrored = /* @__PURE__ */ new Set();
|
|
2885
3030
|
const actor = options.actor;
|
|
2886
3031
|
const targets = [];
|
|
2887
3032
|
this._collectObserveTargets("", targets);
|
|
2888
3033
|
targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
2889
3034
|
const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
|
|
3035
|
+
let batchSeq = 0;
|
|
2890
3036
|
const unsubs = picked.map(
|
|
2891
3037
|
([path, nd]) => nd.subscribe((msgs) => {
|
|
3038
|
+
batchSeq++;
|
|
2892
3039
|
for (const m of msgs) {
|
|
2893
3040
|
const t = m[0];
|
|
2894
|
-
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
|
|
3041
|
+
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
|
|
2895
3042
|
if (t === DATA) {
|
|
2896
3043
|
result.values[path] = m[1];
|
|
2897
3044
|
result.events.push({ type: "data", path, data: m[1], ...base });
|
|
2898
3045
|
} else if (minimal) {
|
|
2899
3046
|
if (t === DIRTY) result.dirtyCount++;
|
|
2900
3047
|
else if (t === RESOLVED) result.resolvedCount++;
|
|
2901
|
-
else if (t === COMPLETE && !
|
|
2902
|
-
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
|
+
}
|
|
2903
3053
|
} else if (t === DIRTY) {
|
|
2904
3054
|
result.dirtyCount++;
|
|
2905
3055
|
result.events.push({ type: "dirty", path, ...base });
|
|
@@ -2907,10 +3057,11 @@ var Graph = class _Graph {
|
|
|
2907
3057
|
result.resolvedCount++;
|
|
2908
3058
|
result.events.push({ type: "resolved", path, ...base });
|
|
2909
3059
|
} else if (t === COMPLETE) {
|
|
2910
|
-
if (!
|
|
3060
|
+
if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
|
|
2911
3061
|
result.events.push({ type: "complete", path, ...base });
|
|
2912
3062
|
} else if (t === ERROR) {
|
|
2913
|
-
result.
|
|
3063
|
+
result.anyErrored = true;
|
|
3064
|
+
nodeErrored.add(path);
|
|
2914
3065
|
result.events.push({ type: "error", path, data: m[1], ...base });
|
|
2915
3066
|
}
|
|
2916
3067
|
}
|
|
@@ -2930,11 +3081,14 @@ var Graph = class _Graph {
|
|
|
2930
3081
|
get events() {
|
|
2931
3082
|
return result.events;
|
|
2932
3083
|
},
|
|
2933
|
-
get
|
|
2934
|
-
return result.
|
|
3084
|
+
get anyCompletedCleanly() {
|
|
3085
|
+
return result.anyCompletedCleanly;
|
|
2935
3086
|
},
|
|
2936
|
-
get
|
|
2937
|
-
return result.
|
|
3087
|
+
get anyErrored() {
|
|
3088
|
+
return result.anyErrored;
|
|
3089
|
+
},
|
|
3090
|
+
get completedWithoutErrors() {
|
|
3091
|
+
return result.anyCompletedCleanly && !result.anyErrored;
|
|
2938
3092
|
},
|
|
2939
3093
|
dispose() {
|
|
2940
3094
|
for (const u of unsubs) u();
|
|
@@ -2947,25 +3101,169 @@ var Graph = class _Graph {
|
|
|
2947
3101
|
} else {
|
|
2948
3102
|
Object.assign(merged, extra);
|
|
2949
3103
|
}
|
|
2950
|
-
|
|
3104
|
+
const expanded = graph._createObserveResultForAll(resolveObserveDetail(merged));
|
|
3105
|
+
if (merged.format != null) {
|
|
3106
|
+
graph._attachFormatLogger(expanded, merged);
|
|
3107
|
+
}
|
|
3108
|
+
return expanded;
|
|
2951
3109
|
}
|
|
2952
3110
|
};
|
|
2953
3111
|
}
|
|
2954
3112
|
/**
|
|
2955
|
-
*
|
|
2956
|
-
*
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
3113
|
+
* Fallback ObserveResult for single-node when inspector is disabled but `format` is requested.
|
|
3114
|
+
* Subscribes to raw messages and accumulates events with timeline info.
|
|
3115
|
+
*/
|
|
3116
|
+
_createFallbackObserveResult(path, options) {
|
|
3117
|
+
const timeline = options.timeline !== false;
|
|
3118
|
+
const acc = {
|
|
3119
|
+
values: {},
|
|
3120
|
+
dirtyCount: 0,
|
|
3121
|
+
resolvedCount: 0,
|
|
3122
|
+
events: [],
|
|
3123
|
+
anyCompletedCleanly: false,
|
|
3124
|
+
anyErrored: false
|
|
3125
|
+
};
|
|
3126
|
+
const target = this.resolve(path);
|
|
3127
|
+
let batchSeq = 0;
|
|
3128
|
+
const unsub = target.subscribe((msgs) => {
|
|
3129
|
+
batchSeq++;
|
|
3130
|
+
for (const m of msgs) {
|
|
3131
|
+
const t = m[0];
|
|
3132
|
+
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
|
|
3133
|
+
if (t === DATA) {
|
|
3134
|
+
acc.values[path] = m[1];
|
|
3135
|
+
acc.events.push({ type: "data", path, data: m[1], ...base });
|
|
3136
|
+
} else if (t === DIRTY) {
|
|
3137
|
+
acc.dirtyCount++;
|
|
3138
|
+
acc.events.push({ type: "dirty", path, ...base });
|
|
3139
|
+
} else if (t === RESOLVED) {
|
|
3140
|
+
acc.resolvedCount++;
|
|
3141
|
+
acc.events.push({ type: "resolved", path, ...base });
|
|
3142
|
+
} else if (t === COMPLETE) {
|
|
3143
|
+
if (!acc.anyErrored) acc.anyCompletedCleanly = true;
|
|
3144
|
+
acc.events.push({ type: "complete", path, ...base });
|
|
3145
|
+
} else if (t === ERROR) {
|
|
3146
|
+
acc.anyErrored = true;
|
|
3147
|
+
acc.events.push({ type: "error", path, data: m[1], ...base });
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
return {
|
|
3152
|
+
get values() {
|
|
3153
|
+
return acc.values;
|
|
3154
|
+
},
|
|
3155
|
+
get dirtyCount() {
|
|
3156
|
+
return acc.dirtyCount;
|
|
3157
|
+
},
|
|
3158
|
+
get resolvedCount() {
|
|
3159
|
+
return acc.resolvedCount;
|
|
3160
|
+
},
|
|
3161
|
+
get events() {
|
|
3162
|
+
return acc.events;
|
|
3163
|
+
},
|
|
3164
|
+
get anyCompletedCleanly() {
|
|
3165
|
+
return acc.anyCompletedCleanly;
|
|
3166
|
+
},
|
|
3167
|
+
get anyErrored() {
|
|
3168
|
+
return acc.anyErrored;
|
|
3169
|
+
},
|
|
3170
|
+
get completedWithoutErrors() {
|
|
3171
|
+
return acc.anyCompletedCleanly && !acc.anyErrored;
|
|
3172
|
+
},
|
|
3173
|
+
dispose() {
|
|
3174
|
+
unsub();
|
|
3175
|
+
},
|
|
3176
|
+
expand() {
|
|
3177
|
+
throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
/**
|
|
3182
|
+
* Fallback ObserveResult for graph-wide when inspector is disabled but `format` is requested.
|
|
3183
|
+
*/
|
|
3184
|
+
_createFallbackObserveResultForAll(options) {
|
|
3185
|
+
const timeline = options.timeline !== false;
|
|
3186
|
+
const actor = options.actor;
|
|
3187
|
+
const acc = {
|
|
3188
|
+
values: {},
|
|
3189
|
+
dirtyCount: 0,
|
|
3190
|
+
resolvedCount: 0,
|
|
3191
|
+
events: [],
|
|
3192
|
+
anyCompletedCleanly: false,
|
|
3193
|
+
anyErrored: false
|
|
3194
|
+
};
|
|
3195
|
+
const nodeErrored = /* @__PURE__ */ new Set();
|
|
3196
|
+
const targets = [];
|
|
3197
|
+
this._collectObserveTargets("", targets);
|
|
3198
|
+
targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
3199
|
+
const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
|
|
3200
|
+
let batchSeq = 0;
|
|
3201
|
+
const unsubs = picked.map(
|
|
3202
|
+
([path, nd]) => nd.subscribe((msgs) => {
|
|
3203
|
+
batchSeq++;
|
|
3204
|
+
for (const m of msgs) {
|
|
3205
|
+
const t = m[0];
|
|
3206
|
+
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
|
|
3207
|
+
if (t === DATA) {
|
|
3208
|
+
acc.values[path] = m[1];
|
|
3209
|
+
acc.events.push({ type: "data", path, data: m[1], ...base });
|
|
3210
|
+
} else if (t === DIRTY) {
|
|
3211
|
+
acc.dirtyCount++;
|
|
3212
|
+
acc.events.push({ type: "dirty", path, ...base });
|
|
3213
|
+
} else if (t === RESOLVED) {
|
|
3214
|
+
acc.resolvedCount++;
|
|
3215
|
+
acc.events.push({ type: "resolved", path, ...base });
|
|
3216
|
+
} else if (t === COMPLETE) {
|
|
3217
|
+
if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
|
|
3218
|
+
acc.events.push({ type: "complete", path, ...base });
|
|
3219
|
+
} else if (t === ERROR) {
|
|
3220
|
+
acc.anyErrored = true;
|
|
3221
|
+
nodeErrored.add(path);
|
|
3222
|
+
acc.events.push({ type: "error", path, data: m[1], ...base });
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
})
|
|
3226
|
+
);
|
|
3227
|
+
return {
|
|
3228
|
+
get values() {
|
|
3229
|
+
return acc.values;
|
|
3230
|
+
},
|
|
3231
|
+
get dirtyCount() {
|
|
3232
|
+
return acc.dirtyCount;
|
|
3233
|
+
},
|
|
3234
|
+
get resolvedCount() {
|
|
3235
|
+
return acc.resolvedCount;
|
|
3236
|
+
},
|
|
3237
|
+
get events() {
|
|
3238
|
+
return acc.events;
|
|
3239
|
+
},
|
|
3240
|
+
get anyCompletedCleanly() {
|
|
3241
|
+
return acc.anyCompletedCleanly;
|
|
3242
|
+
},
|
|
3243
|
+
get anyErrored() {
|
|
3244
|
+
return acc.anyErrored;
|
|
3245
|
+
},
|
|
3246
|
+
get completedWithoutErrors() {
|
|
3247
|
+
return acc.anyCompletedCleanly && !acc.anyErrored;
|
|
3248
|
+
},
|
|
3249
|
+
dispose() {
|
|
3250
|
+
for (const u of unsubs) u();
|
|
3251
|
+
},
|
|
3252
|
+
expand() {
|
|
3253
|
+
throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
|
|
3254
|
+
}
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
/**
|
|
3258
|
+
* Attaches a format logger to an ObserveResult, rendering events as they arrive.
|
|
3259
|
+
* Wraps the result's dispose to flush pending events.
|
|
2962
3260
|
*/
|
|
2963
|
-
|
|
3261
|
+
_attachFormatLogger(result, options) {
|
|
3262
|
+
const format = options.format;
|
|
3263
|
+
const logger = options.logger ?? ((line) => console.log(line));
|
|
2964
3264
|
const include = options.includeTypes ? new Set(options.includeTypes) : null;
|
|
2965
3265
|
const exclude = options.excludeTypes ? new Set(options.excludeTypes) : null;
|
|
2966
|
-
const theme =
|
|
2967
|
-
const format = options.format ?? "pretty";
|
|
2968
|
-
const logger = options.logger ?? ((line) => console.log(line));
|
|
3266
|
+
const theme = resolveObserveTheme(options.theme);
|
|
2969
3267
|
const shouldLog = (type) => {
|
|
2970
3268
|
if (include?.has(type) === false) return false;
|
|
2971
3269
|
if (exclude?.has(type) === true) return false;
|
|
@@ -2990,133 +3288,26 @@ var Graph = class _Graph {
|
|
|
2990
3288
|
const batchPart = event.in_batch ? " [batch]" : "";
|
|
2991
3289
|
return `${pathPart}${color}${event.type.toUpperCase()}${theme.reset}${dataPart}${triggerPart}${batchPart}`;
|
|
2992
3290
|
};
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
const
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
completedCleanly: false,
|
|
3001
|
-
errored: false
|
|
3002
|
-
};
|
|
3003
|
-
let stop2 = () => {
|
|
3004
|
-
};
|
|
3005
|
-
const result2 = {
|
|
3006
|
-
get values() {
|
|
3007
|
-
return acc.values;
|
|
3008
|
-
},
|
|
3009
|
-
get dirtyCount() {
|
|
3010
|
-
return acc.dirtyCount;
|
|
3011
|
-
},
|
|
3012
|
-
get resolvedCount() {
|
|
3013
|
-
return acc.resolvedCount;
|
|
3014
|
-
},
|
|
3015
|
-
get events() {
|
|
3016
|
-
return acc.events;
|
|
3017
|
-
},
|
|
3018
|
-
get completedCleanly() {
|
|
3019
|
-
return acc.completedCleanly;
|
|
3020
|
-
},
|
|
3021
|
-
get errored() {
|
|
3022
|
-
return acc.errored;
|
|
3023
|
-
},
|
|
3024
|
-
dispose() {
|
|
3025
|
-
stop2();
|
|
3026
|
-
},
|
|
3027
|
-
expand() {
|
|
3028
|
-
throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
|
|
3029
|
-
}
|
|
3030
|
-
};
|
|
3031
|
-
const pushEvent = (path, message) => {
|
|
3032
|
-
const t = message[0];
|
|
3033
|
-
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
|
|
3034
|
-
let event;
|
|
3035
|
-
if (t === DATA) {
|
|
3036
|
-
if (path != null) acc.values[path] = message[1];
|
|
3037
|
-
event = { type: "data", ...path != null ? { path } : {}, data: message[1], ...base };
|
|
3038
|
-
} else if (t === DIRTY) {
|
|
3039
|
-
acc.dirtyCount += 1;
|
|
3040
|
-
event = { type: "dirty", ...path != null ? { path } : {}, ...base };
|
|
3041
|
-
} else if (t === RESOLVED) {
|
|
3042
|
-
acc.resolvedCount += 1;
|
|
3043
|
-
event = { type: "resolved", ...path != null ? { path } : {}, ...base };
|
|
3044
|
-
} else if (t === COMPLETE) {
|
|
3045
|
-
if (!acc.errored) acc.completedCleanly = true;
|
|
3046
|
-
event = { type: "complete", ...path != null ? { path } : {}, ...base };
|
|
3047
|
-
} else if (t === ERROR) {
|
|
3048
|
-
acc.errored = true;
|
|
3049
|
-
event = {
|
|
3050
|
-
type: "error",
|
|
3051
|
-
...path != null ? { path } : {},
|
|
3052
|
-
data: message[1],
|
|
3053
|
-
...base
|
|
3054
|
-
};
|
|
3291
|
+
let cursor = 0;
|
|
3292
|
+
const flush = () => {
|
|
3293
|
+
const events = result.events;
|
|
3294
|
+
while (cursor < events.length) {
|
|
3295
|
+
const event = events[cursor++];
|
|
3296
|
+
if (shouldLog(event.type)) {
|
|
3297
|
+
logger(renderEvent(event), event);
|
|
3055
3298
|
}
|
|
3056
|
-
if (!event) return;
|
|
3057
|
-
acc.events.push(event);
|
|
3058
|
-
if (!shouldLog(event.type)) return;
|
|
3059
|
-
logger(renderEvent(event), event);
|
|
3060
|
-
};
|
|
3061
|
-
if (options.path != null) {
|
|
3062
|
-
const stream2 = this.observe(options.path, {
|
|
3063
|
-
actor: options.actor,
|
|
3064
|
-
structured: false
|
|
3065
|
-
});
|
|
3066
|
-
stop2 = stream2.subscribe((messages) => {
|
|
3067
|
-
for (const m of messages) {
|
|
3068
|
-
pushEvent(options.path, m);
|
|
3069
|
-
}
|
|
3070
|
-
});
|
|
3071
|
-
} else {
|
|
3072
|
-
const stream2 = this.observe({ actor: options.actor, structured: false });
|
|
3073
|
-
stop2 = stream2.subscribe((path, messages) => {
|
|
3074
|
-
for (const m of messages) {
|
|
3075
|
-
pushEvent(path, m);
|
|
3076
|
-
}
|
|
3077
|
-
});
|
|
3078
3299
|
}
|
|
3079
|
-
return {
|
|
3080
|
-
result: result2,
|
|
3081
|
-
dispose() {
|
|
3082
|
-
result2.dispose();
|
|
3083
|
-
}
|
|
3084
|
-
};
|
|
3085
|
-
}
|
|
3086
|
-
const structuredObserveOptions = {
|
|
3087
|
-
actor: options.actor,
|
|
3088
|
-
structured: true,
|
|
3089
|
-
...options.timeline !== false ? { timeline: true } : {},
|
|
3090
|
-
...options.causal ? { causal: true } : {},
|
|
3091
|
-
...options.derived ? { derived: true } : {}
|
|
3092
3300
|
};
|
|
3093
|
-
const
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
for (const event of nextEvents) {
|
|
3099
|
-
if (!shouldLog(event.type)) continue;
|
|
3100
|
-
logger(renderEvent(event), event);
|
|
3101
|
-
}
|
|
3301
|
+
const origPush = result.events.push;
|
|
3302
|
+
result.events.push = function(...items) {
|
|
3303
|
+
const ret = origPush.apply(this, items);
|
|
3304
|
+
flush();
|
|
3305
|
+
return ret;
|
|
3102
3306
|
};
|
|
3103
|
-
const
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
}
|
|
3108
|
-
}) : stream.subscribe((_path, messages) => {
|
|
3109
|
-
if (messages.length > 0) {
|
|
3110
|
-
flushNewEvents();
|
|
3111
|
-
}
|
|
3112
|
-
});
|
|
3113
|
-
return {
|
|
3114
|
-
result,
|
|
3115
|
-
dispose() {
|
|
3116
|
-
stop();
|
|
3117
|
-
flushNewEvents();
|
|
3118
|
-
result.dispose();
|
|
3119
|
-
}
|
|
3307
|
+
const origDispose = result.dispose.bind(result);
|
|
3308
|
+
result.dispose = () => {
|
|
3309
|
+
origDispose();
|
|
3310
|
+
flush();
|
|
3120
3311
|
};
|
|
3121
3312
|
}
|
|
3122
3313
|
/**
|
|
@@ -3379,8 +3570,9 @@ var Graph = class _Graph {
|
|
|
3379
3570
|
/**
|
|
3380
3571
|
* Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
|
|
3381
3572
|
*
|
|
3382
|
-
* Checkpoint trigger uses {@link messageTier}: only batches containing tier >=
|
|
3383
|
-
* 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`).
|
|
3384
3576
|
*/
|
|
3385
3577
|
autoCheckpoint(adapter, options = {}) {
|
|
3386
3578
|
const debounceMs = Math.max(0, options.debounceMs ?? 500);
|
|
@@ -3427,7 +3619,7 @@ var Graph = class _Graph {
|
|
|
3427
3619
|
timer = setTimeout(flush, debounceMs);
|
|
3428
3620
|
};
|
|
3429
3621
|
const off = this.observe().subscribe((path, messages) => {
|
|
3430
|
-
const triggeredByTier = messages.some((m) => messageTier(m[0]) >=
|
|
3622
|
+
const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
|
|
3431
3623
|
if (!triggeredByTier) return;
|
|
3432
3624
|
if (options.filter) {
|
|
3433
3625
|
const nd = this.resolve(path);
|
|
@@ -3511,33 +3703,21 @@ var Graph = class _Graph {
|
|
|
3511
3703
|
// ——————————————————————————————————————————————————————————————
|
|
3512
3704
|
/**
|
|
3513
3705
|
* When `false`, structured observation options (`causal`, `timeline`),
|
|
3514
|
-
*
|
|
3706
|
+
* and `trace()` writes are no-ops. Raw `observe()` always works.
|
|
3515
3707
|
*
|
|
3516
3708
|
* Default: `true` outside production (`process.env.NODE_ENV !== "production"`).
|
|
3517
3709
|
*/
|
|
3518
3710
|
static inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
|
|
3519
3711
|
_annotations = /* @__PURE__ */ new Map();
|
|
3520
3712
|
_traceRing = new RingBuffer(1e3);
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
annotate(path, reason) {
|
|
3530
|
-
if (!_Graph.inspectorEnabled) return;
|
|
3531
|
-
this.resolve(path);
|
|
3532
|
-
this._annotations.set(path, reason);
|
|
3533
|
-
this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
|
|
3534
|
-
}
|
|
3535
|
-
/**
|
|
3536
|
-
* Returns a chronological log of all reasoning annotations (ring buffer).
|
|
3537
|
-
*
|
|
3538
|
-
* @returns `[]` when {@link Graph.inspectorEnabled} is `false`.
|
|
3539
|
-
*/
|
|
3540
|
-
traceLog() {
|
|
3713
|
+
trace(path, reason) {
|
|
3714
|
+
if (path != null && reason != null) {
|
|
3715
|
+
if (!_Graph.inspectorEnabled) return;
|
|
3716
|
+
this.resolve(path);
|
|
3717
|
+
this._annotations.set(path, reason);
|
|
3718
|
+
this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3541
3721
|
if (!_Graph.inspectorEnabled) return [];
|
|
3542
3722
|
return this._traceRing.toArray();
|
|
3543
3723
|
}
|