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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/dev-shared.js +882 -405
  2. package/dist/dev.attribution.js +172 -66
  3. package/dist/dev.js +1529 -745
  4. package/dist/observe/affects-marks.js +86 -0
  5. package/dist/observe/affects.js +32 -80
  6. package/dist/observe/boundaries.js +202 -166
  7. package/dist/observe/core/async.js +192 -141
  8. package/dist/observe/core/attribution-queries.js +1 -1
  9. package/dist/observe/core/attribution.js +315 -214
  10. package/dist/observe/core/constants.js +23 -4
  11. package/dist/observe/core/context.js +3 -3
  12. package/dist/observe/core/core.js +532 -360
  13. package/dist/observe/core/dev.js +63 -35
  14. package/dist/observe/core/effect.js +47 -47
  15. package/dist/observe/core/error.js +5 -3
  16. package/dist/observe/core/external.js +5 -6
  17. package/dist/observe/core/graph.js +56 -42
  18. package/dist/observe/core/heap.js +33 -33
  19. package/dist/observe/core/identities.js +62 -0
  20. package/dist/observe/core/lanes.js +242 -229
  21. package/dist/observe/core/owner.js +62 -50
  22. package/dist/observe/core/scheduler.js +322 -168
  23. package/dist/observe/core/truth.js +45 -0
  24. package/dist/observe/core/verdict.js +114 -64
  25. package/dist/observe/deferred.js +184 -0
  26. package/dist/observe/index.js +7 -1
  27. package/dist/observe/map.js +339 -254
  28. package/dist/observe/reveal.js +77 -77
  29. package/dist/observe/signals.js +54 -33
  30. package/dist/observe/store/affects.js +76 -69
  31. package/dist/observe/store/edges.js +39 -0
  32. package/dist/observe/store/hooks.js +13 -0
  33. package/dist/observe/store/index.js +0 -2
  34. package/dist/observe/store/optimistic.js +82 -44
  35. package/dist/observe/store/projection.js +83 -64
  36. package/dist/observe/store/reconcile.js +32 -30
  37. package/dist/observe/store/store.js +219 -123
  38. package/dist/prod/affects-marks.js +86 -0
  39. package/dist/prod/affects.js +33 -79
  40. package/dist/prod/boundaries.js +185 -149
  41. package/dist/prod/core/async.js +176 -135
  42. package/dist/prod/core/constants.js +23 -4
  43. package/dist/prod/core/context.js +3 -3
  44. package/dist/prod/core/core.js +521 -353
  45. package/dist/prod/core/effect.js +66 -66
  46. package/dist/prod/core/error.js +5 -3
  47. package/dist/prod/core/external.js +5 -6
  48. package/dist/prod/core/graph.js +56 -42
  49. package/dist/prod/core/heap.js +38 -38
  50. package/dist/prod/core/identities.js +62 -0
  51. package/dist/prod/core/lanes.js +237 -224
  52. package/dist/prod/core/owner.js +62 -51
  53. package/dist/prod/core/scheduler.js +312 -158
  54. package/dist/prod/core/truth.js +45 -0
  55. package/dist/prod/core/verdict.js +113 -63
  56. package/dist/prod/deferred.js +182 -0
  57. package/dist/prod/index.js +7 -1
  58. package/dist/prod/map.js +366 -281
  59. package/dist/prod/reveal.js +77 -77
  60. package/dist/prod/signals.js +59 -38
  61. package/dist/prod/store/affects.js +76 -69
  62. package/dist/prod/store/edges.js +39 -0
  63. package/dist/prod/store/hooks.js +13 -0
  64. package/dist/prod/store/index.js +0 -2
  65. package/dist/prod/store/optimistic.js +121 -84
  66. package/dist/prod/store/projection.js +46 -34
  67. package/dist/prod/store/reconcile.js +32 -30
  68. package/dist/prod/store/store.js +205 -116
  69. package/dist/types/affects-marks.d.ts +14 -0
  70. package/dist/types/boundaries.d.ts +5 -1
  71. package/dist/types/core/async.d.ts +10 -0
  72. package/dist/types/core/attribution.d.ts +6 -2
  73. package/dist/types/core/constants.d.ts +18 -1
  74. package/dist/types/core/core.d.ts +77 -5
  75. package/dist/types/core/dev.d.ts +43 -13
  76. package/dist/types/core/graph.d.ts +11 -0
  77. package/dist/types/core/identities.d.ts +17 -0
  78. package/dist/types/core/index.d.ts +2 -1
  79. package/dist/types/core/lanes.d.ts +2 -4
  80. package/dist/types/core/owner.d.ts +5 -3
  81. package/dist/types/core/scheduler.d.ts +59 -34
  82. package/dist/types/core/truth.d.ts +16 -0
  83. package/dist/types/core/types.d.ts +21 -6
  84. package/dist/types/core/verdict.d.ts +32 -0
  85. package/dist/types/deferred.d.ts +28 -0
  86. package/dist/types/index.d.ts +6 -0
  87. package/dist/types/map.d.ts +2 -2
  88. package/dist/types/signals.d.ts +28 -7
  89. package/dist/types/store/affects.d.ts +11 -0
  90. package/dist/types/store/edges.d.ts +9 -0
  91. package/dist/types/store/hooks.d.ts +48 -0
  92. package/dist/types/store/index.d.ts +10 -4
  93. package/dist/types/store/optimistic.d.ts +3 -2
  94. package/dist/types/store/projection.d.ts +4 -3
  95. package/dist/types/store/store.d.ts +4 -39
  96. package/dist/types/store/target.d.ts +16 -8
  97. package/dist/types/store/types.d.ts +14 -0
  98. package/package.json +1 -1
@@ -1,25 +1,25 @@
1
1
  import {
2
- br as OBSERVE,
3
- bB as setAttributionHooks,
4
- bC as liveRootOwners,
2
+ bN as OBSERVE,
3
+ bY as setAttributionHooks,
4
+ bZ as liveRootOwners,
5
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,
6
+ as as emitDiagnostic,
7
+ b_ as ownerPath,
8
+ aJ as reportDiagnostic,
9
+ b$ as isSuppressed,
10
+ c0 as blockersOf,
11
+ c1 as CONFIG_PLUMBING,
12
+ c2 as anyExcluded,
13
+ c3 as isExcluded,
14
+ c4 as CONFIG_WIDE,
15
+ c as CONFIG_OVERRIDE,
16
16
  C as CONFIG_GUESS,
17
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
18
+ f as CONFIG_VERDICT,
19
+ c5 as GRAPH_SIZE_WARN_AT,
20
+ c6 as noteFanOut,
21
+ c7 as CONFIG_SUPERSEDES,
22
+ b0 as $REFRESH
23
23
  } from "./dev-shared.js";
24
24
 
25
25
  /** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */
@@ -1070,7 +1070,15 @@ function trackEffectWrite(node, record, value) {
1070
1070
  const writer = info === undefined ? 0 : devId(info.node);
1071
1071
  recordNodes.set(record, node);
1072
1072
  n._devSoleWriter = n._devSoleWriter === undefined || n._devSoleWriter === writer ? writer : null;
1073
- if (info !== undefined && value !== undefined && value === info.node._value) {
1073
+ // An apply half that takes no argument cannot be copying its compute
1074
+ // output: a match is coincidence (`createEffect(open, () =>
1075
+ // setFlipped(measure()))`).
1076
+ if (
1077
+ info !== undefined &&
1078
+ value !== undefined &&
1079
+ value === info.node._value &&
1080
+ (info.node._effectFn?.length ?? 1)
1081
+ ) {
1074
1082
  copyWrites.add(record);
1075
1083
  n._devCopyRuns = n._devCopyFrom === writer ? (n._devCopyRuns ?? 0) + 1 : 1;
1076
1084
  n._devCopyFrom = writer;
@@ -1168,7 +1176,13 @@ function checkRelayTear(victim, causes, prevCauses) {
1168
1176
  // "reset editable state from a source" shape — the tear is real, but a memo
1169
1177
  // is not the answer, so it stays advisory like any other non-derivable tear.
1170
1178
  const derivable = copy && soleWriter;
1171
- const severity = derivable || state.count >= RELAY_WARN_AT ? "warn" : "info";
1179
+ // An apply half that ignores its argument writes from outside the graph
1180
+ // (layout, time, a ref; a signal read there is STRICT_READ's): measuring,
1181
+ // so it stays advisory however often it repeats.
1182
+ const severity =
1183
+ derivable || (state.count >= RELAY_WARN_AT && (relay.node._effectFn?.length ?? 1))
1184
+ ? "warn"
1185
+ : "info";
1172
1186
  // First sighting always reports (advisory); afterwards only the escalation.
1173
1187
  if (state.count > 1 && (severity !== "warn" || state.warned)) return;
1174
1188
  // One verdict per derivable signal: the copy report (checkCopyEffect) and
@@ -1187,12 +1201,10 @@ function checkRelayTear(victim, causes, prevCauses) {
1187
1201
  `writers — editable state reset from a source. If the reset is the intent, the tear ` +
1188
1202
  `is its cost; if "${write.name}" only ever mirrors the source, drop the local copy ` +
1189
1203
  `and read the source.`
1190
- : soleWriter
1191
- ? `Nothing else writes "${write.name}" — it is derived state: make it a memo over what ` +
1192
- `the effect reads and every reader gets it in the same flush.`
1193
- : `If "${write.name}" is computed from what the effect reads, make it a memo so readers ` +
1194
- `get it in the same flush; if the write reads something outside the graph (layout, ` +
1195
- `time), the tear is the cost of measuring.`;
1204
+ : (soleWriter ? `Nothing else writes "${write.name}". ` : "") +
1205
+ `If "${write.name}" is computed from what the effect reads, make it a memo so readers ` +
1206
+ `get it in the same flush; if the write reads something outside the graph (layout, ` +
1207
+ `time), the tear is the cost of measuring.`;
1196
1208
  const message =
1197
1209
  `[EFFECT_RELAY_TEAR] ${victimKind} "${nodeName(victim)}" ran twice for one write of ` +
1198
1210
  `"${shared.name}": once in the flush where "${shared.name}" changed, and again after ` +
@@ -1363,13 +1375,11 @@ function asyncOrigin(el) {
1363
1375
  // WeakMaps: an errored/abandoned flight must not leak its node or block GC.
1364
1376
  const liveFlights = new WeakMap();
1365
1377
  const landedFlights = new WeakMap();
1366
- /**
1367
- * Flight-object identity → earliest known start. Fed by markFlight() (the
1368
- * cooperative preload/cache declaration — populated even while attribution
1369
- * is disabled, so navigation-time marks survive a later enable()) and by
1370
- * first sightings at registration.
1371
- */
1372
- const flightOrigins = new WeakMap();
1378
+ // Flight-object identity → earliest known start lives on the core's slot
1379
+ // (`OBSERVE.attribution.markFlight`/`flightOrigin`), so marks made with no
1380
+ // engine loaded — a router's preloads, `lazy()`'s import — and the server's
1381
+ // boundary check share it. Fed by markFlight() and by first sightings at
1382
+ // registration.
1373
1383
  let waterfallLog = [];
1374
1384
  /** Deepest landed-flight cause reachable through a cause list (derived links included). */
1375
1385
  function flightCauseIn(causes) {
@@ -1412,8 +1422,8 @@ function emitFlight(el, flight, endedAt, outcome) {
1412
1422
  }
1413
1423
  function trackFlightStart(el, flight) {
1414
1424
  const at = now();
1415
- const origin = flightOrigins.get(flight) ?? at;
1416
- if (origin === at) flightOrigins.set(flight, at);
1425
+ const origin = OBSERVE.attribution.flightOrigin(flight) ?? at;
1426
+ if (origin === at) OBSERVE.attribution.markFlight(flight, at);
1417
1427
  // Census: a flight still in the air when the node starts another was
1418
1428
  // superseded — its answer will be discarded.
1419
1429
  const superseded = liveFlights.get(el);
@@ -1913,8 +1923,22 @@ function checkOptimisticRevert(el, shown, truth, how) {
1913
1923
  );
1914
1924
  }
1915
1925
  // --- Graph growth -----------------------------------------------------------------
1916
- /** Per route: the graph's size at its last `visits` settles, oldest first. */
1926
+ /**
1927
+ * Per route: at its last `visits` compared settles, oldest first, the
1928
+ * graph's size and the part of it already alive as a navigation opened.
1929
+ */
1917
1930
  const routeCounts = new Map();
1931
+ /**
1932
+ * The route the app last arrived on: a navigation that stays on it (only
1933
+ * its params or query change) continues the visit and is not compared.
1934
+ */
1935
+ let visitRoute;
1936
+ /**
1937
+ * Every node met as a navigation opened, weakly, for the session: one set,
1938
+ * not one per navigation (see `trackGraph`). Never reset — a node in it was
1939
+ * alive as a navigation opened whichever window met it.
1940
+ */
1941
+ const opened = new WeakSet();
1918
1942
  /**
1919
1943
  * The live graph's size — a walk, not a counter: nothing is charged at node
1920
1944
  * creation, disposal, write or re-run; the engine asks at a navigation's
@@ -1929,26 +1953,42 @@ const routeCounts = new Map();
1929
1953
  * counted unless a subscription still reaches them.
1930
1954
  */
1931
1955
  function graphSize() {
1956
+ return walkGraph();
1957
+ }
1958
+ /**
1959
+ * The walk behind `graphSize()`. Given `before` alone, it adds every node it
1960
+ * meets to that set: what is alive as a navigation opens. Given `before` and
1961
+ * `survived`, it also counts into `survived` the nodes it meets that are in
1962
+ * the set, and an edge when both its ends are: what outlived a navigation.
1963
+ * Nothing is retained — the set is weak.
1964
+ */
1965
+ function walkGraph(before, survived) {
1932
1966
  const roots = liveRootOwners();
1933
- const size = { roots: roots.length, owners: 0, computations: 0, signals: 0, edges: 0 };
1967
+ const size = { roots: 0, owners: 0, computations: 0, signals: 0, edges: 0 };
1934
1968
  const seen = new Set();
1935
1969
  // A worklist, not recursion: a subscriber chain can be thousands deep.
1936
1970
  const work = [];
1937
1971
  const stack = [];
1972
+ const count = (node, key) => {
1973
+ size[key]++;
1974
+ if (survived !== undefined) {
1975
+ if (before.has(node)) survived[key]++;
1976
+ } else if (before !== undefined) before.add(node);
1977
+ };
1938
1978
  const meet = node => {
1939
1979
  if (seen.has(node)) return;
1940
1980
  seen.add(node);
1941
- if (typeof node._fn === "function") size.computations++;
1942
- else size.signals++;
1981
+ count(node, typeof node._fn === "function" ? "computations" : "signals");
1943
1982
  work.push(node);
1944
1983
  };
1945
1984
  for (const root of roots) {
1946
- size.owners++;
1985
+ count(root, "roots");
1986
+ count(root, "owners");
1947
1987
  // Iterative: a deep tree must not grow the call stack.
1948
1988
  let child = root._firstChild;
1949
1989
  for (;;) {
1950
1990
  while (child !== null) {
1951
- size.owners++;
1991
+ count(child, "owners");
1952
1992
  if (typeof child._fn === "function") meet(child);
1953
1993
  if (child._firstChild !== null) stack.push(child);
1954
1994
  child = child._nextSibling;
@@ -1967,10 +2007,20 @@ function graphSize() {
1967
2007
  for (let link = node._deps; link !== null; link = link._nextDep) {
1968
2008
  size.edges++;
1969
2009
  meet(link._dep);
2010
+ if (survived !== undefined && before.has(node) && before.has(link._dep)) survived.edges++;
1970
2011
  }
1971
2012
  }
1972
2013
  return size;
1973
2014
  }
2015
+ /**
2016
+ * openNavigation, while the growth check is on: what is alive as a
2017
+ * navigation opens, added to what earlier ones met, for its settle to tell
2018
+ * apart from what it created.
2019
+ */
2020
+ function snapshotGraph() {
2021
+ walkGraph(opened);
2022
+ return opened;
2023
+ }
1974
2024
  /**
1975
2025
  * settleNavigation: the app has finished moving to a route — count the live
1976
2026
  * graph. One record per settle for a listener; the growth check compares
@@ -1979,54 +2029,95 @@ function graphSize() {
1979
2029
  * `createRoot` in an effect with no dispose, a subscription a component
1980
2030
  * registered outside its owner. Nothing here runs per node; the walk is
1981
2031
  * the cost, at navigation cadence.
2032
+ *
2033
+ * A count taken at an arrival also holds everything the visit just created,
2034
+ * so a page whose correctly owned content is bigger each visit climbs too.
2035
+ * What tells the two apart is what outlived a navigation: the nodes alive
2036
+ * as this or any earlier navigation opened (`opened`) that are still alive
2037
+ * at this settle. A visit's own content is not among them; what an earlier
2038
+ * visit left behind is, whatever the content does. A series reports only
2039
+ * when both climb.
2040
+ *
2041
+ * The set is the session's, not this navigation's own snapshot: the walk
2042
+ * meets a long-lived source only through a page that reads it, so a
2043
+ * per-navigation snapshot holds a module-level signal (an HMR registry, a
2044
+ * global store) only when the page just left read it too, and arriving from
2045
+ * pages that share ever more with this one read as growth (#4101). Kept
2046
+ * across navigations, such a source counts at every settle once met, the
2047
+ * same each visit; only what keeps accumulating climbs.
1982
2048
  */
1983
2049
  const GRAPH_SERIES = ["owners", "computations", "signals", "edges"];
1984
- function trackGraph(navigation) {
2050
+ function trackGraph(state) {
2051
+ const navigation = state.event;
2052
+ const before = state.before;
2053
+ state.before = undefined;
1985
2054
  const cfg = options.graphGrowth;
1986
2055
  if (cfg === false && !records.observed("graph")) return;
1987
- const size = graphSize();
2056
+ const survived = before && {
2057
+ roots: 0,
2058
+ owners: 0,
2059
+ computations: 0,
2060
+ signals: 0,
2061
+ edges: 0
2062
+ };
2063
+ const size = walkGraph(before, survived);
1988
2064
  const route = navigation.name ?? navigation.to;
1989
2065
  const event = { at: now(), ...size, navigation };
1990
2066
  if (route !== undefined) event.route = route;
1991
2067
  records.emit("graph", event, undefined);
2068
+ if (cfg === false || route === undefined) return;
2069
+ // A navigation that stays on the route pattern continues the visit: the
2070
+ // page it kept outlived the move by design, so comparing it would read the
2071
+ // page itself as growth. Only arrivals that enter the route are compared —
2072
+ // a leak made during a visit is still alive when the next one starts. A
2073
+ // superseded navigation never showed, so the app is still where it was.
2074
+ const left = visitRoute;
2075
+ if (navigation.outcome !== "superseded") visitRoute = route;
1992
2076
  // The initial declaration is not a visit (it settles before route content
1993
2077
  // mounts — the same gate the feedback folds apply as `event.initial !== true`);
1994
2078
  // `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
1995
- if (cfg === false || route === undefined || navigation.initial) return;
2079
+ // A navigation opened before the check was on has no snapshot to compare.
2080
+ if (navigation.initial || route === left || survived === undefined) return;
1996
2081
  let history = routeCounts.get(route);
1997
2082
  if (history === undefined) routeCounts.set(route, (history = []));
1998
- history.push(size);
2083
+ history.push({ size, survived });
1999
2084
  if (history.length > cfg.visits) history.shift();
2000
2085
  if (history.length < cfg.visits) return;
2001
- // Any series climbing on every visit to the ratio is growth; which ones
2002
- // did says what leaked — computations with owners flat is an ownerless
2003
- // effect per visit, edges alone is a subscription per visit to something
2004
- // long-lived, owners is an undisposed root.
2005
- const grew = GRAPH_SERIES.filter(key => {
2006
- for (let i = 1; i < history.length; i++)
2007
- if (history[i][key] <= history[i - 1][key]) return false;
2008
- return history[history.length - 1][key] >= history[0][key] * cfg.ratio;
2009
- });
2086
+ const totals = history.map(h => h.size);
2087
+ const kept = history.map(h => h.survived);
2088
+ const climbs = (series, key) => {
2089
+ for (let i = 1; i < series.length; i++) if (series[i][key] <= series[i - 1][key]) return false;
2090
+ return series[series.length - 1][key] >= series[0][key] * cfg.ratio;
2091
+ };
2092
+ // Any series climbing on every visit to the ratio — in the totals and in
2093
+ // what outlived each navigation — is growth; which ones did says what
2094
+ // leaked — computations with owners flat is an ownerless effect per visit,
2095
+ // edges alone is a subscription per visit to something long-lived, owners
2096
+ // is an undisposed root.
2097
+ const grew = GRAPH_SERIES.filter(key => climbs(totals, key) && climbs(kept, key));
2010
2098
  if (grew.length === 0) return;
2011
2099
  // The count is the whole graph's, so a leak shows at every route's settle;
2012
2100
  // the first route to complete its climb reports, naming the others seen.
2013
2101
  const routes = [...routeCounts.keys()];
2014
- const series = grew.map(key => `${key} ${history.map(h => h[key]).join(" → ")}`).join("; ");
2102
+ const join = (series, key) => series.map(h => h[key]).join(" → ");
2103
+ const series = grew.map(key => `${key} ${join(kept, key)} (of ${join(totals, key)})`).join("; ");
2015
2104
  const shape = grew.includes("owners")
2016
2105
  ? "a createRoot() in an effect or handler with no dispose, or a Portal or panel mounted per visit"
2017
2106
  : grew.includes("computations")
2018
2107
  ? "an effect or memo created with no owner (a module-level or callback createEffect) that only its sources keep alive"
2019
2108
  : "a subscription per visit to something long-lived — a global store or signal read by a computation that outlives the visit";
2020
2109
  const message =
2021
- `[GRAPH_GROWTH] the live graph grew on ${cfg.visits} consecutive visits to ${route}: ${series} ` +
2110
+ `[GRAPH_GROWTH] the live graph grew on ${cfg.visits} consecutive visits to ${route}, and so did ` +
2111
+ `the part that outlived each navigation there: ${series} ` +
2022
2112
  `(${size.roots} roots)${routes.length > 1 ? `, across visits to ${routes.join(", ")}` : ""}. Each ` +
2023
2113
  `visit left something behind that the next did not reclaim — the shape says ${shape}. Dispose ` +
2024
2114
  `what a visit creates (onCleanup, or return the disposer from onSettled) and own it under the ` +
2025
- `route's component so leaving the route tears it down.`;
2115
+ `route's component so leaving the route tears it down. A cache that never evicts grows alike.`;
2026
2116
  const data = {
2027
2117
  route,
2028
2118
  grew,
2029
- history: history.map(h => ({ ...h })),
2119
+ history: totals,
2120
+ survived: kept,
2030
2121
  roots: size.roots,
2031
2122
  routes
2032
2123
  };
@@ -2159,6 +2250,14 @@ const openNavs = new Set();
2159
2250
  let navigationLog = [];
2160
2251
  /** Drains completed since enable() — the clock `writeDrain` reads. */
2161
2252
  let drainSeq = 0;
2253
+ /**
2254
+ * Navigations settled in a landing, waiting for their graph count: the hook
2255
+ * fires before the landing's commits dispose the frame parked for the page
2256
+ * left, which the walk would still reach through the sources both pages read
2257
+ * (#3989). `land()` runs only inside `flush()`'s drain, so `flushEnd` counts
2258
+ * them, after those commits — the instant a one-drain navigation is counted.
2259
+ */
2260
+ const landedGraphs = [];
2162
2261
  /**
2163
2262
  * Copy what the router currently says onto the frame (what writes stamped —
2164
2263
  * `formatOrigin` reads it) and the event. Called when the frame opens, when a
@@ -2193,6 +2292,8 @@ function openNavigation(frame, ref) {
2193
2292
  held: false,
2194
2293
  writeDrain: drainSeq
2195
2294
  };
2295
+ // The initial declaration is never compared (see `trackGraph`).
2296
+ if (options.graphGrowth !== false && ref.initial !== true) state.before = snapshotGraph();
2196
2297
  syncNavigation(state);
2197
2298
  navStates.set(frame, state);
2198
2299
  openNavs.add(state);
@@ -2268,7 +2369,7 @@ function settleNavigations(t, hold) {
2268
2369
  if (state === undefined || !openNavs.has(state)) continue;
2269
2370
  // A navigation whose frame is still open when its transition commits
2270
2371
  // (`until()` inside the frame) settles here too: its writes are through.
2271
- settleNavigation(state, state.held ? "held" : "committed", hold);
2372
+ settleNavigation(state, state.held ? "held" : "committed", hold, true);
2272
2373
  }
2273
2374
  }
2274
2375
  let openFlush = null;
@@ -2384,11 +2485,14 @@ function trackFlushEnd() {
2384
2485
  drainSeq++;
2385
2486
  // The swaps this drain committed have rendered.
2386
2487
  displayFallbacks();
2488
+ // Landed first: they settled before anything this drain settles below.
2489
+ for (let i = 0; i < landedGraphs.length; i++) trackGraph(landedGraphs[i]);
2490
+ landedGraphs.length = 0;
2387
2491
  for (const state of openNavs)
2388
2492
  if (state.open === 0 && !state.held) settleNavigation(state, "committed");
2389
2493
  for (const state of openInteractions) maybeSettleInteraction(state);
2390
2494
  }
2391
- function settleNavigation(state, outcome, hold) {
2495
+ function settleNavigation(state, outcome, hold, landed) {
2392
2496
  if (!openNavs.has(state)) return;
2393
2497
  openNavs.delete(state);
2394
2498
  syncNavigation(state);
@@ -2401,7 +2505,8 @@ function settleNavigation(state, outcome, hold) {
2401
2505
  // route's responsiveness in the feedback tables. Consumers get the record.
2402
2506
  if (event.initial !== true) for (const f of folds) f.navigation?.(event);
2403
2507
  records.emit("navigation", event, undefined);
2404
- trackGraph(event);
2508
+ if (landed) landedGraphs.push(state);
2509
+ else trackGraph(state);
2405
2510
  // The interaction that performed it may have been waiting only on this.
2406
2511
  const under = openInteractionOf(event.interaction);
2407
2512
  if (under !== undefined) maybeSettleInteraction(under);
@@ -2885,8 +2990,10 @@ function resetTracking() {
2885
2990
  stagedFallbacks.delete(DRAIN);
2886
2991
  trackingGen++;
2887
2992
  openNavs.clear();
2993
+ landedGraphs.length = 0;
2888
2994
  openInteractions.clear();
2889
2995
  routeCounts.clear();
2996
+ visitRoute = undefined;
2890
2997
  drainSeq = 0;
2891
2998
  originFrames.length = 0;
2892
2999
  effectStack.length = 0;
@@ -3019,11 +3126,10 @@ const attribution = {
3019
3126
  // The buffers are typed by their variable; the switch is the proof.
3020
3127
  return buffer;
3021
3128
  },
3022
- markFlight(flight, startedAt = now()) {
3023
- // Earliest wins: re-marking (a cache re-serving the same promise) must
3024
- // not move the origin later.
3025
- const existing = flightOrigins.get(flight);
3026
- if (existing === undefined || startedAt < existing) flightOrigins.set(flight, startedAt);
3129
+ markFlight(flight, startedAt) {
3130
+ // Earliest wins (the slot's rule): re-marking (a cache re-serving the
3131
+ // same promise) must not move the origin later.
3132
+ OBSERVE.attribution.markFlight(flight, startedAt);
3027
3133
  }
3028
3134
  };
3029
3135