@solidjs/signals 2.0.0-rc.13 → 2.0.0-rc.15

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