@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918

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 (374) hide show
  1. package/README.md +211 -43
  2. package/dist/bin/rango.js +279 -102
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +3313 -1160
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +62 -11
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +63 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +222 -30
  12. package/skills/caching/SKILL.md +263 -8
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +250 -30
  18. package/skills/host-router/SKILL.md +124 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +223 -9
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +77 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +66 -5
  35. package/skills/router-setup/SKILL.md +38 -3
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +319 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/vercel/SKILL.md +107 -0
  57. package/skills/view-transitions/SKILL.md +294 -0
  58. package/src/__augment-tests__/augment.ts +81 -0
  59. package/src/__augment-tests__/augmented.check.ts +116 -0
  60. package/src/__internal.ts +1 -66
  61. package/src/browser/action-coordinator.ts +53 -36
  62. package/src/browser/action-fence.ts +47 -0
  63. package/src/browser/app-shell.ts +39 -0
  64. package/src/browser/app-version.ts +14 -0
  65. package/src/browser/cookie-name.ts +140 -0
  66. package/src/browser/event-controller.ts +81 -147
  67. package/src/browser/history-state.ts +21 -0
  68. package/src/browser/index.ts +3 -3
  69. package/src/browser/invalidate-client-cache.ts +52 -0
  70. package/src/browser/navigation-bridge.ts +66 -14
  71. package/src/browser/navigation-client.ts +172 -109
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +9 -59
  75. package/src/browser/partial-update.ts +79 -93
  76. package/src/browser/prefetch/cache.ts +180 -62
  77. package/src/browser/prefetch/fetch.ts +252 -39
  78. package/src/browser/prefetch/queue.ts +42 -8
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +72 -10
  81. package/src/browser/react/NavigationProvider.tsx +83 -31
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/context.ts +7 -2
  84. package/src/browser/react/filter-segment-order.ts +49 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +166 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +23 -69
  90. package/src/browser/react/use-link-status.ts +0 -4
  91. package/src/browser/react/use-navigation.ts +22 -5
  92. package/src/browser/react/use-params.ts +20 -10
  93. package/src/browser/react/use-reverse.ts +106 -0
  94. package/src/browser/react/use-router.ts +46 -11
  95. package/src/browser/react/use-search-params.ts +0 -5
  96. package/src/browser/react/use-segments.ts +11 -21
  97. package/src/browser/response-adapter.ts +52 -1
  98. package/src/browser/rsc-router.tsx +111 -24
  99. package/src/browser/scroll-restoration.ts +29 -19
  100. package/src/browser/segment-reconciler.ts +36 -14
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +176 -62
  103. package/src/browser/types.ts +60 -11
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +65 -40
  107. package/src/build/generate-route-types.ts +6 -0
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +137 -32
  111. package/src/build/route-types/codegen.ts +4 -4
  112. package/src/build/route-types/include-resolution.ts +9 -2
  113. package/src/build/route-types/param-extraction.ts +6 -3
  114. package/src/build/route-types/per-module-writer.ts +7 -4
  115. package/src/build/route-types/router-processing.ts +333 -94
  116. package/src/build/route-types/scan-filter.ts +9 -2
  117. package/src/build/route-types/source-scan.ts +118 -0
  118. package/src/build/runtime-discovery.ts +9 -20
  119. package/src/cache/cache-error.ts +104 -0
  120. package/src/cache/cache-policy.ts +68 -28
  121. package/src/cache/cache-runtime.ts +134 -32
  122. package/src/cache/cache-scope.ts +100 -74
  123. package/src/cache/cache-tag.ts +98 -0
  124. package/src/cache/cf/cf-cache-store.ts +2256 -241
  125. package/src/cache/cf/index.ts +6 -16
  126. package/src/cache/document-cache.ts +61 -20
  127. package/src/cache/handle-snapshot.ts +63 -0
  128. package/src/cache/index.ts +22 -20
  129. package/src/cache/memory-segment-store.ts +136 -37
  130. package/src/cache/profile-registry.ts +6 -30
  131. package/src/cache/read-through-swr.ts +41 -11
  132. package/src/cache/segment-codec.ts +0 -16
  133. package/src/cache/tag-invalidation.ts +230 -0
  134. package/src/cache/types.ts +33 -100
  135. package/src/cache/vercel/index.ts +11 -0
  136. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  137. package/src/client.rsc.tsx +6 -21
  138. package/src/client.tsx +108 -290
  139. package/src/component-utils.ts +19 -0
  140. package/src/context-var.ts +17 -5
  141. package/src/decode-loader-results.ts +36 -0
  142. package/src/defer.ts +196 -0
  143. package/src/deps/browser.ts +0 -1
  144. package/src/deps/ssr.ts +0 -1
  145. package/src/errors.ts +30 -4
  146. package/src/handle.ts +70 -22
  147. package/src/handles/MetaTags.tsx +0 -14
  148. package/src/handles/breadcrumbs.ts +16 -5
  149. package/src/handles/meta.ts +0 -39
  150. package/src/host/cookie-handler.ts +0 -36
  151. package/src/host/errors.ts +0 -24
  152. package/src/host/index.ts +8 -2
  153. package/src/host/pattern-matcher.ts +7 -50
  154. package/src/host/router.ts +107 -99
  155. package/src/host/testing.ts +40 -27
  156. package/src/host/types.ts +37 -4
  157. package/src/host/utils.ts +1 -1
  158. package/src/href-client.ts +137 -22
  159. package/src/index.rsc.ts +69 -10
  160. package/src/index.ts +112 -14
  161. package/src/internal-debug.ts +2 -4
  162. package/src/loader-store.ts +500 -0
  163. package/src/loader.rsc.ts +20 -13
  164. package/src/loader.ts +12 -11
  165. package/src/missing-id-error.ts +68 -0
  166. package/src/network-error-thrower.tsx +1 -6
  167. package/src/outlet-context.ts +1 -1
  168. package/src/outlet-provider.tsx +1 -5
  169. package/src/prerender/param-hash.ts +10 -11
  170. package/src/prerender/store.ts +37 -41
  171. package/src/prerender.ts +198 -82
  172. package/src/redirect-origin.ts +100 -0
  173. package/src/response-utils.ts +37 -0
  174. package/src/reverse.ts +65 -15
  175. package/src/root-error-boundary.tsx +1 -19
  176. package/src/route-content-wrapper.tsx +7 -72
  177. package/src/route-definition/dsl-helpers.ts +413 -275
  178. package/src/route-definition/helper-factories.ts +29 -139
  179. package/src/route-definition/helpers-types.ts +107 -32
  180. package/src/route-definition/index.ts +3 -0
  181. package/src/route-definition/redirect.ts +50 -8
  182. package/src/route-definition/resolve-handler-use.ts +161 -0
  183. package/src/route-definition/use-item-types.ts +32 -0
  184. package/src/route-map-builder.ts +0 -16
  185. package/src/route-types.ts +37 -41
  186. package/src/router/basename.ts +14 -0
  187. package/src/router/content-negotiation.ts +108 -9
  188. package/src/router/error-handling.ts +13 -17
  189. package/src/router/find-match.ts +44 -23
  190. package/src/router/handler-context.ts +46 -30
  191. package/src/router/intercept-resolution.ts +23 -23
  192. package/src/router/lazy-includes.ts +15 -52
  193. package/src/router/loader-resolution.ts +207 -30
  194. package/src/router/logging.ts +0 -6
  195. package/src/router/manifest.ts +40 -42
  196. package/src/router/match-api.ts +120 -204
  197. package/src/router/match-context.ts +0 -22
  198. package/src/router/match-handlers.ts +58 -58
  199. package/src/router/match-middleware/background-revalidation.ts +0 -7
  200. package/src/router/match-middleware/cache-lookup.ts +161 -262
  201. package/src/router/match-middleware/cache-store.ts +3 -33
  202. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  203. package/src/router/match-middleware/segment-resolution.ts +45 -14
  204. package/src/router/match-pipelines.ts +1 -42
  205. package/src/router/match-result.ts +87 -39
  206. package/src/router/metrics.ts +0 -34
  207. package/src/router/middleware-types.ts +7 -140
  208. package/src/router/middleware.ts +169 -140
  209. package/src/router/navigation-snapshot.ts +131 -0
  210. package/src/router/params-util.ts +23 -0
  211. package/src/router/pattern-matching.ts +109 -63
  212. package/src/router/prerender-match.ts +190 -54
  213. package/src/router/preview-match.ts +32 -102
  214. package/src/router/request-classification.ts +276 -0
  215. package/src/router/revalidation.ts +63 -55
  216. package/src/router/route-snapshot.ts +244 -0
  217. package/src/router/router-context.ts +0 -27
  218. package/src/router/router-interfaces.ts +100 -35
  219. package/src/router/router-options.ts +91 -11
  220. package/src/router/router-registry.ts +2 -5
  221. package/src/router/segment-resolution/fresh.ts +119 -65
  222. package/src/router/segment-resolution/helpers.ts +34 -0
  223. package/src/router/segment-resolution/loader-cache.ts +40 -37
  224. package/src/router/segment-resolution/revalidation.ts +329 -305
  225. package/src/router/segment-resolution/static-store.ts +19 -5
  226. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  227. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  228. package/src/router/segment-resolution.ts +4 -1
  229. package/src/router/segment-wrappers.ts +0 -3
  230. package/src/router/state-cookie-name.ts +33 -0
  231. package/src/router/substitute-pattern-params.ts +56 -0
  232. package/src/router/telemetry-otel.ts +0 -20
  233. package/src/router/telemetry.ts +96 -19
  234. package/src/router/timeout.ts +0 -20
  235. package/src/router/trie-matching.ts +91 -46
  236. package/src/router/types.ts +9 -63
  237. package/src/router/url-params.ts +44 -0
  238. package/src/router.ts +128 -42
  239. package/src/rsc/handler-context.ts +3 -2
  240. package/src/rsc/handler.ts +492 -409
  241. package/src/rsc/helpers.ts +162 -46
  242. package/src/rsc/index.ts +1 -1
  243. package/src/rsc/json-route-result.ts +38 -0
  244. package/src/rsc/loader-fetch.ts +18 -3
  245. package/src/rsc/manifest-init.ts +33 -42
  246. package/src/rsc/origin-guard.ts +39 -25
  247. package/src/rsc/progressive-enhancement.ts +28 -4
  248. package/src/rsc/redirect-guard.ts +99 -0
  249. package/src/rsc/response-error.ts +79 -12
  250. package/src/rsc/response-route-handler.ts +90 -63
  251. package/src/rsc/rsc-rendering.ts +53 -56
  252. package/src/rsc/runtime-warnings.ts +23 -10
  253. package/src/rsc/server-action.ts +74 -69
  254. package/src/rsc/ssr-setup.ts +18 -2
  255. package/src/rsc/types.ts +22 -9
  256. package/src/runtime-env.ts +18 -0
  257. package/src/search-params.ts +4 -20
  258. package/src/segment-content-promise.ts +67 -0
  259. package/src/segment-loader-promise.ts +134 -0
  260. package/src/segment-system.tsx +208 -201
  261. package/src/serialize.ts +243 -0
  262. package/src/server/context.ts +211 -52
  263. package/src/server/cookie-store.ts +80 -5
  264. package/src/server/handle-store.ts +26 -24
  265. package/src/server/loader-registry.ts +10 -28
  266. package/src/server/request-context.ts +289 -124
  267. package/src/ssr/index.tsx +22 -15
  268. package/src/static-handler.ts +27 -18
  269. package/src/testing/cache-status.ts +162 -0
  270. package/src/testing/collect-handle.ts +40 -0
  271. package/src/testing/dispatch.ts +618 -0
  272. package/src/testing/dom.entry.ts +22 -0
  273. package/src/testing/e2e/fixture.ts +188 -0
  274. package/src/testing/e2e/index.ts +128 -0
  275. package/src/testing/e2e/matchers.ts +35 -0
  276. package/src/testing/e2e/page-helpers.ts +272 -0
  277. package/src/testing/e2e/parity.ts +387 -0
  278. package/src/testing/e2e/server.ts +195 -0
  279. package/src/testing/flight-matchers.ts +97 -0
  280. package/src/testing/flight-normalize.ts +11 -0
  281. package/src/testing/flight-runtime.d.ts +57 -0
  282. package/src/testing/flight-tree.ts +682 -0
  283. package/src/testing/flight.entry.ts +52 -0
  284. package/src/testing/flight.ts +232 -0
  285. package/src/testing/generated-routes.ts +183 -0
  286. package/src/testing/index.ts +99 -0
  287. package/src/testing/internal/context.ts +348 -0
  288. package/src/testing/internal/flight-client-globals.ts +30 -0
  289. package/src/testing/internal/seed-vars.ts +54 -0
  290. package/src/testing/render-handler.ts +330 -0
  291. package/src/testing/render-route.tsx +566 -0
  292. package/src/testing/run-loader.ts +378 -0
  293. package/src/testing/run-middleware.ts +205 -0
  294. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  295. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  297. package/src/testing/vitest-stubs/version.ts +5 -0
  298. package/src/testing/vitest.ts +305 -0
  299. package/src/theme/ThemeProvider.tsx +0 -52
  300. package/src/theme/ThemeScript.tsx +0 -6
  301. package/src/theme/constants.ts +0 -12
  302. package/src/theme/index.ts +0 -7
  303. package/src/theme/theme-context.ts +1 -5
  304. package/src/theme/theme-script.ts +0 -14
  305. package/src/theme/use-theme.ts +0 -3
  306. package/src/types/boundaries.ts +0 -35
  307. package/src/types/cache-types.ts +17 -8
  308. package/src/types/error-types.ts +30 -90
  309. package/src/types/global-namespace.ts +54 -41
  310. package/src/types/handler-context.ts +124 -70
  311. package/src/types/index.ts +1 -10
  312. package/src/types/loader-types.ts +40 -11
  313. package/src/types/request-scope.ts +107 -0
  314. package/src/types/route-config.ts +6 -50
  315. package/src/types/route-entry.ts +12 -7
  316. package/src/types/segments.ts +36 -15
  317. package/src/urls/include-helper.ts +33 -70
  318. package/src/urls/index.ts +1 -11
  319. package/src/urls/path-helper-types.ts +58 -11
  320. package/src/urls/path-helper.ts +57 -111
  321. package/src/urls/pattern-types.ts +48 -19
  322. package/src/urls/response-types.ts +25 -22
  323. package/src/urls/type-extraction.ts +58 -139
  324. package/src/urls/urls-function.ts +1 -18
  325. package/src/use-loader.tsx +346 -89
  326. package/src/vite/debug.ts +185 -0
  327. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  328. package/src/vite/discovery/discover-routers.ts +130 -85
  329. package/src/vite/discovery/discovery-errors.ts +194 -0
  330. package/src/vite/discovery/gate-state.ts +171 -0
  331. package/src/vite/discovery/prerender-collection.ts +192 -99
  332. package/src/vite/discovery/route-types-writer.ts +40 -84
  333. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  334. package/src/vite/discovery/state.ts +51 -4
  335. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  336. package/src/vite/index.ts +8 -0
  337. package/src/vite/plugin-types.ts +236 -6
  338. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  339. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  340. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  341. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  342. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  343. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  344. package/src/vite/plugins/expose-action-id.ts +48 -95
  345. package/src/vite/plugins/expose-id-utils.ts +11 -50
  346. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  347. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  348. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  349. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  350. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  351. package/src/vite/plugins/performance-tracks.ts +65 -207
  352. package/src/vite/plugins/refresh-cmd.ts +89 -27
  353. package/src/vite/plugins/use-cache-transform.ts +73 -83
  354. package/src/vite/plugins/vercel-output.ts +258 -0
  355. package/src/vite/plugins/version-injector.ts +21 -25
  356. package/src/vite/plugins/version-plugin.ts +32 -23
  357. package/src/vite/plugins/virtual-entries.ts +46 -17
  358. package/src/vite/rango.ts +207 -125
  359. package/src/vite/router-discovery.ts +931 -133
  360. package/src/vite/utils/ast-handler-extract.ts +15 -31
  361. package/src/vite/utils/banner.ts +1 -1
  362. package/src/vite/utils/bundle-analysis.ts +10 -15
  363. package/src/vite/utils/client-chunks.ts +184 -0
  364. package/src/vite/utils/forward-user-plugins.ts +171 -0
  365. package/src/vite/utils/manifest-utils.ts +4 -59
  366. package/src/vite/utils/package-resolution.ts +20 -52
  367. package/src/vite/utils/prerender-utils.ts +27 -29
  368. package/src/vite/utils/shared-utils.ts +97 -44
  369. package/src/browser/action-response-classifier.ts +0 -99
  370. package/src/browser/debug-channel.ts +0 -93
  371. package/src/browser/react/use-client-cache.ts +0 -58
  372. package/src/browser/shallow.ts +0 -40
  373. package/src/handles/index.ts +0 -7
  374. package/src/router/middleware-cookies.ts +0 -55
@@ -12,17 +12,20 @@ import {
12
12
  startBrowserTransaction,
13
13
  } from "./logging.js";
14
14
  import { getRangoState } from "./rango-state.js";
15
- import { createClientDebugChannel, DEBUG_ID_HEADER } from "./debug-channel.js";
16
- import { findSourceMapURL } from "../deps/browser.js";
15
+ import { isActionFenceActive } from "./action-fence.js";
17
16
  import {
18
17
  extractRscHeaderUrl,
19
18
  emptyResponse,
19
+ handleReloadHeader,
20
20
  teeWithCompletion,
21
+ isForeignRouterId,
21
22
  } from "./response-adapter.js";
22
23
  import {
23
24
  buildPrefetchKey,
25
+ buildSourceKey,
24
26
  consumeInflightPrefetch,
25
27
  consumePrefetch,
28
+ type DecodedPrefetch,
26
29
  } from "./prefetch/cache.js";
27
30
 
28
31
  /**
@@ -32,8 +35,10 @@ import {
32
35
  * deserializing the response using the RSC runtime.
33
36
  *
34
37
  * Checks the in-memory prefetch cache before making a network request.
35
- * The cache key is source-dependent (includes the previous URL) so
36
- * prefetch responses match the exact diff the server would produce.
38
+ * Tries the source-scoped key first (populated when the server tagged
39
+ * the response as source-sensitive via `X-RSC-Prefetch-Scope: source`)
40
+ * and falls back to the Rango-state-keyed wildcard slot used for the
41
+ * common source-agnostic case.
37
42
  *
38
43
  * @param deps - RSC browser dependencies (createFromFetch)
39
44
  * @returns NavigationClient instance
@@ -63,6 +68,7 @@ export function createNavigationClient(
63
68
  staleRevalidation,
64
69
  interceptSourceUrl,
65
70
  version,
71
+ routerId,
66
72
  hmr,
67
73
  } = options;
68
74
 
@@ -90,40 +96,115 @@ export function createNavigationClient(
90
96
  if (version) {
91
97
  fetchUrl.searchParams.set("_rsc_v", version);
92
98
  }
99
+ if (routerId) {
100
+ fetchUrl.searchParams.set("_rsc_rid", routerId);
101
+ }
93
102
 
94
- // Check completed in-memory prefetch cache before making a network request.
95
- // The cache key includes the source URL (previousUrl) because the
96
- // server's diff response depends on the source page context.
103
+ // Check completed in-memory prefetch cache before making a network
104
+ // request. Try the source-scoped key first (populated when the server
105
+ // tagged the prefetch response as source-sensitive, e.g. intercepts,
106
+ // or when a Link opted in with `prefetchKey=":source"`), then fall
107
+ // back to the wildcard slot shared across source pages.
108
+ // Both keys embed the Rango state, so state rotation (deploy or
109
+ // server-action invalidation) auto-invalidates both scopes.
97
110
  // Skip cache for stale revalidation (needs fresh data), HMR (needs
98
111
  // fresh modules), and intercept contexts (source-dependent responses).
99
- //
100
- const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
101
- const cacheKey = buildPrefetchKey(previousUrl, fetchUrl);
102
- const cachedResponse = canUsePrefetch ? consumePrefetch(cacheKey) : null;
103
- const inflightResponsePromise = canUsePrefetch
104
- ? consumeInflightPrefetch(cacheKey)
105
- : null;
112
+ // Suspend prefetch consumption while an action is in flight: a queued
113
+ // prefetch holds pre-mutation data and must not be served until the
114
+ // action's response decides whether anything changed.
115
+ const canUsePrefetch =
116
+ !staleRevalidation &&
117
+ !hmr &&
118
+ !interceptSourceUrl &&
119
+ !isActionFenceActive();
120
+ const rangoState = getRangoState();
121
+ const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
122
+ const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
123
+
124
+ let cachedEntry: DecodedPrefetch | null = null;
125
+ let hitKey: string | null = null;
126
+ if (canUsePrefetch) {
127
+ cachedEntry = consumePrefetch(cacheKey);
128
+ if (cachedEntry) {
129
+ hitKey = cacheKey;
130
+ } else {
131
+ cachedEntry = consumePrefetch(wildcardKey);
132
+ if (cachedEntry) hitKey = wildcardKey;
133
+ }
134
+ }
135
+
136
+ let inflightEntryPromise: Promise<DecodedPrefetch | null> | null = null;
137
+ if (canUsePrefetch && !cachedEntry) {
138
+ inflightEntryPromise = consumeInflightPrefetch(cacheKey);
139
+ if (inflightEntryPromise) {
140
+ hitKey = cacheKey;
141
+ } else {
142
+ inflightEntryPromise = consumeInflightPrefetch(wildcardKey);
143
+ if (inflightEntryPromise) hitKey = wildcardKey;
144
+ }
145
+ }
106
146
  // Track when the stream completes
107
147
  let resolveStreamComplete: () => void;
108
148
  const streamComplete = new Promise<void>((resolve) => {
109
149
  resolveStreamComplete = resolve;
110
150
  });
111
151
 
112
- // Dev-only: create debug channel for React Performance Tracks
113
- const debugId = (import.meta as any).hot
114
- ? crypto.randomUUID()
115
- : undefined;
116
- const debugChannel = debugId
117
- ? createClientDebugChannel(debugId)
118
- : undefined;
119
- if (debugId) {
120
- console.log(
121
- "[perf-tracks] client: debugId =",
122
- debugId,
123
- "channel =",
124
- debugChannel ? "created" : "null (no HMR)",
125
- );
126
- }
152
+ /**
153
+ * Validate RSC control headers on any response (fresh, cached, or
154
+ * in-flight). Handles version-mismatch reloads and server redirects.
155
+ * Returns the response unchanged when no control header is present.
156
+ */
157
+ const validateRscHeaders = (
158
+ response: Response,
159
+ source: string,
160
+ ): Response | Promise<Response> => {
161
+ // Version mismatch — server wants a full page reload
162
+ const reloadResult = handleReloadHeader(response, {
163
+ onBlocked: resolveStreamComplete,
164
+ onReload: (url) => {
165
+ if (tx) {
166
+ browserDebugLog(tx, `version mismatch, reloading (${source})`, {
167
+ reloadUrl: url,
168
+ });
169
+ }
170
+ },
171
+ });
172
+ if (reloadResult) return reloadResult;
173
+
174
+ // Server-side redirect without state: the server returned 204 with
175
+ // X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
176
+ // to a URL rendering full HTML). Throw ServerRedirect so the
177
+ // navigation bridge catches it and re-navigates with _skipCache.
178
+ const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
179
+ if (redirect === "blocked") {
180
+ resolveStreamComplete();
181
+ return emptyResponse();
182
+ }
183
+ if (redirect) {
184
+ if (tx) {
185
+ browserDebugLog(tx, `server redirect (${source})`, {
186
+ redirectUrl: redirect.url,
187
+ });
188
+ }
189
+ resolveStreamComplete();
190
+ throw new ServerRedirect(redirect.url, undefined);
191
+ }
192
+
193
+ // Integrity check (pre-decode): refuse a foreign app's content response
194
+ // before createFromFetch imports its chunks. Ordered AFTER the reload
195
+ // and redirect handlers — control responses are never stamped with
196
+ // X-RSC-Router-Id, so they are steered first and never reach here.
197
+ if (isForeignRouterId(response, routerId)) {
198
+ if (tx) {
199
+ browserDebugLog(tx, `router id mismatch, reloading (${source})`);
200
+ }
201
+ resolveStreamComplete();
202
+ window.location.href = targetUrl;
203
+ return new Promise<Response>(() => {});
204
+ }
205
+
206
+ return response;
207
+ };
127
208
 
128
209
  /** Start a fresh navigation fetch (no cache / inflight hit). */
129
210
  const doFreshFetch = (): Promise<Response> => {
@@ -134,6 +215,11 @@ export function createNavigationClient(
134
215
  }
135
216
 
136
217
  return fetch(fetchUrl, {
218
+ // During an action's flight the state is not rotated, so the old
219
+ // X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass
220
+ // it so a genuine mid-action navigation fetches fresh instead of being
221
+ // served the stale prefetched bytes.
222
+ ...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
137
223
  headers: {
138
224
  "X-RSC-Router-Client-Path": previousUrl,
139
225
  "X-Rango-State": getRangoState(),
@@ -142,47 +228,14 @@ export function createNavigationClient(
142
228
  "X-RSC-Router-Intercept-Source": interceptSourceUrl,
143
229
  }),
144
230
  ...(hmr && { "X-RSC-HMR": "1" }),
145
- ...(debugId && { [DEBUG_ID_HEADER]: debugId }),
146
231
  },
147
232
  signal,
148
233
  }).then((response) => {
149
- // Check for version mismatch - server wants us to reload
150
- const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
151
- if (reload === "blocked") {
152
- resolveStreamComplete();
153
- return emptyResponse();
154
- }
155
- if (reload) {
156
- if (tx) {
157
- browserDebugLog(tx, "version mismatch, reloading", {
158
- reloadUrl: reload.url,
159
- });
160
- }
161
- window.location.href = reload.url;
162
- return new Promise<Response>(() => {});
163
- }
164
-
165
- // Server-side redirect without state: the server returned 204 with
166
- // X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
167
- // to a URL rendering full HTML). Throw ServerRedirect so the
168
- // navigation bridge catches it and re-navigates with _skipCache.
169
- const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
170
- if (redirect === "blocked") {
171
- resolveStreamComplete();
172
- return emptyResponse();
173
- }
174
- if (redirect) {
175
- if (tx) {
176
- browserDebugLog(tx, "server redirect", {
177
- redirectUrl: redirect.url,
178
- });
179
- }
180
- resolveStreamComplete();
181
- throw new ServerRedirect(redirect.url, undefined);
182
- }
234
+ const validated = validateRscHeaders(response, "fetch");
235
+ if (validated instanceof Promise) return validated;
183
236
 
184
237
  return teeWithCompletion(
185
- response,
238
+ validated,
186
239
  () => {
187
240
  if (tx) browserDebugLog(tx, "stream complete");
188
241
  resolveStreamComplete();
@@ -192,59 +245,69 @@ export function createNavigationClient(
192
245
  });
193
246
  };
194
247
 
195
- let responsePromise: Promise<Response>;
248
+ // A warm prefetch hit returns its eagerly-decoded payload directly: the
249
+ // route's chunks were imported during the prefetch, so this click runs
250
+ // no decode and no network. Only the fresh path runs createFromFetch and
251
+ // resolves the local streamComplete (via doFreshFetch's teeWithCompletion
252
+ // and the control-header short-circuits in validateRscHeaders).
253
+ const freshResult = (): {
254
+ payload: Promise<RscPayload>;
255
+ streamComplete: Promise<void>;
256
+ } => ({
257
+ payload: deps.createFromFetch<RscPayload>(doFreshFetch()),
258
+ streamComplete,
259
+ });
260
+
261
+ let payloadPromise: Promise<RscPayload>;
262
+ let streamCompletePromise: Promise<void>;
196
263
 
197
- if (cachedResponse) {
264
+ if (cachedEntry) {
198
265
  if (tx) {
199
- browserDebugLog(tx, "prefetch cache hit", { key: cacheKey });
266
+ browserDebugLog(tx, "prefetch cache hit (warm)", {
267
+ key: hitKey,
268
+ wildcard: hitKey === wildcardKey,
269
+ });
200
270
  }
201
- // Cached response body is already fully buffered (arrayBuffer),
202
- // so stream completion is immediate.
203
- responsePromise = Promise.resolve(cachedResponse).then((response) => {
204
- return teeWithCompletion(
205
- response,
206
- () => {
207
- if (tx) browserDebugLog(tx, "stream complete (from cache)");
208
- resolveStreamComplete();
209
- },
210
- signal,
211
- );
212
- });
213
- } else if (inflightResponsePromise) {
271
+ payloadPromise = cachedEntry.payload;
272
+ streamCompletePromise = cachedEntry.streamComplete;
273
+ } else if (inflightEntryPromise) {
214
274
  if (tx) {
215
- browserDebugLog(tx, "reusing inflight prefetch", { key: cacheKey });
275
+ browserDebugLog(tx, "reusing inflight prefetch", {
276
+ key: hitKey,
277
+ wildcard: hitKey === wildcardKey,
278
+ });
216
279
  }
217
- responsePromise = inflightResponsePromise.then(async (response) => {
218
- if (!response) {
219
- if (tx) {
220
- browserDebugLog(tx, "inflight prefetch unavailable, refetching");
221
- }
222
- return doFreshFetch();
280
+ const adoptedViaWildcard = hitKey === wildcardKey;
281
+ const entry = await inflightEntryPromise;
282
+ if (!entry) {
283
+ if (tx) {
284
+ browserDebugLog(tx, "inflight prefetch unavailable, refetching");
223
285
  }
224
-
225
- return teeWithCompletion(
226
- response,
227
- () => {
228
- if (tx) {
229
- browserDebugLog(tx, "stream complete (from inflight prefetch)");
230
- }
231
- resolveStreamComplete();
232
- },
233
- signal,
234
- );
235
- });
286
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
287
+ freshResult());
288
+ } else if (adoptedViaWildcard && entry.scope === "source") {
289
+ // A wildcard-adopted inflight that turned out source-scoped was
290
+ // built for a different source page. Discard and refetch.
291
+ if (tx) {
292
+ browserDebugLog(
293
+ tx,
294
+ "wildcard inflight turned out source-scoped, refetching",
295
+ );
296
+ }
297
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
298
+ freshResult());
299
+ } else {
300
+ payloadPromise = entry.payload;
301
+ streamCompletePromise = entry.streamComplete;
302
+ }
236
303
  } else {
237
- responsePromise = doFreshFetch();
304
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
305
+ freshResult());
238
306
  }
239
307
 
240
308
  try {
241
- // Deserialize RSC payload
242
- const payload = await deps.createFromFetch<RscPayload>(
243
- responsePromise,
244
- {
245
- ...(debugChannel && { debugChannel, findSourceMapURL }),
246
- },
247
- );
309
+ const payload = await payloadPromise;
310
+
248
311
  if (tx) {
249
312
  browserDebugLog(tx, "response received", {
250
313
  isPartial: payload.metadata?.isPartial,
@@ -252,7 +315,7 @@ export function createNavigationClient(
252
315
  diffCount: payload.metadata?.diff?.length ?? 0,
253
316
  });
254
317
  }
255
- return { payload, streamComplete };
318
+ return { payload, streamComplete: streamCompletePromise };
256
319
  } catch (error) {
257
320
  // Convert network-level errors to NetworkError for proper handling
258
321
  if (isNetworkError(error)) {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A module-level handle to the active navigation store.
3
+ *
4
+ * The boot path (`rsc-router.tsx`) calls `createNavigationStore()` directly;
5
+ * there is no global store singleton. This handle is the live reference for
6
+ * code that needs the store but does not
7
+ * receive it by argument: the jar-divergence observer (below) and the client
8
+ * seat of `invalidateClientCache()` (added later).
9
+ *
10
+ * Dependency-light on purpose: it imports only `setRangoStateObserver` and the
11
+ * store type, so pulling it into the default root entry does not drag the
12
+ * navigation store into bundles that previously lacked it.
13
+ */
14
+
15
+ import { setRangoStateObserver } from "./rango-state.js";
16
+ import type { NavigationStore } from "./types.js";
17
+
18
+ let registeredStore: NavigationStore | null = null;
19
+
20
+ /**
21
+ * Register the active navigation store at boot, and wire the jar-divergence
22
+ * observer: when a per-request cookie read detects an EXTERNAL rotation (a
23
+ * sibling tab, a server `Set-Cookie`, or a cookie clear), mark this tab's
24
+ * history cache stale. The history cache is not state-keyed, so the value
25
+ * rotation alone does not reach it. No broadcast, no prefetch clear, no
26
+ * re-rotation — the value already changed externally.
27
+ */
28
+ export function registerNavigationStore(store: NavigationStore): void {
29
+ registeredStore = store;
30
+ setRangoStateObserver(() => {
31
+ registeredStore?.markHistoryCacheStale();
32
+ });
33
+ }
34
+
35
+ /** The active navigation store, or null before boot has registered it. */
36
+ export function getRegisteredStore(): NavigationStore | null {
37
+ return registeredStore;
38
+ }
@@ -28,9 +28,15 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
28
28
  // Maximum number of history entries to cache (URLs visited)
29
29
  const HISTORY_CACHE_SIZE = 20;
30
30
 
31
- // Cache entry: [url-key, segments, stale, handleData?]
31
+ // Cache entry: [url-key, segments, stale, handleData?, routerId?]
32
32
  // stale=true means the data may be outdated and should be revalidated on access
33
- type HistoryCacheEntry = [string, ResolvedSegment[], boolean, HandleData?];
33
+ type HistoryCacheEntry = [
34
+ string,
35
+ ResolvedSegment[],
36
+ boolean,
37
+ HandleData?,
38
+ string?,
39
+ ];
34
40
 
35
41
  /**
36
42
  * Shallow clone handleData to avoid reference sharing between cache entries.
@@ -124,14 +130,14 @@ export interface NavigationStoreConfig {
124
130
 
125
131
  /**
126
132
  * Enable cross-tab cache invalidation via BroadcastChannel (default: true)
127
- * When cache is cleared (via server actions or useClientCache().clear()),
133
+ * When cache is cleared (via server actions or invalidateClientCache()),
128
134
  * other tabs will also clear their cache
129
135
  */
130
136
  crossTabSync?: boolean;
131
137
 
132
138
  /**
133
139
  * Auto-refresh when another tab mutates data on the same path (default: true)
134
- * Triggered when cache is cleared via server actions or useClientCache().clear()
140
+ * Triggered when cache is cleared via server actions or invalidateClientCache()
135
141
  * Requires crossTabSync to be enabled
136
142
  */
137
143
  crossTabAutoRefresh?: boolean;
@@ -258,6 +264,11 @@ export function createNavigationStore(
258
264
  // Used to maintain intercept context during action revalidation
259
265
  let interceptSourceUrl: string | null = null;
260
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
+
261
272
  // Action state tracking (for useAction hook)
262
273
  // Maps action function ID to its tracked state
263
274
  const actionStates = new Map<string, TrackedActionState>();
@@ -269,18 +280,17 @@ export function createNavigationStore(
269
280
  /**
270
281
  * Create a debounced function that batches rapid calls
271
282
  */
283
+ // A non-keyed notifier is the keyed one restricted to a single constant key;
284
+ // its own keyed instance means the "" key never collides with action keys.
272
285
  function createDebouncedNotifier<T extends (...args: any[]) => void>(
273
286
  fn: T,
274
287
  ms: number = 20,
275
288
  ): T {
276
- let timeout: ReturnType<typeof setTimeout> | null = null;
277
- return ((...args: Parameters<T>) => {
278
- if (timeout !== null) clearTimeout(timeout);
279
- timeout = setTimeout(() => {
280
- timeout = null;
281
- fn(...args);
282
- }, ms);
283
- }) as T;
289
+ const keyed = createKeyedDebouncedNotifier(
290
+ (_key: string, ...args: any[]) => fn(...args),
291
+ ms,
292
+ );
293
+ return ((...args: Parameters<T>) => keyed("", ...args)) as T;
284
294
  }
285
295
 
286
296
  /**
@@ -325,12 +335,24 @@ export function createNavigationStore(
325
335
  }
326
336
 
327
337
  /**
328
- * Mark all cache entries as stale (internal - does not broadcast)
338
+ * Mark every history entry stale WITHOUT touching the prefetch caches or the
339
+ * rango state. Used by the jar-divergence observer: an external rotation has
340
+ * already changed the state value (so prefetch/HTTP entries strand under the
341
+ * retired key), and this tab must NOT re-rotate — only the history cache,
342
+ * which is not state-keyed, needs marking.
329
343
  */
330
- function markCacheAsStaleInternal(): void {
344
+ function markHistoryStale(): void {
331
345
  for (let i = 0; i < historyCache.length; i++) {
332
346
  historyCache[i][2] = true;
333
347
  }
348
+ }
349
+
350
+ /**
351
+ * Mark all cache entries as stale (internal - does not broadcast). Also
352
+ * clears the prefetch caches, which rotates the rango state.
353
+ */
354
+ function markCacheAsStaleInternal(): void {
355
+ markHistoryStale();
334
356
  clearPrefetchCache();
335
357
  }
336
358
 
@@ -571,10 +593,17 @@ export function createNavigationStore(
571
593
  segments,
572
594
  false,
573
595
  clonedHandleData,
596
+ currentRouterId,
574
597
  ];
575
598
  } else {
576
599
  // Add new entry at the end (not stale)
577
- historyCache.push([historyKey, segments, false, clonedHandleData]);
600
+ historyCache.push([
601
+ historyKey,
602
+ segments,
603
+ false,
604
+ clonedHandleData,
605
+ currentRouterId,
606
+ ]);
578
607
  // Remove oldest entries if over limit
579
608
  while (historyCache.length > cacheSize) {
580
609
  historyCache.shift();
@@ -586,14 +615,22 @@ export function createNavigationStore(
586
615
  * Get cached segments for a history entry
587
616
  * Returns { segments, stale, handleData } or undefined if not cached
588
617
  */
589
- getCachedSegments(
590
- historyKey: string,
591
- ):
592
- | { segments: ResolvedSegment[]; stale: boolean; handleData?: HandleData }
618
+ getCachedSegments(historyKey: string):
619
+ | {
620
+ segments: ResolvedSegment[];
621
+ stale: boolean;
622
+ handleData?: HandleData;
623
+ routerId?: string;
624
+ }
593
625
  | undefined {
594
626
  const entry = historyCache.find(([key]) => key === historyKey);
595
627
  if (!entry) return undefined;
596
- return { segments: entry[1], stale: entry[2], handleData: entry[3] };
628
+ return {
629
+ segments: entry[1],
630
+ stale: entry[2],
631
+ handleData: entry[3],
632
+ routerId: entry[4],
633
+ };
597
634
  },
598
635
 
599
636
  /**
@@ -621,6 +658,7 @@ export function createNavigationStore(
621
658
  entry[1],
622
659
  entry[2],
623
660
  clonedHandleData,
661
+ entry[4], // preserve routerId
624
662
  ];
625
663
  }
626
664
  },
@@ -633,6 +671,16 @@ export function createNavigationStore(
633
671
  markCacheAsStaleInternal();
634
672
  },
635
673
 
674
+ /**
675
+ * Mark every history entry stale WITHOUT clearing the prefetch caches or
676
+ * rotating the rango state. The jar-divergence observer calls this after an
677
+ * external rotation has already changed the state value, so re-rotating
678
+ * here would ping-pong with the tab that rotated.
679
+ */
680
+ markHistoryCacheStale(): void {
681
+ markHistoryStale();
682
+ },
683
+
636
684
  /**
637
685
  * Clear the history cache and broadcast to other tabs
638
686
  * Use this for hard invalidation when data is definitely stale
@@ -649,14 +697,6 @@ export function createNavigationStore(
649
697
  markStaleAndBroadcast();
650
698
  },
651
699
 
652
- /**
653
- * Broadcast cache invalidation to other tabs without clearing local cache
654
- * Used after consolidation fetch where local cache has fresh data
655
- */
656
- broadcastCacheInvalidation(): void {
657
- broadcastInvalidation();
658
- },
659
-
660
700
  /**
661
701
  * Set the callback to invoke when cross-tab refresh is triggered
662
702
  * Called by navigation bridge during initialization
@@ -687,6 +727,14 @@ export function createNavigationStore(
687
727
  interceptSourceUrl = url;
688
728
  },
689
729
 
730
+ getRouterId(): string | undefined {
731
+ return currentRouterId;
732
+ },
733
+
734
+ setRouterId(id: string): void {
735
+ currentRouterId = id;
736
+ },
737
+
690
738
  // ========================================================================
691
739
  // UI Update Notifications
692
740
  // ========================================================================
@@ -765,42 +813,3 @@ export function createNavigationStore(
765
813
  },
766
814
  };
767
815
  }
768
-
769
- // Singleton store instance
770
- let storeInstance: NavigationStore | null = null;
771
-
772
- /**
773
- * Initialize the global navigation store
774
- *
775
- * Should be called once during app initialization.
776
- * Subsequent calls return the existing instance.
777
- */
778
- export function initNavigationStore(
779
- config?: NavigationStoreConfig,
780
- ): NavigationStore {
781
- if (!storeInstance) {
782
- storeInstance = createNavigationStore(config);
783
- }
784
- return storeInstance;
785
- }
786
-
787
- /**
788
- * Get the global navigation store
789
- *
790
- * Throws if store hasn't been initialized.
791
- */
792
- export function getNavigationStore(): NavigationStore {
793
- if (!storeInstance) {
794
- throw new Error(
795
- "Navigation store not initialized. Call initNavigationStore first.",
796
- );
797
- }
798
- return storeInstance;
799
- }
800
-
801
- /**
802
- * Reset the store instance (for testing)
803
- */
804
- export function resetNavigationStore(): void {
805
- storeInstance = null;
806
- }