@solidjs/router 2.0.0-next.35 → 2.0.0-next.37

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/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, OBSERVE, onSettled, getObserver, isHydrating, isHydratable, $TRACK, action as action$1 } from 'solid-js';
2
- import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, takeHydrationValue, isResponseEnvelope, REVALIDATE_HEADER, getHydrationWriter } from '@solidjs/web';
1
+ import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, isPending, NotReadyError, DEV, latest, createContext, createSignal, useContext, createComponent, createOwner, createRoot, Show, onSettled, isHydrating, OBSERVE, getObserver, isHydratable, $TRACK, action as action$1 } from 'solid-js';
2
+ import { registerElementClaim, delegateEvents, dispatchAsInteraction, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamicComponent, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, takeHydrationValue, isResponseEnvelope, REVALIDATE_HEADER, getHydrationWriter } from '@solidjs/web';
3
3
  import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
4
4
 
5
5
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
@@ -249,7 +249,9 @@ function setFormClaimHandler(handler) {
249
249
  * included (parameter order aside)
250
250
  * - `data-active` — pathname exact or prefix match (the router's root, its
251
251
  * base path, exact only)
252
- * - `data-pending` — the link is the target of an in-flight navigation
252
+ * - `data-pending` — the link is the target of an in-flight navigation;
253
+ * opt-in through `createRouter({ links: pendingLinks })`. Without the
254
+ * plugin, claims never read pending state and the sweep does not track it.
253
255
  *
254
256
  * The matching rule is `matchLink`, shared with `useLinkState`. The router
255
257
  * only touches an `aria-current` it wrote itself: one the author set (a
@@ -264,8 +266,9 @@ function setFormClaimHandler(handler) {
264
266
  * are the same one-shot untracked refresh, reading the element's current
265
267
  * `href` from the DOM.
266
268
  */
267
- function setupLinkClaims(router, explicitLinks) {
269
+ function setupLinkClaims(router, explicitLinks, links) {
268
270
  const basePath = router.base.path();
271
+ const plugin = links && links(router, basePath);
269
272
  // per-element record; `owned` is whether the `aria-current` on the element
270
273
  // is the router's, so it never writes over or removes an authored one
271
274
  const claimed = new WeakMap();
@@ -299,7 +302,7 @@ function setupLinkClaims(router, explicitLinks) {
299
302
  // read reactive sources unconditionally so the owning effect stays
300
303
  // subscribed even while the anchor is not router-managed
301
304
  const location = router.location;
302
- const routing = router.isRouting();
305
+ const routing = plugin && plugin.track();
303
306
  const url = managedUrl(a);
304
307
  const target = url && url.pathname + url.search;
305
308
  // no per-anchor `end` opt-out like useLinkState has
@@ -307,13 +310,7 @@ function setupLinkClaims(router, explicitLinks) {
307
310
  active,
308
311
  current
309
312
  } = matchLink(location, target, basePath);
310
- // effects observe the committed location during a transition, so the
311
- // in-flight target comes from pendingTarget — readable here because the
312
- // isRouting write flushes after the target is assigned
313
- const pending = routing && !!router.pendingTarget && matchLink({
314
- pathname: router.pendingTarget.value,
315
- search: ""
316
- }, target, basePath).active;
313
+ const pending = !!routing && plugin.pending(target);
317
314
  return {
318
315
  active,
319
316
  pending,
@@ -326,7 +323,7 @@ function setupLinkClaims(router, explicitLinks) {
326
323
  current
327
324
  }) {
328
325
  active ? a.setAttribute("data-active", "") : a.removeAttribute("data-active");
329
- pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
326
+ if (plugin) pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
330
327
  // Ownership is read against the element, not just the record. A
331
328
  // server-component morph resets attributes to the server HTML, which
332
329
  // never carries router link state, then re-claims: an owned value that
@@ -346,9 +343,10 @@ function setupLinkClaims(router, explicitLinks) {
346
343
  const refresh = (a, rec) => untrack(() => apply(a, rec, linkState(a)));
347
344
 
348
345
  // The one subscription for every anchor: compute tracks the sources
349
- // linkState derives from (the in-flight pendingTarget is readable in the
350
- // effect phase because the isRouting write flushes after the target is
351
- // assigned), the effect phase sweeps the registry untracked.
346
+ // linkState derives from (with the plugin, its pending read — the
347
+ // in-flight target is readable in the effect phase because the isRouting
348
+ // write flushes after the target is assigned), the effect phase sweeps the
349
+ // registry untracked.
352
350
  //
353
351
  // `transparent` keeps the effect invisible to the hydration id scheme.
354
352
  // This setup is client-only, so an id-consuming node here has no server
@@ -356,7 +354,7 @@ function setupLinkClaims(router, explicitLinks) {
356
354
  // slot — lazy-route lookups miss and hydration leaves server nodes
357
355
  // unclaimed. (The option is honored by the runtime but missing from the
358
356
  // published EffectOptions type, hence the cast.)
359
- createRenderEffect(() => (router.location.pathname, router.location.search, router.isRouting()), () => registry.forEach(a => refresh(a, claimed.get(a))), {
357
+ createRenderEffect(() => (router.location.pathname, router.location.search, plugin && plugin.track()), () => registry.forEach(a => refresh(a, claimed.get(a))), {
360
358
  transparent: true
361
359
  });
362
360
  onCleanup(registerElementClaim(node => {
@@ -432,12 +430,14 @@ function setupNativeEvents({
432
430
  const to = router.parsePath(url.pathname + url.search + url.hash);
433
431
  const state = a.getAttribute("state");
434
432
  evt.preventDefault();
435
- navigateFromRoute(to, {
433
+ // Only a click the router acts on joins the event's interaction: this
434
+ // listener hears every click on the document.
435
+ dispatchAsInteraction(evt, () => navigateFromRoute(to, {
436
436
  resolve: false,
437
437
  replace: a.hasAttribute("replace"),
438
438
  scroll: !a.hasAttribute("noscroll"),
439
439
  state: state ? JSON.parse(state) : undefined
440
- });
440
+ }));
441
441
  }
442
442
  function handleAnchorPreload(evt) {
443
443
  const res = handleAnchor(evt);
@@ -477,10 +477,12 @@ function setupNativeEvents({
477
477
  const data = new FormData(form, evt.submitter);
478
478
  Promise.resolve().then(function () { return serverForms; }).then(m => m.submitServerForm(router, path, form, data));
479
479
  }
480
+ const handleSubmit = evt => dispatchAsInteraction(evt, () => handleFormSubmit(evt));
480
481
 
481
482
  // ensure delegated event run first
482
483
  delegateEvents(["click", "submit"]);
483
484
  document.addEventListener("click", handleAnchorClick);
485
+ // preloads are not interactions: those listeners run outside any frame
484
486
  if (preload) {
485
487
  document.addEventListener("mousemove", handleAnchorMove, {
486
488
  passive: true
@@ -492,7 +494,7 @@ function setupNativeEvents({
492
494
  passive: true
493
495
  });
494
496
  }
495
- document.addEventListener("submit", handleFormSubmit);
497
+ document.addEventListener("submit", handleSubmit);
496
498
  onCleanup(() => {
497
499
  document.removeEventListener("click", handleAnchorClick);
498
500
  if (preload) {
@@ -500,7 +502,7 @@ function setupNativeEvents({
500
502
  document.removeEventListener("focusin", handleAnchorPreload);
501
503
  document.removeEventListener("touchstart", handleAnchorPreload);
502
504
  }
503
- document.removeEventListener("submit", handleFormSubmit);
505
+ document.removeEventListener("submit", handleSubmit);
504
506
  });
505
507
  };
506
508
  }
@@ -544,6 +546,14 @@ const int = s => /^-?\d+$/.test(s);
544
546
  * untyped, definitionally.
545
547
  */
546
548
 
549
+ /**
550
+ * Sibling routes that share a param prefix (`/posts/:id`, `/posts/:id/edit`)
551
+ * each contribute a call at that node, and a call on an intersection of
552
+ * function types resolves to its first overload only. So the call reads its
553
+ * result back through `this` — the whole intersection — where the
554
+ * continuations of every sibling with the same arity have merged.
555
+ */
556
+
547
557
  /**
548
558
  * The type of a router instance's `paths` proxy for a given route tree.
549
559
  * Requires the tree to be a literal tuple (`as const` or a `const` type
@@ -602,7 +612,7 @@ function createPathsProxy(renderPath = p => p, base = "") {
602
612
 
603
613
  // The light half of server component routes: the brand the router core reads
604
614
  // off a route `component`, and the derivation of a route's call arguments.
605
- // Deliberately free of `query`/`dynamic` — routing.ts imports this module,
615
+ // Deliberately free of `query`/`dynamicComponent` — routing.ts imports this module,
606
616
  // and routing.ts is in every app's eager graph. The heavy half (the helper
607
617
  // that builds the branded component) lives in serverRouteComponent.ts and only
608
618
  // enters a bundle when an app calls `serverRouteComponent()`.
@@ -666,6 +676,86 @@ function serverRouteArgsEqual(a, b) {
666
676
  return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
667
677
  }
668
678
 
679
+ /**
680
+ * Pending navigation state, for the opt-in readers only: `useIsRouting`,
681
+ * `useLinkState().pending` and the `pendingLinks` claims plugin. `isPending`
682
+ * and `latest` are UI affordances — the router's own coordination reads its
683
+ * location writes and their `onSettled` instead — so an app that renders
684
+ * none of these never bundles Solid's verdict machinery.
685
+ */
686
+
687
+ const routing = new WeakMap();
688
+
689
+ /** Whether a navigation is in flight: one memo per router, under its owner. */
690
+ function routingState(router) {
691
+ let read = routing.get(router);
692
+ if (!read) {
693
+ const {
694
+ location,
695
+ matches,
696
+ _source: source
697
+ } = router;
698
+ // `transparent`: created on first use rather than at router setup, so it
699
+ // must not take a hydration id — the server may never create it, or
700
+ // create it at a different point.
701
+ const pending = runWithOwner(router._owner, () => createMemo(() => isPending(() => {
702
+ try {
703
+ matches();
704
+ } catch (e) {
705
+ if (e instanceof NotReadyError) throw e;
706
+ }
707
+ location.search;
708
+ location.hash;
709
+ }), {
710
+ transparent: true,
711
+ ...(DEV && {
712
+ name: "routingPending"
713
+ })
714
+ }));
715
+ read = () => pending() || isPending(source);
716
+ routing.set(router, read);
717
+ }
718
+ return read;
719
+ }
720
+
721
+ /**
722
+ * The target of the in-flight programmatic navigation, if any. A
723
+ * back/forward traversal (`_navigation` -1) is not a link's target.
724
+ */
725
+ function pendingTarget(router) {
726
+ if (!routingState(router)()) return;
727
+ const target = latest(router._source);
728
+ return target._navigation && target._navigation > 0 ? target : undefined;
729
+ }
730
+
731
+ /** Whether `to` is the destination of the in-flight navigation. */
732
+ function linkPending(router, to, base, end) {
733
+ const target = pendingTarget(router);
734
+ return !!target && matchLink({
735
+ pathname: target.value,
736
+ search: ""
737
+ }, to, base, end).active;
738
+ }
739
+
740
+ /**
741
+ * Opt-in `data-pending` for plain anchors. Marks claimed links whose path
742
+ * covers the in-flight destination of a link click or `navigate()` (not
743
+ * back/forward), until it lands. Agrees with `useLinkState().pending`, which
744
+ * works without it. `aria-current` and `data-active` need no plugin.
745
+ *
746
+ * @example
747
+ * ```ts
748
+ * import { createRouter, pendingLinks } from "@solidjs/router";
749
+ *
750
+ * const Router = createRouter({ routes, links: pendingLinks });
751
+ * // CSS: a[data-pending] { opacity: .6 }
752
+ * ```
753
+ */
754
+ const pendingLinks = (router, base) => ({
755
+ track: routingState(router),
756
+ pending: target => linkPending(router, target, base)
757
+ });
758
+
669
759
  const MAX_REDIRECTS = 100;
670
760
 
671
761
  /**
@@ -751,6 +841,8 @@ const useLocation = () => useRouter().location;
751
841
  /**
752
842
  * Retrieves a signal that indicates whether the router is currently processing a navigation.
753
843
  * Useful for showing pending navigation state while the next route and its data settle.
844
+ * This is the way to read routing state: `RouterContext` doesn't carry `isRouting`.
845
+ * For the in-flight destination, read the location with Solid's `isPending`/`latest`.
754
846
  *
755
847
  * @example
756
848
  * ```js
@@ -763,7 +855,7 @@ const useLocation = () => useRouter().location;
763
855
  * );
764
856
  * ```
765
857
  */
766
- const useIsRouting = () => useRouter().isRouting;
858
+ const useIsRouting = () => routingState(useRouter());
767
859
 
768
860
  /**
769
861
  * `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
@@ -951,10 +1043,7 @@ const useLinkState = (href, options = {}) => {
951
1043
  // observe the committed location during a transition
952
1044
  pending: createMemo(() => {
953
1045
  state(); // location dependency: mid-flight target swaps recompute
954
- return router.isRouting() && !!router.pendingTarget && matchLink({
955
- pathname: router.pendingTarget.value,
956
- search: ""
957
- }, to(), base, options.end).active;
1046
+ return linkPending(router, to(), base, options.end);
958
1047
  })
959
1048
  };
960
1049
  };
@@ -1312,7 +1401,12 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1312
1401
  _navigation: isServer ? undefined : 1
1313
1402
  });
1314
1403
  }
1315
- const location = createLocation(() => source().value, () => source().state, utils.queryWrapper);
1404
+
1405
+ // The flushed world — what `latest(source)` answers — as the location's own
1406
+ // memo sees it: it recomputes under the transition that carries a write,
1407
+ // never for a write of this same tick (A28).
1408
+ let flushed = initialSource;
1409
+ const location = createLocation(() => (flushed = source()).value, () => source().state, utils.queryWrapper);
1316
1410
  // The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
1317
1411
  // must be appended before streaming flushes the response headers, and an
1318
1412
  // unread outcome must not haunt a later request's render. Only detection
@@ -1389,7 +1483,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1389
1483
  // NotReadyError's source must be a reactive async node, not the raw
1390
1484
  // Promise. Keep one reader per boundary for this router owner so rejection
1391
1485
  // is delivered through the graph's error channel and resolution wakes the
1392
- // parked matches computation.
1486
+ // parked matches computation. `transparent`: the reader exists on the
1487
+ // client only (the server parks `matches` on the promise directly), so it
1488
+ // must not take a hydration id the server never allocated.
1393
1489
  const routerOwner = getOwner();
1394
1490
  const lazyReaders = new WeakMap();
1395
1491
  const readLazySubtree = record => {
@@ -1398,8 +1494,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1398
1494
  read = runWithOwner(routerOwner, () => createMemo(() => {
1399
1495
  const result = resolveLazySubtree(record);
1400
1496
  return result instanceof Promise ? result.then(() => undefined) : undefined;
1401
- }, DEV && {
1402
- name: "lazyRoutes"
1497
+ }, {
1498
+ transparent: true,
1499
+ ...(DEV && {
1500
+ name: "lazyRoutes"
1501
+ })
1403
1502
  }));
1404
1503
  lazyReaders.set(record, read);
1405
1504
  }
@@ -1434,28 +1533,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1434
1533
  }, DEV && {
1435
1534
  name: "matches"
1436
1535
  });
1437
- const routingPending = createMemo(() => isPending(() => {
1438
- try {
1439
- matches();
1440
- } catch (e) {
1441
- if (e instanceof NotReadyError) throw e;
1442
- }
1443
- location.search;
1444
- location.hash;
1445
- }), DEV && {
1446
- name: "routingPending"
1447
- });
1448
- const isRouting = () => routingPending() || isPending(source);
1536
+ const headedLocation = () => isServer ? untrack(source) : flushed;
1537
+ // A flushed navigation write whose settle has not run is still in flight.
1538
+ const unsettled = change => change._navigation !== undefined && !!integration.settled && !integration.settled(change);
1449
1539
  const transitionIntent = () => {
1450
- if (!isPending(source)) return;
1451
- const navigation = latest(source)._navigation;
1540
+ const head = headedLocation();
1541
+ if (!unsettled(head)) return;
1542
+ const navigation = head._navigation;
1452
1543
  return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
1453
1544
  };
1454
- const pendingNavigation = () => {
1455
- if (!isRouting()) return;
1456
- const target = latest(source);
1457
- return target._navigation && target._navigation > 0 ? target : undefined;
1458
- };
1459
1545
  const buildParams = () => mergeParams(matches());
1460
1546
  const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
1461
1547
  const params = wrapParams(buildParams);
@@ -1473,11 +1559,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1473
1559
  location,
1474
1560
  params,
1475
1561
  wrapParams,
1476
- isRouting,
1477
1562
  intent: transitionIntent,
1478
- get pendingTarget() {
1479
- return pendingNavigation();
1480
- },
1563
+ _source: source,
1564
+ _owner: routerOwner,
1481
1565
  renderPath,
1482
1566
  parsePath,
1483
1567
  navigatorFactory,
@@ -1538,10 +1622,10 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1538
1622
  // the destination the leave guard is told all read it. A write of this
1539
1623
  // same tick is not in it (A28) — see `compose` below.
1540
1624
  //
1541
- // A redirect hop: the previous navigation is still pending, or has landed
1542
- // but not yet reached history (a guard redirecting in the landing flush
1543
- // — its destination was never shown either way).
1544
- const headed = latest(source);
1625
+ // A redirect hop: the previous navigation's write has not settled — it
1626
+ // is still held, or a guard is redirecting in its landing flush — so its
1627
+ // destination was never shown.
1628
+ const headed = headedLocation();
1545
1629
 
1546
1630
  // A composed target (`setSearchParams`) is a function of where the
1547
1631
  // router is heading, an unflushed write of this tick included. Only
@@ -1571,7 +1655,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1571
1655
  resolvedTo = resolveTarget(to);
1572
1656
  if (resolvedTo === undefined) throw unroutable(to);
1573
1657
  }
1574
- const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && (isPending(source) || integration.inflight?.() === headed) ? headed._navigation : 0;
1658
+ const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && unsettled(headed) ? headed._navigation : 0;
1575
1659
  if (navigationDepth >= MAX_REDIRECTS) {
1576
1660
  throw new Error("Too many redirects");
1577
1661
  }
@@ -1825,9 +1909,12 @@ function Routes(props) {
1825
1909
  // they stay subscribed to `matches` and crash on a later navigation (#451)
1826
1910
  onCleanup(() => disposers.forEach(dispose => dispose()));
1827
1911
  // Route roots must outlive re-runs of the `routeStates` memo below, so they
1828
- // are created under the owner of this component rather than the memo's
1829
- // computation (which disposes its children every time it re-runs).
1830
- const owner = getOwner();
1912
+ // are created under an owner of this component rather than the memo's
1913
+ // computation (which disposes its children every time it re-runs). A
1914
+ // dedicated one, created before the memo: the roots draw hydration ids from
1915
+ // its counter, so the ids don't depend on whether the memo first parked on
1916
+ // a lazy subtree (the server's render can park where the client's doesn't).
1917
+ const owner = createOwner();
1831
1918
  const routeStates = createMemo(prev => {
1832
1919
  // While a lazy subtree resolves, `matches()` is not ready and this
1833
1920
  // computation parks with it — no route contexts are created against
@@ -2119,6 +2206,7 @@ function memoryHistory(initial = "/") {
2119
2206
  }
2120
2207
 
2121
2208
  const STORAGE_KEY = "solid-router:scroll";
2209
+ const AUTO_KEY = "solid-router:scroll-auto";
2122
2210
 
2123
2211
  /**
2124
2212
  * Explicit scroll restoration for back/forward navigation. The browser's
@@ -2133,7 +2221,7 @@ const STORAGE_KEY = "solid-router:scroll";
2133
2221
  * so restoration survives reloads, which `scrollRestoration = "manual"`
2134
2222
  * otherwise disables.
2135
2223
  *
2136
- * Restoration is a single scroll once routing settles — the same strategy
2224
+ * Restoration is a single scroll once the traversal settles — the same strategy
2137
2225
  * SvelteKit, TanStack Router and React Router use. Settling after the
2138
2226
  * transition commits is what makes the offset reachable; chasing a still-
2139
2227
  * growing document afterwards (a ResizeObserver re-asserting the offset as
@@ -2143,17 +2231,49 @@ const STORAGE_KEY = "solid-router:scroll";
2143
2231
  * layout changes can feed it back into itself. Content that commits after the
2144
2232
  * transition settles — an image without reserved space, a boundary below the
2145
2233
  * fold — keeps whatever offset the document can hold.
2234
+ *
2235
+ * A server-rendered document load is the exception: the browser restores it
2236
+ * natively once the document — streamed boundaries included — has loaded,
2237
+ * which a restore at the first client flush cannot match. So an entry is
2238
+ * handed back to the browser (`auto`) whenever its document may unload
2239
+ * (pagehide), and a hydrating router arriving on a handed-back entry leaves
2240
+ * the restore to the browser: it does not scroll, and it takes the entry back
2241
+ * (`manual`) only after load or at the first client navigation, whichever
2242
+ * comes first. A programmatic scroll during the load cancels the native
2243
+ * restore in Firefox and WebKit, and WebKit restores just after the load
2244
+ * event, so a `manual` write before then — inside a load listener included —
2245
+ * suppresses it; the write waits a task. Handed-back depths are tracked here
2246
+ * rather than read from `history.scrollRestoration`, which Firefox reports as
2247
+ * the mode before pagehide after a reload. An entry this router pushed and
2248
+ * never handed back stays `manual` and gets no native restore, so the router
2249
+ * restores it as on a client-rendered load.
2146
2250
  */
2147
- function createScrollRestoration() {
2148
- window.history.scrollRestoration = "manual";
2251
+ function createScrollRestoration(hydrating) {
2252
+ const h = window.history;
2149
2253
  // the current entry needs its depth stamp for captures to have a key, even
2150
2254
  // if something replaced history.state after the adapter stamped it
2151
2255
  saveCurrentDepth();
2256
+ const depth = () => window.history.state && window.history.state._depth;
2152
2257
  let positions = {};
2258
+ let handedBack = {};
2153
2259
  try {
2154
2260
  positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
2261
+ handedBack = JSON.parse(sessionStorage.getItem(AUTO_KEY)) || {};
2155
2262
  } catch {}
2156
- const depth = () => window.history.state && window.history.state._depth;
2263
+ const manual = () => {
2264
+ h.scrollRestoration = "manual";
2265
+ const d = depth();
2266
+ if (d != null) delete handedBack[d];
2267
+ };
2268
+ const d = depth();
2269
+ let deferred = !!hydrating && (h.scrollRestoration === "auto" || d != null && !!handedBack[d]);
2270
+ if (!deferred) manual();
2271
+ const claim = () => {
2272
+ if (!deferred) return;
2273
+ deferred = false;
2274
+ manual();
2275
+ };
2276
+ let timer;
2157
2277
  let programmatic = false;
2158
2278
  let pending;
2159
2279
  const unbind = [bindEvent(window, "scroll", () => {
@@ -2162,10 +2282,16 @@ function createScrollRestoration() {
2162
2282
  // the user took over — a pending restore would yank them
2163
2283
  if (!programmatic) pending = undefined;
2164
2284
  }), bindEvent(window, "pagehide", () => {
2285
+ h.scrollRestoration = "auto";
2286
+ const d = depth();
2287
+ if (d != null) handedBack[d] = 1;
2165
2288
  try {
2166
2289
  sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
2290
+ sessionStorage.setItem(AUTO_KEY, JSON.stringify(handedBack));
2167
2291
  } catch {}
2168
- })];
2292
+ }), bindEvent(window, "pageshow", e => {
2293
+ if (e.persisted) manual();
2294
+ }), bindEvent(window, "load", () => deferred && (timer = setTimeout(claim))), () => clearTimeout(timer)];
2169
2295
  const restore = () => {
2170
2296
  if (pending == null) return;
2171
2297
  const y = positions[pending];
@@ -2178,42 +2304,38 @@ function createScrollRestoration() {
2178
2304
  programmatic = false;
2179
2305
  };
2180
2306
  return {
2181
- /** When the adapter notifies a traversal: mark the target for restoration. */
2307
+ /**
2308
+ * When the adapter notifies a traversal: mark the target and restore once
2309
+ * the transition carrying it settles. The adapter notifies right before
2310
+ * the location write, in the same tick, so the settle is that write's —
2311
+ * a same-URL traversal included, which no location key would see.
2312
+ */
2182
2313
  onPop() {
2314
+ claim();
2183
2315
  pending = depth();
2316
+ runWithOwner(null, () => onSettled(restore));
2184
2317
  },
2318
+ /** The initial page settled: restore a reload/back_forward arrival. */
2319
+ settled: () => restore(),
2320
+ /** Before a client navigation writes history: the entry it creates copies the current mode. */
2321
+ beforeWrite: claim,
2185
2322
  /** After a push: forward entries died, and this depth may be reused. */
2186
2323
  onPush() {
2187
2324
  const d = depth();
2188
- if (d != null) for (const k in positions) +k >= d && delete positions[k];
2325
+ if (d != null) {
2326
+ for (const k in positions) +k >= d && delete positions[k];
2327
+ for (const k in handedBack) +k >= d && delete handedBack[k];
2328
+ }
2189
2329
  },
2190
- create(router) {
2191
- // Restore once the traversal has settled: key on the location (a fully
2192
- // synchronous pop commits without isRouting ever flipping) and on
2193
- // isRouting, which reports in-flight transitions — native pops
2194
- // included — and holds the restore until they commit. restore() no-ops
2195
- // unless a traversal marked a target, so push navigations are inert.
2196
- // `transparent` keeps the effect invisible to the hydration id scheme —
2197
- // same reasoning as the link-claims effect (claims.ts): this setup is
2198
- // client-only, so an id-consuming node here has no server counterpart
2199
- // and every hydration id allocated after it shifts by one child slot.
2200
- // The visible failure is any <Loading> content that settled before the
2201
- // shell flush (a cache hit, a preloaded query): its serialized value and
2202
- // inlined markup are keyed under the server's ids, the shifted client
2203
- // misses both, recomputes, and re-renders the route fresh — duplicating
2204
- // the server DOM and leaving it inert.
2205
- createEffect(() => ({
2206
- url: router.location.pathname + router.location.search + router.location.hash,
2207
- routing: router.isRouting()
2208
- }), current => {
2209
- if (!current.routing) restore();
2210
- }, {
2211
- transparent: true
2212
- });
2330
+ create() {
2213
2331
  onCleanup(() => unbind.forEach(u => u()));
2214
2332
  // reload/back_forward document loads land on an existing entry (a fresh
2215
- // navigation starts a new one and belongs at the top); the effect's
2216
- // initial run performs the restore after first render
2333
+ // navigation starts a new one and belongs at the top); the router calls
2334
+ // `settled` once the initial page has; a deferred arrival is the browser's
2335
+ if (deferred) {
2336
+ document.readyState === "complete" && claim();
2337
+ return;
2338
+ }
2217
2339
  const [nav] = performance.getEntriesByType?.("navigation");
2218
2340
  if (nav && nav.type !== "navigate") pending = depth();
2219
2341
  }
@@ -2229,6 +2351,7 @@ function withScrollRestoration(history, restoration) {
2229
2351
  return {
2230
2352
  ...history,
2231
2353
  set(next) {
2354
+ restoration.beforeWrite();
2232
2355
  history.set(next);
2233
2356
  next.replace || restoration.onPush();
2234
2357
  },
@@ -2376,8 +2499,8 @@ function describeInitial(match, location) {
2376
2499
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
2377
2500
  function createIntegration(history, match) {
2378
2501
  let committing = false;
2379
- // Written, not yet in history (see `RouterIntegration.inflight`).
2380
- let inflight;
2502
+ // Writes whose transition has landed (see `RouterIntegration.settled`).
2503
+ const settled = new WeakSet();
2381
2504
  const wrap = value => typeof value === "string" ? {
2382
2505
  value
2383
2506
  } : value;
@@ -2396,14 +2519,16 @@ function createIntegration(history, match) {
2396
2519
  // the no-op rule compares against it, so `navigate()` behind another
2397
2520
  // write in one handler sees that write rather than the flushed world.
2398
2521
  write(headed => (written = resolveLocationWrite(headed, next)) || headed);
2399
- if (written && written._navigation && written._navigation > 0) {
2522
+ if (written && written._navigation) {
2400
2523
  const next = written;
2401
- inflight = next;
2402
2524
  // Register out of band so a destination error boundary replacing the
2403
- // Router subtree cannot suppress the winning history commit.
2525
+ // Router subtree cannot suppress the winning history commit. The
2526
+ // settle is tied to the transition carrying the write: it fires when
2527
+ // that lands, a rejection included, and a superseded write's settle
2528
+ // fires with the transition that absorbed it.
2404
2529
  runWithOwner(null, () => onSettled(() => {
2405
- if (inflight === next) inflight = undefined;
2406
- if (read() !== next) return;
2530
+ settled.add(next);
2531
+ if (next._navigation < 0 || read() !== next) return;
2407
2532
  committing = true;
2408
2533
  try {
2409
2534
  history.set(next);
@@ -2437,7 +2562,7 @@ function createIntegration(history, match) {
2437
2562
  }));
2438
2563
  return {
2439
2564
  signal,
2440
- inflight: () => inflight,
2565
+ settled: write => settled.has(write),
2441
2566
  utils: history.utils
2442
2567
  };
2443
2568
  }
@@ -2502,7 +2627,7 @@ function createRouter(config) {
2502
2627
  let restoration;
2503
2628
  let history = config.history;
2504
2629
  if (!isServer && (config.scrollRestoration ?? !history)) {
2505
- restoration = createScrollRestoration();
2630
+ restoration = createScrollRestoration(isHydrating());
2506
2631
  history = withScrollRestoration(history || browserHistory(), restoration);
2507
2632
  }
2508
2633
  const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
@@ -2522,10 +2647,14 @@ function createRouter(config) {
2522
2647
  actionBase: config.actionBase,
2523
2648
  transformUrl: config.transformUrl
2524
2649
  })(routerState);
2525
- setupLinkClaims(routerState, config.explicitLinks);
2650
+ setupLinkClaims(routerState, config.explicitLinks, config.links);
2526
2651
  if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
2527
- restoration && restoration.create(routerState);
2652
+ restoration && restoration.create();
2528
2653
  }
2654
+ // Registered on both sides, outside the client-only branch: an owned
2655
+ // onSettled takes a hydration id on the server too, so the ids line up.
2656
+ // A no-op when the arrival is left to the browser's native restore.
2657
+ onSettled(() => restoration && restoration.settled());
2529
2658
  return createComponent$1(RouterContextObj, {
2530
2659
  value: routerState,
2531
2660
  get children() {
@@ -2672,7 +2801,7 @@ const useBeforeLeave = listener => {
2672
2801
  // // before: a client component exists only to make the call
2673
2802
  // const getStory = query(storyView, "story");
2674
2803
  // component: props => {
2675
- // const View = dynamic(() => getStory(props.params.id));
2804
+ // const View = dynamicComponent(() => getStory(props.params.id));
2676
2805
  // return <View>{props.children}</View>;
2677
2806
  // }
2678
2807
  // // after
@@ -2734,8 +2863,9 @@ source) {
2734
2863
  };
2735
2864
  function render(args, routeProps) {
2736
2865
  // The source may answer a component, a promise of one, or (a live query)
2737
- // successive components; `dynamic`'s memo lands each the same way.
2738
- const View = dynamic(() => call(args()));
2866
+ // successive components; `dynamicComponent`'s memo lands each the same
2867
+ // way. Never a tag, so the element runtime `dynamic` retains stays out.
2868
+ const View = dynamicComponent(() => call(args()));
2739
2869
  return createComponent(View, {
2740
2870
  get children() {
2741
2871
  return routeProps.children;
@@ -3349,7 +3479,12 @@ function installRouterIntegrations() {
3349
3479
  }
3350
3480
  function useSubmissions(fn, filter) {
3351
3481
  const router = useRouter();
3352
- const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
3482
+ // Submissions are immutable records, so the same records in the same order
3483
+ // are the same list: another action's submissions changing re-runs the
3484
+ // filter but must not re-run this list's readers.
3485
+ const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))), {
3486
+ equals: (a, b) => a.length === b.length && a.every((s, i) => s === b[i])
3487
+ });
3353
3488
  return new Proxy([], {
3354
3489
  get(_, property) {
3355
3490
  if (property === $TRACK) return subs();
@@ -4085,4 +4220,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
4085
4220
  submitServerForm: submitServerForm
4086
4221
  });
4087
4222
 
4088
- export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
4223
+ export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, pendingLinks, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };