@solidjs/signals 2.0.0-rc.14 → 2.0.0-rc.16

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.
Files changed (98) hide show
  1. package/dist/dev-shared.js +882 -405
  2. package/dist/dev.attribution.js +172 -66
  3. package/dist/dev.js +1529 -745
  4. package/dist/observe/affects-marks.js +86 -0
  5. package/dist/observe/affects.js +32 -80
  6. package/dist/observe/boundaries.js +202 -166
  7. package/dist/observe/core/async.js +192 -141
  8. package/dist/observe/core/attribution-queries.js +1 -1
  9. package/dist/observe/core/attribution.js +315 -214
  10. package/dist/observe/core/constants.js +23 -4
  11. package/dist/observe/core/context.js +3 -3
  12. package/dist/observe/core/core.js +532 -360
  13. package/dist/observe/core/dev.js +63 -35
  14. package/dist/observe/core/effect.js +47 -47
  15. package/dist/observe/core/error.js +5 -3
  16. package/dist/observe/core/external.js +5 -6
  17. package/dist/observe/core/graph.js +56 -42
  18. package/dist/observe/core/heap.js +33 -33
  19. package/dist/observe/core/identities.js +62 -0
  20. package/dist/observe/core/lanes.js +242 -229
  21. package/dist/observe/core/owner.js +62 -50
  22. package/dist/observe/core/scheduler.js +322 -168
  23. package/dist/observe/core/truth.js +45 -0
  24. package/dist/observe/core/verdict.js +114 -64
  25. package/dist/observe/deferred.js +184 -0
  26. package/dist/observe/index.js +7 -1
  27. package/dist/observe/map.js +339 -254
  28. package/dist/observe/reveal.js +77 -77
  29. package/dist/observe/signals.js +54 -33
  30. package/dist/observe/store/affects.js +76 -69
  31. package/dist/observe/store/edges.js +39 -0
  32. package/dist/observe/store/hooks.js +13 -0
  33. package/dist/observe/store/index.js +0 -2
  34. package/dist/observe/store/optimistic.js +82 -44
  35. package/dist/observe/store/projection.js +83 -64
  36. package/dist/observe/store/reconcile.js +32 -30
  37. package/dist/observe/store/store.js +219 -123
  38. package/dist/prod/affects-marks.js +86 -0
  39. package/dist/prod/affects.js +33 -79
  40. package/dist/prod/boundaries.js +185 -149
  41. package/dist/prod/core/async.js +176 -135
  42. package/dist/prod/core/constants.js +23 -4
  43. package/dist/prod/core/context.js +3 -3
  44. package/dist/prod/core/core.js +521 -353
  45. package/dist/prod/core/effect.js +66 -66
  46. package/dist/prod/core/error.js +5 -3
  47. package/dist/prod/core/external.js +5 -6
  48. package/dist/prod/core/graph.js +56 -42
  49. package/dist/prod/core/heap.js +38 -38
  50. package/dist/prod/core/identities.js +62 -0
  51. package/dist/prod/core/lanes.js +237 -224
  52. package/dist/prod/core/owner.js +62 -51
  53. package/dist/prod/core/scheduler.js +312 -158
  54. package/dist/prod/core/truth.js +45 -0
  55. package/dist/prod/core/verdict.js +113 -63
  56. package/dist/prod/deferred.js +182 -0
  57. package/dist/prod/index.js +7 -1
  58. package/dist/prod/map.js +366 -281
  59. package/dist/prod/reveal.js +77 -77
  60. package/dist/prod/signals.js +59 -38
  61. package/dist/prod/store/affects.js +76 -69
  62. package/dist/prod/store/edges.js +39 -0
  63. package/dist/prod/store/hooks.js +13 -0
  64. package/dist/prod/store/index.js +0 -2
  65. package/dist/prod/store/optimistic.js +121 -84
  66. package/dist/prod/store/projection.js +46 -34
  67. package/dist/prod/store/reconcile.js +32 -30
  68. package/dist/prod/store/store.js +205 -116
  69. package/dist/types/affects-marks.d.ts +14 -0
  70. package/dist/types/boundaries.d.ts +5 -1
  71. package/dist/types/core/async.d.ts +10 -0
  72. package/dist/types/core/attribution.d.ts +6 -2
  73. package/dist/types/core/constants.d.ts +18 -1
  74. package/dist/types/core/core.d.ts +77 -5
  75. package/dist/types/core/dev.d.ts +43 -13
  76. package/dist/types/core/graph.d.ts +11 -0
  77. package/dist/types/core/identities.d.ts +17 -0
  78. package/dist/types/core/index.d.ts +2 -1
  79. package/dist/types/core/lanes.d.ts +2 -4
  80. package/dist/types/core/owner.d.ts +5 -3
  81. package/dist/types/core/scheduler.d.ts +59 -34
  82. package/dist/types/core/truth.d.ts +16 -0
  83. package/dist/types/core/types.d.ts +21 -6
  84. package/dist/types/core/verdict.d.ts +32 -0
  85. package/dist/types/deferred.d.ts +28 -0
  86. package/dist/types/index.d.ts +6 -0
  87. package/dist/types/map.d.ts +2 -2
  88. package/dist/types/signals.d.ts +28 -7
  89. package/dist/types/store/affects.d.ts +11 -0
  90. package/dist/types/store/edges.d.ts +9 -0
  91. package/dist/types/store/hooks.d.ts +48 -0
  92. package/dist/types/store/index.d.ts +10 -4
  93. package/dist/types/store/optimistic.d.ts +3 -2
  94. package/dist/types/store/projection.d.ts +4 -3
  95. package/dist/types/store/store.d.ts +4 -39
  96. package/dist/types/store/target.d.ts +16 -8
  97. package/dist/types/store/types.d.ts +14 -0
  98. 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.u & CONFIG_PLUMBING) !== 0) return true;
20
+ if ((e.l & CONFIG_PLUMBING) !== 0) return true;
21
21
  if (!anyExcluded()) return false;
22
22
  const n = e;
23
- if (n.me === undefined) n.me = isExcluded(e);
24
- return n.me;
23
+ if (n.he === undefined) n.he = isExcluded(e);
24
+ return n.he;
25
25
  }
26
26
 
27
27
  let attributionActive = false;
@@ -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.ge
330
+ causes: n.pe
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.we !== undefined) i.run = e.we;
348
+ if (e.me !== undefined) i.run = e.me;
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.l) {
461
+ for (let n = e.o; n !== null; n = n.t) {
462
462
  o++;
463
- if (!(n.o.u & CONFIG_PLUMBING)) i++;
463
+ if (!(n.u.l & 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.ve;
487
- e.ve = i;
486
+ const s = e.ge;
487
+ e.ge = 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.ve = {
498
+ e.ge = {
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.ke ?? 0;
513
+ const n = e.we ?? 0;
514
514
  const t = [];
515
- const o = e.ve;
515
+ const o = e.ge;
516
516
  if (o !== undefined && o.seq > n && o.kind === "refresh") t.push(o);
517
- for (let o = e._; o !== null; o = o.p) {
518
- const e = o.I.ve;
517
+ for (let o = e.C; o !== null; o = o.G) {
518
+ const e = o.S.ge;
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.ke = changeSeq;
525
+ e.we = 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._; t !== null; t = t.p) {
538
- n.push(t.I);
539
- if (t === e.S) break;
537
+ for (let t = e.C; t !== null; t = t.G) {
538
+ n.push(t.S);
539
+ if (t === e._) 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._; n !== null; n = n.p) {
558
+ for (let n = e.C; n !== null; n = n.G) {
559
559
  // HMR plumbing is a dev-only hop the scope cannot read past: not a source.
560
- if (!(n.I.u & CONFIG_PLUMBING)) {
560
+ if (!(n.S.l & CONFIG_PLUMBING)) {
561
561
  t++;
562
- if (o.length < 12) o.push(nodeName(n.I));
562
+ if (o.length < 12) o.push(nodeName(n.S));
563
563
  }
564
- if (n === e.S) break;
564
+ if (n === e._) break;
565
565
  // the validated prefix, as captureDeps
566
566
  }
567
567
  const i = e;
568
- if (t < n || t < (i.ye ?? 0) * 1.5) return;
569
- i.ye = t;
568
+ if (t < n || t < (i.ve ?? 0) * 1.5) return;
569
+ i.ve = 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.$e === undefined || i - o.$e > t.windowMs) {
599
- o.$e = i;
600
- o.be = 0;
601
- o.Fe = false;
602
- }
603
- o.be = (o.be ?? 0) + 1;
604
- if (o.Fe || o.be < t.count) return;
605
- o.Fe = true;
598
+ if (o.ke === undefined || i - o.ke > t.windowMs) {
599
+ o.ke = i;
600
+ o.ye = 0;
601
+ o.$e = false;
602
+ }
603
+ o.ye = (o.ye ?? 0) + 1;
604
+ if (o.$e || o.ye < t.count) return;
605
+ o.$e = 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.be;
622
+ a.runs += o.ye;
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.be} times ` + `in ${Math.max(1, i - o.$e)}ms — a hot signal is likely leaking into this ` + `scope. Latest cause: ${s || "(untracked pull)"}`;
625
+ const r = `[HOT_SCOPE_RERUNS] ${nodeKind(e)} "${nodeName(e)}" re-ran ${o.ye} times ` + `in ${Math.max(1, i - o.ke)}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.be,
633
+ runs: o.ye,
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.Le === undefined || s - i.Le > o.windowMs) {
672
- i.Le = s;
673
- i.Ce = 0;
674
- i.Me = false;
675
- }
676
- i.Ce = (i.Ce ?? 0) + n;
677
- if (i.Me || i.Ce < o.budgetMs) return;
678
- i.Me = true;
671
+ if (i.be === undefined || s - i.be > o.windowMs) {
672
+ i.be = s;
673
+ i.Oe = 0;
674
+ i.Fe = false;
675
+ }
676
+ i.Oe = (i.Oe ?? 0) + n;
677
+ if (i.Fe || i.Oe < o.budgetMs) return;
678
+ i.Fe = 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.Ce.toFixed(1)}ms of compute inside one ${o.windowMs}ms window ` + `(budget ${o.budgetMs}ms). Latest cause: ${r || "(untracked pull)"}`;
680
+ const a = `[HOT_SCOPE_TIME] ${nodeKind(e)} "${nodeName(e)}" spent ` + `${i.Oe.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.Ce,
688
+ spentMs: i.Oe,
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.He === undefined || n - a.He > r.windowMs) {
714
- a.He = n;
715
- a.We = 0;
716
- a.Ge = 0;
717
- a.qe = 0;
718
- a.Be = false;
719
- }
720
- a.We = a.We + 1;
713
+ if (a._e === undefined || n - a._e > r.windowMs) {
714
+ a._e = n;
715
+ a.Ae = 0;
716
+ a.Re = 0;
717
+ a.Le = 0;
718
+ a.Ce = false;
719
+ }
720
+ a.Ae = a.Ae + 1;
721
721
  if (!o) {
722
- a.Ge = a.Ge + 1;
723
- a.qe = a.qe + i;
722
+ a.Re = a.Re + 1;
723
+ a.Le = a.Le + i;
724
724
  }
725
- if (a.Be || a.We < r.minRuns || a.Ge / a.We < r.ratio || a.qe < r.budgetMs) return;
726
- a.Be = true;
725
+ if (a.Ce || a.Ae < r.minRuns || a.Re / a.Ae < r.ratio || a.Le < r.budgetMs) return;
726
+ a.Ce = true;
727
727
  const c = {
728
- runs: a.We,
729
- wasted: a.Ge,
730
- wastedMs: a.qe
728
+ runs: a.Ae,
729
+ wasted: a.Re,
730
+ wastedMs: a.Le
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.ge;
753
+ const c = a.pe;
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.xe = d;
759
- a.we = undefined;
760
- a.ge = r;
758
+ a.Me = d;
759
+ a.me = undefined;
760
+ a.pe = 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.Ke = (a.Ke ?? 0) + 1;
770
- a.xe = d;
771
- a.we = f;
772
- a.ge = r;
769
+ const u = a.De = (a.De ?? 0) + 1;
770
+ a.Me = d;
771
+ a.me = f;
772
+ a.pe = 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.Ve = 0;
914
- i.je = false;
913
+ i.He = 0;
914
+ i.We = false;
915
915
  return;
916
916
  }
917
- i.Ve = (i.Ve ?? 0) + 1;
918
- if (i.je || i.Ve < o) return;
919
- i.je = true;
917
+ i.He = (i.He ?? 0) + 1;
918
+ if (i.We || i.He < o) return;
919
+ i.We = 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.Ve} consecutive runs — its equality gate never closes, so every ` + `subscriber re-runs on every upstream change. Return stable references or pass an ` + `\`equals\` option.`;
921
+ const r = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${s} on ` + `${i.He} 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.Ve,
929
+ runs: i.He,
930
930
  shape: s
931
931
  }
932
932
  }, e));
@@ -1065,17 +1065,20 @@ 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.Ye = o.Ye === undefined || o.Ye === r ? r : null;
1069
- if (s !== undefined && t !== undefined && t === s.node.pe) {
1068
+ o.Pe = o.Pe === undefined || o.Pe === r ? r : null;
1069
+ // An apply half that takes no argument cannot be copying its compute
1070
+ // output: a match is coincidence (`createEffect(open, () =>
1071
+ // setFlipped(measure()))`).
1072
+ if (s !== undefined && t !== undefined && t === s.node.Ie && (s.node.Ge?.length ?? 1)) {
1070
1073
  copyWrites.add(n);
1071
- o.ze = o.Xe === r ? (o.ze ?? 0) + 1 : 1;
1072
- o.Xe = r;
1073
- if (o.ze >= 2 && o.Ye === r) checkCopyEffect(s.node, e);
1074
- } else o.ze = 0;
1074
+ o.Ue = o.qe === r ? (o.Ue ?? 0) + 1 : 1;
1075
+ o.qe = r;
1076
+ if (o.Ue >= 2 && o.Pe === r) checkCopyEffect(s.node, e);
1077
+ } else o.Ue = 0;
1075
1078
  }
1076
1079
 
1077
1080
  /** 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._; n !== null; n = n.p) if (n.I.pe === e.pe) return nodeName(n.I);
1081
+ for (let n = e.C; n !== null; n = n.G) if (n.S.Ie === e.Ie) return nodeName(n.S);
1079
1082
  return undefined;
1080
1083
  }
1081
1084
 
@@ -1148,13 +1151,16 @@ function checkCopyEffect(e, n) {
1148
1151
  c.count++;
1149
1152
  const d = copyWrites.has(s);
1150
1153
  const f = recordNodes.get(s);
1151
- const u = f !== undefined && f.Ye === devId(i.node);
1154
+ const u = f !== undefined && f.Pe === devId(i.node);
1152
1155
  // Derivable outright: the value is the compute output and nothing else
1153
1156
  // writes the signal. A copy INTO a signal that has other writers is the
1154
1157
  // "reset editable state from a source" shape — the tear is real, but a memo
1155
1158
  // is not the answer, so it stays advisory like any other non-derivable tear.
1156
1159
  const l = d && u;
1157
- const h = l || c.count >= RELAY_WARN_AT ? "warn" : "info";
1160
+ // An apply half that ignores its argument writes from outside the graph
1161
+ // (layout, time, a ref; a signal read there is STRICT_READ's): measuring,
1162
+ // so it stays advisory however often it repeats.
1163
+ const h = l || c.count >= RELAY_WARN_AT && (i.node.Ge?.length ?? 1) ? "warn" : "info";
1158
1164
  // First sighting always reports (advisory); afterwards only the escalation.
1159
1165
  if (c.count > 1 && (h !== "warn" || c.warned)) return;
1160
1166
  // One verdict per derivable signal: the copy report (checkCopyEffect) and
@@ -1166,7 +1172,7 @@ function checkCopyEffect(e, n) {
1166
1172
  if (h === "warn") c.warned = true;
1167
1173
  const p = e.i ? "effect" : "memo";
1168
1174
  const m = nodeName(i.node);
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.`;
1175
+ 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}". ` : "") + `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
1176
  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}`;
1171
1177
  emitDiagnostic({
1172
1178
  code: "EFFECT_RELAY_TEAR",
@@ -1255,7 +1261,7 @@ const listIdentityWarned = new WeakSet;
1255
1261
  function recordId(e) {
1256
1262
  if (e === null || typeof e !== "object") return undefined;
1257
1263
  const n = e;
1258
- return n.id ?? n.key ?? n.Qe ?? undefined;
1264
+ return n.id ?? n.key ?? n.Be ?? undefined;
1259
1265
  }
1260
1266
 
1261
1267
  function checkListIdentity(e, n, t, o, i) {
@@ -1317,13 +1323,11 @@ const liveFlights = new WeakMap;
1317
1323
 
1318
1324
  const landedFlights = new WeakMap;
1319
1325
 
1320
- /**
1321
- * Flight-object identity → earliest known start. Fed by markFlight() (the
1322
- * cooperative preload/cache declaration — populated even while attribution
1323
- * is disabled, so navigation-time marks survive a later enable()) and by
1324
- * first sightings at registration.
1325
- */ const flightOrigins = new WeakMap;
1326
-
1326
+ // Flight-object identity → earliest known start lives on the core's slot
1327
+ // (`OBSERVE.attribution.markFlight`/`flightOrigin`), so marks made with no
1328
+ // engine loaded — a router's preloads, `lazy()`'s import — and the server's
1329
+ // boundary check share it. Fed by markFlight() and by first sightings at
1330
+ // registration.
1327
1331
  let waterfallLog = [];
1328
1332
 
1329
1333
  /** Deepest landed-flight cause reachable through a cause list (derived links included). */ function flightCauseIn(e) {
@@ -1366,8 +1370,8 @@ let waterfallLog = [];
1366
1370
 
1367
1371
  function trackFlightStart(e, n) {
1368
1372
  const t = now();
1369
- const o = flightOrigins.get(n) ?? t;
1370
- if (o === t) flightOrigins.set(n, t);
1373
+ const o = OBSERVE.attribution.flightOrigin(n) ?? t;
1374
+ if (o === t) OBSERVE.attribution.markFlight(n, t);
1371
1375
  // Census: a flight still in the air when the node starts another was
1372
1376
  // superseded — its answer will be discarded.
1373
1377
  const i = liveFlights.get(e);
@@ -1412,7 +1416,7 @@ function trackFlightStart(e, n) {
1412
1416
  const o = t - n.origin;
1413
1417
  for (const n of folds) n.flightLanded?.(e, o);
1414
1418
  if (records.observed("flight")) emitFlight(e, n, t, "landed");
1415
- const i = e.ve;
1419
+ const i = e.ge;
1416
1420
  // Only a stamp this landing produced may carry the measurement — a stale
1417
1421
  // async record from a previous landing must not be re-labeled.
1418
1422
  if (i !== undefined && i.kind === "async" && i.seq > n.startSeq) landedFlights.set(i, {
@@ -1448,8 +1452,8 @@ function checkWaterfall(e, n, t) {
1448
1452
  }
1449
1453
  if (i < 2) return;
1450
1454
  const r = e;
1451
- if ((r.Ze ?? 0) >= i) return;
1452
- r.Ze = i;
1455
+ if ((r.xe ?? 0) >= i) return;
1456
+ r.xe = i;
1453
1457
  const a = [ ...n.slice(n.length - (i - 1)), {
1454
1458
  name: nodeName(e),
1455
1459
  ms: t
@@ -1486,7 +1490,7 @@ let holdLog = [];
1486
1490
  /** A lane's derivation (L2: a node carrying the lane's value that is not a
1487
1491
  * written guess — lanes.ts) is a lane pass's result, not a write anyone
1488
1492
  * made: not an acknowledgement. */ function isCompanion(e) {
1489
- return (e.u & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
1493
+ return (e.l & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
1490
1494
  }
1491
1495
 
1492
1496
  const HOLD_CENSUS_CAP = 1e4;
@@ -1512,9 +1516,9 @@ function acknowledge(e, n, t, o) {
1512
1516
  const t = {
1513
1517
  left: HOLD_CENSUS_CAP
1514
1518
  };
1515
- const o = GlobalQueue.Je?.(e);
1519
+ const o = GlobalQueue.Ke?.(e);
1516
1520
  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));
1521
+ if (e.I !== null) for (const o of e.I) acknowledge(n, "affects", nodeName(o), reachesEffect(o, t));
1518
1522
  }
1519
1523
 
1520
1524
  /**
@@ -1528,8 +1532,8 @@ function acknowledge(e, n, t, o) {
1528
1532
  const o = [ e ];
1529
1533
  while (o.length > 0 && n.left-- > 0) {
1530
1534
  const e = o.pop();
1531
- for (let n = e.t; n !== null; n = n.l) {
1532
- const e = n.o;
1535
+ for (let n = e.o; n !== null; n = n.t) {
1536
+ const e = n.u;
1533
1537
  if (e.i) return e;
1534
1538
  if (!t.has(e)) {
1535
1539
  t.add(e);
@@ -1553,10 +1557,10 @@ function acknowledge(e, n, t, o) {
1553
1557
  const e = o.pop();
1554
1558
  if (t.has(e)) continue;
1555
1559
  t.add(e);
1556
- for (let t = e.t; t !== null; t = t.l) {
1557
- const s = t.o;
1558
- if (s.u & CONFIG_VERDICT) {
1559
- const t = s.en ?? 0;
1560
+ for (let t = e.o; t !== null; t = t.t) {
1561
+ const s = t.u;
1562
+ if (s.l & CONFIG_VERDICT) {
1563
+ const t = s.Ve ?? 0;
1560
1564
  const o = s.i ? s : reachesEffect(s, i);
1561
1565
  if (t & 1 && o !== null) acknowledge(n, "isPending", nodeName(e), o);
1562
1566
  if (t & 2 && o !== null) acknowledge(n, "latest", nodeName(e), o);
@@ -1628,9 +1632,9 @@ function trackHoldSettled(e) {
1628
1632
  let i;
1629
1633
  let s;
1630
1634
  let r = -Infinity;
1631
- for (const n of e.nn) {
1632
- if (typeof n.re === "function" || isCompanion(n)) continue;
1633
- const e = n.ve;
1635
+ for (const n of e.je) {
1636
+ if (typeof n.X === "function" || isCompanion(n)) continue;
1637
+ const e = n.ge;
1634
1638
  if (e === undefined || e.kind !== "write") continue;
1635
1639
  if (o === null) o = n;
1636
1640
  const a = {
@@ -1657,7 +1661,7 @@ function trackHoldSettled(e) {
1657
1661
  return;
1658
1662
  }
1659
1663
  censusRegistrations(e, n);
1660
- censusCompanions([ ...e.nn, ...n.blockers ], n);
1664
+ censusCompanions([ ...e.je, ...n.blockers ], n);
1661
1665
  const a = now();
1662
1666
  // The hold began no later than its first parked flush; an interaction stamp
1663
1667
  // reaches further back (dispatch). A node rewritten mid-hold keeps only its
@@ -1818,7 +1822,7 @@ function checkLongHold(e, n) {
1818
1822
  if (!options.optimisticReverts || isCompanion(e)) return;
1819
1823
  // Method call, as every commit path does: store slot nodes share one
1820
1824
  // comparator that reads `this._host` (#3687).
1821
- const i = e.tn;
1825
+ const i = e.Ye;
1822
1826
  if (i && i.call(e, n, t)) return;
1823
1827
  const s = nodeName(e);
1824
1828
  // The two values are user data: quoted only under `values: "full"`; the
@@ -1845,7 +1849,21 @@ function checkLongHold(e, n) {
1845
1849
  }
1846
1850
 
1847
1851
  // --- Graph growth -----------------------------------------------------------------
1848
- /** Per route: the graph's size at its last `visits` settles, oldest first. */ const routeCounts = new Map;
1852
+ /**
1853
+ * Per route: at its last `visits` compared settles, oldest first, the
1854
+ * graph's size and the part of it already alive as a navigation opened.
1855
+ */ const routeCounts = new Map;
1856
+
1857
+ /**
1858
+ * The route the app last arrived on: a navigation that stays on it (only
1859
+ * its params or query change) continues the visit and is not compared.
1860
+ */ let visitRoute;
1861
+
1862
+ /**
1863
+ * Every node met as a navigation opened, weakly, for the session: one set,
1864
+ * not one per navigation (see `trackGraph`). Never reset — a node in it was
1865
+ * alive as a navigation opened whichever window met it.
1866
+ */ const opened = new WeakSet;
1849
1867
 
1850
1868
  /**
1851
1869
  * The live graph's size — a walk, not a counter: nothing is charged at node
@@ -1860,51 +1878,78 @@ function checkLongHold(e, n) {
1860
1878
  * millisecond. Dormant nodes are spliced out of their chain and are not
1861
1879
  * counted unless a subscription still reaches them.
1862
1880
  */ function graphSize() {
1863
- const e = liveRootOwners();
1864
- const n = {
1865
- roots: e.length,
1881
+ return walkGraph();
1882
+ }
1883
+
1884
+ /**
1885
+ * The walk behind `graphSize()`. Given `before` alone, it adds every node it
1886
+ * meets to that set: what is alive as a navigation opens. Given `before` and
1887
+ * `survived`, it also counts into `survived` the nodes it meets that are in
1888
+ * the set, and an edge when both its ends are: what outlived a navigation.
1889
+ * Nothing is retained — the set is weak.
1890
+ */ function walkGraph(e, n) {
1891
+ const t = liveRootOwners();
1892
+ const o = {
1893
+ roots: 0,
1866
1894
  owners: 0,
1867
1895
  computations: 0,
1868
1896
  signals: 0,
1869
1897
  edges: 0
1870
1898
  };
1871
- const t = new Set;
1899
+ const i = new Set;
1872
1900
  // A worklist, not recursion: a subscriber chain can be thousands deep.
1873
- const o = [];
1874
- const i = [];
1901
+ const s = [];
1902
+ const r = [];
1903
+ const count = (t, i) => {
1904
+ o[i]++;
1905
+ if (n !== undefined) {
1906
+ if (e.has(t)) n[i]++;
1907
+ } else if (e !== undefined) e.add(t);
1908
+ };
1875
1909
  const meet = e => {
1876
- if (t.has(e)) return;
1877
- t.add(e);
1878
- if (typeof e.re === "function") n.computations++; else n.signals++;
1879
- o.push(e);
1910
+ if (i.has(e)) return;
1911
+ i.add(e);
1912
+ count(e, typeof e.X === "function" ? "computations" : "signals");
1913
+ s.push(e);
1880
1914
  };
1881
- for (const t of e) {
1882
- n.owners++;
1915
+ for (const e of t) {
1916
+ count(e, "roots");
1917
+ count(e, "owners");
1883
1918
  // Iterative: a deep tree must not grow the call stack.
1884
- let e = t.sn;
1919
+ let n = e.ze;
1885
1920
  for (;;) {
1886
- while (e !== null) {
1887
- n.owners++;
1888
- if (typeof e.re === "function") meet(e);
1889
- if (e.sn !== null) i.push(e);
1890
- e = e.rn;
1921
+ while (n !== null) {
1922
+ count(n, "owners");
1923
+ if (typeof n.X === "function") meet(n);
1924
+ if (n.ze !== null) r.push(n);
1925
+ n = n.Xe;
1891
1926
  }
1892
- const t = i.pop();
1893
- if (t === undefined) break;
1894
- e = t.sn;
1927
+ const e = r.pop();
1928
+ if (e === undefined) break;
1929
+ n = e.ze;
1895
1930
  }
1896
1931
  }
1897
- for (let e = 0; e < o.length; e++) {
1898
- const t = o[e];
1932
+ for (let t = 0; t < s.length; t++) {
1933
+ const i = s[t];
1899
1934
  // Whoever reads this node: owned computations are already known; an
1900
1935
  // ownerless one is met only here.
1901
- for (let e = t.t; e !== null; e = e.l) meet(e.o);
1902
- if (typeof t.re === "function") for (let e = t._; e !== null; e = e.p) {
1903
- n.edges++;
1904
- meet(e.I);
1936
+ for (let e = i.o; e !== null; e = e.t) meet(e.u);
1937
+ if (typeof i.X === "function") for (let t = i.C; t !== null; t = t.G) {
1938
+ o.edges++;
1939
+ meet(t.S);
1940
+ if (n !== undefined && e.has(i) && e.has(t.S)) n.edges++;
1905
1941
  }
1906
1942
  }
1907
- return n;
1943
+ return o;
1944
+ }
1945
+
1946
+ /**
1947
+ * openNavigation, while the growth check is on: what is alive as a
1948
+ * navigation opens, added to what earlier ones met, for its settle to tell
1949
+ * apart from what it created.
1950
+ */ function snapshotGraph() {
1951
+ walkGraph(opened);
1952
+ return opened;
1908
1953
  }
1909
1954
 
1910
1955
  /**
@@ -1915,63 +1960,105 @@ function checkLongHold(e, n) {
1915
1960
  * `createRoot` in an effect with no dispose, a subscription a component
1916
1961
  * registered outside its owner. Nothing here runs per node; the walk is
1917
1962
  * the cost, at navigation cadence.
1963
+ *
1964
+ * A count taken at an arrival also holds everything the visit just created,
1965
+ * so a page whose correctly owned content is bigger each visit climbs too.
1966
+ * What tells the two apart is what outlived a navigation: the nodes alive
1967
+ * as this or any earlier navigation opened (`opened`) that are still alive
1968
+ * at this settle. A visit's own content is not among them; what an earlier
1969
+ * visit left behind is, whatever the content does. A series reports only
1970
+ * when both climb.
1971
+ *
1972
+ * The set is the session's, not this navigation's own snapshot: the walk
1973
+ * meets a long-lived source only through a page that reads it, so a
1974
+ * per-navigation snapshot holds a module-level signal (an HMR registry, a
1975
+ * global store) only when the page just left read it too, and arriving from
1976
+ * pages that share ever more with this one read as growth (#4101). Kept
1977
+ * across navigations, such a source counts at every settle once met, the
1978
+ * same each visit; only what keeps accumulating climbs.
1918
1979
  */ const GRAPH_SERIES = [ "owners", "computations", "signals", "edges" ];
1919
1980
 
1920
1981
  function trackGraph(e) {
1921
- const n = options.graphGrowth;
1922
- if (n === false && !records.observed("graph")) return;
1923
- const t = graphSize();
1924
- const o = e.name ?? e.to;
1925
- const i = {
1982
+ const n = e.event;
1983
+ const t = e.before;
1984
+ e.before = undefined;
1985
+ const o = options.graphGrowth;
1986
+ if (o === false && !records.observed("graph")) return;
1987
+ const i = t && {
1988
+ roots: 0,
1989
+ owners: 0,
1990
+ computations: 0,
1991
+ signals: 0,
1992
+ edges: 0
1993
+ };
1994
+ const s = walkGraph(t, i);
1995
+ const r = n.name ?? n.to;
1996
+ const a = {
1926
1997
  at: now(),
1927
- ...t,
1928
- navigation: e
1998
+ ...s,
1999
+ navigation: n
1929
2000
  };
1930
- if (o !== undefined) i.route = o;
1931
- records.emit("graph", i, undefined);
2001
+ if (r !== undefined) a.route = r;
2002
+ records.emit("graph", a, undefined);
2003
+ if (o === false || r === undefined) return;
2004
+ // A navigation that stays on the route pattern continues the visit: the
2005
+ // page it kept outlived the move by design, so comparing it would read the
2006
+ // page itself as growth. Only arrivals that enter the route are compared —
2007
+ // a leak made during a visit is still alive when the next one starts. A
2008
+ // superseded navigation never showed, so the app is still where it was.
2009
+ const c = visitRoute;
2010
+ if (n.outcome !== "superseded") visitRoute = r;
1932
2011
  // The initial declaration is not a visit (it settles before route content
1933
2012
  // mounts — the same gate the feedback folds apply as `event.initial !== true`);
1934
2013
  // `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
1935
- if (n === false || o === undefined || e.initial) return;
1936
- let s = routeCounts.get(o);
1937
- if (s === undefined) routeCounts.set(o, s = []);
1938
- s.push(t);
1939
- if (s.length > n.visits) s.shift();
1940
- if (s.length < n.visits) return;
1941
- // Any series climbing on every visit to the ratio is growth; which ones
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;
2014
+ // A navigation opened before the check was on has no snapshot to compare.
2015
+ if (n.initial || r === c || i === undefined) return;
2016
+ let d = routeCounts.get(r);
2017
+ if (d === undefined) routeCounts.set(r, d = []);
2018
+ d.push({
2019
+ size: s,
2020
+ survived: i
1948
2021
  });
1949
- if (r.length === 0) return;
2022
+ if (d.length > o.visits) d.shift();
2023
+ if (d.length < o.visits) return;
2024
+ const f = d.map(e => e.size);
2025
+ const u = d.map(e => e.survived);
2026
+ const climbs = (e, n) => {
2027
+ for (let t = 1; t < e.length; t++) if (e[t][n] <= e[t - 1][n]) return false;
2028
+ return e[e.length - 1][n] >= e[0][n] * o.ratio;
2029
+ };
2030
+ // Any series climbing on every visit to the ratio — in the totals and in
2031
+ // what outlived each navigation — is growth; which ones did says what
2032
+ // leaked — computations with owners flat is an ownerless effect per visit,
2033
+ // edges alone is a subscription per visit to something long-lived, owners
2034
+ // is an undisposed root.
2035
+ const l = GRAPH_SERIES.filter(e => climbs(f, e) && climbs(u, e));
2036
+ if (l.length === 0) return;
1950
2037
  // The count is the whole graph's, so a leak shows at every route's settle;
1951
2038
  // the first route to complete its climb reports, naming the others seen.
1952
- const a = [ ...routeCounts.keys() ];
1953
- const c = r.map(e => `${e} ${s.map(n => n[e]).join(" → ")}`).join("; ");
1954
- const d = r.includes("owners") ? "a createRoot() in an effect or handler with no dispose, or a Portal or panel mounted per visit" : r.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";
1955
- const f = `[GRAPH_GROWTH] the live graph grew on ${n.visits} consecutive visits to ${o}: ${c} ` + `(${t.roots} roots)${a.length > 1 ? `, across visits to ${a.join(", ")}` : ""}. Each ` + `visit left something behind that the next did not reclaim — the shape says ${d}. 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.`;
1956
- const u = {
1957
- route: o,
1958
- grew: r,
1959
- history: s.map(e => ({
1960
- ...e
1961
- })),
1962
- roots: t.roots,
1963
- routes: a
2039
+ const h = [ ...routeCounts.keys() ];
2040
+ const join = (e, n) => e.map(e => e[n]).join(" → ");
2041
+ const p = l.map(e => `${e} ${join(u, e)} (of ${join(f, e)})`).join("; ");
2042
+ 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";
2043
+ 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. A cache that never evicts grows alike.`;
2044
+ const w = {
2045
+ route: r,
2046
+ grew: l,
2047
+ history: f,
2048
+ survived: u,
2049
+ roots: s.roots,
2050
+ routes: h
1964
2051
  };
1965
- if (e.interaction !== undefined) u.interaction = {
1966
- type: e.interaction.name,
1967
- target: e.interaction.target
2052
+ if (n.interaction !== undefined) w.interaction = {
2053
+ type: n.interaction.name,
2054
+ target: n.interaction.target
1968
2055
  };
1969
2056
  reportDiagnostic(emitDiagnostic({
1970
2057
  code: "GRAPH_GROWTH",
1971
2058
  kind: "perf",
1972
2059
  severity: "warn",
1973
- message: f,
1974
- data: u
2060
+ message: g,
2061
+ data: w
1975
2062
  }, null));
1976
2063
  // Judged once for the graph, not once per route: the next verdict needs
1977
2064
  // another `visits` climbing settles.
@@ -2088,6 +2175,14 @@ let navigationLog = [];
2088
2175
 
2089
2176
  /** Drains completed since enable() — the clock `writeDrain` reads. */ let drainSeq = 0;
2090
2177
 
2178
+ /**
2179
+ * Navigations settled in a landing, waiting for their graph count: the hook
2180
+ * fires before the landing's commits dispose the frame parked for the page
2181
+ * left, which the walk would still reach through the sources both pages read
2182
+ * (#3989). `land()` runs only inside `flush()`'s drain, so `flushEnd` counts
2183
+ * them, after those commits — the instant a one-drain navigation is counted.
2184
+ */ const landedGraphs = [];
2185
+
2091
2186
  /**
2092
2187
  * Copy what the router currently says onto the frame (what writes stamped —
2093
2188
  * `formatOrigin` reads it) and the event. Called when the frame opens, when a
@@ -2126,6 +2221,8 @@ function openNavigation(e, n) {
2126
2221
  held: false,
2127
2222
  writeDrain: drainSeq
2128
2223
  };
2224
+ // The initial declaration is never compared (see `trackGraph`).
2225
+ if (options.graphGrowth !== false && n.initial !== true) o.before = snapshotGraph();
2129
2226
  syncNavigation(o);
2130
2227
  navStates.set(e, o);
2131
2228
  openNavs.add(o);
@@ -2186,8 +2283,8 @@ function closeNavigation(e) {
2186
2283
 
2187
2284
  /** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */ function markNavigationsHeld(e) {
2188
2285
  if (openNavs.size === 0) return;
2189
- for (const n of e.nn) {
2190
- const e = n.ve?.origin;
2286
+ for (const n of e.je) {
2287
+ const e = n.ge?.origin;
2191
2288
  if (e?.kind !== "navigation") continue;
2192
2289
  const t = navStates.get(e);
2193
2290
  if (t !== undefined) t.held = true;
@@ -2196,14 +2293,14 @@ function closeNavigation(e) {
2196
2293
 
2197
2294
  /** transitionSettled: `t` commits — the navigations whose writes it staged are done. */ function settleNavigations(e, n) {
2198
2295
  if (openNavs.size === 0) return;
2199
- for (const t of e.nn) {
2200
- const e = t.ve?.origin;
2296
+ for (const t of e.je) {
2297
+ const e = t.ge?.origin;
2201
2298
  if (e?.kind !== "navigation") continue;
2202
2299
  const o = navStates.get(e);
2203
2300
  if (o === undefined || !openNavs.has(o)) continue;
2204
2301
  // A navigation whose frame is still open when its transition commits
2205
2302
  // (`until()` inside the frame) settles here too: its writes are through.
2206
- settleNavigation(o, o.held ? "held" : "committed", n);
2303
+ settleNavigation(o, o.held ? "held" : "committed", n, true);
2207
2304
  }
2208
2305
  }
2209
2306
 
@@ -2330,27 +2427,30 @@ function trackFallback(e, n, t, o) {
2330
2427
  drainSeq++;
2331
2428
  // The swaps this drain committed have rendered.
2332
2429
  displayFallbacks();
2430
+ // Landed first: they settled before anything this drain settles below.
2431
+ for (let e = 0; e < landedGraphs.length; e++) trackGraph(landedGraphs[e]);
2432
+ landedGraphs.length = 0;
2333
2433
  for (const e of openNavs) if (e.open === 0 && !e.held) settleNavigation(e, "committed");
2334
2434
  for (const e of openInteractions) maybeSettleInteraction(e);
2335
2435
  }
2336
2436
 
2337
- function settleNavigation(e, n, t) {
2437
+ function settleNavigation(e, n, t, o) {
2338
2438
  if (!openNavs.has(e)) return;
2339
2439
  openNavs.delete(e);
2340
2440
  syncNavigation(e);
2341
- const o = e.event;
2342
- o.settledMs = now() - o.at;
2343
- o.outcome = n;
2344
- if (t !== undefined) o.hold = t;
2441
+ const i = e.event;
2442
+ i.settledMs = now() - i.at;
2443
+ i.outcome = n;
2444
+ if (t !== undefined) i.hold = t;
2345
2445
  // The initial declaration is not a navigation the person waited on: its
2346
2446
  // `settledMs` is document start → router built, which would read as the
2347
2447
  // route's responsiveness in the feedback tables. Consumers get the record.
2348
- if (o.initial !== true) for (const e of folds) e.navigation?.(o);
2349
- records.emit("navigation", o, undefined);
2350
- trackGraph(o);
2448
+ if (i.initial !== true) for (const e of folds) e.navigation?.(i);
2449
+ records.emit("navigation", i, undefined);
2450
+ if (o) landedGraphs.push(e); else trackGraph(e);
2351
2451
  // The interaction that performed it may have been waiting only on this.
2352
- const i = openInteractionOf(o.interaction);
2353
- if (i !== undefined) maybeSettleInteraction(i);
2452
+ const s = openInteractionOf(i.interaction);
2453
+ if (s !== undefined) maybeSettleInteraction(s);
2354
2454
  }
2355
2455
 
2356
2456
  const interactionStates = new WeakMap;
@@ -2515,8 +2615,8 @@ function openInteraction(e, n, t) {
2515
2615
 
2516
2616
  /** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */ function markInteractionsHeld(e) {
2517
2617
  if (openInteractions.size === 0) return;
2518
- for (const n of e.nn) {
2519
- const t = openInteractionOf(n.ve?.origin);
2618
+ for (const n of e.je) {
2619
+ const t = openInteractionOf(n.ge?.origin);
2520
2620
  if (t !== undefined) {
2521
2621
  t.heldIn.add(e);
2522
2622
  t.held = true;
@@ -2603,7 +2703,7 @@ const engineHooks = {
2603
2703
  prevDeps: n || !wantsRerun() ? null : captureDeps(e),
2604
2704
  // Mirror recompute's own prev-value resolution: an earlier run in the
2605
2705
  // same flush may still be holding in _pendingValue.
2606
- prevValue: e.Ae !== NOT_PENDING ? e.Ae : e.pe,
2706
+ prevValue: e.ue !== NOT_PENDING ? e.ue : e.Ie,
2607
2707
  // A create run inherits the interaction of whatever is building it: the
2608
2708
  // enclosing recompute (a parent's fn creating children) or, at the top
2609
2709
  // of the recompute stack, the effect callback / handler frame.
@@ -2630,7 +2730,7 @@ const engineHooks = {
2630
2730
  // exempt — a side-effect-only compute's work IS its effect phase, and
2631
2731
  // identity of `undefined` proves nothing.
2632
2732
  if (t && r.causes !== null && e.i) {
2633
- const n = e.Ae !== NOT_PENDING ? e.Ae : e.pe;
2733
+ const n = e.ue !== NOT_PENDING ? e.ue : e.Ie;
2634
2734
  if (n !== undefined && n === r.prevValue) t = false;
2635
2735
  }
2636
2736
  // Unstable-output check: memos only, non-create, plain runs with a
@@ -2638,12 +2738,12 @@ const engineHooks = {
2638
2738
  // plain-flush memo commits and in `_value` for direct ones. Overlay runs
2639
2739
  // are excluded — an optimistic re-derive legitimately produces fresh
2640
2740
  // equivalents while the lane settles.
2641
- if (r.causes !== null && t && !o && !i && !e.i) checkUnstableOutput(e, r.prevValue, e.Ae !== NOT_PENDING ? e.Ae : e.pe);
2741
+ if (r.causes !== null && t && !o && !i && !e.i) checkUnstableOutput(e, r.prevValue, e.ue !== NOT_PENDING ? e.ue : e.Ie);
2642
2742
  // The same exemption for memos: a run that committed `undefined` did its
2643
2743
  // work as a side effect (a projection's draft writes or reconcile, a
2644
2744
  // write from inside a memo), so core's `undefined === undefined` gate
2645
2745
  // proves nothing. Reported as changed, so no consumer counts it as waste.
2646
- if (!t && (e.Ae !== NOT_PENDING ? e.Ae : e.pe) === undefined) t = true;
2746
+ if (!t && (e.ue !== NOT_PENDING ? e.ue : e.Ie) === undefined) t = true;
2647
2747
  if (r.causes !== null) recordRerun(e, r, {
2648
2748
  selfMs: c,
2649
2749
  totalMs: a
@@ -2655,7 +2755,7 @@ const engineHooks = {
2655
2755
  noteInteractionRun(r.interaction, c, true);
2656
2756
  // The first effect callback runs with no re-run record to inherit from;
2657
2757
  // hand it the interaction that built the node (see effectRunStart).
2658
- e.xe = r.interaction;
2758
+ e.Me = r.interaction;
2659
2759
  if (openFlush !== null) noteFlushRun(openFlush, true, r.interaction);
2660
2760
  if (records.observed("create")) {
2661
2761
  const n = {
@@ -2685,9 +2785,9 @@ const engineHooks = {
2685
2785
  trackFlightStart(e, n);
2686
2786
  },
2687
2787
  asyncStart(e) {
2688
- asyncStartSeq = e.ve?.seq ?? 0;
2689
- asyncStartTime = e.Ne;
2690
- asyncStartValue = e.pe;
2788
+ asyncStartSeq = e.ge?.seq ?? 0;
2789
+ asyncStartTime = e.ie;
2790
+ asyncStartValue = e.Ie;
2691
2791
  },
2692
2792
  asyncEnd(e, n, t, o) {
2693
2793
  if (o) {
@@ -2697,7 +2797,7 @@ const engineHooks = {
2697
2797
  // (or `_time`, for a same-reference commit under `equals: false`), and
2698
2798
  // a transition hold parks the value in `_pendingValue`. A landing the
2699
2799
  // equality gate swallowed moves none of them and must leave no stamp.
2700
- const o = e.pe !== asyncStartValue || e.Ne !== asyncStartTime || e.Ae === t;
2800
+ const o = e.Ie !== asyncStartValue || e.ie !== asyncStartTime || e.ue === t;
2701
2801
  if (o) stampWrite(e, "async", n === undefined ? NO_VALUES : n, t);
2702
2802
  // Flight over either way — an equality-swallowed landing still spent
2703
2803
  // the wall time (finalizeFlight only chains through a fresh stamp).
@@ -2707,12 +2807,12 @@ const engineHooks = {
2707
2807
  // Landed through setSignal: reclassify its "write" stamp as an async
2708
2808
  // landing — but only if it actually stamped (the value changed) since
2709
2809
  // asyncStart; a no-change landing must leave no fresh stamp behind.
2710
- const i = e.ve;
2810
+ const i = e.ge;
2711
2811
  if (i !== undefined && i.seq > asyncStartSeq && i.kind === "write") stampWrite(e, "async", NO_VALUES, t);
2712
2812
  finalizeFlight(e);
2713
2813
  },
2714
2814
  effectRunStart(e) {
2715
- pushFrame("effect", nodeName(e), e.xe, e);
2815
+ pushFrame("effect", nodeName(e), e.Me, e);
2716
2816
  // The frame's own `at` doubles as the record's start: set only while
2717
2817
  // listened, so a listener arriving mid-callback finds no start and the
2718
2818
  // end emits nothing for it — and an unlistened callback pays no clock read.
@@ -2816,8 +2916,10 @@ const holds = [];
2816
2916
  stagedFallbacks.delete(DRAIN);
2817
2917
  trackingGen++;
2818
2918
  openNavs.clear();
2919
+ landedGraphs.length = 0;
2819
2920
  openInteractions.clear();
2820
2921
  routeCounts.clear();
2922
+ visitRoute = undefined;
2821
2923
  drainSeq = 0;
2822
2924
  originFrames.length = 0;
2823
2925
  effectStack.length = 0;
@@ -2965,11 +3067,10 @@ const attribution = {
2965
3067
  // The buffers are typed by their variable; the switch is the proof.
2966
3068
  return n;
2967
3069
  },
2968
- markFlight(e, n = now()) {
2969
- // Earliest wins: re-marking (a cache re-serving the same promise) must
2970
- // not move the origin later.
2971
- const t = flightOrigins.get(e);
2972
- if (t === undefined || n < t) flightOrigins.set(e, n);
3070
+ markFlight(e, n) {
3071
+ // Earliest wins (the slot's rule): re-marking (a cache re-serving the
3072
+ // same promise) must not move the origin later.
3073
+ OBSERVE.attribution.markFlight(e, n);
2973
3074
  }
2974
3075
  };
2975
3076