@solidjs/signals 2.0.0-rc.14 → 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 +248 -189
- package/dist/dev.attribution.js +124 -41
- package/dist/dev.js +553 -322
- package/dist/observe/affects.js +34 -31
- package/dist/observe/boundaries.js +166 -132
- package/dist/observe/core/async.js +90 -83
- package/dist/observe/core/attribution-queries.js +1 -1
- package/dist/observe/core/attribution.js +281 -197
- package/dist/observe/core/constants.js +7 -2
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +277 -245
- package/dist/observe/core/dev.js +3 -3
- package/dist/observe/core/effect.js +21 -21
- package/dist/observe/core/external.js +2 -2
- package/dist/observe/core/graph.js +25 -25
- package/dist/observe/core/heap.js +9 -9
- package/dist/observe/core/lanes.js +206 -160
- package/dist/observe/core/owner.js +27 -27
- package/dist/observe/core/scheduler.js +92 -86
- package/dist/observe/core/verdict.js +66 -54
- package/dist/observe/map.js +35 -35
- package/dist/observe/reveal.js +24 -24
- package/dist/observe/signals.js +38 -28
- package/dist/observe/store/affects.js +75 -68
- package/dist/observe/store/hooks.js +13 -0
- package/dist/observe/store/index.js +0 -2
- package/dist/observe/store/optimistic.js +67 -34
- package/dist/observe/store/projection.js +37 -31
- package/dist/observe/store/reconcile.js +32 -30
- package/dist/observe/store/store.js +128 -85
- package/dist/prod/affects.js +34 -31
- package/dist/prod/boundaries.js +143 -109
- package/dist/prod/core/async.js +64 -67
- package/dist/prod/core/constants.js +7 -2
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +302 -270
- package/dist/prod/core/effect.js +38 -38
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +27 -27
- package/dist/prod/core/heap.js +30 -30
- package/dist/prod/core/lanes.js +257 -211
- package/dist/prod/core/owner.js +39 -39
- package/dist/prod/core/scheduler.js +125 -119
- package/dist/prod/core/verdict.js +70 -58
- package/dist/prod/map.js +73 -73
- package/dist/prod/reveal.js +38 -38
- package/dist/prod/signals.js +30 -20
- package/dist/prod/store/affects.js +75 -68
- package/dist/prod/store/hooks.js +13 -0
- package/dist/prod/store/index.js +0 -2
- package/dist/prod/store/optimistic.js +114 -81
- package/dist/prod/store/projection.js +30 -24
- package/dist/prod/store/reconcile.js +32 -30
- package/dist/prod/store/store.js +136 -93
- package/dist/types/boundaries.d.ts +4 -0
- package/dist/types/core/attribution.d.ts +6 -2
- package/dist/types/core/constants.d.ts +5 -0
- package/dist/types/core/core.d.ts +33 -3
- package/dist/types/core/lanes.d.ts +10 -0
- package/dist/types/core/scheduler.d.ts +12 -6
- package/dist/types/core/types.d.ts +0 -6
- package/dist/types/store/affects.d.ts +11 -0
- package/dist/types/store/hooks.d.ts +48 -0
- package/dist/types/store/index.d.ts +0 -1
- package/dist/types/store/store.d.ts +3 -38
- package/package.json +1 -1
|
@@ -17,11 +17,11 @@ import { blockersOf, GlobalQueue } from "./scheduler.js";
|
|
|
17
17
|
* observer exclusion is cached per node once any exists, before that a flag
|
|
18
18
|
* read.
|
|
19
19
|
*/ function excludedNode(e) {
|
|
20
|
-
if ((e.
|
|
20
|
+
if ((e.i & CONFIG_PLUMBING) !== 0) return true;
|
|
21
21
|
if (!anyExcluded()) return false;
|
|
22
22
|
const n = e;
|
|
23
|
-
if (n.
|
|
24
|
-
return n.
|
|
23
|
+
if (n.he === undefined) n.he = isExcluded(e);
|
|
24
|
+
return n.he;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
let attributionActive = false;
|
|
@@ -121,7 +121,7 @@ function nodeName(e) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** The record vocabulary for a computation's kind: effects carry a `_type`, memos do not. */ function nodeKind(e) {
|
|
124
|
-
return e.
|
|
124
|
+
return e.u ? "effect" : "memo";
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
@@ -327,7 +327,7 @@ const effectFrames = new WeakMap;
|
|
|
327
327
|
const n = effectStack[effectStack.length - 1];
|
|
328
328
|
if (n !== undefined) effectFrames.set(e, {
|
|
329
329
|
node: n,
|
|
330
|
-
causes: n.
|
|
330
|
+
causes: n.me
|
|
331
331
|
});
|
|
332
332
|
}
|
|
333
333
|
|
|
@@ -345,7 +345,7 @@ function pushFrame(e, n, t, o) {
|
|
|
345
345
|
if (s !== undefined) i.interaction = s;
|
|
346
346
|
if (o !== undefined) {
|
|
347
347
|
const e = o;
|
|
348
|
-
if (e.
|
|
348
|
+
if (e.ge !== undefined) i.run = e.ge;
|
|
349
349
|
// The frame → node map is filled by the first write inside the callback
|
|
350
350
|
// (noteEffectOrigin), not here: most callbacks never write.
|
|
351
351
|
effectStack.push(o);
|
|
@@ -458,9 +458,9 @@ function originEnd() {
|
|
|
458
458
|
if (typeof t !== "number") return;
|
|
459
459
|
let o = 0;
|
|
460
460
|
let i = 0;
|
|
461
|
-
for (let n = e.t; n !== null; n = n.
|
|
461
|
+
for (let n = e.t; n !== null; n = n.o) {
|
|
462
462
|
o++;
|
|
463
|
-
if (!(n.
|
|
463
|
+
if (!(n.l.i & CONFIG_PLUMBING)) i++;
|
|
464
464
|
}
|
|
465
465
|
if (i < t || o >= GRAPH_SIZE_WARN_AT) return;
|
|
466
466
|
noteFanOut(e, i, n);
|
|
@@ -483,8 +483,8 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
483
483
|
i.origin = n === "async" ? asyncOrigin(e) : currentOrigin();
|
|
484
484
|
i.at = now();
|
|
485
485
|
i.stack = captureStack();
|
|
486
|
-
const s = e.
|
|
487
|
-
e.
|
|
486
|
+
const s = e.we;
|
|
487
|
+
e.we = i;
|
|
488
488
|
noteNavigationWrite(s, i);
|
|
489
489
|
noteInteractionWrite(i.origin, excludedNode(e));
|
|
490
490
|
if (n === "write") trackEffectWrite(e, i, o);
|
|
@@ -495,7 +495,7 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
/** Record a derived change (memo produced a new value) with its causes. */ function stampDerived(e, n) {
|
|
498
|
-
e.
|
|
498
|
+
e.we = {
|
|
499
499
|
seq: ++changeSeq,
|
|
500
500
|
kind: "derived",
|
|
501
501
|
name: nodeName(e),
|
|
@@ -510,19 +510,19 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
510
510
|
* run's links. A refresh() stamp on the node itself also counts — that is a
|
|
511
511
|
* self-invalidation, not a dep change.
|
|
512
512
|
*/ function collectCauses(e) {
|
|
513
|
-
const n = e.
|
|
513
|
+
const n = e.ve ?? 0;
|
|
514
514
|
const t = [];
|
|
515
|
-
const o = e.
|
|
515
|
+
const o = e.we;
|
|
516
516
|
if (o !== undefined && o.seq > n && o.kind === "refresh") t.push(o);
|
|
517
|
-
for (let o = e.
|
|
518
|
-
const e = o.I.
|
|
517
|
+
for (let o = e.G; o !== null; o = o._) {
|
|
518
|
+
const e = o.I.we;
|
|
519
519
|
if (e !== undefined && e.seq > n) t.push(e);
|
|
520
520
|
}
|
|
521
521
|
return t;
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
/** Advance the node's seen-cursor to the present. Call after every run. */ function markSeen(e) {
|
|
525
|
-
e.
|
|
525
|
+
e.ve = changeSeq;
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
/**
|
|
@@ -534,9 +534,9 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
534
534
|
* still owed) and not part of the subscription diff.
|
|
535
535
|
*/ function captureDeps(e) {
|
|
536
536
|
const n = [];
|
|
537
|
-
for (let t = e.
|
|
537
|
+
for (let t = e.G; t !== null; t = t._) {
|
|
538
538
|
n.push(t.I);
|
|
539
|
-
if (t === e.
|
|
539
|
+
if (t === e.p) break;
|
|
540
540
|
}
|
|
541
541
|
return n;
|
|
542
542
|
}
|
|
@@ -555,18 +555,18 @@ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
|
|
|
555
555
|
if (n === false || false) return;
|
|
556
556
|
let t = 0;
|
|
557
557
|
const o = [];
|
|
558
|
-
for (let n = e.
|
|
558
|
+
for (let n = e.G; n !== null; n = n._) {
|
|
559
559
|
// HMR plumbing is a dev-only hop the scope cannot read past: not a source.
|
|
560
|
-
if (!(n.I.
|
|
560
|
+
if (!(n.I.i & CONFIG_PLUMBING)) {
|
|
561
561
|
t++;
|
|
562
562
|
if (o.length < 12) o.push(nodeName(n.I));
|
|
563
563
|
}
|
|
564
|
-
if (n === e.
|
|
564
|
+
if (n === e.p) break;
|
|
565
565
|
// the validated prefix, as captureDeps
|
|
566
566
|
}
|
|
567
567
|
const i = e;
|
|
568
|
-
if (t < n || t < (i.
|
|
569
|
-
i.
|
|
568
|
+
if (t < n || t < (i.ke ?? 0) * 1.5) return;
|
|
569
|
+
i.ke = t;
|
|
570
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 ? ", …" : ""}`;
|
|
571
571
|
reportDiagnostic(emitDiagnostic({
|
|
572
572
|
code: "WIDE_SCOPE_DEPS",
|
|
@@ -595,14 +595,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
595
595
|
if (t === false) return;
|
|
596
596
|
const o = e;
|
|
597
597
|
const i = Date.now();
|
|
598
|
-
if (o
|
|
599
|
-
o
|
|
600
|
-
o
|
|
601
|
-
o.
|
|
602
|
-
}
|
|
603
|
-
o
|
|
604
|
-
if (o.
|
|
605
|
-
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;
|
|
606
606
|
// Root-cause key: the set of originating writes behind this scope's latest
|
|
607
607
|
// re-run. Scopes hot from the SAME roots share one aggregation window.
|
|
608
608
|
const s = new Set;
|
|
@@ -619,10 +619,10 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
619
619
|
hotCauses.set(r, a);
|
|
620
620
|
}
|
|
621
621
|
a.scopes++;
|
|
622
|
-
a.runs += o
|
|
622
|
+
a.runs += o.$e;
|
|
623
623
|
if (a.scopes === 1) {
|
|
624
624
|
const s = n.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
625
|
-
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)"}`;
|
|
626
626
|
reportDiagnostic(emitDiagnostic({
|
|
627
627
|
code: "HOT_SCOPE_RERUNS",
|
|
628
628
|
kind: "perf",
|
|
@@ -630,7 +630,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
630
630
|
message: r,
|
|
631
631
|
nodeName: nodeName(e),
|
|
632
632
|
data: {
|
|
633
|
-
runs: o
|
|
633
|
+
runs: o.$e,
|
|
634
634
|
windowMs: t.windowMs,
|
|
635
635
|
causes: n.map(e => e.name)
|
|
636
636
|
}
|
|
@@ -668,16 +668,16 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
668
668
|
if (o === false) return;
|
|
669
669
|
const i = e;
|
|
670
670
|
const s = now();
|
|
671
|
-
if (i.
|
|
672
|
-
i.
|
|
673
|
-
i.
|
|
674
|
-
i.
|
|
675
|
-
}
|
|
676
|
-
i.
|
|
677
|
-
if (i.
|
|
678
|
-
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;
|
|
679
679
|
const r = t.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
680
|
-
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)"}`;
|
|
681
681
|
reportDiagnostic(emitDiagnostic({
|
|
682
682
|
code: "HOT_SCOPE_TIME",
|
|
683
683
|
kind: "perf",
|
|
@@ -685,7 +685,7 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
685
685
|
message: a,
|
|
686
686
|
nodeName: nodeName(e),
|
|
687
687
|
data: {
|
|
688
|
-
spentMs: i.
|
|
688
|
+
spentMs: i.Le,
|
|
689
689
|
budgetMs: o.budgetMs,
|
|
690
690
|
windowMs: o.windowMs,
|
|
691
691
|
causes: t.map(e => e.name)
|
|
@@ -710,24 +710,24 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
710
710
|
// This runs on every re-run: fields on the node (one property read each,
|
|
711
711
|
// like hotRuns) and the run's own `at` — no clock read, no map lookup.
|
|
712
712
|
const a = e;
|
|
713
|
-
if (a.
|
|
714
|
-
a.
|
|
713
|
+
if (a.Me === undefined || n - a.Me > r.windowMs) {
|
|
714
|
+
a.Me = n;
|
|
715
|
+
a.He = 0;
|
|
715
716
|
a.We = 0;
|
|
716
717
|
a.Ge = 0;
|
|
717
|
-
a.
|
|
718
|
-
a.Be = false;
|
|
718
|
+
a.Ue = false;
|
|
719
719
|
}
|
|
720
|
-
a.
|
|
720
|
+
a.He = a.He + 1;
|
|
721
721
|
if (!o) {
|
|
722
|
-
a.
|
|
723
|
-
a.
|
|
722
|
+
a.We = a.We + 1;
|
|
723
|
+
a.Ge = a.Ge + i;
|
|
724
724
|
}
|
|
725
|
-
if (a.
|
|
726
|
-
a.
|
|
725
|
+
if (a.Ue || a.He < r.minRuns || a.We / a.He < r.ratio || a.Ge < r.budgetMs) return;
|
|
726
|
+
a.Ue = true;
|
|
727
727
|
const c = {
|
|
728
|
-
runs: a.
|
|
729
|
-
wasted: a.
|
|
730
|
-
wastedMs: a.
|
|
728
|
+
runs: a.He,
|
|
729
|
+
wasted: a.We,
|
|
730
|
+
wastedMs: a.Ge
|
|
731
731
|
};
|
|
732
732
|
const d = s.map(e => `"${e.name}" (${e.kind})`).join(", ");
|
|
733
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)"}`;
|
|
@@ -750,14 +750,14 @@ const HOT_FANOUT_FIRST_MILESTONE = 5;
|
|
|
750
750
|
function recordRerun(e, n, t, o, i, s) {
|
|
751
751
|
const r = n.causes;
|
|
752
752
|
const a = e;
|
|
753
|
-
const c = a.
|
|
753
|
+
const c = a.me;
|
|
754
754
|
const d = n.interaction;
|
|
755
755
|
if (excludedNode(e)) {
|
|
756
756
|
// The observer's own computation: keep the per-node bookkeeping its
|
|
757
757
|
// effect phase reads (see effectRunStart) and record nothing.
|
|
758
|
-
a.
|
|
759
|
-
a.
|
|
760
|
-
a.
|
|
758
|
+
a.qe = d;
|
|
759
|
+
a.ge = undefined;
|
|
760
|
+
a.me = r;
|
|
761
761
|
return;
|
|
762
762
|
}
|
|
763
763
|
// The facts every run leaves on the node, record or not: the run sequence
|
|
@@ -766,10 +766,10 @@ function recordRerun(e, n, t, o, i, s) {
|
|
|
766
766
|
// cause list of its own, so it takes this run's interaction and causes
|
|
767
767
|
// (see effectRunStart / pushFrame).
|
|
768
768
|
const f = ++runSeq;
|
|
769
|
-
const u = a.
|
|
770
|
-
a.
|
|
771
|
-
a.
|
|
772
|
-
a.
|
|
769
|
+
const u = a.Be = (a.Be ?? 0) + 1;
|
|
770
|
+
a.qe = d;
|
|
771
|
+
a.ge = f;
|
|
772
|
+
a.me = r;
|
|
773
773
|
noteInteractionRun(d, t.selfMs, false);
|
|
774
774
|
if (openFlush !== null) noteFlushRun(openFlush, false, d);
|
|
775
775
|
// The checks read the facts, not the record, so they run with or without
|
|
@@ -910,15 +910,15 @@ function shallowEquivalent(e, n) {
|
|
|
910
910
|
const i = e;
|
|
911
911
|
if (n === t || // paranoia: changed runs should never hit this
|
|
912
912
|
!isPlainShape(n) || !isPlainShape(t) || !shallowEquivalent(n, t)) {
|
|
913
|
-
i.
|
|
914
|
-
i.
|
|
913
|
+
i.xe = 0;
|
|
914
|
+
i.Ke = false;
|
|
915
915
|
return;
|
|
916
916
|
}
|
|
917
|
-
i.
|
|
918
|
-
if (i.
|
|
919
|
-
i.
|
|
917
|
+
i.xe = (i.xe ?? 0) + 1;
|
|
918
|
+
if (i.Ke || i.xe < o) return;
|
|
919
|
+
i.Ke = true;
|
|
920
920
|
const s = Array.isArray(t) ? "array" : "object";
|
|
921
|
-
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.`;
|
|
922
922
|
reportDiagnostic(emitDiagnostic({
|
|
923
923
|
code: "UNSTABLE_MEMO_OUTPUT",
|
|
924
924
|
kind: "perf",
|
|
@@ -926,7 +926,7 @@ function shallowEquivalent(e, n) {
|
|
|
926
926
|
message: r,
|
|
927
927
|
nodeName: nodeName(e),
|
|
928
928
|
data: {
|
|
929
|
-
runs: i.
|
|
929
|
+
runs: i.xe,
|
|
930
930
|
shape: s
|
|
931
931
|
}
|
|
932
932
|
}, e));
|
|
@@ -1065,17 +1065,17 @@ const copyReported = new WeakSet;
|
|
|
1065
1065
|
const s = i !== undefined && i.kind === "effect" ? effectFrames.get(i) : undefined;
|
|
1066
1066
|
const r = s === undefined ? 0 : devId(s.node);
|
|
1067
1067
|
recordNodes.set(n, e);
|
|
1068
|
-
o.
|
|
1069
|
-
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) {
|
|
1070
1070
|
copyWrites.add(n);
|
|
1071
|
-
o.
|
|
1072
|
-
o.
|
|
1073
|
-
if (o.
|
|
1074
|
-
} 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;
|
|
1075
1075
|
}
|
|
1076
1076
|
|
|
1077
1077
|
/** The effect's source whose current value the compute output is, if any (the prop-to-state port). */ function passthroughSource(e) {
|
|
1078
|
-
for (let n = e.
|
|
1078
|
+
for (let n = e.G; n !== null; n = n._) if (n.I.Ae === e.Ae) return nodeName(n.I);
|
|
1079
1079
|
return undefined;
|
|
1080
1080
|
}
|
|
1081
1081
|
|
|
@@ -1148,7 +1148,7 @@ function checkCopyEffect(e, n) {
|
|
|
1148
1148
|
c.count++;
|
|
1149
1149
|
const d = copyWrites.has(s);
|
|
1150
1150
|
const f = recordNodes.get(s);
|
|
1151
|
-
const u = f !== undefined && f.
|
|
1151
|
+
const u = f !== undefined && f.Ve === devId(i.node);
|
|
1152
1152
|
// Derivable outright: the value is the compute output and nothing else
|
|
1153
1153
|
// writes the signal. A copy INTO a signal that has other writers is the
|
|
1154
1154
|
// "reset editable state from a source" shape — the tear is real, but a memo
|
|
@@ -1164,7 +1164,7 @@ function checkCopyEffect(e, n) {
|
|
|
1164
1164
|
copyReported.add(f);
|
|
1165
1165
|
}
|
|
1166
1166
|
if (h === "warn") c.warned = true;
|
|
1167
|
-
const p = e.
|
|
1167
|
+
const p = e.u ? "effect" : "memo";
|
|
1168
1168
|
const m = nodeName(i.node);
|
|
1169
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.`;
|
|
1170
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}`;
|
|
@@ -1255,7 +1255,7 @@ const listIdentityWarned = new WeakSet;
|
|
|
1255
1255
|
function recordId(e) {
|
|
1256
1256
|
if (e === null || typeof e !== "object") return undefined;
|
|
1257
1257
|
const n = e;
|
|
1258
|
-
return n.id ?? n.key ?? n.
|
|
1258
|
+
return n.id ?? n.key ?? n.ze ?? undefined;
|
|
1259
1259
|
}
|
|
1260
1260
|
|
|
1261
1261
|
function checkListIdentity(e, n, t, o, i) {
|
|
@@ -1412,7 +1412,7 @@ function trackFlightStart(e, n) {
|
|
|
1412
1412
|
const o = t - n.origin;
|
|
1413
1413
|
for (const n of folds) n.flightLanded?.(e, o);
|
|
1414
1414
|
if (records.observed("flight")) emitFlight(e, n, t, "landed");
|
|
1415
|
-
const i = e.
|
|
1415
|
+
const i = e.we;
|
|
1416
1416
|
// Only a stamp this landing produced may carry the measurement — a stale
|
|
1417
1417
|
// async record from a previous landing must not be re-labeled.
|
|
1418
1418
|
if (i !== undefined && i.kind === "async" && i.seq > n.startSeq) landedFlights.set(i, {
|
|
@@ -1448,8 +1448,8 @@ function checkWaterfall(e, n, t) {
|
|
|
1448
1448
|
}
|
|
1449
1449
|
if (i < 2) return;
|
|
1450
1450
|
const r = e;
|
|
1451
|
-
if ((r.
|
|
1452
|
-
r.
|
|
1451
|
+
if ((r.Xe ?? 0) >= i) return;
|
|
1452
|
+
r.Xe = i;
|
|
1453
1453
|
const a = [ ...n.slice(n.length - (i - 1)), {
|
|
1454
1454
|
name: nodeName(e),
|
|
1455
1455
|
ms: t
|
|
@@ -1486,7 +1486,7 @@ let holdLog = [];
|
|
|
1486
1486
|
/** A lane's derivation (L2: a node carrying the lane's value that is not a
|
|
1487
1487
|
* written guess — lanes.ts) is a lane pass's result, not a write anyone
|
|
1488
1488
|
* made: not an acknowledgement. */ function isCompanion(e) {
|
|
1489
|
-
return (e.
|
|
1489
|
+
return (e.i & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
|
|
1490
1490
|
}
|
|
1491
1491
|
|
|
1492
1492
|
const HOLD_CENSUS_CAP = 1e4;
|
|
@@ -1512,7 +1512,7 @@ function acknowledge(e, n, t, o) {
|
|
|
1512
1512
|
const t = {
|
|
1513
1513
|
left: HOLD_CENSUS_CAP
|
|
1514
1514
|
};
|
|
1515
|
-
const o = GlobalQueue.
|
|
1515
|
+
const o = GlobalQueue.Qe?.(e);
|
|
1516
1516
|
if (o !== undefined) for (const e of o) acknowledge(n, "optimistic", nodeName(e), reachesEffect(e, t));
|
|
1517
1517
|
if (e.T !== null) for (const o of e.T) acknowledge(n, "affects", nodeName(o), reachesEffect(o, t));
|
|
1518
1518
|
}
|
|
@@ -1528,9 +1528,9 @@ function acknowledge(e, n, t, o) {
|
|
|
1528
1528
|
const o = [ e ];
|
|
1529
1529
|
while (o.length > 0 && n.left-- > 0) {
|
|
1530
1530
|
const e = o.pop();
|
|
1531
|
-
for (let n = e.t; n !== null; n = n.
|
|
1532
|
-
const e = n.
|
|
1533
|
-
if (e.
|
|
1531
|
+
for (let n = e.t; n !== null; n = n.o) {
|
|
1532
|
+
const e = n.l;
|
|
1533
|
+
if (e.u) return e;
|
|
1534
1534
|
if (!t.has(e)) {
|
|
1535
1535
|
t.add(e);
|
|
1536
1536
|
o.push(e);
|
|
@@ -1553,11 +1553,11 @@ function acknowledge(e, n, t, o) {
|
|
|
1553
1553
|
const e = o.pop();
|
|
1554
1554
|
if (t.has(e)) continue;
|
|
1555
1555
|
t.add(e);
|
|
1556
|
-
for (let t = e.t; t !== null; t = t.
|
|
1557
|
-
const s = t.
|
|
1558
|
-
if (s.
|
|
1559
|
-
const t = s.
|
|
1560
|
-
const o = s.
|
|
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
1561
|
if (t & 1 && o !== null) acknowledge(n, "isPending", nodeName(e), o);
|
|
1562
1562
|
if (t & 2 && o !== null) acknowledge(n, "latest", nodeName(e), o);
|
|
1563
1563
|
}
|
|
@@ -1628,9 +1628,9 @@ function trackHoldSettled(e) {
|
|
|
1628
1628
|
let i;
|
|
1629
1629
|
let s;
|
|
1630
1630
|
let r = -Infinity;
|
|
1631
|
-
for (const n of e.
|
|
1632
|
-
if (typeof n.
|
|
1633
|
-
const e = n.
|
|
1631
|
+
for (const n of e.Je) {
|
|
1632
|
+
if (typeof n.ee === "function" || isCompanion(n)) continue;
|
|
1633
|
+
const e = n.we;
|
|
1634
1634
|
if (e === undefined || e.kind !== "write") continue;
|
|
1635
1635
|
if (o === null) o = n;
|
|
1636
1636
|
const a = {
|
|
@@ -1657,7 +1657,7 @@ function trackHoldSettled(e) {
|
|
|
1657
1657
|
return;
|
|
1658
1658
|
}
|
|
1659
1659
|
censusRegistrations(e, n);
|
|
1660
|
-
censusCompanions([ ...e.
|
|
1660
|
+
censusCompanions([ ...e.Je, ...n.blockers ], n);
|
|
1661
1661
|
const a = now();
|
|
1662
1662
|
// The hold began no later than its first parked flush; an interaction stamp
|
|
1663
1663
|
// reaches further back (dispatch). A node rewritten mid-hold keeps only its
|
|
@@ -1818,7 +1818,7 @@ function checkLongHold(e, n) {
|
|
|
1818
1818
|
if (!options.optimisticReverts || isCompanion(e)) return;
|
|
1819
1819
|
// Method call, as every commit path does: store slot nodes share one
|
|
1820
1820
|
// comparator that reads `this._host` (#3687).
|
|
1821
|
-
const i = e.
|
|
1821
|
+
const i = e.en;
|
|
1822
1822
|
if (i && i.call(e, n, t)) return;
|
|
1823
1823
|
const s = nodeName(e);
|
|
1824
1824
|
// The two values are user data: quoted only under `values: "full"`; the
|
|
@@ -1845,7 +1845,16 @@ function checkLongHold(e, n) {
|
|
|
1845
1845
|
}
|
|
1846
1846
|
|
|
1847
1847
|
// --- Graph growth -----------------------------------------------------------------
|
|
1848
|
-
/**
|
|
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;
|
|
1849
1858
|
|
|
1850
1859
|
/**
|
|
1851
1860
|
* The live graph's size — a walk, not a counter: nothing is charged at node
|
|
@@ -1860,51 +1869,78 @@ function checkLongHold(e, n) {
|
|
|
1860
1869
|
* millisecond. Dormant nodes are spliced out of their chain and are not
|
|
1861
1870
|
* counted unless a subscription still reaches them.
|
|
1862
1871
|
*/ function graphSize() {
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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,
|
|
1866
1885
|
owners: 0,
|
|
1867
1886
|
computations: 0,
|
|
1868
1887
|
signals: 0,
|
|
1869
1888
|
edges: 0
|
|
1870
1889
|
};
|
|
1871
|
-
const
|
|
1890
|
+
const i = new Set;
|
|
1872
1891
|
// A worklist, not recursion: a subscriber chain can be thousands deep.
|
|
1873
|
-
const
|
|
1874
|
-
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
|
+
};
|
|
1875
1900
|
const meet = e => {
|
|
1876
|
-
if (
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1901
|
+
if (i.has(e)) return;
|
|
1902
|
+
i.add(e);
|
|
1903
|
+
count(e, typeof e.ee === "function" ? "computations" : "signals");
|
|
1904
|
+
s.push(e);
|
|
1880
1905
|
};
|
|
1881
|
-
for (const
|
|
1882
|
-
|
|
1906
|
+
for (const e of t) {
|
|
1907
|
+
count(e, "roots");
|
|
1908
|
+
count(e, "owners");
|
|
1883
1909
|
// Iterative: a deep tree must not grow the call stack.
|
|
1884
|
-
let
|
|
1910
|
+
let n = e.nn;
|
|
1885
1911
|
for (;;) {
|
|
1886
|
-
while (
|
|
1887
|
-
n
|
|
1888
|
-
if (typeof
|
|
1889
|
-
if (
|
|
1890
|
-
|
|
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;
|
|
1891
1917
|
}
|
|
1892
|
-
const
|
|
1893
|
-
if (
|
|
1894
|
-
|
|
1918
|
+
const e = r.pop();
|
|
1919
|
+
if (e === undefined) break;
|
|
1920
|
+
n = e.nn;
|
|
1895
1921
|
}
|
|
1896
1922
|
}
|
|
1897
|
-
for (let
|
|
1898
|
-
const
|
|
1923
|
+
for (let t = 0; t < s.length; t++) {
|
|
1924
|
+
const i = s[t];
|
|
1899
1925
|
// Whoever reads this node: owned computations are already known; an
|
|
1900
1926
|
// ownerless one is met only here.
|
|
1901
|
-
for (let e =
|
|
1902
|
-
if (typeof
|
|
1903
|
-
|
|
1904
|
-
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++;
|
|
1905
1932
|
}
|
|
1906
1933
|
}
|
|
1907
|
-
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;
|
|
1908
1944
|
}
|
|
1909
1945
|
|
|
1910
1946
|
/**
|
|
@@ -1915,63 +1951,96 @@ function checkLongHold(e, n) {
|
|
|
1915
1951
|
* `createRoot` in an effect with no dispose, a subscription a component
|
|
1916
1952
|
* registered outside its owner. Nothing here runs per node; the walk is
|
|
1917
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.
|
|
1918
1961
|
*/ const GRAPH_SERIES = [ "owners", "computations", "signals", "edges" ];
|
|
1919
1962
|
|
|
1920
1963
|
function trackGraph(e) {
|
|
1921
|
-
const n =
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
const o =
|
|
1925
|
-
|
|
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 = {
|
|
1926
1979
|
at: now(),
|
|
1927
|
-
...
|
|
1928
|
-
navigation:
|
|
1980
|
+
...s,
|
|
1981
|
+
navigation: n
|
|
1929
1982
|
};
|
|
1930
|
-
if (
|
|
1931
|
-
records.emit("graph",
|
|
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;
|
|
1932
1993
|
// The initial declaration is not a visit (it settles before route content
|
|
1933
1994
|
// mounts — the same gate the feedback folds apply as `event.initial !== true`);
|
|
1934
1995
|
// `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
// did says what leaked — computations with owners flat is an ownerless
|
|
1943
|
-
// effect per visit, edges alone is a subscription per visit to something
|
|
1944
|
-
// long-lived, owners is an undisposed root.
|
|
1945
|
-
const r = GRAPH_SERIES.filter(e => {
|
|
1946
|
-
for (let n = 1; n < s.length; n++) if (s[n][e] <= s[n - 1][e]) return false;
|
|
1947
|
-
return s[s.length - 1][e] >= s[0][e] * n.ratio;
|
|
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
|
|
1948
2003
|
});
|
|
1949
|
-
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;
|
|
1950
2019
|
// The count is the whole graph's, so a leak shows at every route's settle;
|
|
1951
2020
|
// the first route to complete its climb reports, naming the others seen.
|
|
1952
|
-
const
|
|
1953
|
-
const
|
|
1954
|
-
const
|
|
1955
|
-
const
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
roots:
|
|
1963
|
-
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
|
|
1964
2033
|
};
|
|
1965
|
-
if (
|
|
1966
|
-
type:
|
|
1967
|
-
target:
|
|
2034
|
+
if (n.interaction !== undefined) w.interaction = {
|
|
2035
|
+
type: n.interaction.name,
|
|
2036
|
+
target: n.interaction.target
|
|
1968
2037
|
};
|
|
1969
2038
|
reportDiagnostic(emitDiagnostic({
|
|
1970
2039
|
code: "GRAPH_GROWTH",
|
|
1971
2040
|
kind: "perf",
|
|
1972
2041
|
severity: "warn",
|
|
1973
|
-
message:
|
|
1974
|
-
data:
|
|
2042
|
+
message: g,
|
|
2043
|
+
data: w
|
|
1975
2044
|
}, null));
|
|
1976
2045
|
// Judged once for the graph, not once per route: the next verdict needs
|
|
1977
2046
|
// another `visits` climbing settles.
|
|
@@ -2088,6 +2157,14 @@ let navigationLog = [];
|
|
|
2088
2157
|
|
|
2089
2158
|
/** Drains completed since enable() — the clock `writeDrain` reads. */ let drainSeq = 0;
|
|
2090
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
|
+
|
|
2091
2168
|
/**
|
|
2092
2169
|
* Copy what the router currently says onto the frame (what writes stamped —
|
|
2093
2170
|
* `formatOrigin` reads it) and the event. Called when the frame opens, when a
|
|
@@ -2126,6 +2203,8 @@ function openNavigation(e, n) {
|
|
|
2126
2203
|
held: false,
|
|
2127
2204
|
writeDrain: drainSeq
|
|
2128
2205
|
};
|
|
2206
|
+
// The initial declaration is never compared (see `trackGraph`).
|
|
2207
|
+
if (options.graphGrowth !== false && n.initial !== true) o.before = snapshotGraph();
|
|
2129
2208
|
syncNavigation(o);
|
|
2130
2209
|
navStates.set(e, o);
|
|
2131
2210
|
openNavs.add(o);
|
|
@@ -2186,8 +2265,8 @@ function closeNavigation(e) {
|
|
|
2186
2265
|
|
|
2187
2266
|
/** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */ function markNavigationsHeld(e) {
|
|
2188
2267
|
if (openNavs.size === 0) return;
|
|
2189
|
-
for (const n of e.
|
|
2190
|
-
const e = n.
|
|
2268
|
+
for (const n of e.Je) {
|
|
2269
|
+
const e = n.we?.origin;
|
|
2191
2270
|
if (e?.kind !== "navigation") continue;
|
|
2192
2271
|
const t = navStates.get(e);
|
|
2193
2272
|
if (t !== undefined) t.held = true;
|
|
@@ -2196,14 +2275,14 @@ function closeNavigation(e) {
|
|
|
2196
2275
|
|
|
2197
2276
|
/** transitionSettled: `t` commits — the navigations whose writes it staged are done. */ function settleNavigations(e, n) {
|
|
2198
2277
|
if (openNavs.size === 0) return;
|
|
2199
|
-
for (const t of e.
|
|
2200
|
-
const e = t.
|
|
2278
|
+
for (const t of e.Je) {
|
|
2279
|
+
const e = t.we?.origin;
|
|
2201
2280
|
if (e?.kind !== "navigation") continue;
|
|
2202
2281
|
const o = navStates.get(e);
|
|
2203
2282
|
if (o === undefined || !openNavs.has(o)) continue;
|
|
2204
2283
|
// A navigation whose frame is still open when its transition commits
|
|
2205
2284
|
// (`until()` inside the frame) settles here too: its writes are through.
|
|
2206
|
-
settleNavigation(o, o.held ? "held" : "committed", n);
|
|
2285
|
+
settleNavigation(o, o.held ? "held" : "committed", n, true);
|
|
2207
2286
|
}
|
|
2208
2287
|
}
|
|
2209
2288
|
|
|
@@ -2330,27 +2409,30 @@ function trackFallback(e, n, t, o) {
|
|
|
2330
2409
|
drainSeq++;
|
|
2331
2410
|
// The swaps this drain committed have rendered.
|
|
2332
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;
|
|
2333
2415
|
for (const e of openNavs) if (e.open === 0 && !e.held) settleNavigation(e, "committed");
|
|
2334
2416
|
for (const e of openInteractions) maybeSettleInteraction(e);
|
|
2335
2417
|
}
|
|
2336
2418
|
|
|
2337
|
-
function settleNavigation(e, n, t) {
|
|
2419
|
+
function settleNavigation(e, n, t, o) {
|
|
2338
2420
|
if (!openNavs.has(e)) return;
|
|
2339
2421
|
openNavs.delete(e);
|
|
2340
2422
|
syncNavigation(e);
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
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;
|
|
2345
2427
|
// The initial declaration is not a navigation the person waited on: its
|
|
2346
2428
|
// `settledMs` is document start → router built, which would read as the
|
|
2347
2429
|
// route's responsiveness in the feedback tables. Consumers get the record.
|
|
2348
|
-
if (
|
|
2349
|
-
records.emit("navigation",
|
|
2350
|
-
|
|
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);
|
|
2351
2433
|
// The interaction that performed it may have been waiting only on this.
|
|
2352
|
-
const
|
|
2353
|
-
if (
|
|
2434
|
+
const s = openInteractionOf(i.interaction);
|
|
2435
|
+
if (s !== undefined) maybeSettleInteraction(s);
|
|
2354
2436
|
}
|
|
2355
2437
|
|
|
2356
2438
|
const interactionStates = new WeakMap;
|
|
@@ -2515,8 +2597,8 @@ function openInteraction(e, n, t) {
|
|
|
2515
2597
|
|
|
2516
2598
|
/** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */ function markInteractionsHeld(e) {
|
|
2517
2599
|
if (openInteractions.size === 0) return;
|
|
2518
|
-
for (const n of e.
|
|
2519
|
-
const t = openInteractionOf(n.
|
|
2600
|
+
for (const n of e.Je) {
|
|
2601
|
+
const t = openInteractionOf(n.we?.origin);
|
|
2520
2602
|
if (t !== undefined) {
|
|
2521
2603
|
t.heldIn.add(e);
|
|
2522
2604
|
t.held = true;
|
|
@@ -2603,7 +2685,7 @@ const engineHooks = {
|
|
|
2603
2685
|
prevDeps: n || !wantsRerun() ? null : captureDeps(e),
|
|
2604
2686
|
// Mirror recompute's own prev-value resolution: an earlier run in the
|
|
2605
2687
|
// same flush may still be holding in _pendingValue.
|
|
2606
|
-
prevValue: e.
|
|
2688
|
+
prevValue: e.Oe !== NOT_PENDING ? e.Oe : e.Ae,
|
|
2607
2689
|
// A create run inherits the interaction of whatever is building it: the
|
|
2608
2690
|
// enclosing recompute (a parent's fn creating children) or, at the top
|
|
2609
2691
|
// of the recompute stack, the effect callback / handler frame.
|
|
@@ -2629,8 +2711,8 @@ const engineHooks = {
|
|
|
2629
2711
|
// committed compute output is an unchanged run. `undefined` outputs are
|
|
2630
2712
|
// exempt — a side-effect-only compute's work IS its effect phase, and
|
|
2631
2713
|
// identity of `undefined` proves nothing.
|
|
2632
|
-
if (t && r.causes !== null && e.
|
|
2633
|
-
const n = e.
|
|
2714
|
+
if (t && r.causes !== null && e.u) {
|
|
2715
|
+
const n = e.Oe !== NOT_PENDING ? e.Oe : e.Ae;
|
|
2634
2716
|
if (n !== undefined && n === r.prevValue) t = false;
|
|
2635
2717
|
}
|
|
2636
2718
|
// Unstable-output check: memos only, non-create, plain runs with a
|
|
@@ -2638,12 +2720,12 @@ const engineHooks = {
|
|
|
2638
2720
|
// plain-flush memo commits and in `_value` for direct ones. Overlay runs
|
|
2639
2721
|
// are excluded — an optimistic re-derive legitimately produces fresh
|
|
2640
2722
|
// equivalents while the lane settles.
|
|
2641
|
-
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);
|
|
2642
2724
|
// The same exemption for memos: a run that committed `undefined` did its
|
|
2643
2725
|
// work as a side effect (a projection's draft writes or reconcile, a
|
|
2644
2726
|
// write from inside a memo), so core's `undefined === undefined` gate
|
|
2645
2727
|
// proves nothing. Reported as changed, so no consumer counts it as waste.
|
|
2646
|
-
if (!t && (e.
|
|
2728
|
+
if (!t && (e.Oe !== NOT_PENDING ? e.Oe : e.Ae) === undefined) t = true;
|
|
2647
2729
|
if (r.causes !== null) recordRerun(e, r, {
|
|
2648
2730
|
selfMs: c,
|
|
2649
2731
|
totalMs: a
|
|
@@ -2655,7 +2737,7 @@ const engineHooks = {
|
|
|
2655
2737
|
noteInteractionRun(r.interaction, c, true);
|
|
2656
2738
|
// The first effect callback runs with no re-run record to inherit from;
|
|
2657
2739
|
// hand it the interaction that built the node (see effectRunStart).
|
|
2658
|
-
e.
|
|
2740
|
+
e.qe = r.interaction;
|
|
2659
2741
|
if (openFlush !== null) noteFlushRun(openFlush, true, r.interaction);
|
|
2660
2742
|
if (records.observed("create")) {
|
|
2661
2743
|
const n = {
|
|
@@ -2685,9 +2767,9 @@ const engineHooks = {
|
|
|
2685
2767
|
trackFlightStart(e, n);
|
|
2686
2768
|
},
|
|
2687
2769
|
asyncStart(e) {
|
|
2688
|
-
asyncStartSeq = e.
|
|
2689
|
-
asyncStartTime = e.
|
|
2690
|
-
asyncStartValue = e.
|
|
2770
|
+
asyncStartSeq = e.we?.seq ?? 0;
|
|
2771
|
+
asyncStartTime = e.Ie;
|
|
2772
|
+
asyncStartValue = e.Ae;
|
|
2691
2773
|
},
|
|
2692
2774
|
asyncEnd(e, n, t, o) {
|
|
2693
2775
|
if (o) {
|
|
@@ -2697,7 +2779,7 @@ const engineHooks = {
|
|
|
2697
2779
|
// (or `_time`, for a same-reference commit under `equals: false`), and
|
|
2698
2780
|
// a transition hold parks the value in `_pendingValue`. A landing the
|
|
2699
2781
|
// equality gate swallowed moves none of them and must leave no stamp.
|
|
2700
|
-
const o = e.
|
|
2782
|
+
const o = e.Ae !== asyncStartValue || e.Ie !== asyncStartTime || e.Oe === t;
|
|
2701
2783
|
if (o) stampWrite(e, "async", n === undefined ? NO_VALUES : n, t);
|
|
2702
2784
|
// Flight over either way — an equality-swallowed landing still spent
|
|
2703
2785
|
// the wall time (finalizeFlight only chains through a fresh stamp).
|
|
@@ -2707,12 +2789,12 @@ const engineHooks = {
|
|
|
2707
2789
|
// Landed through setSignal: reclassify its "write" stamp as an async
|
|
2708
2790
|
// landing — but only if it actually stamped (the value changed) since
|
|
2709
2791
|
// asyncStart; a no-change landing must leave no fresh stamp behind.
|
|
2710
|
-
const i = e.
|
|
2792
|
+
const i = e.we;
|
|
2711
2793
|
if (i !== undefined && i.seq > asyncStartSeq && i.kind === "write") stampWrite(e, "async", NO_VALUES, t);
|
|
2712
2794
|
finalizeFlight(e);
|
|
2713
2795
|
},
|
|
2714
2796
|
effectRunStart(e) {
|
|
2715
|
-
pushFrame("effect", nodeName(e), e.
|
|
2797
|
+
pushFrame("effect", nodeName(e), e.qe, e);
|
|
2716
2798
|
// The frame's own `at` doubles as the record's start: set only while
|
|
2717
2799
|
// listened, so a listener arriving mid-callback finds no start and the
|
|
2718
2800
|
// end emits nothing for it — and an unlistened callback pays no clock read.
|
|
@@ -2816,8 +2898,10 @@ const holds = [];
|
|
|
2816
2898
|
stagedFallbacks.delete(DRAIN);
|
|
2817
2899
|
trackingGen++;
|
|
2818
2900
|
openNavs.clear();
|
|
2901
|
+
landedGraphs.length = 0;
|
|
2819
2902
|
openInteractions.clear();
|
|
2820
2903
|
routeCounts.clear();
|
|
2904
|
+
visitRoute = undefined;
|
|
2821
2905
|
drainSeq = 0;
|
|
2822
2906
|
originFrames.length = 0;
|
|
2823
2907
|
effectStack.length = 0;
|