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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/dev-shared.js +248 -189
  2. package/dist/dev.attribution.js +124 -41
  3. package/dist/dev.js +553 -322
  4. package/dist/observe/affects.js +34 -31
  5. package/dist/observe/boundaries.js +166 -132
  6. package/dist/observe/core/async.js +90 -83
  7. package/dist/observe/core/attribution-queries.js +1 -1
  8. package/dist/observe/core/attribution.js +281 -197
  9. package/dist/observe/core/constants.js +7 -2
  10. package/dist/observe/core/context.js +3 -3
  11. package/dist/observe/core/core.js +277 -245
  12. package/dist/observe/core/dev.js +3 -3
  13. package/dist/observe/core/effect.js +21 -21
  14. package/dist/observe/core/external.js +2 -2
  15. package/dist/observe/core/graph.js +25 -25
  16. package/dist/observe/core/heap.js +9 -9
  17. package/dist/observe/core/lanes.js +206 -160
  18. package/dist/observe/core/owner.js +27 -27
  19. package/dist/observe/core/scheduler.js +92 -86
  20. package/dist/observe/core/verdict.js +66 -54
  21. package/dist/observe/map.js +35 -35
  22. package/dist/observe/reveal.js +24 -24
  23. package/dist/observe/signals.js +38 -28
  24. package/dist/observe/store/affects.js +75 -68
  25. package/dist/observe/store/hooks.js +13 -0
  26. package/dist/observe/store/index.js +0 -2
  27. package/dist/observe/store/optimistic.js +67 -34
  28. package/dist/observe/store/projection.js +37 -31
  29. package/dist/observe/store/reconcile.js +32 -30
  30. package/dist/observe/store/store.js +128 -85
  31. package/dist/prod/affects.js +34 -31
  32. package/dist/prod/boundaries.js +143 -109
  33. package/dist/prod/core/async.js +64 -67
  34. package/dist/prod/core/constants.js +7 -2
  35. package/dist/prod/core/context.js +3 -3
  36. package/dist/prod/core/core.js +302 -270
  37. package/dist/prod/core/effect.js +38 -38
  38. package/dist/prod/core/external.js +4 -4
  39. package/dist/prod/core/graph.js +27 -27
  40. package/dist/prod/core/heap.js +30 -30
  41. package/dist/prod/core/lanes.js +257 -211
  42. package/dist/prod/core/owner.js +39 -39
  43. package/dist/prod/core/scheduler.js +125 -119
  44. package/dist/prod/core/verdict.js +70 -58
  45. package/dist/prod/map.js +73 -73
  46. package/dist/prod/reveal.js +38 -38
  47. package/dist/prod/signals.js +30 -20
  48. package/dist/prod/store/affects.js +75 -68
  49. package/dist/prod/store/hooks.js +13 -0
  50. package/dist/prod/store/index.js +0 -2
  51. package/dist/prod/store/optimistic.js +114 -81
  52. package/dist/prod/store/projection.js +30 -24
  53. package/dist/prod/store/reconcile.js +32 -30
  54. package/dist/prod/store/store.js +136 -93
  55. package/dist/types/boundaries.d.ts +4 -0
  56. package/dist/types/core/attribution.d.ts +6 -2
  57. package/dist/types/core/constants.d.ts +5 -0
  58. package/dist/types/core/core.d.ts +33 -3
  59. package/dist/types/core/lanes.d.ts +10 -0
  60. package/dist/types/core/scheduler.d.ts +12 -6
  61. package/dist/types/core/types.d.ts +0 -6
  62. package/dist/types/store/affects.d.ts +11 -0
  63. package/dist/types/store/hooks.d.ts +48 -0
  64. package/dist/types/store/index.d.ts +0 -1
  65. package/dist/types/store/store.d.ts +3 -38
  66. 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
+ bv as OBSERVE,
3
+ bF as setAttributionHooks,
4
+ bG 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,
6
+ ap as emitDiagnostic,
7
+ bH as ownerPath,
8
+ aD as reportDiagnostic,
9
+ bI as isSuppressed,
10
+ bJ as blockersOf,
11
+ bK as CONFIG_PLUMBING,
12
+ bL as anyExcluded,
13
+ bM as isExcluded,
14
+ bN as CONFIG_WIDE,
15
15
  a as CONFIG_OVERRIDE,
16
16
  C as CONFIG_GUESS,
17
17
  G as GlobalQueue,
18
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
19
+ bO as GRAPH_SIZE_WARN_AT,
20
+ bP as noteFanOut,
21
+ bQ as CONFIG_SUPERSEDES,
22
+ aV 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. */
@@ -1913,8 +1913,17 @@ function checkOptimisticRevert(el, shown, truth, how) {
1913
1913
  );
1914
1914
  }
1915
1915
  // --- Graph growth -----------------------------------------------------------------
1916
- /** Per route: the graph's size at its last `visits` settles, oldest first. */
1916
+ /**
1917
+ * Per route: at its last `visits` compared settles, oldest first, the
1918
+ * graph's size and the part of it that was already alive when that
1919
+ * navigation opened.
1920
+ */
1917
1921
  const routeCounts = new Map();
1922
+ /**
1923
+ * The route the app last arrived on: a navigation that stays on it (only
1924
+ * its params or query change) continues the visit and is not compared.
1925
+ */
1926
+ let visitRoute;
1918
1927
  /**
1919
1928
  * The live graph's size — a walk, not a counter: nothing is charged at node
1920
1929
  * creation, disposal, write or re-run; the engine asks at a navigation's
@@ -1929,26 +1938,42 @@ const routeCounts = new Map();
1929
1938
  * counted unless a subscription still reaches them.
1930
1939
  */
1931
1940
  function graphSize() {
1941
+ return walkGraph();
1942
+ }
1943
+ /**
1944
+ * The walk behind `graphSize()`. Given `before` alone, it adds every node it
1945
+ * meets to that set: what is alive as a navigation opens. Given `before` and
1946
+ * `survived`, it also counts into `survived` the nodes it meets that are in
1947
+ * the set, and an edge when both its ends are: what outlived the navigation.
1948
+ * Nothing is retained — the set is weak.
1949
+ */
1950
+ function walkGraph(before, survived) {
1932
1951
  const roots = liveRootOwners();
1933
- const size = { roots: roots.length, owners: 0, computations: 0, signals: 0, edges: 0 };
1952
+ const size = { roots: 0, owners: 0, computations: 0, signals: 0, edges: 0 };
1934
1953
  const seen = new Set();
1935
1954
  // A worklist, not recursion: a subscriber chain can be thousands deep.
1936
1955
  const work = [];
1937
1956
  const stack = [];
1957
+ const count = (node, key) => {
1958
+ size[key]++;
1959
+ if (survived !== undefined) {
1960
+ if (before.has(node)) survived[key]++;
1961
+ } else if (before !== undefined) before.add(node);
1962
+ };
1938
1963
  const meet = node => {
1939
1964
  if (seen.has(node)) return;
1940
1965
  seen.add(node);
1941
- if (typeof node._fn === "function") size.computations++;
1942
- else size.signals++;
1966
+ count(node, typeof node._fn === "function" ? "computations" : "signals");
1943
1967
  work.push(node);
1944
1968
  };
1945
1969
  for (const root of roots) {
1946
- size.owners++;
1970
+ count(root, "roots");
1971
+ count(root, "owners");
1947
1972
  // Iterative: a deep tree must not grow the call stack.
1948
1973
  let child = root._firstChild;
1949
1974
  for (;;) {
1950
1975
  while (child !== null) {
1951
- size.owners++;
1976
+ count(child, "owners");
1952
1977
  if (typeof child._fn === "function") meet(child);
1953
1978
  if (child._firstChild !== null) stack.push(child);
1954
1979
  child = child._nextSibling;
@@ -1967,10 +1992,20 @@ function graphSize() {
1967
1992
  for (let link = node._deps; link !== null; link = link._nextDep) {
1968
1993
  size.edges++;
1969
1994
  meet(link._dep);
1995
+ if (survived !== undefined && before.has(node) && before.has(link._dep)) survived.edges++;
1970
1996
  }
1971
1997
  }
1972
1998
  return size;
1973
1999
  }
2000
+ /**
2001
+ * openNavigation, while the growth check is on: what is alive as a
2002
+ * navigation opens, for its settle to tell apart from what it created.
2003
+ */
2004
+ function snapshotGraph() {
2005
+ const before = new WeakSet();
2006
+ walkGraph(before);
2007
+ return before;
2008
+ }
1974
2009
  /**
1975
2010
  * settleNavigation: the app has finished moving to a route — count the live
1976
2011
  * graph. One record per settle for a listener; the growth check compares
@@ -1979,46 +2014,77 @@ function graphSize() {
1979
2014
  * `createRoot` in an effect with no dispose, a subscription a component
1980
2015
  * registered outside its owner. Nothing here runs per node; the walk is
1981
2016
  * the cost, at navigation cadence.
2017
+ *
2018
+ * A count taken at an arrival also holds everything the visit just created,
2019
+ * so a page whose correctly owned content is bigger each visit climbs too.
2020
+ * What tells the two apart is what outlived the navigation: the nodes alive
2021
+ * when it opened (`NavState.before`) that are still alive at its settle. A
2022
+ * visit's own content is not among them; what an earlier visit left behind
2023
+ * is, whatever the content does. A series reports only when both climb.
1982
2024
  */
1983
2025
  const GRAPH_SERIES = ["owners", "computations", "signals", "edges"];
1984
- function trackGraph(navigation) {
2026
+ function trackGraph(state) {
2027
+ const navigation = state.event;
2028
+ const before = state.before;
2029
+ state.before = undefined;
1985
2030
  const cfg = options.graphGrowth;
1986
2031
  if (cfg === false && !records.observed("graph")) return;
1987
- const size = graphSize();
2032
+ const survived = before && {
2033
+ roots: 0,
2034
+ owners: 0,
2035
+ computations: 0,
2036
+ signals: 0,
2037
+ edges: 0
2038
+ };
2039
+ const size = walkGraph(before, survived);
1988
2040
  const route = navigation.name ?? navigation.to;
1989
2041
  const event = { at: now(), ...size, navigation };
1990
2042
  if (route !== undefined) event.route = route;
1991
2043
  records.emit("graph", event, undefined);
2044
+ if (cfg === false || route === undefined) return;
2045
+ // A navigation that stays on the route pattern continues the visit: the
2046
+ // page it kept outlived the move by design, so comparing it would read the
2047
+ // page itself as growth. Only arrivals that enter the route are compared —
2048
+ // a leak made during a visit is still alive when the next one starts. A
2049
+ // superseded navigation never showed, so the app is still where it was.
2050
+ const left = visitRoute;
2051
+ if (navigation.outcome !== "superseded") visitRoute = route;
1992
2052
  // The initial declaration is not a visit (it settles before route content
1993
2053
  // mounts — the same gate the feedback folds apply as `event.initial !== true`);
1994
2054
  // `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
1995
- if (cfg === false || route === undefined || navigation.initial) return;
2055
+ // A navigation opened before the check was on has no snapshot to compare.
2056
+ if (navigation.initial || route === left || survived === undefined) return;
1996
2057
  let history = routeCounts.get(route);
1997
2058
  if (history === undefined) routeCounts.set(route, (history = []));
1998
- history.push(size);
2059
+ history.push({ size, survived });
1999
2060
  if (history.length > cfg.visits) history.shift();
2000
2061
  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
- });
2062
+ const totals = history.map(h => h.size);
2063
+ const kept = history.map(h => h.survived);
2064
+ const climbs = (series, key) => {
2065
+ for (let i = 1; i < series.length; i++) if (series[i][key] <= series[i - 1][key]) return false;
2066
+ return series[series.length - 1][key] >= series[0][key] * cfg.ratio;
2067
+ };
2068
+ // Any series climbing on every visit to the ratio — in the totals and in
2069
+ // what outlived each navigation — is growth; which ones did says what
2070
+ // leaked — computations with owners flat is an ownerless effect per visit,
2071
+ // edges alone is a subscription per visit to something long-lived, owners
2072
+ // is an undisposed root.
2073
+ const grew = GRAPH_SERIES.filter(key => climbs(totals, key) && climbs(kept, key));
2010
2074
  if (grew.length === 0) return;
2011
2075
  // The count is the whole graph's, so a leak shows at every route's settle;
2012
2076
  // the first route to complete its climb reports, naming the others seen.
2013
2077
  const routes = [...routeCounts.keys()];
2014
- const series = grew.map(key => `${key} ${history.map(h => h[key]).join(" → ")}`).join("; ");
2078
+ const join = (series, key) => series.map(h => h[key]).join(" → ");
2079
+ const series = grew.map(key => `${key} ${join(kept, key)} (of ${join(totals, key)})`).join("; ");
2015
2080
  const shape = grew.includes("owners")
2016
2081
  ? "a createRoot() in an effect or handler with no dispose, or a Portal or panel mounted per visit"
2017
2082
  : grew.includes("computations")
2018
2083
  ? "an effect or memo created with no owner (a module-level or callback createEffect) that only its sources keep alive"
2019
2084
  : "a subscription per visit to something long-lived — a global store or signal read by a computation that outlives the visit";
2020
2085
  const message =
2021
- `[GRAPH_GROWTH] the live graph grew on ${cfg.visits} consecutive visits to ${route}: ${series} ` +
2086
+ `[GRAPH_GROWTH] the live graph grew on ${cfg.visits} consecutive visits to ${route}, and so did ` +
2087
+ `the part that outlived each navigation there: ${series} ` +
2022
2088
  `(${size.roots} roots)${routes.length > 1 ? `, across visits to ${routes.join(", ")}` : ""}. Each ` +
2023
2089
  `visit left something behind that the next did not reclaim — the shape says ${shape}. Dispose ` +
2024
2090
  `what a visit creates (onCleanup, or return the disposer from onSettled) and own it under the ` +
@@ -2026,7 +2092,8 @@ function trackGraph(navigation) {
2026
2092
  const data = {
2027
2093
  route,
2028
2094
  grew,
2029
- history: history.map(h => ({ ...h })),
2095
+ history: totals,
2096
+ survived: kept,
2030
2097
  roots: size.roots,
2031
2098
  routes
2032
2099
  };
@@ -2159,6 +2226,14 @@ const openNavs = new Set();
2159
2226
  let navigationLog = [];
2160
2227
  /** Drains completed since enable() — the clock `writeDrain` reads. */
2161
2228
  let drainSeq = 0;
2229
+ /**
2230
+ * Navigations settled in a landing, waiting for their graph count: the hook
2231
+ * fires before the landing's commits dispose the frame parked for the page
2232
+ * left, which the walk would still reach through the sources both pages read
2233
+ * (#3989). `land()` runs only inside `flush()`'s drain, so `flushEnd` counts
2234
+ * them, after those commits — the instant a one-drain navigation is counted.
2235
+ */
2236
+ const landedGraphs = [];
2162
2237
  /**
2163
2238
  * Copy what the router currently says onto the frame (what writes stamped —
2164
2239
  * `formatOrigin` reads it) and the event. Called when the frame opens, when a
@@ -2193,6 +2268,8 @@ function openNavigation(frame, ref) {
2193
2268
  held: false,
2194
2269
  writeDrain: drainSeq
2195
2270
  };
2271
+ // The initial declaration is never compared (see `trackGraph`).
2272
+ if (options.graphGrowth !== false && ref.initial !== true) state.before = snapshotGraph();
2196
2273
  syncNavigation(state);
2197
2274
  navStates.set(frame, state);
2198
2275
  openNavs.add(state);
@@ -2268,7 +2345,7 @@ function settleNavigations(t, hold) {
2268
2345
  if (state === undefined || !openNavs.has(state)) continue;
2269
2346
  // A navigation whose frame is still open when its transition commits
2270
2347
  // (`until()` inside the frame) settles here too: its writes are through.
2271
- settleNavigation(state, state.held ? "held" : "committed", hold);
2348
+ settleNavigation(state, state.held ? "held" : "committed", hold, true);
2272
2349
  }
2273
2350
  }
2274
2351
  let openFlush = null;
@@ -2384,11 +2461,14 @@ function trackFlushEnd() {
2384
2461
  drainSeq++;
2385
2462
  // The swaps this drain committed have rendered.
2386
2463
  displayFallbacks();
2464
+ // Landed first: they settled before anything this drain settles below.
2465
+ for (let i = 0; i < landedGraphs.length; i++) trackGraph(landedGraphs[i]);
2466
+ landedGraphs.length = 0;
2387
2467
  for (const state of openNavs)
2388
2468
  if (state.open === 0 && !state.held) settleNavigation(state, "committed");
2389
2469
  for (const state of openInteractions) maybeSettleInteraction(state);
2390
2470
  }
2391
- function settleNavigation(state, outcome, hold) {
2471
+ function settleNavigation(state, outcome, hold, landed) {
2392
2472
  if (!openNavs.has(state)) return;
2393
2473
  openNavs.delete(state);
2394
2474
  syncNavigation(state);
@@ -2401,7 +2481,8 @@ function settleNavigation(state, outcome, hold) {
2401
2481
  // route's responsiveness in the feedback tables. Consumers get the record.
2402
2482
  if (event.initial !== true) for (const f of folds) f.navigation?.(event);
2403
2483
  records.emit("navigation", event, undefined);
2404
- trackGraph(event);
2484
+ if (landed) landedGraphs.push(state);
2485
+ else trackGraph(state);
2405
2486
  // The interaction that performed it may have been waiting only on this.
2406
2487
  const under = openInteractionOf(event.interaction);
2407
2488
  if (under !== undefined) maybeSettleInteraction(under);
@@ -2885,8 +2966,10 @@ function resetTracking() {
2885
2966
  stagedFallbacks.delete(DRAIN);
2886
2967
  trackingGen++;
2887
2968
  openNavs.clear();
2969
+ landedGraphs.length = 0;
2888
2970
  openInteractions.clear();
2889
2971
  routeCounts.clear();
2972
+ visitRoute = undefined;
2890
2973
  drainSeq = 0;
2891
2974
  originFrames.length = 0;
2892
2975
  effectStack.length = 0;