@graphrefly/graphrefly 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-TNKODJ6E.js → chunk-AHRKWMNI.js} +7 -3
- package/dist/{chunk-TNKODJ6E.js.map → chunk-AHRKWMNI.js.map} +1 -1
- package/dist/{chunk-76YPZQTW.js → chunk-BER7UYLM.js} +27 -26
- package/dist/chunk-BER7UYLM.js.map +1 -0
- package/dist/{chunk-F6ORUNO7.js → chunk-IRZAGZUB.js} +34 -2
- package/dist/{chunk-F6ORUNO7.js.map → chunk-IRZAGZUB.js.map} +1 -1
- package/dist/{chunk-LB3RYLSC.js → chunk-JC2SN46B.js} +197 -42
- package/dist/chunk-JC2SN46B.js.map +1 -0
- package/dist/{chunk-KJGUP35I.js → chunk-OO5QOAXI.js} +4 -4
- package/dist/{chunk-UVWEKTYC.js → chunk-UW77D7SP.js} +3 -3
- package/dist/{chunk-J7S54G7I.js → chunk-XUOY3YKN.js} +7 -2
- package/dist/chunk-XUOY3YKN.js.map +1 -0
- package/dist/chunk-YLR5JUJZ.js +111 -0
- package/dist/chunk-YLR5JUJZ.js.map +1 -0
- package/dist/{chunk-BV3TPSBK.js → chunk-YXR3WW3Q.js} +740 -755
- package/dist/chunk-YXR3WW3Q.js.map +1 -0
- package/dist/compat/nestjs/index.cjs +931 -784
- package/dist/compat/nestjs/index.cjs.map +1 -1
- package/dist/compat/nestjs/index.d.cts +4 -4
- package/dist/compat/nestjs/index.d.ts +4 -4
- package/dist/compat/nestjs/index.js +7 -7
- package/dist/core/index.cjs +651 -664
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +7 -3
- package/dist/extra/index.cjs +686 -672
- package/dist/extra/index.cjs.map +1 -1
- package/dist/extra/index.d.cts +4 -4
- package/dist/extra/index.d.ts +4 -4
- package/dist/extra/index.js +5 -5
- package/dist/graph/index.cjs +836 -808
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +3 -3
- package/dist/graph/index.d.ts +3 -3
- package/dist/graph/index.js +8 -8
- package/dist/{graph-gISB9n3n.d.ts → graph-KsTe57nI.d.cts} +82 -8
- package/dist/{graph-BYFlyNpX.d.cts → graph-mILUUqW8.d.ts} +82 -8
- package/dist/{index-CgKPpiu8.d.ts → index-8a605sg9.d.ts} +2 -2
- package/dist/{index-DKaB2x0T.d.ts → index-B2SvPEbc.d.ts} +6 -65
- package/dist/{index-B80mMeuf.d.ts → index-BBUYZfJ1.d.cts} +122 -76
- package/dist/{index-D_tUMcpz.d.cts → index-Bjh5C1Tp.d.cts} +37 -32
- package/dist/{index-B43mC7uY.d.cts → index-BjtlNirP.d.cts} +3 -3
- package/dist/{index-7WnwgjMu.d.ts → index-BnkMgNNa.d.ts} +37 -32
- package/dist/{index-CEDaJaYE.d.ts → index-CgSiUouz.d.ts} +3 -3
- package/dist/{index-EmzYk-TG.d.cts → index-CvKzv0AW.d.ts} +122 -76
- package/dist/{index-Ci_vPaVm.d.cts → index-UudxGnzc.d.cts} +6 -65
- package/dist/{index-BqOWSFhr.d.cts → index-VHA43cGP.d.cts} +2 -2
- package/dist/index.cjs +5920 -5572
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +595 -399
- package/dist/index.d.ts +595 -399
- package/dist/index.js +4357 -4063
- package/dist/index.js.map +1 -1
- package/dist/{meta-npl5b97j.d.cts → meta-BnG7XAaE.d.cts} +394 -236
- package/dist/{meta-npl5b97j.d.ts → meta-BnG7XAaE.d.ts} +394 -236
- package/dist/{observable-DFBCBELR.d.cts → observable-C8Kx_O6k.d.cts} +1 -1
- package/dist/{observable-oAGygKvc.d.ts → observable-DcBwQY7t.d.ts} +1 -1
- package/dist/patterns/reactive-layout/index.cjs +865 -718
- package/dist/patterns/reactive-layout/index.cjs.map +1 -1
- package/dist/patterns/reactive-layout/index.d.cts +3 -3
- package/dist/patterns/reactive-layout/index.d.ts +3 -3
- package/dist/patterns/reactive-layout/index.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-76YPZQTW.js.map +0 -1
- package/dist/chunk-BV3TPSBK.js.map +0 -1
- package/dist/chunk-FCLROC4Q.js +0 -231
- package/dist/chunk-FCLROC4Q.js.map +0 -1
- package/dist/chunk-J7S54G7I.js.map +0 -1
- package/dist/chunk-LB3RYLSC.js.map +0 -1
- /package/dist/{chunk-KJGUP35I.js.map → chunk-OO5QOAXI.js.map} +0 -0
- /package/dist/{chunk-UVWEKTYC.js.map → chunk-UW77D7SP.js.map} +0 -0
package/dist/graph/index.cjs
CHANGED
|
@@ -110,6 +110,7 @@ function replayWAL(entries) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// src/core/messages.ts
|
|
113
|
+
var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
|
|
113
114
|
var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
|
|
114
115
|
var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
|
|
115
116
|
var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
|
|
@@ -119,13 +120,27 @@ var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
|
|
|
119
120
|
var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
|
|
120
121
|
var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
|
|
121
122
|
var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
|
|
123
|
+
var knownMessageTypes = [
|
|
124
|
+
START,
|
|
125
|
+
DATA,
|
|
126
|
+
DIRTY,
|
|
127
|
+
RESOLVED,
|
|
128
|
+
INVALIDATE,
|
|
129
|
+
PAUSE,
|
|
130
|
+
RESUME,
|
|
131
|
+
TEARDOWN,
|
|
132
|
+
COMPLETE,
|
|
133
|
+
ERROR
|
|
134
|
+
];
|
|
135
|
+
var knownMessageSet = new Set(knownMessageTypes);
|
|
122
136
|
function messageTier(t) {
|
|
123
|
-
if (t ===
|
|
124
|
-
if (t ===
|
|
125
|
-
if (t ===
|
|
126
|
-
if (t ===
|
|
127
|
-
if (t ===
|
|
128
|
-
return
|
|
137
|
+
if (t === START) return 0;
|
|
138
|
+
if (t === DIRTY || t === INVALIDATE) return 1;
|
|
139
|
+
if (t === PAUSE || t === RESUME) return 2;
|
|
140
|
+
if (t === DATA || t === RESOLVED) return 3;
|
|
141
|
+
if (t === COMPLETE || t === ERROR) return 4;
|
|
142
|
+
if (t === TEARDOWN) return 5;
|
|
143
|
+
return 1;
|
|
129
144
|
}
|
|
130
145
|
function isPhase2Message(msg) {
|
|
131
146
|
const t = msg[0];
|
|
@@ -213,14 +228,14 @@ function _downSequential(sink, messages, phase = 2) {
|
|
|
213
228
|
const dataQueue = phase === 3 ? pendingPhase3 : pendingPhase2;
|
|
214
229
|
for (const msg of messages) {
|
|
215
230
|
const tier = messageTier(msg[0]);
|
|
216
|
-
if (tier ===
|
|
231
|
+
if (tier === 3) {
|
|
217
232
|
if (isBatching()) {
|
|
218
233
|
const m = msg;
|
|
219
234
|
dataQueue.push(() => sink([m]));
|
|
220
235
|
} else {
|
|
221
236
|
sink([msg]);
|
|
222
237
|
}
|
|
223
|
-
} else if (tier >=
|
|
238
|
+
} else if (tier >= 4) {
|
|
224
239
|
if (isBatching()) {
|
|
225
240
|
const m = msg;
|
|
226
241
|
pendingPhase3.push(() => sink([m]));
|
|
@@ -339,10 +354,24 @@ function advanceVersion(info, newValue, hashFn) {
|
|
|
339
354
|
}
|
|
340
355
|
}
|
|
341
356
|
|
|
342
|
-
// src/core/node.ts
|
|
357
|
+
// src/core/node-base.ts
|
|
343
358
|
var NO_VALUE = /* @__PURE__ */ Symbol.for("graphrefly/NO_VALUE");
|
|
344
359
|
var CLEANUP_RESULT = /* @__PURE__ */ Symbol.for("graphrefly/CLEANUP_RESULT");
|
|
345
|
-
|
|
360
|
+
var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
|
|
361
|
+
var isCleanupFn = (value) => typeof value === "function";
|
|
362
|
+
function statusAfterMessage(status, msg) {
|
|
363
|
+
const t = msg[0];
|
|
364
|
+
if (t === DIRTY) return "dirty";
|
|
365
|
+
if (t === DATA) return "settled";
|
|
366
|
+
if (t === RESOLVED) return "resolved";
|
|
367
|
+
if (t === COMPLETE) return "completed";
|
|
368
|
+
if (t === ERROR) return "errored";
|
|
369
|
+
if (t === INVALIDATE) return "dirty";
|
|
370
|
+
if (t === TEARDOWN) return "disconnected";
|
|
371
|
+
return status;
|
|
372
|
+
}
|
|
373
|
+
function createIntBitSet(size) {
|
|
374
|
+
const fullMask = size >= 32 ? -1 : ~(-1 << size);
|
|
346
375
|
let bits = 0;
|
|
347
376
|
return {
|
|
348
377
|
set(i) {
|
|
@@ -355,7 +384,8 @@ function createIntBitSet() {
|
|
|
355
384
|
return (bits & 1 << i) !== 0;
|
|
356
385
|
},
|
|
357
386
|
covers(other) {
|
|
358
|
-
|
|
387
|
+
const otherBits = other._bits();
|
|
388
|
+
return (bits & otherBits) === otherBits;
|
|
359
389
|
},
|
|
360
390
|
any() {
|
|
361
391
|
return bits !== 0;
|
|
@@ -363,6 +393,9 @@ function createIntBitSet() {
|
|
|
363
393
|
reset() {
|
|
364
394
|
bits = 0;
|
|
365
395
|
},
|
|
396
|
+
setAll() {
|
|
397
|
+
bits = fullMask;
|
|
398
|
+
},
|
|
366
399
|
_bits() {
|
|
367
400
|
return bits;
|
|
368
401
|
}
|
|
@@ -370,6 +403,8 @@ function createIntBitSet() {
|
|
|
370
403
|
}
|
|
371
404
|
function createArrayBitSet(size) {
|
|
372
405
|
const words = new Uint32Array(Math.ceil(size / 32));
|
|
406
|
+
const lastBits = size % 32;
|
|
407
|
+
const lastWordMask = lastBits === 0 ? 4294967295 : (1 << lastBits) - 1 >>> 0;
|
|
373
408
|
return {
|
|
374
409
|
set(i) {
|
|
375
410
|
words[i >>> 5] |= 1 << (i & 31);
|
|
@@ -396,130 +431,103 @@ function createArrayBitSet(size) {
|
|
|
396
431
|
reset() {
|
|
397
432
|
words.fill(0);
|
|
398
433
|
},
|
|
434
|
+
setAll() {
|
|
435
|
+
for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
|
|
436
|
+
if (words.length > 0) words[words.length - 1] = lastWordMask;
|
|
437
|
+
},
|
|
399
438
|
_words: words
|
|
400
439
|
};
|
|
401
440
|
}
|
|
402
441
|
function createBitSet(size) {
|
|
403
|
-
return size <= 31 ? createIntBitSet() : createArrayBitSet(size);
|
|
442
|
+
return size <= 31 ? createIntBitSet(size) : createArrayBitSet(size);
|
|
404
443
|
}
|
|
405
|
-
var
|
|
406
|
-
|
|
407
|
-
var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
|
|
408
|
-
var isCleanupFn = (value) => typeof value === "function";
|
|
409
|
-
var statusAfterMessage = (status, msg) => {
|
|
410
|
-
const t = msg[0];
|
|
411
|
-
if (t === DIRTY) return "dirty";
|
|
412
|
-
if (t === DATA) return "settled";
|
|
413
|
-
if (t === RESOLVED) return "resolved";
|
|
414
|
-
if (t === COMPLETE) return "completed";
|
|
415
|
-
if (t === ERROR) return "errored";
|
|
416
|
-
if (t === INVALIDATE) return "dirty";
|
|
417
|
-
if (t === TEARDOWN) return "disconnected";
|
|
418
|
-
return status;
|
|
419
|
-
};
|
|
420
|
-
var NodeImpl = class {
|
|
421
|
-
// --- Configuration (set once, never reassigned) ---
|
|
444
|
+
var NodeBase = class {
|
|
445
|
+
// --- Identity (set once) ---
|
|
422
446
|
_optsName;
|
|
423
447
|
_registryName;
|
|
424
|
-
/** @internal
|
|
448
|
+
/** @internal Read by `describeNode` before inference. */
|
|
425
449
|
_describeKind;
|
|
426
450
|
meta;
|
|
427
|
-
|
|
428
|
-
_fn;
|
|
429
|
-
_opts;
|
|
451
|
+
// --- Options ---
|
|
430
452
|
_equals;
|
|
453
|
+
_resubscribable;
|
|
454
|
+
_resetOnTeardown;
|
|
455
|
+
_onResubscribe;
|
|
431
456
|
_onMessage;
|
|
432
|
-
/** @internal
|
|
457
|
+
/** @internal Read by `describeNode` for `accessHintForGuard`. */
|
|
433
458
|
_guard;
|
|
459
|
+
/** @internal Subclasses update this through {@link _recordMutation}. */
|
|
434
460
|
_lastMutation;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
// ---
|
|
461
|
+
// --- Versioning ---
|
|
462
|
+
_hashFn;
|
|
463
|
+
_versioning;
|
|
464
|
+
// --- Lifecycle state ---
|
|
465
|
+
/** @internal Read by `describeNode` and `graph.ts`. */
|
|
439
466
|
_cached;
|
|
467
|
+
/** @internal Read externally via `get status()`. */
|
|
440
468
|
_status;
|
|
441
469
|
_terminal = false;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
_manualEmitUsed = false;
|
|
470
|
+
_active = false;
|
|
471
|
+
// --- Sink storage ---
|
|
472
|
+
/** @internal Read by `graph/profile.ts` for subscriber counts. */
|
|
446
473
|
_sinkCount = 0;
|
|
447
474
|
_singleDepSinkCount = 0;
|
|
448
|
-
// --- Object/collection state ---
|
|
449
|
-
_depDirtyMask;
|
|
450
|
-
_depSettledMask;
|
|
451
|
-
_depCompleteMask;
|
|
452
|
-
_allDepsCompleteMask;
|
|
453
|
-
_lastDepValues;
|
|
454
|
-
_cleanup;
|
|
455
|
-
_sinks = null;
|
|
456
475
|
_singleDepSinks = /* @__PURE__ */ new WeakSet();
|
|
457
|
-
|
|
476
|
+
_sinks = null;
|
|
477
|
+
// --- Actions + bound helpers ---
|
|
458
478
|
_actions;
|
|
459
479
|
_boundDownToSinks;
|
|
480
|
+
// --- Inspector hook (Graph observability) ---
|
|
460
481
|
_inspectorHook;
|
|
461
|
-
|
|
462
|
-
_hashFn;
|
|
463
|
-
constructor(deps, fn, opts) {
|
|
464
|
-
this._deps = deps;
|
|
465
|
-
this._fn = fn;
|
|
466
|
-
this._opts = opts;
|
|
482
|
+
constructor(opts) {
|
|
467
483
|
this._optsName = opts.name;
|
|
468
484
|
this._describeKind = opts.describeKind;
|
|
469
485
|
this._equals = opts.equals ?? Object.is;
|
|
486
|
+
this._resubscribable = opts.resubscribable ?? false;
|
|
487
|
+
this._resetOnTeardown = opts.resetOnTeardown ?? false;
|
|
488
|
+
this._onResubscribe = opts.onResubscribe;
|
|
470
489
|
this._onMessage = opts.onMessage;
|
|
471
490
|
this._guard = opts.guard;
|
|
472
|
-
this._hasDeps = deps.length > 0;
|
|
473
|
-
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
474
|
-
this._isSingleDep = deps.length === 1 && fn != null;
|
|
475
491
|
this._cached = "initial" in opts ? opts.initial : NO_VALUE;
|
|
476
|
-
this._status =
|
|
492
|
+
this._status = "disconnected";
|
|
477
493
|
this._hashFn = opts.versioningHash ?? defaultHash;
|
|
478
494
|
this._versioning = opts.versioning != null ? createVersioning(opts.versioning, this._cached === NO_VALUE ? void 0 : this._cached, {
|
|
479
495
|
id: opts.versioningId,
|
|
480
496
|
hash: this._hashFn
|
|
481
497
|
}) : void 0;
|
|
482
|
-
this._depDirtyMask = createBitSet(deps.length);
|
|
483
|
-
this._depSettledMask = createBitSet(deps.length);
|
|
484
|
-
this._depCompleteMask = createBitSet(deps.length);
|
|
485
|
-
this._allDepsCompleteMask = createBitSet(deps.length);
|
|
486
|
-
for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
|
|
487
498
|
const meta = {};
|
|
488
499
|
for (const [k, v] of Object.entries(opts.meta ?? {})) {
|
|
489
|
-
meta[k] =
|
|
490
|
-
initial: v,
|
|
491
|
-
name: `${opts.name ?? "node"}:meta:${k}`,
|
|
492
|
-
describeKind: "state",
|
|
493
|
-
...opts.guard != null ? { guard: opts.guard } : {}
|
|
494
|
-
});
|
|
500
|
+
meta[k] = this._createMetaNode(k, v, opts);
|
|
495
501
|
}
|
|
496
502
|
Object.freeze(meta);
|
|
497
503
|
this.meta = meta;
|
|
498
504
|
const self = this;
|
|
499
505
|
this._actions = {
|
|
500
506
|
down(messages) {
|
|
501
|
-
self.
|
|
507
|
+
self._onManualEmit();
|
|
502
508
|
self._downInternal(messages);
|
|
503
509
|
},
|
|
504
510
|
emit(value) {
|
|
505
|
-
self.
|
|
511
|
+
self._onManualEmit();
|
|
506
512
|
self._downAutoValue(value);
|
|
507
513
|
},
|
|
508
514
|
up(messages) {
|
|
509
515
|
self._upInternal(messages);
|
|
510
516
|
}
|
|
511
517
|
};
|
|
512
|
-
this.down = this.down.bind(this);
|
|
513
|
-
this.up = this.up.bind(this);
|
|
514
518
|
this._boundDownToSinks = this._downToSinks.bind(this);
|
|
515
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Subclass hook invoked by `actions.down` / `actions.emit`. Default no-op;
|
|
522
|
+
* {@link NodeImpl} overrides to set `_manualEmitUsed`.
|
|
523
|
+
*/
|
|
524
|
+
_onManualEmit() {
|
|
525
|
+
}
|
|
526
|
+
// --- Identity getters ---
|
|
516
527
|
get name() {
|
|
517
528
|
return this._registryName ?? this._optsName;
|
|
518
529
|
}
|
|
519
|
-
/**
|
|
520
|
-
* When a node is registered with {@link Graph.add} without an options `name`,
|
|
521
|
-
* the graph assigns the registry local name for introspection (parity with graphrefly-py).
|
|
522
|
-
*/
|
|
530
|
+
/** @internal Assigned by `Graph.add` when registered without an options `name`. */
|
|
523
531
|
_assignRegistryName(localName) {
|
|
524
532
|
if (this._optsName !== void 0 || this._registryName !== void 0) return;
|
|
525
533
|
this._registryName = localName;
|
|
@@ -537,7 +545,10 @@ var NodeImpl = class {
|
|
|
537
545
|
}
|
|
538
546
|
};
|
|
539
547
|
}
|
|
540
|
-
|
|
548
|
+
/** @internal Used by subclasses to surface inspector events. */
|
|
549
|
+
_emitInspectorHook(event) {
|
|
550
|
+
this._inspectorHook?.(event);
|
|
551
|
+
}
|
|
541
552
|
get status() {
|
|
542
553
|
return this._status;
|
|
543
554
|
}
|
|
@@ -547,15 +558,7 @@ var NodeImpl = class {
|
|
|
547
558
|
get v() {
|
|
548
559
|
return this._versioning;
|
|
549
560
|
}
|
|
550
|
-
/**
|
|
551
|
-
* Retroactively apply versioning to a node that was created without it.
|
|
552
|
-
* No-op if versioning is already enabled.
|
|
553
|
-
*
|
|
554
|
-
* Version starts at 0 regardless of prior DATA emissions — it tracks
|
|
555
|
-
* changes from the moment versioning is enabled, not historical ones.
|
|
556
|
-
*
|
|
557
|
-
* @internal — used by {@link Graph.setVersioning}.
|
|
558
|
-
*/
|
|
561
|
+
/** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
|
|
559
562
|
_applyVersioning(level, opts) {
|
|
560
563
|
if (this._versioning != null) return;
|
|
561
564
|
this._hashFn = opts?.hash ?? this._hashFn;
|
|
@@ -575,6 +578,7 @@ var NodeImpl = class {
|
|
|
575
578
|
if (this._guard == null) return true;
|
|
576
579
|
return this._guard(normalizeActor(actor), "observe");
|
|
577
580
|
}
|
|
581
|
+
// --- Public transport ---
|
|
578
582
|
get() {
|
|
579
583
|
return this._cached === NO_VALUE ? void 0 : this._cached;
|
|
580
584
|
}
|
|
@@ -587,43 +591,25 @@ var NodeImpl = class {
|
|
|
587
591
|
if (!this._guard(actor, action)) {
|
|
588
592
|
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
589
593
|
}
|
|
590
|
-
this.
|
|
594
|
+
this._recordMutation(actor);
|
|
591
595
|
}
|
|
592
596
|
this._downInternal(messages);
|
|
593
597
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
this.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
608
|
-
const t = sinkMessages[i][0];
|
|
609
|
-
if (t === DATA || t === RESOLVED) {
|
|
610
|
-
hasPhase2 = true;
|
|
611
|
-
break;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (hasPhase2) {
|
|
615
|
-
const filtered = [];
|
|
616
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
617
|
-
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
618
|
-
}
|
|
619
|
-
if (filtered.length > 0) {
|
|
620
|
-
downWithBatch(this._boundDownToSinks, filtered);
|
|
621
|
-
}
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
598
|
+
/** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
|
|
599
|
+
_recordMutation(actor) {
|
|
600
|
+
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* At-most-once deactivation guard. Both TEARDOWN (eager) and
|
|
604
|
+
* unsubscribe-body (lazy) call this. The `_active` flag ensures
|
|
605
|
+
* `_doDeactivate` runs exactly once per activation cycle.
|
|
606
|
+
*/
|
|
607
|
+
_onDeactivate() {
|
|
608
|
+
if (!this._active) return;
|
|
609
|
+
this._active = false;
|
|
610
|
+
this._doDeactivate();
|
|
626
611
|
}
|
|
612
|
+
// --- Subscribe (uniform across node shapes) ---
|
|
627
613
|
subscribe(sink, hints) {
|
|
628
614
|
if (hints?.actor != null && this._guard != null) {
|
|
629
615
|
const actor = normalizeActor(hints.actor);
|
|
@@ -631,17 +617,21 @@ var NodeImpl = class {
|
|
|
631
617
|
throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
|
|
632
618
|
}
|
|
633
619
|
}
|
|
634
|
-
if (this._terminal && this.
|
|
620
|
+
if (this._terminal && this._resubscribable) {
|
|
635
621
|
this._terminal = false;
|
|
636
622
|
this._cached = NO_VALUE;
|
|
637
|
-
this._status =
|
|
638
|
-
this.
|
|
623
|
+
this._status = "disconnected";
|
|
624
|
+
this._onResubscribe?.();
|
|
639
625
|
}
|
|
640
626
|
this._sinkCount += 1;
|
|
641
627
|
if (hints?.singleDep) {
|
|
642
628
|
this._singleDepSinkCount += 1;
|
|
643
629
|
this._singleDepSinks.add(sink);
|
|
644
630
|
}
|
|
631
|
+
if (!this._terminal) {
|
|
632
|
+
const startMessages = this._cached === NO_VALUE ? [[START]] : [[START], [DATA, this._cached]];
|
|
633
|
+
downWithBatch(sink, startMessages);
|
|
634
|
+
}
|
|
645
635
|
if (this._sinks == null) {
|
|
646
636
|
this._sinks = sink;
|
|
647
637
|
} else if (typeof this._sinks === "function") {
|
|
@@ -649,10 +639,12 @@ var NodeImpl = class {
|
|
|
649
639
|
} else {
|
|
650
640
|
this._sinks.add(sink);
|
|
651
641
|
}
|
|
652
|
-
if (this.
|
|
653
|
-
this.
|
|
654
|
-
|
|
655
|
-
|
|
642
|
+
if (this._sinkCount === 1 && !this._terminal) {
|
|
643
|
+
this._active = true;
|
|
644
|
+
this._onActivate();
|
|
645
|
+
}
|
|
646
|
+
if (!this._terminal && this._status === "disconnected" && this._cached === NO_VALUE) {
|
|
647
|
+
this._status = "pending";
|
|
656
648
|
}
|
|
657
649
|
let removed = false;
|
|
658
650
|
return () => {
|
|
@@ -676,39 +668,49 @@ var NodeImpl = class {
|
|
|
676
668
|
}
|
|
677
669
|
}
|
|
678
670
|
if (this._sinks == null) {
|
|
679
|
-
this.
|
|
680
|
-
this._stopProducer();
|
|
671
|
+
this._onDeactivate();
|
|
681
672
|
}
|
|
682
673
|
};
|
|
683
674
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
675
|
+
// --- Down pipeline ---
|
|
676
|
+
/**
|
|
677
|
+
* Core outgoing dispatch. Applies terminal filter + local lifecycle
|
|
678
|
+
* update, then hands messages to `downWithBatch` for tier-aware delivery.
|
|
679
|
+
*/
|
|
680
|
+
_downInternal(messages) {
|
|
681
|
+
if (messages.length === 0) return;
|
|
682
|
+
let sinkMessages = messages;
|
|
683
|
+
if (this._terminal && !this._resubscribable) {
|
|
684
|
+
const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
685
|
+
if (pass.length === 0) return;
|
|
686
|
+
sinkMessages = pass;
|
|
692
687
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
688
|
+
this._handleLocalLifecycle(sinkMessages);
|
|
689
|
+
if (this._canSkipDirty()) {
|
|
690
|
+
let hasPhase2 = false;
|
|
691
|
+
for (let i = 0; i < sinkMessages.length; i++) {
|
|
692
|
+
const t = sinkMessages[i][0];
|
|
693
|
+
if (t === DATA || t === RESOLVED) {
|
|
694
|
+
hasPhase2 = true;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (hasPhase2) {
|
|
699
|
+
const filtered = [];
|
|
700
|
+
for (let i = 0; i < sinkMessages.length; i++) {
|
|
701
|
+
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
702
|
+
}
|
|
703
|
+
if (filtered.length > 0) {
|
|
704
|
+
downWithBatch(this._boundDownToSinks, filtered);
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
698
707
|
}
|
|
699
708
|
}
|
|
709
|
+
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
700
710
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
for (const dep of this._deps) {
|
|
704
|
-
dep.up?.(messages, { internal: true });
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
unsubscribe() {
|
|
708
|
-
if (!this._hasDeps) return;
|
|
709
|
-
this._disconnectUpstream();
|
|
711
|
+
_canSkipDirty() {
|
|
712
|
+
return this._sinkCount === 1 && this._singleDepSinkCount === 1;
|
|
710
713
|
}
|
|
711
|
-
// --- Private methods (prototype, _ prefix) ---
|
|
712
714
|
_downToSinks(messages) {
|
|
713
715
|
if (this._sinks == null) return;
|
|
714
716
|
if (typeof this._sinks === "function") {
|
|
@@ -720,6 +722,11 @@ var NodeImpl = class {
|
|
|
720
722
|
sink(messages);
|
|
721
723
|
}
|
|
722
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Update `_cached`, `_status`, `_terminal` from message batch before
|
|
727
|
+
* delivery. Subclass hooks `_onInvalidate` / `_onTeardown` let
|
|
728
|
+
* {@link NodeImpl} clear `_lastDepValues` and invoke cleanup fns.
|
|
729
|
+
*/
|
|
723
730
|
_handleLocalLifecycle(messages) {
|
|
724
731
|
for (const m of messages) {
|
|
725
732
|
const t = m[0];
|
|
@@ -733,28 +740,22 @@ var NodeImpl = class {
|
|
|
733
740
|
}
|
|
734
741
|
}
|
|
735
742
|
if (t === INVALIDATE) {
|
|
736
|
-
|
|
737
|
-
this._cleanup = void 0;
|
|
738
|
-
cleanupFn?.();
|
|
743
|
+
this._onInvalidate();
|
|
739
744
|
this._cached = NO_VALUE;
|
|
740
|
-
this._lastDepValues = void 0;
|
|
741
745
|
}
|
|
742
746
|
this._status = statusAfterMessage(this._status, m);
|
|
743
747
|
if (t === COMPLETE || t === ERROR) {
|
|
744
748
|
this._terminal = true;
|
|
745
749
|
}
|
|
746
750
|
if (t === TEARDOWN) {
|
|
747
|
-
if (this.
|
|
751
|
+
if (this._resetOnTeardown) {
|
|
748
752
|
this._cached = NO_VALUE;
|
|
749
753
|
}
|
|
750
|
-
|
|
751
|
-
this._cleanup = void 0;
|
|
752
|
-
teardownCleanup?.();
|
|
754
|
+
this._onTeardown();
|
|
753
755
|
try {
|
|
754
756
|
this._propagateToMeta(t);
|
|
755
757
|
} finally {
|
|
756
|
-
this.
|
|
757
|
-
this._stopProducer();
|
|
758
|
+
this._onDeactivate();
|
|
758
759
|
}
|
|
759
760
|
}
|
|
760
761
|
if (t !== TEARDOWN && propagatesToMeta(t)) {
|
|
@@ -762,7 +763,20 @@ var NodeImpl = class {
|
|
|
762
763
|
}
|
|
763
764
|
}
|
|
764
765
|
}
|
|
765
|
-
/**
|
|
766
|
+
/**
|
|
767
|
+
* Subclass hook: invoked when INVALIDATE arrives (before `_cached` is
|
|
768
|
+
* cleared). {@link NodeImpl} uses this to run the fn cleanup fn and
|
|
769
|
+
* drop `_lastDepValues` so the next wave re-runs fn.
|
|
770
|
+
*/
|
|
771
|
+
_onInvalidate() {
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
|
|
775
|
+
* {@link NodeImpl} uses this to run any pending cleanup fn.
|
|
776
|
+
*/
|
|
777
|
+
_onTeardown() {
|
|
778
|
+
}
|
|
779
|
+
/** Forward a signal to all companion meta nodes (best-effort). */
|
|
766
780
|
_propagateToMeta(t) {
|
|
767
781
|
for (const metaNode of Object.values(this.meta)) {
|
|
768
782
|
try {
|
|
@@ -771,9 +785,10 @@ var NodeImpl = class {
|
|
|
771
785
|
}
|
|
772
786
|
}
|
|
773
787
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
788
|
+
/**
|
|
789
|
+
* Frame a computed value into the right protocol messages and dispatch
|
|
790
|
+
* via `_downInternal`. Used by `_runFn` and `actions.emit`.
|
|
791
|
+
*/
|
|
777
792
|
_downAutoValue(value) {
|
|
778
793
|
const wasDirty = this._status === "dirty";
|
|
779
794
|
let unchanged;
|
|
@@ -781,7 +796,9 @@ var NodeImpl = class {
|
|
|
781
796
|
unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
|
|
782
797
|
} catch (eqErr) {
|
|
783
798
|
const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
|
|
784
|
-
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
|
|
799
|
+
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
|
|
800
|
+
cause: eqErr
|
|
801
|
+
});
|
|
785
802
|
this._downInternal([[ERROR, wrapped]]);
|
|
786
803
|
return;
|
|
787
804
|
}
|
|
@@ -791,89 +808,173 @@ var NodeImpl = class {
|
|
|
791
808
|
}
|
|
792
809
|
this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
|
|
793
810
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if ("value" in out) {
|
|
829
|
-
this._downAutoValue(out.value);
|
|
830
|
-
}
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
if (isCleanupFn(out)) {
|
|
834
|
-
this._cleanup = out;
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
if (this._manualEmitUsed) return;
|
|
838
|
-
if (out === void 0) return;
|
|
839
|
-
this._downAutoValue(out);
|
|
840
|
-
} catch (err) {
|
|
841
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
842
|
-
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
|
|
843
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// src/core/node.ts
|
|
814
|
+
var NodeImpl = class extends NodeBase {
|
|
815
|
+
// --- Dep configuration (set once) ---
|
|
816
|
+
_deps;
|
|
817
|
+
_fn;
|
|
818
|
+
_opts;
|
|
819
|
+
_hasDeps;
|
|
820
|
+
_isSingleDep;
|
|
821
|
+
_autoComplete;
|
|
822
|
+
// --- Wave tracking masks ---
|
|
823
|
+
_depDirtyMask;
|
|
824
|
+
_depSettledMask;
|
|
825
|
+
_depCompleteMask;
|
|
826
|
+
_allDepsCompleteMask;
|
|
827
|
+
// --- Identity-skip optimization ---
|
|
828
|
+
_lastDepValues;
|
|
829
|
+
_cleanup;
|
|
830
|
+
// --- Upstream bookkeeping ---
|
|
831
|
+
_upstreamUnsubs = [];
|
|
832
|
+
// --- Fn behavior flag ---
|
|
833
|
+
/** @internal Read by `describeNode` to infer `"operator"` label. */
|
|
834
|
+
_manualEmitUsed = false;
|
|
835
|
+
constructor(deps, fn, opts) {
|
|
836
|
+
super(opts);
|
|
837
|
+
this._deps = deps;
|
|
838
|
+
this._fn = fn;
|
|
839
|
+
this._opts = opts;
|
|
840
|
+
this._hasDeps = deps.length > 0;
|
|
841
|
+
this._isSingleDep = deps.length === 1 && fn != null;
|
|
842
|
+
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
843
|
+
if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
|
|
844
|
+
this._status = "settled";
|
|
844
845
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
this.
|
|
849
|
-
this.
|
|
850
|
-
|
|
851
|
-
|
|
846
|
+
this._depDirtyMask = createBitSet(deps.length);
|
|
847
|
+
this._depSettledMask = createBitSet(deps.length);
|
|
848
|
+
this._depCompleteMask = createBitSet(deps.length);
|
|
849
|
+
this._allDepsCompleteMask = createBitSet(deps.length);
|
|
850
|
+
for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
|
|
851
|
+
this.down = this.down.bind(this);
|
|
852
|
+
this.up = this.up.bind(this);
|
|
853
|
+
}
|
|
854
|
+
// --- Meta node factory (called from base constructor) ---
|
|
855
|
+
_createMetaNode(key, initialValue, opts) {
|
|
856
|
+
return node({
|
|
857
|
+
initial: initialValue,
|
|
858
|
+
name: `${opts.name ?? "node"}:meta:${key}`,
|
|
859
|
+
describeKind: "state",
|
|
860
|
+
...opts.guard != null ? { guard: opts.guard } : {}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
// --- Manual emit tracker (set by actions.down / actions.emit) ---
|
|
864
|
+
_onManualEmit() {
|
|
865
|
+
this._manualEmitUsed = true;
|
|
866
|
+
}
|
|
867
|
+
// --- Up / unsubscribe ---
|
|
868
|
+
up(messages, options) {
|
|
869
|
+
if (!this._hasDeps) return;
|
|
870
|
+
if (!options?.internal && this._guard != null) {
|
|
871
|
+
const actor = normalizeActor(options?.actor);
|
|
872
|
+
if (!this._guard(actor, "write")) {
|
|
873
|
+
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
874
|
+
}
|
|
875
|
+
this._recordMutation(actor);
|
|
876
|
+
}
|
|
877
|
+
for (const dep of this._deps) {
|
|
878
|
+
if (options === void 0) {
|
|
879
|
+
dep.up?.(messages);
|
|
880
|
+
} else {
|
|
881
|
+
dep.up?.(messages, options);
|
|
882
|
+
}
|
|
852
883
|
}
|
|
853
884
|
}
|
|
854
|
-
|
|
855
|
-
if (!this.
|
|
856
|
-
|
|
885
|
+
_upInternal(messages) {
|
|
886
|
+
if (!this._hasDeps) return;
|
|
887
|
+
for (const dep of this._deps) {
|
|
888
|
+
dep.up?.(messages, { internal: true });
|
|
857
889
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
890
|
+
}
|
|
891
|
+
unsubscribe() {
|
|
892
|
+
if (!this._hasDeps) return;
|
|
893
|
+
this._disconnectUpstream();
|
|
894
|
+
}
|
|
895
|
+
// --- Activation (first-subscriber / last-subscriber hooks) ---
|
|
896
|
+
_onActivate() {
|
|
897
|
+
if (this._hasDeps) {
|
|
898
|
+
this._connectUpstream();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (this._fn) {
|
|
862
902
|
this._runFn();
|
|
903
|
+
return;
|
|
863
904
|
}
|
|
864
905
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
906
|
+
_doDeactivate() {
|
|
907
|
+
this._disconnectUpstream();
|
|
908
|
+
const cleanup = this._cleanup;
|
|
909
|
+
this._cleanup = void 0;
|
|
910
|
+
cleanup?.();
|
|
911
|
+
if (this._fn != null) {
|
|
912
|
+
this._cached = NO_VALUE;
|
|
913
|
+
this._lastDepValues = void 0;
|
|
914
|
+
}
|
|
915
|
+
if (this._hasDeps || this._fn != null) {
|
|
916
|
+
this._status = "disconnected";
|
|
868
917
|
}
|
|
869
918
|
}
|
|
919
|
+
// --- INVALIDATE / TEARDOWN hooks (clear fn state) ---
|
|
920
|
+
_onInvalidate() {
|
|
921
|
+
const cleanup = this._cleanup;
|
|
922
|
+
this._cleanup = void 0;
|
|
923
|
+
cleanup?.();
|
|
924
|
+
this._lastDepValues = void 0;
|
|
925
|
+
}
|
|
926
|
+
_onTeardown() {
|
|
927
|
+
const cleanup = this._cleanup;
|
|
928
|
+
this._cleanup = void 0;
|
|
929
|
+
cleanup?.();
|
|
930
|
+
}
|
|
931
|
+
// --- Upstream connect / disconnect ---
|
|
932
|
+
_connectUpstream() {
|
|
933
|
+
if (!this._hasDeps) return;
|
|
934
|
+
if (this._upstreamUnsubs.length > 0) return;
|
|
935
|
+
this._depDirtyMask.setAll();
|
|
936
|
+
this._depSettledMask.reset();
|
|
937
|
+
this._depCompleteMask.reset();
|
|
938
|
+
const depValuesBefore = this._lastDepValues;
|
|
939
|
+
const subHints = this._isSingleDep ? { singleDep: true } : void 0;
|
|
940
|
+
for (let i = 0; i < this._deps.length; i += 1) {
|
|
941
|
+
const dep = this._deps[i];
|
|
942
|
+
this._upstreamUnsubs.push(
|
|
943
|
+
dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
|
|
947
|
+
this._runFn();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
_disconnectUpstream() {
|
|
951
|
+
if (this._upstreamUnsubs.length === 0) return;
|
|
952
|
+
for (const unsub of this._upstreamUnsubs.splice(0)) {
|
|
953
|
+
unsub();
|
|
954
|
+
}
|
|
955
|
+
this._depDirtyMask.reset();
|
|
956
|
+
this._depSettledMask.reset();
|
|
957
|
+
this._depCompleteMask.reset();
|
|
958
|
+
}
|
|
959
|
+
// --- Wave handling ---
|
|
870
960
|
_handleDepMessages(index, messages) {
|
|
871
961
|
for (const msg of messages) {
|
|
872
|
-
this.
|
|
962
|
+
this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
|
|
873
963
|
const t = msg[0];
|
|
874
964
|
if (this._onMessage) {
|
|
875
965
|
try {
|
|
876
|
-
|
|
966
|
+
const consumed = this._onMessage(msg, index, this._actions);
|
|
967
|
+
if (consumed) {
|
|
968
|
+
if (t === START) {
|
|
969
|
+
this._depDirtyMask.clear(index);
|
|
970
|
+
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
971
|
+
this._depDirtyMask.reset();
|
|
972
|
+
this._depSettledMask.reset();
|
|
973
|
+
this._runFn();
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
877
978
|
} catch (err) {
|
|
878
979
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
879
980
|
const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
|
|
@@ -883,6 +984,7 @@ var NodeImpl = class {
|
|
|
883
984
|
return;
|
|
884
985
|
}
|
|
885
986
|
}
|
|
987
|
+
if (messageTier(t) < 1) continue;
|
|
886
988
|
if (!this._fn) {
|
|
887
989
|
if (t === COMPLETE && this._deps.length > 1) {
|
|
888
990
|
this._depCompleteMask.set(index);
|
|
@@ -926,53 +1028,85 @@ var NodeImpl = class {
|
|
|
926
1028
|
this._downInternal([msg]);
|
|
927
1029
|
}
|
|
928
1030
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
this.
|
|
932
|
-
this.
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
this._status = "settled";
|
|
936
|
-
const subHints = this._isSingleDep ? { singleDep: true } : void 0;
|
|
937
|
-
this._connecting = true;
|
|
938
|
-
try {
|
|
939
|
-
for (let i = 0; i < this._deps.length; i += 1) {
|
|
940
|
-
const dep = this._deps[i];
|
|
941
|
-
this._upstreamUnsubs.push(
|
|
942
|
-
dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
} finally {
|
|
946
|
-
this._connecting = false;
|
|
1031
|
+
_onDepDirty(index) {
|
|
1032
|
+
const wasDirty = this._depDirtyMask.has(index);
|
|
1033
|
+
this._depDirtyMask.set(index);
|
|
1034
|
+
this._depSettledMask.clear(index);
|
|
1035
|
+
if (!wasDirty) {
|
|
1036
|
+
this._downInternal([[DIRTY]]);
|
|
947
1037
|
}
|
|
948
|
-
|
|
1038
|
+
}
|
|
1039
|
+
_onDepSettled(index) {
|
|
1040
|
+
if (!this._depDirtyMask.has(index)) {
|
|
1041
|
+
this._onDepDirty(index);
|
|
1042
|
+
}
|
|
1043
|
+
this._depSettledMask.set(index);
|
|
1044
|
+
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
1045
|
+
this._depDirtyMask.reset();
|
|
1046
|
+
this._depSettledMask.reset();
|
|
949
1047
|
this._runFn();
|
|
950
1048
|
}
|
|
951
1049
|
}
|
|
952
|
-
|
|
953
|
-
if (
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
this._cleanup = void 0;
|
|
957
|
-
producerCleanup?.();
|
|
958
|
-
}
|
|
959
|
-
_startProducer() {
|
|
960
|
-
if (this._deps.length !== 0 || !this._fn || this._producerStarted) return;
|
|
961
|
-
this._producerStarted = true;
|
|
962
|
-
this._runFn();
|
|
1050
|
+
_maybeCompleteFromDeps() {
|
|
1051
|
+
if (this._autoComplete && this._deps.length > 0 && this._depCompleteMask.covers(this._allDepsCompleteMask)) {
|
|
1052
|
+
this._downInternal([[COMPLETE]]);
|
|
1053
|
+
}
|
|
963
1054
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1055
|
+
// --- Fn execution ---
|
|
1056
|
+
_runFn() {
|
|
1057
|
+
if (!this._fn) return;
|
|
1058
|
+
if (this._terminal && !this._resubscribable) return;
|
|
1059
|
+
try {
|
|
1060
|
+
const n = this._deps.length;
|
|
1061
|
+
const depValues = new Array(n);
|
|
1062
|
+
for (let i = 0; i < n; i++) depValues[i] = this._deps[i].get();
|
|
1063
|
+
const prev = this._lastDepValues;
|
|
1064
|
+
if (n > 0 && prev != null && prev.length === n) {
|
|
1065
|
+
let allSame = true;
|
|
1066
|
+
for (let i = 0; i < n; i++) {
|
|
1067
|
+
if (!Object.is(depValues[i], prev[i])) {
|
|
1068
|
+
allSame = false;
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (allSame) {
|
|
1073
|
+
if (this._status === "dirty") {
|
|
1074
|
+
this._downInternal([[RESOLVED]]);
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const prevCleanup = this._cleanup;
|
|
1080
|
+
this._cleanup = void 0;
|
|
1081
|
+
prevCleanup?.();
|
|
1082
|
+
this._manualEmitUsed = false;
|
|
1083
|
+
this._lastDepValues = depValues;
|
|
1084
|
+
this._emitInspectorHook({ kind: "run", depValues });
|
|
1085
|
+
const out = this._fn(depValues, this._actions);
|
|
1086
|
+
if (isCleanupResult(out)) {
|
|
1087
|
+
this._cleanup = out.cleanup;
|
|
1088
|
+
if (this._manualEmitUsed) return;
|
|
1089
|
+
if ("value" in out) {
|
|
1090
|
+
this._downAutoValue(out.value);
|
|
1091
|
+
}
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (isCleanupFn(out)) {
|
|
1095
|
+
this._cleanup = out;
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (this._manualEmitUsed) return;
|
|
1099
|
+
if (out === void 0) return;
|
|
1100
|
+
this._downAutoValue(out);
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1103
|
+
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
|
|
1104
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
968
1105
|
}
|
|
969
|
-
this._connected = false;
|
|
970
|
-
this._depDirtyMask.reset();
|
|
971
|
-
this._depSettledMask.reset();
|
|
972
|
-
this._depCompleteMask.reset();
|
|
973
|
-
this._status = "disconnected";
|
|
974
1106
|
}
|
|
975
1107
|
};
|
|
1108
|
+
var isNodeArray = (value) => Array.isArray(value);
|
|
1109
|
+
var isNodeOptions = (value) => typeof value === "object" && value != null && !Array.isArray(value);
|
|
976
1110
|
function node(depsOrFn, fnOrOpts, optsArg) {
|
|
977
1111
|
const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
|
|
978
1112
|
const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
|
|
@@ -988,227 +1122,44 @@ function node(depsOrFn, fnOrOpts, optsArg) {
|
|
|
988
1122
|
}
|
|
989
1123
|
|
|
990
1124
|
// src/core/dynamic-node.ts
|
|
991
|
-
var
|
|
992
|
-
|
|
993
|
-
_registryName;
|
|
994
|
-
_describeKind;
|
|
995
|
-
meta;
|
|
1125
|
+
var MAX_RERUN = 16;
|
|
1126
|
+
var DynamicNodeImpl = class extends NodeBase {
|
|
996
1127
|
_fn;
|
|
997
|
-
_equals;
|
|
998
|
-
_resubscribable;
|
|
999
|
-
_resetOnTeardown;
|
|
1000
1128
|
_autoComplete;
|
|
1001
|
-
_onMessage;
|
|
1002
|
-
_onResubscribe;
|
|
1003
|
-
/** @internal — read by {@link describeNode} for `accessHintForGuard`. */
|
|
1004
|
-
_guard;
|
|
1005
|
-
_lastMutation;
|
|
1006
|
-
_inspectorHook;
|
|
1007
|
-
// Sink tracking
|
|
1008
|
-
_sinkCount = 0;
|
|
1009
|
-
_singleDepSinkCount = 0;
|
|
1010
|
-
_singleDepSinks = /* @__PURE__ */ new WeakSet();
|
|
1011
|
-
// Actions object (for onMessage handler)
|
|
1012
|
-
_actions;
|
|
1013
|
-
_boundDownToSinks;
|
|
1014
|
-
// Mutable state
|
|
1015
|
-
_cached = NO_VALUE;
|
|
1016
|
-
_status = "disconnected";
|
|
1017
|
-
_terminal = false;
|
|
1018
|
-
_connected = false;
|
|
1019
|
-
_rewiring = false;
|
|
1020
|
-
// re-entrancy guard
|
|
1021
1129
|
// Dynamic deps tracking
|
|
1130
|
+
/** @internal Read by `describeNode`. */
|
|
1022
1131
|
_deps = [];
|
|
1023
1132
|
_depUnsubs = [];
|
|
1024
1133
|
_depIndexMap = /* @__PURE__ */ new Map();
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1134
|
+
_depDirtyBits = /* @__PURE__ */ new Set();
|
|
1135
|
+
_depSettledBits = /* @__PURE__ */ new Set();
|
|
1136
|
+
_depCompleteBits = /* @__PURE__ */ new Set();
|
|
1137
|
+
// Execution state
|
|
1138
|
+
_running = false;
|
|
1139
|
+
_rewiring = false;
|
|
1140
|
+
_bufferedDepMessages = [];
|
|
1141
|
+
_trackedValues = /* @__PURE__ */ new Map();
|
|
1142
|
+
_rerunCount = 0;
|
|
1031
1143
|
constructor(fn, opts) {
|
|
1144
|
+
super(opts);
|
|
1032
1145
|
this._fn = fn;
|
|
1033
|
-
this._optsName = opts.name;
|
|
1034
|
-
this._describeKind = opts.describeKind;
|
|
1035
|
-
this._equals = opts.equals ?? Object.is;
|
|
1036
|
-
this._resubscribable = opts.resubscribable ?? false;
|
|
1037
|
-
this._resetOnTeardown = opts.resetOnTeardown ?? false;
|
|
1038
1146
|
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1039
|
-
this.
|
|
1040
|
-
this.
|
|
1041
|
-
this._guard = opts.guard;
|
|
1042
|
-
this._inspectorHook = void 0;
|
|
1043
|
-
const meta = {};
|
|
1044
|
-
for (const [k, v] of Object.entries(opts.meta ?? {})) {
|
|
1045
|
-
meta[k] = node({
|
|
1046
|
-
initial: v,
|
|
1047
|
-
name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
|
|
1048
|
-
describeKind: "state",
|
|
1049
|
-
...opts.guard != null ? { guard: opts.guard } : {}
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
Object.freeze(meta);
|
|
1053
|
-
this.meta = meta;
|
|
1054
|
-
const self = this;
|
|
1055
|
-
this._actions = {
|
|
1056
|
-
down(messages) {
|
|
1057
|
-
self._downInternal(messages);
|
|
1058
|
-
},
|
|
1059
|
-
emit(value) {
|
|
1060
|
-
self._downAutoValue(value);
|
|
1061
|
-
},
|
|
1062
|
-
up(messages) {
|
|
1063
|
-
for (const dep of self._deps) {
|
|
1064
|
-
dep.up?.(messages, { internal: true });
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
this._boundDownToSinks = this._downToSinks.bind(this);
|
|
1069
|
-
}
|
|
1070
|
-
get name() {
|
|
1071
|
-
return this._registryName ?? this._optsName;
|
|
1072
|
-
}
|
|
1073
|
-
/** @internal */
|
|
1074
|
-
_assignRegistryName(localName) {
|
|
1075
|
-
if (this._optsName !== void 0 || this._registryName !== void 0) return;
|
|
1076
|
-
this._registryName = localName;
|
|
1077
|
-
}
|
|
1078
|
-
/**
|
|
1079
|
-
* @internal Attach/remove inspector hook for graph-level observability.
|
|
1080
|
-
* Returns a disposer that restores the previous hook.
|
|
1081
|
-
*/
|
|
1082
|
-
_setInspectorHook(hook) {
|
|
1083
|
-
const prev = this._inspectorHook;
|
|
1084
|
-
this._inspectorHook = hook;
|
|
1085
|
-
return () => {
|
|
1086
|
-
if (this._inspectorHook === hook) {
|
|
1087
|
-
this._inspectorHook = prev;
|
|
1088
|
-
}
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
get status() {
|
|
1092
|
-
return this._status;
|
|
1147
|
+
this.down = this.down.bind(this);
|
|
1148
|
+
this.up = this.up.bind(this);
|
|
1093
1149
|
}
|
|
1094
|
-
|
|
1095
|
-
return
|
|
1150
|
+
_createMetaNode(key, initialValue, opts) {
|
|
1151
|
+
return node({
|
|
1152
|
+
initial: initialValue,
|
|
1153
|
+
name: `${opts.name ?? "dynamicNode"}:meta:${key}`,
|
|
1154
|
+
describeKind: "state",
|
|
1155
|
+
...opts.guard != null ? { guard: opts.guard } : {}
|
|
1156
|
+
});
|
|
1096
1157
|
}
|
|
1097
|
-
/** Versioning not
|
|
1158
|
+
/** Versioning not supported on DynamicNodeImpl (override base). */
|
|
1098
1159
|
get v() {
|
|
1099
1160
|
return void 0;
|
|
1100
1161
|
}
|
|
1101
|
-
|
|
1102
|
-
return this._guard != null;
|
|
1103
|
-
}
|
|
1104
|
-
allowsObserve(actor) {
|
|
1105
|
-
if (this._guard == null) return true;
|
|
1106
|
-
return this._guard(normalizeActor(actor), "observe");
|
|
1107
|
-
}
|
|
1108
|
-
get() {
|
|
1109
|
-
return this._cached === NO_VALUE ? void 0 : this._cached;
|
|
1110
|
-
}
|
|
1111
|
-
down(messages, options) {
|
|
1112
|
-
if (messages.length === 0) return;
|
|
1113
|
-
if (!options?.internal && this._guard != null) {
|
|
1114
|
-
const actor = normalizeActor(options?.actor);
|
|
1115
|
-
const delivery = options?.delivery ?? "write";
|
|
1116
|
-
const action = delivery === "signal" ? "signal" : "write";
|
|
1117
|
-
if (!this._guard(actor, action)) {
|
|
1118
|
-
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
1119
|
-
}
|
|
1120
|
-
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
1121
|
-
}
|
|
1122
|
-
this._downInternal(messages);
|
|
1123
|
-
}
|
|
1124
|
-
_downInternal(messages) {
|
|
1125
|
-
if (messages.length === 0) return;
|
|
1126
|
-
let sinkMessages = messages;
|
|
1127
|
-
if (this._terminal && !this._resubscribable) {
|
|
1128
|
-
const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
1129
|
-
if (pass.length === 0) return;
|
|
1130
|
-
sinkMessages = pass;
|
|
1131
|
-
}
|
|
1132
|
-
this._handleLocalLifecycle(sinkMessages);
|
|
1133
|
-
if (this._canSkipDirty()) {
|
|
1134
|
-
let hasPhase2 = false;
|
|
1135
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
1136
|
-
const t = sinkMessages[i][0];
|
|
1137
|
-
if (t === DATA || t === RESOLVED) {
|
|
1138
|
-
hasPhase2 = true;
|
|
1139
|
-
break;
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
if (hasPhase2) {
|
|
1143
|
-
const filtered = [];
|
|
1144
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
1145
|
-
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
1146
|
-
}
|
|
1147
|
-
if (filtered.length > 0) {
|
|
1148
|
-
downWithBatch(this._boundDownToSinks, filtered);
|
|
1149
|
-
}
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
1154
|
-
}
|
|
1155
|
-
_canSkipDirty() {
|
|
1156
|
-
return this._sinkCount === 1 && this._singleDepSinkCount === 1;
|
|
1157
|
-
}
|
|
1158
|
-
subscribe(sink, hints) {
|
|
1159
|
-
if (hints?.actor != null && this._guard != null) {
|
|
1160
|
-
const actor = normalizeActor(hints.actor);
|
|
1161
|
-
if (!this._guard(actor, "observe")) {
|
|
1162
|
-
throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
if (this._terminal && this._resubscribable) {
|
|
1166
|
-
this._terminal = false;
|
|
1167
|
-
this._cached = NO_VALUE;
|
|
1168
|
-
this._status = "disconnected";
|
|
1169
|
-
this._onResubscribe?.();
|
|
1170
|
-
}
|
|
1171
|
-
this._sinkCount += 1;
|
|
1172
|
-
if (hints?.singleDep) {
|
|
1173
|
-
this._singleDepSinkCount += 1;
|
|
1174
|
-
this._singleDepSinks.add(sink);
|
|
1175
|
-
}
|
|
1176
|
-
if (this._sinks == null) {
|
|
1177
|
-
this._sinks = sink;
|
|
1178
|
-
} else if (typeof this._sinks === "function") {
|
|
1179
|
-
this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
|
|
1180
|
-
} else {
|
|
1181
|
-
this._sinks.add(sink);
|
|
1182
|
-
}
|
|
1183
|
-
if (!this._connected) {
|
|
1184
|
-
this._connect();
|
|
1185
|
-
}
|
|
1186
|
-
let removed = false;
|
|
1187
|
-
return () => {
|
|
1188
|
-
if (removed) return;
|
|
1189
|
-
removed = true;
|
|
1190
|
-
this._sinkCount -= 1;
|
|
1191
|
-
if (this._singleDepSinks.has(sink)) {
|
|
1192
|
-
this._singleDepSinkCount -= 1;
|
|
1193
|
-
this._singleDepSinks.delete(sink);
|
|
1194
|
-
}
|
|
1195
|
-
if (this._sinks == null) return;
|
|
1196
|
-
if (typeof this._sinks === "function") {
|
|
1197
|
-
if (this._sinks === sink) this._sinks = null;
|
|
1198
|
-
} else {
|
|
1199
|
-
this._sinks.delete(sink);
|
|
1200
|
-
if (this._sinks.size === 1) {
|
|
1201
|
-
const [only] = this._sinks;
|
|
1202
|
-
this._sinks = only;
|
|
1203
|
-
} else if (this._sinks.size === 0) {
|
|
1204
|
-
this._sinks = null;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
if (this._sinks == null) {
|
|
1208
|
-
this._disconnect();
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1162
|
+
// --- Up / unsubscribe ---
|
|
1212
1163
|
up(messages, options) {
|
|
1213
1164
|
if (this._deps.length === 0) return;
|
|
1214
1165
|
if (!options?.internal && this._guard != null) {
|
|
@@ -1216,221 +1167,227 @@ var DynamicNodeImpl = class {
|
|
|
1216
1167
|
if (!this._guard(actor, "write")) {
|
|
1217
1168
|
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
1218
1169
|
}
|
|
1219
|
-
this.
|
|
1170
|
+
this._recordMutation(actor);
|
|
1220
1171
|
}
|
|
1221
1172
|
for (const dep of this._deps) {
|
|
1222
1173
|
dep.up?.(messages, options);
|
|
1223
1174
|
}
|
|
1224
1175
|
}
|
|
1225
|
-
|
|
1226
|
-
this.
|
|
1227
|
-
|
|
1228
|
-
// --- Private methods ---
|
|
1229
|
-
_downToSinks(messages) {
|
|
1230
|
-
if (this._sinks == null) return;
|
|
1231
|
-
if (typeof this._sinks === "function") {
|
|
1232
|
-
this._sinks(messages);
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
const snapshot = [...this._sinks];
|
|
1236
|
-
for (const sink of snapshot) {
|
|
1237
|
-
sink(messages);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
_handleLocalLifecycle(messages) {
|
|
1241
|
-
for (const m of messages) {
|
|
1242
|
-
const t = m[0];
|
|
1243
|
-
if (t === DATA) this._cached = m[1];
|
|
1244
|
-
if (t === INVALIDATE) {
|
|
1245
|
-
this._cached = NO_VALUE;
|
|
1246
|
-
this._status = "dirty";
|
|
1247
|
-
}
|
|
1248
|
-
if (t === DATA) {
|
|
1249
|
-
this._status = "settled";
|
|
1250
|
-
} else if (t === RESOLVED) {
|
|
1251
|
-
this._status = "resolved";
|
|
1252
|
-
} else if (t === DIRTY) {
|
|
1253
|
-
this._status = "dirty";
|
|
1254
|
-
} else if (t === COMPLETE) {
|
|
1255
|
-
this._status = "completed";
|
|
1256
|
-
this._terminal = true;
|
|
1257
|
-
} else if (t === ERROR) {
|
|
1258
|
-
this._status = "errored";
|
|
1259
|
-
this._terminal = true;
|
|
1260
|
-
}
|
|
1261
|
-
if (t === TEARDOWN) {
|
|
1262
|
-
if (this._resetOnTeardown) this._cached = NO_VALUE;
|
|
1263
|
-
try {
|
|
1264
|
-
this._propagateToMeta(t);
|
|
1265
|
-
} finally {
|
|
1266
|
-
this._disconnect();
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
if (t !== TEARDOWN && propagatesToMeta(t)) {
|
|
1270
|
-
this._propagateToMeta(t);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
/** Propagate a signal to all companion meta nodes (best-effort). */
|
|
1275
|
-
_propagateToMeta(t) {
|
|
1276
|
-
for (const metaNode of Object.values(this.meta)) {
|
|
1277
|
-
try {
|
|
1278
|
-
metaNode.down([[t]], { internal: true });
|
|
1279
|
-
} catch {
|
|
1280
|
-
}
|
|
1176
|
+
_upInternal(messages) {
|
|
1177
|
+
for (const dep of this._deps) {
|
|
1178
|
+
dep.up?.(messages, { internal: true });
|
|
1281
1179
|
}
|
|
1282
1180
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
let unchanged;
|
|
1286
|
-
try {
|
|
1287
|
-
unchanged = this._cached !== NO_VALUE && this._equals(this._cached, value);
|
|
1288
|
-
} catch (eqErr) {
|
|
1289
|
-
const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
|
|
1290
|
-
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, { cause: eqErr });
|
|
1291
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
if (unchanged) {
|
|
1295
|
-
this._downInternal(wasDirty ? [[RESOLVED]] : [[DIRTY], [RESOLVED]]);
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
this._cached = value;
|
|
1299
|
-
this._downInternal(wasDirty ? [[DATA, value]] : [[DIRTY], [DATA, value]]);
|
|
1181
|
+
unsubscribe() {
|
|
1182
|
+
this._disconnect();
|
|
1300
1183
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
this._connected = true;
|
|
1304
|
-
this._status = "settled";
|
|
1305
|
-
this._dirtyBits.clear();
|
|
1306
|
-
this._settledBits.clear();
|
|
1307
|
-
this._completeBits.clear();
|
|
1184
|
+
// --- Activation hooks ---
|
|
1185
|
+
_onActivate() {
|
|
1308
1186
|
this._runFn();
|
|
1309
1187
|
}
|
|
1188
|
+
_doDeactivate() {
|
|
1189
|
+
this._disconnect();
|
|
1190
|
+
}
|
|
1310
1191
|
_disconnect() {
|
|
1311
|
-
if (!this._connected) return;
|
|
1312
1192
|
for (const unsub of this._depUnsubs) unsub();
|
|
1313
1193
|
this._depUnsubs = [];
|
|
1314
1194
|
this._deps = [];
|
|
1315
1195
|
this._depIndexMap.clear();
|
|
1316
|
-
this.
|
|
1317
|
-
this.
|
|
1318
|
-
this.
|
|
1319
|
-
this.
|
|
1196
|
+
this._depDirtyBits.clear();
|
|
1197
|
+
this._depSettledBits.clear();
|
|
1198
|
+
this._depCompleteBits.clear();
|
|
1199
|
+
this._cached = NO_VALUE;
|
|
1320
1200
|
this._status = "disconnected";
|
|
1321
1201
|
}
|
|
1202
|
+
// --- Fn execution with rewire buffer ---
|
|
1322
1203
|
_runFn() {
|
|
1323
1204
|
if (this._terminal && !this._resubscribable) return;
|
|
1324
|
-
if (this.
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
if (!trackedSet.has(dep)) {
|
|
1329
|
-
trackedSet.add(dep);
|
|
1330
|
-
trackedDeps.push(dep);
|
|
1331
|
-
}
|
|
1332
|
-
return dep.get();
|
|
1333
|
-
};
|
|
1205
|
+
if (this._running) return;
|
|
1206
|
+
this._running = true;
|
|
1207
|
+
this._rerunCount = 0;
|
|
1208
|
+
let result;
|
|
1334
1209
|
try {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1210
|
+
for (; ; ) {
|
|
1211
|
+
const trackedDeps = [];
|
|
1212
|
+
const trackedValuesMap = /* @__PURE__ */ new Map();
|
|
1213
|
+
const trackedSet = /* @__PURE__ */ new Set();
|
|
1214
|
+
const get = (dep) => {
|
|
1215
|
+
if (!trackedSet.has(dep)) {
|
|
1216
|
+
trackedSet.add(dep);
|
|
1217
|
+
trackedDeps.push(dep);
|
|
1218
|
+
trackedValuesMap.set(dep, dep.get());
|
|
1219
|
+
}
|
|
1220
|
+
return dep.get();
|
|
1221
|
+
};
|
|
1222
|
+
this._trackedValues = trackedValuesMap;
|
|
1223
|
+
const depValues = [];
|
|
1224
|
+
for (const dep of this._deps) depValues.push(dep.get());
|
|
1225
|
+
this._emitInspectorHook({ kind: "run", depValues });
|
|
1226
|
+
try {
|
|
1227
|
+
result = this._fn(get);
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1230
|
+
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, {
|
|
1231
|
+
cause: err
|
|
1232
|
+
});
|
|
1233
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
this._rewiring = true;
|
|
1237
|
+
this._bufferedDepMessages = [];
|
|
1238
|
+
try {
|
|
1239
|
+
this._rewire(trackedDeps);
|
|
1240
|
+
} finally {
|
|
1241
|
+
this._rewiring = false;
|
|
1242
|
+
}
|
|
1243
|
+
let needsRerun = false;
|
|
1244
|
+
for (const entry of this._bufferedDepMessages) {
|
|
1245
|
+
for (const msg of entry.msgs) {
|
|
1246
|
+
if (msg[0] === DATA) {
|
|
1247
|
+
const dep = this._deps[entry.index];
|
|
1248
|
+
const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
|
|
1249
|
+
const actualValue = msg[1];
|
|
1250
|
+
if (!this._equals(trackedValue, actualValue)) {
|
|
1251
|
+
needsRerun = true;
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (needsRerun) break;
|
|
1257
|
+
}
|
|
1258
|
+
if (needsRerun) {
|
|
1259
|
+
this._rerunCount += 1;
|
|
1260
|
+
if (this._rerunCount > MAX_RERUN) {
|
|
1261
|
+
this._bufferedDepMessages = [];
|
|
1262
|
+
this._downInternal([
|
|
1263
|
+
[
|
|
1264
|
+
ERROR,
|
|
1265
|
+
new Error(
|
|
1266
|
+
`dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
|
|
1267
|
+
)
|
|
1268
|
+
]
|
|
1269
|
+
]);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
this._bufferedDepMessages = [];
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
const drain = this._bufferedDepMessages;
|
|
1276
|
+
this._bufferedDepMessages = [];
|
|
1277
|
+
for (const entry of drain) {
|
|
1278
|
+
for (const msg of entry.msgs) {
|
|
1279
|
+
this._updateMasksForMessage(entry.index, msg);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
this._depDirtyBits.clear();
|
|
1283
|
+
this._depSettledBits.clear();
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
} finally {
|
|
1287
|
+
this._running = false;
|
|
1346
1288
|
}
|
|
1289
|
+
this._downAutoValue(result);
|
|
1347
1290
|
}
|
|
1348
1291
|
_rewire(newDeps) {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
|
|
1365
|
-
newUnsubs.push(unsub);
|
|
1366
|
-
}
|
|
1292
|
+
const oldMap = this._depIndexMap;
|
|
1293
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1294
|
+
const newUnsubs = [];
|
|
1295
|
+
for (let i = 0; i < newDeps.length; i++) {
|
|
1296
|
+
const dep = newDeps[i];
|
|
1297
|
+
newMap.set(dep, i);
|
|
1298
|
+
const oldIdx = oldMap.get(dep);
|
|
1299
|
+
if (oldIdx !== void 0) {
|
|
1300
|
+
newUnsubs.push(this._depUnsubs[oldIdx]);
|
|
1301
|
+
this._depUnsubs[oldIdx] = () => {
|
|
1302
|
+
};
|
|
1303
|
+
} else {
|
|
1304
|
+
const idx = i;
|
|
1305
|
+
const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
|
|
1306
|
+
newUnsubs.push(unsub);
|
|
1367
1307
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1308
|
+
}
|
|
1309
|
+
for (const [dep, oldIdx] of oldMap) {
|
|
1310
|
+
if (!newMap.has(dep)) {
|
|
1311
|
+
this._depUnsubs[oldIdx]();
|
|
1372
1312
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1313
|
+
}
|
|
1314
|
+
this._deps = newDeps;
|
|
1315
|
+
this._depUnsubs = newUnsubs;
|
|
1316
|
+
this._depIndexMap = newMap;
|
|
1317
|
+
this._depDirtyBits.clear();
|
|
1318
|
+
this._depSettledBits.clear();
|
|
1319
|
+
const newCompleteBits = /* @__PURE__ */ new Set();
|
|
1320
|
+
for (const oldIdx of this._depCompleteBits) {
|
|
1321
|
+
for (const [dep, idx] of oldMap) {
|
|
1322
|
+
if (idx === oldIdx && newMap.has(dep)) {
|
|
1382
1323
|
newCompleteBits.add(newMap.get(dep));
|
|
1324
|
+
break;
|
|
1383
1325
|
}
|
|
1384
1326
|
}
|
|
1385
|
-
this._completeBits = newCompleteBits;
|
|
1386
|
-
} finally {
|
|
1387
|
-
this._rewiring = false;
|
|
1388
1327
|
}
|
|
1328
|
+
this._depCompleteBits = newCompleteBits;
|
|
1389
1329
|
}
|
|
1330
|
+
// --- Dep message handling ---
|
|
1390
1331
|
_handleDepMessages(index, messages) {
|
|
1391
|
-
if (this._rewiring)
|
|
1332
|
+
if (this._rewiring) {
|
|
1333
|
+
this._bufferedDepMessages.push({ index, msgs: messages });
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1392
1336
|
for (const msg of messages) {
|
|
1393
|
-
this.
|
|
1337
|
+
this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
|
|
1394
1338
|
const t = msg[0];
|
|
1395
1339
|
if (this._onMessage) {
|
|
1396
1340
|
try {
|
|
1397
1341
|
if (this._onMessage(msg, index, this._actions)) continue;
|
|
1398
1342
|
} catch (err) {
|
|
1399
|
-
|
|
1343
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1344
|
+
const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
|
|
1345
|
+
cause: err
|
|
1346
|
+
});
|
|
1347
|
+
this._downInternal([[ERROR, wrapped]]);
|
|
1400
1348
|
return;
|
|
1401
1349
|
}
|
|
1402
1350
|
}
|
|
1351
|
+
if (messageTier(t) < 1) continue;
|
|
1403
1352
|
if (t === DIRTY) {
|
|
1404
|
-
this.
|
|
1405
|
-
this.
|
|
1406
|
-
|
|
1407
|
-
|
|
1353
|
+
const wasEmpty = this._depDirtyBits.size === 0;
|
|
1354
|
+
this._depDirtyBits.add(index);
|
|
1355
|
+
this._depSettledBits.delete(index);
|
|
1356
|
+
if (wasEmpty) {
|
|
1357
|
+
this._downInternal([[DIRTY]]);
|
|
1408
1358
|
}
|
|
1409
1359
|
continue;
|
|
1410
1360
|
}
|
|
1411
1361
|
if (t === DATA || t === RESOLVED) {
|
|
1412
|
-
if (!this.
|
|
1413
|
-
this.
|
|
1414
|
-
|
|
1362
|
+
if (!this._depDirtyBits.has(index)) {
|
|
1363
|
+
const wasEmpty = this._depDirtyBits.size === 0;
|
|
1364
|
+
this._depDirtyBits.add(index);
|
|
1365
|
+
if (wasEmpty) {
|
|
1366
|
+
this._downInternal([[DIRTY]]);
|
|
1367
|
+
}
|
|
1415
1368
|
}
|
|
1416
|
-
this.
|
|
1369
|
+
this._depSettledBits.add(index);
|
|
1417
1370
|
if (this._allDirtySettled()) {
|
|
1418
|
-
this.
|
|
1419
|
-
this.
|
|
1420
|
-
this.
|
|
1371
|
+
this._depDirtyBits.clear();
|
|
1372
|
+
this._depSettledBits.clear();
|
|
1373
|
+
if (!this._running) {
|
|
1374
|
+
if (this._depValuesDifferFromTracked()) {
|
|
1375
|
+
this._runFn();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1421
1378
|
}
|
|
1422
1379
|
continue;
|
|
1423
1380
|
}
|
|
1424
1381
|
if (t === COMPLETE) {
|
|
1425
|
-
this.
|
|
1426
|
-
this.
|
|
1427
|
-
this.
|
|
1382
|
+
this._depCompleteBits.add(index);
|
|
1383
|
+
this._depDirtyBits.delete(index);
|
|
1384
|
+
this._depSettledBits.delete(index);
|
|
1428
1385
|
if (this._allDirtySettled()) {
|
|
1429
|
-
this.
|
|
1430
|
-
this.
|
|
1431
|
-
this._runFn();
|
|
1386
|
+
this._depDirtyBits.clear();
|
|
1387
|
+
this._depSettledBits.clear();
|
|
1388
|
+
if (!this._running) this._runFn();
|
|
1432
1389
|
}
|
|
1433
|
-
if (this._autoComplete && this.
|
|
1390
|
+
if (this._autoComplete && this._depCompleteBits.size >= this._deps.length && this._deps.length > 0) {
|
|
1434
1391
|
this._downInternal([[COMPLETE]]);
|
|
1435
1392
|
}
|
|
1436
1393
|
continue;
|
|
@@ -1446,13 +1403,46 @@ var DynamicNodeImpl = class {
|
|
|
1446
1403
|
this._downInternal([msg]);
|
|
1447
1404
|
}
|
|
1448
1405
|
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Update dep masks for a message without triggering `_runFn` — used
|
|
1408
|
+
* during post-rewire drain so the wave state is consistent with the
|
|
1409
|
+
* buffered activation cascade without recursing.
|
|
1410
|
+
*/
|
|
1411
|
+
_updateMasksForMessage(index, msg) {
|
|
1412
|
+
const t = msg[0];
|
|
1413
|
+
if (t === DIRTY) {
|
|
1414
|
+
this._depDirtyBits.add(index);
|
|
1415
|
+
this._depSettledBits.delete(index);
|
|
1416
|
+
} else if (t === DATA || t === RESOLVED) {
|
|
1417
|
+
this._depDirtyBits.add(index);
|
|
1418
|
+
this._depSettledBits.add(index);
|
|
1419
|
+
} else if (t === COMPLETE) {
|
|
1420
|
+
this._depCompleteBits.add(index);
|
|
1421
|
+
this._depDirtyBits.delete(index);
|
|
1422
|
+
this._depSettledBits.delete(index);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1449
1425
|
_allDirtySettled() {
|
|
1450
|
-
if (this.
|
|
1451
|
-
for (const idx of this.
|
|
1452
|
-
if (!this.
|
|
1426
|
+
if (this._depDirtyBits.size === 0) return false;
|
|
1427
|
+
for (const idx of this._depDirtyBits) {
|
|
1428
|
+
if (!this._depSettledBits.has(idx)) return false;
|
|
1453
1429
|
}
|
|
1454
1430
|
return true;
|
|
1455
1431
|
}
|
|
1432
|
+
/**
|
|
1433
|
+
* True if any current dep value differs from what the last `_runFn`
|
|
1434
|
+
* saw via `get()`. Used to suppress redundant re-runs when deferred
|
|
1435
|
+
* handshake messages arrive after `_rewire` for a dep whose value
|
|
1436
|
+
* already matches `_trackedValues`.
|
|
1437
|
+
*/
|
|
1438
|
+
_depValuesDifferFromTracked() {
|
|
1439
|
+
for (const dep of this._deps) {
|
|
1440
|
+
const current = dep.get();
|
|
1441
|
+
const tracked = this._trackedValues.get(dep);
|
|
1442
|
+
if (!this._equals(current, tracked)) return true;
|
|
1443
|
+
}
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1456
1446
|
};
|
|
1457
1447
|
|
|
1458
1448
|
// src/core/meta.ts
|
|
@@ -1522,6 +1512,10 @@ function describeNode(node2, includeFields) {
|
|
|
1522
1512
|
out.name = node2.name;
|
|
1523
1513
|
}
|
|
1524
1514
|
if (all || includeFields.has("value")) {
|
|
1515
|
+
const isSentinel = node2 instanceof NodeImpl && node2._cached === NO_VALUE || node2 instanceof DynamicNodeImpl && node2._cached === NO_VALUE;
|
|
1516
|
+
if (isSentinel) {
|
|
1517
|
+
out.sentinel = true;
|
|
1518
|
+
}
|
|
1525
1519
|
try {
|
|
1526
1520
|
out.value = node2.get();
|
|
1527
1521
|
} catch {
|
|
@@ -1553,6 +1547,129 @@ function state(initial, opts) {
|
|
|
1553
1547
|
return node([], { ...opts, initial });
|
|
1554
1548
|
}
|
|
1555
1549
|
|
|
1550
|
+
// src/graph/sizeof.ts
|
|
1551
|
+
var OVERHEAD = {
|
|
1552
|
+
object: 56,
|
|
1553
|
+
array: 64,
|
|
1554
|
+
string: 40,
|
|
1555
|
+
// header; content added separately
|
|
1556
|
+
number: 8,
|
|
1557
|
+
boolean: 4,
|
|
1558
|
+
null: 0,
|
|
1559
|
+
undefined: 0,
|
|
1560
|
+
symbol: 40,
|
|
1561
|
+
bigint: 16,
|
|
1562
|
+
function: 120,
|
|
1563
|
+
map: 72,
|
|
1564
|
+
set: 72,
|
|
1565
|
+
mapEntry: 40,
|
|
1566
|
+
setEntry: 24
|
|
1567
|
+
};
|
|
1568
|
+
function sizeof(value) {
|
|
1569
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1570
|
+
return _sizeof(value, seen);
|
|
1571
|
+
}
|
|
1572
|
+
function _sizeof(value, seen) {
|
|
1573
|
+
if (value == null) return 0;
|
|
1574
|
+
const t = typeof value;
|
|
1575
|
+
switch (t) {
|
|
1576
|
+
case "number":
|
|
1577
|
+
return OVERHEAD.number;
|
|
1578
|
+
case "boolean":
|
|
1579
|
+
return OVERHEAD.boolean;
|
|
1580
|
+
case "string":
|
|
1581
|
+
return OVERHEAD.string + value.length * 2;
|
|
1582
|
+
// UTF-16
|
|
1583
|
+
case "bigint":
|
|
1584
|
+
return OVERHEAD.bigint;
|
|
1585
|
+
case "symbol":
|
|
1586
|
+
return OVERHEAD.symbol;
|
|
1587
|
+
case "function":
|
|
1588
|
+
if (seen.has(value)) return 0;
|
|
1589
|
+
seen.add(value);
|
|
1590
|
+
return OVERHEAD.function;
|
|
1591
|
+
case "undefined":
|
|
1592
|
+
return 0;
|
|
1593
|
+
}
|
|
1594
|
+
const obj = value;
|
|
1595
|
+
if (seen.has(obj)) return 0;
|
|
1596
|
+
seen.add(obj);
|
|
1597
|
+
if (obj instanceof Map) {
|
|
1598
|
+
let size2 = OVERHEAD.map;
|
|
1599
|
+
for (const [k, v] of obj) {
|
|
1600
|
+
size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
|
|
1601
|
+
}
|
|
1602
|
+
return size2;
|
|
1603
|
+
}
|
|
1604
|
+
if (obj instanceof Set) {
|
|
1605
|
+
let size2 = OVERHEAD.set;
|
|
1606
|
+
for (const v of obj) {
|
|
1607
|
+
size2 += OVERHEAD.setEntry + _sizeof(v, seen);
|
|
1608
|
+
}
|
|
1609
|
+
return size2;
|
|
1610
|
+
}
|
|
1611
|
+
if (Array.isArray(obj)) {
|
|
1612
|
+
let size2 = OVERHEAD.array + obj.length * 8;
|
|
1613
|
+
for (const item of obj) {
|
|
1614
|
+
size2 += _sizeof(item, seen);
|
|
1615
|
+
}
|
|
1616
|
+
return size2;
|
|
1617
|
+
}
|
|
1618
|
+
if (obj instanceof ArrayBuffer) return obj.byteLength;
|
|
1619
|
+
if (ArrayBuffer.isView(obj)) return obj.byteLength;
|
|
1620
|
+
let size = OVERHEAD.object;
|
|
1621
|
+
const keys = Object.keys(obj);
|
|
1622
|
+
for (const key of keys) {
|
|
1623
|
+
size += OVERHEAD.string + key.length * 2;
|
|
1624
|
+
size += _sizeof(obj[key], seen);
|
|
1625
|
+
}
|
|
1626
|
+
return size;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/graph/profile.ts
|
|
1630
|
+
function graphProfile(graph, opts) {
|
|
1631
|
+
const topN = opts?.topN ?? 10;
|
|
1632
|
+
const desc = graph.describe({ detail: "standard" });
|
|
1633
|
+
const targets = [];
|
|
1634
|
+
if (typeof graph._collectObserveTargets === "function") {
|
|
1635
|
+
graph._collectObserveTargets("", targets);
|
|
1636
|
+
}
|
|
1637
|
+
const pathToNode = /* @__PURE__ */ new Map();
|
|
1638
|
+
for (const [p, n] of targets) {
|
|
1639
|
+
pathToNode.set(p, n);
|
|
1640
|
+
}
|
|
1641
|
+
const profiles = [];
|
|
1642
|
+
for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
|
|
1643
|
+
const nd = pathToNode.get(path);
|
|
1644
|
+
const impl = nd instanceof NodeImpl ? nd : null;
|
|
1645
|
+
const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
|
|
1646
|
+
const subscriberCount = impl ? impl._sinkCount : 0;
|
|
1647
|
+
const depCount = nodeDesc.deps?.length ?? 0;
|
|
1648
|
+
const isOrphanEffect = nodeDesc.type === "effect" && subscriberCount === 0;
|
|
1649
|
+
profiles.push({
|
|
1650
|
+
path,
|
|
1651
|
+
type: nodeDesc.type,
|
|
1652
|
+
status: nodeDesc.status ?? "unknown",
|
|
1653
|
+
valueSizeBytes,
|
|
1654
|
+
subscriberCount,
|
|
1655
|
+
depCount,
|
|
1656
|
+
isOrphanEffect
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
|
|
1660
|
+
const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
|
|
1661
|
+
const orphanEffects = profiles.filter((p) => p.isOrphanEffect);
|
|
1662
|
+
return {
|
|
1663
|
+
nodeCount: profiles.length,
|
|
1664
|
+
edgeCount: desc.edges.length,
|
|
1665
|
+
subgraphCount: desc.subgraphs.length,
|
|
1666
|
+
nodes: profiles,
|
|
1667
|
+
totalValueSizeBytes,
|
|
1668
|
+
hotspots,
|
|
1669
|
+
orphanEffects
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1556
1673
|
// src/graph/graph.ts
|
|
1557
1674
|
var PATH_SEP = "::";
|
|
1558
1675
|
var GRAPH_META_SEGMENT = "__meta__";
|
|
@@ -2465,6 +2582,16 @@ var Graph = class _Graph {
|
|
|
2465
2582
|
}
|
|
2466
2583
|
return out;
|
|
2467
2584
|
}
|
|
2585
|
+
/**
|
|
2586
|
+
* Snapshot-based resource profile: per-node stats, orphan effect detection,
|
|
2587
|
+
* memory hotspots. Zero runtime overhead — walks nodes on demand.
|
|
2588
|
+
*
|
|
2589
|
+
* @param opts - Optional `topN` for hotspot limit (default 10).
|
|
2590
|
+
* @returns Aggregate profile with per-node details, hotspots, and orphan effects.
|
|
2591
|
+
*/
|
|
2592
|
+
resourceProfile(opts) {
|
|
2593
|
+
return graphProfile(this, opts);
|
|
2594
|
+
}
|
|
2468
2595
|
_qualifyEdgeEndpoint(part, prefix) {
|
|
2469
2596
|
if (part.includes(PATH_SEP)) return part;
|
|
2470
2597
|
return prefix === "" ? part : `${prefix}${PATH_SEP}${part}`;
|
|
@@ -2566,8 +2693,8 @@ var Graph = class _Graph {
|
|
|
2566
2693
|
dirtyCount: 0,
|
|
2567
2694
|
resolvedCount: 0,
|
|
2568
2695
|
events: [],
|
|
2569
|
-
|
|
2570
|
-
|
|
2696
|
+
anyCompletedCleanly: false,
|
|
2697
|
+
anyErrored: false
|
|
2571
2698
|
};
|
|
2572
2699
|
let lastTriggerDepIndex;
|
|
2573
2700
|
let lastRunDepValues;
|
|
@@ -2611,8 +2738,8 @@ var Graph = class _Graph {
|
|
|
2611
2738
|
} else if (minimal) {
|
|
2612
2739
|
if (t === DIRTY) result.dirtyCount++;
|
|
2613
2740
|
else if (t === RESOLVED) result.resolvedCount++;
|
|
2614
|
-
else if (t === COMPLETE && !result.
|
|
2615
|
-
else if (t === ERROR) result.
|
|
2741
|
+
else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
|
|
2742
|
+
else if (t === ERROR) result.anyErrored = true;
|
|
2616
2743
|
} else if (t === DIRTY) {
|
|
2617
2744
|
result.dirtyCount++;
|
|
2618
2745
|
result.events.push({ type: "dirty", path, ...base });
|
|
@@ -2620,10 +2747,10 @@ var Graph = class _Graph {
|
|
|
2620
2747
|
result.resolvedCount++;
|
|
2621
2748
|
result.events.push({ type: "resolved", path, ...base, ...withCausal });
|
|
2622
2749
|
} else if (t === COMPLETE) {
|
|
2623
|
-
if (!result.
|
|
2750
|
+
if (!result.anyErrored) result.anyCompletedCleanly = true;
|
|
2624
2751
|
result.events.push({ type: "complete", path, ...base });
|
|
2625
2752
|
} else if (t === ERROR) {
|
|
2626
|
-
result.
|
|
2753
|
+
result.anyErrored = true;
|
|
2627
2754
|
result.events.push({ type: "error", path, data: m[1], ...base });
|
|
2628
2755
|
}
|
|
2629
2756
|
}
|
|
@@ -2643,11 +2770,14 @@ var Graph = class _Graph {
|
|
|
2643
2770
|
get events() {
|
|
2644
2771
|
return result.events;
|
|
2645
2772
|
},
|
|
2646
|
-
get
|
|
2647
|
-
return result.
|
|
2773
|
+
get anyCompletedCleanly() {
|
|
2774
|
+
return result.anyCompletedCleanly;
|
|
2648
2775
|
},
|
|
2649
|
-
get
|
|
2650
|
-
return result.
|
|
2776
|
+
get anyErrored() {
|
|
2777
|
+
return result.anyErrored;
|
|
2778
|
+
},
|
|
2779
|
+
get completedWithoutErrors() {
|
|
2780
|
+
return result.anyCompletedCleanly && !result.anyErrored;
|
|
2651
2781
|
},
|
|
2652
2782
|
dispose() {
|
|
2653
2783
|
unsub();
|
|
@@ -2683,9 +2813,10 @@ var Graph = class _Graph {
|
|
|
2683
2813
|
dirtyCount: 0,
|
|
2684
2814
|
resolvedCount: 0,
|
|
2685
2815
|
events: [],
|
|
2686
|
-
|
|
2687
|
-
|
|
2816
|
+
anyCompletedCleanly: false,
|
|
2817
|
+
anyErrored: false
|
|
2688
2818
|
};
|
|
2819
|
+
const nodeErrored = /* @__PURE__ */ new Set();
|
|
2689
2820
|
const actor = options.actor;
|
|
2690
2821
|
const targets = [];
|
|
2691
2822
|
this._collectObserveTargets("", targets);
|
|
@@ -2704,8 +2835,11 @@ var Graph = class _Graph {
|
|
|
2704
2835
|
} else if (minimal) {
|
|
2705
2836
|
if (t === DIRTY) result.dirtyCount++;
|
|
2706
2837
|
else if (t === RESOLVED) result.resolvedCount++;
|
|
2707
|
-
else if (t === COMPLETE && !
|
|
2708
|
-
else if (t === ERROR)
|
|
2838
|
+
else if (t === COMPLETE && !nodeErrored.has(path)) result.anyCompletedCleanly = true;
|
|
2839
|
+
else if (t === ERROR) {
|
|
2840
|
+
result.anyErrored = true;
|
|
2841
|
+
nodeErrored.add(path);
|
|
2842
|
+
}
|
|
2709
2843
|
} else if (t === DIRTY) {
|
|
2710
2844
|
result.dirtyCount++;
|
|
2711
2845
|
result.events.push({ type: "dirty", path, ...base });
|
|
@@ -2713,10 +2847,11 @@ var Graph = class _Graph {
|
|
|
2713
2847
|
result.resolvedCount++;
|
|
2714
2848
|
result.events.push({ type: "resolved", path, ...base });
|
|
2715
2849
|
} else if (t === COMPLETE) {
|
|
2716
|
-
if (!
|
|
2850
|
+
if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
|
|
2717
2851
|
result.events.push({ type: "complete", path, ...base });
|
|
2718
2852
|
} else if (t === ERROR) {
|
|
2719
|
-
result.
|
|
2853
|
+
result.anyErrored = true;
|
|
2854
|
+
nodeErrored.add(path);
|
|
2720
2855
|
result.events.push({ type: "error", path, data: m[1], ...base });
|
|
2721
2856
|
}
|
|
2722
2857
|
}
|
|
@@ -2736,11 +2871,14 @@ var Graph = class _Graph {
|
|
|
2736
2871
|
get events() {
|
|
2737
2872
|
return result.events;
|
|
2738
2873
|
},
|
|
2739
|
-
get
|
|
2740
|
-
return result.
|
|
2874
|
+
get anyCompletedCleanly() {
|
|
2875
|
+
return result.anyCompletedCleanly;
|
|
2741
2876
|
},
|
|
2742
|
-
get
|
|
2743
|
-
return result.
|
|
2877
|
+
get anyErrored() {
|
|
2878
|
+
return result.anyErrored;
|
|
2879
|
+
},
|
|
2880
|
+
get completedWithoutErrors() {
|
|
2881
|
+
return result.anyCompletedCleanly && !result.anyErrored;
|
|
2744
2882
|
},
|
|
2745
2883
|
dispose() {
|
|
2746
2884
|
for (const u of unsubs) u();
|
|
@@ -2772,8 +2910,8 @@ var Graph = class _Graph {
|
|
|
2772
2910
|
dirtyCount: 0,
|
|
2773
2911
|
resolvedCount: 0,
|
|
2774
2912
|
events: [],
|
|
2775
|
-
|
|
2776
|
-
|
|
2913
|
+
anyCompletedCleanly: false,
|
|
2914
|
+
anyErrored: false
|
|
2777
2915
|
};
|
|
2778
2916
|
const target = this.resolve(path);
|
|
2779
2917
|
let batchSeq = 0;
|
|
@@ -2792,10 +2930,10 @@ var Graph = class _Graph {
|
|
|
2792
2930
|
acc.resolvedCount++;
|
|
2793
2931
|
acc.events.push({ type: "resolved", path, ...base });
|
|
2794
2932
|
} else if (t === COMPLETE) {
|
|
2795
|
-
if (!acc.
|
|
2933
|
+
if (!acc.anyErrored) acc.anyCompletedCleanly = true;
|
|
2796
2934
|
acc.events.push({ type: "complete", path, ...base });
|
|
2797
2935
|
} else if (t === ERROR) {
|
|
2798
|
-
acc.
|
|
2936
|
+
acc.anyErrored = true;
|
|
2799
2937
|
acc.events.push({ type: "error", path, data: m[1], ...base });
|
|
2800
2938
|
}
|
|
2801
2939
|
}
|
|
@@ -2813,11 +2951,14 @@ var Graph = class _Graph {
|
|
|
2813
2951
|
get events() {
|
|
2814
2952
|
return acc.events;
|
|
2815
2953
|
},
|
|
2816
|
-
get
|
|
2817
|
-
return acc.
|
|
2954
|
+
get anyCompletedCleanly() {
|
|
2955
|
+
return acc.anyCompletedCleanly;
|
|
2956
|
+
},
|
|
2957
|
+
get anyErrored() {
|
|
2958
|
+
return acc.anyErrored;
|
|
2818
2959
|
},
|
|
2819
|
-
get
|
|
2820
|
-
return acc.
|
|
2960
|
+
get completedWithoutErrors() {
|
|
2961
|
+
return acc.anyCompletedCleanly && !acc.anyErrored;
|
|
2821
2962
|
},
|
|
2822
2963
|
dispose() {
|
|
2823
2964
|
unsub();
|
|
@@ -2838,9 +2979,10 @@ var Graph = class _Graph {
|
|
|
2838
2979
|
dirtyCount: 0,
|
|
2839
2980
|
resolvedCount: 0,
|
|
2840
2981
|
events: [],
|
|
2841
|
-
|
|
2842
|
-
|
|
2982
|
+
anyCompletedCleanly: false,
|
|
2983
|
+
anyErrored: false
|
|
2843
2984
|
};
|
|
2985
|
+
const nodeErrored = /* @__PURE__ */ new Set();
|
|
2844
2986
|
const targets = [];
|
|
2845
2987
|
this._collectObserveTargets("", targets);
|
|
2846
2988
|
targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
@@ -2862,10 +3004,11 @@ var Graph = class _Graph {
|
|
|
2862
3004
|
acc.resolvedCount++;
|
|
2863
3005
|
acc.events.push({ type: "resolved", path, ...base });
|
|
2864
3006
|
} else if (t === COMPLETE) {
|
|
2865
|
-
if (!
|
|
3007
|
+
if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
|
|
2866
3008
|
acc.events.push({ type: "complete", path, ...base });
|
|
2867
3009
|
} else if (t === ERROR) {
|
|
2868
|
-
acc.
|
|
3010
|
+
acc.anyErrored = true;
|
|
3011
|
+
nodeErrored.add(path);
|
|
2869
3012
|
acc.events.push({ type: "error", path, data: m[1], ...base });
|
|
2870
3013
|
}
|
|
2871
3014
|
}
|
|
@@ -2884,11 +3027,14 @@ var Graph = class _Graph {
|
|
|
2884
3027
|
get events() {
|
|
2885
3028
|
return acc.events;
|
|
2886
3029
|
},
|
|
2887
|
-
get
|
|
2888
|
-
return acc.
|
|
3030
|
+
get anyCompletedCleanly() {
|
|
3031
|
+
return acc.anyCompletedCleanly;
|
|
2889
3032
|
},
|
|
2890
|
-
get
|
|
2891
|
-
return acc.
|
|
3033
|
+
get anyErrored() {
|
|
3034
|
+
return acc.anyErrored;
|
|
3035
|
+
},
|
|
3036
|
+
get completedWithoutErrors() {
|
|
3037
|
+
return acc.anyCompletedCleanly && !acc.anyErrored;
|
|
2892
3038
|
},
|
|
2893
3039
|
dispose() {
|
|
2894
3040
|
for (const u of unsubs) u();
|
|
@@ -3214,8 +3360,9 @@ var Graph = class _Graph {
|
|
|
3214
3360
|
/**
|
|
3215
3361
|
* Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
|
|
3216
3362
|
*
|
|
3217
|
-
* Checkpoint trigger uses {@link messageTier}: only batches containing tier >=
|
|
3218
|
-
* schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1 control
|
|
3363
|
+
* Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 3 messages
|
|
3364
|
+
* schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1/2 control
|
|
3365
|
+
* waves (`START`/`DIRTY`/`INVALIDATE`/`PAUSE`/`RESUME`).
|
|
3219
3366
|
*/
|
|
3220
3367
|
autoCheckpoint(adapter, options = {}) {
|
|
3221
3368
|
const debounceMs = Math.max(0, options.debounceMs ?? 500);
|
|
@@ -3262,7 +3409,7 @@ var Graph = class _Graph {
|
|
|
3262
3409
|
timer = setTimeout(flush, debounceMs);
|
|
3263
3410
|
};
|
|
3264
3411
|
const off = this.observe().subscribe((path, messages) => {
|
|
3265
|
-
const triggeredByTier = messages.some((m) => messageTier(m[0]) >=
|
|
3412
|
+
const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
|
|
3266
3413
|
if (!triggeredByTier) return;
|
|
3267
3414
|
if (options.filter) {
|
|
3268
3415
|
const nd = this.resolve(path);
|
|
@@ -3488,125 +3635,6 @@ function reachable(described, from, direction, options = {}) {
|
|
|
3488
3635
|
}
|
|
3489
3636
|
return [...out].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
3490
3637
|
}
|
|
3491
|
-
|
|
3492
|
-
// src/graph/sizeof.ts
|
|
3493
|
-
var OVERHEAD = {
|
|
3494
|
-
object: 56,
|
|
3495
|
-
array: 64,
|
|
3496
|
-
string: 40,
|
|
3497
|
-
// header; content added separately
|
|
3498
|
-
number: 8,
|
|
3499
|
-
boolean: 4,
|
|
3500
|
-
null: 0,
|
|
3501
|
-
undefined: 0,
|
|
3502
|
-
symbol: 40,
|
|
3503
|
-
bigint: 16,
|
|
3504
|
-
function: 120,
|
|
3505
|
-
map: 72,
|
|
3506
|
-
set: 72,
|
|
3507
|
-
mapEntry: 40,
|
|
3508
|
-
setEntry: 24
|
|
3509
|
-
};
|
|
3510
|
-
function sizeof(value) {
|
|
3511
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
3512
|
-
return _sizeof(value, seen);
|
|
3513
|
-
}
|
|
3514
|
-
function _sizeof(value, seen) {
|
|
3515
|
-
if (value == null) return 0;
|
|
3516
|
-
const t = typeof value;
|
|
3517
|
-
switch (t) {
|
|
3518
|
-
case "number":
|
|
3519
|
-
return OVERHEAD.number;
|
|
3520
|
-
case "boolean":
|
|
3521
|
-
return OVERHEAD.boolean;
|
|
3522
|
-
case "string":
|
|
3523
|
-
return OVERHEAD.string + value.length * 2;
|
|
3524
|
-
// UTF-16
|
|
3525
|
-
case "bigint":
|
|
3526
|
-
return OVERHEAD.bigint;
|
|
3527
|
-
case "symbol":
|
|
3528
|
-
return OVERHEAD.symbol;
|
|
3529
|
-
case "function":
|
|
3530
|
-
if (seen.has(value)) return 0;
|
|
3531
|
-
seen.add(value);
|
|
3532
|
-
return OVERHEAD.function;
|
|
3533
|
-
case "undefined":
|
|
3534
|
-
return 0;
|
|
3535
|
-
}
|
|
3536
|
-
const obj = value;
|
|
3537
|
-
if (seen.has(obj)) return 0;
|
|
3538
|
-
seen.add(obj);
|
|
3539
|
-
if (obj instanceof Map) {
|
|
3540
|
-
let size2 = OVERHEAD.map;
|
|
3541
|
-
for (const [k, v] of obj) {
|
|
3542
|
-
size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
|
|
3543
|
-
}
|
|
3544
|
-
return size2;
|
|
3545
|
-
}
|
|
3546
|
-
if (obj instanceof Set) {
|
|
3547
|
-
let size2 = OVERHEAD.set;
|
|
3548
|
-
for (const v of obj) {
|
|
3549
|
-
size2 += OVERHEAD.setEntry + _sizeof(v, seen);
|
|
3550
|
-
}
|
|
3551
|
-
return size2;
|
|
3552
|
-
}
|
|
3553
|
-
if (Array.isArray(obj)) {
|
|
3554
|
-
let size2 = OVERHEAD.array + obj.length * 8;
|
|
3555
|
-
for (const item of obj) {
|
|
3556
|
-
size2 += _sizeof(item, seen);
|
|
3557
|
-
}
|
|
3558
|
-
return size2;
|
|
3559
|
-
}
|
|
3560
|
-
if (obj instanceof ArrayBuffer) return obj.byteLength;
|
|
3561
|
-
if (ArrayBuffer.isView(obj)) return obj.byteLength;
|
|
3562
|
-
let size = OVERHEAD.object;
|
|
3563
|
-
const keys = Object.keys(obj);
|
|
3564
|
-
for (const key of keys) {
|
|
3565
|
-
size += OVERHEAD.string + key.length * 2;
|
|
3566
|
-
size += _sizeof(obj[key], seen);
|
|
3567
|
-
}
|
|
3568
|
-
return size;
|
|
3569
|
-
}
|
|
3570
|
-
|
|
3571
|
-
// src/graph/profile.ts
|
|
3572
|
-
function graphProfile(graph, opts) {
|
|
3573
|
-
const topN = opts?.topN ?? 10;
|
|
3574
|
-
const desc = graph.describe({ detail: "standard" });
|
|
3575
|
-
const targets = [];
|
|
3576
|
-
if (typeof graph._collectObserveTargets === "function") {
|
|
3577
|
-
graph._collectObserveTargets("", targets);
|
|
3578
|
-
}
|
|
3579
|
-
const pathToNode = /* @__PURE__ */ new Map();
|
|
3580
|
-
for (const [p, n] of targets) {
|
|
3581
|
-
pathToNode.set(p, n);
|
|
3582
|
-
}
|
|
3583
|
-
const profiles = [];
|
|
3584
|
-
for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
|
|
3585
|
-
const nd = pathToNode.get(path);
|
|
3586
|
-
const impl = nd instanceof NodeImpl ? nd : null;
|
|
3587
|
-
const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
|
|
3588
|
-
const subscriberCount = impl ? impl._sinkCount : 0;
|
|
3589
|
-
const depCount = nodeDesc.deps?.length ?? 0;
|
|
3590
|
-
profiles.push({
|
|
3591
|
-
path,
|
|
3592
|
-
type: nodeDesc.type,
|
|
3593
|
-
status: nodeDesc.status ?? "unknown",
|
|
3594
|
-
valueSizeBytes,
|
|
3595
|
-
subscriberCount,
|
|
3596
|
-
depCount
|
|
3597
|
-
});
|
|
3598
|
-
}
|
|
3599
|
-
const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
|
|
3600
|
-
const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
|
|
3601
|
-
return {
|
|
3602
|
-
nodeCount: profiles.length,
|
|
3603
|
-
edgeCount: desc.edges.length,
|
|
3604
|
-
subgraphCount: desc.subgraphs.length,
|
|
3605
|
-
nodes: profiles,
|
|
3606
|
-
totalValueSizeBytes,
|
|
3607
|
-
hotspots
|
|
3608
|
-
};
|
|
3609
|
-
}
|
|
3610
3638
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3611
3639
|
0 && (module.exports = {
|
|
3612
3640
|
GRAPH_META_SEGMENT,
|