@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.
- package/dist/dev-shared.js +882 -405
- package/dist/dev.attribution.js +172 -66
- package/dist/dev.js +1529 -745
- package/dist/observe/affects-marks.js +86 -0
- package/dist/observe/affects.js +32 -80
- package/dist/observe/boundaries.js +202 -166
- package/dist/observe/core/async.js +192 -141
- package/dist/observe/core/attribution-queries.js +1 -1
- package/dist/observe/core/attribution.js +315 -214
- package/dist/observe/core/constants.js +23 -4
- package/dist/observe/core/context.js +3 -3
- package/dist/observe/core/core.js +532 -360
- package/dist/observe/core/dev.js +63 -35
- package/dist/observe/core/effect.js +47 -47
- package/dist/observe/core/error.js +5 -3
- package/dist/observe/core/external.js +5 -6
- package/dist/observe/core/graph.js +56 -42
- package/dist/observe/core/heap.js +33 -33
- package/dist/observe/core/identities.js +62 -0
- package/dist/observe/core/lanes.js +242 -229
- package/dist/observe/core/owner.js +62 -50
- package/dist/observe/core/scheduler.js +322 -168
- package/dist/observe/core/truth.js +45 -0
- package/dist/observe/core/verdict.js +114 -64
- package/dist/observe/deferred.js +184 -0
- package/dist/observe/index.js +7 -1
- package/dist/observe/map.js +339 -254
- package/dist/observe/reveal.js +77 -77
- package/dist/observe/signals.js +54 -33
- package/dist/observe/store/affects.js +76 -69
- package/dist/observe/store/edges.js +39 -0
- package/dist/observe/store/hooks.js +13 -0
- package/dist/observe/store/index.js +0 -2
- package/dist/observe/store/optimistic.js +82 -44
- package/dist/observe/store/projection.js +83 -64
- package/dist/observe/store/reconcile.js +32 -30
- package/dist/observe/store/store.js +219 -123
- package/dist/prod/affects-marks.js +86 -0
- package/dist/prod/affects.js +33 -79
- package/dist/prod/boundaries.js +185 -149
- package/dist/prod/core/async.js +176 -135
- package/dist/prod/core/constants.js +23 -4
- package/dist/prod/core/context.js +3 -3
- package/dist/prod/core/core.js +521 -353
- package/dist/prod/core/effect.js +66 -66
- package/dist/prod/core/error.js +5 -3
- package/dist/prod/core/external.js +5 -6
- package/dist/prod/core/graph.js +56 -42
- package/dist/prod/core/heap.js +38 -38
- package/dist/prod/core/identities.js +62 -0
- package/dist/prod/core/lanes.js +237 -224
- package/dist/prod/core/owner.js +62 -51
- package/dist/prod/core/scheduler.js +312 -158
- package/dist/prod/core/truth.js +45 -0
- package/dist/prod/core/verdict.js +113 -63
- package/dist/prod/deferred.js +182 -0
- package/dist/prod/index.js +7 -1
- package/dist/prod/map.js +366 -281
- package/dist/prod/reveal.js +77 -77
- package/dist/prod/signals.js +59 -38
- package/dist/prod/store/affects.js +76 -69
- package/dist/prod/store/edges.js +39 -0
- package/dist/prod/store/hooks.js +13 -0
- package/dist/prod/store/index.js +0 -2
- package/dist/prod/store/optimistic.js +121 -84
- package/dist/prod/store/projection.js +46 -34
- package/dist/prod/store/reconcile.js +32 -30
- package/dist/prod/store/store.js +205 -116
- package/dist/types/affects-marks.d.ts +14 -0
- package/dist/types/boundaries.d.ts +5 -1
- package/dist/types/core/async.d.ts +10 -0
- package/dist/types/core/attribution.d.ts +6 -2
- package/dist/types/core/constants.d.ts +18 -1
- package/dist/types/core/core.d.ts +77 -5
- package/dist/types/core/dev.d.ts +43 -13
- package/dist/types/core/graph.d.ts +11 -0
- package/dist/types/core/identities.d.ts +17 -0
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/lanes.d.ts +2 -4
- package/dist/types/core/owner.d.ts +5 -3
- package/dist/types/core/scheduler.d.ts +59 -34
- package/dist/types/core/truth.d.ts +16 -0
- package/dist/types/core/types.d.ts +21 -6
- package/dist/types/core/verdict.d.ts +32 -0
- package/dist/types/deferred.d.ts +28 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/map.d.ts +2 -2
- package/dist/types/signals.d.ts +28 -7
- package/dist/types/store/affects.d.ts +11 -0
- package/dist/types/store/edges.d.ts +9 -0
- package/dist/types/store/hooks.d.ts +48 -0
- package/dist/types/store/index.d.ts +10 -4
- package/dist/types/store/optimistic.d.ts +3 -2
- package/dist/types/store/projection.d.ts +4 -3
- package/dist/types/store/store.d.ts +4 -39
- package/dist/types/store/target.d.ts +16 -8
- package/dist/types/store/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/dev.attribution.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
bN as OBSERVE,
|
|
3
|
+
bY as setAttributionHooks,
|
|
4
|
+
bZ as liveRootOwners,
|
|
5
5
|
N as NOT_PENDING,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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 =
|
|
1416
|
-
if (origin === 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
|
-
/**
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
|
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}
|
|
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:
|
|
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
|
-
|
|
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
|
|
3023
|
-
// Earliest wins: re-marking (a cache re-serving the
|
|
3024
|
-
// not move the origin later.
|
|
3025
|
-
|
|
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
|
|