@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71

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 (307) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4951 -930
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +135 -35
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +748 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1379 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +151 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -5,7 +5,23 @@ 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
+ consumeInflightPrefetch,
23
+ consumePrefetch,
24
+ } from "./prefetch/cache.js";
9
25
 
10
26
  /**
11
27
  * Create a navigation client for fetching RSC payloads
@@ -13,21 +29,12 @@ import { NetworkError, isNetworkError } from "../errors.js";
13
29
  * The client handles building URLs with RSC parameters and
14
30
  * deserializing the response using the RSC runtime.
15
31
  *
32
+ * Checks the in-memory prefetch cache before making a network request.
33
+ * The cache key is source-dependent (includes the previous URL) so
34
+ * prefetch responses match the exact diff the server would produce.
35
+ *
16
36
  * @param deps - RSC browser dependencies (createFromFetch)
17
37
  * @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
38
  */
32
39
  export function createNavigationClient(
33
40
  deps: Pick<RscBrowserDependencies, "createFromFetch">,
@@ -36,8 +43,9 @@ export function createNavigationClient(
36
43
  /**
37
44
  * Fetch a partial RSC payload for navigation
38
45
  *
39
- * Sends current segment IDs to the server so it can determine
40
- * which segments need to be re-rendered (diff).
46
+ * First checks the in-memory prefetch cache for a matching entry.
47
+ * If found, uses the cached response instantly. Otherwise sends
48
+ * current segment IDs to the server for diff-based rendering.
41
49
  *
42
50
  * @param options - Fetch options
43
51
  * @returns RSC payload with segments and metadata, plus stream completion promise
@@ -53,17 +61,25 @@ export function createNavigationClient(
53
61
  staleRevalidation,
54
62
  interceptSourceUrl,
55
63
  version,
64
+ routerId,
65
+ hmr,
56
66
  } = options;
57
67
 
58
- console.log(`\n[Browser] >>> NAVIGATION`);
59
- console.log(`[Browser] From: ${previousUrl}`);
60
- console.log(`[Browser] To: ${targetUrl}`);
61
- console.log(`[Browser] Segments to send: ${segmentIds.join(", ")}`);
62
- if (staleRevalidation) {
63
- console.log(`[Browser] Stale revalidation request`);
68
+ const debugEnabled = isBrowserDebugEnabled();
69
+ const tx = debugEnabled
70
+ ? startBrowserTransaction(staleRevalidation ? "revalidate" : "navigate")
71
+ : null;
72
+ if (tx) {
73
+ browserDebugLog(tx, "request start", {
74
+ from: previousUrl,
75
+ to: targetUrl,
76
+ segments: segmentIds,
77
+ staleRevalidation: !!staleRevalidation,
78
+ });
64
79
  }
65
80
 
66
- // Build fetch URL with partial rendering params
81
+ // Build fetch URL with partial rendering params (used for both
82
+ // cache key lookup and actual fetch if cache misses)
67
83
  const fetchUrl = new URL(targetUrl, window.location.origin);
68
84
  fetchUrl.searchParams.set("_rsc_partial", "true");
69
85
  fetchUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
@@ -73,75 +89,169 @@ export function createNavigationClient(
73
89
  if (version) {
74
90
  fetchUrl.searchParams.set("_rsc_v", version);
75
91
  }
92
+ if (routerId) {
93
+ fetchUrl.searchParams.set("_rsc_rid", routerId);
94
+ }
76
95
 
77
- console.log(`[Browser] Fetching: ${fetchUrl.pathname}${fetchUrl.search}`);
78
-
96
+ // Check completed in-memory prefetch cache before making a network request.
97
+ // The cache key includes the source URL (previousUrl) because the
98
+ // server's diff response depends on the source page context.
99
+ // Skip cache for stale revalidation (needs fresh data), HMR (needs
100
+ // fresh modules), and intercept contexts (source-dependent responses).
101
+ //
102
+ const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
103
+ const cacheKey = buildPrefetchKey(previousUrl, fetchUrl);
104
+ const cachedResponse = canUsePrefetch ? consumePrefetch(cacheKey) : null;
105
+ const inflightResponsePromise = canUsePrefetch
106
+ ? consumeInflightPrefetch(cacheKey)
107
+ : null;
79
108
  // Track when the stream completes
80
109
  let resolveStreamComplete: () => void;
81
110
  const streamComplete = new Promise<void>((resolve) => {
82
111
  resolveStreamComplete = resolve;
83
112
  });
84
113
 
85
- // Create a response promise that tracks stream completion
86
- const responsePromise = fetch(fetchUrl, {
87
- headers: {
88
- "X-RSC-Router-Client-Path": previousUrl,
89
- ...(interceptSourceUrl && {
90
- "X-RSC-Router-Intercept-Source": interceptSourceUrl,
91
- }),
92
- },
93
- signal,
94
- }).then((response) => {
95
- // Check for version mismatch - server wants us to reload
96
- const reloadUrl = response.headers.get("X-RSC-Reload");
97
- if (reloadUrl) {
98
- console.log(`[Browser] Version mismatch - reloading: ${reloadUrl}`);
99
- window.location.href = reloadUrl;
100
- // Return a never-resolving promise to prevent further processing
114
+ /**
115
+ * Validate RSC control headers on any response (fresh, cached, or
116
+ * in-flight). Handles version-mismatch reloads and server redirects.
117
+ * Returns the response unchanged when no control header is present.
118
+ */
119
+ const validateRscHeaders = (
120
+ response: Response,
121
+ source: string,
122
+ ): Response | Promise<Response> => {
123
+ // Version mismatch — server wants a full page reload
124
+ const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
125
+ if (reload === "blocked") {
126
+ resolveStreamComplete();
127
+ return emptyResponse();
128
+ }
129
+ if (reload) {
130
+ if (tx) {
131
+ browserDebugLog(tx, `version mismatch, reloading (${source})`, {
132
+ reloadUrl: reload.url,
133
+ });
134
+ }
135
+ window.location.href = reload.url;
136
+ // Block further processing — page is reloading
101
137
  return new Promise<Response>(() => {});
102
138
  }
103
139
 
104
- if (!response.body) {
105
- // No body means stream is already complete
140
+ // Server-side redirect without state: the server returned 204 with
141
+ // X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
142
+ // to a URL rendering full HTML). Throw ServerRedirect so the
143
+ // navigation bridge catches it and re-navigates with _skipCache.
144
+ const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
145
+ if (redirect === "blocked") {
146
+ resolveStreamComplete();
147
+ return emptyResponse();
148
+ }
149
+ if (redirect) {
150
+ if (tx) {
151
+ browserDebugLog(tx, `server redirect (${source})`, {
152
+ redirectUrl: redirect.url,
153
+ });
154
+ }
106
155
  resolveStreamComplete();
107
- return response;
156
+ throw new ServerRedirect(redirect.url, undefined);
157
+ }
158
+
159
+ return response;
160
+ };
161
+
162
+ /** Start a fresh navigation fetch (no cache / inflight hit). */
163
+ const doFreshFetch = (): Promise<Response> => {
164
+ if (tx) {
165
+ browserDebugLog(tx, "fetching", {
166
+ path: `${fetchUrl.pathname}${fetchUrl.search}`,
167
+ });
108
168
  }
109
169
 
110
- // Tee the stream: one for RSC runtime, one for tracking completion
111
- const [rscStream, trackingStream] = response.body.tee();
170
+ return fetch(fetchUrl, {
171
+ headers: {
172
+ "X-RSC-Router-Client-Path": previousUrl,
173
+ "X-Rango-State": getRangoState(),
174
+ ...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
175
+ ...(interceptSourceUrl && {
176
+ "X-RSC-Router-Intercept-Source": interceptSourceUrl,
177
+ }),
178
+ ...(hmr && { "X-RSC-HMR": "1" }),
179
+ },
180
+ signal,
181
+ }).then((response) => {
182
+ const validated = validateRscHeaders(response, "fetch");
183
+ if (validated instanceof Promise) return validated;
112
184
 
113
- // Consume the tracking stream to detect when it closes
114
- (async () => {
115
- const reader = trackingStream.getReader();
185
+ return teeWithCompletion(
186
+ validated,
187
+ () => {
188
+ if (tx) browserDebugLog(tx, "stream complete");
189
+ resolveStreamComplete();
190
+ },
191
+ signal,
192
+ );
193
+ });
194
+ };
116
195
 
117
- // Cancel tracking if navigation is aborted
118
- const onAbort = reader.cancel.bind(reader);
119
- signal?.addEventListener("abort", onAbort, { once: true });
196
+ let responsePromise: Promise<Response>;
120
197
 
121
- try {
122
- while (true) {
123
- const { done } = await reader.read();
124
- if (done) break;
198
+ if (cachedResponse) {
199
+ if (tx) {
200
+ browserDebugLog(tx, "prefetch cache hit", { key: cacheKey });
201
+ }
202
+ responsePromise = Promise.resolve(cachedResponse).then((response) => {
203
+ const validated = validateRscHeaders(response, "prefetch cache");
204
+ if (validated instanceof Promise) return validated;
205
+
206
+ return teeWithCompletion(
207
+ validated,
208
+ () => {
209
+ if (tx) browserDebugLog(tx, "stream complete (from cache)");
210
+ resolveStreamComplete();
211
+ },
212
+ signal,
213
+ );
214
+ });
215
+ } else if (inflightResponsePromise) {
216
+ if (tx) {
217
+ browserDebugLog(tx, "reusing inflight prefetch", { key: cacheKey });
218
+ }
219
+ responsePromise = inflightResponsePromise.then(async (response) => {
220
+ if (!response) {
221
+ if (tx) {
222
+ browserDebugLog(tx, "inflight prefetch unavailable, refetching");
125
223
  }
126
- } finally {
127
- signal?.removeEventListener("abort", onAbort);
128
- reader.releaseLock();
129
- console.log("[STREAMING] RSC stream complete");
130
- resolveStreamComplete();
224
+ return doFreshFetch();
131
225
  }
132
- })();
133
226
 
134
- // Return response with the RSC stream
135
- return new Response(rscStream, {
136
- headers: response.headers,
137
- status: response.status,
138
- statusText: response.statusText,
227
+ const validated = validateRscHeaders(response, "inflight prefetch");
228
+ if (validated instanceof Promise) return validated;
229
+
230
+ return teeWithCompletion(
231
+ validated,
232
+ () => {
233
+ if (tx) {
234
+ browserDebugLog(tx, "stream complete (from inflight prefetch)");
235
+ }
236
+ resolveStreamComplete();
237
+ },
238
+ signal,
239
+ );
139
240
  });
140
- });
241
+ } else {
242
+ responsePromise = doFreshFetch();
243
+ }
141
244
 
142
245
  try {
143
- // Deserialize RSC payload
144
246
  const payload = await deps.createFromFetch<RscPayload>(responsePromise);
247
+
248
+ if (tx) {
249
+ browserDebugLog(tx, "response received", {
250
+ isPartial: payload.metadata?.isPartial,
251
+ matchedCount: payload.metadata?.matched?.length ?? 0,
252
+ diffCount: payload.metadata?.diff?.length ?? 0,
253
+ });
254
+ }
145
255
  return { payload, streamComplete };
146
256
  } catch (error) {
147
257
  // Convert network-level errors to NetworkError for proper handling
@@ -12,6 +12,7 @@ import type {
12
12
  ActionStateListener,
13
13
  HandleData,
14
14
  } from "./types.js";
15
+ import { clearPrefetchCache } from "./prefetch/cache.js";
15
16
 
16
17
  /**
17
18
  * Default action state (idle with no payload)
@@ -27,9 +28,15 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
27
28
  // Maximum number of history entries to cache (URLs visited)
28
29
  const HISTORY_CACHE_SIZE = 20;
29
30
 
30
- // Cache entry: [url-key, segments, stale, handleData?]
31
+ // Cache entry: [url-key, segments, stale, handleData?, routerId?]
31
32
  // stale=true means the data may be outdated and should be revalidated on access
32
- type HistoryCacheEntry = [string, ResolvedSegment[], boolean, HandleData?];
33
+ type HistoryCacheEntry = [
34
+ string,
35
+ ResolvedSegment[],
36
+ boolean,
37
+ HandleData?,
38
+ string?,
39
+ ];
33
40
 
34
41
  /**
35
42
  * Shallow clone handleData to avoid reference sharing between cache entries.
@@ -88,7 +95,7 @@ export interface HistoryKeyOptions {
88
95
  */
89
96
  export function generateHistoryKey(
90
97
  url?: string,
91
- options?: HistoryKeyOptions
98
+ options?: HistoryKeyOptions,
92
99
  ): string {
93
100
  if (!url) {
94
101
  url = typeof window !== "undefined" ? window.location.href : "/";
@@ -182,7 +189,7 @@ function createLocation(loc: { href: string }): NavigationLocation {
182
189
  * ```
183
190
  */
184
191
  export function createNavigationStore(
185
- config?: NavigationStoreConfig
192
+ config?: NavigationStoreConfig,
186
193
  ): NavigationStore {
187
194
  // Default location from window or config
188
195
  const defaultLocation: NavigationLocation =
@@ -257,6 +264,11 @@ export function createNavigationStore(
257
264
  // Used to maintain intercept context during action revalidation
258
265
  let interceptSourceUrl: string | null = null;
259
266
 
267
+ // Router identity - tracks which router is currently active.
268
+ // When this changes on a partial response, the client forces a full
269
+ // tree replacement instead of reconciling with stale segments.
270
+ let currentRouterId: string | undefined;
271
+
260
272
  // Action state tracking (for useAction hook)
261
273
  // Maps action function ID to its tracked state
262
274
  const actionStates = new Map<string, TrackedActionState>();
@@ -270,7 +282,7 @@ export function createNavigationStore(
270
282
  */
271
283
  function createDebouncedNotifier<T extends (...args: any[]) => void>(
272
284
  fn: T,
273
- ms: number = 20
285
+ ms: number = 20,
274
286
  ): T {
275
287
  let timeout: ReturnType<typeof setTimeout> | null = null;
276
288
  return ((...args: Parameters<T>) => {
@@ -297,7 +309,7 @@ export function createNavigationStore(
297
309
  setTimeout(() => {
298
310
  timeouts.delete(key);
299
311
  fn(key, ...args);
300
- }, ms)
312
+ }, ms),
301
313
  );
302
314
  }) as T;
303
315
  }
@@ -312,7 +324,7 @@ export function createNavigationStore(
312
324
  if (listeners) {
313
325
  listeners.forEach((listener) => listener(state));
314
326
  }
315
- }
327
+ },
316
328
  );
317
329
 
318
330
  /**
@@ -320,6 +332,7 @@ export function createNavigationStore(
320
332
  */
321
333
  function clearCacheInternal(): void {
322
334
  historyCache.length = 0;
335
+ clearPrefetchCache();
323
336
  }
324
337
 
325
338
  /**
@@ -329,13 +342,13 @@ export function createNavigationStore(
329
342
  for (let i = 0; i < historyCache.length; i++) {
330
343
  historyCache[i][2] = true;
331
344
  }
345
+ clearPrefetchCache();
332
346
  }
333
347
 
334
348
  /**
335
349
  * Clear the history cache and broadcast to other tabs
336
350
  */
337
351
  function clearCacheAndBroadcast(): void {
338
- console.log("[Browser] Clearing cache and broadcasting to other tabs");
339
352
  clearCacheInternal();
340
353
  broadcastInvalidation();
341
354
  }
@@ -344,9 +357,6 @@ export function createNavigationStore(
344
357
  * Mark cache as stale and broadcast to other tabs
345
358
  */
346
359
  function markStaleAndBroadcast(): void {
347
- console.log(
348
- "[Browser] Marking cache as stale and broadcasting to other tabs"
349
- );
350
360
  markCacheAsStaleInternal();
351
361
  broadcastInvalidation();
352
362
  }
@@ -369,14 +379,6 @@ export function createNavigationStore(
369
379
  path: currentPath,
370
380
  segmentIds: currentSegmentIds,
371
381
  });
372
- console.log(
373
- "[Browser] Broadcast sent for path:",
374
- currentPath,
375
- "segments:",
376
- currentSegmentIds.join(", ")
377
- );
378
- } else {
379
- console.warn("[Browser] No BroadcastChannel available");
380
382
  }
381
383
  }
382
384
 
@@ -393,7 +395,7 @@ export function createNavigationStore(
393
395
  // Check for shared segments between tabs
394
396
  // Routes sharing any segment (layout, loader, etc.) should invalidate together
395
397
  const hasSharedSegment = mutatedSegmentIds.some((id) =>
396
- currentSegmentIds.includes(id)
398
+ currentSegmentIds.includes(id),
397
399
  );
398
400
 
399
401
  if (!hasSharedSegment) {
@@ -401,34 +403,21 @@ export function createNavigationStore(
401
403
  return;
402
404
  }
403
405
 
404
- console.log(
405
- "[Browser] Cache marked stale by another tab, shared segments:",
406
- mutatedSegmentIds
407
- .filter((id) => currentSegmentIds.includes(id))
408
- .join(", ")
409
- );
410
406
  markCacheAsStaleInternal();
411
407
 
412
408
  // Auto-refresh if enabled and callback is registered
413
409
  if (crossTabAutoRefresh && crossTabRefreshCallback) {
414
410
  // If idle, refresh immediately. If loading, wait for idle then refresh.
415
411
  if (navState.state === "idle") {
416
- console.log("[Browser] Cross-tab refresh triggered (idle)");
417
412
  crossTabRefreshCallback();
418
413
  } else if (!pendingCrossTabRefresh) {
419
414
  // Only queue one refresh, ignore subsequent events while loading
420
415
  pendingCrossTabRefresh = true;
421
- console.log(
422
- "[Browser] Navigation in progress, deferring cross-tab refresh"
423
- );
424
416
  // Subscribe to state changes, refresh when idle
425
417
  const listener: StateListener = () => {
426
418
  if (navState.state === "idle") {
427
419
  stateListeners.delete(listener);
428
420
  pendingCrossTabRefresh = false;
429
- console.log(
430
- "[Browser] Cross-tab refresh triggered (deferred)"
431
- );
432
421
  crossTabRefreshCallback?.();
433
422
  }
434
423
  };
@@ -574,7 +563,7 @@ export function createNavigationStore(
574
563
  cacheSegmentsForHistory(
575
564
  historyKey: string,
576
565
  segments: ResolvedSegment[],
577
- handleData?: HandleData
566
+ handleData?: HandleData,
578
567
  ): void {
579
568
  // Shallow clone handleData arrays to avoid reference sharing between cache entries
580
569
  // We only clone the structure (objects and arrays), not the data items themselves,
@@ -585,13 +574,25 @@ export function createNavigationStore(
585
574
 
586
575
  // Check if entry already exists and update it
587
576
  const existingIndex = historyCache.findIndex(
588
- ([key]) => key === historyKey
577
+ ([key]) => key === historyKey,
589
578
  );
590
579
  if (existingIndex !== -1) {
591
- historyCache[existingIndex] = [historyKey, segments, false, clonedHandleData];
580
+ historyCache[existingIndex] = [
581
+ historyKey,
582
+ segments,
583
+ false,
584
+ clonedHandleData,
585
+ currentRouterId,
586
+ ];
592
587
  } else {
593
588
  // Add new entry at the end (not stale)
594
- historyCache.push([historyKey, segments, false, clonedHandleData]);
589
+ historyCache.push([
590
+ historyKey,
591
+ segments,
592
+ false,
593
+ clonedHandleData,
594
+ currentRouterId,
595
+ ]);
595
596
  // Remove oldest entries if over limit
596
597
  while (historyCache.length > cacheSize) {
597
598
  historyCache.shift();
@@ -603,12 +604,22 @@ export function createNavigationStore(
603
604
  * Get cached segments for a history entry
604
605
  * Returns { segments, stale, handleData } or undefined if not cached
605
606
  */
606
- getCachedSegments(
607
- historyKey: string
608
- ): { segments: ResolvedSegment[]; stale: boolean; handleData?: HandleData } | undefined {
607
+ getCachedSegments(historyKey: string):
608
+ | {
609
+ segments: ResolvedSegment[];
610
+ stale: boolean;
611
+ handleData?: HandleData;
612
+ routerId?: string;
613
+ }
614
+ | undefined {
609
615
  const entry = historyCache.find(([key]) => key === historyKey);
610
616
  if (!entry) return undefined;
611
- return { segments: entry[1], stale: entry[2], handleData: entry[3] };
617
+ return {
618
+ segments: entry[1],
619
+ stale: entry[2],
620
+ handleData: entry[3],
621
+ routerId: entry[4],
622
+ };
612
623
  },
613
624
 
614
625
  /**
@@ -625,13 +636,19 @@ export function createNavigationStore(
625
636
  */
626
637
  updateCacheHandleData(historyKey: string, handleData: HandleData): void {
627
638
  const existingIndex = historyCache.findIndex(
628
- ([key]) => key === historyKey
639
+ ([key]) => key === historyKey,
629
640
  );
630
641
  if (existingIndex !== -1) {
631
642
  const entry = historyCache[existingIndex];
632
643
  // Shallow clone handleData arrays to avoid reference sharing
633
644
  const clonedHandleData = cloneHandleData(handleData);
634
- historyCache[existingIndex] = [entry[0], entry[1], entry[2], clonedHandleData];
645
+ historyCache[existingIndex] = [
646
+ entry[0],
647
+ entry[1],
648
+ entry[2],
649
+ clonedHandleData,
650
+ entry[4], // preserve routerId
651
+ ];
635
652
  }
636
653
  },
637
654
 
@@ -640,14 +657,7 @@ export function createNavigationStore(
640
657
  * Called after server actions to indicate data may be outdated
641
658
  */
642
659
  markCacheAsStale(): void {
643
- for (let i = 0; i < historyCache.length; i++) {
644
- historyCache[i][2] = true;
645
- }
646
- console.log(
647
- "[Browser] Marked",
648
- historyCache.length,
649
- "cache entries as stale"
650
- );
660
+ markCacheAsStaleInternal();
651
661
  },
652
662
 
653
663
  /**
@@ -704,6 +714,14 @@ export function createNavigationStore(
704
714
  interceptSourceUrl = url;
705
715
  },
706
716
 
717
+ getRouterId(): string | undefined {
718
+ return currentRouterId;
719
+ },
720
+
721
+ setRouterId(id: string): void {
722
+ currentRouterId = id;
723
+ },
724
+
707
725
  // ========================================================================
708
726
  // UI Update Notifications
709
727
  // ========================================================================
@@ -745,7 +763,7 @@ export function createNavigationStore(
745
763
  */
746
764
  setActionState(
747
765
  actionId: string,
748
- partial: Partial<TrackedActionState>
766
+ partial: Partial<TrackedActionState>,
749
767
  ): void {
750
768
  const current = actionStates.get(actionId) ?? { ...DEFAULT_ACTION_STATE };
751
769
  const updated: TrackedActionState = {
@@ -763,7 +781,7 @@ export function createNavigationStore(
763
781
  */
764
782
  subscribeToAction(
765
783
  actionId: string,
766
- listener: ActionStateListener
784
+ listener: ActionStateListener,
767
785
  ): () => void {
768
786
  let listeners = actionListeners.get(actionId);
769
787
  if (!listeners) {
@@ -793,7 +811,7 @@ let storeInstance: NavigationStore | null = null;
793
811
  * Subsequent calls return the existing instance.
794
812
  */
795
813
  export function initNavigationStore(
796
- config?: NavigationStoreConfig
814
+ config?: NavigationStoreConfig,
797
815
  ): NavigationStore {
798
816
  if (!storeInstance) {
799
817
  storeInstance = createNavigationStore(config);
@@ -809,7 +827,7 @@ export function initNavigationStore(
809
827
  export function getNavigationStore(): NavigationStore {
810
828
  if (!storeInstance) {
811
829
  throw new Error(
812
- "Navigation store not initialized. Call initNavigationStore first."
830
+ "Navigation store not initialized. Call initNavigationStore first.",
813
831
  );
814
832
  }
815
833
  return storeInstance;