@graphrefly/graphrefly 0.18.0 → 0.20.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-J7S54G7I.js → chunk-2L5J6RPM.js} +7 -2
- package/dist/chunk-2L5J6RPM.js.map +1 -0
- package/dist/{chunk-LB3RYLSC.js → chunk-3N2Y6PCR.js} +197 -42
- package/dist/chunk-3N2Y6PCR.js.map +1 -0
- package/dist/{chunk-KJGUP35I.js → chunk-5PSVTDNZ.js} +22 -9
- package/dist/chunk-5PSVTDNZ.js.map +1 -0
- package/dist/{chunk-76YPZQTW.js → chunk-BJAOEU4D.js} +34 -29
- package/dist/chunk-BJAOEU4D.js.map +1 -0
- package/dist/{chunk-UVWEKTYC.js → chunk-IAPLC4NR.js} +3 -3
- package/dist/{chunk-F6ORUNO7.js → chunk-OOA2UTXF.js} +58 -2
- package/dist/chunk-OOA2UTXF.js.map +1 -0
- package/dist/{chunk-TNKODJ6E.js → chunk-PGEU5MEH.js} +7 -3
- package/dist/{chunk-TNKODJ6E.js.map → chunk-PGEU5MEH.js.map} +1 -1
- package/dist/chunk-R2LPZIY2.js +111 -0
- package/dist/chunk-R2LPZIY2.js.map +1 -0
- package/dist/{chunk-BV3TPSBK.js → chunk-XYL3GLB3.js} +742 -757
- package/dist/chunk-XYL3GLB3.js.map +1 -0
- package/dist/compat/nestjs/index.cjs +967 -811
- package/dist/compat/nestjs/index.cjs.map +1 -1
- package/dist/compat/nestjs/index.d.cts +4 -4
- package/dist/compat/nestjs/index.d.ts +4 -4
- package/dist/compat/nestjs/index.js +7 -7
- package/dist/core/index.cjs +653 -666
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +7 -3
- package/dist/extra/index.cjs +728 -688
- 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 +9 -5
- package/dist/graph/index.cjs +836 -808
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +3 -3
- package/dist/graph/index.d.ts +3 -3
- package/dist/graph/index.js +8 -8
- package/dist/{graph-gISB9n3n.d.ts → graph-KsTe57nI.d.cts} +82 -8
- package/dist/{graph-BYFlyNpX.d.cts → graph-mILUUqW8.d.ts} +82 -8
- package/dist/{index-CgKPpiu8.d.ts → index-8a605sg9.d.ts} +2 -2
- package/dist/{index-DKaB2x0T.d.ts → index-B2SvPEbc.d.ts} +6 -65
- package/dist/{index-EmzYk-TG.d.cts → index-BHfg_Ez3.d.ts} +123 -77
- package/dist/{index-B80mMeuf.d.ts → index-Bc_diYYJ.d.cts} +123 -77
- package/dist/{index-B43mC7uY.d.cts → index-BjtlNirP.d.cts} +3 -3
- package/dist/{index-CEDaJaYE.d.ts → index-CgSiUouz.d.ts} +3 -3
- package/dist/{index-7WnwgjMu.d.ts → index-DuN3bhtm.d.ts} +82 -32
- package/dist/{index-D_tUMcpz.d.cts → index-SFzE_KTa.d.cts} +82 -32
- package/dist/{index-Ci_vPaVm.d.cts → index-UudxGnzc.d.cts} +6 -65
- package/dist/{index-BqOWSFhr.d.cts → index-VHA43cGP.d.cts} +2 -2
- package/dist/index.cjs +5936 -5522
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +644 -379
- package/dist/index.d.ts +644 -379
- package/dist/index.js +4388 -4058
- package/dist/index.js.map +1 -1
- package/dist/{meta-npl5b97j.d.cts → meta-BnG7XAaE.d.cts} +394 -236
- package/dist/{meta-npl5b97j.d.ts → meta-BnG7XAaE.d.ts} +394 -236
- package/dist/{observable-DFBCBELR.d.cts → observable-C8Kx_O6k.d.cts} +1 -1
- package/dist/{observable-oAGygKvc.d.ts → observable-DcBwQY7t.d.ts} +1 -1
- package/dist/patterns/reactive-layout/index.cjs +865 -718
- package/dist/patterns/reactive-layout/index.cjs.map +1 -1
- package/dist/patterns/reactive-layout/index.d.cts +3 -3
- package/dist/patterns/reactive-layout/index.d.ts +3 -3
- package/dist/patterns/reactive-layout/index.js +4 -4
- package/package.json +2 -2
- package/dist/chunk-76YPZQTW.js.map +0 -1
- package/dist/chunk-BV3TPSBK.js.map +0 -1
- package/dist/chunk-F6ORUNO7.js.map +0 -1
- package/dist/chunk-FCLROC4Q.js +0 -231
- package/dist/chunk-FCLROC4Q.js.map +0 -1
- package/dist/chunk-J7S54G7I.js.map +0 -1
- package/dist/chunk-KJGUP35I.js.map +0 -1
- package/dist/chunk-LB3RYLSC.js.map +0 -1
- /package/dist/{chunk-UVWEKTYC.js.map → chunk-IAPLC4NR.js.map} +0 -0
package/dist/core/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(core_exports, {
|
|
|
34
34
|
RESOLVED: () => RESOLVED,
|
|
35
35
|
RESUME: () => RESUME,
|
|
36
36
|
ResettableTimer: () => ResettableTimer,
|
|
37
|
+
START: () => START,
|
|
37
38
|
TEARDOWN: () => TEARDOWN,
|
|
38
39
|
accessHintForGuard: () => accessHintForGuard,
|
|
39
40
|
advanceVersion: () => advanceVersion,
|
|
@@ -48,6 +49,7 @@ __export(core_exports, {
|
|
|
48
49
|
effect: () => effect,
|
|
49
50
|
isBatching: () => isBatching,
|
|
50
51
|
isKnownMessageType: () => isKnownMessageType,
|
|
52
|
+
isLocalOnly: () => isLocalOnly,
|
|
51
53
|
isPhase2Message: () => isPhase2Message,
|
|
52
54
|
isTerminalMessage: () => isTerminalMessage,
|
|
53
55
|
isV1: () => isV1,
|
|
@@ -81,6 +83,7 @@ function normalizeActor(actor) {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// src/core/messages.ts
|
|
86
|
+
var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
|
|
84
87
|
var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
|
|
85
88
|
var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
|
|
86
89
|
var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
|
|
@@ -91,6 +94,7 @@ var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
|
|
|
91
94
|
var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
|
|
92
95
|
var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
|
|
93
96
|
var knownMessageTypes = [
|
|
97
|
+
START,
|
|
94
98
|
DATA,
|
|
95
99
|
DIRTY,
|
|
96
100
|
RESOLVED,
|
|
@@ -101,16 +105,18 @@ var knownMessageTypes = [
|
|
|
101
105
|
COMPLETE,
|
|
102
106
|
ERROR
|
|
103
107
|
];
|
|
108
|
+
var knownMessageSet = new Set(knownMessageTypes);
|
|
104
109
|
function isKnownMessageType(t) {
|
|
105
|
-
return
|
|
110
|
+
return knownMessageSet.has(t);
|
|
106
111
|
}
|
|
107
112
|
function messageTier(t) {
|
|
108
|
-
if (t ===
|
|
109
|
-
if (t ===
|
|
110
|
-
if (t ===
|
|
111
|
-
if (t ===
|
|
112
|
-
if (t ===
|
|
113
|
-
return
|
|
113
|
+
if (t === START) return 0;
|
|
114
|
+
if (t === DIRTY || t === INVALIDATE) return 1;
|
|
115
|
+
if (t === PAUSE || t === RESUME) return 2;
|
|
116
|
+
if (t === DATA || t === RESOLVED) return 3;
|
|
117
|
+
if (t === COMPLETE || t === ERROR) return 4;
|
|
118
|
+
if (t === TEARDOWN) return 5;
|
|
119
|
+
return 1;
|
|
114
120
|
}
|
|
115
121
|
function isPhase2Message(msg) {
|
|
116
122
|
const t = msg[0];
|
|
@@ -119,6 +125,10 @@ function isPhase2Message(msg) {
|
|
|
119
125
|
function isTerminalMessage(t) {
|
|
120
126
|
return t === COMPLETE || t === ERROR;
|
|
121
127
|
}
|
|
128
|
+
function isLocalOnly(t) {
|
|
129
|
+
if (!knownMessageSet.has(t)) return false;
|
|
130
|
+
return messageTier(t) < 3;
|
|
131
|
+
}
|
|
122
132
|
function propagatesToMeta(t) {
|
|
123
133
|
return t === TEARDOWN;
|
|
124
134
|
}
|
|
@@ -279,14 +289,14 @@ function _downSequential(sink, messages, phase = 2) {
|
|
|
279
289
|
const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
|
|
280
290
|
for (const msg of messages) {
|
|
281
291
|
const tier = messageTier(msg[0]);
|
|
282
|
-
if (tier ===
|
|
292
|
+
if (tier === 3) {
|
|
283
293
|
if (isBatching()) {
|
|
284
294
|
const m = msg;
|
|
285
295
|
dataQueue.push(() => sink([m]));
|
|
286
296
|
} else {
|
|
287
297
|
sink([msg]);
|
|
288
298
|
}
|
|
289
|
-
} else if (tier >=
|
|
299
|
+
} else if (tier >= 4) {
|
|
290
300
|
if (isBatching()) {
|
|
291
301
|
const m = msg;
|
|
292
302
|
pendingPhase3.push(() => sink([m]));
|
|
@@ -299,14 +309,6 @@ function _downSequential(sink, messages, phase = 2) {
|
|
|
299
309
|
}
|
|
300
310
|
}
|
|
301
311
|
|
|
302
|
-
// src/core/clock.ts
|
|
303
|
-
function monotonicNs() {
|
|
304
|
-
return Math.trunc(performance.now() * 1e6);
|
|
305
|
-
}
|
|
306
|
-
function wallClockNs() {
|
|
307
|
-
return Date.now() * 1e6;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
312
|
// src/core/guard.ts
|
|
311
313
|
var GuardDenied = class extends Error {
|
|
312
314
|
actor;
|
|
@@ -405,6 +407,14 @@ function accessHintForGuard(guard) {
|
|
|
405
407
|
return allowed.join("+");
|
|
406
408
|
}
|
|
407
409
|
|
|
410
|
+
// src/core/clock.ts
|
|
411
|
+
function monotonicNs() {
|
|
412
|
+
return Math.trunc(performance.now() * 1e6);
|
|
413
|
+
}
|
|
414
|
+
function wallClockNs() {
|
|
415
|
+
return Date.now() * 1e6;
|
|
416
|
+
}
|
|
417
|
+
|
|
408
418
|
// src/core/versioning.ts
|
|
409
419
|
var import_node_crypto = require("crypto");
|
|
410
420
|
function canonicalizeForHash(value) {
|
|
@@ -460,10 +470,29 @@ function isV1(info) {
|
|
|
460
470
|
return "cid" in info;
|
|
461
471
|
}
|
|
462
472
|
|
|
463
|
-
// src/core/node.ts
|
|
473
|
+
// src/core/node-base.ts
|
|
464
474
|
var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
|
|
465
475
|
var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
|
|
466
|
-
function
|
|
476
|
+
function cleanupResult(cleanup, ...args) {
|
|
477
|
+
const r = { [CLEANUP_RESULT]: true, cleanup };
|
|
478
|
+
if (args.length > 0) r.value = args[0];
|
|
479
|
+
return r;
|
|
480
|
+
}
|
|
481
|
+
var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
|
|
482
|
+
var isCleanupFn = (value) => typeof value === "function";
|
|
483
|
+
function statusAfterMessage(status, msg) {
|
|
484
|
+
const t = msg[0];
|
|
485
|
+
if (t === DIRTY) return "dirty";
|
|
486
|
+
if (t === DATA) return "settled";
|
|
487
|
+
if (t === RESOLVED) return "resolved";
|
|
488
|
+
if (t === COMPLETE) return "completed";
|
|
489
|
+
if (t === ERROR) return "errored";
|
|
490
|
+
if (t === INVALIDATE) return "dirty";
|
|
491
|
+
if (t === TEARDOWN) return "disconnected";
|
|
492
|
+
return status;
|
|
493
|
+
}
|
|
494
|
+
function createIntBitSet(size) {
|
|
495
|
+
const fullMask = size >= 32 ? -1 : ~(-1 << size);
|
|
467
496
|
let bits = 0;
|
|
468
497
|
return {
|
|
469
498
|
set(i) {
|
|
@@ -476,7 +505,8 @@ function createIntBitSet() {
|
|
|
476
505
|
return (bits & 1 << i) !== 0;
|
|
477
506
|
},
|
|
478
507
|
covers(other) {
|
|
479
|
-
|
|
508
|
+
const otherBits = other._bits();
|
|
509
|
+
return (bits & otherBits) === otherBits;
|
|
480
510
|
},
|
|
481
511
|
any() {
|
|
482
512
|
return bits !== 0;
|
|
@@ -484,6 +514,9 @@ function createIntBitSet() {
|
|
|
484
514
|
reset() {
|
|
485
515
|
bits = 0;
|
|
486
516
|
},
|
|
517
|
+
setAll() {
|
|
518
|
+
bits = fullMask;
|
|
519
|
+
},
|
|
487
520
|
_bits() {
|
|
488
521
|
return bits;
|
|
489
522
|
}
|
|
@@ -491,6 +524,8 @@ function createIntBitSet() {
|
|
|
491
524
|
}
|
|
492
525
|
function createArrayBitSet(size) {
|
|
493
526
|
const words = new Uint32Array(Math.ceil(size / 32));
|
|
527
|
+
const lastBits = size % 32;
|
|
528
|
+
const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
|
|
494
529
|
return {
|
|
495
530
|
set(i) {
|
|
496
531
|
words[i >>> 5] |= 1 << (i & 31);
|
|
@@ -517,135 +552,103 @@ function createArrayBitSet(size) {
|
|
|
517
552
|
reset() {
|
|
518
553
|
words.fill(0);
|
|
519
554
|
},
|
|
555
|
+
setAll() {
|
|
556
|
+
for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
|
|
557
|
+
if (words.length > 0) words[words.length - 1] = lastWordMask;
|
|
558
|
+
},
|
|
520
559
|
_words: words
|
|
521
560
|
};
|
|
522
561
|
}
|
|
523
562
|
function createBitSet(size) {
|
|
524
|
-
return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
|
|
525
|
-
}
|
|
526
|
-
var isNodeArray = (value) => Array.isArray(value);
|
|
527
|
-
var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
|
|
528
|
-
function cleanupResult(cleanup, ...args) {
|
|
529
|
-
const r = { [CLEANUP_RESULT]: true, cleanup };
|
|
530
|
-
if (args.length > 0) r.value = args[0];
|
|
531
|
-
return r;
|
|
563
|
+
return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
|
|
532
564
|
}
|
|
533
|
-
var
|
|
534
|
-
|
|
535
|
-
var statusAfterMessage = (status, msg) => {
|
|
536
|
-
const t = msg[0];
|
|
537
|
-
if (t === DIRTY) return "dirty";
|
|
538
|
-
if (t === DATA) return "settled";
|
|
539
|
-
if (t === RESOLVED) return "resolved";
|
|
540
|
-
if (t === COMPLETE) return "completed";
|
|
541
|
-
if (t === ERROR) return "errored";
|
|
542
|
-
if (t === INVALIDATE) return "dirty";
|
|
543
|
-
if (t === TEARDOWN) return "disconnected";
|
|
544
|
-
return status;
|
|
545
|
-
};
|
|
546
|
-
var NodeImpl = class {
|
|
547
|
-
// --- Configuration (set once, never reassigned) ---
|
|
565
|
+
var NodeBase = class {
|
|
566
|
+
// --- Identity (set once) ---
|
|
548
567
|
_optsName;
|
|
549
568
|
_registryName;
|
|
550
|
-
/** @internal
|
|
569
|
+
/** @internal Read by `describeNode` before inference. */
|
|
551
570
|
_describeKind;
|
|
552
571
|
meta;
|
|
553
|
-
|
|
554
|
-
_fn;
|
|
555
|
-
_opts;
|
|
572
|
+
// --- Options ---
|
|
556
573
|
_equals;
|
|
574
|
+
_resubscribable;
|
|
575
|
+
_resetOnTeardown;
|
|
576
|
+
_onResubscribe;
|
|
557
577
|
_onMessage;
|
|
558
|
-
/** @internal
|
|
578
|
+
/** @internal Read by `describeNode` for `accessHintForGuard`. */
|
|
559
579
|
_guard;
|
|
580
|
+
/** @internal Subclasses update this through {@link _recordMutation}. */
|
|
560
581
|
_lastMutation;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
// ---
|
|
582
|
+
// --- Versioning ---
|
|
583
|
+
_hashFn;
|
|
584
|
+
_versioning;
|
|
585
|
+
// --- Lifecycle state ---
|
|
586
|
+
/** @internal Read by `describeNode` and `graph.ts`. */
|
|
565
587
|
_cached;
|
|
588
|
+
/** @internal Read externally via `get status()`. */
|
|
566
589
|
_status;
|
|
567
590
|
_terminal = false;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
_manualEmitUsed = false;
|
|
591
|
+
_active = false;
|
|
592
|
+
// --- Sink storage ---
|
|
593
|
+
/** @internal Read by `graph/profile.ts` for subscriber counts. */
|
|
572
594
|
_sinkCount = 0;
|
|
573
595
|
_singleDepSinkCount = 0;
|
|
574
|
-
// --- Object/collection state ---
|
|
575
|
-
_depDirtyMask;
|
|
576
|
-
_depSettledMask;
|
|
577
|
-
_depCompleteMask;
|
|
578
|
-
_allDepsCompleteMask;
|
|
579
|
-
_lastDepValues;
|
|
580
|
-
_cleanup;
|
|
581
|
-
_sinks = null;
|
|
582
596
|
_singleDepSinks = /* @__PURE__ */ new WeakSet();
|
|
583
|
-
|
|
597
|
+
_sinks = null;
|
|
598
|
+
// --- Actions + bound helpers ---
|
|
584
599
|
_actions;
|
|
585
600
|
_boundDownToSinks;
|
|
601
|
+
// --- Inspector hook (Graph observability) ---
|
|
586
602
|
_inspectorHook;
|
|
587
|
-
|
|
588
|
-
_hashFn;
|
|
589
|
-
constructor(deps, fn, opts) {
|
|
590
|
-
this._deps = deps;
|
|
591
|
-
this._fn = fn;
|
|
592
|
-
this._opts = opts;
|
|
603
|
+
constructor(opts) {
|
|
593
604
|
this._optsName = opts.name;
|
|
594
605
|
this._describeKind = opts.describeKind;
|
|
595
606
|
this._equals = opts.equals ?? Object.is;
|
|
607
|
+
this._resubscribable = opts.resubscribable ?? false;
|
|
608
|
+
this._resetOnTeardown = opts.resetOnTeardown ?? false;
|
|
609
|
+
this._onResubscribe = opts.onResubscribe;
|
|
596
610
|
this._onMessage = opts.onMessage;
|
|
597
611
|
this._guard = opts.guard;
|
|
598
|
-
this._hasDeps = deps.length > 0;
|
|
599
|
-
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
600
|
-
this._isSingleDep = deps.length === 1 && fn != null;
|
|
601
612
|
this._cached = "initial" in opts ? opts.initial : NO_VALUE;
|
|
602
|
-
this._status =
|
|
613
|
+
this._status = "disconnected";
|
|
603
614
|
this._hashFn = opts.versioningHash ?? defaultHash;
|
|
604
615
|
this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
|
|
605
616
|
id: opts.versioningId,
|
|
606
617
|
hash: this._hashFn
|
|
607
618
|
}) : void 0;
|
|
608
|
-
this._depDirtyMask = createBitSet(deps.length);
|
|
609
|
-
this._depSettledMask = createBitSet(deps.length);
|
|
610
|
-
this._depCompleteMask = createBitSet(deps.length);
|
|
611
|
-
this._allDepsCompleteMask = createBitSet(deps.length);
|
|
612
|
-
for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
|
|
613
619
|
const meta = {};
|
|
614
620
|
for (const [k, v] of Object.entries(opts.meta ?? {})) {
|
|
615
|
-
meta[k] =
|
|
616
|
-
initial: v,
|
|
617
|
-
name: `${opts.name ?? "node"}:meta:${k}`,
|
|
618
|
-
describeKind: "state",
|
|
619
|
-
...opts.guard != null ? { guard: opts.guard } : {}
|
|
620
|
-
});
|
|
621
|
+
meta[k] = this._createMetaNode(k, v, opts);
|
|
621
622
|
}
|
|
622
623
|
Object.freeze(meta);
|
|
623
624
|
this.meta = meta;
|
|
624
625
|
const self = this;
|
|
625
626
|
this._actions = {
|
|
626
627
|
down(messages) {
|
|
627
|
-
self.
|
|
628
|
+
self._onManualEmit();
|
|
628
629
|
self._downInternal(messages);
|
|
629
630
|
},
|
|
630
631
|
emit(value) {
|
|
631
|
-
self.
|
|
632
|
+
self._onManualEmit();
|
|
632
633
|
self._downAutoValue(value);
|
|
633
634
|
},
|
|
634
635
|
up(messages) {
|
|
635
636
|
self._upInternal(messages);
|
|
636
637
|
}
|
|
637
638
|
};
|
|
638
|
-
this.down = this.down.bind(this);
|
|
639
|
-
this.up = this.up.bind(this);
|
|
640
639
|
this._boundDownToSinks = this._downToSinks.bind(this);
|
|
641
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
|
|
643
|
+
* {@link NodeImpl} overrides to set `_manualEmitUsed`.
|
|
644
|
+
*/
|
|
645
|
+
_onManualEmit() {
|
|
646
|
+
}
|
|
647
|
+
// --- Identity getters ---
|
|
642
648
|
get name() {
|
|
643
649
|
return this._registryName ?? this._optsName;
|
|
644
650
|
}
|
|
645
|
-
/**
|
|
646
|
-
* When a node is registered with {@link Graph.add} without an options `name`,
|
|
647
|
-
* the graph assigns the registry local name for introspection (parity with graphrefly-py).
|
|
648
|
-
*/
|
|
651
|
+
/** @internal Assigned by `Graph.add` when registered without an options `name`. */
|
|
649
652
|
_assignRegistryName(localName) {
|
|
650
653
|
if (this._optsName !== void 0 || this._registryName !== void 0) return;
|
|
651
654
|
this._registryName = localName;
|
|
@@ -663,7 +666,10 @@ var NodeImpl = class {
|
|
|
663
666
|
}
|
|
664
667
|
};
|
|
665
668
|
}
|
|
666
|
-
|
|
669
|
+
/** @internal Used by subclasses to surface inspector events. */
|
|
670
|
+
_emitInspectorHook(event) {
|
|
671
|
+
this._inspectorHook?.(event);
|
|
672
|
+
}
|
|
667
673
|
get status() {
|
|
668
674
|
return this._status;
|
|
669
675
|
}
|
|
@@ -673,15 +679,7 @@ var NodeImpl = class {
|
|
|
673
679
|
get v() {
|
|
674
680
|
return this._versioning;
|
|
675
681
|
}
|
|
676
|
-
/**
|
|
677
|
-
* Retroactively apply versioning to a node that was created without it.
|
|
678
|
-
* No-op if versioning is already enabled.
|
|
679
|
-
*
|
|
680
|
-
* Version starts at 0 regardless of prior DATA emissions — it tracks
|
|
681
|
-
* changes from the moment versioning is enabled, not historical ones.
|
|
682
|
-
*
|
|
683
|
-
* @internal — used by {@link Graph.setVersioning}.
|
|
684
|
-
*/
|
|
682
|
+
/** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
|
|
685
683
|
_applyVersioning(level, opts) {
|
|
686
684
|
if (this._versioning != null) return;
|
|
687
685
|
this._hashFn = opts?.hash ?? this._hashFn;
|
|
@@ -701,6 +699,7 @@ var NodeImpl = class {
|
|
|
701
699
|
if (this._guard == null) return true;
|
|
702
700
|
return this._guard(normalizeActor(actor), "observe");
|
|
703
701
|
}
|
|
702
|
+
// --- Public transport ---
|
|
704
703
|
get() {
|
|
705
704
|
return this._cached === NO_VALUE ? void 0 : this._cached;
|
|
706
705
|
}
|
|
@@ -713,43 +712,25 @@ var NodeImpl = class {
|
|
|
713
712
|
if (!this._guard(actor, action)) {
|
|
714
713
|
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
715
714
|
}
|
|
716
|
-
this.
|
|
715
|
+
this._recordMutation(actor);
|
|
717
716
|
}
|
|
718
717
|
this._downInternal(messages);
|
|
719
718
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
this.
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
734
|
-
const t = sinkMessages[i][0];
|
|
735
|
-
if (t === DATA || t === RESOLVED) {
|
|
736
|
-
hasPhase2 = true;
|
|
737
|
-
break;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
if (hasPhase2) {
|
|
741
|
-
const filtered = [];
|
|
742
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
743
|
-
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
744
|
-
}
|
|
745
|
-
if (filtered.length > 0) {
|
|
746
|
-
downWithBatch(this._boundDownToSinks, filtered);
|
|
747
|
-
}
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
719
|
+
/** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
|
|
720
|
+
_recordMutation(actor) {
|
|
721
|
+
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* At-most-once deactivation guard. Both TEARDOWN (eager) and
|
|
725
|
+
* unsubscribe-body (lazy) call this. The `_active` flag ensures
|
|
726
|
+
* `_doDeactivate` runs exactly once per activation cycle.
|
|
727
|
+
*/
|
|
728
|
+
_onDeactivate() {
|
|
729
|
+
if (!this._active) return;
|
|
730
|
+
this._active = false;
|
|
731
|
+
this._doDeactivate();
|
|
752
732
|
}
|
|
733
|
+
// --- Subscribe (uniform across node shapes) ---
|
|
753
734
|
subscribe(sink, hints) {
|
|
754
735
|
if (hints?.actor != null && this._guard != null) {
|
|
755
736
|
const actor = normalizeActor(hints.actor);
|
|
@@ -757,17 +738,21 @@ var NodeImpl = class {
|
|
|
757
738
|
throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
|
|
758
739
|
}
|
|
759
740
|
}
|
|
760
|
-
if (this._terminal && this.
|
|
741
|
+
if (this._terminal && this._resubscribable) {
|
|
761
742
|
this._terminal = false;
|
|
762
743
|
this._cached = NO_VALUE;
|
|
763
|
-
this._status =
|
|
764
|
-
this.
|
|
744
|
+
this._status = "disconnected";
|
|
745
|
+
this._onResubscribe?.();
|
|
765
746
|
}
|
|
766
747
|
this._sinkCount += 1;
|
|
767
748
|
if (hints?.singleDep) {
|
|
768
749
|
this._singleDepSinkCount += 1;
|
|
769
750
|
this._singleDepSinks.add(sink);
|
|
770
751
|
}
|
|
752
|
+
if (!this._terminal) {
|
|
753
|
+
const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
|
|
754
|
+
downWithBatch(sink, startMessages);
|
|
755
|
+
}
|
|
771
756
|
if (this._sinks == null) {
|
|
772
757
|
this._sinks = sink;
|
|
773
758
|
} else if (typeof this._sinks === "function") {
|
|
@@ -775,10 +760,12 @@ var NodeImpl = class {
|
|
|
775
760
|
} else {
|
|
776
761
|
this._sinks.add(sink);
|
|
777
762
|
}
|
|
778
|
-
if (this.
|
|
779
|
-
this.
|
|
780
|
-
|
|
781
|
-
|
|
763
|
+
if (this._sinkCount === 1 && !this._terminal) {
|
|
764
|
+
this._active = true;
|
|
765
|
+
this._onActivate();
|
|
766
|
+
}
|
|
767
|
+
if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
|
|
768
|
+
this._status = "pending";
|
|
782
769
|
}
|
|
783
770
|
let removed = false;
|
|
784
771
|
return () => {
|
|
@@ -802,39 +789,49 @@ var NodeImpl = class {
|
|
|
802
789
|
}
|
|
803
790
|
}
|
|
804
791
|
if (this._sinks == null) {
|
|
805
|
-
this.
|
|
806
|
-
this._stopProducer();
|
|
792
|
+
this._onDeactivate();
|
|
807
793
|
}
|
|
808
794
|
};
|
|
809
795
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
796
|
+
// --- Down pipeline ---
|
|
797
|
+
/**
|
|
798
|
+
* Core outgoing dispatch. Applies terminal filter + local lifecycle
|
|
799
|
+
* update, then hands messages to `downWithBatch` for tier-aware delivery.
|
|
800
|
+
*/
|
|
801
|
+
_downInternal(messages) {
|
|
802
|
+
if (messages.length === 0) return;
|
|
803
|
+
let sinkMessages = messages;
|
|
804
|
+
if (this._terminal && !this._resubscribable) {
|
|
805
|
+
const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
806
|
+
if (pass.length === 0) return;
|
|
807
|
+
sinkMessages = pass;
|
|
818
808
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
809
|
+
this._handleLocalLifecycle(sinkMessages);
|
|
810
|
+
if (this._canSkipDirty()) {
|
|
811
|
+
let hasPhase2 = false;
|
|
812
|
+
for (let i = 0; i < sinkMessages.length; i++) {
|
|
813
|
+
const t = sinkMessages[i][0];
|
|
814
|
+
if (t === DATA || t === RESOLVED) {
|
|
815
|
+
hasPhase2 = true;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (hasPhase2) {
|
|
820
|
+
const filtered = [];
|
|
821
|
+
for (let i = 0; i < sinkMessages.length; i++) {
|
|
822
|
+
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
823
|
+
}
|
|
824
|
+
if (filtered.length > 0) {
|
|
825
|
+
downWithBatch(this._boundDownToSinks, filtered);
|
|
826
|
+
}
|
|
827
|
+
return;
|
|
824
828
|
}
|
|
825
829
|
}
|
|
830
|
+
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
826
831
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
for (const dep of this._deps) {
|
|
830
|
-
dep.up?.(messages, { internal: true });
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
unsubscribe() {
|
|
834
|
-
if (!this._hasDeps) return;
|
|
835
|
-
this._disconnectUpstream();
|
|
832
|
+
_canSkipDirty() {
|
|
833
|
+
return this._sinkCount === 1 && this._singleDepSinkCount === 1;
|
|
836
834
|
}
|
|
837
|
-
// --- Private methods (prototype, _ prefix) ---
|
|
838
835
|
_downToSinks(messages) {
|
|
839
836
|
if (this._sinks == null) return;
|
|
840
837
|
if (typeof this._sinks === "function") {
|
|
@@ -846,6 +843,11 @@ var NodeImpl = class {
|
|
|
846
843
|
sink(messages);
|
|
847
844
|
}
|
|
848
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* Update `_cached`, `_status`, `_terminal` from message batch before
|
|
848
|
+
* delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
|
|
849
|
+
* {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
|
|
850
|
+
*/
|
|
849
851
|
_handleLocalLifecycle(messages) {
|
|
850
852
|
for (const m of messages) {
|
|
851
853
|
const t = m[0];
|
|
@@ -859,28 +861,22 @@ var NodeImpl = class {
|
|
|
859
861
|
}
|
|
860
862
|
}
|
|
861
863
|
if (t === INVALIDATE) {
|
|
862
|
-
|
|
863
|
-
this._cleanup = void 0;
|
|
864
|
-
cleanupFn?.();
|
|
864
|
+
this._onInvalidate();
|
|
865
865
|
this._cached = NO_VALUE;
|
|
866
|
-
this._lastDepValues = void 0;
|
|
867
866
|
}
|
|
868
867
|
this._status = statusAfterMessage(this._status, m);
|
|
869
868
|
if (t === COMPLETE || t === ERROR) {
|
|
870
869
|
this._terminal = true;
|
|
871
870
|
}
|
|
872
871
|
if (t === TEARDOWN) {
|
|
873
|
-
if (this.
|
|
872
|
+
if (this._resetOnTeardown) {
|
|
874
873
|
this._cached = NO_VALUE;
|
|
875
874
|
}
|
|
876
|
-
|
|
877
|
-
this._cleanup = void 0;
|
|
878
|
-
teardownCleanup?.();
|
|
875
|
+
this._onTeardown();
|
|
879
876
|
try {
|
|
880
877
|
this._propagateToMeta(t);
|
|
881
878
|
} finally {
|
|
882
|
-
this.
|
|
883
|
-
this._stopProducer();
|
|
879
|
+
this._onDeactivate();
|
|
884
880
|
}
|
|
885
881
|
}
|
|
886
882
|
if (t !== TEARDOWN && propagatesToMeta(t)) {
|
|
@@ -888,7 +884,20 @@ var NodeImpl = class {
|
|
|
888
884
|
}
|
|
889
885
|
}
|
|
890
886
|
}
|
|
891
|
-
/**
|
|
887
|
+
/**
|
|
888
|
+
* Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
|
|
889
|
+
* cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
|
|
890
|
+
* drop `_lastDepValues` so the next wave re-runs fn.
|
|
891
|
+
*/
|
|
892
|
+
_onInvalidate() {
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
|
|
896
|
+
* {@link NodeImpl} uses this to run any pending cleanup fn.
|
|
897
|
+
*/
|
|
898
|
+
_onTeardown() {
|
|
899
|
+
}
|
|
900
|
+
/** Forward a signal to all companion meta nodes (best-effort). */
|
|
892
901
|
_propagateToMeta(t) {
|
|
893
902
|
for (const metaNode of Object.values(this.meta)) {
|
|
894
903
|
try {
|
|
@@ -897,9 +906,10 @@ var NodeImpl = class {
|
|
|
897
906
|
}
|
|
898
907
|
}
|
|
899
908
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
909
|
+
/**
|
|
910
|
+
* Frame a computed value into the right protocol messages and dispatch
|
|
911
|
+
* via `_downInternal`. Used by `_runFn` and `actions.emit`.
|
|
912
|
+
*/
|
|
903
913
|
_downAutoValue(value) {
|
|
904
914
|
const wasDirty = this._status === "dirty";
|
|
905
915
|
let unchanged;
|
|
@@ -907,7 +917,9 @@ var NodeImpl = class {
|
|
|
907
917
|
unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
|
|
908
918
|
} catch (eqErr) {
|
|
909
919
|
const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
|
|
910
|
-
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
|
|
920
|
+
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
|
|
921
|
+
cause: eqErr
|
|
922
|
+
});
|
|
911
923
|
this._downInternal([[ERROR, wrapped]]);
|
|
912
924
|
return;
|
|
913
925
|
}
|
|
@@ -917,89 +929,173 @@ var NodeImpl = class {
|
|
|
917
929
|
}
|
|
918
930
|
this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
|
|
919
931
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
if ("value" in out) {
|
|
955
|
-
this._downAutoValue(out.value);
|
|
956
|
-
}
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
if (isCleanupFn(out)) {
|
|
960
|
-
this._cleanup = out;
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
if (this._manualEmitUsed) return;
|
|
964
|
-
if (out === void 0) return;
|
|
965
|
-
this._downAutoValue(out);
|
|
966
|
-
} catch (err) {
|
|
967
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
968
|
-
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
|
|
969
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
_onDepDirty(index) {
|
|
973
|
-
const wasDirty = this._depDirtyMask.has(index);
|
|
974
|
-
this._depDirtyMask.set(index);
|
|
975
|
-
this._depSettledMask.clear(index);
|
|
976
|
-
if (!wasDirty) {
|
|
977
|
-
this._downInternal([[DIRTY]]);
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// src/core/node.ts
|
|
935
|
+
var NodeImpl = class extends NodeBase {
|
|
936
|
+
// --- Dep configuration (set once) ---
|
|
937
|
+
_deps;
|
|
938
|
+
_fn;
|
|
939
|
+
_opts;
|
|
940
|
+
_hasDeps;
|
|
941
|
+
_isSingleDep;
|
|
942
|
+
_autoComplete;
|
|
943
|
+
// --- Wave tracking masks ---
|
|
944
|
+
_depDirtyMask;
|
|
945
|
+
_depSettledMask;
|
|
946
|
+
_depCompleteMask;
|
|
947
|
+
_allDepsCompleteMask;
|
|
948
|
+
// --- Identity-skip optimization ---
|
|
949
|
+
_lastDepValues;
|
|
950
|
+
_cleanup;
|
|
951
|
+
// --- Upstream bookkeeping ---
|
|
952
|
+
_upstreamUnsubs = [];
|
|
953
|
+
// --- Fn behavior flag ---
|
|
954
|
+
/** @internal Read by `describeNode` to infer `"operator"` label. */
|
|
955
|
+
_manualEmitUsed = false;
|
|
956
|
+
constructor(deps, fn, opts) {
|
|
957
|
+
super(opts);
|
|
958
|
+
this._deps = deps;
|
|
959
|
+
this._fn = fn;
|
|
960
|
+
this._opts = opts;
|
|
961
|
+
this._hasDeps = deps.length > 0;
|
|
962
|
+
this._isSingleDep = deps.length === 1 && fn != null;
|
|
963
|
+
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
964
|
+
if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
|
|
965
|
+
this._status = "settled";
|
|
978
966
|
}
|
|
967
|
+
this._depDirtyMask = createBitSet(deps.length);
|
|
968
|
+
this._depSettledMask = createBitSet(deps.length);
|
|
969
|
+
this._depCompleteMask = createBitSet(deps.length);
|
|
970
|
+
this._allDepsCompleteMask = createBitSet(deps.length);
|
|
971
|
+
for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
|
|
972
|
+
this.down = this.down.bind(this);
|
|
973
|
+
this.up = this.up.bind(this);
|
|
979
974
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
975
|
+
// --- Meta node factory (called from base constructor) ---
|
|
976
|
+
_createMetaNode(key, initialValue, opts) {
|
|
977
|
+
return node({
|
|
978
|
+
initial: initialValue,
|
|
979
|
+
name: `${opts.name ?? "node"}:meta:${key}`,
|
|
980
|
+
describeKind: "state",
|
|
981
|
+
...opts.guard != null ? { guard: opts.guard } : {}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
// --- Manual emit tracker (set by actions.down / actions.emit) ---
|
|
985
|
+
_onManualEmit() {
|
|
986
|
+
this._manualEmitUsed = true;
|
|
987
|
+
}
|
|
988
|
+
// --- Up / unsubscribe ---
|
|
989
|
+
up(messages, options) {
|
|
990
|
+
if (!this._hasDeps) return;
|
|
991
|
+
if (!options?.internal && this._guard != null) {
|
|
992
|
+
const actor = normalizeActor(options?.actor);
|
|
993
|
+
if (!this._guard(actor, "write")) {
|
|
994
|
+
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
995
|
+
}
|
|
996
|
+
this._recordMutation(actor);
|
|
983
997
|
}
|
|
984
|
-
this.
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
998
|
+
for (const dep of this._deps) {
|
|
999
|
+
if (options === void 0) {
|
|
1000
|
+
dep.up?.(messages);
|
|
1001
|
+
} else {
|
|
1002
|
+
dep.up?.(messages, options);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
_upInternal(messages) {
|
|
1007
|
+
if (!this._hasDeps) return;
|
|
1008
|
+
for (const dep of this._deps) {
|
|
1009
|
+
dep.up?.(messages, { internal: true });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
unsubscribe() {
|
|
1013
|
+
if (!this._hasDeps) return;
|
|
1014
|
+
this._disconnectUpstream();
|
|
1015
|
+
}
|
|
1016
|
+
// --- Activation (first-subscriber / last-subscriber hooks) ---
|
|
1017
|
+
_onActivate() {
|
|
1018
|
+
if (this._hasDeps) {
|
|
1019
|
+
this._connectUpstream();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (this._fn) {
|
|
988
1023
|
this._runFn();
|
|
1024
|
+
return;
|
|
989
1025
|
}
|
|
990
1026
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1027
|
+
_doDeactivate() {
|
|
1028
|
+
this._disconnectUpstream();
|
|
1029
|
+
const cleanup = this._cleanup;
|
|
1030
|
+
this._cleanup = void 0;
|
|
1031
|
+
cleanup?.();
|
|
1032
|
+
if (this._fn != null) {
|
|
1033
|
+
this._cached = NO_VALUE;
|
|
1034
|
+
this._lastDepValues = void 0;
|
|
1035
|
+
}
|
|
1036
|
+
if (this._hasDeps || this._fn != null) {
|
|
1037
|
+
this._status = "disconnected";
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
|
|
1041
|
+
_onInvalidate() {
|
|
1042
|
+
const cleanup = this._cleanup;
|
|
1043
|
+
this._cleanup = void 0;
|
|
1044
|
+
cleanup?.();
|
|
1045
|
+
this._lastDepValues = void 0;
|
|
1046
|
+
}
|
|
1047
|
+
_onTeardown() {
|
|
1048
|
+
const cleanup = this._cleanup;
|
|
1049
|
+
this._cleanup = void 0;
|
|
1050
|
+
cleanup?.();
|
|
1051
|
+
}
|
|
1052
|
+
// --- Upstream connect / disconnect ---
|
|
1053
|
+
_connectUpstream() {
|
|
1054
|
+
if (!this._hasDeps) return;
|
|
1055
|
+
if (this._upstreamUnsubs.length > 0) return;
|
|
1056
|
+
this._depDirtyMask.setAll();
|
|
1057
|
+
this._depSettledMask.reset();
|
|
1058
|
+
this._depCompleteMask.reset();
|
|
1059
|
+
const depValuesBefore = this._lastDepValues;
|
|
1060
|
+
const subHints = this._isSingleDep ? { singleDep: true } : void 0;
|
|
1061
|
+
for (let i = 0; i < this._deps.length; i += 1) {
|
|
1062
|
+
const dep = this._deps[i];
|
|
1063
|
+
this._upstreamUnsubs.push(
|
|
1064
|
+
dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
|
|
1068
|
+
this._runFn();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
_disconnectUpstream() {
|
|
1072
|
+
if (this._upstreamUnsubs.length === 0) return;
|
|
1073
|
+
for (const unsub of this._upstreamUnsubs.splice(0)) {
|
|
1074
|
+
unsub();
|
|
994
1075
|
}
|
|
1076
|
+
this._depDirtyMask.reset();
|
|
1077
|
+
this._depSettledMask.reset();
|
|
1078
|
+
this._depCompleteMask.reset();
|
|
995
1079
|
}
|
|
1080
|
+
// --- Wave handling ---
|
|
996
1081
|
_handleDepMessages(index, messages) {
|
|
997
1082
|
for (const msg of messages) {
|
|
998
|
-
this.
|
|
1083
|
+
this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
|
|
999
1084
|
const t = msg[0];
|
|
1000
1085
|
if (this._onMessage) {
|
|
1001
1086
|
try {
|
|
1002
|
-
|
|
1087
|
+
const consumed = this._onMessage(msg, index, this._actions);
|
|
1088
|
+
if (consumed) {
|
|
1089
|
+
if (t === START) {
|
|
1090
|
+
this._depDirtyMask.clear(index);
|
|
1091
|
+
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
1092
|
+
this._depDirtyMask.reset();
|
|
1093
|
+
this._depSettledMask.reset();
|
|
1094
|
+
this._runFn();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1003
1099
|
} catch (err) {
|
|
1004
1100
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1005
1101
|
const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
|
|
@@ -1009,6 +1105,7 @@ var NodeImpl = class {
|
|
|
1009
1105
|
return;
|
|
1010
1106
|
}
|
|
1011
1107
|
}
|
|
1108
|
+
if (messageTier(t) < 1) continue;
|
|
1012
1109
|
if (!this._fn) {
|
|
1013
1110
|
if (t === COMPLETE && this._deps.length > 1) {
|
|
1014
1111
|
this._depCompleteMask.set(index);
|
|
@@ -1052,53 +1149,85 @@ var NodeImpl = class {
|
|
|
1052
1149
|
this._downInternal([msg]);
|
|
1053
1150
|
}
|
|
1054
1151
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
this.
|
|
1058
|
-
this.
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
this._status = "settled";
|
|
1062
|
-
const subHints = this._isSingleDep ? { singleDep: true } : void 0;
|
|
1063
|
-
this._connecting = true;
|
|
1064
|
-
try {
|
|
1065
|
-
for (let i = 0; i < this._deps.length; i += 1) {
|
|
1066
|
-
const dep = this._deps[i];
|
|
1067
|
-
this._upstreamUnsubs.push(
|
|
1068
|
-
dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
|
|
1069
|
-
);
|
|
1070
|
-
}
|
|
1071
|
-
} finally {
|
|
1072
|
-
this._connecting = false;
|
|
1152
|
+
_onDepDirty(index) {
|
|
1153
|
+
const wasDirty = this._depDirtyMask.has(index);
|
|
1154
|
+
this._depDirtyMask.set(index);
|
|
1155
|
+
this._depSettledMask.clear(index);
|
|
1156
|
+
if (!wasDirty) {
|
|
1157
|
+
this._downInternal([[DIRTY]]);
|
|
1073
1158
|
}
|
|
1074
|
-
|
|
1159
|
+
}
|
|
1160
|
+
_onDepSettled(index) {
|
|
1161
|
+
if (!this._depDirtyMask.has(index)) {
|
|
1162
|
+
this._onDepDirty(index);
|
|
1163
|
+
}
|
|
1164
|
+
this._depSettledMask.set(index);
|
|
1165
|
+
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
1166
|
+
this._depDirtyMask.reset();
|
|
1167
|
+
this._depSettledMask.reset();
|
|
1075
1168
|
this._runFn();
|
|
1076
1169
|
}
|
|
1077
1170
|
}
|
|
1078
|
-
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
this._cleanup = void 0;
|
|
1083
|
-
producerCleanup?.();
|
|
1084
|
-
}
|
|
1085
|
-
_startProducer() {
|
|
1086
|
-
if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
|
|
1087
|
-
this._producerStarted = true;
|
|
1088
|
-
this._runFn();
|
|
1171
|
+
_maybeCompleteFromDeps() {
|
|
1172
|
+
if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
|
|
1173
|
+
this._downInternal([[COMPLETE]]);
|
|
1174
|
+
}
|
|
1089
1175
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1176
|
+
// --- Fn execution ---
|
|
1177
|
+
_runFn() {
|
|
1178
|
+
if (!this._fn) return;
|
|
1179
|
+
if (this._terminal && !this._resubscribable) return;
|
|
1180
|
+
try {
|
|
1181
|
+
const n = this._deps.length;
|
|
1182
|
+
const depValues = new Array(n);
|
|
1183
|
+
for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
|
|
1184
|
+
const prev = this._lastDepValues;
|
|
1185
|
+
if (n > 0 && prev != null && prev.length === n) {
|
|
1186
|
+
let allSame = true;
|
|
1187
|
+
for (let i = 0; i < n; i++) {
|
|
1188
|
+
if (!Object.is(depValues[i], prev[i])) {
|
|
1189
|
+
allSame = false;
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (allSame) {
|
|
1194
|
+
if (this._status === "dirty") {
|
|
1195
|
+
this._downInternal([[RESOLVED]]);
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const prevCleanup = this._cleanup;
|
|
1201
|
+
this._cleanup = void 0;
|
|
1202
|
+
prevCleanup?.();
|
|
1203
|
+
this._manualEmitUsed = false;
|
|
1204
|
+
this._lastDepValues = depValues;
|
|
1205
|
+
this._emitInspectorHook({ kind: "run", depValues });
|
|
1206
|
+
const out = this._fn(depValues, this._actions);
|
|
1207
|
+
if (isCleanupResult(out)) {
|
|
1208
|
+
this._cleanup = out.cleanup;
|
|
1209
|
+
if (this._manualEmitUsed) return;
|
|
1210
|
+
if ("value" in out) {
|
|
1211
|
+
this._downAutoValue(out.value);
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (isCleanupFn(out)) {
|
|
1216
|
+
this._cleanup = out;
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (this._manualEmitUsed) return;
|
|
1220
|
+
if (out === void 0) return;
|
|
1221
|
+
this._downAutoValue(out);
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1224
|
+
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
|
|
1225
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
1094
1226
|
}
|
|
1095
|
-
this._connected = false;
|
|
1096
|
-
this._depDirtyMask.reset();
|
|
1097
|
-
this._depSettledMask.reset();
|
|
1098
|
-
this._depCompleteMask.reset();
|
|
1099
|
-
this._status = "disconnected";
|
|
1100
1227
|
}
|
|
1101
1228
|
};
|
|
1229
|
+
var isNodeArray = (value) => Array.isArray(value);
|
|
1230
|
+
var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
|
|
1102
1231
|
function node(depsOrFn, fnOrOpts, optsArg) {
|
|
1103
1232
|
const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
|
|
1104
1233
|
const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
|
|
@@ -1165,230 +1294,47 @@ function bridge(from, to, opts) {
|
|
|
1165
1294
|
}
|
|
1166
1295
|
|
|
1167
1296
|
// src/core/dynamic-node.ts
|
|
1297
|
+
var MAX_RERUN = 16;
|
|
1168
1298
|
function dynamicNode(fn, opts) {
|
|
1169
1299
|
return new DynamicNodeImpl(fn, opts ?? {});
|
|
1170
1300
|
}
|
|
1171
|
-
var DynamicNodeImpl = class {
|
|
1172
|
-
_optsName;
|
|
1173
|
-
_registryName;
|
|
1174
|
-
_describeKind;
|
|
1175
|
-
meta;
|
|
1301
|
+
var DynamicNodeImpl = class extends NodeBase {
|
|
1176
1302
|
_fn;
|
|
1177
|
-
_equals;
|
|
1178
|
-
_resubscribable;
|
|
1179
|
-
_resetOnTeardown;
|
|
1180
1303
|
_autoComplete;
|
|
1181
|
-
_onMessage;
|
|
1182
|
-
_onResubscribe;
|
|
1183
|
-
/** @internal — read by {@link describeNode} for `accessHintForGuard`. */
|
|
1184
|
-
_guard;
|
|
1185
|
-
_lastMutation;
|
|
1186
|
-
_inspectorHook;
|
|
1187
|
-
// Sink tracking
|
|
1188
|
-
_sinkCount = 0;
|
|
1189
|
-
_singleDepSinkCount = 0;
|
|
1190
|
-
_singleDepSinks = /* @__PURE__ */ new WeakSet();
|
|
1191
|
-
// Actions object (for onMessage handler)
|
|
1192
|
-
_actions;
|
|
1193
|
-
_boundDownToSinks;
|
|
1194
|
-
// Mutable state
|
|
1195
|
-
_cached = NO_VALUE;
|
|
1196
|
-
_status = "disconnected";
|
|
1197
|
-
_terminal = false;
|
|
1198
|
-
_connected = false;
|
|
1199
|
-
_rewiring = false;
|
|
1200
|
-
// re-entrancy guard
|
|
1201
1304
|
// Dynamic deps tracking
|
|
1305
|
+
/** @internal Read by `describeNode`. */
|
|
1202
1306
|
_deps = [];
|
|
1203
1307
|
_depUnsubs = [];
|
|
1204
1308
|
_depIndexMap = /* @__PURE__ */ new Map();
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1309
|
+
_depDirtyBits = /* @__PURE__ */ new Set();
|
|
1310
|
+
_depSettledBits = /* @__PURE__ */ new Set();
|
|
1311
|
+
_depCompleteBits = /* @__PURE__ */ new Set();
|
|
1312
|
+
// Execution state
|
|
1313
|
+
_running = false;
|
|
1314
|
+
_rewiring = false;
|
|
1315
|
+
_bufferedDepMessages = [];
|
|
1316
|
+
_trackedValues = /* @__PURE__ */ new Map();
|
|
1317
|
+
_rerunCount = 0;
|
|
1211
1318
|
constructor(fn, opts) {
|
|
1319
|
+
super(opts);
|
|
1212
1320
|
this._fn = fn;
|
|
1213
|
-
this._optsName = opts.name;
|
|
1214
|
-
this._describeKind = opts.describeKind;
|
|
1215
|
-
this._equals = opts.equals ?? Object.is;
|
|
1216
|
-
this._resubscribable = opts.resubscribable ?? false;
|
|
1217
|
-
this._resetOnTeardown = opts.resetOnTeardown ?? false;
|
|
1218
1321
|
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1219
|
-
this.
|
|
1220
|
-
this.
|
|
1221
|
-
this._guard = opts.guard;
|
|
1222
|
-
this._inspectorHook = void 0;
|
|
1223
|
-
const meta = {};
|
|
1224
|
-
for (const [k, v] of Object.entries(opts.meta ?? {})) {
|
|
1225
|
-
meta[k] = node({
|
|
1226
|
-
initial: v,
|
|
1227
|
-
name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
|
|
1228
|
-
describeKind: "state",
|
|
1229
|
-
...opts.guard != null ? { guard: opts.guard } : {}
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
Object.freeze(meta);
|
|
1233
|
-
this.meta = meta;
|
|
1234
|
-
const self = this;
|
|
1235
|
-
this._actions = {
|
|
1236
|
-
down(messages) {
|
|
1237
|
-
self._downInternal(messages);
|
|
1238
|
-
},
|
|
1239
|
-
emit(value) {
|
|
1240
|
-
self._downAutoValue(value);
|
|
1241
|
-
},
|
|
1242
|
-
up(messages) {
|
|
1243
|
-
for (const dep of self._deps) {
|
|
1244
|
-
dep.up?.(messages, { internal: true });
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
this._boundDownToSinks = this._downToSinks.bind(this);
|
|
1249
|
-
}
|
|
1250
|
-
get name() {
|
|
1251
|
-
return this._registryName ?? this._optsName;
|
|
1252
|
-
}
|
|
1253
|
-
/** @internal */
|
|
1254
|
-
_assignRegistryName(localName) {
|
|
1255
|
-
if (this._optsName !== void 0 || this._registryName !== void 0) return;
|
|
1256
|
-
this._registryName = localName;
|
|
1257
|
-
}
|
|
1258
|
-
/**
|
|
1259
|
-
* @internal Attach/remove inspector hook for graph-level observability.
|
|
1260
|
-
* Returns a disposer that restores the previous hook.
|
|
1261
|
-
*/
|
|
1262
|
-
_setInspectorHook(hook) {
|
|
1263
|
-
const prev = this._inspectorHook;
|
|
1264
|
-
this._inspectorHook = hook;
|
|
1265
|
-
return () => {
|
|
1266
|
-
if (this._inspectorHook === hook) {
|
|
1267
|
-
this._inspectorHook = prev;
|
|
1268
|
-
}
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
get status() {
|
|
1272
|
-
return this._status;
|
|
1322
|
+
this.down = this.down.bind(this);
|
|
1323
|
+
this.up = this.up.bind(this);
|
|
1273
1324
|
}
|
|
1274
|
-
|
|
1275
|
-
return
|
|
1325
|
+
_createMetaNode(key, initialValue, opts) {
|
|
1326
|
+
return node({
|
|
1327
|
+
initial: initialValue,
|
|
1328
|
+
name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
|
|
1329
|
+
describeKind: "state",
|
|
1330
|
+
...opts.guard != null ? { guard: opts.guard } : {}
|
|
1331
|
+
});
|
|
1276
1332
|
}
|
|
1277
|
-
/** Versioning not
|
|
1333
|
+
/** Versioning not supported on DynamicNodeImpl (override base). */
|
|
1278
1334
|
get v() {
|
|
1279
1335
|
return void 0;
|
|
1280
1336
|
}
|
|
1281
|
-
|
|
1282
|
-
return this._guard != null;
|
|
1283
|
-
}
|
|
1284
|
-
allowsObserve(actor) {
|
|
1285
|
-
if (this._guard == null) return true;
|
|
1286
|
-
return this._guard(normalizeActor(actor), "observe");
|
|
1287
|
-
}
|
|
1288
|
-
get() {
|
|
1289
|
-
return this._cached === NO_VALUE ? void 0 : this._cached;
|
|
1290
|
-
}
|
|
1291
|
-
down(messages, options) {
|
|
1292
|
-
if (messages.length === 0) return;
|
|
1293
|
-
if (!options?.internal && this._guard != null) {
|
|
1294
|
-
const actor = normalizeActor(options?.actor);
|
|
1295
|
-
const delivery = options?.delivery ?? "write";
|
|
1296
|
-
const action = delivery === "signal" ? "signal" : "write";
|
|
1297
|
-
if (!this._guard(actor, action)) {
|
|
1298
|
-
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
1299
|
-
}
|
|
1300
|
-
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
1301
|
-
}
|
|
1302
|
-
this._downInternal(messages);
|
|
1303
|
-
}
|
|
1304
|
-
_downInternal(messages) {
|
|
1305
|
-
if (messages.length === 0) return;
|
|
1306
|
-
let sinkMessages = messages;
|
|
1307
|
-
if (this._terminal && !this._resubscribable) {
|
|
1308
|
-
const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
1309
|
-
if (pass.length === 0) return;
|
|
1310
|
-
sinkMessages = pass;
|
|
1311
|
-
}
|
|
1312
|
-
this._handleLocalLifecycle(sinkMessages);
|
|
1313
|
-
if (this._canSkipDirty()) {
|
|
1314
|
-
let hasPhase2 = false;
|
|
1315
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
1316
|
-
const t = sinkMessages[i][0];
|
|
1317
|
-
if (t === DATA || t === RESOLVED) {
|
|
1318
|
-
hasPhase2 = true;
|
|
1319
|
-
break;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
if (hasPhase2) {
|
|
1323
|
-
const filtered = [];
|
|
1324
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
1325
|
-
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
1326
|
-
}
|
|
1327
|
-
if (filtered.length > 0) {
|
|
1328
|
-
downWithBatch(this._boundDownToSinks, filtered);
|
|
1329
|
-
}
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
1334
|
-
}
|
|
1335
|
-
_canSkipDirty() {
|
|
1336
|
-
return this._sinkCount === 1 && this._singleDepSinkCount === 1;
|
|
1337
|
-
}
|
|
1338
|
-
subscribe(sink, hints) {
|
|
1339
|
-
if (hints?.actor != null && this._guard != null) {
|
|
1340
|
-
const actor = normalizeActor(hints.actor);
|
|
1341
|
-
if (!this._guard(actor, "observe")) {
|
|
1342
|
-
throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
if (this._terminal && this._resubscribable) {
|
|
1346
|
-
this._terminal = false;
|
|
1347
|
-
this._cached = NO_VALUE;
|
|
1348
|
-
this._status = "disconnected";
|
|
1349
|
-
this._onResubscribe?.();
|
|
1350
|
-
}
|
|
1351
|
-
this._sinkCount += 1;
|
|
1352
|
-
if (hints?.singleDep) {
|
|
1353
|
-
this._singleDepSinkCount += 1;
|
|
1354
|
-
this._singleDepSinks.add(sink);
|
|
1355
|
-
}
|
|
1356
|
-
if (this._sinks == null) {
|
|
1357
|
-
this._sinks = sink;
|
|
1358
|
-
} else if (typeof this._sinks === "function") {
|
|
1359
|
-
this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
|
|
1360
|
-
} else {
|
|
1361
|
-
this._sinks.add(sink);
|
|
1362
|
-
}
|
|
1363
|
-
if (!this._connected) {
|
|
1364
|
-
this._connect();
|
|
1365
|
-
}
|
|
1366
|
-
let removed = false;
|
|
1367
|
-
return () => {
|
|
1368
|
-
if (removed) return;
|
|
1369
|
-
removed = true;
|
|
1370
|
-
this._sinkCount -= 1;
|
|
1371
|
-
if (this._singleDepSinks.has(sink)) {
|
|
1372
|
-
this._singleDepSinkCount -= 1;
|
|
1373
|
-
this._singleDepSinks.delete(sink);
|
|
1374
|
-
}
|
|
1375
|
-
if (this._sinks == null) return;
|
|
1376
|
-
if (typeof this._sinks === "function") {
|
|
1377
|
-
if (this._sinks === sink) this._sinks = null;
|
|
1378
|
-
} else {
|
|
1379
|
-
this._sinks.delete(sink);
|
|
1380
|
-
if (this._sinks.size === 1) {
|
|
1381
|
-
const [only] = this._sinks;
|
|
1382
|
-
this._sinks = only;
|
|
1383
|
-
} else if (this._sinks.size === 0) {
|
|
1384
|
-
this._sinks = null;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
if (this._sinks == null) {
|
|
1388
|
-
this._disconnect();
|
|
1389
|
-
}
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1337
|
+
// --- Up / unsubscribe ---
|
|
1392
1338
|
up(messages, options) {
|
|
1393
1339
|
if (this._deps.length === 0) return;
|
|
1394
1340
|
if (!options?.internal && this._guard != null) {
|
|
@@ -1396,221 +1342,227 @@ var DynamicNodeImpl = class {
|
|
|
1396
1342
|
if (!this._guard(actor, "write")) {
|
|
1397
1343
|
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
1398
1344
|
}
|
|
1399
|
-
this.
|
|
1345
|
+
this._recordMutation(actor);
|
|
1400
1346
|
}
|
|
1401
1347
|
for (const dep of this._deps) {
|
|
1402
1348
|
dep.up?.(messages, options);
|
|
1403
1349
|
}
|
|
1404
1350
|
}
|
|
1405
|
-
|
|
1406
|
-
this.
|
|
1407
|
-
|
|
1408
|
-
// --- Private methods ---
|
|
1409
|
-
_downToSinks(messages) {
|
|
1410
|
-
if (this._sinks == null) return;
|
|
1411
|
-
if (typeof this._sinks === "function") {
|
|
1412
|
-
this._sinks(messages);
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
const snapshot = [...this._sinks];
|
|
1416
|
-
for (const sink of snapshot) {
|
|
1417
|
-
sink(messages);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
_handleLocalLifecycle(messages) {
|
|
1421
|
-
for (const m of messages) {
|
|
1422
|
-
const t = m[0];
|
|
1423
|
-
if (t === DATA) this._cached = m[1];
|
|
1424
|
-
if (t === INVALIDATE) {
|
|
1425
|
-
this._cached = NO_VALUE;
|
|
1426
|
-
this._status = "dirty";
|
|
1427
|
-
}
|
|
1428
|
-
if (t === DATA) {
|
|
1429
|
-
this._status = "settled";
|
|
1430
|
-
} else if (t === RESOLVED) {
|
|
1431
|
-
this._status = "resolved";
|
|
1432
|
-
} else if (t === DIRTY) {
|
|
1433
|
-
this._status = "dirty";
|
|
1434
|
-
} else if (t === COMPLETE) {
|
|
1435
|
-
this._status = "completed";
|
|
1436
|
-
this._terminal = true;
|
|
1437
|
-
} else if (t === ERROR) {
|
|
1438
|
-
this._status = "errored";
|
|
1439
|
-
this._terminal = true;
|
|
1440
|
-
}
|
|
1441
|
-
if (t === TEARDOWN) {
|
|
1442
|
-
if (this._resetOnTeardown) this._cached = NO_VALUE;
|
|
1443
|
-
try {
|
|
1444
|
-
this._propagateToMeta(t);
|
|
1445
|
-
} finally {
|
|
1446
|
-
this._disconnect();
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
if (t !== TEARDOWN && propagatesToMeta(t)) {
|
|
1450
|
-
this._propagateToMeta(t);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
/** Propagate a signal to all companion meta nodes (best-effort). */
|
|
1455
|
-
_propagateToMeta(t) {
|
|
1456
|
-
for (const metaNode of Object.values(this.meta)) {
|
|
1457
|
-
try {
|
|
1458
|
-
metaNode.down([[t]], { internal: true });
|
|
1459
|
-
} catch {
|
|
1460
|
-
}
|
|
1351
|
+
_upInternal(messages) {
|
|
1352
|
+
for (const dep of this._deps) {
|
|
1353
|
+
dep.up?.(messages, { internal: true });
|
|
1461
1354
|
}
|
|
1462
1355
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
let unchanged;
|
|
1466
|
-
try {
|
|
1467
|
-
unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
|
|
1468
|
-
} catch (eqErr) {
|
|
1469
|
-
const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
|
|
1470
|
-
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
|
|
1471
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
if (unchanged) {
|
|
1475
|
-
this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
this._cached = value;
|
|
1479
|
-
this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
|
|
1356
|
+
unsubscribe() {
|
|
1357
|
+
this._disconnect();
|
|
1480
1358
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
this._connected = true;
|
|
1484
|
-
this._status = "settled";
|
|
1485
|
-
this._dirtyBits.clear();
|
|
1486
|
-
this._settledBits.clear();
|
|
1487
|
-
this._completeBits.clear();
|
|
1359
|
+
// --- Activation hooks ---
|
|
1360
|
+
_onActivate() {
|
|
1488
1361
|
this._runFn();
|
|
1489
1362
|
}
|
|
1363
|
+
_doDeactivate() {
|
|
1364
|
+
this._disconnect();
|
|
1365
|
+
}
|
|
1490
1366
|
_disconnect() {
|
|
1491
|
-
if (!this._connected) return;
|
|
1492
1367
|
for (const unsub of this._depUnsubs) unsub();
|
|
1493
1368
|
this._depUnsubs = [];
|
|
1494
1369
|
this._deps = [];
|
|
1495
1370
|
this._depIndexMap.clear();
|
|
1496
|
-
this.
|
|
1497
|
-
this.
|
|
1498
|
-
this.
|
|
1499
|
-
this.
|
|
1371
|
+
this._depDirtyBits.clear();
|
|
1372
|
+
this._depSettledBits.clear();
|
|
1373
|
+
this._depCompleteBits.clear();
|
|
1374
|
+
this._cached = NO_VALUE;
|
|
1500
1375
|
this._status = "disconnected";
|
|
1501
1376
|
}
|
|
1377
|
+
// --- Fn execution with rewire buffer ---
|
|
1502
1378
|
_runFn() {
|
|
1503
1379
|
if (this._terminal && !this._resubscribable) return;
|
|
1504
|
-
if (this.
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
if (!trackedSet.has(dep)) {
|
|
1509
|
-
trackedSet.add(dep);
|
|
1510
|
-
trackedDeps.push(dep);
|
|
1511
|
-
}
|
|
1512
|
-
return dep.get();
|
|
1513
|
-
};
|
|
1380
|
+
if (this._running) return;
|
|
1381
|
+
this._running = true;
|
|
1382
|
+
this._rerunCount = 0;
|
|
1383
|
+
let result;
|
|
1514
1384
|
try {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1385
|
+
for (; ; ) {
|
|
1386
|
+
const trackedDeps = [];
|
|
1387
|
+
const trackedValuesMap = /* @__PURE__ */ new Map();
|
|
1388
|
+
const trackedSet = /* @__PURE__ */ new Set();
|
|
1389
|
+
const get = (dep) => {
|
|
1390
|
+
if (!trackedSet.has(dep)) {
|
|
1391
|
+
trackedSet.add(dep);
|
|
1392
|
+
trackedDeps.push(dep);
|
|
1393
|
+
trackedValuesMap.set(dep, dep.get());
|
|
1394
|
+
}
|
|
1395
|
+
return dep.get();
|
|
1396
|
+
};
|
|
1397
|
+
this._trackedValues = trackedValuesMap;
|
|
1398
|
+
const depValues = [];
|
|
1399
|
+
for (const dep of this._deps) depValues.push(dep.get());
|
|
1400
|
+
this._emitInspectorHook({ kind: "run", depValues });
|
|
1401
|
+
try {
|
|
1402
|
+
result = this._fn(get);
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1405
|
+
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
|
|
1406
|
+
cause: err
|
|
1407
|
+
});
|
|
1408
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
this._rewiring = true;
|
|
1412
|
+
this._bufferedDepMessages = [];
|
|
1413
|
+
try {
|
|
1414
|
+
this._rewire(trackedDeps);
|
|
1415
|
+
} finally {
|
|
1416
|
+
this._rewiring = false;
|
|
1417
|
+
}
|
|
1418
|
+
let needsRerun = false;
|
|
1419
|
+
for (const entry of this._bufferedDepMessages) {
|
|
1420
|
+
for (const msg of entry.msgs) {
|
|
1421
|
+
if (msg[0] === DATA) {
|
|
1422
|
+
const dep = this._deps[entry.index];
|
|
1423
|
+
const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
|
|
1424
|
+
const actualValue = msg[1];
|
|
1425
|
+
if (!this._equals(trackedValue, actualValue)) {
|
|
1426
|
+
needsRerun = true;
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (needsRerun) break;
|
|
1432
|
+
}
|
|
1433
|
+
if (needsRerun) {
|
|
1434
|
+
this._rerunCount += 1;
|
|
1435
|
+
if (this._rerunCount > MAX_RERUN) {
|
|
1436
|
+
this._bufferedDepMessages = [];
|
|
1437
|
+
this._downInternal([
|
|
1438
|
+
[
|
|
1439
|
+
ERROR,
|
|
1440
|
+
new Error(
|
|
1441
|
+
`dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
|
|
1442
|
+
)
|
|
1443
|
+
]
|
|
1444
|
+
]);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
this._bufferedDepMessages = [];
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
const drain = this._bufferedDepMessages;
|
|
1451
|
+
this._bufferedDepMessages = [];
|
|
1452
|
+
for (const entry of drain) {
|
|
1453
|
+
for (const msg of entry.msgs) {
|
|
1454
|
+
this._updateMasksForMessage(entry.index, msg);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
this._depDirtyBits.clear();
|
|
1458
|
+
this._depSettledBits.clear();
|
|
1459
|
+
break;
|
|
1518
1460
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
this._rewire(trackedDeps);
|
|
1522
|
-
if (result === void 0) return;
|
|
1523
|
-
this._downAutoValue(result);
|
|
1524
|
-
} catch (err) {
|
|
1525
|
-
this._downInternal([[ERROR, err]]);
|
|
1461
|
+
} finally {
|
|
1462
|
+
this._running = false;
|
|
1526
1463
|
}
|
|
1464
|
+
this._downAutoValue(result);
|
|
1527
1465
|
}
|
|
1528
1466
|
_rewire(newDeps) {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
|
|
1545
|
-
newUnsubs.push(unsub);
|
|
1546
|
-
}
|
|
1467
|
+
const oldMap = this._depIndexMap;
|
|
1468
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1469
|
+
const newUnsubs = [];
|
|
1470
|
+
for (let i = 0; i < newDeps.length; i++) {
|
|
1471
|
+
const dep = newDeps[i];
|
|
1472
|
+
newMap.set(dep, i);
|
|
1473
|
+
const oldIdx = oldMap.get(dep);
|
|
1474
|
+
if (oldIdx !== void 0) {
|
|
1475
|
+
newUnsubs.push(this._depUnsubs[oldIdx]);
|
|
1476
|
+
this._depUnsubs[oldIdx] = () => {
|
|
1477
|
+
};
|
|
1478
|
+
} else {
|
|
1479
|
+
const idx = i;
|
|
1480
|
+
const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
|
|
1481
|
+
newUnsubs.push(unsub);
|
|
1547
1482
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1483
|
+
}
|
|
1484
|
+
for (const [dep, oldIdx] of oldMap) {
|
|
1485
|
+
if (!newMap.has(dep)) {
|
|
1486
|
+
this._depUnsubs[oldIdx]();
|
|
1552
1487
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1488
|
+
}
|
|
1489
|
+
this._deps = newDeps;
|
|
1490
|
+
this._depUnsubs = newUnsubs;
|
|
1491
|
+
this._depIndexMap = newMap;
|
|
1492
|
+
this._depDirtyBits.clear();
|
|
1493
|
+
this._depSettledBits.clear();
|
|
1494
|
+
const newCompleteBits = /* @__PURE__ */ new Set();
|
|
1495
|
+
for (const oldIdx of this._depCompleteBits) {
|
|
1496
|
+
for (const [dep, idx] of oldMap) {
|
|
1497
|
+
if (idx === oldIdx && newMap.has(dep)) {
|
|
1562
1498
|
newCompleteBits.add(newMap.get(dep));
|
|
1499
|
+
break;
|
|
1563
1500
|
}
|
|
1564
1501
|
}
|
|
1565
|
-
this._completeBits = newCompleteBits;
|
|
1566
|
-
} finally {
|
|
1567
|
-
this._rewiring = false;
|
|
1568
1502
|
}
|
|
1503
|
+
this._depCompleteBits = newCompleteBits;
|
|
1569
1504
|
}
|
|
1505
|
+
// --- Dep message handling ---
|
|
1570
1506
|
_handleDepMessages(index, messages) {
|
|
1571
|
-
if (this._rewiring)
|
|
1507
|
+
if (this._rewiring) {
|
|
1508
|
+
this._bufferedDepMessages.push({ index, msgs: messages });
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1572
1511
|
for (const msg of messages) {
|
|
1573
|
-
this.
|
|
1512
|
+
this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
|
|
1574
1513
|
const t = msg[0];
|
|
1575
1514
|
if (this._onMessage) {
|
|
1576
1515
|
try {
|
|
1577
1516
|
if (this._onMessage(msg, index, this._actions)) continue;
|
|
1578
1517
|
} catch (err) {
|
|
1579
|
-
|
|
1518
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1519
|
+
const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
|
|
1520
|
+
cause: err
|
|
1521
|
+
});
|
|
1522
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
1580
1523
|
return;
|
|
1581
1524
|
}
|
|
1582
1525
|
}
|
|
1526
|
+
if (messageTier(t) < 1) continue;
|
|
1583
1527
|
if (t === DIRTY) {
|
|
1584
|
-
this.
|
|
1585
|
-
this.
|
|
1586
|
-
|
|
1587
|
-
|
|
1528
|
+
const wasEmpty = this._depDirtyBits.size === 0;
|
|
1529
|
+
this._depDirtyBits.add(index);
|
|
1530
|
+
this._depSettledBits.delete(index);
|
|
1531
|
+
if (wasEmpty) {
|
|
1532
|
+
this._downInternal([[DIRTY]]);
|
|
1588
1533
|
}
|
|
1589
1534
|
continue;
|
|
1590
1535
|
}
|
|
1591
1536
|
if (t === DATA || t === RESOLVED) {
|
|
1592
|
-
if (!this.
|
|
1593
|
-
this.
|
|
1594
|
-
|
|
1537
|
+
if (!this._depDirtyBits.has(index)) {
|
|
1538
|
+
const wasEmpty = this._depDirtyBits.size === 0;
|
|
1539
|
+
this._depDirtyBits.add(index);
|
|
1540
|
+
if (wasEmpty) {
|
|
1541
|
+
this._downInternal([[DIRTY]]);
|
|
1542
|
+
}
|
|
1595
1543
|
}
|
|
1596
|
-
this.
|
|
1544
|
+
this._depSettledBits.add(index);
|
|
1597
1545
|
if (this._allDirtySettled()) {
|
|
1598
|
-
this.
|
|
1599
|
-
this.
|
|
1600
|
-
this.
|
|
1546
|
+
this._depDirtyBits.clear();
|
|
1547
|
+
this._depSettledBits.clear();
|
|
1548
|
+
if (!this._running) {
|
|
1549
|
+
if (this._depValuesDifferFromTracked()) {
|
|
1550
|
+
this._runFn();
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1601
1553
|
}
|
|
1602
1554
|
continue;
|
|
1603
1555
|
}
|
|
1604
1556
|
if (t === COMPLETE) {
|
|
1605
|
-
this.
|
|
1606
|
-
this.
|
|
1607
|
-
this.
|
|
1557
|
+
this._depCompleteBits.add(index);
|
|
1558
|
+
this._depDirtyBits.delete(index);
|
|
1559
|
+
this._depSettledBits.delete(index);
|
|
1608
1560
|
if (this._allDirtySettled()) {
|
|
1609
|
-
this.
|
|
1610
|
-
this.
|
|
1611
|
-
this._runFn();
|
|
1561
|
+
this._depDirtyBits.clear();
|
|
1562
|
+
this._depSettledBits.clear();
|
|
1563
|
+
if (!this._running) this._runFn();
|
|
1612
1564
|
}
|
|
1613
|
-
if (this._autoComplete && this.
|
|
1565
|
+
if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
|
|
1614
1566
|
this._downInternal([[COMPLETE]]);
|
|
1615
1567
|
}
|
|
1616
1568
|
continue;
|
|
@@ -1626,13 +1578,46 @@ var DynamicNodeImpl = class {
|
|
|
1626
1578
|
this._downInternal([msg]);
|
|
1627
1579
|
}
|
|
1628
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Update dep masks for a message without triggering `_runFn` — used
|
|
1583
|
+
* during post-rewire drain so the wave state is consistent with the
|
|
1584
|
+
* buffered activation cascade without recursing.
|
|
1585
|
+
*/
|
|
1586
|
+
_updateMasksForMessage(index, msg) {
|
|
1587
|
+
const t = msg[0];
|
|
1588
|
+
if (t === DIRTY) {
|
|
1589
|
+
this._depDirtyBits.add(index);
|
|
1590
|
+
this._depSettledBits.delete(index);
|
|
1591
|
+
} else if (t === DATA || t === RESOLVED) {
|
|
1592
|
+
this._depDirtyBits.add(index);
|
|
1593
|
+
this._depSettledBits.add(index);
|
|
1594
|
+
} else if (t === COMPLETE) {
|
|
1595
|
+
this._depCompleteBits.add(index);
|
|
1596
|
+
this._depDirtyBits.delete(index);
|
|
1597
|
+
this._depSettledBits.delete(index);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1629
1600
|
_allDirtySettled() {
|
|
1630
|
-
if (this.
|
|
1631
|
-
for (const idx of this.
|
|
1632
|
-
if (!this.
|
|
1601
|
+
if (this._depDirtyBits.size === 0) return false;
|
|
1602
|
+
for (const idx of this._depDirtyBits) {
|
|
1603
|
+
if (!this._depSettledBits.has(idx)) return false;
|
|
1633
1604
|
}
|
|
1634
1605
|
return true;
|
|
1635
1606
|
}
|
|
1607
|
+
/**
|
|
1608
|
+
* True if any current dep value differs from what the last `_runFn`
|
|
1609
|
+
* saw via `get()`. Used to suppress redundant re-runs when deferred
|
|
1610
|
+
* handshake messages arrive after `_rewire` for a dep whose value
|
|
1611
|
+
* already matches `_trackedValues`.
|
|
1612
|
+
*/
|
|
1613
|
+
_depValuesDifferFromTracked() {
|
|
1614
|
+
for (const dep of this._deps) {
|
|
1615
|
+
const current = dep.get();
|
|
1616
|
+
const tracked = this._trackedValues.get(dep);
|
|
1617
|
+
if (!this._equals(current, tracked)) return true;
|
|
1618
|
+
}
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1636
1621
|
};
|
|
1637
1622
|
|
|
1638
1623
|
// src/core/meta.ts
|
|
@@ -1659,8 +1644,8 @@ function producer(fn, opts) {
|
|
|
1659
1644
|
function derived(deps, fn, opts) {
|
|
1660
1645
|
return node(deps, fn, { describeKind: "derived", ...opts });
|
|
1661
1646
|
}
|
|
1662
|
-
function effect(deps, fn) {
|
|
1663
|
-
return node(deps, fn, { describeKind: "effect" });
|
|
1647
|
+
function effect(deps, fn, opts) {
|
|
1648
|
+
return node(deps, fn, { describeKind: "effect", ...opts });
|
|
1664
1649
|
}
|
|
1665
1650
|
function pipe(source, ...ops) {
|
|
1666
1651
|
let current = source;
|
|
@@ -1713,6 +1698,7 @@ var ResettableTimer = class {
|
|
|
1713
1698
|
RESOLVED,
|
|
1714
1699
|
RESUME,
|
|
1715
1700
|
ResettableTimer,
|
|
1701
|
+
START,
|
|
1716
1702
|
TEARDOWN,
|
|
1717
1703
|
accessHintForGuard,
|
|
1718
1704
|
advanceVersion,
|
|
@@ -1727,6 +1713,7 @@ var ResettableTimer = class {
|
|
|
1727
1713
|
effect,
|
|
1728
1714
|
isBatching,
|
|
1729
1715
|
isKnownMessageType,
|
|
1716
|
+
isLocalOnly,
|
|
1730
1717
|
isPhase2Message,
|
|
1731
1718
|
isTerminalMessage,
|
|
1732
1719
|
isV1,
|