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

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 (118) hide show
  1. package/dist/dev-shared.js +2000 -3768
  2. package/dist/dev.attribution.js +132 -79
  3. package/dist/dev.js +7226 -8488
  4. package/dist/observe/affects.js +125 -104
  5. package/dist/observe/boundaries.js +458 -665
  6. package/dist/observe/core/action.js +63 -66
  7. package/dist/observe/core/async.js +240 -383
  8. package/dist/observe/core/attribution-queries.js +1 -1
  9. package/dist/observe/core/attribution.js +240 -183
  10. package/dist/observe/core/constants.js +115 -187
  11. package/dist/observe/core/context.js +3 -3
  12. package/dist/observe/core/core.js +840 -1438
  13. package/dist/observe/core/dev.js +3 -3
  14. package/dist/observe/core/effect.js +75 -86
  15. package/dist/observe/core/external.js +5 -5
  16. package/dist/observe/core/graph.js +61 -70
  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 +704 -181
  20. package/dist/observe/core/owner.js +80 -101
  21. package/dist/observe/core/scheduler.js +708 -1351
  22. package/dist/observe/core/verdict.js +302 -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 +133 -215
  28. package/dist/observe/store/affects.js +135 -0
  29. package/dist/observe/store/index.js +29 -24
  30. package/dist/observe/store/optimistic.js +538 -0
  31. package/dist/observe/store/projection.js +302 -0
  32. package/dist/observe/store/reconcile.js +257 -0
  33. package/dist/observe/store/store.js +1945 -277
  34. package/dist/observe/store/storePath.js +1 -1
  35. package/dist/{prod/store/next → observe/store}/target.js +17 -18
  36. package/dist/observe/store/types.js +73 -0
  37. package/dist/observe/store/utils.js +1 -13
  38. package/dist/prod/affects.js +127 -106
  39. package/dist/prod/boundaries.js +442 -654
  40. package/dist/prod/core/action.js +71 -74
  41. package/dist/prod/core/async.js +307 -442
  42. package/dist/prod/core/constants.js +112 -186
  43. package/dist/prod/core/context.js +3 -3
  44. package/dist/prod/core/core.js +852 -1458
  45. package/dist/prod/core/effect.js +88 -99
  46. package/dist/prod/core/external.js +5 -5
  47. package/dist/prod/core/graph.js +61 -70
  48. package/dist/prod/core/heap.js +75 -98
  49. package/dist/prod/core/invariants.js +1 -32
  50. package/dist/prod/core/lanes.js +666 -181
  51. package/dist/prod/core/owner.js +75 -96
  52. package/dist/prod/core/scheduler.js +677 -1332
  53. package/dist/prod/core/verdict.js +297 -621
  54. package/dist/prod/flatten.js +77 -0
  55. package/dist/prod/index.js +15 -13
  56. package/dist/prod/map.js +143 -146
  57. package/dist/prod/reveal.js +212 -0
  58. package/dist/prod/signals.js +144 -226
  59. package/dist/prod/store/affects.js +135 -0
  60. package/dist/prod/store/index.js +28 -19
  61. package/dist/prod/store/optimistic.js +533 -0
  62. package/dist/prod/store/projection.js +295 -0
  63. package/dist/prod/store/reconcile.js +257 -0
  64. package/dist/prod/store/store.js +1840 -272
  65. package/dist/prod/store/storePath.js +1 -1
  66. package/dist/{observe/store/next → prod/store}/target.js +13 -17
  67. package/dist/prod/store/types.js +73 -0
  68. package/dist/prod/store/utils.js +1 -11
  69. package/dist/types/affects.d.ts +11 -30
  70. package/dist/types/boundaries.d.ts +62 -176
  71. package/dist/types/carved.d.ts +1 -0
  72. package/dist/types/core/action.d.ts +5 -6
  73. package/dist/types/core/async.d.ts +10 -5
  74. package/dist/types/core/attribution-hooks.d.ts +20 -1
  75. package/dist/types/core/attribution.d.ts +20 -9
  76. package/dist/types/core/constants.d.ts +125 -180
  77. package/dist/types/core/core.d.ts +104 -264
  78. package/dist/types/core/dev.d.ts +2 -2
  79. package/dist/types/core/effect.d.ts +0 -7
  80. package/dist/types/core/graph.d.ts +7 -1
  81. package/dist/types/core/heap.d.ts +5 -7
  82. package/dist/types/core/index.d.ts +5 -5
  83. package/dist/types/core/invariants.d.ts +0 -29
  84. package/dist/types/core/lanes.d.ts +65 -87
  85. package/dist/types/core/owner.d.ts +3 -3
  86. package/dist/types/core/scheduler.d.ts +249 -331
  87. package/dist/types/core/types.d.ts +51 -85
  88. package/dist/types/core/verdict.d.ts +3 -0
  89. package/dist/types/flatten.d.ts +8 -0
  90. package/dist/types/index.d.ts +6 -4
  91. package/dist/types/reveal.d.ts +19 -0
  92. package/dist/types/signals.d.ts +35 -91
  93. package/dist/types/store/affects.d.ts +1 -0
  94. package/dist/types/store/index.d.ts +35 -15
  95. package/dist/types/store/optimistic.d.ts +23 -0
  96. package/dist/types/store/projection.d.ts +18 -0
  97. package/dist/types/store/reconcile.d.ts +3 -0
  98. package/dist/types/store/store.d.ts +165 -143
  99. package/dist/types/store/storePath.d.ts +1 -1
  100. package/dist/types/store/target.d.ts +151 -0
  101. package/dist/types/store/types.d.ts +84 -0
  102. package/package.json +1 -1
  103. package/dist/observe/core/optimistic.js +0 -669
  104. package/dist/observe/store/next/optimistic.js +0 -717
  105. package/dist/observe/store/next/projection.js +0 -266
  106. package/dist/observe/store/next/reconcile.js +0 -375
  107. package/dist/observe/store/next/store.js +0 -2608
  108. package/dist/prod/core/optimistic.js +0 -658
  109. package/dist/prod/store/next/optimistic.js +0 -710
  110. package/dist/prod/store/next/projection.js +0 -257
  111. package/dist/prod/store/next/reconcile.js +0 -373
  112. package/dist/prod/store/next/store.js +0 -2484
  113. package/dist/types/core/optimistic.d.ts +0 -6
  114. package/dist/types/store/next/optimistic.d.ts +0 -25
  115. package/dist/types/store/next/projection.d.ts +0 -27
  116. package/dist/types/store/next/reconcile.d.ts +0 -10
  117. package/dist/types/store/next/store.d.ts +0 -145
  118. 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.u & 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.me === undefined) n.me = isExcluded(e);
24
+ return n.me;
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.i ? "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.ge
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.we !== undefined) i.run = e.we;
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.l) {
462
+ o++;
463
+ if (!(n.o.u & 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.ve;
487
+ e.ve = 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.ve = {
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.ke ?? 0;
503
514
  const t = [];
504
- const o = e.ze;
515
+ const o = e.ve;
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._; o !== null; o = o.p) {
518
+ const e = o.I.ve;
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.ke = 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._; t !== null; t = t.p) {
538
+ n.push(t.I);
539
+ if (t === e.S) 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._; n !== null; n = n.p) {
559
+ // HMR plumbing is a dev-only hop the scope cannot read past: not a source.
560
+ if (!(n.I.u & CONFIG_PLUMBING)) {
561
+ t++;
562
+ if (o.length < 12) o.push(nodeName(n.I));
563
+ }
564
+ if (n === e.S) 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.ye ?? 0) * 1.5) return;
569
+ i.ye = 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.$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;
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.be;
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.be} times ` + `in ${Math.max(1, i - o.$e)}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.be,
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.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;
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.Ce.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.Ce,
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.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;
700
721
  if (!o) {
701
- a.un = a.un + 1;
702
- a.ln = a.ln + i;
722
+ a.Ge = a.Ge + 1;
723
+ a.qe = a.qe + 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.Be || a.We < r.minRuns || a.Ge / a.We < r.ratio || a.qe < r.budgetMs) return;
726
+ a.Be = true;
706
727
  const c = {
707
- runs: a.fn,
708
- wasted: a.un,
709
- wastedMs: a.ln
728
+ runs: a.We,
729
+ wasted: a.Ge,
730
+ wastedMs: a.qe
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.ge;
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.xe = d;
759
+ a.we = undefined;
760
+ a.ge = 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.Ke = (a.Ke ?? 0) + 1;
770
+ a.xe = d;
771
+ a.we = f;
772
+ a.ge = 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.Ve = 0;
914
+ i.je = 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.Ve = (i.Ve ?? 0) + 1;
918
+ if (i.je || i.Ve < o) return;
919
+ i.je = 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.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.`;
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.Ve,
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.Ye = o.Ye === undefined || o.Ye === r ? r : null;
1069
+ if (s !== undefined && t !== undefined && t === s.node.pe) {
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.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;
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._; n !== null; n = n.p) if (n.I.pe === e.pe) 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.Ye === 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.i ? "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.Qe ?? 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.ve;
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.Ze ?? 0) >= i) return;
1452
+ r.Ze = 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.u & (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.Je?.(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.l) {
1532
+ const e = n.o;
1533
+ if (e.i) 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.l) {
1557
+ const s = t.o;
1558
+ if (s.u & CONFIG_VERDICT) {
1559
+ const t = s.en ?? 0;
1560
+ const o = s.i ? 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.nn) {
1632
+ if (typeof n.re === "function" || isCompanion(n)) continue;
1633
+ const e = n.ve;
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.nn, ...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.tn;
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
@@ -1846,33 +1875,33 @@ function checkLongHold(e, n) {
1846
1875
  const meet = e => {
1847
1876
  if (t.has(e)) return;
1848
1877
  t.add(e);
1849
- if (typeof e.Se === "function") n.computations++; else n.signals++;
1878
+ if (typeof e.re === "function") n.computations++; else n.signals++;
1850
1879
  o.push(e);
1851
1880
  };
1852
1881
  for (const t of e) {
1853
1882
  n.owners++;
1854
1883
  // Iterative: a deep tree must not grow the call stack.
1855
- let e = t.On;
1884
+ let e = t.sn;
1856
1885
  for (;;) {
1857
1886
  while (e !== null) {
1858
1887
  n.owners++;
1859
- if (typeof e.Se === "function") meet(e);
1860
- if (e.On !== null) i.push(e);
1861
- e = e._n;
1888
+ if (typeof e.re === "function") meet(e);
1889
+ if (e.sn !== null) i.push(e);
1890
+ e = e.rn;
1862
1891
  }
1863
1892
  const t = i.pop();
1864
1893
  if (t === undefined) break;
1865
- e = t.On;
1894
+ e = t.sn;
1866
1895
  }
1867
1896
  }
1868
1897
  for (let e = 0; e < o.length; e++) {
1869
1898
  const t = o[e];
1870
1899
  // Whoever reads this node: owned computations are already known; an
1871
1900
  // 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) {
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) {
1874
1903
  n.edges++;
1875
- meet(e.De);
1904
+ meet(e.I);
1876
1905
  }
1877
1906
  }
1878
1907
  return n;
@@ -1900,7 +1929,10 @@ function trackGraph(e) {
1900
1929
  };
1901
1930
  if (o !== undefined) i.route = o;
1902
1931
  records.emit("graph", i, undefined);
1903
- if (n === false || o === undefined) return;
1932
+ // The initial declaration is not a visit (it settles before route content
1933
+ // mounts — the same gate the feedback folds apply as `event.initial !== true`);
1934
+ // `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
1935
+ if (n === false || o === undefined || e.initial) return;
1904
1936
  let s = routeCounts.get(o);
1905
1937
  if (s === undefined) routeCounts.set(o, s = []);
1906
1938
  s.push(t);
@@ -1953,10 +1985,13 @@ function trackGraph(e) {
1953
1985
  * One source abandoning flight after flight inside a window: every input
1954
1986
  * asked again and the earlier answers were thrown away — the
1955
1987
  * request-per-keystroke signature `feedback().flights[].abandoned` counts,
1956
- * as a finding while it happens. Warned once per window per source.
1988
+ * as a finding while it happens. Warned once per window per source. A
1989
+ * framework node superseding its own flights by design (CONFIG_SUPERSEDES)
1990
+ * is not judged; its flights are still recorded.
1957
1991
  */ function checkAbandonedFlights(e, n) {
1958
1992
  const t = options.abandonedFlights;
1959
- if (t === false || excludedNode(e)) return;
1993
+ // Only dev sets CONFIG_SUPERSEDES (see constants.ts).
1994
+ if (t === false || false || excludedNode(e)) return;
1960
1995
  const o = Date.now();
1961
1996
  let i = abandonWindows.get(e);
1962
1997
  if (i === undefined || o - i.start > t.windowMs) {
@@ -2151,8 +2186,8 @@ function closeNavigation(e) {
2151
2186
 
2152
2187
  /** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */ function markNavigationsHeld(e) {
2153
2188
  if (openNavs.size === 0) return;
2154
- for (const n of e.In) {
2155
- const e = n.ze?.origin;
2189
+ for (const n of e.nn) {
2190
+ const e = n.ve?.origin;
2156
2191
  if (e?.kind !== "navigation") continue;
2157
2192
  const t = navStates.get(e);
2158
2193
  if (t !== undefined) t.held = true;
@@ -2161,8 +2196,8 @@ function closeNavigation(e) {
2161
2196
 
2162
2197
  /** transitionSettled: `t` commits — the navigations whose writes it staged are done. */ function settleNavigations(e, n) {
2163
2198
  if (openNavs.size === 0) return;
2164
- for (const t of e.In) {
2165
- const e = t.ze?.origin;
2199
+ for (const t of e.nn) {
2200
+ const e = t.ve?.origin;
2166
2201
  if (e?.kind !== "navigation") continue;
2167
2202
  const o = navStates.get(e);
2168
2203
  if (o === undefined || !openNavs.has(o)) continue;
@@ -2320,12 +2355,14 @@ function settleNavigation(e, n, t) {
2320
2355
 
2321
2356
  const interactionStates = new WeakMap;
2322
2357
 
2358
+ /** `InteractionRef.event` → the record its frames join (while `joinable`). Weak: the Event is never held. */ const eventInteractions = new WeakMap;
2359
+
2323
2360
  /** Opened, not yet settled. */ const openInteractions = new Set;
2324
2361
 
2325
2362
  let interactionLog = [];
2326
2363
 
2327
- function openInteraction(e, n) {
2328
- const t = {
2364
+ function openInteraction(e, n, t) {
2365
+ const o = {
2329
2366
  name: e.name,
2330
2367
  at: e.at,
2331
2368
  handlerMs: 0,
@@ -2337,26 +2374,37 @@ function openInteraction(e, n) {
2337
2374
  navigations: [],
2338
2375
  origin: e
2339
2376
  };
2340
- if (e.target !== undefined) t.target = e.target;
2377
+ if (e.target !== undefined) o.target = e.target;
2341
2378
  // The runtime may date the frame from the event's own timestamp (before
2342
2379
  // any queued task ran); the gap to here is the input delay the browser's
2343
2380
  // INP counts first.
2344
- if (n > t.at) t.inputDelayMs = n - t.at;
2345
- const o = {
2346
- event: t,
2381
+ if (n > o.at) o.inputDelayMs = n - o.at;
2382
+ const i = {
2383
+ event: o,
2347
2384
  open: true,
2348
2385
  opened: n,
2349
2386
  writeDrain: drainSeq,
2350
2387
  heldIn: new Set,
2351
2388
  held: false,
2352
2389
  excludedWrites: 0,
2353
- awaiting: false,
2354
- actioned: false
2390
+ awaiting: 0,
2391
+ actioned: false,
2392
+ joinable: t !== undefined
2355
2393
  };
2356
- interactionStates.set(e, o);
2357
- openInteractions.add(o);
2358
- interactionLog.push(t);
2394
+ interactionStates.set(e, i);
2395
+ openInteractions.add(i);
2396
+ interactionLog.push(o);
2359
2397
  if (interactionLog.length > options.historyLimit) interactionLog.shift();
2398
+ if (t !== undefined) {
2399
+ eventInteractions.set(t, i);
2400
+ // A microtask is too early: native dispatch runs microtasks between
2401
+ // listeners. The callback must not close over `key`: a throttled timer
2402
+ // would hold the Event strongly long past its dispatch.
2403
+ setTimeout(() => {
2404
+ i.joinable = false;
2405
+ if (openInteractions.has(i)) maybeSettleInteraction(i, i.settleAt);
2406
+ });
2407
+ }
2360
2408
  }
2361
2409
 
2362
2410
  /**
@@ -2377,19 +2425,19 @@ function openInteraction(e, n) {
2377
2425
  // screen could have shown the wait.
2378
2426
  const i = n !== null && (typeof n === "object" || typeof n === "function") && typeof n.then === "function" ? n : null;
2379
2427
  if (i !== null) {
2380
- t.awaiting = true;
2428
+ t.awaiting++;
2381
2429
  let e = false;
2382
2430
  let n;
2383
2431
  const settle = () => {
2384
2432
  if (e) return;
2385
2433
  e = true;
2386
2434
  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;
2435
+ if (!openInteractions.has(t) || --t.awaiting > 0) return;
2436
+ const o = now();
2437
+ // From the last handler's return: the frames of one event share a record.
2438
+ t.event.continuationMs = o - (t.opened + t.event.handlerMs);
2391
2439
  checkUntrackedAsyncHandler(t);
2392
- maybeSettleInteraction(t, i);
2440
+ maybeSettleInteraction(t, o);
2393
2441
  };
2394
2442
  n = setTimeout(settle, ASYNC_HANDLER_CAP_MS);
2395
2443
  // A pending cap must not hold a Node process (tests, SSR harnesses) open.
@@ -2467,8 +2515,8 @@ function openInteraction(e, n) {
2467
2515
 
2468
2516
  /** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */ function markInteractionsHeld(e) {
2469
2517
  if (openInteractions.size === 0) return;
2470
- for (const n of e.In) {
2471
- const t = openInteractionOf(n.ze?.origin);
2518
+ for (const n of e.nn) {
2519
+ const t = openInteractionOf(n.ve?.origin);
2472
2520
  if (t !== undefined) {
2473
2521
  t.heldIn.add(e);
2474
2522
  t.held = true;
@@ -2491,12 +2539,16 @@ function openInteraction(e, n) {
2491
2539
 
2492
2540
  function maybeSettleInteraction(e, n = now()) {
2493
2541
  const t = e.event;
2494
- if (e.open || e.awaiting || e.heldIn.size > 0) return;
2542
+ if (e.open || e.awaiting > 0 || e.heldIn.size > 0) return;
2495
2543
  // A drain must have committed the last write (the handler's, or a redirect
2496
2544
  // hop's after the click's own drain) — the handler returning is not the
2497
2545
  // screen having it.
2498
2546
  if (t.writes > 0 && drainSeq <= e.writeDrain) return;
2499
2547
  for (const e of t.navigations) if (e.outcome === undefined) return;
2548
+ if (e.joinable) {
2549
+ e.settleAt = n;
2550
+ return;
2551
+ }
2500
2552
  openInteractions.delete(e);
2501
2553
  // Every write went to an excluded subject and nothing of the app's ran: the
2502
2554
  // click was on the observer's own UI (a devtools panel's button). Not a
@@ -2551,7 +2603,7 @@ const engineHooks = {
2551
2603
  prevDeps: n || !wantsRerun() ? null : captureDeps(e),
2552
2604
  // Mirror recompute's own prev-value resolution: an earlier run in the
2553
2605
  // same flush may still be holding in _pendingValue.
2554
- prevValue: e._e !== NOT_PENDING ? e._e : e.ce,
2606
+ prevValue: e.Ae !== NOT_PENDING ? e.Ae : e.pe,
2555
2607
  // A create run inherits the interaction of whatever is building it: the
2556
2608
  // enclosing recompute (a parent's fn creating children) or, at the top
2557
2609
  // of the recompute stack, the effect callback / handler frame.
@@ -2577,8 +2629,8 @@ const engineHooks = {
2577
2629
  // committed compute output is an unchanged run. `undefined` outputs are
2578
2630
  // exempt — a side-effect-only compute's work IS its effect phase, and
2579
2631
  // identity of `undefined` proves nothing.
2580
- if (t && r.causes !== null && e.He) {
2581
- const n = e._e !== NOT_PENDING ? e._e : e.ce;
2632
+ if (t && r.causes !== null && e.i) {
2633
+ const n = e.Ae !== NOT_PENDING ? e.Ae : e.pe;
2582
2634
  if (n !== undefined && n === r.prevValue) t = false;
2583
2635
  }
2584
2636
  // Unstable-output check: memos only, non-create, plain runs with a
@@ -2586,7 +2638,12 @@ const engineHooks = {
2586
2638
  // plain-flush memo commits and in `_value` for direct ones. Overlay runs
2587
2639
  // are excluded — an optimistic re-derive legitimately produces fresh
2588
2640
  // 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);
2641
+ if (r.causes !== null && t && !o && !i && !e.i) checkUnstableOutput(e, r.prevValue, e.Ae !== NOT_PENDING ? e.Ae : e.pe);
2642
+ // The same exemption for memos: a run that committed `undefined` did its
2643
+ // work as a side effect (a projection's draft writes or reconcile, a
2644
+ // write from inside a memo), so core's `undefined === undefined` gate
2645
+ // 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;
2590
2647
  if (r.causes !== null) recordRerun(e, r, {
2591
2648
  selfMs: c,
2592
2649
  totalMs: a
@@ -2598,7 +2655,7 @@ const engineHooks = {
2598
2655
  noteInteractionRun(r.interaction, c, true);
2599
2656
  // The first effect callback runs with no re-run record to inherit from;
2600
2657
  // hand it the interaction that built the node (see effectRunStart).
2601
- e.pn = r.interaction;
2658
+ e.xe = r.interaction;
2602
2659
  if (openFlush !== null) noteFlushRun(openFlush, true, r.interaction);
2603
2660
  if (records.observed("create")) {
2604
2661
  const n = {
@@ -2628,9 +2685,9 @@ const engineHooks = {
2628
2685
  trackFlightStart(e, n);
2629
2686
  },
2630
2687
  asyncStart(e) {
2631
- asyncStartSeq = e.ze?.seq ?? 0;
2632
- asyncStartTime = e.Ge;
2633
- asyncStartValue = e.ce;
2688
+ asyncStartSeq = e.ve?.seq ?? 0;
2689
+ asyncStartTime = e.Ne;
2690
+ asyncStartValue = e.pe;
2634
2691
  },
2635
2692
  asyncEnd(e, n, t, o) {
2636
2693
  if (o) {
@@ -2640,7 +2697,7 @@ const engineHooks = {
2640
2697
  // (or `_time`, for a same-reference commit under `equals: false`), and
2641
2698
  // a transition hold parks the value in `_pendingValue`. A landing the
2642
2699
  // equality gate swallowed moves none of them and must leave no stamp.
2643
- const o = e.ce !== asyncStartValue || e.Ge !== asyncStartTime || e._e === t;
2700
+ const o = e.pe !== asyncStartValue || e.Ne !== asyncStartTime || e.Ae === t;
2644
2701
  if (o) stampWrite(e, "async", n === undefined ? NO_VALUES : n, t);
2645
2702
  // Flight over either way — an equality-swallowed landing still spent
2646
2703
  // the wall time (finalizeFlight only chains through a fresh stamp).
@@ -2650,12 +2707,12 @@ const engineHooks = {
2650
2707
  // Landed through setSignal: reclassify its "write" stamp as an async
2651
2708
  // landing — but only if it actually stamped (the value changed) since
2652
2709
  // asyncStart; a no-change landing must leave no fresh stamp behind.
2653
- const i = e.ze;
2710
+ const i = e.ve;
2654
2711
  if (i !== undefined && i.seq > asyncStartSeq && i.kind === "write") stampWrite(e, "async", NO_VALUES, t);
2655
2712
  finalizeFlight(e);
2656
2713
  },
2657
2714
  effectRunStart(e) {
2658
- pushFrame("effect", nodeName(e), e.pn, e);
2715
+ pushFrame("effect", nodeName(e), e.xe, e);
2659
2716
  // The frame's own `at` doubles as the record's start: set only while
2660
2717
  // listened, so a listener arriving mid-callback finds no start and the
2661
2718
  // end emits nothing for it — and an unlistened callback pays no clock read.