@solidjs/signals 2.0.0-rc.13 → 2.0.0-rc.15
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/dev-shared.js +2059 -3768
- package/dist/dev.attribution.js +237 -101
- package/dist/dev.js +7466 -8497
- package/dist/observe/affects.js +128 -104
- package/dist/observe/boundaries.js +492 -665
- package/dist/observe/core/action.js +63 -66
- package/dist/observe/core/async.js +243 -379
- package/dist/observe/core/attribution-queries.js +1 -1
- package/dist/observe/core/attribution.js +394 -253
- package/dist/observe/core/constants.js +120 -187
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +874 -1440
- package/dist/observe/core/dev.js +3 -3
- package/dist/observe/core/effect.js +73 -84
- package/dist/observe/core/external.js +5 -5
- package/dist/observe/core/graph.js +62 -71
- package/dist/observe/core/heap.js +75 -98
- package/dist/observe/core/invariants.js +1 -34
- package/dist/observe/core/lanes.js +750 -181
- package/dist/observe/core/owner.js +80 -101
- package/dist/observe/core/scheduler.js +719 -1356
- package/dist/observe/core/verdict.js +314 -621
- package/dist/observe/flatten.js +79 -0
- package/dist/observe/index.js +15 -13
- package/dist/observe/map.js +232 -235
- package/dist/observe/reveal.js +212 -0
- package/dist/observe/signals.js +144 -216
- package/dist/observe/store/affects.js +142 -0
- package/dist/observe/store/hooks.js +13 -0
- package/dist/observe/store/index.js +27 -24
- package/dist/observe/store/optimistic.js +571 -0
- package/dist/observe/store/projection.js +308 -0
- package/dist/observe/store/reconcile.js +259 -0
- package/dist/observe/store/store.js +1988 -277
- package/dist/observe/store/storePath.js +1 -1
- package/dist/{prod/store/next → observe/store}/target.js +17 -18
- package/dist/observe/store/types.js +73 -0
- package/dist/observe/store/utils.js +1 -13
- package/dist/prod/affects.js +130 -106
- package/dist/prod/boundaries.js +476 -654
- package/dist/prod/core/action.js +71 -74
- package/dist/prod/core/async.js +306 -444
- package/dist/prod/core/constants.js +117 -186
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +888 -1462
- package/dist/prod/core/effect.js +88 -99
- package/dist/prod/core/external.js +5 -5
- package/dist/prod/core/graph.js +62 -71
- package/dist/prod/core/heap.js +75 -98
- package/dist/prod/core/invariants.js +1 -32
- package/dist/prod/core/lanes.js +712 -181
- package/dist/prod/core/owner.js +75 -96
- package/dist/prod/core/scheduler.js +686 -1335
- package/dist/prod/core/verdict.js +309 -621
- package/dist/prod/flatten.js +77 -0
- package/dist/prod/index.js +15 -13
- package/dist/prod/map.js +142 -145
- package/dist/prod/reveal.js +212 -0
- package/dist/prod/signals.js +155 -227
- package/dist/prod/store/affects.js +142 -0
- package/dist/prod/store/hooks.js +13 -0
- package/dist/prod/store/index.js +26 -19
- package/dist/prod/store/optimistic.js +566 -0
- package/dist/prod/store/projection.js +301 -0
- package/dist/prod/store/reconcile.js +259 -0
- package/dist/prod/store/store.js +1883 -272
- package/dist/prod/store/storePath.js +1 -1
- package/dist/{observe/store/next → prod/store}/target.js +13 -17
- package/dist/prod/store/types.js +73 -0
- package/dist/prod/store/utils.js +1 -11
- package/dist/types/affects.d.ts +11 -30
- package/dist/types/boundaries.d.ts +66 -176
- package/dist/types/carved.d.ts +1 -0
- package/dist/types/core/action.d.ts +5 -6
- package/dist/types/core/async.d.ts +10 -5
- package/dist/types/core/attribution-hooks.d.ts +20 -1
- package/dist/types/core/attribution.d.ts +26 -11
- package/dist/types/core/constants.d.ts +130 -180
- package/dist/types/core/core.d.ts +134 -264
- package/dist/types/core/dev.d.ts +2 -2
- package/dist/types/core/effect.d.ts +0 -7
- package/dist/types/core/graph.d.ts +7 -1
- package/dist/types/core/heap.d.ts +5 -7
- package/dist/types/core/index.d.ts +5 -5
- package/dist/types/core/invariants.d.ts +0 -29
- package/dist/types/core/lanes.d.ts +74 -86
- package/dist/types/core/owner.d.ts +3 -3
- package/dist/types/core/scheduler.d.ts +255 -331
- package/dist/types/core/types.d.ts +45 -85
- package/dist/types/core/verdict.d.ts +3 -0
- package/dist/types/flatten.d.ts +8 -0
- package/dist/types/index.d.ts +6 -4
- package/dist/types/reveal.d.ts +19 -0
- package/dist/types/signals.d.ts +35 -91
- package/dist/types/store/affects.d.ts +12 -0
- package/dist/types/store/hooks.d.ts +48 -0
- package/dist/types/store/index.d.ts +34 -15
- package/dist/types/store/optimistic.d.ts +23 -0
- package/dist/types/store/projection.d.ts +18 -0
- package/dist/types/store/reconcile.d.ts +3 -0
- package/dist/types/store/store.d.ts +132 -145
- package/dist/types/store/storePath.d.ts +1 -1
- package/dist/types/store/target.d.ts +151 -0
- package/dist/types/store/types.d.ts +84 -0
- package/package.json +1 -1
- package/dist/observe/core/optimistic.js +0 -669
- package/dist/observe/store/next/optimistic.js +0 -717
- package/dist/observe/store/next/projection.js +0 -266
- package/dist/observe/store/next/reconcile.js +0 -375
- package/dist/observe/store/next/store.js +0 -2608
- package/dist/prod/core/optimistic.js +0 -658
- package/dist/prod/store/next/optimistic.js +0 -710
- package/dist/prod/store/next/projection.js +0 -257
- package/dist/prod/store/next/reconcile.js +0 -373
- package/dist/prod/store/next/store.js +0 -2484
- package/dist/types/core/optimistic.d.ts +0 -6
- package/dist/types/store/next/optimistic.d.ts +0 -25
- package/dist/types/store/next/projection.d.ts +0 -27
- package/dist/types/store/next/reconcile.d.ts +0 -10
- package/dist/types/store/next/store.d.ts +0 -145
- package/dist/types/store/next/target.d.ts +0 -196
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { setAttributionHooks } from "./attribution-hooks.js";
|
|
2
2
|
|
|
3
|
-
import { NOT_PENDING, CONFIG_PLUMBING,
|
|
3
|
+
import { NOT_PENDING, CONFIG_PLUMBING, CONFIG_OVERRIDE, CONFIG_GUESS, CONFIG_VERDICT } from "./constants.js";
|
|
4
4
|
|
|
5
5
|
import { OBSERVE, liveRootOwners, emitDiagnostic, ownerPath, reportDiagnostic, isSuppressed, anyExcluded, isExcluded, GRAPH_SIZE_WARN_AT, noteFanOut } from "./dev.js";
|
|
6
6
|
|
|
7
|
+
import { blockersOf, GlobalQueue } from "./scheduler.js";
|
|
8
|
+
|
|
7
9
|
/** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */ const FALLBACK_FLASH_MS = 150;
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -15,11 +17,11 @@ import { OBSERVE, liveRootOwners, emitDiagnostic, ownerPath, reportDiagnostic, i
|
|
|
15
17
|
* observer exclusion is cached per node once any exists, before that a flag
|
|
16
18
|
* read.
|
|
17
19
|
*/ function excludedNode(e) {
|
|
18
|
-
if ((e.
|
|
20
|
+
if ((e.i & CONFIG_PLUMBING) !== 0) return true;
|
|
19
21
|
if (!anyExcluded()) return false;
|
|
20
22
|
const n = e;
|
|
21
|
-
if (n
|
|
22
|
-
return n
|
|
23
|
+
if (n.he === undefined) n.he = isExcluded(e);
|
|
24
|
+
return n.he;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
let attributionActive = false;
|
|
@@ -42,7 +44,7 @@ const defaultOptions = {
|
|
|
42
44
|
count: 120,
|
|
43
45
|
windowMs: 1e3
|
|
44
46
|
},
|
|
45
|
-
wideDeps:
|
|
47
|
+
wideDeps: 200,
|
|
46
48
|
hotTime: {
|
|
47
49
|
budgetMs: 8,
|
|
48
50
|
windowMs: 1e3
|
|
@@ -119,7 +121,7 @@ function nodeName(e) {
|
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
/** The record vocabulary for a computation's kind: effects carry a `_type`, memos do not. */ function nodeKind(e) {
|
|
122
|
-
return e.
|
|
124
|
+
return e.u ? "effect" : "memo";
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/**
|
|
@@ -224,17 +226,24 @@ const interactionStack = [];
|
|
|
224
226
|
|
|
225
227
|
function interactionStart(e) {
|
|
226
228
|
interactionStack.push(currentInteraction);
|
|
229
|
+
// No `ref.event`: the WeakMap answers undefined.
|
|
230
|
+
const n = eventInteractions.get(e.event);
|
|
231
|
+
if (n?.joinable && openInteractions.has(n)) {
|
|
232
|
+
n.open = true;
|
|
233
|
+
currentInteraction = n.event.origin;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
227
236
|
// One clock read: the frame opens now; the interaction began at `ref.at`
|
|
228
237
|
// when the runtime dated it (the event's own timestamp), else now too.
|
|
229
|
-
const
|
|
230
|
-
const
|
|
238
|
+
const t = now();
|
|
239
|
+
const o = {
|
|
231
240
|
kind: "interaction",
|
|
232
241
|
name: e.type,
|
|
233
|
-
at: e.at ??
|
|
242
|
+
at: e.at ?? t
|
|
234
243
|
};
|
|
235
|
-
if (e.target)
|
|
236
|
-
currentInteraction =
|
|
237
|
-
openInteraction(t,
|
|
244
|
+
if (e.target) o.target = targetLabel(e.target);
|
|
245
|
+
currentInteraction = o;
|
|
246
|
+
openInteraction(o, t, e.event);
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
function interactionEnd(e) {
|
|
@@ -318,7 +327,7 @@ const effectFrames = new WeakMap;
|
|
|
318
327
|
const n = effectStack[effectStack.length - 1];
|
|
319
328
|
if (n !== undefined) effectFrames.set(e, {
|
|
320
329
|
node: n,
|
|
321
|
-
causes: n.
|
|
330
|
+
causes: n.me
|
|
322
331
|
});
|
|
323
332
|
}
|
|
324
333
|
|
|
@@ -336,7 +345,7 @@ function pushFrame(e, n, t, o) {
|
|
|
336
345
|
if (s !== undefined) i.interaction = s;
|
|
337
346
|
if (o !== undefined) {
|
|
338
347
|
const e = o;
|
|
339
|
-
if (e.
|
|
348
|
+
if (e.ge !== undefined) i.run = e.ge;
|
|
340
349
|
// The frame → node map is filled by the first write inside the callback
|
|
341
350
|
// (noteEffectOrigin), not here: most callbacks never write.
|
|
342
351
|
effectStack.push(o);
|
|
@@ -433,12 +442,6 @@ function originEnd() {
|
|
|
433
442
|
}
|
|
434
443
|
|
|
435
444
|
/** Record a root change (setSignal / refresh / async landing) on the node. */
|
|
436
|
-
/** Live subscriber count, walked on demand — the core keeps no counter. */ function countSubscribers(e) {
|
|
437
|
-
let n = 0;
|
|
438
|
-
for (let t = e.u; t !== null; t = t.Pe) n++;
|
|
439
|
-
return n;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
445
|
/**
|
|
443
446
|
* HUGE_FAN_OUT from the engine's lower `fanOut` threshold (see dev.ts): a
|
|
444
447
|
* committed root invalidation reaching hundreds of subscribers re-runs all
|
|
@@ -447,12 +450,20 @@ function originEnd() {
|
|
|
447
450
|
* was a post-construction field that forked node shapes). Stops at
|
|
448
451
|
* GRAPH_SIZE_WARN_AT, where the always-on core check takes over, so the two
|
|
449
452
|
* never fire for the same write; the once-per-node dedupe is the core's.
|
|
453
|
+
* HMR plumbing subscribers (the refresh memo of every mounted instance, on
|
|
454
|
+
* a component's registration signal) are not counted toward `fanOut`; the
|
|
455
|
+
* handover is on the full list, which is what the core counts.
|
|
450
456
|
*/ function checkFanOut(e, n) {
|
|
451
457
|
const t = options.fanOut;
|
|
452
458
|
if (typeof t !== "number") return;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
459
|
+
let o = 0;
|
|
460
|
+
let i = 0;
|
|
461
|
+
for (let n = e.t; n !== null; n = n.o) {
|
|
462
|
+
o++;
|
|
463
|
+
if (!(n.l.i & CONFIG_PLUMBING)) i++;
|
|
464
|
+
}
|
|
465
|
+
if (i < t || o >= GRAPH_SIZE_WARN_AT) return;
|
|
466
|
+
noteFanOut(e, i, n);
|
|
456
467
|
}
|
|
457
468
|
|
|
458
469
|
function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
@@ -472,8 +483,8 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
472
483
|
i.origin = n === "async" ? asyncOrigin(e) : currentOrigin();
|
|
473
484
|
i.at = now();
|
|
474
485
|
i.stack = captureStack();
|
|
475
|
-
const s = e.
|
|
476
|
-
e.
|
|
486
|
+
const s = e.we;
|
|
487
|
+
e.we = i;
|
|
477
488
|
noteNavigationWrite(s, i);
|
|
478
489
|
noteInteractionWrite(i.origin, excludedNode(e));
|
|
479
490
|
if (n === "write") trackEffectWrite(e, i, o);
|
|
@@ -484,7 +495,7 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
484
495
|
}
|
|
485
496
|
|
|
486
497
|
/** Record a derived change (memo produced a new value) with its causes. */ function stampDerived(e, n) {
|
|
487
|
-
e.
|
|
498
|
+
e.we = {
|
|
488
499
|
seq: ++changeSeq,
|
|
489
500
|
kind: "derived",
|
|
490
501
|
name: nodeName(e),
|
|
@@ -499,19 +510,19 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
499
510
|
* run's links. A refresh() stamp on the node itself also counts — that is a
|
|
500
511
|
* self-invalidation, not a dep change.
|
|
501
512
|
*/ function collectCauses(e) {
|
|
502
|
-
const n = e.
|
|
513
|
+
const n = e.ve ?? 0;
|
|
503
514
|
const t = [];
|
|
504
|
-
const o = e.
|
|
515
|
+
const o = e.we;
|
|
505
516
|
if (o !== undefined && o.seq > n && o.kind === "refresh") t.push(o);
|
|
506
|
-
for (let o = e.
|
|
507
|
-
const e = o.
|
|
517
|
+
for (let o = e.G; o !== null; o = o._) {
|
|
518
|
+
const e = o.I.we;
|
|
508
519
|
if (e !== undefined && e.seq > n) t.push(e);
|
|
509
520
|
}
|
|
510
521
|
return t;
|
|
511
522
|
}
|
|
512
523
|
|
|
513
524
|
/** Advance the node's seen-cursor to the present. Call after every run. */ function markSeen(e) {
|
|
514
|
-
e.
|
|
525
|
+
e.ve = changeSeq;
|
|
515
526
|
}
|
|
516
527
|
|
|
517
528
|
/**
|
|
@@ -523,32 +534,39 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
523
534
|
* still owed) and not part of the subscription diff.
|
|
524
535
|
*/ function captureDeps(e) {
|
|
525
536
|
const n = [];
|
|
526
|
-
for (let t = e.
|
|
527
|
-
n.push(t.
|
|
528
|
-
if (t === e.
|
|
537
|
+
for (let t = e.G; t !== null; t = t._) {
|
|
538
|
+
n.push(t.I);
|
|
539
|
+
if (t === e.p) break;
|
|
529
540
|
}
|
|
530
541
|
return n;
|
|
531
542
|
}
|
|
532
543
|
|
|
533
544
|
/**
|
|
534
545
|
* Wide-scope warning — the coarse-read / helper-leak signature: one scope
|
|
535
|
-
* subscribed to
|
|
546
|
+
* subscribed to hundreds of sources re-runs when ANY of them change. Fired from
|
|
536
547
|
* recordRerun for re-runs and directly from recompute for creation runs (a
|
|
537
|
-
* memo can be born too wide). Re-warns only on 50% further growth.
|
|
548
|
+
* memo can be born too wide). Re-warns only on 50% further growth. Counts
|
|
549
|
+
* what the scope's author can act on: HMR plumbing sources are not counted,
|
|
550
|
+
* and a framework scope wide by construction (CONFIG_WIDE) is not judged —
|
|
551
|
+
* the always-on HUGE_FAN_IN still is.
|
|
538
552
|
*/ function checkDepWidth(e) {
|
|
539
553
|
const n = options.wideDeps;
|
|
540
|
-
|
|
554
|
+
// Only dev sets CONFIG_WIDE (see constants.ts).
|
|
555
|
+
if (n === false || false) return;
|
|
541
556
|
let t = 0;
|
|
542
557
|
const o = [];
|
|
543
|
-
for (let n = e.
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
|
|
558
|
+
for (let n = e.G; n !== null; n = n._) {
|
|
559
|
+
// HMR plumbing is a dev-only hop the scope cannot read past: not a source.
|
|
560
|
+
if (!(n.I.i & CONFIG_PLUMBING)) {
|
|
561
|
+
t++;
|
|
562
|
+
if (o.length < 12) o.push(nodeName(n.I));
|
|
563
|
+
}
|
|
564
|
+
if (n === e.p) break;
|
|
547
565
|
// the validated prefix, as captureDeps
|
|
548
566
|
}
|
|
549
567
|
const i = e;
|
|
550
|
-
if (t < n || t < (i.
|
|
551
|
-
i.
|
|
568
|
+
if (t < n || t < (i.ke ?? 0) * 1.5) return;
|
|
569
|
+
i.ke = t;
|
|
552
570
|
const s = `[WIDE_SCOPE_DEPS] ${nodeKind(e)} "${nodeName(e)}" is subscribed to ${t} sources — ` + `it re-runs when any of them change. Narrow its reads or split it into smaller memos. ` + `Sources: ${o.join(", ")}${t > o.length ? ", …" : ""}`;
|
|
553
571
|
reportDiagnostic(emitDiagnostic({
|
|
554
572
|
code: "WIDE_SCOPE_DEPS",
|
|
@@ -577,14 +595,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
577
595
|
if (t === false) return;
|
|
578
596
|
const o = e;
|
|
579
597
|
const i = Date.now();
|
|
580
|
-
if (o.
|
|
581
|
-
o.
|
|
582
|
-
o
|
|
583
|
-
o.
|
|
584
|
-
}
|
|
585
|
-
o
|
|
586
|
-
if (o.
|
|
587
|
-
o.
|
|
598
|
+
if (o.ye === undefined || i - o.ye > t.windowMs) {
|
|
599
|
+
o.ye = i;
|
|
600
|
+
o.$e = 0;
|
|
601
|
+
o.be = false;
|
|
602
|
+
}
|
|
603
|
+
o.$e = (o.$e ?? 0) + 1;
|
|
604
|
+
if (o.be || o.$e < t.count) return;
|
|
605
|
+
o.be = true;
|
|
588
606
|
// Root-cause key: the set of originating writes behind this scope's latest
|
|
589
607
|
// re-run. Scopes hot from the SAME roots share one aggregation window.
|
|
590
608
|
const s = new Set;
|
|
@@ -601,10 +619,10 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
601
619
|
hotCauses.set(r, a);
|
|
602
620
|
}
|
|
603
621
|
a.scopes++;
|
|
604
|
-
a.runs += o
|
|
622
|
+
a.runs += o.$e;
|
|
605
623
|
if (a.scopes === 1) {
|
|
606
624
|
const s = n.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
607
|
-
const r = `[HOT_SCOPE_RERUNS] ${nodeKind(e)} "${nodeName(e)}" re-ran ${o
|
|
625
|
+
const r = `[HOT_SCOPE_RERUNS] ${nodeKind(e)} "${nodeName(e)}" re-ran ${o.$e} times ` + `in ${Math.max(1, i - o.ye)}ms — a hot signal is likely leaking into this ` + `scope. Latest cause: ${s || "(untracked pull)"}`;
|
|
608
626
|
reportDiagnostic(emitDiagnostic({
|
|
609
627
|
code: "HOT_SCOPE_RERUNS",
|
|
610
628
|
kind: "perf",
|
|
@@ -612,7 +630,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
612
630
|
message: r,
|
|
613
631
|
nodeName: nodeName(e),
|
|
614
632
|
data: {
|
|
615
|
-
runs: o
|
|
633
|
+
runs: o.$e,
|
|
616
634
|
windowMs: t.windowMs,
|
|
617
635
|
causes: n.map(e => e.name)
|
|
618
636
|
}
|
|
@@ -650,16 +668,16 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
650
668
|
if (o === false) return;
|
|
651
669
|
const i = e;
|
|
652
670
|
const s = now();
|
|
653
|
-
if (i.
|
|
654
|
-
i.
|
|
655
|
-
i.
|
|
656
|
-
i.
|
|
657
|
-
}
|
|
658
|
-
i.
|
|
659
|
-
if (i.
|
|
660
|
-
i.
|
|
671
|
+
if (i.Fe === undefined || s - i.Fe > o.windowMs) {
|
|
672
|
+
i.Fe = s;
|
|
673
|
+
i.Le = 0;
|
|
674
|
+
i.Ce = false;
|
|
675
|
+
}
|
|
676
|
+
i.Le = (i.Le ?? 0) + n;
|
|
677
|
+
if (i.Ce || i.Le < o.budgetMs) return;
|
|
678
|
+
i.Ce = true;
|
|
661
679
|
const r = t.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
662
|
-
const a = `[HOT_SCOPE_TIME] ${nodeKind(e)} "${nodeName(e)}" spent ` + `${i.
|
|
680
|
+
const a = `[HOT_SCOPE_TIME] ${nodeKind(e)} "${nodeName(e)}" spent ` + `${i.Le.toFixed(1)}ms of compute inside one ${o.windowMs}ms window ` + `(budget ${o.budgetMs}ms). Latest cause: ${r || "(untracked pull)"}`;
|
|
663
681
|
reportDiagnostic(emitDiagnostic({
|
|
664
682
|
code: "HOT_SCOPE_TIME",
|
|
665
683
|
kind: "perf",
|
|
@@ -667,7 +685,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
667
685
|
message: a,
|
|
668
686
|
nodeName: nodeName(e),
|
|
669
687
|
data: {
|
|
670
|
-
spentMs: i.
|
|
688
|
+
spentMs: i.Le,
|
|
671
689
|
budgetMs: o.budgetMs,
|
|
672
690
|
windowMs: o.windowMs,
|
|
673
691
|
causes: t.map(e => e.name)
|
|
@@ -682,31 +700,34 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
682
700
|
* names it while it happens, with the input that keeps triggering it. The
|
|
683
701
|
* fix is upstream: an equality boundary on the part of the input the scope
|
|
684
702
|
* depends on, or a narrower read. Plain runs only — a held or overlay run
|
|
685
|
-
* may be replayed and is never blamed as waste.
|
|
703
|
+
* may be replayed and is never blamed as waste. A run that committed
|
|
704
|
+
* `undefined` arrives here as changed (see `recomputeEnd`): its work is a
|
|
705
|
+
* side effect (a projection's draft writes, a write from inside a memo), not
|
|
706
|
+
* its output.
|
|
686
707
|
*/ function checkWastedRecompute(e, n, t, o, i, s) {
|
|
687
708
|
const r = options.wastedRecompute;
|
|
688
709
|
if (r === false || t !== "plain") return;
|
|
689
710
|
// This runs on every re-run: fields on the node (one property read each,
|
|
690
711
|
// like hotRuns) and the run's own `at` — no clock read, no map lookup.
|
|
691
712
|
const a = e;
|
|
692
|
-
if (a.
|
|
693
|
-
a.
|
|
694
|
-
a.
|
|
695
|
-
a.
|
|
696
|
-
a.
|
|
697
|
-
a.
|
|
698
|
-
}
|
|
699
|
-
a.
|
|
713
|
+
if (a.Me === undefined || n - a.Me > r.windowMs) {
|
|
714
|
+
a.Me = n;
|
|
715
|
+
a.He = 0;
|
|
716
|
+
a.We = 0;
|
|
717
|
+
a.Ge = 0;
|
|
718
|
+
a.Ue = false;
|
|
719
|
+
}
|
|
720
|
+
a.He = a.He + 1;
|
|
700
721
|
if (!o) {
|
|
701
|
-
a.
|
|
702
|
-
a.
|
|
722
|
+
a.We = a.We + 1;
|
|
723
|
+
a.Ge = a.Ge + i;
|
|
703
724
|
}
|
|
704
|
-
if (a.
|
|
705
|
-
a.
|
|
725
|
+
if (a.Ue || a.He < r.minRuns || a.We / a.He < r.ratio || a.Ge < r.budgetMs) return;
|
|
726
|
+
a.Ue = true;
|
|
706
727
|
const c = {
|
|
707
|
-
runs: a.
|
|
708
|
-
wasted: a.
|
|
709
|
-
wastedMs: a.
|
|
728
|
+
runs: a.He,
|
|
729
|
+
wasted: a.We,
|
|
730
|
+
wastedMs: a.Ge
|
|
710
731
|
};
|
|
711
732
|
const d = s.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
712
733
|
const f = `[WASTED_RECOMPUTE] ${nodeKind(e)} "${nodeName(e)}" re-ran ${c.runs} times in ` + `${r.windowMs}ms and ${c.wasted} of those produced the same value — ` + `${c.wastedMs.toFixed(1)}ms of compute the equality gate then discarded. Its inputs ` + `change without changing its result: put an equality boundary upstream (a memo over the ` + `part of the input it reads, or an \`equals\` on the source), or read a narrower slice ` + `(the property, not the object). Latest cause: ${d || "(untracked pull)"}`;
|
|
@@ -729,14 +750,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
729
750
|
function recordRerun(e, n, t, o, i, s) {
|
|
730
751
|
const r = n.causes;
|
|
731
752
|
const a = e;
|
|
732
|
-
const c = a.
|
|
753
|
+
const c = a.me;
|
|
733
754
|
const d = n.interaction;
|
|
734
755
|
if (excludedNode(e)) {
|
|
735
756
|
// The observer's own computation: keep the per-node bookkeeping its
|
|
736
757
|
// effect phase reads (see effectRunStart) and record nothing.
|
|
737
|
-
a.
|
|
738
|
-
a.
|
|
739
|
-
a.
|
|
758
|
+
a.qe = d;
|
|
759
|
+
a.ge = undefined;
|
|
760
|
+
a.me = r;
|
|
740
761
|
return;
|
|
741
762
|
}
|
|
742
763
|
// The facts every run leaves on the node, record or not: the run sequence
|
|
@@ -745,10 +766,10 @@ function recordRerun(e, n, t, o, i, s) {
|
|
|
745
766
|
// cause list of its own, so it takes this run's interaction and causes
|
|
746
767
|
// (see effectRunStart / pushFrame).
|
|
747
768
|
const f = ++runSeq;
|
|
748
|
-
const u = a.
|
|
749
|
-
a.
|
|
750
|
-
a.
|
|
751
|
-
a.
|
|
769
|
+
const u = a.Be = (a.Be ?? 0) + 1;
|
|
770
|
+
a.qe = d;
|
|
771
|
+
a.ge = f;
|
|
772
|
+
a.me = r;
|
|
752
773
|
noteInteractionRun(d, t.selfMs, false);
|
|
753
774
|
if (openFlush !== null) noteFlushRun(openFlush, false, d);
|
|
754
775
|
// The checks read the facts, not the record, so they run with or without
|
|
@@ -865,8 +886,10 @@ function shallowEquivalent(e, n) {
|
|
|
865
886
|
for (let e = 0; e < t.length; e++) if (t[e] !== o[e]) return false;
|
|
866
887
|
return true;
|
|
867
888
|
}
|
|
868
|
-
|
|
869
|
-
|
|
889
|
+
// Every own key, symbols included: a symbol-keyed box holding a fresh value
|
|
890
|
+
// each run (web's `dynamic` boxes its in-flight promise) is not equivalent.
|
|
891
|
+
const o = Reflect.ownKeys(e);
|
|
892
|
+
if (o.length > UNSTABLE_KEY_CAP || o.length !== Reflect.ownKeys(n).length) return false;
|
|
870
893
|
for (const t of o) {
|
|
871
894
|
if (!(t in n) || e[t] !== n[t]) return false;
|
|
872
895
|
}
|
|
@@ -887,15 +910,15 @@ function shallowEquivalent(e, n) {
|
|
|
887
910
|
const i = e;
|
|
888
911
|
if (n === t || // paranoia: changed runs should never hit this
|
|
889
912
|
!isPlainShape(n) || !isPlainShape(t) || !shallowEquivalent(n, t)) {
|
|
890
|
-
i.
|
|
891
|
-
i.
|
|
913
|
+
i.xe = 0;
|
|
914
|
+
i.Ke = false;
|
|
892
915
|
return;
|
|
893
916
|
}
|
|
894
|
-
i.
|
|
895
|
-
if (i.
|
|
896
|
-
i.
|
|
917
|
+
i.xe = (i.xe ?? 0) + 1;
|
|
918
|
+
if (i.Ke || i.xe < o) return;
|
|
919
|
+
i.Ke = true;
|
|
897
920
|
const s = Array.isArray(t) ? "array" : "object";
|
|
898
|
-
const r = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${s} on ` + `${i.
|
|
921
|
+
const r = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${s} on ` + `${i.xe} consecutive runs — its equality gate never closes, so every ` + `subscriber re-runs on every upstream change. Return stable references or pass an ` + `\`equals\` option.`;
|
|
899
922
|
reportDiagnostic(emitDiagnostic({
|
|
900
923
|
code: "UNSTABLE_MEMO_OUTPUT",
|
|
901
924
|
kind: "perf",
|
|
@@ -903,7 +926,7 @@ function shallowEquivalent(e, n) {
|
|
|
903
926
|
message: r,
|
|
904
927
|
nodeName: nodeName(e),
|
|
905
928
|
data: {
|
|
906
|
-
runs: i.
|
|
929
|
+
runs: i.xe,
|
|
907
930
|
shape: s
|
|
908
931
|
}
|
|
909
932
|
}, e));
|
|
@@ -1042,17 +1065,17 @@ const copyReported = new WeakSet;
|
|
|
1042
1065
|
const s = i !== undefined && i.kind === "effect" ? effectFrames.get(i) : undefined;
|
|
1043
1066
|
const r = s === undefined ? 0 : devId(s.node);
|
|
1044
1067
|
recordNodes.set(n, e);
|
|
1045
|
-
o.
|
|
1046
|
-
if (s !== undefined && t !== undefined && t === s.node.
|
|
1068
|
+
o.Ve = o.Ve === undefined || o.Ve === r ? r : null;
|
|
1069
|
+
if (s !== undefined && t !== undefined && t === s.node.Ae) {
|
|
1047
1070
|
copyWrites.add(n);
|
|
1048
|
-
o.
|
|
1049
|
-
o.
|
|
1050
|
-
if (o.
|
|
1051
|
-
} else o.
|
|
1071
|
+
o.je = o.Ye === r ? (o.je ?? 0) + 1 : 1;
|
|
1072
|
+
o.Ye = r;
|
|
1073
|
+
if (o.je >= 2 && o.Ve === r) checkCopyEffect(s.node, e);
|
|
1074
|
+
} else o.je = 0;
|
|
1052
1075
|
}
|
|
1053
1076
|
|
|
1054
1077
|
/** The effect's source whose current value the compute output is, if any (the prop-to-state port). */ function passthroughSource(e) {
|
|
1055
|
-
for (let n = e.
|
|
1078
|
+
for (let n = e.G; n !== null; n = n._) if (n.I.Ae === e.Ae) return nodeName(n.I);
|
|
1056
1079
|
return undefined;
|
|
1057
1080
|
}
|
|
1058
1081
|
|
|
@@ -1125,7 +1148,7 @@ function checkCopyEffect(e, n) {
|
|
|
1125
1148
|
c.count++;
|
|
1126
1149
|
const d = copyWrites.has(s);
|
|
1127
1150
|
const f = recordNodes.get(s);
|
|
1128
|
-
const u = f !== undefined && f.
|
|
1151
|
+
const u = f !== undefined && f.Ve === devId(i.node);
|
|
1129
1152
|
// Derivable outright: the value is the compute output and nothing else
|
|
1130
1153
|
// writes the signal. A copy INTO a signal that has other writers is the
|
|
1131
1154
|
// "reset editable state from a source" shape — the tear is real, but a memo
|
|
@@ -1141,7 +1164,7 @@ function checkCopyEffect(e, n) {
|
|
|
1141
1164
|
copyReported.add(f);
|
|
1142
1165
|
}
|
|
1143
1166
|
if (h === "warn") c.warned = true;
|
|
1144
|
-
const p = e.
|
|
1167
|
+
const p = e.u ? "effect" : "memo";
|
|
1145
1168
|
const m = nodeName(i.node);
|
|
1146
1169
|
const g = l ? copyRepair(i.node, s.name) : d ? `The written value is the effect's compute output, but "${s.name}" has other ` + `writers — editable state reset from a source. If the reset is the intent, the tear ` + `is its cost; if "${s.name}" only ever mirrors the source, drop the local copy ` + `and read the source.` : u ? `Nothing else writes "${s.name}" — it is derived state: make it a memo over what ` + `the effect reads and every reader gets it in the same flush.` : `If "${s.name}" is computed from what the effect reads, make it a memo so readers ` + `get it in the same flush; if the write reads something outside the graph (layout, ` + `time), the tear is the cost of measuring.`;
|
|
1147
1170
|
const w = `[EFFECT_RELAY_TEAR] ${p} "${nodeName(e)}" ran twice for one write of ` + `"${r.name}": once in the flush where "${r.name}" changed, and again after ` + `effect "${m}" relayed it by writing "${s.name}" — the first frame showed the ` + `new "${r.name}" with the stale "${s.name}"` + (c.count > 1 ? ` (${c.count} times so far)` : "") + `. ${g}`;
|
|
@@ -1232,7 +1255,7 @@ const listIdentityWarned = new WeakSet;
|
|
|
1232
1255
|
function recordId(e) {
|
|
1233
1256
|
if (e === null || typeof e !== "object") return undefined;
|
|
1234
1257
|
const n = e;
|
|
1235
|
-
return n.id ?? n.key ?? n.
|
|
1258
|
+
return n.id ?? n.key ?? n.ze ?? undefined;
|
|
1236
1259
|
}
|
|
1237
1260
|
|
|
1238
1261
|
function checkListIdentity(e, n, t, o, i) {
|
|
@@ -1389,7 +1412,7 @@ function trackFlightStart(e, n) {
|
|
|
1389
1412
|
const o = t - n.origin;
|
|
1390
1413
|
for (const n of folds) n.flightLanded?.(e, o);
|
|
1391
1414
|
if (records.observed("flight")) emitFlight(e, n, t, "landed");
|
|
1392
|
-
const i = e.
|
|
1415
|
+
const i = e.we;
|
|
1393
1416
|
// Only a stamp this landing produced may carry the measurement — a stale
|
|
1394
1417
|
// async record from a previous landing must not be re-labeled.
|
|
1395
1418
|
if (i !== undefined && i.kind === "async" && i.seq > n.startSeq) landedFlights.set(i, {
|
|
@@ -1425,8 +1448,8 @@ function checkWaterfall(e, n, t) {
|
|
|
1425
1448
|
}
|
|
1426
1449
|
if (i < 2) return;
|
|
1427
1450
|
const r = e;
|
|
1428
|
-
if ((r.
|
|
1429
|
-
r.
|
|
1451
|
+
if ((r.Xe ?? 0) >= i) return;
|
|
1452
|
+
r.Xe = i;
|
|
1430
1453
|
const a = [ ...n.slice(n.length - (i - 1)), {
|
|
1431
1454
|
name: nodeName(e),
|
|
1432
1455
|
ms: t
|
|
@@ -1460,10 +1483,10 @@ let activeHold = null;
|
|
|
1460
1483
|
|
|
1461
1484
|
let holdLog = [];
|
|
1462
1485
|
|
|
1463
|
-
/**
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
return
|
|
1486
|
+
/** A lane's derivation (L2: a node carrying the lane's value that is not a
|
|
1487
|
+
* written guess — lanes.ts) is a lane pass's result, not a write anyone
|
|
1488
|
+
* made: not an acknowledgement. */ function isCompanion(e) {
|
|
1489
|
+
return (e.i & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
|
|
1467
1490
|
}
|
|
1468
1491
|
|
|
1469
1492
|
const HOLD_CENSUS_CAP = 1e4;
|
|
@@ -1484,13 +1507,14 @@ function acknowledge(e, n, t, o) {
|
|
|
1484
1507
|
}); else if (r !== undefined) s.reader ??= r;
|
|
1485
1508
|
}
|
|
1486
1509
|
|
|
1487
|
-
|
|
1510
|
+
/** The declarations made against the hold: the guesses of its lanes
|
|
1511
|
+
* (lanes.ts `_laneGuesses`) and its `affects()` marks (`_marks`). */ function censusRegistrations(e, n) {
|
|
1488
1512
|
const t = {
|
|
1489
1513
|
left: HOLD_CENSUS_CAP
|
|
1490
1514
|
};
|
|
1491
|
-
|
|
1492
|
-
for (const
|
|
1493
|
-
for (const o of e.
|
|
1515
|
+
const o = GlobalQueue.Qe?.(e);
|
|
1516
|
+
if (o !== undefined) for (const e of o) acknowledge(n, "optimistic", nodeName(e), reachesEffect(e, t));
|
|
1517
|
+
if (e.T !== null) for (const o of e.T) acknowledge(n, "affects", nodeName(o), reachesEffect(o, t));
|
|
1494
1518
|
}
|
|
1495
1519
|
|
|
1496
1520
|
/**
|
|
@@ -1504,9 +1528,9 @@ function censusRegistrations(e, n) {
|
|
|
1504
1528
|
const o = [ e ];
|
|
1505
1529
|
while (o.length > 0 && n.left-- > 0) {
|
|
1506
1530
|
const e = o.pop();
|
|
1507
|
-
for (let n = e.
|
|
1508
|
-
const e = n.
|
|
1509
|
-
if (e.
|
|
1531
|
+
for (let n = e.t; n !== null; n = n.o) {
|
|
1532
|
+
const e = n.l;
|
|
1533
|
+
if (e.u) return e;
|
|
1510
1534
|
if (!t.has(e)) {
|
|
1511
1535
|
t.add(e);
|
|
1512
1536
|
o.push(e);
|
|
@@ -1516,7 +1540,10 @@ function censusRegistrations(e, n) {
|
|
|
1516
1540
|
return null;
|
|
1517
1541
|
}
|
|
1518
1542
|
|
|
1519
|
-
/**
|
|
1543
|
+
/** Verdict readers an effect reads, anywhere downstream of the hold's nodes
|
|
1544
|
+
* (L2: a pass that entered an `isPending`/`latest` window is marked
|
|
1545
|
+
* CONFIG_VERDICT; the observe tier notes which windows — verdict.ts
|
|
1546
|
+
* `_devWindows`). The reader acknowledges the node it derives from. */ function censusCompanions(e, n) {
|
|
1520
1547
|
const t = new Set;
|
|
1521
1548
|
const o = [ ...e ];
|
|
1522
1549
|
const i = {
|
|
@@ -1526,14 +1553,16 @@ function censusRegistrations(e, n) {
|
|
|
1526
1553
|
const e = o.pop();
|
|
1527
1554
|
if (t.has(e)) continue;
|
|
1528
1555
|
t.add(e);
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1556
|
+
for (let t = e.t; t !== null; t = t.o) {
|
|
1557
|
+
const s = t.l;
|
|
1558
|
+
if (s.i & CONFIG_VERDICT) {
|
|
1559
|
+
const t = s.Ze ?? 0;
|
|
1560
|
+
const o = s.u ? s : reachesEffect(s, i);
|
|
1561
|
+
if (t & 1 && o !== null) acknowledge(n, "isPending", nodeName(e), o);
|
|
1562
|
+
if (t & 2 && o !== null) acknowledge(n, "latest", nodeName(e), o);
|
|
1563
|
+
}
|
|
1564
|
+
o.push(s);
|
|
1535
1565
|
}
|
|
1536
|
-
for (let n = e.u; n !== null; n = n.Pe) o.push(n.Ae);
|
|
1537
1566
|
}
|
|
1538
1567
|
}
|
|
1539
1568
|
|
|
@@ -1561,8 +1590,8 @@ function trackHoldStart(e) {
|
|
|
1561
1590
|
if (options.holds === false) return;
|
|
1562
1591
|
const n = holdState(e);
|
|
1563
1592
|
n.flushes++;
|
|
1564
|
-
if (e.tt
|
|
1565
|
-
for (const
|
|
1593
|
+
if (e.tt > 0) n.action = true;
|
|
1594
|
+
for (const t of blockersOf(e)) n.blockers.add(t);
|
|
1566
1595
|
censusRegistrations(e, n);
|
|
1567
1596
|
activeHold = n;
|
|
1568
1597
|
}
|
|
@@ -1599,9 +1628,9 @@ function trackHoldSettled(e) {
|
|
|
1599
1628
|
let i;
|
|
1600
1629
|
let s;
|
|
1601
1630
|
let r = -Infinity;
|
|
1602
|
-
for (const n of e.
|
|
1603
|
-
if (typeof n.
|
|
1604
|
-
const e = n.
|
|
1631
|
+
for (const n of e.Je) {
|
|
1632
|
+
if (typeof n.ee === "function" || isCompanion(n)) continue;
|
|
1633
|
+
const e = n.we;
|
|
1605
1634
|
if (e === undefined || e.kind !== "write") continue;
|
|
1606
1635
|
if (o === null) o = n;
|
|
1607
1636
|
const a = {
|
|
@@ -1628,7 +1657,7 @@ function trackHoldSettled(e) {
|
|
|
1628
1657
|
return;
|
|
1629
1658
|
}
|
|
1630
1659
|
censusRegistrations(e, n);
|
|
1631
|
-
censusCompanions([ ...e.
|
|
1660
|
+
censusCompanions([ ...e.Je, ...n.blockers ], n);
|
|
1632
1661
|
const a = now();
|
|
1633
1662
|
// The hold began no later than its first parked flush; an interaction stamp
|
|
1634
1663
|
// reaches further back (dispatch). A node rewritten mid-hold keeps only its
|
|
@@ -1789,7 +1818,7 @@ function checkLongHold(e, n) {
|
|
|
1789
1818
|
if (!options.optimisticReverts || isCompanion(e)) return;
|
|
1790
1819
|
// Method call, as every commit path does: store slot nodes share one
|
|
1791
1820
|
// comparator that reads `this._host` (#3687).
|
|
1792
|
-
const i = e.
|
|
1821
|
+
const i = e.en;
|
|
1793
1822
|
if (i && i.call(e, n, t)) return;
|
|
1794
1823
|
const s = nodeName(e);
|
|
1795
1824
|
// The two values are user data: quoted only under `values: "full"`; the
|
|
@@ -1816,7 +1845,16 @@ function checkLongHold(e, n) {
|
|
|
1816
1845
|
}
|
|
1817
1846
|
|
|
1818
1847
|
// --- Graph growth -----------------------------------------------------------------
|
|
1819
|
-
/**
|
|
1848
|
+
/**
|
|
1849
|
+
* Per route: at its last `visits` compared settles, oldest first, the
|
|
1850
|
+
* graph's size and the part of it that was already alive when that
|
|
1851
|
+
* navigation opened.
|
|
1852
|
+
*/ const routeCounts = new Map;
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* The route the app last arrived on: a navigation that stays on it (only
|
|
1856
|
+
* its params or query change) continues the visit and is not compared.
|
|
1857
|
+
*/ let visitRoute;
|
|
1820
1858
|
|
|
1821
1859
|
/**
|
|
1822
1860
|
* The live graph's size — a walk, not a counter: nothing is charged at node
|
|
@@ -1831,51 +1869,78 @@ function checkLongHold(e, n) {
|
|
|
1831
1869
|
* millisecond. Dormant nodes are spliced out of their chain and are not
|
|
1832
1870
|
* counted unless a subscription still reaches them.
|
|
1833
1871
|
*/ function graphSize() {
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1872
|
+
return walkGraph();
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* The walk behind `graphSize()`. Given `before` alone, it adds every node it
|
|
1877
|
+
* meets to that set: what is alive as a navigation opens. Given `before` and
|
|
1878
|
+
* `survived`, it also counts into `survived` the nodes it meets that are in
|
|
1879
|
+
* the set, and an edge when both its ends are: what outlived the navigation.
|
|
1880
|
+
* Nothing is retained — the set is weak.
|
|
1881
|
+
*/ function walkGraph(e, n) {
|
|
1882
|
+
const t = liveRootOwners();
|
|
1883
|
+
const o = {
|
|
1884
|
+
roots: 0,
|
|
1837
1885
|
owners: 0,
|
|
1838
1886
|
computations: 0,
|
|
1839
1887
|
signals: 0,
|
|
1840
1888
|
edges: 0
|
|
1841
1889
|
};
|
|
1842
|
-
const
|
|
1890
|
+
const i = new Set;
|
|
1843
1891
|
// A worklist, not recursion: a subscriber chain can be thousands deep.
|
|
1844
|
-
const
|
|
1845
|
-
const
|
|
1892
|
+
const s = [];
|
|
1893
|
+
const r = [];
|
|
1894
|
+
const count = (t, i) => {
|
|
1895
|
+
o[i]++;
|
|
1896
|
+
if (n !== undefined) {
|
|
1897
|
+
if (e.has(t)) n[i]++;
|
|
1898
|
+
} else if (e !== undefined) e.add(t);
|
|
1899
|
+
};
|
|
1846
1900
|
const meet = e => {
|
|
1847
|
-
if (
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1901
|
+
if (i.has(e)) return;
|
|
1902
|
+
i.add(e);
|
|
1903
|
+
count(e, typeof e.ee === "function" ? "computations" : "signals");
|
|
1904
|
+
s.push(e);
|
|
1851
1905
|
};
|
|
1852
|
-
for (const
|
|
1853
|
-
|
|
1906
|
+
for (const e of t) {
|
|
1907
|
+
count(e, "roots");
|
|
1908
|
+
count(e, "owners");
|
|
1854
1909
|
// Iterative: a deep tree must not grow the call stack.
|
|
1855
|
-
let
|
|
1910
|
+
let n = e.nn;
|
|
1856
1911
|
for (;;) {
|
|
1857
|
-
while (
|
|
1858
|
-
n
|
|
1859
|
-
if (typeof
|
|
1860
|
-
if (
|
|
1861
|
-
|
|
1912
|
+
while (n !== null) {
|
|
1913
|
+
count(n, "owners");
|
|
1914
|
+
if (typeof n.ee === "function") meet(n);
|
|
1915
|
+
if (n.nn !== null) r.push(n);
|
|
1916
|
+
n = n.tn;
|
|
1862
1917
|
}
|
|
1863
|
-
const
|
|
1864
|
-
if (
|
|
1865
|
-
|
|
1918
|
+
const e = r.pop();
|
|
1919
|
+
if (e === undefined) break;
|
|
1920
|
+
n = e.nn;
|
|
1866
1921
|
}
|
|
1867
1922
|
}
|
|
1868
|
-
for (let
|
|
1869
|
-
const
|
|
1923
|
+
for (let t = 0; t < s.length; t++) {
|
|
1924
|
+
const i = s[t];
|
|
1870
1925
|
// Whoever reads this node: owned computations are already known; an
|
|
1871
1926
|
// ownerless one is met only here.
|
|
1872
|
-
for (let e = t
|
|
1873
|
-
if (typeof
|
|
1874
|
-
|
|
1875
|
-
meet(
|
|
1927
|
+
for (let e = i.t; e !== null; e = e.o) meet(e.l);
|
|
1928
|
+
if (typeof i.ee === "function") for (let t = i.G; t !== null; t = t._) {
|
|
1929
|
+
o.edges++;
|
|
1930
|
+
meet(t.I);
|
|
1931
|
+
if (n !== undefined && e.has(i) && e.has(t.I)) n.edges++;
|
|
1876
1932
|
}
|
|
1877
1933
|
}
|
|
1878
|
-
return
|
|
1934
|
+
return o;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/**
|
|
1938
|
+
* openNavigation, while the growth check is on: what is alive as a
|
|
1939
|
+
* navigation opens, for its settle to tell apart from what it created.
|
|
1940
|
+
*/ function snapshotGraph() {
|
|
1941
|
+
const e = new WeakSet;
|
|
1942
|
+
walkGraph(e);
|
|
1943
|
+
return e;
|
|
1879
1944
|
}
|
|
1880
1945
|
|
|
1881
1946
|
/**
|
|
@@ -1886,60 +1951,96 @@ function checkLongHold(e, n) {
|
|
|
1886
1951
|
* `createRoot` in an effect with no dispose, a subscription a component
|
|
1887
1952
|
* registered outside its owner. Nothing here runs per node; the walk is
|
|
1888
1953
|
* the cost, at navigation cadence.
|
|
1954
|
+
*
|
|
1955
|
+
* A count taken at an arrival also holds everything the visit just created,
|
|
1956
|
+
* so a page whose correctly owned content is bigger each visit climbs too.
|
|
1957
|
+
* What tells the two apart is what outlived the navigation: the nodes alive
|
|
1958
|
+
* when it opened (`NavState.before`) that are still alive at its settle. A
|
|
1959
|
+
* visit's own content is not among them; what an earlier visit left behind
|
|
1960
|
+
* is, whatever the content does. A series reports only when both climb.
|
|
1889
1961
|
*/ const GRAPH_SERIES = [ "owners", "computations", "signals", "edges" ];
|
|
1890
1962
|
|
|
1891
1963
|
function trackGraph(e) {
|
|
1892
|
-
const n =
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
const o =
|
|
1896
|
-
|
|
1964
|
+
const n = e.event;
|
|
1965
|
+
const t = e.before;
|
|
1966
|
+
e.before = undefined;
|
|
1967
|
+
const o = options.graphGrowth;
|
|
1968
|
+
if (o === false && !records.observed("graph")) return;
|
|
1969
|
+
const i = t && {
|
|
1970
|
+
roots: 0,
|
|
1971
|
+
owners: 0,
|
|
1972
|
+
computations: 0,
|
|
1973
|
+
signals: 0,
|
|
1974
|
+
edges: 0
|
|
1975
|
+
};
|
|
1976
|
+
const s = walkGraph(t, i);
|
|
1977
|
+
const r = n.name ?? n.to;
|
|
1978
|
+
const a = {
|
|
1897
1979
|
at: now(),
|
|
1898
|
-
...
|
|
1899
|
-
navigation:
|
|
1980
|
+
...s,
|
|
1981
|
+
navigation: n
|
|
1900
1982
|
};
|
|
1901
|
-
if (
|
|
1902
|
-
records.emit("graph",
|
|
1903
|
-
if (
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
//
|
|
1912
|
-
//
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1983
|
+
if (r !== undefined) a.route = r;
|
|
1984
|
+
records.emit("graph", a, undefined);
|
|
1985
|
+
if (o === false || r === undefined) return;
|
|
1986
|
+
// A navigation that stays on the route pattern continues the visit: the
|
|
1987
|
+
// page it kept outlived the move by design, so comparing it would read the
|
|
1988
|
+
// page itself as growth. Only arrivals that enter the route are compared —
|
|
1989
|
+
// a leak made during a visit is still alive when the next one starts. A
|
|
1990
|
+
// superseded navigation never showed, so the app is still where it was.
|
|
1991
|
+
const c = visitRoute;
|
|
1992
|
+
if (n.outcome !== "superseded") visitRoute = r;
|
|
1993
|
+
// The initial declaration is not a visit (it settles before route content
|
|
1994
|
+
// mounts — the same gate the feedback folds apply as `event.initial !== true`);
|
|
1995
|
+
// `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
|
|
1996
|
+
// A navigation opened before the check was on has no snapshot to compare.
|
|
1997
|
+
if (n.initial || r === c || i === undefined) return;
|
|
1998
|
+
let d = routeCounts.get(r);
|
|
1999
|
+
if (d === undefined) routeCounts.set(r, d = []);
|
|
2000
|
+
d.push({
|
|
2001
|
+
size: s,
|
|
2002
|
+
survived: i
|
|
1916
2003
|
});
|
|
1917
|
-
if (
|
|
2004
|
+
if (d.length > o.visits) d.shift();
|
|
2005
|
+
if (d.length < o.visits) return;
|
|
2006
|
+
const f = d.map(e => e.size);
|
|
2007
|
+
const u = d.map(e => e.survived);
|
|
2008
|
+
const climbs = (e, n) => {
|
|
2009
|
+
for (let t = 1; t < e.length; t++) if (e[t][n] <= e[t - 1][n]) return false;
|
|
2010
|
+
return e[e.length - 1][n] >= e[0][n] * o.ratio;
|
|
2011
|
+
};
|
|
2012
|
+
// Any series climbing on every visit to the ratio — in the totals and in
|
|
2013
|
+
// what outlived each navigation — is growth; which ones did says what
|
|
2014
|
+
// leaked — computations with owners flat is an ownerless effect per visit,
|
|
2015
|
+
// edges alone is a subscription per visit to something long-lived, owners
|
|
2016
|
+
// is an undisposed root.
|
|
2017
|
+
const l = GRAPH_SERIES.filter(e => climbs(f, e) && climbs(u, e));
|
|
2018
|
+
if (l.length === 0) return;
|
|
1918
2019
|
// The count is the whole graph's, so a leak shows at every route's settle;
|
|
1919
2020
|
// the first route to complete its climb reports, naming the others seen.
|
|
1920
|
-
const
|
|
1921
|
-
const
|
|
1922
|
-
const
|
|
1923
|
-
const
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
roots:
|
|
1931
|
-
routes:
|
|
2021
|
+
const h = [ ...routeCounts.keys() ];
|
|
2022
|
+
const join = (e, n) => e.map(e => e[n]).join(" → ");
|
|
2023
|
+
const p = l.map(e => `${e} ${join(u, e)} (of ${join(f, e)})`).join("; ");
|
|
2024
|
+
const m = l.includes("owners") ? "a createRoot() in an effect or handler with no dispose, or a Portal or panel mounted per visit" : l.includes("computations") ? "an effect or memo created with no owner (a module-level or callback createEffect) that only its sources keep alive" : "a subscription per visit to something long-lived — a global store or signal read by a computation that outlives the visit";
|
|
2025
|
+
const g = `[GRAPH_GROWTH] the live graph grew on ${o.visits} consecutive visits to ${r}, and so did ` + `the part that outlived each navigation there: ${p} ` + `(${s.roots} roots)${h.length > 1 ? `, across visits to ${h.join(", ")}` : ""}. Each ` + `visit left something behind that the next did not reclaim — the shape says ${m}. Dispose ` + `what a visit creates (onCleanup, or return the disposer from onSettled) and own it under the ` + `route's component so leaving the route tears it down.`;
|
|
2026
|
+
const w = {
|
|
2027
|
+
route: r,
|
|
2028
|
+
grew: l,
|
|
2029
|
+
history: f,
|
|
2030
|
+
survived: u,
|
|
2031
|
+
roots: s.roots,
|
|
2032
|
+
routes: h
|
|
1932
2033
|
};
|
|
1933
|
-
if (
|
|
1934
|
-
type:
|
|
1935
|
-
target:
|
|
2034
|
+
if (n.interaction !== undefined) w.interaction = {
|
|
2035
|
+
type: n.interaction.name,
|
|
2036
|
+
target: n.interaction.target
|
|
1936
2037
|
};
|
|
1937
2038
|
reportDiagnostic(emitDiagnostic({
|
|
1938
2039
|
code: "GRAPH_GROWTH",
|
|
1939
2040
|
kind: "perf",
|
|
1940
2041
|
severity: "warn",
|
|
1941
|
-
message:
|
|
1942
|
-
data:
|
|
2042
|
+
message: g,
|
|
2043
|
+
data: w
|
|
1943
2044
|
}, null));
|
|
1944
2045
|
// Judged once for the graph, not once per route: the next verdict needs
|
|
1945
2046
|
// another `visits` climbing settles.
|
|
@@ -1953,10 +2054,13 @@ function trackGraph(e) {
|
|
|
1953
2054
|
* One source abandoning flight after flight inside a window: every input
|
|
1954
2055
|
* asked again and the earlier answers were thrown away — the
|
|
1955
2056
|
* request-per-keystroke signature `feedback().flights[].abandoned` counts,
|
|
1956
|
-
* as a finding while it happens. Warned once per window per source.
|
|
2057
|
+
* as a finding while it happens. Warned once per window per source. A
|
|
2058
|
+
* framework node superseding its own flights by design (CONFIG_SUPERSEDES)
|
|
2059
|
+
* is not judged; its flights are still recorded.
|
|
1957
2060
|
*/ function checkAbandonedFlights(e, n) {
|
|
1958
2061
|
const t = options.abandonedFlights;
|
|
1959
|
-
|
|
2062
|
+
// Only dev sets CONFIG_SUPERSEDES (see constants.ts).
|
|
2063
|
+
if (t === false || false || excludedNode(e)) return;
|
|
1960
2064
|
const o = Date.now();
|
|
1961
2065
|
let i = abandonWindows.get(e);
|
|
1962
2066
|
if (i === undefined || o - i.start > t.windowMs) {
|
|
@@ -2053,6 +2157,14 @@ let navigationLog = [];
|
|
|
2053
2157
|
|
|
2054
2158
|
/** Drains completed since enable() — the clock `writeDrain` reads. */ let drainSeq = 0;
|
|
2055
2159
|
|
|
2160
|
+
/**
|
|
2161
|
+
* Navigations settled in a landing, waiting for their graph count: the hook
|
|
2162
|
+
* fires before the landing's commits dispose the frame parked for the page
|
|
2163
|
+
* left, which the walk would still reach through the sources both pages read
|
|
2164
|
+
* (#3989). `land()` runs only inside `flush()`'s drain, so `flushEnd` counts
|
|
2165
|
+
* them, after those commits — the instant a one-drain navigation is counted.
|
|
2166
|
+
*/ const landedGraphs = [];
|
|
2167
|
+
|
|
2056
2168
|
/**
|
|
2057
2169
|
* Copy what the router currently says onto the frame (what writes stamped —
|
|
2058
2170
|
* `formatOrigin` reads it) and the event. Called when the frame opens, when a
|
|
@@ -2091,6 +2203,8 @@ function openNavigation(e, n) {
|
|
|
2091
2203
|
held: false,
|
|
2092
2204
|
writeDrain: drainSeq
|
|
2093
2205
|
};
|
|
2206
|
+
// The initial declaration is never compared (see `trackGraph`).
|
|
2207
|
+
if (options.graphGrowth !== false && n.initial !== true) o.before = snapshotGraph();
|
|
2094
2208
|
syncNavigation(o);
|
|
2095
2209
|
navStates.set(e, o);
|
|
2096
2210
|
openNavs.add(o);
|
|
@@ -2151,8 +2265,8 @@ function closeNavigation(e) {
|
|
|
2151
2265
|
|
|
2152
2266
|
/** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */ function markNavigationsHeld(e) {
|
|
2153
2267
|
if (openNavs.size === 0) return;
|
|
2154
|
-
for (const n of e.
|
|
2155
|
-
const e = n.
|
|
2268
|
+
for (const n of e.Je) {
|
|
2269
|
+
const e = n.we?.origin;
|
|
2156
2270
|
if (e?.kind !== "navigation") continue;
|
|
2157
2271
|
const t = navStates.get(e);
|
|
2158
2272
|
if (t !== undefined) t.held = true;
|
|
@@ -2161,14 +2275,14 @@ function closeNavigation(e) {
|
|
|
2161
2275
|
|
|
2162
2276
|
/** transitionSettled: `t` commits — the navigations whose writes it staged are done. */ function settleNavigations(e, n) {
|
|
2163
2277
|
if (openNavs.size === 0) return;
|
|
2164
|
-
for (const t of e.
|
|
2165
|
-
const e = t.
|
|
2278
|
+
for (const t of e.Je) {
|
|
2279
|
+
const e = t.we?.origin;
|
|
2166
2280
|
if (e?.kind !== "navigation") continue;
|
|
2167
2281
|
const o = navStates.get(e);
|
|
2168
2282
|
if (o === undefined || !openNavs.has(o)) continue;
|
|
2169
2283
|
// A navigation whose frame is still open when its transition commits
|
|
2170
2284
|
// (`until()` inside the frame) settles here too: its writes are through.
|
|
2171
|
-
settleNavigation(o, o.held ? "held" : "committed", n);
|
|
2285
|
+
settleNavigation(o, o.held ? "held" : "committed", n, true);
|
|
2172
2286
|
}
|
|
2173
2287
|
}
|
|
2174
2288
|
|
|
@@ -2295,37 +2409,42 @@ function trackFallback(e, n, t, o) {
|
|
|
2295
2409
|
drainSeq++;
|
|
2296
2410
|
// The swaps this drain committed have rendered.
|
|
2297
2411
|
displayFallbacks();
|
|
2412
|
+
// Landed first: they settled before anything this drain settles below.
|
|
2413
|
+
for (let e = 0; e < landedGraphs.length; e++) trackGraph(landedGraphs[e]);
|
|
2414
|
+
landedGraphs.length = 0;
|
|
2298
2415
|
for (const e of openNavs) if (e.open === 0 && !e.held) settleNavigation(e, "committed");
|
|
2299
2416
|
for (const e of openInteractions) maybeSettleInteraction(e);
|
|
2300
2417
|
}
|
|
2301
2418
|
|
|
2302
|
-
function settleNavigation(e, n, t) {
|
|
2419
|
+
function settleNavigation(e, n, t, o) {
|
|
2303
2420
|
if (!openNavs.has(e)) return;
|
|
2304
2421
|
openNavs.delete(e);
|
|
2305
2422
|
syncNavigation(e);
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
if (t !== undefined)
|
|
2423
|
+
const i = e.event;
|
|
2424
|
+
i.settledMs = now() - i.at;
|
|
2425
|
+
i.outcome = n;
|
|
2426
|
+
if (t !== undefined) i.hold = t;
|
|
2310
2427
|
// The initial declaration is not a navigation the person waited on: its
|
|
2311
2428
|
// `settledMs` is document start → router built, which would read as the
|
|
2312
2429
|
// route's responsiveness in the feedback tables. Consumers get the record.
|
|
2313
|
-
if (
|
|
2314
|
-
records.emit("navigation",
|
|
2315
|
-
|
|
2430
|
+
if (i.initial !== true) for (const e of folds) e.navigation?.(i);
|
|
2431
|
+
records.emit("navigation", i, undefined);
|
|
2432
|
+
if (o) landedGraphs.push(e); else trackGraph(e);
|
|
2316
2433
|
// The interaction that performed it may have been waiting only on this.
|
|
2317
|
-
const
|
|
2318
|
-
if (
|
|
2434
|
+
const s = openInteractionOf(i.interaction);
|
|
2435
|
+
if (s !== undefined) maybeSettleInteraction(s);
|
|
2319
2436
|
}
|
|
2320
2437
|
|
|
2321
2438
|
const interactionStates = new WeakMap;
|
|
2322
2439
|
|
|
2440
|
+
/** `InteractionRef.event` → the record its frames join (while `joinable`). Weak: the Event is never held. */ const eventInteractions = new WeakMap;
|
|
2441
|
+
|
|
2323
2442
|
/** Opened, not yet settled. */ const openInteractions = new Set;
|
|
2324
2443
|
|
|
2325
2444
|
let interactionLog = [];
|
|
2326
2445
|
|
|
2327
|
-
function openInteraction(e, n) {
|
|
2328
|
-
const
|
|
2446
|
+
function openInteraction(e, n, t) {
|
|
2447
|
+
const o = {
|
|
2329
2448
|
name: e.name,
|
|
2330
2449
|
at: e.at,
|
|
2331
2450
|
handlerMs: 0,
|
|
@@ -2337,26 +2456,37 @@ function openInteraction(e, n) {
|
|
|
2337
2456
|
navigations: [],
|
|
2338
2457
|
origin: e
|
|
2339
2458
|
};
|
|
2340
|
-
if (e.target !== undefined)
|
|
2459
|
+
if (e.target !== undefined) o.target = e.target;
|
|
2341
2460
|
// The runtime may date the frame from the event's own timestamp (before
|
|
2342
2461
|
// any queued task ran); the gap to here is the input delay the browser's
|
|
2343
2462
|
// INP counts first.
|
|
2344
|
-
if (n >
|
|
2345
|
-
const
|
|
2346
|
-
event:
|
|
2463
|
+
if (n > o.at) o.inputDelayMs = n - o.at;
|
|
2464
|
+
const i = {
|
|
2465
|
+
event: o,
|
|
2347
2466
|
open: true,
|
|
2348
2467
|
opened: n,
|
|
2349
2468
|
writeDrain: drainSeq,
|
|
2350
2469
|
heldIn: new Set,
|
|
2351
2470
|
held: false,
|
|
2352
2471
|
excludedWrites: 0,
|
|
2353
|
-
awaiting:
|
|
2354
|
-
actioned: false
|
|
2472
|
+
awaiting: 0,
|
|
2473
|
+
actioned: false,
|
|
2474
|
+
joinable: t !== undefined
|
|
2355
2475
|
};
|
|
2356
|
-
interactionStates.set(e,
|
|
2357
|
-
openInteractions.add(
|
|
2358
|
-
interactionLog.push(
|
|
2476
|
+
interactionStates.set(e, i);
|
|
2477
|
+
openInteractions.add(i);
|
|
2478
|
+
interactionLog.push(o);
|
|
2359
2479
|
if (interactionLog.length > options.historyLimit) interactionLog.shift();
|
|
2480
|
+
if (t !== undefined) {
|
|
2481
|
+
eventInteractions.set(t, i);
|
|
2482
|
+
// A microtask is too early: native dispatch runs microtasks between
|
|
2483
|
+
// listeners. The callback must not close over `key`: a throttled timer
|
|
2484
|
+
// would hold the Event strongly long past its dispatch.
|
|
2485
|
+
setTimeout(() => {
|
|
2486
|
+
i.joinable = false;
|
|
2487
|
+
if (openInteractions.has(i)) maybeSettleInteraction(i, i.settleAt);
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2360
2490
|
}
|
|
2361
2491
|
|
|
2362
2492
|
/**
|
|
@@ -2377,19 +2507,19 @@ function openInteraction(e, n) {
|
|
|
2377
2507
|
// screen could have shown the wait.
|
|
2378
2508
|
const i = n !== null && (typeof n === "object" || typeof n === "function") && typeof n.then === "function" ? n : null;
|
|
2379
2509
|
if (i !== null) {
|
|
2380
|
-
t.awaiting
|
|
2510
|
+
t.awaiting++;
|
|
2381
2511
|
let e = false;
|
|
2382
2512
|
let n;
|
|
2383
2513
|
const settle = () => {
|
|
2384
2514
|
if (e) return;
|
|
2385
2515
|
e = true;
|
|
2386
2516
|
if (n !== undefined) clearTimeout(n);
|
|
2387
|
-
if (!openInteractions.has(t)) return;
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2517
|
+
if (!openInteractions.has(t) || --t.awaiting > 0) return;
|
|
2518
|
+
const o = now();
|
|
2519
|
+
// From the last handler's return: the frames of one event share a record.
|
|
2520
|
+
t.event.continuationMs = o - (t.opened + t.event.handlerMs);
|
|
2391
2521
|
checkUntrackedAsyncHandler(t);
|
|
2392
|
-
maybeSettleInteraction(t,
|
|
2522
|
+
maybeSettleInteraction(t, o);
|
|
2393
2523
|
};
|
|
2394
2524
|
n = setTimeout(settle, ASYNC_HANDLER_CAP_MS);
|
|
2395
2525
|
// A pending cap must not hold a Node process (tests, SSR harnesses) open.
|
|
@@ -2467,8 +2597,8 @@ function openInteraction(e, n) {
|
|
|
2467
2597
|
|
|
2468
2598
|
/** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */ function markInteractionsHeld(e) {
|
|
2469
2599
|
if (openInteractions.size === 0) return;
|
|
2470
|
-
for (const n of e.
|
|
2471
|
-
const t = openInteractionOf(n.
|
|
2600
|
+
for (const n of e.Je) {
|
|
2601
|
+
const t = openInteractionOf(n.we?.origin);
|
|
2472
2602
|
if (t !== undefined) {
|
|
2473
2603
|
t.heldIn.add(e);
|
|
2474
2604
|
t.held = true;
|
|
@@ -2491,12 +2621,16 @@ function openInteraction(e, n) {
|
|
|
2491
2621
|
|
|
2492
2622
|
function maybeSettleInteraction(e, n = now()) {
|
|
2493
2623
|
const t = e.event;
|
|
2494
|
-
if (e.open || e.awaiting || e.heldIn.size > 0) return;
|
|
2624
|
+
if (e.open || e.awaiting > 0 || e.heldIn.size > 0) return;
|
|
2495
2625
|
// A drain must have committed the last write (the handler's, or a redirect
|
|
2496
2626
|
// hop's after the click's own drain) — the handler returning is not the
|
|
2497
2627
|
// screen having it.
|
|
2498
2628
|
if (t.writes > 0 && drainSeq <= e.writeDrain) return;
|
|
2499
2629
|
for (const e of t.navigations) if (e.outcome === undefined) return;
|
|
2630
|
+
if (e.joinable) {
|
|
2631
|
+
e.settleAt = n;
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2500
2634
|
openInteractions.delete(e);
|
|
2501
2635
|
// Every write went to an excluded subject and nothing of the app's ran: the
|
|
2502
2636
|
// click was on the observer's own UI (a devtools panel's button). Not a
|
|
@@ -2551,7 +2685,7 @@ const engineHooks = {
|
|
|
2551
2685
|
prevDeps: n || !wantsRerun() ? null : captureDeps(e),
|
|
2552
2686
|
// Mirror recompute's own prev-value resolution: an earlier run in the
|
|
2553
2687
|
// same flush may still be holding in _pendingValue.
|
|
2554
|
-
prevValue: e.
|
|
2688
|
+
prevValue: e.Oe !== NOT_PENDING ? e.Oe : e.Ae,
|
|
2555
2689
|
// A create run inherits the interaction of whatever is building it: the
|
|
2556
2690
|
// enclosing recompute (a parent's fn creating children) or, at the top
|
|
2557
2691
|
// of the recompute stack, the effect callback / handler frame.
|
|
@@ -2577,8 +2711,8 @@ const engineHooks = {
|
|
|
2577
2711
|
// committed compute output is an unchanged run. `undefined` outputs are
|
|
2578
2712
|
// exempt — a side-effect-only compute's work IS its effect phase, and
|
|
2579
2713
|
// identity of `undefined` proves nothing.
|
|
2580
|
-
if (t && r.causes !== null && e.
|
|
2581
|
-
const n = e.
|
|
2714
|
+
if (t && r.causes !== null && e.u) {
|
|
2715
|
+
const n = e.Oe !== NOT_PENDING ? e.Oe : e.Ae;
|
|
2582
2716
|
if (n !== undefined && n === r.prevValue) t = false;
|
|
2583
2717
|
}
|
|
2584
2718
|
// Unstable-output check: memos only, non-create, plain runs with a
|
|
@@ -2586,7 +2720,12 @@ const engineHooks = {
|
|
|
2586
2720
|
// plain-flush memo commits and in `_value` for direct ones. Overlay runs
|
|
2587
2721
|
// are excluded — an optimistic re-derive legitimately produces fresh
|
|
2588
2722
|
// equivalents while the lane settles.
|
|
2589
|
-
if (r.causes !== null && t && !o && !i && !e.
|
|
2723
|
+
if (r.causes !== null && t && !o && !i && !e.u) checkUnstableOutput(e, r.prevValue, e.Oe !== NOT_PENDING ? e.Oe : e.Ae);
|
|
2724
|
+
// The same exemption for memos: a run that committed `undefined` did its
|
|
2725
|
+
// work as a side effect (a projection's draft writes or reconcile, a
|
|
2726
|
+
// write from inside a memo), so core's `undefined === undefined` gate
|
|
2727
|
+
// proves nothing. Reported as changed, so no consumer counts it as waste.
|
|
2728
|
+
if (!t && (e.Oe !== NOT_PENDING ? e.Oe : e.Ae) === undefined) t = true;
|
|
2590
2729
|
if (r.causes !== null) recordRerun(e, r, {
|
|
2591
2730
|
selfMs: c,
|
|
2592
2731
|
totalMs: a
|
|
@@ -2598,7 +2737,7 @@ const engineHooks = {
|
|
|
2598
2737
|
noteInteractionRun(r.interaction, c, true);
|
|
2599
2738
|
// The first effect callback runs with no re-run record to inherit from;
|
|
2600
2739
|
// hand it the interaction that built the node (see effectRunStart).
|
|
2601
|
-
e.
|
|
2740
|
+
e.qe = r.interaction;
|
|
2602
2741
|
if (openFlush !== null) noteFlushRun(openFlush, true, r.interaction);
|
|
2603
2742
|
if (records.observed("create")) {
|
|
2604
2743
|
const n = {
|
|
@@ -2628,9 +2767,9 @@ const engineHooks = {
|
|
|
2628
2767
|
trackFlightStart(e, n);
|
|
2629
2768
|
},
|
|
2630
2769
|
asyncStart(e) {
|
|
2631
|
-
asyncStartSeq = e.
|
|
2632
|
-
asyncStartTime = e.
|
|
2633
|
-
asyncStartValue = e.
|
|
2770
|
+
asyncStartSeq = e.we?.seq ?? 0;
|
|
2771
|
+
asyncStartTime = e.Ie;
|
|
2772
|
+
asyncStartValue = e.Ae;
|
|
2634
2773
|
},
|
|
2635
2774
|
asyncEnd(e, n, t, o) {
|
|
2636
2775
|
if (o) {
|
|
@@ -2640,7 +2779,7 @@ const engineHooks = {
|
|
|
2640
2779
|
// (or `_time`, for a same-reference commit under `equals: false`), and
|
|
2641
2780
|
// a transition hold parks the value in `_pendingValue`. A landing the
|
|
2642
2781
|
// equality gate swallowed moves none of them and must leave no stamp.
|
|
2643
|
-
const o = e.
|
|
2782
|
+
const o = e.Ae !== asyncStartValue || e.Ie !== asyncStartTime || e.Oe === t;
|
|
2644
2783
|
if (o) stampWrite(e, "async", n === undefined ? NO_VALUES : n, t);
|
|
2645
2784
|
// Flight over either way — an equality-swallowed landing still spent
|
|
2646
2785
|
// the wall time (finalizeFlight only chains through a fresh stamp).
|
|
@@ -2650,12 +2789,12 @@ const engineHooks = {
|
|
|
2650
2789
|
// Landed through setSignal: reclassify its "write" stamp as an async
|
|
2651
2790
|
// landing — but only if it actually stamped (the value changed) since
|
|
2652
2791
|
// asyncStart; a no-change landing must leave no fresh stamp behind.
|
|
2653
|
-
const i = e.
|
|
2792
|
+
const i = e.we;
|
|
2654
2793
|
if (i !== undefined && i.seq > asyncStartSeq && i.kind === "write") stampWrite(e, "async", NO_VALUES, t);
|
|
2655
2794
|
finalizeFlight(e);
|
|
2656
2795
|
},
|
|
2657
2796
|
effectRunStart(e) {
|
|
2658
|
-
pushFrame("effect", nodeName(e), e.
|
|
2797
|
+
pushFrame("effect", nodeName(e), e.qe, e);
|
|
2659
2798
|
// The frame's own `at` doubles as the record's start: set only while
|
|
2660
2799
|
// listened, so a listener arriving mid-callback finds no start and the
|
|
2661
2800
|
// end emits nothing for it — and an unlistened callback pays no clock read.
|
|
@@ -2759,8 +2898,10 @@ const holds = [];
|
|
|
2759
2898
|
stagedFallbacks.delete(DRAIN);
|
|
2760
2899
|
trackingGen++;
|
|
2761
2900
|
openNavs.clear();
|
|
2901
|
+
landedGraphs.length = 0;
|
|
2762
2902
|
openInteractions.clear();
|
|
2763
2903
|
routeCounts.clear();
|
|
2904
|
+
visitRoute = undefined;
|
|
2764
2905
|
drainSeq = 0;
|
|
2765
2906
|
originFrames.length = 0;
|
|
2766
2907
|
effectStack.length = 0;
|