@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -5,7 +5,24 @@ import type {
5
5
  RscPayload,
6
6
  RscBrowserDependencies,
7
7
  } from "./types.js";
8
- import { NetworkError, isNetworkError } from "../errors.js";
8
+ import { NetworkError, ServerRedirect, isNetworkError } from "../errors.js";
9
+ import {
10
+ browserDebugLog,
11
+ isBrowserDebugEnabled,
12
+ startBrowserTransaction,
13
+ } from "./logging.js";
14
+ import { getRangoState } from "./rango-state.js";
15
+ import {
16
+ extractRscHeaderUrl,
17
+ emptyResponse,
18
+ teeWithCompletion,
19
+ } from "./response-adapter.js";
20
+ import {
21
+ buildPrefetchKey,
22
+ buildSourceKey,
23
+ consumeInflightPrefetch,
24
+ consumePrefetch,
25
+ } from "./prefetch/cache.js";
9
26
 
10
27
  /**
11
28
  * Create a navigation client for fetching RSC payloads
@@ -13,32 +30,25 @@ import { NetworkError, isNetworkError } from "../errors.js";
13
30
  * The client handles building URLs with RSC parameters and
14
31
  * deserializing the response using the RSC runtime.
15
32
  *
33
+ * Checks the in-memory prefetch cache before making a network request.
34
+ * Tries the source-scoped key first (populated when the server tagged
35
+ * the response as source-sensitive via `X-RSC-Prefetch-Scope: source`)
36
+ * and falls back to the Rango-state-keyed wildcard slot used for the
37
+ * common source-agnostic case.
38
+ *
16
39
  * @param deps - RSC browser dependencies (createFromFetch)
17
40
  * @returns NavigationClient instance
18
- *
19
- * @example
20
- * ```typescript
21
- * import { createFromFetch } from "@vitejs/plugin-rsc/browser";
22
- *
23
- * const client = createNavigationClient({ createFromFetch });
24
- *
25
- * const payload = await client.fetchPartial({
26
- * targetUrl: "/shop/products",
27
- * segmentIds: ["root", "shop"],
28
- * previousUrl: "/",
29
- * });
30
- * ```
31
41
  */
32
42
  export function createNavigationClient(
33
43
  deps: Pick<RscBrowserDependencies, "createFromFetch">,
34
44
  ): NavigationClient {
35
-
36
45
  return {
37
46
  /**
38
47
  * Fetch a partial RSC payload for navigation
39
48
  *
40
- * Sends current segment IDs to the server so it can determine
41
- * which segments need to be re-rendered (diff).
49
+ * First checks the in-memory prefetch cache for a matching entry.
50
+ * If found, uses the cached response instantly. Otherwise sends
51
+ * current segment IDs to the server for diff-based rendering.
42
52
  *
43
53
  * @param options - Fetch options
44
54
  * @returns RSC payload with segments and metadata, plus stream completion promise
@@ -54,18 +64,25 @@ export function createNavigationClient(
54
64
  staleRevalidation,
55
65
  interceptSourceUrl,
56
66
  version,
67
+ routerId,
57
68
  hmr,
58
69
  } = options;
59
70
 
60
- console.log(`\n[Browser] >>> NAVIGATION`);
61
- console.log(`[Browser] From: ${previousUrl}`);
62
- console.log(`[Browser] To: ${targetUrl}`);
63
- console.log(`[Browser] Segments to send: ${segmentIds.join(", ")}`);
64
- if (staleRevalidation) {
65
- console.log(`[Browser] Stale revalidation request`);
71
+ const debugEnabled = isBrowserDebugEnabled();
72
+ const tx = debugEnabled
73
+ ? startBrowserTransaction(staleRevalidation ? "revalidate" : "navigate")
74
+ : null;
75
+ if (tx) {
76
+ browserDebugLog(tx, "request start", {
77
+ from: previousUrl,
78
+ to: targetUrl,
79
+ segments: segmentIds,
80
+ staleRevalidation: !!staleRevalidation,
81
+ });
66
82
  }
67
83
 
68
- // Build fetch URL with partial rendering params
84
+ // Build fetch URL with partial rendering params (used for both
85
+ // cache key lookup and actual fetch if cache misses)
69
86
  const fetchUrl = new URL(targetUrl, window.location.origin);
70
87
  fetchUrl.searchParams.set("_rsc_partial", "true");
71
88
  fetchUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
@@ -75,76 +92,217 @@ export function createNavigationClient(
75
92
  if (version) {
76
93
  fetchUrl.searchParams.set("_rsc_v", version);
77
94
  }
95
+ if (routerId) {
96
+ fetchUrl.searchParams.set("_rsc_rid", routerId);
97
+ }
98
+
99
+ // Check completed in-memory prefetch cache before making a network
100
+ // request. Try the source-scoped key first (populated when the server
101
+ // tagged the prefetch response as source-sensitive, e.g. intercepts,
102
+ // or when a Link opted in with `prefetchKey=":source"`), then fall
103
+ // back to the wildcard slot shared across source pages.
104
+ // Both keys embed the Rango state, so state rotation (deploy or
105
+ // server-action invalidation) auto-invalidates both scopes.
106
+ // Skip cache for stale revalidation (needs fresh data), HMR (needs
107
+ // fresh modules), and intercept contexts (source-dependent responses).
108
+ const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
109
+ const rangoState = getRangoState();
110
+ const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
111
+ const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
78
112
 
79
- console.log(`[Browser] Fetching: ${fetchUrl.pathname}${fetchUrl.search}`);
113
+ let cachedResponse: Response | null = null;
114
+ let hitKey: string | null = null;
115
+ if (canUsePrefetch) {
116
+ cachedResponse = consumePrefetch(cacheKey);
117
+ if (cachedResponse) {
118
+ hitKey = cacheKey;
119
+ } else {
120
+ cachedResponse = consumePrefetch(wildcardKey);
121
+ if (cachedResponse) hitKey = wildcardKey;
122
+ }
123
+ }
80
124
 
125
+ let inflightResponsePromise: Promise<Response | null> | null = null;
126
+ if (canUsePrefetch && !cachedResponse) {
127
+ inflightResponsePromise = consumeInflightPrefetch(cacheKey);
128
+ if (inflightResponsePromise) {
129
+ hitKey = cacheKey;
130
+ } else {
131
+ inflightResponsePromise = consumeInflightPrefetch(wildcardKey);
132
+ if (inflightResponsePromise) hitKey = wildcardKey;
133
+ }
134
+ }
81
135
  // Track when the stream completes
82
136
  let resolveStreamComplete: () => void;
83
137
  const streamComplete = new Promise<void>((resolve) => {
84
138
  resolveStreamComplete = resolve;
85
139
  });
86
140
 
87
- // Create a response promise that tracks stream completion
88
- const responsePromise = fetch(fetchUrl, {
89
- headers: {
90
- "X-RSC-Router-Client-Path": previousUrl,
91
- ...(interceptSourceUrl && {
92
- "X-RSC-Router-Intercept-Source": interceptSourceUrl,
93
- }),
94
- ...(hmr && { "X-RSC-HMR": "1" }),
95
- },
96
- signal,
97
- }).then((response) => {
98
- // Check for version mismatch - server wants us to reload
99
- const reloadUrl = response.headers.get("X-RSC-Reload");
100
- if (reloadUrl) {
101
- console.log(`[Browser] Version mismatch - reloading: ${reloadUrl}`);
102
- window.location.href = reloadUrl;
103
- // Return a never-resolving promise to prevent further processing
141
+ /**
142
+ * Validate RSC control headers on any response (fresh, cached, or
143
+ * in-flight). Handles version-mismatch reloads and server redirects.
144
+ * Returns the response unchanged when no control header is present.
145
+ */
146
+ const validateRscHeaders = (
147
+ response: Response,
148
+ source: string,
149
+ ): Response | Promise<Response> => {
150
+ // Version mismatch — server wants a full page reload
151
+ const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
152
+ if (reload === "blocked") {
153
+ resolveStreamComplete();
154
+ return emptyResponse();
155
+ }
156
+ if (reload) {
157
+ if (tx) {
158
+ browserDebugLog(tx, `version mismatch, reloading (${source})`, {
159
+ reloadUrl: reload.url,
160
+ });
161
+ }
162
+ window.location.href = reload.url;
163
+ // Block further processing — page is reloading
104
164
  return new Promise<Response>(() => {});
105
165
  }
106
166
 
107
- if (!response.body) {
108
- // No body means stream is already complete
167
+ // Server-side redirect without state: the server returned 204 with
168
+ // X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
169
+ // to a URL rendering full HTML). Throw ServerRedirect so the
170
+ // navigation bridge catches it and re-navigates with _skipCache.
171
+ const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
172
+ if (redirect === "blocked") {
109
173
  resolveStreamComplete();
110
- return response;
174
+ return emptyResponse();
175
+ }
176
+ if (redirect) {
177
+ if (tx) {
178
+ browserDebugLog(tx, `server redirect (${source})`, {
179
+ redirectUrl: redirect.url,
180
+ });
181
+ }
182
+ resolveStreamComplete();
183
+ throw new ServerRedirect(redirect.url, undefined);
184
+ }
185
+
186
+ return response;
187
+ };
188
+
189
+ /** Start a fresh navigation fetch (no cache / inflight hit). */
190
+ const doFreshFetch = (): Promise<Response> => {
191
+ if (tx) {
192
+ browserDebugLog(tx, "fetching", {
193
+ path: `${fetchUrl.pathname}${fetchUrl.search}`,
194
+ });
111
195
  }
112
196
 
113
- // Tee the stream: one for RSC runtime, one for tracking completion
114
- const [rscStream, trackingStream] = response.body.tee();
197
+ return fetch(fetchUrl, {
198
+ headers: {
199
+ "X-RSC-Router-Client-Path": previousUrl,
200
+ "X-Rango-State": getRangoState(),
201
+ ...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
202
+ ...(interceptSourceUrl && {
203
+ "X-RSC-Router-Intercept-Source": interceptSourceUrl,
204
+ }),
205
+ ...(hmr && { "X-RSC-HMR": "1" }),
206
+ },
207
+ signal,
208
+ }).then((response) => {
209
+ const validated = validateRscHeaders(response, "fetch");
210
+ if (validated instanceof Promise) return validated;
115
211
 
116
- // Consume the tracking stream to detect when it closes
117
- (async () => {
118
- const reader = trackingStream.getReader();
212
+ return teeWithCompletion(
213
+ validated,
214
+ () => {
215
+ if (tx) browserDebugLog(tx, "stream complete");
216
+ resolveStreamComplete();
217
+ },
218
+ signal,
219
+ );
220
+ });
221
+ };
119
222
 
120
- // Cancel tracking if navigation is aborted
121
- const onAbort = reader.cancel.bind(reader);
122
- signal?.addEventListener("abort", onAbort, { once: true });
223
+ let responsePromise: Promise<Response>;
123
224
 
124
- try {
125
- while (true) {
126
- const { done } = await reader.read();
127
- if (done) break;
225
+ if (cachedResponse) {
226
+ if (tx) {
227
+ browserDebugLog(tx, "prefetch cache hit", {
228
+ key: hitKey,
229
+ wildcard: hitKey === wildcardKey,
230
+ });
231
+ }
232
+ responsePromise = Promise.resolve(cachedResponse).then((response) => {
233
+ const validated = validateRscHeaders(response, "prefetch cache");
234
+ if (validated instanceof Promise) return validated;
235
+
236
+ return teeWithCompletion(
237
+ validated,
238
+ () => {
239
+ if (tx) browserDebugLog(tx, "stream complete (from cache)");
240
+ resolveStreamComplete();
241
+ },
242
+ signal,
243
+ );
244
+ });
245
+ } else if (inflightResponsePromise) {
246
+ if (tx) {
247
+ browserDebugLog(tx, "reusing inflight prefetch", {
248
+ key: hitKey,
249
+ wildcard: hitKey === wildcardKey,
250
+ });
251
+ }
252
+ const adoptedViaWildcard = hitKey === wildcardKey;
253
+ responsePromise = inflightResponsePromise.then(async (response) => {
254
+ if (!response) {
255
+ if (tx) {
256
+ browserDebugLog(tx, "inflight prefetch unavailable, refetching");
128
257
  }
129
- } finally {
130
- signal?.removeEventListener("abort", onAbort);
131
- reader.releaseLock();
132
- console.log("[STREAMING] RSC stream complete");
133
- resolveStreamComplete();
258
+ return doFreshFetch();
134
259
  }
135
- })();
136
260
 
137
- // Return response with the RSC stream
138
- return new Response(rscStream, {
139
- headers: response.headers,
140
- status: response.status,
141
- statusText: response.statusText,
261
+ // Cross-source safety: an inflight promise adopted via the
262
+ // wildcard key may turn out to be source-scoped (server emitted
263
+ // `X-RSC-Prefetch-Scope: source`), which means it was built for
264
+ // a different source page. Discard and refetch.
265
+ if (
266
+ adoptedViaWildcard &&
267
+ response.headers.get("x-rsc-prefetch-scope") === "source"
268
+ ) {
269
+ if (tx) {
270
+ browserDebugLog(
271
+ tx,
272
+ "wildcard inflight turned out source-scoped, refetching",
273
+ );
274
+ }
275
+ return doFreshFetch();
276
+ }
277
+
278
+ const validated = validateRscHeaders(response, "inflight prefetch");
279
+ if (validated instanceof Promise) return validated;
280
+
281
+ return teeWithCompletion(
282
+ validated,
283
+ () => {
284
+ if (tx) {
285
+ browserDebugLog(tx, "stream complete (from inflight prefetch)");
286
+ }
287
+ resolveStreamComplete();
288
+ },
289
+ signal,
290
+ );
142
291
  });
143
- });
292
+ } else {
293
+ responsePromise = doFreshFetch();
294
+ }
144
295
 
145
296
  try {
146
- // Deserialize RSC payload
147
297
  const payload = await deps.createFromFetch<RscPayload>(responsePromise);
298
+
299
+ if (tx) {
300
+ browserDebugLog(tx, "response received", {
301
+ isPartial: payload.metadata?.isPartial,
302
+ matchedCount: payload.metadata?.matched?.length ?? 0,
303
+ diffCount: payload.metadata?.diff?.length ?? 0,
304
+ });
305
+ }
148
306
  return { payload, streamComplete };
149
307
  } catch (error) {
150
308
  // Convert network-level errors to NetworkError for proper handling