@solidjs/signals 2.0.0-rc.11 → 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 +2056 -3773
  2. package/dist/dev.attribution.js +159 -91
  3. package/dist/dev.js +7263 -8384
  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 +271 -199
  10. package/dist/observe/core/constants.js +115 -178
  11. package/dist/observe/core/context.js +3 -3
  12. package/dist/observe/core/core.js +840 -1428
  13. package/dist/observe/core/dev.js +41 -18
  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 -177
  43. package/dist/prod/core/context.js +3 -3
  44. package/dist/prod/core/core.js +852 -1448
  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 +48 -1
  75. package/dist/types/core/attribution.d.ts +29 -9
  76. package/dist/types/core/constants.d.ts +125 -171
  77. package/dist/types/core/core.d.ts +104 -264
  78. package/dist/types/core/dev.d.ts +60 -7
  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 +6 -6
  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 +7 -5
  91. package/dist/types/reveal.d.ts +19 -0
  92. package/dist/types/signals.d.ts +53 -92
  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 -2480
  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 -2356
  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 -143
  118. package/dist/types/store/next/target.d.ts +0 -196
@@ -1,30 +1,37 @@
1
1
  import {
2
- bL as OBSERVE,
3
- bV as setAttributionHooks,
4
- bW as liveRootOwners,
5
- b as NOT_PENDING,
6
- ay as emitDiagnostic,
7
- bX as ownerPath,
8
- ax as reportDiagnostic,
9
- bY as isSuppressed,
10
- bZ as CONFIG_PLUMBING,
11
- b_ as anyExcluded,
12
- b$ as isExcluded,
13
- q as CONFIG_DERIVED_OVERRIDE,
14
- c0 as GRAPH_SIZE_WARN_AT,
15
- c1 as noteFanOut,
16
- aV as $REFRESH
2
+ br as OBSERVE,
3
+ bB as setAttributionHooks,
4
+ bC as liveRootOwners,
5
+ N as NOT_PENDING,
6
+ an as emitDiagnostic,
7
+ bD as ownerPath,
8
+ aB as reportDiagnostic,
9
+ bE as isSuppressed,
10
+ bF as blockersOf,
11
+ bG as CONFIG_PLUMBING,
12
+ bH as anyExcluded,
13
+ bI as isExcluded,
14
+ bJ as CONFIG_WIDE,
15
+ a as CONFIG_OVERRIDE,
16
+ C as CONFIG_GUESS,
17
+ G as GlobalQueue,
18
+ b as CONFIG_VERDICT,
19
+ bK as GRAPH_SIZE_WARN_AT,
20
+ bL as noteFanOut,
21
+ bM as CONFIG_SUPERSEDES,
22
+ aT as $REFRESH
17
23
  } from "./dev-shared.js";
18
24
 
19
25
  /** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */
20
26
  const FALLBACK_FLASH_MS = 150;
21
27
  /**
22
- * Under an owner the observer marked as its own (`OBSERVE.exclude`), or
23
- * framework plumbing itself (`CONFIG_PLUMBING` — the HMR memo between a
24
- * component's root and its body, which is nobody's node): the engine records
25
- * nothing about the node. Plumbing is a bit read and excludes the node alone,
26
- * not what it owns; the observer exclusion is cached per node once any
27
- * exists, before that a flag read.
28
+ * Under an owner the observer marked as its own (`OBSERVE.exclude`, and not
29
+ * re-admitted by a nearer `OBSERVE.include`), or framework plumbing itself
30
+ * (`CONFIG_PLUMBING` — the HMR memo between a component's root and its
31
+ * body, which is nobody's node): the engine records nothing about the node.
32
+ * Plumbing is a bit read and excludes the node alone, not what it owns; the
33
+ * observer exclusion is cached per node once any exists, before that a flag
34
+ * read.
28
35
  */
29
36
  function excludedNode(el) {
30
37
  if ((el._config & CONFIG_PLUMBING) !== 0) return true;
@@ -47,7 +54,7 @@ const defaultOptions = {
47
54
  stacks: false,
48
55
  historyLimit: 200,
49
56
  hotRuns: { count: 120, windowMs: 1000 },
50
- wideDeps: 30,
57
+ wideDeps: 200,
51
58
  hotTime: { budgetMs: 8, windowMs: 1000 },
52
59
  unstableMemos: 4,
53
60
  wastedRecompute: { minRuns: 5, ratio: 0.8, budgetMs: 2, windowMs: 1000 },
@@ -191,13 +198,20 @@ function targetLabel(target) {
191
198
  }
192
199
  function interactionStart(ref) {
193
200
  interactionStack.push(currentInteraction);
201
+ // No `ref.event`: the WeakMap answers undefined.
202
+ const joined = eventInteractions.get(ref.event);
203
+ if (joined?.joinable && openInteractions.has(joined)) {
204
+ joined.open = true;
205
+ currentInteraction = joined.event.origin;
206
+ return;
207
+ }
194
208
  // One clock read: the frame opens now; the interaction began at `ref.at`
195
209
  // when the runtime dated it (the event's own timestamp), else now too.
196
210
  const opened = now();
197
211
  const origin = { kind: "interaction", name: ref.type, at: ref.at ?? opened };
198
212
  if (ref.target) origin.target = targetLabel(ref.target);
199
213
  currentInteraction = origin;
200
- openInteraction(origin, opened);
214
+ openInteraction(origin, opened, ref.event);
201
215
  }
202
216
  function interactionEnd(returned) {
203
217
  const closing = currentInteraction;
@@ -333,9 +347,16 @@ function originStart(ref) {
333
347
  return;
334
348
  }
335
349
  }
336
- const frame = { kind: ref.kind, at: ref.at ?? now() };
337
- if (ref.from !== undefined) frame.from = ref.from;
338
- const under = enclosingInteraction();
350
+ // The initial navigation is the document's: its request is the time origin
351
+ // unless the router says otherwise, and it left nowhere.
352
+ const initial = ref.initial === true;
353
+ const frame = { kind: ref.kind, at: ref.at ?? (initial ? 0 : now()) };
354
+ if (!initial && ref.from !== undefined) frame.from = ref.from;
355
+ // Declared beats ambient: a router that awaited before writing hands back
356
+ // the origin it captured in the request (`undefined` when the request ran
357
+ // under none — still a declaration); what is on the stack now is whatever
358
+ // happened to be running.
359
+ const under = "interaction" in ref ? interactionOf(ref.interaction) : enclosingInteraction();
339
360
  if (under !== undefined) frame.interaction = under;
340
361
  originFrames.push(frame);
341
362
  openNavigation(frame, ref);
@@ -369,19 +390,14 @@ function formatOrigin(origin) {
369
390
  notes.push(
370
391
  `redirected from ${redirects.map(hop => hop.to ?? hop.name ?? "?").join(" → ")}`
371
392
  );
372
- return `navigation to ${name}${notes.length > 0 ? ` (${notes.join(", ")})` : ""}`;
393
+ const initial = navStates.get(origin)?.event.initial === true;
394
+ return `${initial ? "initial " : ""}navigation to ${name}${notes.length > 0 ? ` (${notes.join(", ")})` : ""}`;
373
395
  }
374
396
  default:
375
397
  return "outside the reactive system";
376
398
  }
377
399
  }
378
400
  /** Record a root change (setSignal / refresh / async landing) on the node. */
379
- /** Live subscriber count, walked on demand — the core keeps no counter. */
380
- function countSubscribers(node) {
381
- let n = 0;
382
- for (let s = node._subs; s !== null; s = s._nextSub) n++;
383
- return n;
384
- }
385
401
  /**
386
402
  * HUGE_FAN_OUT from the engine's lower `fanOut` threshold (see dev.ts): a
387
403
  * committed root invalidation reaching hundreds of subscribers re-runs all
@@ -390,12 +406,20 @@ function countSubscribers(node) {
390
406
  * was a post-construction field that forked node shapes). Stops at
391
407
  * GRAPH_SIZE_WARN_AT, where the always-on core check takes over, so the two
392
408
  * never fire for the same write; the once-per-node dedupe is the core's.
409
+ * HMR plumbing subscribers (the refresh memo of every mounted instance, on
410
+ * a component's registration signal) are not counted toward `fanOut`; the
411
+ * handover is on the full list, which is what the core counts.
393
412
  */
394
413
  function checkFanOut(node, kind) {
395
414
  const limit = options.fanOut;
396
415
  if (typeof limit !== "number") return;
397
- const subs = countSubscribers(node);
398
- if (subs < limit || subs >= GRAPH_SIZE_WARN_AT) return;
416
+ let all = 0;
417
+ let subs = 0;
418
+ for (let s = node._subs; s !== null; s = s._nextSub) {
419
+ all++;
420
+ if (!(s._sub._config & CONFIG_PLUMBING)) subs++;
421
+ }
422
+ if (subs < limit || all >= GRAPH_SIZE_WARN_AT) return;
399
423
  noteFanOut(node, subs, kind);
400
424
  }
401
425
  function stampWrite(node, kind, prev = NO_VALUES, value = NO_VALUES) {
@@ -474,18 +498,25 @@ function captureDeps(el) {
474
498
  }
475
499
  /**
476
500
  * Wide-scope warning — the coarse-read / helper-leak signature: one scope
477
- * subscribed to dozens of sources re-runs when ANY of them change. Fired from
501
+ * subscribed to hundreds of sources re-runs when ANY of them change. Fired from
478
502
  * recordRerun for re-runs and directly from recompute for creation runs (a
479
- * memo can be born too wide). Re-warns only on 50% further growth.
503
+ * memo can be born too wide). Re-warns only on 50% further growth. Counts
504
+ * what the scope's author can act on: HMR plumbing sources are not counted,
505
+ * and a framework scope wide by construction (CONFIG_WIDE) is not judged —
506
+ * the always-on HUGE_FAN_IN still is.
480
507
  */
481
508
  function checkDepWidth(el) {
482
509
  const limit = options.wideDeps;
483
- if (limit === false) return;
510
+ // Only dev sets CONFIG_WIDE (see constants.ts).
511
+ if (limit === false || el._config & CONFIG_WIDE) return;
484
512
  let count = 0;
485
513
  const names = [];
486
514
  for (let l = el._deps; l !== null; l = l._nextDep) {
487
- count++;
488
- if (names.length < 12) names.push(nodeName(l._dep));
515
+ // HMR plumbing is a dev-only hop the scope cannot read past: not a source.
516
+ if (!(l._dep._config & CONFIG_PLUMBING)) {
517
+ count++;
518
+ if (names.length < 12) names.push(nodeName(l._dep));
519
+ }
489
520
  if (l === el._depsTail) break; // the validated prefix, as captureDeps
490
521
  }
491
522
  const node = el;
@@ -641,7 +672,10 @@ function checkHotTime(el, selfMs, causes) {
641
672
  * names it while it happens, with the input that keeps triggering it. The
642
673
  * fix is upstream: an equality boundary on the part of the input the scope
643
674
  * depends on, or a narrower read. Plain runs only — a held or overlay run
644
- * may be replayed and is never blamed as waste.
675
+ * may be replayed and is never blamed as waste. A run that committed
676
+ * `undefined` arrives here as changed (see `recomputeEnd`): its work is a
677
+ * side effect (a projection's draft writes, a write from inside a memo), not
678
+ * its output.
645
679
  */
646
680
  function checkWastedRecompute(el, at, phase, changed, selfMs, causes) {
647
681
  const cfg = options.wastedRecompute;
@@ -842,8 +876,10 @@ function shallowEquivalent(a, b) {
842
876
  for (let i = 0; i < arrA.length; i++) if (arrA[i] !== arrB[i]) return false;
843
877
  return true;
844
878
  }
845
- const keys = Object.keys(a);
846
- if (keys.length > UNSTABLE_KEY_CAP || keys.length !== Object.keys(b).length) return false;
879
+ // Every own key, symbols included: a symbol-keyed box holding a fresh value
880
+ // each run (web's `dynamic` boxes its in-flight promise) is not equivalent.
881
+ const keys = Reflect.ownKeys(a);
882
+ if (keys.length > UNSTABLE_KEY_CAP || keys.length !== Reflect.ownKeys(b).length) return false;
847
883
  for (const key of keys) {
848
884
  if (!(key in b) || a[key] !== b[key]) return false;
849
885
  }
@@ -1477,14 +1513,11 @@ function checkWaterfall(el, chain, ms) {
1477
1513
  const holdStates = new WeakMap();
1478
1514
  let activeHold = null;
1479
1515
  let holdLog = [];
1480
- /** Companions are optimistic nodes too; `_parentSource` marks them. So is a
1481
- * memo carrying a DERIVED override (lanes stage, #3479) — a lane pass's
1482
- * result, not a write anyone made: neither is an acknowledgement. */
1516
+ /** A lane's derivation (L2: a node carrying the lane's value that is not a
1517
+ * written guess — lanes.ts) is a lane pass's result, not a write anyone
1518
+ * made: not an acknowledgement. */
1483
1519
  function isCompanion(node) {
1484
- return (
1485
- (!!node._x && node._x._parentSource !== undefined) ||
1486
- (node._config & CONFIG_DERIVED_OVERRIDE) !== 0
1487
- );
1520
+ return (node._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
1488
1521
  }
1489
1522
  const HOLD_CENSUS_CAP = 10_000;
1490
1523
  function acknowledge(state, kind, source, reader) {
@@ -1500,15 +1533,17 @@ function acknowledge(state, kind, source, reader) {
1500
1533
  );
1501
1534
  else if (path !== undefined) existing.reader ??= path;
1502
1535
  }
1536
+ /** The declarations made against the hold: the guesses of its lanes
1537
+ * (lanes.ts `_laneGuesses`) and its `affects()` marks (`_marks`). */
1503
1538
  function censusRegistrations(t, state) {
1504
1539
  const budget = { left: HOLD_CENSUS_CAP };
1505
- for (const node of t._optimisticNodes)
1506
- if (!isCompanion(node))
1540
+ const guesses = GlobalQueue._laneGuesses?.(t);
1541
+ if (guesses !== undefined)
1542
+ for (const node of guesses)
1507
1543
  acknowledge(state, "optimistic", nodeName(node), reachesEffect(node, budget));
1508
- for (const store of t._optimisticStores)
1509
- acknowledge(state, "optimistic", store?._name ?? "store", null);
1510
- for (const node of t._affectsNodes)
1511
- acknowledge(state, "affects", nodeName(node), reachesEffect(node, budget));
1544
+ if (t._marks !== null)
1545
+ for (const node of t._marks)
1546
+ acknowledge(state, "affects", nodeName(node), reachesEffect(node, budget));
1512
1547
  }
1513
1548
  /**
1514
1549
  * Does anything that paints read `companion` — an effect, through however many
@@ -1533,7 +1568,10 @@ function reachesEffect(companion, budget) {
1533
1568
  }
1534
1569
  return null;
1535
1570
  }
1536
- /** Companions an effect reads, anywhere downstream of the hold's nodes. */
1571
+ /** Verdict readers an effect reads, anywhere downstream of the hold's nodes
1572
+ * (L2: a pass that entered an `isPending`/`latest` window is marked
1573
+ * CONFIG_VERDICT; the observe tier notes which windows — verdict.ts
1574
+ * `_devWindows`). The reader acknowledges the node it derives from. */
1537
1575
  function censusCompanions(roots, state) {
1538
1576
  const visited = new Set();
1539
1577
  const stack = [...roots];
@@ -1542,20 +1580,16 @@ function censusCompanions(roots, state) {
1542
1580
  const node = stack.pop();
1543
1581
  if (visited.has(node)) continue;
1544
1582
  visited.add(node);
1545
- const x = node._x;
1546
- if (x) {
1547
- let reader;
1548
- if (x._pendingSignal !== undefined && (reader = reachesEffect(x._pendingSignal, budget)))
1549
- acknowledge(state, "isPending", nodeName(node), reader);
1550
- if (
1551
- x._latestValueComputed !== undefined &&
1552
- (reader = reachesEffect(x._latestValueComputed, budget))
1553
- )
1554
- acknowledge(state, "latest", nodeName(node), reader);
1555
- for (let child = x._child ?? null; child !== null; child = child._nextChild ?? null)
1556
- stack.push(child);
1583
+ for (let s = node._subs; s !== null; s = s._nextSub) {
1584
+ const sub = s._sub;
1585
+ if (sub._config & CONFIG_VERDICT) {
1586
+ const windows = sub._devWindows ?? 0;
1587
+ const reader = sub._type ? sub : reachesEffect(sub, budget);
1588
+ if (windows & 1 && reader !== null) acknowledge(state, "isPending", nodeName(node), reader);
1589
+ if (windows & 2 && reader !== null) acknowledge(state, "latest", nodeName(node), reader);
1590
+ }
1591
+ stack.push(sub);
1557
1592
  }
1558
- for (let s = node._subs; s !== null; s = s._nextSub) stack.push(s._sub);
1559
1593
  }
1560
1594
  }
1561
1595
  function holdState(t) {
@@ -1581,9 +1615,8 @@ function trackHoldStart(t) {
1581
1615
  if (options.holds === false) return;
1582
1616
  const state = holdState(t);
1583
1617
  state.flushes++;
1584
- if (t._actions.length > 0) state.action = true;
1585
- for (const [source, reporters] of t._asyncReporters)
1586
- if (reporters.size > 0) state.blockers.add(source);
1618
+ if (t._open > 0) state.action = true;
1619
+ for (const source of blockersOf(t)) state.blockers.add(source);
1587
1620
  censusRegistrations(t, state);
1588
1621
  activeHold = state;
1589
1622
  }
@@ -1619,7 +1652,7 @@ function trackHoldSettled(t) {
1619
1652
  let interaction;
1620
1653
  let origin;
1621
1654
  let lastJoinAt = -Infinity;
1622
- for (const node of t._pendingNodes) {
1655
+ for (const node of t._nodes) {
1623
1656
  if (typeof node._fn === "function" || isCompanion(node)) continue;
1624
1657
  const change = node._devChange;
1625
1658
  if (change === undefined || change.kind !== "write") continue;
@@ -1649,7 +1682,7 @@ function trackHoldSettled(t) {
1649
1682
  return;
1650
1683
  }
1651
1684
  censusRegistrations(t, state);
1652
- censusCompanions([...t._pendingNodes, ...state.blockers], state);
1685
+ censusCompanions([...t._nodes, ...state.blockers], state);
1653
1686
  const end = now();
1654
1687
  // The hold began no later than its first parked flush; an interaction stamp
1655
1688
  // reaches further back (dispatch). A node rewritten mid-hold keeps only its
@@ -1844,8 +1877,10 @@ function checkOptimisticRevert(el, shown, truth, how) {
1844
1877
  // derived override promotes rather than reverts. Same predicate the hold
1845
1878
  // census uses to skip them.
1846
1879
  if (!options.optimisticReverts || isCompanion(el)) return;
1880
+ // Method call, as every commit path does: store slot nodes share one
1881
+ // comparator that reads `this._host` (#3687).
1847
1882
  const equals = el._equals;
1848
- if (equals && equals(shown, truth)) return;
1883
+ if (equals && equals.call(el, shown, truth)) return;
1849
1884
  const source = nodeName(el);
1850
1885
  // The two values are user data: quoted only under `values: "full"`; the
1851
1886
  // other levels say what happened without saying what was shown.
@@ -1954,7 +1989,10 @@ function trackGraph(navigation) {
1954
1989
  const event = { at: now(), ...size, navigation };
1955
1990
  if (route !== undefined) event.route = route;
1956
1991
  records.emit("graph", event, undefined);
1957
- if (cfg === false || route === undefined) return;
1992
+ // The initial declaration is not a visit (it settles before route content
1993
+ // mounts — the same gate the feedback folds apply as `event.initial !== true`);
1994
+ // `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
1995
+ if (cfg === false || route === undefined || navigation.initial) return;
1958
1996
  let history = routeCounts.get(route);
1959
1997
  if (history === undefined) routeCounts.set(route, (history = []));
1960
1998
  history.push(size);
@@ -2008,11 +2046,14 @@ const abandonWindows = new WeakMap();
2008
2046
  * One source abandoning flight after flight inside a window: every input
2009
2047
  * asked again and the earlier answers were thrown away — the
2010
2048
  * request-per-keystroke signature `feedback().flights[].abandoned` counts,
2011
- * as a finding while it happens. Warned once per window per source.
2049
+ * as a finding while it happens. Warned once per window per source. A
2050
+ * framework node superseding its own flights by design (CONFIG_SUPERSEDES)
2051
+ * is not judged; its flights are still recorded.
2012
2052
  */
2013
2053
  function checkAbandonedFlights(el, abandoned) {
2014
2054
  const cfg = options.abandonedFlights;
2015
- if (cfg === false || excludedNode(el)) return;
2055
+ // Only dev sets CONFIG_SUPERSEDES (see constants.ts).
2056
+ if (cfg === false || el._config & CONFIG_SUPERSEDES || excludedNode(el)) return;
2016
2057
  const at = Date.now();
2017
2058
  let win = abandonWindows.get(el);
2018
2059
  if (win === undefined || at - win.start > cfg.windowMs) {
@@ -2142,6 +2183,7 @@ function syncNavigation(state) {
2142
2183
  }
2143
2184
  function openNavigation(frame, ref) {
2144
2185
  const event = { at: frame.at, writes: 0, origin: frame };
2186
+ if (ref.initial === true) event.initial = true;
2145
2187
  if (frame.from !== undefined) event.from = frame.from;
2146
2188
  if (frame.interaction !== undefined) event.interaction = frame.interaction;
2147
2189
  const state = {
@@ -2209,7 +2251,7 @@ function noteNavigationWrite(prior, record) {
2209
2251
  /** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */
2210
2252
  function markNavigationsHeld(t) {
2211
2253
  if (openNavs.size === 0) return;
2212
- for (const node of t._pendingNodes) {
2254
+ for (const node of t._nodes) {
2213
2255
  const origin = node._devChange?.origin;
2214
2256
  if (origin?.kind !== "navigation") continue;
2215
2257
  const state = navStates.get(origin);
@@ -2219,7 +2261,7 @@ function markNavigationsHeld(t) {
2219
2261
  /** transitionSettled: `t` commits — the navigations whose writes it staged are done. */
2220
2262
  function settleNavigations(t, hold) {
2221
2263
  if (openNavs.size === 0) return;
2222
- for (const node of t._pendingNodes) {
2264
+ for (const node of t._nodes) {
2223
2265
  const origin = node._devChange?.origin;
2224
2266
  if (origin?.kind !== "navigation") continue;
2225
2267
  const state = navStates.get(origin);
@@ -2354,7 +2396,10 @@ function settleNavigation(state, outcome, hold) {
2354
2396
  event.settledMs = now() - event.at;
2355
2397
  event.outcome = outcome;
2356
2398
  if (hold !== undefined) event.hold = hold;
2357
- for (const f of folds) f.navigation?.(event);
2399
+ // The initial declaration is not a navigation the person waited on: its
2400
+ // `settledMs` is document start → router built, which would read as the
2401
+ // route's responsiveness in the feedback tables. Consumers get the record.
2402
+ if (event.initial !== true) for (const f of folds) f.navigation?.(event);
2358
2403
  records.emit("navigation", event, undefined);
2359
2404
  trackGraph(event);
2360
2405
  // The interaction that performed it may have been waiting only on this.
@@ -2362,10 +2407,12 @@ function settleNavigation(state, outcome, hold) {
2362
2407
  if (under !== undefined) maybeSettleInteraction(under);
2363
2408
  }
2364
2409
  const interactionStates = new WeakMap();
2410
+ /** `InteractionRef.event` → the record its frames join (while `joinable`). Weak: the Event is never held. */
2411
+ const eventInteractions = new WeakMap();
2365
2412
  /** Opened, not yet settled. */
2366
2413
  const openInteractions = new Set();
2367
2414
  let interactionLog = [];
2368
- function openInteraction(frame, opened) {
2415
+ function openInteraction(frame, opened, key) {
2369
2416
  const event = {
2370
2417
  name: frame.name,
2371
2418
  at: frame.at,
@@ -2391,13 +2438,24 @@ function openInteraction(frame, opened) {
2391
2438
  heldIn: new Set(),
2392
2439
  held: false,
2393
2440
  excludedWrites: 0,
2394
- awaiting: false,
2395
- actioned: false
2441
+ awaiting: 0,
2442
+ actioned: false,
2443
+ joinable: key !== undefined
2396
2444
  };
2397
2445
  interactionStates.set(frame, state);
2398
2446
  openInteractions.add(state);
2399
2447
  interactionLog.push(event);
2400
2448
  if (interactionLog.length > options.historyLimit) interactionLog.shift();
2449
+ if (key !== undefined) {
2450
+ eventInteractions.set(key, state);
2451
+ // A microtask is too early: native dispatch runs microtasks between
2452
+ // listeners. The callback must not close over `key`: a throttled timer
2453
+ // would hold the Event strongly long past its dispatch.
2454
+ setTimeout(() => {
2455
+ state.joinable = false;
2456
+ if (openInteractions.has(state)) maybeSettleInteraction(state, state.settleAt);
2457
+ });
2458
+ }
2401
2459
  }
2402
2460
  /**
2403
2461
  * How long the record waits on a handler's returned promise before settling
@@ -2423,17 +2481,17 @@ function closeInteraction(frame, returned) {
2423
2481
  ? returned
2424
2482
  : null;
2425
2483
  if (thenable !== null) {
2426
- state.awaiting = true;
2484
+ state.awaiting++;
2427
2485
  let done = false;
2428
2486
  let timer;
2429
2487
  const settle = () => {
2430
2488
  if (done) return;
2431
2489
  done = true;
2432
2490
  if (timer !== undefined) clearTimeout(timer);
2433
- if (!openInteractions.has(state)) return;
2434
- state.awaiting = false;
2491
+ if (!openInteractions.has(state) || --state.awaiting > 0) return;
2435
2492
  const at = now();
2436
- state.event.continuationMs = at - end;
2493
+ // From the last handler's return: the frames of one event share a record.
2494
+ state.event.continuationMs = at - (state.opened + state.event.handlerMs);
2437
2495
  checkUntrackedAsyncHandler(state);
2438
2496
  maybeSettleInteraction(state, at);
2439
2497
  };
@@ -2513,7 +2571,7 @@ function noteInteractionNavigation(event) {
2513
2571
  /** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */
2514
2572
  function markInteractionsHeld(t) {
2515
2573
  if (openInteractions.size === 0) return;
2516
- for (const node of t._pendingNodes) {
2574
+ for (const node of t._nodes) {
2517
2575
  const state = openInteractionOf(node._devChange?.origin);
2518
2576
  if (state !== undefined) {
2519
2577
  state.heldIn.add(t);
@@ -2536,12 +2594,16 @@ function settleInteractionsHeld(t, hold) {
2536
2594
  }
2537
2595
  function maybeSettleInteraction(state, end = now()) {
2538
2596
  const event = state.event;
2539
- if (state.open || state.awaiting || state.heldIn.size > 0) return;
2597
+ if (state.open || state.awaiting > 0 || state.heldIn.size > 0) return;
2540
2598
  // A drain must have committed the last write (the handler's, or a redirect
2541
2599
  // hop's after the click's own drain) — the handler returning is not the
2542
2600
  // screen having it.
2543
2601
  if (event.writes > 0 && drainSeq <= state.writeDrain) return;
2544
2602
  for (const nav of event.navigations) if (nav.outcome === undefined) return;
2603
+ if (state.joinable) {
2604
+ state.settleAt = end;
2605
+ return;
2606
+ }
2545
2607
  openInteractions.delete(state);
2546
2608
  // Every write went to an excluded subject and nothing of the app's ran: the
2547
2609
  // click was on the observer's own UI (a devtools panel's button). Not a
@@ -2638,6 +2700,12 @@ const engineHooks = {
2638
2700
  frame.prevValue,
2639
2701
  el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value
2640
2702
  );
2703
+ // The same exemption for memos: a run that committed `undefined` did its
2704
+ // work as a side effect (a projection's draft writes or reconcile, a
2705
+ // write from inside a memo), so core's `undefined === undefined` gate
2706
+ // proves nothing. Reported as changed, so no consumer counts it as waste.
2707
+ if (!changed && (el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value) === undefined)
2708
+ changed = true;
2641
2709
  if (frame.causes !== null)
2642
2710
  recordRerun(
2643
2711
  el,