@solidjs/router 2.0.0-next.36 → 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
  }
@@ -610,7 +612,7 @@ function createPathsProxy(renderPath = p => p, base = "") {
610
612
 
611
613
  // The light half of server component routes: the brand the router core reads
612
614
  // off a route `component`, and the derivation of a route's call arguments.
613
- // Deliberately free of `query`/`dynamic` — routing.ts imports this module,
615
+ // Deliberately free of `query`/`dynamicComponent` — routing.ts imports this module,
614
616
  // and routing.ts is in every app's eager graph. The heavy half (the helper
615
617
  // that builds the branded component) lives in serverRouteComponent.ts and only
616
618
  // enters a bundle when an app calls `serverRouteComponent()`.
@@ -674,6 +676,86 @@ function serverRouteArgsEqual(a, b) {
674
676
  return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
675
677
  }
676
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
+
677
759
  const MAX_REDIRECTS = 100;
678
760
 
679
761
  /**
@@ -759,6 +841,8 @@ const useLocation = () => useRouter().location;
759
841
  /**
760
842
  * Retrieves a signal that indicates whether the router is currently processing a navigation.
761
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`.
762
846
  *
763
847
  * @example
764
848
  * ```js
@@ -771,7 +855,7 @@ const useLocation = () => useRouter().location;
771
855
  * );
772
856
  * ```
773
857
  */
774
- const useIsRouting = () => useRouter().isRouting;
858
+ const useIsRouting = () => routingState(useRouter());
775
859
 
776
860
  /**
777
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.
@@ -959,10 +1043,7 @@ const useLinkState = (href, options = {}) => {
959
1043
  // observe the committed location during a transition
960
1044
  pending: createMemo(() => {
961
1045
  state(); // location dependency: mid-flight target swaps recompute
962
- return router.isRouting() && !!router.pendingTarget && matchLink({
963
- pathname: router.pendingTarget.value,
964
- search: ""
965
- }, to(), base, options.end).active;
1046
+ return linkPending(router, to(), base, options.end);
966
1047
  })
967
1048
  };
968
1049
  };
@@ -1320,7 +1401,12 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1320
1401
  _navigation: isServer ? undefined : 1
1321
1402
  });
1322
1403
  }
1323
- 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);
1324
1410
  // The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
1325
1411
  // must be appended before streaming flushes the response headers, and an
1326
1412
  // unread outcome must not haunt a later request's render. Only detection
@@ -1397,7 +1483,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1397
1483
  // NotReadyError's source must be a reactive async node, not the raw
1398
1484
  // Promise. Keep one reader per boundary for this router owner so rejection
1399
1485
  // is delivered through the graph's error channel and resolution wakes the
1400
- // 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.
1401
1489
  const routerOwner = getOwner();
1402
1490
  const lazyReaders = new WeakMap();
1403
1491
  const readLazySubtree = record => {
@@ -1406,8 +1494,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1406
1494
  read = runWithOwner(routerOwner, () => createMemo(() => {
1407
1495
  const result = resolveLazySubtree(record);
1408
1496
  return result instanceof Promise ? result.then(() => undefined) : undefined;
1409
- }, DEV && {
1410
- name: "lazyRoutes"
1497
+ }, {
1498
+ transparent: true,
1499
+ ...(DEV && {
1500
+ name: "lazyRoutes"
1501
+ })
1411
1502
  }));
1412
1503
  lazyReaders.set(record, read);
1413
1504
  }
@@ -1442,28 +1533,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1442
1533
  }, DEV && {
1443
1534
  name: "matches"
1444
1535
  });
1445
- const routingPending = createMemo(() => isPending(() => {
1446
- try {
1447
- matches();
1448
- } catch (e) {
1449
- if (e instanceof NotReadyError) throw e;
1450
- }
1451
- location.search;
1452
- location.hash;
1453
- }), DEV && {
1454
- name: "routingPending"
1455
- });
1456
- 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);
1457
1539
  const transitionIntent = () => {
1458
- if (!isPending(source)) return;
1459
- const navigation = latest(source)._navigation;
1540
+ const head = headedLocation();
1541
+ if (!unsettled(head)) return;
1542
+ const navigation = head._navigation;
1460
1543
  return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
1461
1544
  };
1462
- const pendingNavigation = () => {
1463
- if (!isRouting()) return;
1464
- const target = latest(source);
1465
- return target._navigation && target._navigation > 0 ? target : undefined;
1466
- };
1467
1545
  const buildParams = () => mergeParams(matches());
1468
1546
  const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
1469
1547
  const params = wrapParams(buildParams);
@@ -1481,11 +1559,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1481
1559
  location,
1482
1560
  params,
1483
1561
  wrapParams,
1484
- isRouting,
1485
1562
  intent: transitionIntent,
1486
- get pendingTarget() {
1487
- return pendingNavigation();
1488
- },
1563
+ _source: source,
1564
+ _owner: routerOwner,
1489
1565
  renderPath,
1490
1566
  parsePath,
1491
1567
  navigatorFactory,
@@ -1546,10 +1622,10 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1546
1622
  // the destination the leave guard is told all read it. A write of this
1547
1623
  // same tick is not in it (A28) — see `compose` below.
1548
1624
  //
1549
- // A redirect hop: the previous navigation is still pending, or has landed
1550
- // but not yet reached history (a guard redirecting in the landing flush
1551
- // — its destination was never shown either way).
1552
- 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();
1553
1629
 
1554
1630
  // A composed target (`setSearchParams`) is a function of where the
1555
1631
  // router is heading, an unflushed write of this tick included. Only
@@ -1579,7 +1655,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1579
1655
  resolvedTo = resolveTarget(to);
1580
1656
  if (resolvedTo === undefined) throw unroutable(to);
1581
1657
  }
1582
- 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;
1583
1659
  if (navigationDepth >= MAX_REDIRECTS) {
1584
1660
  throw new Error("Too many redirects");
1585
1661
  }
@@ -1833,9 +1909,12 @@ function Routes(props) {
1833
1909
  // they stay subscribed to `matches` and crash on a later navigation (#451)
1834
1910
  onCleanup(() => disposers.forEach(dispose => dispose()));
1835
1911
  // Route roots must outlive re-runs of the `routeStates` memo below, so they
1836
- // are created under the owner of this component rather than the memo's
1837
- // computation (which disposes its children every time it re-runs).
1838
- 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();
1839
1918
  const routeStates = createMemo(prev => {
1840
1919
  // While a lazy subtree resolves, `matches()` is not ready and this
1841
1920
  // computation parks with it — no route contexts are created against
@@ -2127,6 +2206,7 @@ function memoryHistory(initial = "/") {
2127
2206
  }
2128
2207
 
2129
2208
  const STORAGE_KEY = "solid-router:scroll";
2209
+ const AUTO_KEY = "solid-router:scroll-auto";
2130
2210
 
2131
2211
  /**
2132
2212
  * Explicit scroll restoration for back/forward navigation. The browser's
@@ -2141,7 +2221,7 @@ const STORAGE_KEY = "solid-router:scroll";
2141
2221
  * so restoration survives reloads, which `scrollRestoration = "manual"`
2142
2222
  * otherwise disables.
2143
2223
  *
2144
- * Restoration is a single scroll once routing settles — the same strategy
2224
+ * Restoration is a single scroll once the traversal settles — the same strategy
2145
2225
  * SvelteKit, TanStack Router and React Router use. Settling after the
2146
2226
  * transition commits is what makes the offset reachable; chasing a still-
2147
2227
  * growing document afterwards (a ResizeObserver re-asserting the offset as
@@ -2151,17 +2231,49 @@ const STORAGE_KEY = "solid-router:scroll";
2151
2231
  * layout changes can feed it back into itself. Content that commits after the
2152
2232
  * transition settles — an image without reserved space, a boundary below the
2153
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.
2154
2250
  */
2155
- function createScrollRestoration() {
2156
- window.history.scrollRestoration = "manual";
2251
+ function createScrollRestoration(hydrating) {
2252
+ const h = window.history;
2157
2253
  // the current entry needs its depth stamp for captures to have a key, even
2158
2254
  // if something replaced history.state after the adapter stamped it
2159
2255
  saveCurrentDepth();
2256
+ const depth = () => window.history.state && window.history.state._depth;
2160
2257
  let positions = {};
2258
+ let handedBack = {};
2161
2259
  try {
2162
2260
  positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
2261
+ handedBack = JSON.parse(sessionStorage.getItem(AUTO_KEY)) || {};
2163
2262
  } catch {}
2164
- 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;
2165
2277
  let programmatic = false;
2166
2278
  let pending;
2167
2279
  const unbind = [bindEvent(window, "scroll", () => {
@@ -2170,10 +2282,16 @@ function createScrollRestoration() {
2170
2282
  // the user took over — a pending restore would yank them
2171
2283
  if (!programmatic) pending = undefined;
2172
2284
  }), bindEvent(window, "pagehide", () => {
2285
+ h.scrollRestoration = "auto";
2286
+ const d = depth();
2287
+ if (d != null) handedBack[d] = 1;
2173
2288
  try {
2174
2289
  sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
2290
+ sessionStorage.setItem(AUTO_KEY, JSON.stringify(handedBack));
2175
2291
  } catch {}
2176
- })];
2292
+ }), bindEvent(window, "pageshow", e => {
2293
+ if (e.persisted) manual();
2294
+ }), bindEvent(window, "load", () => deferred && (timer = setTimeout(claim))), () => clearTimeout(timer)];
2177
2295
  const restore = () => {
2178
2296
  if (pending == null) return;
2179
2297
  const y = positions[pending];
@@ -2186,42 +2304,38 @@ function createScrollRestoration() {
2186
2304
  programmatic = false;
2187
2305
  };
2188
2306
  return {
2189
- /** 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
+ */
2190
2313
  onPop() {
2314
+ claim();
2191
2315
  pending = depth();
2316
+ runWithOwner(null, () => onSettled(restore));
2192
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,
2193
2322
  /** After a push: forward entries died, and this depth may be reused. */
2194
2323
  onPush() {
2195
2324
  const d = depth();
2196
- 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
+ }
2197
2329
  },
2198
- create(router) {
2199
- // Restore once the traversal has settled: key on the location (a fully
2200
- // synchronous pop commits without isRouting ever flipping) and on
2201
- // isRouting, which reports in-flight transitions — native pops
2202
- // included — and holds the restore until they commit. restore() no-ops
2203
- // unless a traversal marked a target, so push navigations are inert.
2204
- // `transparent` keeps the effect invisible to the hydration id scheme —
2205
- // same reasoning as the link-claims effect (claims.ts): this setup is
2206
- // client-only, so an id-consuming node here has no server counterpart
2207
- // and every hydration id allocated after it shifts by one child slot.
2208
- // The visible failure is any <Loading> content that settled before the
2209
- // shell flush (a cache hit, a preloaded query): its serialized value and
2210
- // inlined markup are keyed under the server's ids, the shifted client
2211
- // misses both, recomputes, and re-renders the route fresh — duplicating
2212
- // the server DOM and leaving it inert.
2213
- createEffect(() => ({
2214
- url: router.location.pathname + router.location.search + router.location.hash,
2215
- routing: router.isRouting()
2216
- }), current => {
2217
- if (!current.routing) restore();
2218
- }, {
2219
- transparent: true
2220
- });
2330
+ create() {
2221
2331
  onCleanup(() => unbind.forEach(u => u()));
2222
2332
  // reload/back_forward document loads land on an existing entry (a fresh
2223
- // navigation starts a new one and belongs at the top); the effect's
2224
- // 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
+ }
2225
2339
  const [nav] = performance.getEntriesByType?.("navigation");
2226
2340
  if (nav && nav.type !== "navigate") pending = depth();
2227
2341
  }
@@ -2237,6 +2351,7 @@ function withScrollRestoration(history, restoration) {
2237
2351
  return {
2238
2352
  ...history,
2239
2353
  set(next) {
2354
+ restoration.beforeWrite();
2240
2355
  history.set(next);
2241
2356
  next.replace || restoration.onPush();
2242
2357
  },
@@ -2384,8 +2499,8 @@ function describeInitial(match, location) {
2384
2499
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
2385
2500
  function createIntegration(history, match) {
2386
2501
  let committing = false;
2387
- // Written, not yet in history (see `RouterIntegration.inflight`).
2388
- let inflight;
2502
+ // Writes whose transition has landed (see `RouterIntegration.settled`).
2503
+ const settled = new WeakSet();
2389
2504
  const wrap = value => typeof value === "string" ? {
2390
2505
  value
2391
2506
  } : value;
@@ -2404,14 +2519,16 @@ function createIntegration(history, match) {
2404
2519
  // the no-op rule compares against it, so `navigate()` behind another
2405
2520
  // write in one handler sees that write rather than the flushed world.
2406
2521
  write(headed => (written = resolveLocationWrite(headed, next)) || headed);
2407
- if (written && written._navigation && written._navigation > 0) {
2522
+ if (written && written._navigation) {
2408
2523
  const next = written;
2409
- inflight = next;
2410
2524
  // Register out of band so a destination error boundary replacing the
2411
- // 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.
2412
2529
  runWithOwner(null, () => onSettled(() => {
2413
- if (inflight === next) inflight = undefined;
2414
- if (read() !== next) return;
2530
+ settled.add(next);
2531
+ if (next._navigation < 0 || read() !== next) return;
2415
2532
  committing = true;
2416
2533
  try {
2417
2534
  history.set(next);
@@ -2445,7 +2562,7 @@ function createIntegration(history, match) {
2445
2562
  }));
2446
2563
  return {
2447
2564
  signal,
2448
- inflight: () => inflight,
2565
+ settled: write => settled.has(write),
2449
2566
  utils: history.utils
2450
2567
  };
2451
2568
  }
@@ -2510,7 +2627,7 @@ function createRouter(config) {
2510
2627
  let restoration;
2511
2628
  let history = config.history;
2512
2629
  if (!isServer && (config.scrollRestoration ?? !history)) {
2513
- restoration = createScrollRestoration();
2630
+ restoration = createScrollRestoration(isHydrating());
2514
2631
  history = withScrollRestoration(history || browserHistory(), restoration);
2515
2632
  }
2516
2633
  const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
@@ -2530,10 +2647,14 @@ function createRouter(config) {
2530
2647
  actionBase: config.actionBase,
2531
2648
  transformUrl: config.transformUrl
2532
2649
  })(routerState);
2533
- setupLinkClaims(routerState, config.explicitLinks);
2650
+ setupLinkClaims(routerState, config.explicitLinks, config.links);
2534
2651
  if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
2535
- restoration && restoration.create(routerState);
2652
+ restoration && restoration.create();
2536
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());
2537
2658
  return createComponent$1(RouterContextObj, {
2538
2659
  value: routerState,
2539
2660
  get children() {
@@ -2680,7 +2801,7 @@ const useBeforeLeave = listener => {
2680
2801
  // // before: a client component exists only to make the call
2681
2802
  // const getStory = query(storyView, "story");
2682
2803
  // component: props => {
2683
- // const View = dynamic(() => getStory(props.params.id));
2804
+ // const View = dynamicComponent(() => getStory(props.params.id));
2684
2805
  // return <View>{props.children}</View>;
2685
2806
  // }
2686
2807
  // // after
@@ -2742,8 +2863,9 @@ source) {
2742
2863
  };
2743
2864
  function render(args, routeProps) {
2744
2865
  // The source may answer a component, a promise of one, or (a live query)
2745
- // successive components; `dynamic`'s memo lands each the same way.
2746
- 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()));
2747
2869
  return createComponent(View, {
2748
2870
  get children() {
2749
2871
  return routeProps.children;
@@ -4098,4 +4220,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
4098
4220
  submitServerForm: submitServerForm
4099
4221
  });
4100
4222
 
4101
- 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 };
package/dist/index.jsx CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./routers/index.js";
2
2
  export * from "./lifecycle.js";
3
3
  export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
4
+ export { pendingLinks } from "./pending.js";
4
5
  export { mergeSearchString as _mergeSearchString } from "./utils.js";
5
6
  export { int } from "./paths.js";
6
7
  export { serverRouteComponent } from "./serverRouteComponent.js";