@graphrefly/graphrefly 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-R6OHUUYB.js → chunk-AHRKWMNI.js} +7 -7
- package/dist/chunk-AHRKWMNI.js.map +1 -0
- package/dist/{chunk-2PORF4RP.js → chunk-BER7UYLM.js} +27 -32
- package/dist/chunk-BER7UYLM.js.map +1 -0
- package/dist/{chunk-646OG3PO.js → chunk-IRZAGZUB.js} +51 -52
- package/dist/chunk-IRZAGZUB.js.map +1 -0
- package/dist/{chunk-IHJHBADD.js → chunk-JC2SN46B.js} +385 -197
- package/dist/chunk-JC2SN46B.js.map +1 -0
- package/dist/{chunk-XJ6EMQ22.js → chunk-OO5QOAXI.js} +4 -10
- package/dist/chunk-OO5QOAXI.js.map +1 -0
- package/dist/{chunk-YXROQFXZ.js → chunk-UW77D7SP.js} +3 -3
- package/dist/{chunk-F2ULI3Q3.js → chunk-XUOY3YKN.js} +7 -3
- package/dist/chunk-XUOY3YKN.js.map +1 -0
- package/dist/chunk-YLR5JUJZ.js +111 -0
- package/dist/chunk-YLR5JUJZ.js.map +1 -0
- package/dist/{chunk-BV3TPSBK.js → chunk-YXR3WW3Q.js} +740 -755
- package/dist/chunk-YXR3WW3Q.js.map +1 -0
- package/dist/compat/nestjs/index.cjs +1127 -983
- package/dist/compat/nestjs/index.cjs.map +1 -1
- package/dist/compat/nestjs/index.d.cts +4 -4
- package/dist/compat/nestjs/index.d.ts +4 -4
- package/dist/compat/nestjs/index.js +7 -13
- package/dist/core/index.cjs +653 -749
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +7 -7
- package/dist/extra/index.cjs +773 -795
- package/dist/extra/index.cjs.map +1 -1
- package/dist/extra/index.d.cts +4 -4
- package/dist/extra/index.d.ts +4 -4
- package/dist/extra/index.js +5 -11
- package/dist/graph/index.cjs +1036 -975
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +3 -3
- package/dist/graph/index.d.ts +3 -3
- package/dist/graph/index.js +8 -8
- package/dist/{graph-fCsaaVIa.d.cts → graph-KsTe57nI.d.cts} +127 -51
- package/dist/{graph-Dc-P9BVm.d.ts → graph-mILUUqW8.d.ts} +127 -51
- package/dist/{index-DhXznWyH.d.ts → index-8a605sg9.d.ts} +2 -2
- package/dist/{index-D7y9Q8W4.d.ts → index-B2SvPEbc.d.ts} +8 -69
- package/dist/{index-YlOH1Gw6.d.cts → index-BBUYZfJ1.d.cts} +122 -78
- package/dist/{index-ClaKZFPl.d.cts → index-Bjh5C1Tp.d.cts} +38 -35
- package/dist/{index-DWq0P9T6.d.ts → index-BjtlNirP.d.cts} +5 -7
- package/dist/{index-N704txAA.d.ts → index-BnkMgNNa.d.ts} +38 -35
- package/dist/{index-BBVBYPxr.d.cts → index-CgSiUouz.d.ts} +5 -7
- package/dist/{index-BmoUvOGN.d.ts → index-CvKzv0AW.d.ts} +122 -78
- package/dist/{index-4OIX-q0C.d.cts → index-UudxGnzc.d.cts} +8 -69
- package/dist/{index-DlGMf_Qe.d.cts → index-VHA43cGP.d.cts} +2 -2
- package/dist/index.cjs +6146 -5725
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +617 -383
- package/dist/index.d.ts +617 -383
- package/dist/index.js +4401 -4028
- package/dist/index.js.map +1 -1
- package/dist/{meta-BV4pj9ML.d.cts → meta-BnG7XAaE.d.cts} +395 -289
- package/dist/{meta-BV4pj9ML.d.ts → meta-BnG7XAaE.d.ts} +395 -289
- package/dist/observable-C8Kx_O6k.d.cts +36 -0
- package/dist/observable-DcBwQY7t.d.ts +36 -0
- package/dist/patterns/reactive-layout/index.cjs +1037 -857
- package/dist/patterns/reactive-layout/index.cjs.map +1 -1
- package/dist/patterns/reactive-layout/index.d.cts +3 -3
- package/dist/patterns/reactive-layout/index.d.ts +3 -3
- package/dist/patterns/reactive-layout/index.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-2PORF4RP.js.map +0 -1
- package/dist/chunk-646OG3PO.js.map +0 -1
- package/dist/chunk-BV3TPSBK.js.map +0 -1
- package/dist/chunk-EBNKJULL.js +0 -231
- package/dist/chunk-EBNKJULL.js.map +0 -1
- package/dist/chunk-F2ULI3Q3.js.map +0 -1
- package/dist/chunk-IHJHBADD.js.map +0 -1
- package/dist/chunk-R6OHUUYB.js.map +0 -1
- package/dist/chunk-XJ6EMQ22.js.map +0 -1
- package/dist/observable-Cz-AWhwR.d.cts +0 -42
- package/dist/observable-DCqlwGyl.d.ts +0 -42
- /package/dist/{chunk-YXROQFXZ.js.map → chunk-UW77D7SP.js.map} +0 -0
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";
|
|
917
|
+
}
|
|
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();
|
|
868
954
|
}
|
|
955
|
+
this._depDirtyMask.reset();
|
|
956
|
+
this._depSettledMask.reset();
|
|
957
|
+
this._depCompleteMask.reset();
|
|
869
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
_sinks = null;
|
|
1031
|
-
constructor(fn, opts) {
|
|
1032
|
-
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
|
-
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1039
|
-
this._onMessage = opts.onMessage;
|
|
1040
|
-
this._onResubscribe = opts.onResubscribe;
|
|
1041
|
-
this._guard = opts.guard;
|
|
1042
|
-
this._inspectorHook = void 0;
|
|
1043
|
-
const meta = {};
|
|
1044
|
-
for (const [k, v] of Object.entries(opts.meta ?? {})) {
|
|
1045
|
-
meta[k] = node({
|
|
1046
|
-
initial: v,
|
|
1047
|
-
name: `${opts.name ?? "dynamicNode"}:meta:${k}`,
|
|
1048
|
-
describeKind: "state",
|
|
1049
|
-
...opts.guard != null ? { guard: opts.guard } : {}
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
Object.freeze(meta);
|
|
1053
|
-
this.meta = meta;
|
|
1054
|
-
const self = this;
|
|
1055
|
-
this._actions = {
|
|
1056
|
-
down(messages) {
|
|
1057
|
-
self._downInternal(messages);
|
|
1058
|
-
},
|
|
1059
|
-
emit(value) {
|
|
1060
|
-
self._downAutoValue(value);
|
|
1061
|
-
},
|
|
1062
|
-
up(messages) {
|
|
1063
|
-
for (const dep of self._deps) {
|
|
1064
|
-
dep.up?.(messages, { internal: true });
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
this._boundDownToSinks = this._downToSinks.bind(this);
|
|
1069
|
-
}
|
|
1070
|
-
get name() {
|
|
1071
|
-
return this._registryName ?? this._optsName;
|
|
1072
|
-
}
|
|
1073
|
-
/** @internal */
|
|
1074
|
-
_assignRegistryName(localName) {
|
|
1075
|
-
if (this._optsName !== void 0 || this._registryName !== void 0) return;
|
|
1076
|
-
this._registryName = localName;
|
|
1077
|
-
}
|
|
1078
|
-
/**
|
|
1079
|
-
* @internal Attach/remove inspector hook for graph-level observability.
|
|
1080
|
-
* Returns a disposer that restores the previous hook.
|
|
1081
|
-
*/
|
|
1082
|
-
_setInspectorHook(hook) {
|
|
1083
|
-
const prev = this._inspectorHook;
|
|
1084
|
-
this._inspectorHook = hook;
|
|
1085
|
-
return () => {
|
|
1086
|
-
if (this._inspectorHook === hook) {
|
|
1087
|
-
this._inspectorHook = prev;
|
|
1088
|
-
}
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
get status() {
|
|
1092
|
-
return this._status;
|
|
1093
|
-
}
|
|
1094
|
-
get lastMutation() {
|
|
1095
|
-
return this._lastMutation;
|
|
1096
|
-
}
|
|
1097
|
-
/** Versioning not yet supported on DynamicNodeImpl. */
|
|
1098
|
-
get v() {
|
|
1099
|
-
return void 0;
|
|
1100
|
-
}
|
|
1101
|
-
hasGuard() {
|
|
1102
|
-
return this._guard != null;
|
|
1103
|
-
}
|
|
1104
|
-
allowsObserve(actor) {
|
|
1105
|
-
if (this._guard == null) return true;
|
|
1106
|
-
return this._guard(normalizeActor(actor), "observe");
|
|
1107
|
-
}
|
|
1108
|
-
get() {
|
|
1109
|
-
return this._cached === NO_VALUE ? void 0 : this._cached;
|
|
1110
|
-
}
|
|
1111
|
-
down(messages, options) {
|
|
1112
|
-
if (messages.length === 0) return;
|
|
1113
|
-
if (!options?.internal && this._guard != null) {
|
|
1114
|
-
const actor = normalizeActor(options?.actor);
|
|
1115
|
-
const delivery = options?.delivery ?? "write";
|
|
1116
|
-
const action = delivery === "signal" ? "signal" : "write";
|
|
1117
|
-
if (!this._guard(actor, action)) {
|
|
1118
|
-
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
1119
|
-
}
|
|
1120
|
-
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
1121
|
-
}
|
|
1122
|
-
this._downInternal(messages);
|
|
1123
|
-
}
|
|
1124
|
-
_downInternal(messages) {
|
|
1125
|
-
if (messages.length === 0) return;
|
|
1126
|
-
let sinkMessages = messages;
|
|
1127
|
-
if (this._terminal && !this._resubscribable) {
|
|
1128
|
-
const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
1129
|
-
if (pass.length === 0) return;
|
|
1130
|
-
sinkMessages = pass;
|
|
1131
|
-
}
|
|
1132
|
-
this._handleLocalLifecycle(sinkMessages);
|
|
1133
|
-
if (this._canSkipDirty()) {
|
|
1134
|
-
let hasPhase2 = false;
|
|
1135
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
1136
|
-
const t = sinkMessages[i][0];
|
|
1137
|
-
if (t === DATA || t === RESOLVED) {
|
|
1138
|
-
hasPhase2 = true;
|
|
1139
|
-
break;
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
if (hasPhase2) {
|
|
1143
|
-
const filtered = [];
|
|
1144
|
-
for (let i = 0; i < sinkMessages.length; i++) {
|
|
1145
|
-
if (sinkMessages[i][0] !== DIRTY) filtered.push(sinkMessages[i]);
|
|
1146
|
-
}
|
|
1147
|
-
if (filtered.length > 0) {
|
|
1148
|
-
downWithBatch(this._boundDownToSinks, filtered);
|
|
1149
|
-
}
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
1154
|
-
}
|
|
1155
|
-
_canSkipDirty() {
|
|
1156
|
-
return this._sinkCount === 1 && this._singleDepSinkCount === 1;
|
|
1157
|
-
}
|
|
1158
|
-
subscribe(sink, hints) {
|
|
1159
|
-
if (hints?.actor != null && this._guard != null) {
|
|
1160
|
-
const actor = normalizeActor(hints.actor);
|
|
1161
|
-
if (!this._guard(actor, "observe")) {
|
|
1162
|
-
throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
if (this._terminal && this._resubscribable) {
|
|
1166
|
-
this._terminal = false;
|
|
1167
|
-
this._cached = NO_VALUE;
|
|
1168
|
-
this._status = "disconnected";
|
|
1169
|
-
this._onResubscribe?.();
|
|
1170
|
-
}
|
|
1171
|
-
this._sinkCount += 1;
|
|
1172
|
-
if (hints?.singleDep) {
|
|
1173
|
-
this._singleDepSinkCount += 1;
|
|
1174
|
-
this._singleDepSinks.add(sink);
|
|
1175
|
-
}
|
|
1176
|
-
if (this._sinks == null) {
|
|
1177
|
-
this._sinks = sink;
|
|
1178
|
-
} else if (typeof this._sinks === "function") {
|
|
1179
|
-
this._sinks = /* @__PURE__ */ new Set([this._sinks, sink]);
|
|
1180
|
-
} else {
|
|
1181
|
-
this._sinks.add(sink);
|
|
1182
|
-
}
|
|
1183
|
-
if (!this._connected) {
|
|
1184
|
-
this._connect();
|
|
1185
|
-
}
|
|
1186
|
-
let removed = false;
|
|
1187
|
-
return () => {
|
|
1188
|
-
if (removed) return;
|
|
1189
|
-
removed = true;
|
|
1190
|
-
this._sinkCount -= 1;
|
|
1191
|
-
if (this._singleDepSinks.has(sink)) {
|
|
1192
|
-
this._singleDepSinkCount -= 1;
|
|
1193
|
-
this._singleDepSinks.delete(sink);
|
|
1194
|
-
}
|
|
1195
|
-
if (this._sinks == null) return;
|
|
1196
|
-
if (typeof this._sinks === "function") {
|
|
1197
|
-
if (this._sinks === sink) this._sinks = null;
|
|
1198
|
-
} else {
|
|
1199
|
-
this._sinks.delete(sink);
|
|
1200
|
-
if (this._sinks.size === 1) {
|
|
1201
|
-
const [only] = this._sinks;
|
|
1202
|
-
this._sinks = only;
|
|
1203
|
-
} else if (this._sinks.size === 0) {
|
|
1204
|
-
this._sinks = null;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
if (this._sinks == null) {
|
|
1208
|
-
this._disconnect();
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1128
|
+
_autoComplete;
|
|
1129
|
+
// Dynamic deps tracking
|
|
1130
|
+
/** @internal Read by `describeNode`. */
|
|
1131
|
+
_deps = [];
|
|
1132
|
+
_depUnsubs = [];
|
|
1133
|
+
_depIndexMap = /* @__PURE__ */ new Map();
|
|
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;
|
|
1143
|
+
constructor(fn, opts) {
|
|
1144
|
+
super(opts);
|
|
1145
|
+
this._fn = fn;
|
|
1146
|
+
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1147
|
+
this.down = this.down.bind(this);
|
|
1148
|
+
this.up = this.up.bind(this);
|
|
1149
|
+
}
|
|
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
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
/** Versioning not supported on DynamicNodeImpl (override base). */
|
|
1159
|
+
get v() {
|
|
1160
|
+
return void 0;
|
|
1211
1161
|
}
|
|
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__";
|
|
@@ -1695,7 +1812,7 @@ var RingBuffer = class {
|
|
|
1695
1812
|
return result;
|
|
1696
1813
|
}
|
|
1697
1814
|
};
|
|
1698
|
-
var
|
|
1815
|
+
var OBSERVE_ANSI_THEME = {
|
|
1699
1816
|
data: "\x1B[32m",
|
|
1700
1817
|
dirty: "\x1B[33m",
|
|
1701
1818
|
resolved: "\x1B[36m",
|
|
@@ -1705,7 +1822,7 @@ var SPY_ANSI_THEME = {
|
|
|
1705
1822
|
path: "\x1B[90m",
|
|
1706
1823
|
reset: "\x1B[0m"
|
|
1707
1824
|
};
|
|
1708
|
-
var
|
|
1825
|
+
var OBSERVE_NO_COLOR_THEME = {
|
|
1709
1826
|
data: "",
|
|
1710
1827
|
dirty: "",
|
|
1711
1828
|
resolved: "",
|
|
@@ -1725,9 +1842,9 @@ function describeData(value) {
|
|
|
1725
1842
|
return "[unserializable]";
|
|
1726
1843
|
}
|
|
1727
1844
|
}
|
|
1728
|
-
function
|
|
1729
|
-
if (theme === "none") return
|
|
1730
|
-
if (theme === "ansi" || theme == null) return
|
|
1845
|
+
function resolveObserveTheme(theme) {
|
|
1846
|
+
if (theme === "none") return OBSERVE_NO_COLOR_THEME;
|
|
1847
|
+
if (theme === "ansi" || theme == null) return OBSERVE_ANSI_THEME;
|
|
1731
1848
|
return {
|
|
1732
1849
|
data: theme.data ?? "",
|
|
1733
1850
|
dirty: theme.dirty ?? "",
|
|
@@ -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}`;
|
|
@@ -2498,9 +2625,13 @@ var Graph = class _Graph {
|
|
|
2498
2625
|
if (actor2 != null && !target.allowsObserve(actor2)) {
|
|
2499
2626
|
throw new GuardDenied({ actor: actor2, action: "observe", nodeName: path });
|
|
2500
2627
|
}
|
|
2501
|
-
const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full";
|
|
2502
|
-
if (wantsStructured2
|
|
2503
|
-
|
|
2628
|
+
const wantsStructured2 = resolved.structured === true || resolved.timeline === true || resolved.causal === true || resolved.derived === true || resolved.detail === "minimal" || resolved.detail === "full" || resolved.format != null;
|
|
2629
|
+
if (wantsStructured2) {
|
|
2630
|
+
const result = _Graph.inspectorEnabled ? this._createObserveResult(path, target, resolved) : this._createFallbackObserveResult(path, resolved);
|
|
2631
|
+
if (resolved.format != null) {
|
|
2632
|
+
this._attachFormatLogger(result, resolved);
|
|
2633
|
+
}
|
|
2634
|
+
return result;
|
|
2504
2635
|
}
|
|
2505
2636
|
return {
|
|
2506
2637
|
subscribe(sink) {
|
|
@@ -2518,9 +2649,13 @@ var Graph = class _Graph {
|
|
|
2518
2649
|
}
|
|
2519
2650
|
const opts = resolveObserveDetail(pathOrOpts);
|
|
2520
2651
|
const actor = opts.actor;
|
|
2521
|
-
const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full";
|
|
2522
|
-
if (wantsStructured
|
|
2523
|
-
|
|
2652
|
+
const wantsStructured = opts.structured === true || opts.timeline === true || opts.causal === true || opts.derived === true || opts.detail === "minimal" || opts.detail === "full" || opts.format != null;
|
|
2653
|
+
if (wantsStructured) {
|
|
2654
|
+
const result = _Graph.inspectorEnabled ? this._createObserveResultForAll(opts) : this._createFallbackObserveResultForAll(opts);
|
|
2655
|
+
if (opts.format != null) {
|
|
2656
|
+
this._attachFormatLogger(result, opts);
|
|
2657
|
+
}
|
|
2658
|
+
return result;
|
|
2524
2659
|
}
|
|
2525
2660
|
return {
|
|
2526
2661
|
subscribe: (sink) => {
|
|
@@ -2558,12 +2693,13 @@ var Graph = class _Graph {
|
|
|
2558
2693
|
dirtyCount: 0,
|
|
2559
2694
|
resolvedCount: 0,
|
|
2560
2695
|
events: [],
|
|
2561
|
-
|
|
2562
|
-
|
|
2696
|
+
anyCompletedCleanly: false,
|
|
2697
|
+
anyErrored: false
|
|
2563
2698
|
};
|
|
2564
2699
|
let lastTriggerDepIndex;
|
|
2565
2700
|
let lastRunDepValues;
|
|
2566
2701
|
let detachInspectorHook;
|
|
2702
|
+
let batchSeq = 0;
|
|
2567
2703
|
if ((causal || derived) && target instanceof NodeImpl) {
|
|
2568
2704
|
detachInspectorHook = target._setInspectorHook((event) => {
|
|
2569
2705
|
if (event.kind === "dep_message") {
|
|
@@ -2576,15 +2712,16 @@ var Graph = class _Graph {
|
|
|
2576
2712
|
type: "derived",
|
|
2577
2713
|
path,
|
|
2578
2714
|
dep_values: [...event.depValues],
|
|
2579
|
-
...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {}
|
|
2715
|
+
...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {}
|
|
2580
2716
|
});
|
|
2581
2717
|
}
|
|
2582
2718
|
});
|
|
2583
2719
|
}
|
|
2584
2720
|
const unsub = target.subscribe((msgs) => {
|
|
2721
|
+
batchSeq++;
|
|
2585
2722
|
for (const m of msgs) {
|
|
2586
2723
|
const t = m[0];
|
|
2587
|
-
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
|
|
2724
|
+
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
|
|
2588
2725
|
const withCausal = causal && lastRunDepValues != null ? (() => {
|
|
2589
2726
|
const triggerDep = lastTriggerDepIndex != null && lastTriggerDepIndex >= 0 && target instanceof NodeImpl ? target._deps[lastTriggerDepIndex] : void 0;
|
|
2590
2727
|
const tv = triggerDep?.v;
|
|
@@ -2601,8 +2738,8 @@ var Graph = class _Graph {
|
|
|
2601
2738
|
} else if (minimal) {
|
|
2602
2739
|
if (t === DIRTY) result.dirtyCount++;
|
|
2603
2740
|
else if (t === RESOLVED) result.resolvedCount++;
|
|
2604
|
-
else if (t === COMPLETE && !result.
|
|
2605
|
-
else if (t === ERROR) result.
|
|
2741
|
+
else if (t === COMPLETE && !result.anyErrored) result.anyCompletedCleanly = true;
|
|
2742
|
+
else if (t === ERROR) result.anyErrored = true;
|
|
2606
2743
|
} else if (t === DIRTY) {
|
|
2607
2744
|
result.dirtyCount++;
|
|
2608
2745
|
result.events.push({ type: "dirty", path, ...base });
|
|
@@ -2610,10 +2747,10 @@ var Graph = class _Graph {
|
|
|
2610
2747
|
result.resolvedCount++;
|
|
2611
2748
|
result.events.push({ type: "resolved", path, ...base, ...withCausal });
|
|
2612
2749
|
} else if (t === COMPLETE) {
|
|
2613
|
-
if (!result.
|
|
2750
|
+
if (!result.anyErrored) result.anyCompletedCleanly = true;
|
|
2614
2751
|
result.events.push({ type: "complete", path, ...base });
|
|
2615
2752
|
} else if (t === ERROR) {
|
|
2616
|
-
result.
|
|
2753
|
+
result.anyErrored = true;
|
|
2617
2754
|
result.events.push({ type: "error", path, data: m[1], ...base });
|
|
2618
2755
|
}
|
|
2619
2756
|
}
|
|
@@ -2633,11 +2770,14 @@ var Graph = class _Graph {
|
|
|
2633
2770
|
get events() {
|
|
2634
2771
|
return result.events;
|
|
2635
2772
|
},
|
|
2636
|
-
get
|
|
2637
|
-
return result.
|
|
2773
|
+
get anyCompletedCleanly() {
|
|
2774
|
+
return result.anyCompletedCleanly;
|
|
2775
|
+
},
|
|
2776
|
+
get anyErrored() {
|
|
2777
|
+
return result.anyErrored;
|
|
2638
2778
|
},
|
|
2639
|
-
get
|
|
2640
|
-
return result.
|
|
2779
|
+
get completedWithoutErrors() {
|
|
2780
|
+
return result.anyCompletedCleanly && !result.anyErrored;
|
|
2641
2781
|
},
|
|
2642
2782
|
dispose() {
|
|
2643
2783
|
unsub();
|
|
@@ -2653,11 +2793,15 @@ var Graph = class _Graph {
|
|
|
2653
2793
|
Object.assign(merged, extra);
|
|
2654
2794
|
}
|
|
2655
2795
|
const resolvedTarget = graph.resolve(basePath);
|
|
2656
|
-
|
|
2796
|
+
const expanded = graph._createObserveResult(
|
|
2657
2797
|
basePath,
|
|
2658
2798
|
resolvedTarget,
|
|
2659
2799
|
resolveObserveDetail(merged)
|
|
2660
2800
|
);
|
|
2801
|
+
if (merged.format != null) {
|
|
2802
|
+
graph._attachFormatLogger(expanded, merged);
|
|
2803
|
+
}
|
|
2804
|
+
return expanded;
|
|
2661
2805
|
}
|
|
2662
2806
|
};
|
|
2663
2807
|
}
|
|
@@ -2669,27 +2813,33 @@ var Graph = class _Graph {
|
|
|
2669
2813
|
dirtyCount: 0,
|
|
2670
2814
|
resolvedCount: 0,
|
|
2671
2815
|
events: [],
|
|
2672
|
-
|
|
2673
|
-
|
|
2816
|
+
anyCompletedCleanly: false,
|
|
2817
|
+
anyErrored: false
|
|
2674
2818
|
};
|
|
2819
|
+
const nodeErrored = /* @__PURE__ */ new Set();
|
|
2675
2820
|
const actor = options.actor;
|
|
2676
2821
|
const targets = [];
|
|
2677
2822
|
this._collectObserveTargets("", targets);
|
|
2678
2823
|
targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
2679
2824
|
const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
|
|
2825
|
+
let batchSeq = 0;
|
|
2680
2826
|
const unsubs = picked.map(
|
|
2681
2827
|
([path, nd]) => nd.subscribe((msgs) => {
|
|
2828
|
+
batchSeq++;
|
|
2682
2829
|
for (const m of msgs) {
|
|
2683
2830
|
const t = m[0];
|
|
2684
|
-
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
|
|
2831
|
+
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
|
|
2685
2832
|
if (t === DATA) {
|
|
2686
2833
|
result.values[path] = m[1];
|
|
2687
2834
|
result.events.push({ type: "data", path, data: m[1], ...base });
|
|
2688
2835
|
} else if (minimal) {
|
|
2689
2836
|
if (t === DIRTY) result.dirtyCount++;
|
|
2690
2837
|
else if (t === RESOLVED) result.resolvedCount++;
|
|
2691
|
-
else if (t === COMPLETE && !
|
|
2692
|
-
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
|
+
}
|
|
2693
2843
|
} else if (t === DIRTY) {
|
|
2694
2844
|
result.dirtyCount++;
|
|
2695
2845
|
result.events.push({ type: "dirty", path, ...base });
|
|
@@ -2697,10 +2847,11 @@ var Graph = class _Graph {
|
|
|
2697
2847
|
result.resolvedCount++;
|
|
2698
2848
|
result.events.push({ type: "resolved", path, ...base });
|
|
2699
2849
|
} else if (t === COMPLETE) {
|
|
2700
|
-
if (!
|
|
2850
|
+
if (!nodeErrored.has(path)) result.anyCompletedCleanly = true;
|
|
2701
2851
|
result.events.push({ type: "complete", path, ...base });
|
|
2702
2852
|
} else if (t === ERROR) {
|
|
2703
|
-
result.
|
|
2853
|
+
result.anyErrored = true;
|
|
2854
|
+
nodeErrored.add(path);
|
|
2704
2855
|
result.events.push({ type: "error", path, data: m[1], ...base });
|
|
2705
2856
|
}
|
|
2706
2857
|
}
|
|
@@ -2720,11 +2871,14 @@ var Graph = class _Graph {
|
|
|
2720
2871
|
get events() {
|
|
2721
2872
|
return result.events;
|
|
2722
2873
|
},
|
|
2723
|
-
get
|
|
2724
|
-
return result.
|
|
2874
|
+
get anyCompletedCleanly() {
|
|
2875
|
+
return result.anyCompletedCleanly;
|
|
2725
2876
|
},
|
|
2726
|
-
get
|
|
2727
|
-
return result.
|
|
2877
|
+
get anyErrored() {
|
|
2878
|
+
return result.anyErrored;
|
|
2879
|
+
},
|
|
2880
|
+
get completedWithoutErrors() {
|
|
2881
|
+
return result.anyCompletedCleanly && !result.anyErrored;
|
|
2728
2882
|
},
|
|
2729
2883
|
dispose() {
|
|
2730
2884
|
for (const u of unsubs) u();
|
|
@@ -2737,25 +2891,169 @@ var Graph = class _Graph {
|
|
|
2737
2891
|
} else {
|
|
2738
2892
|
Object.assign(merged, extra);
|
|
2739
2893
|
}
|
|
2740
|
-
|
|
2894
|
+
const expanded = graph._createObserveResultForAll(resolveObserveDetail(merged));
|
|
2895
|
+
if (merged.format != null) {
|
|
2896
|
+
graph._attachFormatLogger(expanded, merged);
|
|
2897
|
+
}
|
|
2898
|
+
return expanded;
|
|
2741
2899
|
}
|
|
2742
2900
|
};
|
|
2743
2901
|
}
|
|
2744
2902
|
/**
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2903
|
+
* Fallback ObserveResult for single-node when inspector is disabled but `format` is requested.
|
|
2904
|
+
* Subscribes to raw messages and accumulates events with timeline info.
|
|
2905
|
+
*/
|
|
2906
|
+
_createFallbackObserveResult(path, options) {
|
|
2907
|
+
const timeline = options.timeline !== false;
|
|
2908
|
+
const acc = {
|
|
2909
|
+
values: {},
|
|
2910
|
+
dirtyCount: 0,
|
|
2911
|
+
resolvedCount: 0,
|
|
2912
|
+
events: [],
|
|
2913
|
+
anyCompletedCleanly: false,
|
|
2914
|
+
anyErrored: false
|
|
2915
|
+
};
|
|
2916
|
+
const target = this.resolve(path);
|
|
2917
|
+
let batchSeq = 0;
|
|
2918
|
+
const unsub = target.subscribe((msgs) => {
|
|
2919
|
+
batchSeq++;
|
|
2920
|
+
for (const m of msgs) {
|
|
2921
|
+
const t = m[0];
|
|
2922
|
+
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
|
|
2923
|
+
if (t === DATA) {
|
|
2924
|
+
acc.values[path] = m[1];
|
|
2925
|
+
acc.events.push({ type: "data", path, data: m[1], ...base });
|
|
2926
|
+
} else if (t === DIRTY) {
|
|
2927
|
+
acc.dirtyCount++;
|
|
2928
|
+
acc.events.push({ type: "dirty", path, ...base });
|
|
2929
|
+
} else if (t === RESOLVED) {
|
|
2930
|
+
acc.resolvedCount++;
|
|
2931
|
+
acc.events.push({ type: "resolved", path, ...base });
|
|
2932
|
+
} else if (t === COMPLETE) {
|
|
2933
|
+
if (!acc.anyErrored) acc.anyCompletedCleanly = true;
|
|
2934
|
+
acc.events.push({ type: "complete", path, ...base });
|
|
2935
|
+
} else if (t === ERROR) {
|
|
2936
|
+
acc.anyErrored = true;
|
|
2937
|
+
acc.events.push({ type: "error", path, data: m[1], ...base });
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
return {
|
|
2942
|
+
get values() {
|
|
2943
|
+
return acc.values;
|
|
2944
|
+
},
|
|
2945
|
+
get dirtyCount() {
|
|
2946
|
+
return acc.dirtyCount;
|
|
2947
|
+
},
|
|
2948
|
+
get resolvedCount() {
|
|
2949
|
+
return acc.resolvedCount;
|
|
2950
|
+
},
|
|
2951
|
+
get events() {
|
|
2952
|
+
return acc.events;
|
|
2953
|
+
},
|
|
2954
|
+
get anyCompletedCleanly() {
|
|
2955
|
+
return acc.anyCompletedCleanly;
|
|
2956
|
+
},
|
|
2957
|
+
get anyErrored() {
|
|
2958
|
+
return acc.anyErrored;
|
|
2959
|
+
},
|
|
2960
|
+
get completedWithoutErrors() {
|
|
2961
|
+
return acc.anyCompletedCleanly && !acc.anyErrored;
|
|
2962
|
+
},
|
|
2963
|
+
dispose() {
|
|
2964
|
+
unsub();
|
|
2965
|
+
},
|
|
2966
|
+
expand() {
|
|
2967
|
+
throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Fallback ObserveResult for graph-wide when inspector is disabled but `format` is requested.
|
|
2973
|
+
*/
|
|
2974
|
+
_createFallbackObserveResultForAll(options) {
|
|
2975
|
+
const timeline = options.timeline !== false;
|
|
2976
|
+
const actor = options.actor;
|
|
2977
|
+
const acc = {
|
|
2978
|
+
values: {},
|
|
2979
|
+
dirtyCount: 0,
|
|
2980
|
+
resolvedCount: 0,
|
|
2981
|
+
events: [],
|
|
2982
|
+
anyCompletedCleanly: false,
|
|
2983
|
+
anyErrored: false
|
|
2984
|
+
};
|
|
2985
|
+
const nodeErrored = /* @__PURE__ */ new Set();
|
|
2986
|
+
const targets = [];
|
|
2987
|
+
this._collectObserveTargets("", targets);
|
|
2988
|
+
targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
2989
|
+
const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
|
|
2990
|
+
let batchSeq = 0;
|
|
2991
|
+
const unsubs = picked.map(
|
|
2992
|
+
([path, nd]) => nd.subscribe((msgs) => {
|
|
2993
|
+
batchSeq++;
|
|
2994
|
+
for (const m of msgs) {
|
|
2995
|
+
const t = m[0];
|
|
2996
|
+
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching(), batch_id: batchSeq } : {};
|
|
2997
|
+
if (t === DATA) {
|
|
2998
|
+
acc.values[path] = m[1];
|
|
2999
|
+
acc.events.push({ type: "data", path, data: m[1], ...base });
|
|
3000
|
+
} else if (t === DIRTY) {
|
|
3001
|
+
acc.dirtyCount++;
|
|
3002
|
+
acc.events.push({ type: "dirty", path, ...base });
|
|
3003
|
+
} else if (t === RESOLVED) {
|
|
3004
|
+
acc.resolvedCount++;
|
|
3005
|
+
acc.events.push({ type: "resolved", path, ...base });
|
|
3006
|
+
} else if (t === COMPLETE) {
|
|
3007
|
+
if (!nodeErrored.has(path)) acc.anyCompletedCleanly = true;
|
|
3008
|
+
acc.events.push({ type: "complete", path, ...base });
|
|
3009
|
+
} else if (t === ERROR) {
|
|
3010
|
+
acc.anyErrored = true;
|
|
3011
|
+
nodeErrored.add(path);
|
|
3012
|
+
acc.events.push({ type: "error", path, data: m[1], ...base });
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
})
|
|
3016
|
+
);
|
|
3017
|
+
return {
|
|
3018
|
+
get values() {
|
|
3019
|
+
return acc.values;
|
|
3020
|
+
},
|
|
3021
|
+
get dirtyCount() {
|
|
3022
|
+
return acc.dirtyCount;
|
|
3023
|
+
},
|
|
3024
|
+
get resolvedCount() {
|
|
3025
|
+
return acc.resolvedCount;
|
|
3026
|
+
},
|
|
3027
|
+
get events() {
|
|
3028
|
+
return acc.events;
|
|
3029
|
+
},
|
|
3030
|
+
get anyCompletedCleanly() {
|
|
3031
|
+
return acc.anyCompletedCleanly;
|
|
3032
|
+
},
|
|
3033
|
+
get anyErrored() {
|
|
3034
|
+
return acc.anyErrored;
|
|
3035
|
+
},
|
|
3036
|
+
get completedWithoutErrors() {
|
|
3037
|
+
return acc.anyCompletedCleanly && !acc.anyErrored;
|
|
3038
|
+
},
|
|
3039
|
+
dispose() {
|
|
3040
|
+
for (const u of unsubs) u();
|
|
3041
|
+
},
|
|
3042
|
+
expand() {
|
|
3043
|
+
throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
|
|
3044
|
+
}
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
/**
|
|
3048
|
+
* Attaches a format logger to an ObserveResult, rendering events as they arrive.
|
|
3049
|
+
* Wraps the result's dispose to flush pending events.
|
|
2752
3050
|
*/
|
|
2753
|
-
|
|
3051
|
+
_attachFormatLogger(result, options) {
|
|
3052
|
+
const format = options.format;
|
|
3053
|
+
const logger = options.logger ?? ((line) => console.log(line));
|
|
2754
3054
|
const include = options.includeTypes ? new Set(options.includeTypes) : null;
|
|
2755
3055
|
const exclude = options.excludeTypes ? new Set(options.excludeTypes) : null;
|
|
2756
|
-
const theme =
|
|
2757
|
-
const format = options.format ?? "pretty";
|
|
2758
|
-
const logger = options.logger ?? ((line) => console.log(line));
|
|
3056
|
+
const theme = resolveObserveTheme(options.theme);
|
|
2759
3057
|
const shouldLog = (type) => {
|
|
2760
3058
|
if (include?.has(type) === false) return false;
|
|
2761
3059
|
if (exclude?.has(type) === true) return false;
|
|
@@ -2780,133 +3078,26 @@ var Graph = class _Graph {
|
|
|
2780
3078
|
const batchPart = event.in_batch ? " [batch]" : "";
|
|
2781
3079
|
return `${pathPart}${color}${event.type.toUpperCase()}${theme.reset}${dataPart}${triggerPart}${batchPart}`;
|
|
2782
3080
|
};
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
const
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
completedCleanly: false,
|
|
2791
|
-
errored: false
|
|
2792
|
-
};
|
|
2793
|
-
let stop2 = () => {
|
|
2794
|
-
};
|
|
2795
|
-
const result2 = {
|
|
2796
|
-
get values() {
|
|
2797
|
-
return acc.values;
|
|
2798
|
-
},
|
|
2799
|
-
get dirtyCount() {
|
|
2800
|
-
return acc.dirtyCount;
|
|
2801
|
-
},
|
|
2802
|
-
get resolvedCount() {
|
|
2803
|
-
return acc.resolvedCount;
|
|
2804
|
-
},
|
|
2805
|
-
get events() {
|
|
2806
|
-
return acc.events;
|
|
2807
|
-
},
|
|
2808
|
-
get completedCleanly() {
|
|
2809
|
-
return acc.completedCleanly;
|
|
2810
|
-
},
|
|
2811
|
-
get errored() {
|
|
2812
|
-
return acc.errored;
|
|
2813
|
-
},
|
|
2814
|
-
dispose() {
|
|
2815
|
-
stop2();
|
|
2816
|
-
},
|
|
2817
|
-
expand() {
|
|
2818
|
-
throw new Error("expand() requires inspector mode (Graph.inspectorEnabled = true)");
|
|
2819
|
-
}
|
|
2820
|
-
};
|
|
2821
|
-
const pushEvent = (path, message) => {
|
|
2822
|
-
const t = message[0];
|
|
2823
|
-
const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
|
|
2824
|
-
let event;
|
|
2825
|
-
if (t === DATA) {
|
|
2826
|
-
if (path != null) acc.values[path] = message[1];
|
|
2827
|
-
event = { type: "data", ...path != null ? { path } : {}, data: message[1], ...base };
|
|
2828
|
-
} else if (t === DIRTY) {
|
|
2829
|
-
acc.dirtyCount += 1;
|
|
2830
|
-
event = { type: "dirty", ...path != null ? { path } : {}, ...base };
|
|
2831
|
-
} else if (t === RESOLVED) {
|
|
2832
|
-
acc.resolvedCount += 1;
|
|
2833
|
-
event = { type: "resolved", ...path != null ? { path } : {}, ...base };
|
|
2834
|
-
} else if (t === COMPLETE) {
|
|
2835
|
-
if (!acc.errored) acc.completedCleanly = true;
|
|
2836
|
-
event = { type: "complete", ...path != null ? { path } : {}, ...base };
|
|
2837
|
-
} else if (t === ERROR) {
|
|
2838
|
-
acc.errored = true;
|
|
2839
|
-
event = {
|
|
2840
|
-
type: "error",
|
|
2841
|
-
...path != null ? { path } : {},
|
|
2842
|
-
data: message[1],
|
|
2843
|
-
...base
|
|
2844
|
-
};
|
|
3081
|
+
let cursor = 0;
|
|
3082
|
+
const flush = () => {
|
|
3083
|
+
const events = result.events;
|
|
3084
|
+
while (cursor < events.length) {
|
|
3085
|
+
const event = events[cursor++];
|
|
3086
|
+
if (shouldLog(event.type)) {
|
|
3087
|
+
logger(renderEvent(event), event);
|
|
2845
3088
|
}
|
|
2846
|
-
if (!event) return;
|
|
2847
|
-
acc.events.push(event);
|
|
2848
|
-
if (!shouldLog(event.type)) return;
|
|
2849
|
-
logger(renderEvent(event), event);
|
|
2850
|
-
};
|
|
2851
|
-
if (options.path != null) {
|
|
2852
|
-
const stream2 = this.observe(options.path, {
|
|
2853
|
-
actor: options.actor,
|
|
2854
|
-
structured: false
|
|
2855
|
-
});
|
|
2856
|
-
stop2 = stream2.subscribe((messages) => {
|
|
2857
|
-
for (const m of messages) {
|
|
2858
|
-
pushEvent(options.path, m);
|
|
2859
|
-
}
|
|
2860
|
-
});
|
|
2861
|
-
} else {
|
|
2862
|
-
const stream2 = this.observe({ actor: options.actor, structured: false });
|
|
2863
|
-
stop2 = stream2.subscribe((path, messages) => {
|
|
2864
|
-
for (const m of messages) {
|
|
2865
|
-
pushEvent(path, m);
|
|
2866
|
-
}
|
|
2867
|
-
});
|
|
2868
3089
|
}
|
|
2869
|
-
return {
|
|
2870
|
-
result: result2,
|
|
2871
|
-
dispose() {
|
|
2872
|
-
result2.dispose();
|
|
2873
|
-
}
|
|
2874
|
-
};
|
|
2875
|
-
}
|
|
2876
|
-
const structuredObserveOptions = {
|
|
2877
|
-
actor: options.actor,
|
|
2878
|
-
structured: true,
|
|
2879
|
-
...options.timeline !== false ? { timeline: true } : {},
|
|
2880
|
-
...options.causal ? { causal: true } : {},
|
|
2881
|
-
...options.derived ? { derived: true } : {}
|
|
2882
3090
|
};
|
|
2883
|
-
const
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
for (const event of nextEvents) {
|
|
2889
|
-
if (!shouldLog(event.type)) continue;
|
|
2890
|
-
logger(renderEvent(event), event);
|
|
2891
|
-
}
|
|
3091
|
+
const origPush = result.events.push;
|
|
3092
|
+
result.events.push = function(...items) {
|
|
3093
|
+
const ret = origPush.apply(this, items);
|
|
3094
|
+
flush();
|
|
3095
|
+
return ret;
|
|
2892
3096
|
};
|
|
2893
|
-
const
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
}
|
|
2898
|
-
}) : stream.subscribe((_path, messages) => {
|
|
2899
|
-
if (messages.length > 0) {
|
|
2900
|
-
flushNewEvents();
|
|
2901
|
-
}
|
|
2902
|
-
});
|
|
2903
|
-
return {
|
|
2904
|
-
result,
|
|
2905
|
-
dispose() {
|
|
2906
|
-
stop();
|
|
2907
|
-
flushNewEvents();
|
|
2908
|
-
result.dispose();
|
|
2909
|
-
}
|
|
3097
|
+
const origDispose = result.dispose.bind(result);
|
|
3098
|
+
result.dispose = () => {
|
|
3099
|
+
origDispose();
|
|
3100
|
+
flush();
|
|
2910
3101
|
};
|
|
2911
3102
|
}
|
|
2912
3103
|
/**
|
|
@@ -3169,8 +3360,9 @@ var Graph = class _Graph {
|
|
|
3169
3360
|
/**
|
|
3170
3361
|
* Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
|
|
3171
3362
|
*
|
|
3172
|
-
* Checkpoint trigger uses {@link messageTier}: only batches containing tier >=
|
|
3173
|
-
* 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`).
|
|
3174
3366
|
*/
|
|
3175
3367
|
autoCheckpoint(adapter, options = {}) {
|
|
3176
3368
|
const debounceMs = Math.max(0, options.debounceMs ?? 500);
|
|
@@ -3217,7 +3409,7 @@ var Graph = class _Graph {
|
|
|
3217
3409
|
timer = setTimeout(flush, debounceMs);
|
|
3218
3410
|
};
|
|
3219
3411
|
const off = this.observe().subscribe((path, messages) => {
|
|
3220
|
-
const triggeredByTier = messages.some((m) => messageTier(m[0]) >=
|
|
3412
|
+
const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 3);
|
|
3221
3413
|
if (!triggeredByTier) return;
|
|
3222
3414
|
if (options.filter) {
|
|
3223
3415
|
const nd = this.resolve(path);
|
|
@@ -3301,33 +3493,21 @@ var Graph = class _Graph {
|
|
|
3301
3493
|
// ——————————————————————————————————————————————————————————————
|
|
3302
3494
|
/**
|
|
3303
3495
|
* When `false`, structured observation options (`causal`, `timeline`),
|
|
3304
|
-
*
|
|
3496
|
+
* and `trace()` writes are no-ops. Raw `observe()` always works.
|
|
3305
3497
|
*
|
|
3306
3498
|
* Default: `true` outside production (`process.env.NODE_ENV !== "production"`).
|
|
3307
3499
|
*/
|
|
3308
3500
|
static inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
|
|
3309
3501
|
_annotations = /* @__PURE__ */ new Map();
|
|
3310
3502
|
_traceRing = new RingBuffer(1e3);
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
annotate(path, reason) {
|
|
3320
|
-
if (!_Graph.inspectorEnabled) return;
|
|
3321
|
-
this.resolve(path);
|
|
3322
|
-
this._annotations.set(path, reason);
|
|
3323
|
-
this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
|
|
3324
|
-
}
|
|
3325
|
-
/**
|
|
3326
|
-
* Returns a chronological log of all reasoning annotations (ring buffer).
|
|
3327
|
-
*
|
|
3328
|
-
* @returns `[]` when {@link Graph.inspectorEnabled} is `false`.
|
|
3329
|
-
*/
|
|
3330
|
-
traceLog() {
|
|
3503
|
+
trace(path, reason) {
|
|
3504
|
+
if (path != null && reason != null) {
|
|
3505
|
+
if (!_Graph.inspectorEnabled) return;
|
|
3506
|
+
this.resolve(path);
|
|
3507
|
+
this._annotations.set(path, reason);
|
|
3508
|
+
this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3331
3511
|
if (!_Graph.inspectorEnabled) return [];
|
|
3332
3512
|
return this._traceRing.toArray();
|
|
3333
3513
|
}
|
|
@@ -3455,125 +3635,6 @@ function reachable(described, from, direction, options = {}) {
|
|
|
3455
3635
|
}
|
|
3456
3636
|
return [...out].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
3457
3637
|
}
|
|
3458
|
-
|
|
3459
|
-
// src/graph/sizeof.ts
|
|
3460
|
-
var OVERHEAD = {
|
|
3461
|
-
object: 56,
|
|
3462
|
-
array: 64,
|
|
3463
|
-
string: 40,
|
|
3464
|
-
// header; content added separately
|
|
3465
|
-
number: 8,
|
|
3466
|
-
boolean: 4,
|
|
3467
|
-
null: 0,
|
|
3468
|
-
undefined: 0,
|
|
3469
|
-
symbol: 40,
|
|
3470
|
-
bigint: 16,
|
|
3471
|
-
function: 120,
|
|
3472
|
-
map: 72,
|
|
3473
|
-
set: 72,
|
|
3474
|
-
mapEntry: 40,
|
|
3475
|
-
setEntry: 24
|
|
3476
|
-
};
|
|
3477
|
-
function sizeof(value) {
|
|
3478
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
3479
|
-
return _sizeof(value, seen);
|
|
3480
|
-
}
|
|
3481
|
-
function _sizeof(value, seen) {
|
|
3482
|
-
if (value == null) return 0;
|
|
3483
|
-
const t = typeof value;
|
|
3484
|
-
switch (t) {
|
|
3485
|
-
case "number":
|
|
3486
|
-
return OVERHEAD.number;
|
|
3487
|
-
case "boolean":
|
|
3488
|
-
return OVERHEAD.boolean;
|
|
3489
|
-
case "string":
|
|
3490
|
-
return OVERHEAD.string + value.length * 2;
|
|
3491
|
-
// UTF-16
|
|
3492
|
-
case "bigint":
|
|
3493
|
-
return OVERHEAD.bigint;
|
|
3494
|
-
case "symbol":
|
|
3495
|
-
return OVERHEAD.symbol;
|
|
3496
|
-
case "function":
|
|
3497
|
-
if (seen.has(value)) return 0;
|
|
3498
|
-
seen.add(value);
|
|
3499
|
-
return OVERHEAD.function;
|
|
3500
|
-
case "undefined":
|
|
3501
|
-
return 0;
|
|
3502
|
-
}
|
|
3503
|
-
const obj = value;
|
|
3504
|
-
if (seen.has(obj)) return 0;
|
|
3505
|
-
seen.add(obj);
|
|
3506
|
-
if (obj instanceof Map) {
|
|
3507
|
-
let size2 = OVERHEAD.map;
|
|
3508
|
-
for (const [k, v] of obj) {
|
|
3509
|
-
size2 += OVERHEAD.mapEntry + _sizeof(k, seen) + _sizeof(v, seen);
|
|
3510
|
-
}
|
|
3511
|
-
return size2;
|
|
3512
|
-
}
|
|
3513
|
-
if (obj instanceof Set) {
|
|
3514
|
-
let size2 = OVERHEAD.set;
|
|
3515
|
-
for (const v of obj) {
|
|
3516
|
-
size2 += OVERHEAD.setEntry + _sizeof(v, seen);
|
|
3517
|
-
}
|
|
3518
|
-
return size2;
|
|
3519
|
-
}
|
|
3520
|
-
if (Array.isArray(obj)) {
|
|
3521
|
-
let size2 = OVERHEAD.array + obj.length * 8;
|
|
3522
|
-
for (const item of obj) {
|
|
3523
|
-
size2 += _sizeof(item, seen);
|
|
3524
|
-
}
|
|
3525
|
-
return size2;
|
|
3526
|
-
}
|
|
3527
|
-
if (obj instanceof ArrayBuffer) return obj.byteLength;
|
|
3528
|
-
if (ArrayBuffer.isView(obj)) return obj.byteLength;
|
|
3529
|
-
let size = OVERHEAD.object;
|
|
3530
|
-
const keys = Object.keys(obj);
|
|
3531
|
-
for (const key of keys) {
|
|
3532
|
-
size += OVERHEAD.string + key.length * 2;
|
|
3533
|
-
size += _sizeof(obj[key], seen);
|
|
3534
|
-
}
|
|
3535
|
-
return size;
|
|
3536
|
-
}
|
|
3537
|
-
|
|
3538
|
-
// src/graph/profile.ts
|
|
3539
|
-
function graphProfile(graph, opts) {
|
|
3540
|
-
const topN = opts?.topN ?? 10;
|
|
3541
|
-
const desc = graph.describe({ detail: "standard" });
|
|
3542
|
-
const targets = [];
|
|
3543
|
-
if (typeof graph._collectObserveTargets === "function") {
|
|
3544
|
-
graph._collectObserveTargets("", targets);
|
|
3545
|
-
}
|
|
3546
|
-
const pathToNode = /* @__PURE__ */ new Map();
|
|
3547
|
-
for (const [p, n] of targets) {
|
|
3548
|
-
pathToNode.set(p, n);
|
|
3549
|
-
}
|
|
3550
|
-
const profiles = [];
|
|
3551
|
-
for (const [path, nodeDesc] of Object.entries(desc.nodes)) {
|
|
3552
|
-
const nd = pathToNode.get(path);
|
|
3553
|
-
const impl = nd instanceof NodeImpl ? nd : null;
|
|
3554
|
-
const valueSizeBytes = impl ? sizeof(impl.get()) : 0;
|
|
3555
|
-
const subscriberCount = impl ? impl._sinkCount : 0;
|
|
3556
|
-
const depCount = nodeDesc.deps?.length ?? 0;
|
|
3557
|
-
profiles.push({
|
|
3558
|
-
path,
|
|
3559
|
-
type: nodeDesc.type,
|
|
3560
|
-
status: nodeDesc.status ?? "unknown",
|
|
3561
|
-
valueSizeBytes,
|
|
3562
|
-
subscriberCount,
|
|
3563
|
-
depCount
|
|
3564
|
-
});
|
|
3565
|
-
}
|
|
3566
|
-
const totalValueSizeBytes = profiles.reduce((sum, p) => sum + p.valueSizeBytes, 0);
|
|
3567
|
-
const hotspots = [...profiles].sort((a, b) => b.valueSizeBytes - a.valueSizeBytes).slice(0, topN);
|
|
3568
|
-
return {
|
|
3569
|
-
nodeCount: profiles.length,
|
|
3570
|
-
edgeCount: desc.edges.length,
|
|
3571
|
-
subgraphCount: desc.subgraphs.length,
|
|
3572
|
-
nodes: profiles,
|
|
3573
|
-
totalValueSizeBytes,
|
|
3574
|
-
hotspots
|
|
3575
|
-
};
|
|
3576
|
-
}
|
|
3577
3638
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3578
3639
|
0 && (module.exports = {
|
|
3579
3640
|
GRAPH_META_SEGMENT,
|