@solidjs/router 2.0.0-next.30 → 2.0.0-next.32

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, isPending, DEV, latest, createComponent, createRoot, Show, createEffect, sharedConfig, OBSERVE, onSettled, getObserver, $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, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
1
+ import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, OBSERVE, onSettled, getObserver, 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';
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;
@@ -45,13 +45,16 @@ function validateSearch(schema, raw) {
45
45
  return outcome;
46
46
  }
47
47
  function extractSearchParams(url) {
48
- const params = {};
48
+ const params = Object.create(null);
49
49
  url.searchParams.forEach((value, key) => {
50
50
  if (key in params) {
51
51
  if (Array.isArray(params[key])) params[key].push(value);else params[key] = [params[key], value];
52
52
  } else params[key] = value;
53
53
  });
54
- return params;
54
+ // Keep __proto__ as an own property on the plain object returned to callers.
55
+ return {
56
+ ...params
57
+ };
55
58
  }
56
59
  function createMatcher(path, partial, matchFilters) {
57
60
  const [pattern, splat] = path.split("/*", 2);
@@ -72,13 +75,13 @@ function createMatcher(path, partial, matchFilters) {
72
75
  path: len ? "" : "/",
73
76
  params: {}
74
77
  };
75
- const matchFilter = s => matchFilters === undefined ? undefined : matchFilters[s];
78
+ const filters = matchFilters;
76
79
  for (let i = 0; i < len; i++) {
77
80
  const segment = segments[i];
78
81
  const dynamic = segment[0] === ":";
79
82
  const locSegment = dynamic ? locSegments[i] : locSegments[i].toLowerCase();
80
83
  const key = dynamic ? segment.slice(1) : segment.toLowerCase();
81
- if (dynamic && matchSegment(locSegment, matchFilter(key))) {
84
+ if (dynamic && matchSegment(locSegment, filters?.[key])) {
82
85
  match.params[key] = locSegment;
83
86
  } else if (dynamic || !matchSegment(locSegment, key)) {
84
87
  return null;
@@ -87,7 +90,7 @@ function createMatcher(path, partial, matchFilters) {
87
90
  }
88
91
  if (splat) {
89
92
  const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
90
- if (matchSegment(remainder, matchFilter(splat))) {
93
+ if (matchSegment(remainder, filters?.[splat])) {
91
94
  match.params[splat] = remainder;
92
95
  } else {
93
96
  return null;
@@ -96,21 +99,9 @@ function createMatcher(path, partial, matchFilters) {
96
99
  return match;
97
100
  };
98
101
  }
99
- function matchSegment(input, filter) {
100
- const isEqual = s => s === input;
101
- if (filter === undefined) {
102
- return true;
103
- } else if (typeof filter === "string") {
104
- return isEqual(filter);
105
- } else if (typeof filter === "function") {
106
- return filter(input);
107
- } else if (Array.isArray(filter)) {
108
- return filter.some(isEqual);
109
- } else if (filter instanceof RegExp) {
110
- return filter.test(input);
111
- }
112
- return false;
113
- }
102
+
103
+ // a string filter is the segment itself; anything else not listed never matches
104
+ const matchSegment = (input, filter) => filter === undefined || (typeof filter === "function" ? filter(input) : Array.isArray(filter) ? filter.includes(input) : filter instanceof RegExp ? filter.test(input) : filter === input);
114
105
  function scoreRoute(route) {
115
106
  const [pattern, splat] = route.pattern.split("/*", 2);
116
107
  const segments = pattern.split("/").filter(Boolean);
@@ -340,7 +331,7 @@ function setupNativeEvents({
340
331
  const target = svg ? a.target.baseVal : a.target;
341
332
  if (target || !href && !a.hasAttribute("state")) return;
342
333
  const rel = (a.getAttribute("rel") || "").split(/\s+/);
343
- if (a.hasAttribute("download") || rel && rel.includes("external")) return;
334
+ if (a.hasAttribute("download") || rel.includes("external")) return;
344
335
  const url = svg ? new URL(href, document.baseURI) : new URL(href);
345
336
  // Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs
346
337
  // inherit the page origin, so the origin check below won't reject them. #382
@@ -514,8 +505,8 @@ function createPathsProxy(renderPath = p => p, base = "") {
514
505
  };
515
506
  return new Proxy(build, {
516
507
  get(_, prop) {
517
- if (prop === "toString") return () => toHref(pathname);
518
- if (typeof prop === "symbol") return prop === Symbol.toPrimitive ? () => toHref(pathname) : prop === HREF ? pathname || "/" : undefined;
508
+ if (prop === "toString" || prop === Symbol.toPrimitive) return () => toHref(pathname);
509
+ if (typeof prop === "symbol") return prop === HREF ? pathname || "/" : undefined;
519
510
  return node(`${pathname}/${prop}`);
520
511
  }
521
512
  });
@@ -1023,9 +1014,7 @@ function createRoutes(routeDef, base = "") {
1023
1014
  for (const expandedPath of expandOptionals(originalPath)) {
1024
1015
  const path = joinPaths(base, expandedPath);
1025
1016
  let pattern = isLeaf ? path : path.split("/*", 1)[0];
1026
- pattern = pattern.split("/").map(s => {
1027
- return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
1028
- }).join("/");
1017
+ pattern = pattern.split("/").map(s => /^[:*]/.test(s) ? s : encodeSegment(s)).join("/");
1029
1018
  acc.push({
1030
1019
  ...shared,
1031
1020
  originalPath,
@@ -1116,13 +1105,12 @@ function mergeParams(matches) {
1116
1105
  return params;
1117
1106
  }
1118
1107
  function createLocation(path, state, queryWrapper) {
1119
- const origin = new URL(mockBase);
1120
- const url = createMemo((prev = origin) => {
1108
+ const url = createMemo((prev = new URL(mockBase)) => {
1121
1109
  const path_ = path();
1122
1110
  try {
1123
1111
  // anchor rooted paths against the origin explicitly - a path with
1124
1112
  // doubled leading slashes would otherwise parse as protocol-relative
1125
- return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
1113
+ return new URL(path_[0] === "/" ? mockBase + path_ : path_, mockBase);
1126
1114
  } catch (err) {
1127
1115
  DEV && console.error(`Invalid path ${path_}`);
1128
1116
  return prev;
@@ -1133,8 +1121,8 @@ function createLocation(path, state, queryWrapper) {
1133
1121
  const pathname = createMemo(() => url().pathname);
1134
1122
  const search = createMemo(() => url().search);
1135
1123
  const hash = createMemo(() => url().hash);
1136
- const key = () => "";
1137
- const queryFn = createMemo(() => extractSearchParams(url()));
1124
+ // Pathname and hash changes do not affect the query.
1125
+ const queryFn = createMemo(() => extractSearchParams(new URL(search(), mockBase)));
1138
1126
  return {
1139
1127
  get pathname() {
1140
1128
  return pathname();
@@ -1149,7 +1137,7 @@ function createLocation(path, state, queryWrapper) {
1149
1137
  return state();
1150
1138
  },
1151
1139
  get key() {
1152
- return key();
1140
+ return "";
1153
1141
  },
1154
1142
  query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
1155
1143
  };
@@ -1315,7 +1303,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1315
1303
  read = runWithOwner(routerOwner, () => createMemo(() => {
1316
1304
  const result = resolveLazySubtree(record);
1317
1305
  return result instanceof Promise ? result.then(() => undefined) : undefined;
1318
- }, {
1306
+ }, DEV && {
1319
1307
  name: "lazyRoutes"
1320
1308
  }));
1321
1309
  lazyReaders.set(record, read);
@@ -1348,7 +1336,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1348
1336
  }
1349
1337
  }
1350
1338
  return m;
1351
- }, {
1339
+ }, DEV && {
1352
1340
  name: "matches"
1353
1341
  });
1354
1342
  const routingPending = createMemo(() => isPending(() => {
@@ -1359,7 +1347,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1359
1347
  }
1360
1348
  location.search;
1361
1349
  location.hash;
1362
- }), {
1350
+ }), DEV && {
1363
1351
  name: "routingPending"
1364
1352
  });
1365
1353
  const isRouting = () => routingPending() || isPending(source);
@@ -1565,11 +1553,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1565
1553
  route,
1566
1554
  params
1567
1555
  } = next[match];
1568
- route.component && route.component.preload && route.component.preload();
1569
1556
  const {
1570
1557
  preload,
1571
1558
  component
1572
1559
  } = route;
1560
+ component?.preload?.();
1573
1561
  const now = current && current[match];
1574
1562
  const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
1575
1563
  inPreloadFn = true;
@@ -1633,7 +1621,7 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
1633
1621
  // its last valid value while this route is being torn down, so outgoing
1634
1622
  // components and preloads never observe another route's params.
1635
1623
  const params = wrapParams(() => mergeParams(matches()));
1636
- component && component.preload && component.preload();
1624
+ component?.preload?.();
1637
1625
  inPreloadFn = true;
1638
1626
  const data = preload ? preload({
1639
1627
  params,
@@ -1760,9 +1748,7 @@ function Routes(props) {
1760
1748
  next[i] = prev[i];
1761
1749
  } else {
1762
1750
  equal = false;
1763
- if (disposers[i]) {
1764
- disposers[i]();
1765
- }
1751
+ disposers[i]?.();
1766
1752
  runWithOwner(owner, () => createRoot(dispose => {
1767
1753
  disposers[i] = dispose;
1768
1754
  const routeKey = nextMatch.route.key;
@@ -1851,13 +1837,14 @@ function bindEvent(target, type, handler) {
1851
1837
 
1852
1838
  let depth;
1853
1839
  function saveCurrentDepth() {
1854
- if (!window.history.state || window.history.state._depth == null) {
1855
- window.history.replaceState({
1856
- ...window.history.state,
1857
- _depth: window.history.length - 1
1840
+ const h = window.history;
1841
+ if (!h.state || h.state._depth == null) {
1842
+ h.replaceState({
1843
+ ...h.state,
1844
+ _depth: h.length - 1
1858
1845
  }, "");
1859
1846
  }
1860
- depth = window.history.state._depth;
1847
+ depth = h.state?._depth ?? h.length - 1;
1861
1848
  }
1862
1849
  function keepDepth(state) {
1863
1850
  return {
@@ -1901,11 +1888,10 @@ function scrollToHash(hash, fallbackTop) {
1901
1888
 
1902
1889
  function browserHistory() {
1903
1890
  const getSource = () => {
1904
- const url = window.location.pathname + window.location.search;
1905
- const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
1891
+ const s = window.history.state;
1906
1892
  return {
1907
- value: url + window.location.hash,
1908
- state
1893
+ value: window.location.pathname + window.location.search + window.location.hash,
1894
+ state: s && s._depth && Object.keys(s).length === 1 ? undefined : s
1909
1895
  };
1910
1896
  };
1911
1897
  const beforeLeave = {};
@@ -2133,7 +2119,7 @@ function createScrollRestoration() {
2133
2119
  // reload/back_forward document loads land on an existing entry (a fresh
2134
2120
  // navigation starts a new one and belongs at the top); the effect's
2135
2121
  // initial run performs the restore after first render
2136
- const [nav] = performance.getEntriesByType && performance.getEntriesByType("navigation");
2122
+ const [nav] = performance.getEntriesByType?.("navigation");
2137
2123
  if (nav && nav.type !== "navigate") pending = depth();
2138
2124
  }
2139
2125
  };
@@ -2229,7 +2215,6 @@ function describeNavigation(match, next, written, from) {
2229
2215
  const t = to();
2230
2216
  return t === undefined ? undefined : new URL(t, mockBase).pathname;
2231
2217
  };
2232
- const matches = pathname => untrack(() => match(pathname));
2233
2218
  const ref = {
2234
2219
  kind: "navigation",
2235
2220
  get to() {
@@ -2237,21 +2222,62 @@ function describeNavigation(match, next, written, from) {
2237
2222
  },
2238
2223
  from,
2239
2224
  get name() {
2240
- const p = pathname();
2241
- if (p === undefined) return undefined;
2242
- const m = matches(p);
2243
- return m.length ? m[m.length - 1].route.pattern || "/" : p;
2225
+ return routeName(match, pathname());
2244
2226
  },
2245
2227
  get params() {
2246
- const p = pathname();
2247
- const m = p === undefined ? [] : matches(p);
2248
- return m.length ? mergeParams(m) : undefined;
2228
+ return routeParams(match, pathname());
2249
2229
  }
2250
2230
  };
2251
2231
  if (next._navigation !== undefined && next._navigation > 1) ref.redirect = next._navigation - 1;
2252
2232
  return ref;
2253
2233
  }
2254
2234
 
2235
+ /** The parametrized route `pathname` matches — the last match's pattern, `/` for the root; the pathname itself when nothing matched. */
2236
+ function routeName(match, pathname) {
2237
+ if (pathname === undefined) return undefined;
2238
+ const m = untrack(() => match(pathname));
2239
+ return m.length ? m[m.length - 1].route.pattern || "/" : pathname;
2240
+ }
2241
+
2242
+ /** The params the matched chain bound, merged; `undefined` when nothing matched. */
2243
+ function routeParams(match, pathname) {
2244
+ if (pathname === undefined) return undefined;
2245
+ const m = untrack(() => match(pathname));
2246
+ return m.length ? mergeParams(m) : undefined;
2247
+ }
2248
+
2249
+ /**
2250
+ * The route the document arrived on, for solid's observe tier: the same
2251
+ * declaration as a navigation's, around the work that establishes the
2252
+ * router's initial match — building its context — since a fresh document has
2253
+ * no location write to wrap. On the client the engine opens it at the time
2254
+ * origin and settles it as the context is built (the first `"navigation"`
2255
+ * record, `initial: true`); on the server the same call names the request's
2256
+ * `"render"` record (`RenderEvent.route`). Every field is a getter read at
2257
+ * settle, as for a navigation: `to` is the location as the router then holds
2258
+ * it — the arrival normalized onto the base path when it came in empty, a
2259
+ * write the frame itself took (see the setter) — and `name`/`params` match
2260
+ * it then, so a lazy subtree that resolved while the server rendered names
2261
+ * the exact route.
2262
+ */
2263
+ function describeInitial(match, location) {
2264
+ const to = () => untrack(location).value;
2265
+ const pathname = () => new URL(to(), mockBase).pathname;
2266
+ return {
2267
+ kind: "navigation",
2268
+ initial: true,
2269
+ get to() {
2270
+ return to();
2271
+ },
2272
+ get name() {
2273
+ return routeName(match, pathname());
2274
+ },
2275
+ get params() {
2276
+ return routeParams(match, pathname());
2277
+ }
2278
+ };
2279
+ }
2280
+
2255
2281
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
2256
2282
  function createIntegration(history, match) {
2257
2283
  let committing = false;
@@ -2266,7 +2292,6 @@ function createIntegration(history, match) {
2266
2292
  name: "location"
2267
2293
  });
2268
2294
  const signal = [read, next => {
2269
- if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
2270
2295
  // What the write resolved to, or undefined when there was nothing to
2271
2296
  // write (see `resolveLocationWrite`).
2272
2297
  let written;
@@ -2299,7 +2324,14 @@ function createIntegration(history, match) {
2299
2324
  // while a navigation is pending, which is the `from` a hop wants too.
2300
2325
  // A write the no-op rule drops is still declared: the engine settles a
2301
2326
  // navigation whose write "did not survive the equality gate" on the spot.
2302
- OBSERVE ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, untrack(read).value), commit) : commit();
2327
+ // A write from NO location is not a navigation: it is the arrival being
2328
+ // normalized (`createRouterContext` moving an empty location — a hash
2329
+ // history with no hash, a memory history seeded with "" — onto the base
2330
+ // path), and the arrival is already declared by the initial frame open
2331
+ // around the context build (`describeInitial`), which this write then
2332
+ // stamps. The location is never empty again after that first write.
2333
+ const from = untrack(read).value;
2334
+ OBSERVE && from ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, from), commit) : commit();
2303
2335
  }];
2304
2336
  history.init && onCleanup(history.init((value = history.get()) => {
2305
2337
  if (committing) return;
@@ -2380,11 +2412,14 @@ function createRouter(config) {
2380
2412
  }
2381
2413
  const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
2382
2414
  let context;
2383
- const routerState = createRouterContext(integration, branches, () => context, {
2415
+ const buildContext = () => createRouterContext(integration, branches, () => context, {
2384
2416
  base: basePath,
2385
2417
  singleFlight: config.singleFlight,
2386
2418
  transformUrl: config.transformUrl
2387
2419
  });
2420
+ // The route the document arrived on, declared around the initial match
2421
+ // (see `describeInitial`); folded out of shipped bundles with `OBSERVE`.
2422
+ const routerState = OBSERVE ? OBSERVE.attribution.withOrigin(describeInitial(matchPath, integration.signal[0]), buildContext) : buildContext();
2388
2423
  if (!isServer) {
2389
2424
  setupNativeEvents({
2390
2425
  preload: config.preloadLinks,
@@ -2432,11 +2467,10 @@ function createRouter(config) {
2432
2467
  }));
2433
2468
  },
2434
2469
  keysFor(url) {
2435
- const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
2436
2470
  const keys = [];
2437
2471
  for (const {
2438
2472
  route
2439
- } of matchPath(pathname)) {
2473
+ } of matchPath(pathnameOf(url))) {
2440
2474
  const key = serverRouteOf(route.component)?.call?.key;
2441
2475
  typeof key === "string" && key && !keys.includes(key) && keys.push(key);
2442
2476
  }
@@ -2623,10 +2657,22 @@ source) {
2623
2657
  const LocationHeader = "Location";
2624
2658
  const PRELOAD_TIMEOUT$1 = 5000;
2625
2659
  const CACHE_TIMEOUT = 180000;
2626
- // When this client booted. Flight-registry entries (sharedConfig.has/load)
2660
+ const responseBodyHeaders = /* #__PURE__ */new Set(["content-type", "content-length", "content-encoding", "transfer-encoding", "content-disposition", "content-language", "content-location", "content-range", "accept-ranges", "etag", "last-modified"]);
2661
+ const forwardedCookies = /* #__PURE__ */new WeakMap();
2662
+ // When this client booted. Flight-registry entries (takeHydrationValue)
2627
2663
  // hold values the server computed while rendering THIS page, so their age is
2628
2664
  // anchored here — not at whenever a late query() call happens to consume one.
2629
2665
  const bootTime = Date.now();
2666
+
2667
+ // Every hydratable read ships its entry: the writer keeps the first write
2668
+ // per key, so a read skipped in a NoHydration zone never hides the entry
2669
+ // from an island that reads the same key later.
2670
+ function serializeForHydration(key, res) {
2671
+ const writer = getHydrationWriter();
2672
+ if (!writer || !writer.async || !isHydratable()) return;
2673
+ const e = getRequestEvent();
2674
+ (!e || !e.serverOnly) && writer.write(key, res);
2675
+ }
2630
2676
  let cacheMap = new Map();
2631
2677
 
2632
2678
  // cleanup forward/back cache
@@ -2751,10 +2797,11 @@ function query(fn, name) {
2751
2797
  }
2752
2798
  let res = cached[1];
2753
2799
  if (intent !== "preload") {
2754
- res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
2800
+ res = settle(res);
2755
2801
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
2756
2802
  }
2757
2803
  inPreloadFn && "then" in res && res.catch(() => {});
2804
+ isServer && serializeForHydration(key, res);
2758
2805
  return res;
2759
2806
  }
2760
2807
  let res;
@@ -2771,13 +2818,11 @@ function query(fn, name) {
2771
2818
  // active navigation (or its preload) accepts the payload only while a
2772
2819
  // real preload of the same age would still satisfy it — anything older
2773
2820
  // refetches instead of serving a minutes-old value on a fresh navigation.
2774
- if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
2775
- const payloadAge = now - bootTime;
2776
- if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1)) {
2821
+ if (!isServer && (!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1))) {
2822
+ const entry = takeHydrationValue(key); // hydrating
2823
+ if (entry) {
2777
2824
  adopted = true;
2778
- res = sharedConfig.load(key); // hydrating
2779
- // @ts-ignore at least until we add a delete method to sharedConfig
2780
- delete globalThis._$HY.r[key];
2825
+ res = entry.status === "pending" ? entry.promise : entry.status === "resolved" ? Promise.resolve(entry.value) : Promise.reject(entry.error);
2781
2826
  }
2782
2827
  // A payload too old for this intent stays in the registry: it is
2783
2828
  // inert while the refetched entry below lives, but a later claim
@@ -2793,6 +2838,7 @@ function query(fn, name) {
2793
2838
  // it would for a preload performed when the server fetched the data.
2794
2839
  const stamp = adopted ? bootTime : now;
2795
2840
  if (cached) {
2841
+ isServer && forwardedCookies.delete(cached);
2796
2842
  cached[0] = stamp;
2797
2843
  cached[1] = res;
2798
2844
  cached[3] = intent;
@@ -2811,17 +2857,18 @@ function query(fn, name) {
2811
2857
  const e = getRequestEvent();
2812
2858
  if (e && e.router.dataOnly) return e.router.data[key] = res;
2813
2859
  }
2814
- if (intent !== "preload") {
2815
- res = "then" in res ? res.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(res);
2816
- }
2860
+ if (intent !== "preload") res = settle(res);
2817
2861
  inPreloadFn && "then" in res && res.catch(() => {});
2818
- // serialize on server
2819
- if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
2820
- const e = getRequestEvent();
2821
- (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
2822
- }
2862
+ isServer && serializeForHydration(key, res);
2823
2863
  return res;
2824
- function handleResponse(error) {
2864
+
2865
+ // a settled read: the response handling applied to the value or the promise
2866
+ function settle(v) {
2867
+ let cookies = isServer ? forwardedCookies.get(cached) : undefined;
2868
+ if (isServer && !cookies) forwardedCookies.set(cached, cookies = new WeakSet());
2869
+ return "then" in v ? v.then(handleResponse(false, cookies), handleResponse(true, cookies)) : handleResponse(false, cookies)(v);
2870
+ }
2871
+ function handleResponse(error, cookies) {
2825
2872
  return async v => {
2826
2873
  let enveloped;
2827
2874
  let hasEnveloped = false;
@@ -2834,8 +2881,18 @@ function query(fn, name) {
2834
2881
  if (v instanceof Response) {
2835
2882
  const e = getRequestEvent();
2836
2883
  if (e) {
2884
+ const headers = e.response.headers;
2885
+ const skipCookies = cookies?.has(headers);
2837
2886
  for (const [key, value] of v.headers) {
2838
- if (key == "set-cookie") e.response.headers.append("set-cookie", value);else e.response.headers.set(key, value);
2887
+ // Keep metadata for the query's body off the document response.
2888
+ if (responseBodyHeaders.has(key)) continue;
2889
+ if (key === "set-cookie") {
2890
+ if (!skipCookies) headers.append(key, value);
2891
+ } else if (headers.get(key) !== value) headers.set(key, value);
2892
+ }
2893
+ // Each cached result contributes its cookies once per document.
2894
+ if (!skipCookies && v.headers.has("set-cookie")) {
2895
+ cookies?.add(headers);
2839
2896
  }
2840
2897
  }
2841
2898
  let url = v.headers.get(LocationHeader);
@@ -2918,6 +2975,7 @@ query.set = (key, value) => {
2918
2975
  const now = Date.now();
2919
2976
  let cached = cache.get(key);
2920
2977
  if (cached) {
2978
+ isServer && forwardedCookies.delete(cached);
2921
2979
  cached[0] = now;
2922
2980
  cached[1] = Promise.resolve(value);
2923
2981
  cached[2] = value;
@@ -2965,7 +3023,7 @@ function hashKey(args) {
2965
3023
  return JSON.stringify(args, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => {
2966
3024
  result[key] = val[key];
2967
3025
  return result;
2968
- }, {}) : val);
3026
+ }, Object.create(null)) : val);
2969
3027
  }
2970
3028
  function isPlainObject(obj) {
2971
3029
  let proto;
@@ -2997,7 +3055,8 @@ const actions = /* #__PURE__ */new Map();
2997
3055
  */
2998
3056
  function handleFormAction(evt, router, actionBase) {
2999
3057
  if (evt.defaultPrevented) return;
3000
- let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : evt.target.getAttribute("action");
3058
+ const form = evt.target;
3059
+ let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : form.getAttribute("action");
3001
3060
  if (!actionRef) return;
3002
3061
  const serverAction = !actionRef.startsWith("https://action/");
3003
3062
  if (serverAction) {
@@ -3006,7 +3065,7 @@ function handleFormAction(evt, router, actionBase) {
3006
3065
  actionRef = router.parsePath(url.pathname + url.search);
3007
3066
  if (!actionRef.startsWith(actionBase)) return;
3008
3067
  }
3009
- if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
3068
+ if (form.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
3010
3069
  // A registry miss on a server-action url is a direct bind whose module
3011
3070
  // never loaded client-side (server components): the url is self-describing
3012
3071
  // (the id in the path, bound `?args` in the query), so a generic invocation
@@ -3017,11 +3076,11 @@ function handleFormAction(evt, router, actionBase) {
3017
3076
  const handler = actions.get(actionRef) || serverAction && createServerFormAction(actionRef);
3018
3077
  if (handler) {
3019
3078
  evt.preventDefault();
3020
- const data = new FormData(evt.target, evt.submitter);
3079
+ const data = new FormData(form, evt.submitter);
3021
3080
  handler.call({
3022
3081
  r: router,
3023
- f: evt.target
3024
- }, evt.target.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
3082
+ f: form
3083
+ }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
3025
3084
  }
3026
3085
  }
3027
3086
 
@@ -3128,23 +3187,21 @@ function actionImpl(fn, options = {}) {
3128
3187
  const runMutation = () => fn(...variables);
3129
3188
  const run = action$1(async function* (context) {
3130
3189
  context.optimistic?.();
3190
+ let value;
3191
+ let error = false;
3131
3192
  try {
3132
- const value = await context.call();
3133
- yield;
3134
- return {
3135
- error: false,
3136
- value
3137
- };
3138
- } catch (error) {
3139
- yield;
3140
- return {
3141
- error: true,
3142
- value: error
3143
- };
3193
+ value = await context.call();
3194
+ } catch (e) {
3195
+ value = e;
3196
+ error = true;
3144
3197
  }
3198
+ const read = await readResponse(value, error);
3199
+ yield;
3200
+ // Apply inside the transition so the default revalidation's refetch and
3201
+ // the release of the caller's optimistic writes commit as one frame (#619).
3202
+ return applyResponse(read, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
3145
3203
  });
3146
3204
  form && setFormBusy(form, 1);
3147
- let settled;
3148
3205
  let response;
3149
3206
  // The transport consumer is awaited before a single-flight mutation
3150
3207
  // resolves, so a counter delta over the call tells whether this action's
@@ -3154,13 +3211,12 @@ function actionImpl(fn, options = {}) {
3154
3211
  // which misses every response the server returned without flight data.
3155
3212
  const flightApplicationsBefore = flightApplications;
3156
3213
  try {
3157
- settled = await settleActionResult(run({
3214
+ response = await settleActionResult(run({
3158
3215
  call: runMutation,
3159
3216
  optimistic: submitHooks.size ? () => {
3160
3217
  for (const hook of submitHooks.values()) hook(...variables);
3161
3218
  } : undefined
3162
3219
  }));
3163
- response = await handleResponse(settled.value, settled.error, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
3164
3220
  } finally {
3165
3221
  form && setFormBusy(form, -1);
3166
3222
  }
@@ -3168,8 +3224,8 @@ function actionImpl(fn, options = {}) {
3168
3224
  submission = {
3169
3225
  input: variables,
3170
3226
  url,
3171
- result: response && response.data,
3172
- error: response && response.error,
3227
+ result: response?.data,
3228
+ error: response?.error,
3173
3229
  clear() {
3174
3230
  router.submissions[1](entries => entries.filter(entry => entry !== submission));
3175
3231
  },
@@ -3215,8 +3271,7 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
3215
3271
  fn.with = function (...args) {
3216
3272
  const uri = new URL(url, mockBase);
3217
3273
  uri.searchParams.set("args", hashKey(args));
3218
- const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
3219
- return next;
3274
+ return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
3220
3275
  };
3221
3276
  fn.onSubmit = function (hook) {
3222
3277
  const id = Symbol("actionOnSubmitHook");
@@ -3335,8 +3390,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
3335
3390
  cacheKeyOp(keys, entry => entry[0] = 0);
3336
3391
  // set cache — and fan the payload out to other keyed stores (live query
3337
3392
  // channels adopt delivered values instead of reconnecting)
3338
- flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
3339
- flightData && deliverFlightData(flightData);
3393
+ if (flightData) {
3394
+ Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
3395
+ deliverFlightData(flightData);
3396
+ }
3340
3397
  // trigger revalidation inside the same transition as the navigation, so
3341
3398
  // the redirect commits atomically with fresh data: surviving consumers
3342
3399
  // (shared layouts) the flight payload didn't seed refetch and hold the
@@ -3344,7 +3401,7 @@ function applyResponseMetadata(metadata, navigate, flightData) {
3344
3401
  // are fresh again by now, so the sweep re-reads them from cache.
3345
3402
  revalidate(keys, false);
3346
3403
  }
3347
- async function handleResponse(response, error, navigate, metadataHandled) {
3404
+ async function readResponse(response, error) {
3348
3405
  let data;
3349
3406
  let flightData;
3350
3407
  let metadata;
@@ -3367,6 +3424,19 @@ async function handleResponse(response, error, navigate, metadataHandled) {
3367
3424
  } else if (error) return {
3368
3425
  error: response
3369
3426
  };else data = response;
3427
+ return {
3428
+ data,
3429
+ flightData,
3430
+ metadata
3431
+ };
3432
+ }
3433
+ function applyResponse(read, navigate, metadataHandled) {
3434
+ if ("error" in read) return read;
3435
+ const {
3436
+ data,
3437
+ flightData,
3438
+ metadata
3439
+ } = read;
3370
3440
  // The transport consumer applies metadata before returning a server
3371
3441
  // function's unwrapped value. Do not treat that value as a second plain
3372
3442
  // action response and invalidate the freshly seeded query cache again.
package/dist/paths.js CHANGED
@@ -37,14 +37,10 @@ export function createPathsProxy(renderPath = p => p, base = "") {
37
37
  };
38
38
  return new Proxy(build, {
39
39
  get(_, prop) {
40
- if (prop === "toString")
40
+ if (prop === "toString" || prop === Symbol.toPrimitive)
41
41
  return () => toHref(pathname);
42
42
  if (typeof prop === "symbol")
43
- return prop === Symbol.toPrimitive
44
- ? () => toHref(pathname)
45
- : prop === HREF
46
- ? pathname || "/"
47
- : undefined;
43
+ return prop === HREF ? pathname || "/" : undefined;
48
44
  return node(`${pathname}/${prop}`);
49
45
  }
50
46
  });
@@ -74,9 +74,7 @@ export function Routes(props) {
74
74
  }
75
75
  else {
76
76
  equal = false;
77
- if (disposers[i]) {
78
- disposers[i]();
79
- }
77
+ disposers[i]?.();
80
78
  runWithOwner(owner, () => createRoot(dispose => {
81
79
  disposers[i] = dispose;
82
80
  const routeKey = nextMatch.route.key;