@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430

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