@rangojs/router 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/event-controller.d.ts +2 -1
  4. package/dist/types/browser/history-state.d.ts +3 -9
  5. package/dist/types/browser/navigation-store.d.ts +8 -6
  6. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  7. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  8. package/dist/types/browser/types.d.ts +2 -1
  9. package/dist/types/browser/wire-protocol.d.ts +25 -0
  10. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  11. package/dist/types/cache/cache-scope.d.ts +34 -22
  12. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  13. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  14. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  15. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  16. package/dist/types/cache/segment-codec.d.ts +14 -0
  17. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  18. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  19. package/dist/types/cache/taint.d.ts +5 -3
  20. package/dist/types/cache/types.d.ts +7 -3
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  22. package/dist/types/prefetch-markers.d.ts +5 -14
  23. package/dist/types/prerender/store.d.ts +31 -0
  24. package/dist/types/prerender/warm-request.d.ts +3 -3
  25. package/dist/types/prerender.d.ts +2 -2
  26. package/dist/types/route-name.d.ts +5 -0
  27. package/dist/types/router/error-handling.d.ts +4 -4
  28. package/dist/types/router/instrument.d.ts +4 -3
  29. package/dist/types/router/intercept-resolution.d.ts +25 -9
  30. package/dist/types/router/match-context.d.ts +4 -11
  31. package/dist/types/router/match-handlers.d.ts +2 -6
  32. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  33. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  34. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  35. package/dist/types/router/prerender-match.d.ts +21 -6
  36. package/dist/types/router/preview-match.d.ts +2 -7
  37. package/dist/types/router/request-classification.d.ts +5 -0
  38. package/dist/types/router/router-context.d.ts +7 -46
  39. package/dist/types/router/router-interfaces.d.ts +8 -14
  40. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  41. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  42. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  43. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  44. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  45. package/dist/types/router/segment-resolution.d.ts +1 -2
  46. package/dist/types/router/segment-wrappers.d.ts +11 -48
  47. package/dist/types/router/telemetry.d.ts +2 -0
  48. package/dist/types/router/types.d.ts +10 -0
  49. package/dist/types/rsc/full-payload.d.ts +2 -2
  50. package/dist/types/rsc/handler-context.d.ts +6 -6
  51. package/dist/types/rsc/json-route-result.d.ts +5 -9
  52. package/dist/types/rsc/request-stages.d.ts +203 -0
  53. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  54. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  55. package/dist/types/rsc/routine-plan.d.ts +1 -1
  56. package/dist/types/rsc/server-action.d.ts +2 -2
  57. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  58. package/dist/types/rsc/shell-capture.d.ts +132 -15
  59. package/dist/types/rsc/shell-serve.d.ts +37 -12
  60. package/dist/types/server/context.d.ts +9 -8
  61. package/dist/types/server/handle-store.d.ts +16 -0
  62. package/dist/types/server/render-pass.d.ts +72 -0
  63. package/dist/types/server/request-context.d.ts +24 -104
  64. package/dist/types/server/rewrite-trail.d.ts +2 -2
  65. package/dist/types/testing/dispatch.d.ts +35 -18
  66. package/dist/types/types/cache-types.d.ts +6 -3
  67. package/dist/types/types/error-types.d.ts +9 -1
  68. package/dist/types/types/handler-context.d.ts +9 -1
  69. package/dist/types/types/segments.d.ts +1 -1
  70. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  71. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  72. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  73. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  74. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  75. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  76. package/dist/types/vite/discovery/state.d.ts +12 -0
  77. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  78. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  79. package/dist/types/vite/router-discovery.d.ts +8 -47
  80. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  81. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  82. package/dist/types/wire-params.d.ts +34 -0
  83. package/dist/vite/index.js +3809 -3710
  84. package/package.json +1 -1
  85. package/skills/cache-guide/SKILL.md +8 -5
  86. package/skills/caching/SKILL.md +37 -18
  87. package/skills/document-cache/SKILL.md +13 -1
  88. package/skills/handler-use/SKILL.md +1 -1
  89. package/skills/intercept/SKILL.md +17 -7
  90. package/skills/middleware/SKILL.md +18 -0
  91. package/skills/ppr/SKILL.md +18 -3
  92. package/skills/prerender/SKILL.md +39 -15
  93. package/skills/rango/SKILL.md +5 -1
  94. package/skills/server-actions/SKILL.md +8 -1
  95. package/skills/testing/response-routes.md +6 -4
  96. package/skills/use-cache/SKILL.md +46 -8
  97. package/src/__internal.ts +7 -1
  98. package/src/browser/connection-warmup.ts +3 -1
  99. package/src/browser/event-controller.ts +5 -3
  100. package/src/browser/history-state.ts +6 -12
  101. package/src/browser/navigation-bridge.ts +14 -13
  102. package/src/browser/navigation-client.ts +18 -25
  103. package/src/browser/navigation-store.ts +25 -27
  104. package/src/browser/navigation-transaction.ts +4 -1
  105. package/src/browser/prefetch/cache.ts +2 -1
  106. package/src/browser/prefetch/fetch.ts +8 -16
  107. package/src/browser/prefetch/queue.ts +4 -4
  108. package/src/browser/react/NavigationProvider.tsx +2 -1
  109. package/src/browser/react/filter-segment-order.ts +2 -2
  110. package/src/browser/rsc-router.tsx +1 -0
  111. package/src/browser/segment-reconciler.ts +66 -53
  112. package/src/browser/server-action-bridge.ts +50 -57
  113. package/src/browser/types.ts +2 -1
  114. package/src/browser/wire-protocol.ts +41 -0
  115. package/src/cache/cache-key-utils.ts +11 -5
  116. package/src/cache/cache-runtime.ts +110 -50
  117. package/src/cache/cache-scope.ts +110 -64
  118. package/src/cache/cf/cf-cache-constants.ts +2 -2
  119. package/src/cache/cf/cf-cache-store.ts +486 -874
  120. package/src/cache/cf/cf-kv-utils.ts +1 -1
  121. package/src/cache/document-cache.ts +34 -13
  122. package/src/cache/handle-snapshot.ts +19 -7
  123. package/src/cache/segment-codec.ts +55 -0
  124. package/src/cache/shell-snapshot.ts +2 -2
  125. package/src/cache/tag-invalidation.ts +21 -2
  126. package/src/cache/taint.ts +5 -3
  127. package/src/cache/types.ts +7 -3
  128. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  129. package/src/handles/pushed-by.ts +1 -1
  130. package/src/prefetch-markers.ts +5 -16
  131. package/src/prerender/store.ts +76 -0
  132. package/src/prerender/warm-request.ts +8 -4
  133. package/src/prerender.ts +2 -2
  134. package/src/route-name.ts +8 -0
  135. package/src/router/error-handling.ts +4 -4
  136. package/src/router/instrument.ts +4 -3
  137. package/src/router/intercept-resolution.ts +77 -75
  138. package/src/router/loader-resolution.ts +5 -4
  139. package/src/router/match-api.ts +4 -5
  140. package/src/router/match-context.ts +20 -12
  141. package/src/router/match-handlers.ts +8 -20
  142. package/src/router/match-middleware/background-revalidation.ts +41 -53
  143. package/src/router/match-middleware/cache-lookup.ts +675 -726
  144. package/src/router/match-middleware/cache-store.ts +4 -8
  145. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  146. package/src/router/match-middleware/segment-resolution.ts +34 -61
  147. package/src/router/middleware.ts +69 -11
  148. package/src/router/navigation-snapshot.ts +4 -4
  149. package/src/router/prerender-match.ts +100 -44
  150. package/src/router/preview-match.ts +6 -41
  151. package/src/router/request-classification.ts +13 -7
  152. package/src/router/route-snapshot.ts +2 -5
  153. package/src/router/router-context.ts +10 -95
  154. package/src/router/router-interfaces.ts +8 -14
  155. package/src/router/segment-resolution/loader-cache.ts +14 -9
  156. package/src/router/segment-resolution/loader-mask.ts +5 -5
  157. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  158. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  159. package/src/router/segment-resolution/revalidation.ts +897 -776
  160. package/src/router/segment-resolution.ts +4 -13
  161. package/src/router/segment-wrappers.ts +51 -269
  162. package/src/router/telemetry.ts +5 -0
  163. package/src/router/types.ts +10 -0
  164. package/src/router.ts +12 -13
  165. package/src/rsc/full-payload.ts +6 -5
  166. package/src/rsc/handler-context.ts +8 -14
  167. package/src/rsc/handler.ts +126 -507
  168. package/src/rsc/json-route-result.ts +5 -9
  169. package/src/rsc/loader-fetch.ts +6 -5
  170. package/src/rsc/progressive-enhancement.ts +5 -0
  171. package/src/rsc/request-stages.ts +757 -0
  172. package/src/rsc/response-cache-serve.ts +10 -14
  173. package/src/rsc/response-route-handler.ts +13 -5
  174. package/src/rsc/rewrite-passes.ts +2 -1
  175. package/src/rsc/routine-plan.ts +1 -1
  176. package/src/rsc/rsc-rendering.ts +164 -349
  177. package/src/rsc/server-action.ts +87 -113
  178. package/src/rsc/shell-capture-constants.ts +1 -1
  179. package/src/rsc/shell-capture.ts +212 -169
  180. package/src/rsc/shell-serve.ts +121 -18
  181. package/src/rsc/ssr-setup.ts +5 -4
  182. package/src/segment-system.tsx +3 -3
  183. package/src/server/context.ts +22 -25
  184. package/src/server/handle-store.ts +55 -0
  185. package/src/server/render-pass.ts +291 -0
  186. package/src/server/request-context.ts +26 -115
  187. package/src/server/rewrite-trail.ts +6 -4
  188. package/src/ssr/index.tsx +1 -1
  189. package/src/testing/dispatch.ts +223 -710
  190. package/src/testing/e2e/page-helpers.ts +2 -2
  191. package/src/testing/serve-shell-request.ts +23 -36
  192. package/src/types/cache-types.ts +6 -3
  193. package/src/types/error-types.ts +9 -1
  194. package/src/types/handler-context.ts +12 -1
  195. package/src/types/segments.ts +1 -1
  196. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  197. package/src/vite/discovery/build-env.ts +148 -0
  198. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  199. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  200. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  201. package/src/vite/discovery/discover-routers.ts +1 -1
  202. package/src/vite/discovery/discovery-errors.ts +1 -1
  203. package/src/vite/discovery/prerender-collection.ts +24 -39
  204. package/src/vite/discovery/state.ts +15 -0
  205. package/src/vite/plugins/version-plugin.ts +1 -1
  206. package/src/vite/plugins/virtual-entries.ts +1 -1
  207. package/src/vite/router-discovery.ts +29 -2428
  208. package/src/vite/temp-rsc-server.ts +223 -0
  209. package/src/vite/utils/prerender-utils.ts +47 -0
  210. package/src/wire-params.ts +35 -0
  211. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  212. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -9,6 +9,7 @@ import {
9
9
  resolveLocationStateEntries,
10
10
  stampLocationState,
11
11
  } from "./react/location-state-shared.js";
12
+ import { WIRE } from "../wire-params.js";
12
13
 
13
14
  /**
14
15
  * Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
@@ -137,16 +138,9 @@ export function pushHistoryWithIdx(
137
138
  }
138
139
 
139
140
  /**
140
- * The forced-MISS reload marker (`SHELL_MISS_PARAM` in rsc/shell-serve.ts,
141
- * which the browser bundle must not import): a degraded PPR shell HIT
142
- * reloads the page with it, and the server renders a marked request like a
143
- * cache miss.
144
- */
145
- export const SHELL_MISS_MARKER: string = "_rsc_shell";
146
-
147
- /**
148
- * Drop the forced-MISS marker from the address bar at boot, keeping the
149
- * history entry's state (initBrowserApp calls it before any location read).
141
+ * Drop the forced-MISS marker (`WIRE.shellMiss`, rsc/shell-serve.ts) from the
142
+ * address bar at boot, keeping the history entry's state (initBrowserApp
143
+ * calls it before any location read).
150
144
  * Left in place, a refresh or a shared link would stay off the shell and the
151
145
  * app's search params would carry it. The server strips the same marker from
152
146
  * the request before rendering (shell-serve.ts withoutShellMissMarker), so
@@ -154,8 +148,8 @@ export const SHELL_MISS_MARKER: string = "_rsc_shell";
154
148
  */
155
149
  export function stripShellMissMarker(): void {
156
150
  const url = new URL(window.location.href);
157
- if (!url.searchParams.has(SHELL_MISS_MARKER)) return;
158
- url.searchParams.delete(SHELL_MISS_MARKER);
151
+ if (!url.searchParams.has(WIRE.shellMiss)) return;
152
+ url.searchParams.delete(WIRE.shellMiss);
159
153
  window.history.replaceState(window.history.state, "", url.href);
160
154
  }
161
155
 
@@ -273,15 +273,18 @@ export function createNavigationBridge(
273
273
  targetUrl.pathname === new URL(window.location.href).pathname
274
274
  ) {
275
275
  // Preserve intercept context from the current history entry so that
276
- // popstate uses the correct cache key (:intercept suffix) and restores
277
- // the right full-page vs modal semantics.
276
+ // popstate uses the same cache key and restores the right full-page
277
+ // vs intercept semantics.
278
278
  const currentHistoryState = window.history.state;
279
279
  const isIntercept = currentHistoryState?.intercept === true;
280
280
  const interceptSourceUrl = isIntercept
281
281
  ? currentHistoryState?.sourceUrl
282
282
  : undefined;
283
283
 
284
- const historyKey = generateHistoryKey(url, { intercept: isIntercept });
284
+ const historyKey = generateHistoryKey(url, {
285
+ intercept: isIntercept,
286
+ interceptSource: interceptSourceUrl,
287
+ });
285
288
 
286
289
  // Copy current segments to the new history key so back/forward restores instantly.
287
290
  // The array itself, not a clone: getPageHistoryKeys reads a shared
@@ -405,12 +408,6 @@ export function createNavigationBridge(
405
408
  });
406
409
  }
407
410
 
408
- // Also check if there's an intercept cache entry for this URL
409
- // If so, this URL CAN be intercepted, and we shouldn't use the non-intercept cache
410
- // because the navigation might result in an intercept (depending on source URL)
411
- const interceptHistoryKey = generateHistoryKey(url, { intercept: true });
412
- const hasInterceptCache = store.hasHistoryCache(interceptHistoryKey);
413
-
414
411
  // Skip cached SWR for:
415
412
  // 1. intercept caches - interception depends on source page context
416
413
  // 2. routes that CAN be intercepted - we don't know if this navigation will intercept
@@ -420,10 +417,12 @@ export function createNavigationBridge(
420
417
  const hasUsableCache =
421
418
  isRestorable(cachedSegments) &&
422
419
  !isInterceptOnlyCache(cachedSegments) &&
423
- !hasInterceptCache &&
424
420
  !isLeavingIntercept &&
425
421
  !cached?.stale &&
426
- !options?._skipCache;
422
+ !options?._skipCache &&
423
+ // A cached intercept entry for this URL (from any source page) means
424
+ // the navigation might intercept, depending on the source URL.
425
+ !store.hasInterceptCache(historyKey);
427
426
 
428
427
  // Forward navigations always await fetchPartialUpdate before rendering,
429
428
  // so useNavigation should always report "loading". skipLoadingState is
@@ -664,8 +663,10 @@ export function createNavigationBridge(
664
663
  const isLeavingIntercept =
665
664
  !isIntercept && currentInterceptSource !== null;
666
665
 
667
- // Compute history key from URL (with intercept suffix if applicable)
668
- const historyKey = generateHistoryKey(url, { intercept: isIntercept });
666
+ const historyKey = generateHistoryKey(url, {
667
+ intercept: isIntercept,
668
+ interceptSource: interceptSourceUrl,
669
+ });
669
670
 
670
671
  debugLog(
671
672
  "[Browser] Popstate -",
@@ -38,7 +38,7 @@ import {
38
38
  type DecodedPrefetch,
39
39
  } from "./prefetch/cache.js";
40
40
  import { cancelAllPrefetches } from "./prefetch/loader.js";
41
- import { FILL_PARAM, STALE_PARAM } from "../prefetch-markers.js";
41
+ import { writePartialParams } from "./wire-protocol.js";
42
42
 
43
43
  /**
44
44
  * Create a navigation client for fetching RSC payloads
@@ -103,30 +103,23 @@ export function createNavigationClient(
103
103
  }
104
104
 
105
105
  // Build fetch URL with partial rendering params (used for both
106
- // cache key lookup and actual fetch if cache misses)
107
- const fetchUrl = new URL(targetUrl, window.location.origin);
108
- fetchUrl.searchParams.set("_rsc_partial", "true");
109
- fetchUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
110
- if (staleRevalidation) {
111
- fetchUrl.searchParams.set(STALE_PARAM, "true");
112
- }
113
- if (version) {
114
- fetchUrl.searchParams.set("_rsc_v", version);
115
- }
116
- if (routerId) {
117
- fetchUrl.searchParams.set("_rsc_rid", routerId);
118
- }
119
- // The page left's rewrite target (#1144), in both prefetch keys like
120
- // the params above: buildPrefetchUrl sets it in the same place.
121
- if (rewriteTarget) {
122
- fetchUrl.searchParams.set("_rsc_rt", rewriteTarget);
123
- }
124
- // A param, not a header: the document cache decides from the raw URL
125
- // (cache/document-cache.ts), and it keeps the fill out of every
126
- // prefetch key below.
127
- if (fill) {
128
- fetchUrl.searchParams.set(FILL_PARAM, "1");
129
- }
106
+ // cache key lookup and actual fetch if cache misses). The page left's
107
+ // rewrite target (#1144) is in both prefetch keys, like the params
108
+ // around it. The fill is a param, not a header: the document cache
109
+ // decides from the raw URL (cache/document-cache.ts), and it keeps the
110
+ // fill out of every prefetch key below.
111
+ const fetchUrl = writePartialParams(
112
+ new URL(targetUrl, window.location.origin),
113
+ {
114
+ segmentIds,
115
+ emptySegments: "send",
116
+ stale: staleRevalidation,
117
+ version,
118
+ routerId,
119
+ rewriteTarget,
120
+ fill,
121
+ },
122
+ );
130
123
 
131
124
  // If the lazy prefetch runtime has not loaded yet, prevent a deferred
132
125
  // speculative request from starting after this navigation misses the
@@ -34,7 +34,7 @@ const HISTORY_ENTRY_MEMORY_SIZE = 200;
34
34
  // stale=true means the data may be outdated and should be revalidated on access.
35
35
  // navInstance is the monotonic nav-instance token (see navInstance below): it
36
36
  // identifies the per-commit visit that owns this entry. generateHistoryKey is
37
- // URL-only, so A->B->A reuses the same key; the token lets a late async
37
+ // the URL (plus the source page for an intercept), so A->B->A reuses the key; the token lets a late async
38
38
  // resolution tell its own visit's entry apart from a newer same-URL visit's, so
39
39
  // a stale nav can never clobber a fresher one.
40
40
  // handlesPending=true means the entry's handle data is INCOMPLETE (a deferred
@@ -124,36 +124,35 @@ function getCacheInvalidationChannel(): BroadcastChannel | null {
124
124
  * Options for generating a history key
125
125
  */
126
126
  export interface HistoryKeyOptions {
127
- /** If true, append :intercept suffix to differentiate intercept entries */
127
+ /** Key an intercept entry (apart from the plain entry for the URL). */
128
128
  intercept?: boolean;
129
+ /** The page the intercept was opened over (history.state.sourceUrl). */
130
+ interceptSource?: string;
131
+ }
132
+
133
+ function urlKey(url: string): string {
134
+ const parsed = new URL(url, "http://localhost");
135
+ return parsed.pathname + parsed.search;
129
136
  }
130
137
 
131
138
  /**
132
- * Generate a cache key from a URL.
133
- * Uses pathname + search (query params) directly as the key.
134
- * Hash fragments (#) are excluded since they don't affect server data.
139
+ * Generate a cache key from a URL: pathname + search. Hash fragments (#) are
140
+ * excluded since they don't affect server data.
135
141
  *
136
- * For intercept routes, append `:intercept` suffix to cache them separately
137
- * from non-intercept versions of the same URL.
142
+ * An intercept entry is keyed `<url>:intercept:<source>`: its cached tree
143
+ * includes the page under the intercept. hasInterceptCache relies on the
144
+ * `:intercept:` separator.
138
145
  */
139
146
  export function generateHistoryKey(
140
147
  url?: string,
141
148
  options?: HistoryKeyOptions,
142
149
  ): string {
143
- if (!url) {
144
- url = typeof window !== "undefined" ? window.location.href : "/";
145
- }
146
-
147
- // Parse URL and use only pathname + search (exclude hash fragment)
148
- const parsed = new URL(url, "http://localhost");
149
- let key = parsed.pathname + parsed.search;
150
-
151
- // Append intercept suffix for separate caching
152
- if (options?.intercept) {
153
- key += ":intercept";
154
- }
155
-
156
- return key;
150
+ const key = urlKey(
151
+ url || (typeof window !== "undefined" ? window.location.href : "/"),
152
+ );
153
+ if (!options?.intercept) return key;
154
+ const source = options.interceptSource;
155
+ return `${key}:intercept:${source ? urlKey(source) : ""}`;
157
156
  }
158
157
 
159
158
  /**
@@ -258,7 +257,8 @@ export function createNavigationStore(
258
257
 
259
258
  // Monotonic nav-instance token. Bumped each time a cache entry is created or
260
259
  // replaced in cacheSegmentsForHistory (i.e. once per commit). Because
261
- // generateHistoryKey is URL-only, two visits to the same URL share a key; this
260
+ // generateHistoryKey is the URL (plus the source page for an intercept), two
261
+ // visits to the same URL share a key; this
262
262
  // token gives each visit a distinct identity so a late async handle resolution
263
263
  // can tell whether it still owns the live page / the target cache entry, and
264
264
  // never overwrite a newer same-URL visit's state.
@@ -580,11 +580,9 @@ export function createNavigationStore(
580
580
  };
581
581
  },
582
582
 
583
- /**
584
- * Check if segments are cached for a history entry
585
- */
586
- hasHistoryCache(historyKey: string): boolean {
587
- return historyCache.some(([key]) => key === historyKey);
583
+ hasInterceptCache(historyKey: string): boolean {
584
+ const prefix = `${historyKey}:intercept:`;
585
+ return historyCache.some(([key]) => key.startsWith(prefix));
588
586
  },
589
587
 
590
588
  /**
@@ -180,7 +180,10 @@ export function createNavigationTransaction(
180
180
 
181
181
  const parsedUrl = new URL(url, window.location.origin);
182
182
 
183
- const historyKey = generateHistoryKey(url, { intercept });
183
+ const historyKey = generateHistoryKey(url, {
184
+ intercept,
185
+ interceptSource: interceptSourceUrl,
186
+ });
184
187
 
185
188
  if (cacheOnly) {
186
189
  const currentHandleData = eventController.getHandleState().data;
@@ -43,6 +43,7 @@ import { abortAllPrefetches } from "./loader.js";
43
43
  import { notifyPrefetchCacheInvalidated } from "./invalidation.js";
44
44
  import { invalidateRangoState } from "../rango-state.js";
45
45
  import type { RscPayload } from "../types.js";
46
+ import { WIRE } from "../../wire-params.js";
46
47
 
47
48
  /**
48
49
  * A prefetch that has been fetched AND eagerly decoded. Storing the decoded
@@ -190,7 +191,7 @@ let generation = 0;
190
191
  */
191
192
  export function buildPrefetchKey(prefix: string, targetUrl: URL): string {
192
193
  const normalizedTarget = new URL(targetUrl);
193
- normalizedTarget.searchParams.delete("_rsc_segments");
194
+ normalizedTarget.searchParams.delete(WIRE.segments);
194
195
  return prefix + "\0" + normalizedTarget.pathname + normalizedTarget.search;
195
196
  }
196
197
 
@@ -38,6 +38,7 @@ import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../../segment-fragments.js";
38
38
  import { PREFETCH_HEADER } from "../../prefetch-markers.js";
39
39
  import type { RscPayload } from "../types.js";
40
40
  import { getRegisteredStore } from "../navigation-store-handle.js";
41
+ import { writePartialParams } from "../wire-protocol.js";
41
42
 
42
43
  /**
43
44
  * Decoder injected at app startup (see setPrefetchDecoder). This is
@@ -153,24 +154,15 @@ function buildPrefetchUrl(
153
154
  if (targetUrl.origin !== window.location.origin) {
154
155
  return null;
155
156
  }
156
- targetUrl.searchParams.set("_rsc_partial", "true");
157
- if (segmentIds.length > 0) {
158
- targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
159
- }
160
- if (version) {
161
- targetUrl.searchParams.set("_rsc_v", version);
162
- }
163
- if (routerId) {
164
- targetUrl.searchParams.set("_rsc_rid", routerId);
165
- }
166
157
  // The page's rewrite target, so the server diffs against the route that
167
158
  // rendered it, as for the navigation.
168
- const rewriteTarget =
169
- getRegisteredStore()?.getHistoryEntryMemory()?.rewriteTarget;
170
- if (rewriteTarget) {
171
- targetUrl.searchParams.set("_rsc_rt", rewriteTarget);
172
- }
173
- return targetUrl;
159
+ return writePartialParams(targetUrl, {
160
+ segmentIds,
161
+ emptySegments: "omit",
162
+ version,
163
+ routerId,
164
+ rewriteTarget: getRegisteredStore()?.getHistoryEntryMemory()?.rewriteTarget,
165
+ });
174
166
  }
175
167
 
176
168
  /**
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { wait, waitForIdle, waitForViewportImages } from "./resource-ready.js";
18
+ import { WIRE_PREFIX } from "../../wire-params.js";
18
19
 
19
20
  // Max prefetches executing at once. Mirrors DEFAULT_PREFETCH_CONCURRENCY
20
21
  // (router/prefetch-limits.ts); kept as a local literal so the client bundle
@@ -128,16 +129,15 @@ export function enqueuePrefetch(
128
129
 
129
130
  /**
130
131
  * Normalize a URL-like string for keep-alive matching: parse against a
131
- * placeholder origin and strip internal `_rsc_*` query params. Returns
132
+ * placeholder origin and strip every `WIRE_PREFIX` query param. Returns
132
133
  * `pathname + search` so comparisons ignore hash and the internal params
133
- * that prefetch appends to targets (`_rsc_partial`, `_rsc_segments`,
134
- * `_rsc_v`, `_rsc_rid`, `_rsc_stale`).
134
+ * that prefetch appends to targets.
135
135
  */
136
136
  function normalizeForMatch(urlish: string): string {
137
137
  try {
138
138
  const u = new URL(urlish, "http://placeholder");
139
139
  for (const k of [...u.searchParams.keys()]) {
140
- if (k.startsWith("_rsc_")) u.searchParams.delete(k);
140
+ if (k.startsWith(WIRE_PREFIX)) u.searchParams.delete(k);
141
141
  }
142
142
  return u.pathname + u.search;
143
143
  } catch {
@@ -78,7 +78,8 @@ async function processHandles(
78
78
 
79
79
  // This nav's instance token, captured before any await — processHandles runs
80
80
  // right after its own commit, so this is that commit's token. generateHistoryKey
81
- // is URL-only, so an A->B->A revisit reuses the key; the token lets a late
81
+ // is the URL (plus the source page for an intercept), so an A->B->A revisit
82
+ // reuses the key; the token lets a late
82
83
  // resolution tell its own visit apart from a newer same-URL visit, so a stale
83
84
  // nav can never clobber a fresher one's live state or cache (P1).
84
85
  const myInstance = store.getNavInstance();
@@ -33,8 +33,8 @@ export function loaderParentId(id: string): string {
33
33
  * 2. Place each parallel slot id (contains ".@") immediately after its
34
34
  * parent layout/route id. Raw segment-resolution emission order does NOT
35
35
  * guarantee this: route-mounted parallels are resolved/pushed BEFORE the
36
- * route handler's segment is appended (see fresh.ts:resolveSegment for
37
- * routes, and revalidation.ts ~915-919), so matched can read
36
+ * route handler's segment is appended (see resolveSegment in
37
+ * segment-resolution/revalidation.ts), so matched can read
38
38
  * `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
39
39
  * with later-wins semantics, so without normalization the route handler's
40
40
  * Meta would override the slot's more-specific Meta — backwards.
@@ -614,6 +614,7 @@ export async function initBrowserApp(
614
614
 
615
615
  const historyKey = generateHistoryKey(window.location.href, {
616
616
  intercept: responseIsIntercept,
617
+ interceptSource: interceptSourceUrl ?? undefined,
617
618
  });
618
619
  store.setHistoryKey(historyKey);
619
620
  const currentHandleData = liveHandleDataForCache(
@@ -46,6 +46,60 @@ export interface ReconcileResult {
46
46
  interceptSegments: ResolvedSegment[];
47
47
  }
48
48
 
49
+ /**
50
+ * A segment the server sent over its cached copy, keeping the cached
51
+ * structural properties so the React tree keeps its shape. Changing them
52
+ * between renders alters the element nesting (with/without
53
+ * RouteContentWrapper, MountContextProvider, etc.), which remounts components
54
+ * and destroys useActionState.
55
+ */
56
+ function mergeCached(
57
+ fromServer: ResolvedSegment,
58
+ fromCache: ResolvedSegment,
59
+ actor: ReconcileActor,
60
+ ): ResolvedSegment {
61
+ let merged = fromServer;
62
+
63
+ // When server returns component: null for a layout segment, it means
64
+ // "this segment doesn't need re-rendering" - preserve the cached component
65
+ // to maintain the outlet chain and prevent React tree changes
66
+ if (
67
+ fromServer.component === null &&
68
+ fromServer.type === "layout" &&
69
+ fromCache.component != null
70
+ ) {
71
+ merged = { ...merged, component: fromCache.component };
72
+ }
73
+
74
+ // Loading preservation is actor-aware:
75
+ // - action/stale-revalidation: always preserve cached value to prevent
76
+ // tree remount (even when cached is undefined, to avoid adding a
77
+ // Suspense boundary that wasn't there before)
78
+ // - navigation: only when cached is defined (building a new tree)
79
+ if (
80
+ fromServer.loading !== fromCache.loading &&
81
+ (actor !== "navigation" || fromCache.loading !== undefined)
82
+ ) {
83
+ merged = { ...merged, loading: fromCache.loading };
84
+ }
85
+
86
+ // mountPath: SSR segments may lack mountPath while revalidated segments
87
+ // include it. The conditional MountContextProvider wrapper changes tree depth.
88
+ if (fromServer.mountPath !== fromCache.mountPath) {
89
+ merged = { ...merged, mountPath: fromCache.mountPath };
90
+ }
91
+
92
+ // Dev-mode assertion: warn if the merged result still differs from cache
93
+ // in tree-structural properties. This catches bugs where the merge code
94
+ // above fails to preserve a value it should have.
95
+ assertSegmentStructure(
96
+ fromCache,
97
+ merged,
98
+ actor === "action" ? "action-bridge" : "partial-update",
99
+ );
100
+ return merged;
101
+ }
102
+
49
103
  /**
50
104
  * Single source of truth for merging server segments with cached segments.
51
105
  *
@@ -70,7 +124,6 @@ export interface ReconcileResult {
70
124
  */
71
125
  export function reconcileSegments(input: ReconcileInput): ReconcileResult {
72
126
  const { actor, matched, diff, insertMissingDiff } = input;
73
- const context = actor === "action" ? "action-bridge" : "partial-update";
74
127
 
75
128
  // Build lookup maps from arrays
76
129
  const serverSegments = new Map<string, ResolvedSegment>();
@@ -111,59 +164,13 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
111
164
 
112
165
  if (fromServer) {
113
166
  const inDiff = diffSet.has(segId);
114
- // Preserve cached structural properties to maintain consistent React tree.
115
- // Changing these between renders alters the element nesting
116
- // (with/without RouteContentWrapper, MountContextProvider, etc.),
117
- // causing React to remount components and destroy useActionState.
118
167
  if (fromCache) {
119
- let merged = fromServer;
120
-
121
- // When server returns component: null for a layout segment, it means
122
- // "this segment doesn't need re-rendering" - preserve the cached component
123
- // to maintain the outlet chain and prevent React tree changes
124
- if (
125
- fromServer.component === null &&
126
- fromServer.type === "layout" &&
127
- fromCache.component != null
128
- ) {
129
- merged = { ...merged, component: fromCache.component };
130
- }
131
-
132
- // Loading preservation is actor-aware:
133
- // - action/stale-revalidation: always preserve cached value to prevent
134
- // tree remount (even when cached is undefined, to avoid adding a
135
- // Suspense boundary that wasn't there before)
136
- // - navigation: only when cached is defined (building a new tree)
137
- if (actor !== "navigation") {
138
- if (fromServer.loading !== fromCache.loading) {
139
- merged = { ...merged, loading: fromCache.loading };
140
- }
141
- } else {
142
- if (
143
- fromCache.loading !== undefined &&
144
- fromServer.loading !== fromCache.loading
145
- ) {
146
- merged = { ...merged, loading: fromCache.loading };
147
- }
148
- }
149
-
150
- // mountPath: SSR segments may lack mountPath while revalidated segments
151
- // include it. The conditional MountContextProvider wrapper changes tree depth.
152
- if (fromServer.mountPath !== fromCache.mountPath) {
153
- merged = { ...merged, mountPath: fromCache.mountPath };
154
- }
155
-
156
- // Dev-mode assertion: warn if the merged result still differs from cache
157
- // in tree-structural properties. This catches bugs where the merge code
158
- // above fails to preserve a value it should have.
159
- assertSegmentStructure(fromCache, merged, context);
160
-
161
168
  if (IS_BROWSER_DEBUG) {
162
169
  debugLog(
163
170
  `[reconcile] ${segId}: SERVER+CACHE merge (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, component=${fromServer.component === null ? "null→cached" : "server"})`,
164
171
  );
165
172
  }
166
- return merged;
173
+ return mergeCached(fromServer, fromCache, actor);
167
174
  }
168
175
  if (IS_BROWSER_DEBUG) {
169
176
  debugLog(
@@ -221,11 +228,13 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
221
228
  }
222
229
 
223
230
  /**
224
- * Reconcile error segments with cached segments.
231
+ * Reconcile an error-boundary response with cached segments.
225
232
  *
226
- * For error responses, the server returns the error boundary segment.
227
- * This function overlays error segments onto the full cached tree,
228
- * preserving sibling layouts that aren't in the error parent chain.
233
+ * The server returns the error boundary segment and, above it, what the
234
+ * action's revalidation re-ran (rsc/server-action.ts). This function overlays
235
+ * them onto the full cached tree, preserving sibling layouts that aren't in
236
+ * the error parent chain. The error segment replaces its cached copy; every
237
+ * other segment merges as an action's does (mergeCached).
229
238
  */
230
239
  export function reconcileErrorSegments(
231
240
  cachedSegments: ResolvedSegment[],
@@ -236,7 +245,11 @@ export function reconcileErrorSegments(
236
245
 
237
246
  const segments = cachedSegments.map((cached) => {
238
247
  const fromServer = errorMap.get(cached.id);
239
- return fromServer || cached;
248
+ return !fromServer
249
+ ? cached
250
+ : fromServer.type === "error"
251
+ ? fromServer
252
+ : mergeCached(fromServer, cached, "action");
240
253
  });
241
254
 
242
255
  const { main, intercept } = splitInterceptSegments(segments);