@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7

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 (402) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +440 -133
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3373 -1176
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +68 -14
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +64 -2
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +224 -32
  13. package/skills/caching/SKILL.md +279 -17
  14. package/skills/composability/SKILL.md +27 -3
  15. package/skills/css/SKILL.md +76 -0
  16. package/skills/debug-manifest/SKILL.md +4 -2
  17. package/skills/document-cache/SKILL.md +78 -55
  18. package/skills/handler-use/SKILL.md +364 -0
  19. package/skills/hooks/SKILL.md +250 -30
  20. package/skills/host-router/SKILL.md +83 -23
  21. package/skills/i18n/SKILL.md +276 -0
  22. package/skills/intercept/SKILL.md +87 -18
  23. package/skills/layout/SKILL.md +35 -9
  24. package/skills/links/SKILL.md +249 -17
  25. package/skills/loader/SKILL.md +235 -9
  26. package/skills/middleware/SKILL.md +52 -13
  27. package/skills/migrate-nextjs/SKILL.md +584 -0
  28. package/skills/migrate-react-router/SKILL.md +771 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +77 -7
  32. package/skills/prerender/SKILL.md +172 -125
  33. package/skills/rango/SKILL.md +251 -22
  34. package/skills/react-compiler/SKILL.md +168 -0
  35. package/skills/response-routes/SKILL.md +123 -48
  36. package/skills/route/SKILL.md +70 -5
  37. package/skills/router-setup/SKILL.md +65 -8
  38. package/skills/scripts/SKILL.md +179 -0
  39. package/skills/server-actions/SKILL.md +775 -0
  40. package/skills/streams-and-websockets/SKILL.md +283 -0
  41. package/skills/tailwind/SKILL.md +27 -3
  42. package/skills/testing/SKILL.md +130 -0
  43. package/skills/testing/bindings.md +103 -0
  44. package/skills/testing/cache-prerender.md +127 -0
  45. package/skills/testing/client-components.md +124 -0
  46. package/skills/testing/e2e-parity.md +125 -0
  47. package/skills/testing/flight.md +91 -0
  48. package/skills/testing/handles.md +129 -0
  49. package/skills/testing/loader.md +128 -0
  50. package/skills/testing/middleware.md +99 -0
  51. package/skills/testing/render-handler.md +122 -0
  52. package/skills/testing/response-routes.md +95 -0
  53. package/skills/testing/reverse-and-types.md +84 -0
  54. package/skills/testing/server-actions.md +107 -0
  55. package/skills/testing/server-tree.md +128 -0
  56. package/skills/testing/setup.md +123 -0
  57. package/skills/typesafety/SKILL.md +322 -29
  58. package/skills/use-cache/SKILL.md +57 -14
  59. package/skills/view-transitions/SKILL.md +337 -0
  60. package/src/__augment-tests__/augment.ts +81 -0
  61. package/src/__augment-tests__/augmented.check.ts +116 -0
  62. package/src/__internal.ts +1 -66
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +192 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/navigation-bridge.ts +131 -30
  74. package/src/browser/navigation-client.ts +186 -100
  75. package/src/browser/navigation-store-handle.ts +38 -0
  76. package/src/browser/navigation-store.ts +157 -74
  77. package/src/browser/navigation-transaction.ts +9 -59
  78. package/src/browser/network-error-handler.ts +34 -7
  79. package/src/browser/partial-update.ts +165 -112
  80. package/src/browser/prefetch/cache.ts +205 -62
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +42 -8
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +102 -15
  85. package/src/browser/react/NavigationProvider.tsx +295 -119
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  89. package/src/browser/react/filter-segment-order.ts +66 -7
  90. package/src/browser/react/index.ts +0 -48
  91. package/src/browser/react/location-state-shared.ts +178 -8
  92. package/src/browser/react/location-state.ts +39 -14
  93. package/src/browser/react/use-action.ts +6 -15
  94. package/src/browser/react/use-handle.ts +23 -69
  95. package/src/browser/react/use-href.tsx +8 -1
  96. package/src/browser/react/use-link-status.ts +33 -8
  97. package/src/browser/react/use-navigation.ts +32 -7
  98. package/src/browser/react/use-params.ts +20 -10
  99. package/src/browser/react/use-reverse.ts +106 -0
  100. package/src/browser/react/use-router.ts +46 -11
  101. package/src/browser/react/use-search-params.ts +0 -5
  102. package/src/browser/react/use-segments.ts +11 -21
  103. package/src/browser/response-adapter.ts +99 -8
  104. package/src/browser/rsc-router.tsx +114 -24
  105. package/src/browser/scroll-restoration.ts +37 -22
  106. package/src/browser/segment-reconciler.ts +36 -14
  107. package/src/browser/segment-structure-assert.ts +2 -2
  108. package/src/browser/server-action-bridge.ts +222 -72
  109. package/src/browser/types.ts +102 -12
  110. package/src/browser/validate-redirect-origin.ts +43 -16
  111. package/src/build/collect-fallback-refs.ts +107 -0
  112. package/src/build/generate-manifest.ts +65 -40
  113. package/src/build/generate-route-types.ts +5 -1
  114. package/src/build/index.ts +8 -2
  115. package/src/build/prefix-tree-utils.ts +123 -0
  116. package/src/build/route-trie.ts +165 -36
  117. package/src/build/route-types/ast-route-extraction.ts +15 -8
  118. package/src/build/route-types/codegen.ts +16 -5
  119. package/src/build/route-types/include-resolution.ts +125 -24
  120. package/src/build/route-types/param-extraction.ts +6 -3
  121. package/src/build/route-types/per-module-writer.ts +22 -6
  122. package/src/build/route-types/router-processing.ts +260 -94
  123. package/src/build/route-types/scan-filter.ts +9 -2
  124. package/src/build/route-types/source-scan.ts +216 -0
  125. package/src/build/runtime-discovery.ts +9 -20
  126. package/src/cache/cache-error.ts +104 -0
  127. package/src/cache/cache-key-utils.ts +29 -13
  128. package/src/cache/cache-policy.ts +108 -34
  129. package/src/cache/cache-runtime.ts +224 -41
  130. package/src/cache/cache-scope.ts +188 -82
  131. package/src/cache/cache-tag.ts +103 -0
  132. package/src/cache/cf/cf-base64.ts +33 -0
  133. package/src/cache/cf/cf-cache-constants.ts +127 -0
  134. package/src/cache/cf/cf-cache-store.ts +1989 -378
  135. package/src/cache/cf/cf-cache-types.ts +349 -0
  136. package/src/cache/cf/cf-kv-utils.ts +46 -0
  137. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  138. package/src/cache/cf/index.ts +6 -16
  139. package/src/cache/document-cache.ts +89 -21
  140. package/src/cache/handle-snapshot.ts +70 -0
  141. package/src/cache/index.ts +10 -20
  142. package/src/cache/memory-segment-store.ts +136 -37
  143. package/src/cache/profile-registry.ts +46 -31
  144. package/src/cache/read-through-swr.ts +56 -12
  145. package/src/cache/segment-codec.ts +9 -17
  146. package/src/cache/tag-invalidation.ts +230 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +18 -6
  155. package/src/decode-loader-results.ts +52 -0
  156. package/src/defer.ts +196 -0
  157. package/src/deps/ssr.ts +0 -1
  158. package/src/encode-kv.ts +49 -0
  159. package/src/errors.ts +30 -4
  160. package/src/escape-script.ts +52 -0
  161. package/src/handle.ts +70 -22
  162. package/src/handles/MetaTags.tsx +62 -19
  163. package/src/handles/Scripts.tsx +183 -0
  164. package/src/handles/breadcrumbs.ts +37 -8
  165. package/src/handles/is-thenable.ts +19 -0
  166. package/src/handles/meta.ts +51 -40
  167. package/src/handles/script.ts +244 -0
  168. package/src/host/cookie-handler.ts +9 -60
  169. package/src/host/errors.ts +0 -24
  170. package/src/host/index.ts +8 -2
  171. package/src/host/pattern-matcher.ts +23 -52
  172. package/src/host/router.ts +107 -99
  173. package/src/host/testing.ts +40 -27
  174. package/src/host/types.ts +37 -4
  175. package/src/host/utils.ts +1 -1
  176. package/src/href-client.ts +137 -22
  177. package/src/index.rsc.ts +99 -13
  178. package/src/index.ts +139 -19
  179. package/src/internal-debug.ts +11 -10
  180. package/src/loader-store.ts +500 -0
  181. package/src/loader.rsc.ts +20 -13
  182. package/src/loader.ts +12 -11
  183. package/src/missing-id-error.ts +68 -0
  184. package/src/outlet-context.ts +1 -1
  185. package/src/outlet-provider.tsx +1 -5
  186. package/src/prerender/param-hash.ts +16 -16
  187. package/src/prerender/store.ts +37 -41
  188. package/src/prerender.ts +198 -82
  189. package/src/redirect-origin.ts +100 -0
  190. package/src/regex-escape.ts +8 -0
  191. package/src/render-error-thrower.tsx +20 -0
  192. package/src/response-utils.ts +62 -0
  193. package/src/reverse.ts +65 -15
  194. package/src/root-error-boundary.tsx +1 -19
  195. package/src/route-content-wrapper.tsx +19 -77
  196. package/src/route-definition/dsl-helpers.ts +461 -304
  197. package/src/route-definition/helper-factories.ts +28 -140
  198. package/src/route-definition/helpers-types.ts +143 -69
  199. package/src/route-definition/index.ts +4 -2
  200. package/src/route-definition/redirect.ts +51 -10
  201. package/src/route-definition/resolve-handler-use.ts +160 -0
  202. package/src/route-definition/use-item-types.ts +29 -0
  203. package/src/route-map-builder.ts +0 -16
  204. package/src/route-types.ts +37 -46
  205. package/src/router/basename.ts +14 -0
  206. package/src/router/content-negotiation.ts +164 -17
  207. package/src/router/error-handling.ts +45 -18
  208. package/src/router/find-match.ts +44 -23
  209. package/src/router/handler-context.ts +52 -31
  210. package/src/router/instrument.ts +350 -0
  211. package/src/router/intercept-resolution.ts +48 -24
  212. package/src/router/lazy-includes.ts +15 -52
  213. package/src/router/loader-resolution.ts +268 -56
  214. package/src/router/logging.ts +0 -6
  215. package/src/router/manifest.ts +40 -42
  216. package/src/router/match-api.ts +124 -204
  217. package/src/router/match-context.ts +0 -22
  218. package/src/router/match-handlers.ts +58 -58
  219. package/src/router/match-middleware/background-revalidation.ts +40 -24
  220. package/src/router/match-middleware/cache-lookup.ts +170 -276
  221. package/src/router/match-middleware/cache-store.ts +64 -52
  222. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  223. package/src/router/match-middleware/segment-resolution.ts +45 -14
  224. package/src/router/match-pipelines.ts +1 -42
  225. package/src/router/match-result.ts +87 -39
  226. package/src/router/metrics.ts +0 -34
  227. package/src/router/middleware-types.ts +7 -140
  228. package/src/router/middleware.ts +266 -169
  229. package/src/router/navigation-snapshot.ts +131 -0
  230. package/src/router/params-util.ts +23 -0
  231. package/src/router/pattern-matching.ts +132 -90
  232. package/src/router/prefetch-cache-ttl.ts +51 -0
  233. package/src/router/prerender-match.ts +195 -56
  234. package/src/router/preview-match.ts +32 -102
  235. package/src/router/request-classification.ts +276 -0
  236. package/src/router/revalidation.ts +123 -73
  237. package/src/router/route-snapshot.ts +244 -0
  238. package/src/router/router-context.ts +3 -28
  239. package/src/router/router-interfaces.ts +115 -35
  240. package/src/router/router-options.ts +172 -15
  241. package/src/router/router-registry.ts +2 -5
  242. package/src/router/segment-resolution/fresh.ts +162 -84
  243. package/src/router/segment-resolution/helpers.ts +86 -6
  244. package/src/router/segment-resolution/loader-cache.ts +76 -39
  245. package/src/router/segment-resolution/revalidation.ts +351 -321
  246. package/src/router/segment-resolution/static-store.ts +19 -5
  247. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  248. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  249. package/src/router/segment-resolution.ts +5 -1
  250. package/src/router/segment-wrappers.ts +6 -5
  251. package/src/router/state-cookie-name.ts +33 -0
  252. package/src/router/substitute-pattern-params.ts +56 -0
  253. package/src/router/telemetry-otel.ts +161 -199
  254. package/src/router/telemetry.ts +96 -19
  255. package/src/router/timeout.ts +0 -20
  256. package/src/router/tracing.ts +206 -0
  257. package/src/router/trie-matching.ts +163 -59
  258. package/src/router/types.ts +9 -63
  259. package/src/router/url-params.ts +44 -0
  260. package/src/router.ts +157 -54
  261. package/src/rsc/handler-context.ts +3 -2
  262. package/src/rsc/handler.ts +655 -529
  263. package/src/rsc/helpers.ts +168 -46
  264. package/src/rsc/index.ts +2 -5
  265. package/src/rsc/json-route-result.ts +38 -0
  266. package/src/rsc/loader-fetch.ts +122 -31
  267. package/src/rsc/manifest-init.ts +33 -42
  268. package/src/rsc/origin-guard.ts +39 -25
  269. package/src/rsc/progressive-enhancement.ts +131 -14
  270. package/src/rsc/redirect-guard.ts +99 -0
  271. package/src/rsc/response-cache-serve.ts +238 -0
  272. package/src/rsc/response-error.ts +79 -12
  273. package/src/rsc/response-route-handler.ts +99 -189
  274. package/src/rsc/rsc-rendering.ts +109 -74
  275. package/src/rsc/runtime-warnings.ts +23 -10
  276. package/src/rsc/server-action.ts +287 -115
  277. package/src/rsc/ssr-setup.ts +18 -2
  278. package/src/rsc/transition-gate.ts +89 -0
  279. package/src/rsc/types.ts +29 -9
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +236 -202
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +224 -52
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +38 -46
  291. package/src/server/request-context.ts +401 -173
  292. package/src/ssr/index.tsx +24 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +105 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +357 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/run-transition-when.ts +164 -0
  320. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  321. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  322. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  323. package/src/testing/vitest-stubs/version.ts +5 -0
  324. package/src/testing/vitest.ts +305 -0
  325. package/src/theme/ThemeProvider.tsx +20 -58
  326. package/src/theme/ThemeScript.tsx +7 -9
  327. package/src/theme/constants.ts +52 -13
  328. package/src/theme/index.ts +0 -7
  329. package/src/theme/theme-context.ts +1 -5
  330. package/src/theme/theme-script.ts +22 -21
  331. package/src/theme/use-theme.ts +0 -3
  332. package/src/types/boundaries.ts +0 -35
  333. package/src/types/cache-types.ts +17 -8
  334. package/src/types/error-types.ts +30 -90
  335. package/src/types/global-namespace.ts +54 -41
  336. package/src/types/handler-context.ts +125 -71
  337. package/src/types/index.ts +3 -10
  338. package/src/types/loader-types.ts +40 -11
  339. package/src/types/request-scope.ts +112 -0
  340. package/src/types/route-config.ts +6 -50
  341. package/src/types/route-entry.ts +12 -7
  342. package/src/types/segments.ts +136 -15
  343. package/src/urls/include-helper.ts +33 -70
  344. package/src/urls/index.ts +1 -11
  345. package/src/urls/path-helper-types.ts +68 -18
  346. package/src/urls/path-helper.ts +57 -111
  347. package/src/urls/pattern-types.ts +48 -19
  348. package/src/urls/response-types.ts +25 -22
  349. package/src/urls/type-extraction.ts +58 -139
  350. package/src/urls/urls-function.ts +1 -19
  351. package/src/use-loader.tsx +346 -89
  352. package/src/vite/debug.ts +185 -0
  353. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  354. package/src/vite/discovery/discover-routers.ts +130 -85
  355. package/src/vite/discovery/discovery-errors.ts +194 -0
  356. package/src/vite/discovery/gate-state.ts +171 -0
  357. package/src/vite/discovery/prerender-collection.ts +214 -132
  358. package/src/vite/discovery/route-types-writer.ts +40 -84
  359. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  360. package/src/vite/discovery/state.ts +57 -4
  361. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  362. package/src/vite/index.ts +6 -0
  363. package/src/vite/inject-client-debug.ts +36 -0
  364. package/src/vite/plugin-types.ts +178 -5
  365. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  366. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  367. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  369. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  370. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  371. package/src/vite/plugins/expose-action-id.ts +48 -95
  372. package/src/vite/plugins/expose-id-utils.ts +96 -51
  373. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  374. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  375. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  376. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  377. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  378. package/src/vite/plugins/performance-tracks.ts +64 -170
  379. package/src/vite/plugins/refresh-cmd.ts +89 -27
  380. package/src/vite/plugins/use-cache-transform.ts +73 -83
  381. package/src/vite/plugins/version-injector.ts +40 -29
  382. package/src/vite/plugins/version-plugin.ts +37 -40
  383. package/src/vite/plugins/virtual-entries.ts +39 -25
  384. package/src/vite/rango.ts +118 -114
  385. package/src/vite/router-discovery.ts +941 -142
  386. package/src/vite/utils/ast-handler-extract.ts +26 -35
  387. package/src/vite/utils/banner.ts +1 -1
  388. package/src/vite/utils/bundle-analysis.ts +10 -15
  389. package/src/vite/utils/client-chunks.ts +184 -0
  390. package/src/vite/utils/directive-prologue.ts +40 -0
  391. package/src/vite/utils/forward-user-plugins.ts +171 -0
  392. package/src/vite/utils/manifest-utils.ts +4 -59
  393. package/src/vite/utils/package-resolution.ts +20 -52
  394. package/src/vite/utils/prerender-utils.ts +81 -34
  395. package/src/vite/utils/shared-utils.ts +92 -42
  396. package/src/browser/action-response-classifier.ts +0 -99
  397. package/src/browser/debug-channel.ts +0 -93
  398. package/src/browser/react/use-client-cache.ts +0 -58
  399. package/src/browser/shallow.ts +0 -40
  400. package/src/handles/index.ts +0 -7
  401. package/src/network-error-thrower.tsx +0 -23
  402. package/src/router/middleware-cookies.ts +0 -55
@@ -12,16 +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";
15
+ import { isActionFenceActive } from "./action-fence.js";
16
16
  import {
17
17
  extractRscHeaderUrl,
18
18
  emptyResponse,
19
+ handleReloadHeader,
19
20
  teeWithCompletion,
21
+ isForeignRouterId,
20
22
  } from "./response-adapter.js";
21
23
  import {
22
24
  buildPrefetchKey,
25
+ buildSourceKey,
23
26
  consumeInflightPrefetch,
24
27
  consumePrefetch,
28
+ type DecodedPrefetch,
25
29
  } from "./prefetch/cache.js";
26
30
 
27
31
  /**
@@ -31,8 +35,10 @@ import {
31
35
  * deserializing the response using the RSC runtime.
32
36
  *
33
37
  * Checks the in-memory prefetch cache before making a network request.
34
- * The cache key is source-dependent (includes the previous URL) so
35
- * 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.
36
42
  *
37
43
  * @param deps - RSC browser dependencies (createFromFetch)
38
44
  * @returns NavigationClient instance
@@ -62,6 +68,7 @@ export function createNavigationClient(
62
68
  staleRevalidation,
63
69
  interceptSourceUrl,
64
70
  version,
71
+ routerId,
65
72
  hmr,
66
73
  } = options;
67
74
 
@@ -89,32 +96,115 @@ export function createNavigationClient(
89
96
  if (version) {
90
97
  fetchUrl.searchParams.set("_rsc_v", version);
91
98
  }
99
+ if (routerId) {
100
+ fetchUrl.searchParams.set("_rsc_rid", routerId);
101
+ }
92
102
 
93
- // Check completed in-memory prefetch cache before making a network request.
94
- // The cache key includes the source URL (previousUrl) because the
95
- // 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.
96
110
  // Skip cache for stale revalidation (needs fresh data), HMR (needs
97
111
  // fresh modules), and intercept contexts (source-dependent responses).
98
- //
99
- const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
100
- const cacheKey = buildPrefetchKey(previousUrl, fetchUrl);
101
- const cachedResponse = canUsePrefetch ? consumePrefetch(cacheKey) : null;
102
- const inflightResponsePromise = canUsePrefetch
103
- ? consumeInflightPrefetch(cacheKey)
104
- : 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
+ }
105
146
  // Track when the stream completes
106
147
  let resolveStreamComplete: () => void;
107
148
  const streamComplete = new Promise<void>((resolve) => {
108
149
  resolveStreamComplete = resolve;
109
150
  });
110
151
 
111
- // Dev-only: create debug channel for React Performance Tracks
112
- const debugId = (import.meta as any).hot
113
- ? crypto.randomUUID()
114
- : undefined;
115
- const debugChannel = debugId
116
- ? createClientDebugChannel(debugId)
117
- : undefined;
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
+ };
118
208
 
119
209
  /** Start a fresh navigation fetch (no cache / inflight hit). */
120
210
  const doFreshFetch = (): Promise<Response> => {
@@ -125,6 +215,11 @@ export function createNavigationClient(
125
215
  }
126
216
 
127
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 }),
128
223
  headers: {
129
224
  "X-RSC-Router-Client-Path": previousUrl,
130
225
  "X-Rango-State": getRangoState(),
@@ -133,47 +228,14 @@ export function createNavigationClient(
133
228
  "X-RSC-Router-Intercept-Source": interceptSourceUrl,
134
229
  }),
135
230
  ...(hmr && { "X-RSC-HMR": "1" }),
136
- ...(debugId && { [DEBUG_ID_HEADER]: debugId }),
137
231
  },
138
232
  signal,
139
233
  }).then((response) => {
140
- // Check for version mismatch - server wants us to reload
141
- const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
142
- if (reload === "blocked") {
143
- resolveStreamComplete();
144
- return emptyResponse();
145
- }
146
- if (reload) {
147
- if (tx) {
148
- browserDebugLog(tx, "version mismatch, reloading", {
149
- reloadUrl: reload.url,
150
- });
151
- }
152
- window.location.href = reload.url;
153
- return new Promise<Response>(() => {});
154
- }
155
-
156
- // Server-side redirect without state: the server returned 204 with
157
- // X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
158
- // to a URL rendering full HTML). Throw ServerRedirect so the
159
- // navigation bridge catches it and re-navigates with _skipCache.
160
- const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
161
- if (redirect === "blocked") {
162
- resolveStreamComplete();
163
- return emptyResponse();
164
- }
165
- if (redirect) {
166
- if (tx) {
167
- browserDebugLog(tx, "server redirect", {
168
- redirectUrl: redirect.url,
169
- });
170
- }
171
- resolveStreamComplete();
172
- throw new ServerRedirect(redirect.url, undefined);
173
- }
234
+ const validated = validateRscHeaders(response, "fetch");
235
+ if (validated instanceof Promise) return validated;
174
236
 
175
237
  return teeWithCompletion(
176
- response,
238
+ validated,
177
239
  () => {
178
240
  if (tx) browserDebugLog(tx, "stream complete");
179
241
  resolveStreamComplete();
@@ -183,59 +245,79 @@ export function createNavigationClient(
183
245
  });
184
246
  };
185
247
 
186
- 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>;
263
+ // True only for a prefetch-cache hit whose stream had already fully drained
264
+ // (complete === true). A still-streaming hit and the fresh path stay false,
265
+ // so only a fully-warmed prefetch commits in a transition (no fallback flash).
266
+ let fullyPrefetched = false;
187
267
 
188
- if (cachedResponse) {
268
+ if (cachedEntry) {
189
269
  if (tx) {
190
- browserDebugLog(tx, "prefetch cache hit", { key: cacheKey });
270
+ browserDebugLog(tx, "prefetch cache hit (warm)", {
271
+ key: hitKey,
272
+ wildcard: hitKey === wildcardKey,
273
+ });
191
274
  }
192
- // Cached response body is already fully buffered (arrayBuffer),
193
- // so stream completion is immediate.
194
- responsePromise = Promise.resolve(cachedResponse).then((response) => {
195
- return teeWithCompletion(
196
- response,
197
- () => {
198
- if (tx) browserDebugLog(tx, "stream complete (from cache)");
199
- resolveStreamComplete();
200
- },
201
- signal,
202
- );
203
- });
204
- } else if (inflightResponsePromise) {
275
+ payloadPromise = cachedEntry.payload;
276
+ streamCompletePromise = cachedEntry.streamComplete;
277
+ // Only a hit whose stream already fully drained is "fully prefetched";
278
+ // a still-streaming hit must keep streaming its fallbacks like a cold load.
279
+ fullyPrefetched = cachedEntry.complete;
280
+ } else if (inflightEntryPromise) {
205
281
  if (tx) {
206
- browserDebugLog(tx, "reusing inflight prefetch", { key: cacheKey });
282
+ browserDebugLog(tx, "reusing inflight prefetch", {
283
+ key: hitKey,
284
+ wildcard: hitKey === wildcardKey,
285
+ });
207
286
  }
208
- responsePromise = inflightResponsePromise.then(async (response) => {
209
- if (!response) {
210
- if (tx) {
211
- browserDebugLog(tx, "inflight prefetch unavailable, refetching");
212
- }
213
- return doFreshFetch();
287
+ const adoptedViaWildcard = hitKey === wildcardKey;
288
+ const entry = await inflightEntryPromise;
289
+ if (!entry) {
290
+ if (tx) {
291
+ browserDebugLog(tx, "inflight prefetch unavailable, refetching");
214
292
  }
215
-
216
- return teeWithCompletion(
217
- response,
218
- () => {
219
- if (tx) {
220
- browserDebugLog(tx, "stream complete (from inflight prefetch)");
221
- }
222
- resolveStreamComplete();
223
- },
224
- signal,
225
- );
226
- });
293
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
294
+ freshResult());
295
+ } else if (adoptedViaWildcard && entry.scope === "source") {
296
+ // A wildcard-adopted inflight that turned out source-scoped was
297
+ // built for a different source page. Discard and refetch.
298
+ if (tx) {
299
+ browserDebugLog(
300
+ tx,
301
+ "wildcard inflight turned out source-scoped, refetching",
302
+ );
303
+ }
304
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
305
+ freshResult());
306
+ } else {
307
+ payloadPromise = entry.payload;
308
+ streamCompletePromise = entry.streamComplete;
309
+ // Adopted inflight is normally still streaming (false), but read the
310
+ // flag in case it completed between publish and adoption.
311
+ fullyPrefetched = entry.complete;
312
+ }
227
313
  } else {
228
- responsePromise = doFreshFetch();
314
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
315
+ freshResult());
229
316
  }
230
317
 
231
318
  try {
232
- // Deserialize RSC payload
233
- const payload = await deps.createFromFetch<RscPayload>(
234
- responsePromise,
235
- {
236
- ...(debugChannel && { debugChannel }),
237
- },
238
- );
319
+ const payload = await payloadPromise;
320
+
239
321
  if (tx) {
240
322
  browserDebugLog(tx, "response received", {
241
323
  isPartial: payload.metadata?.isPartial,
@@ -243,7 +325,11 @@ export function createNavigationClient(
243
325
  diffCount: payload.metadata?.diff?.length ?? 0,
244
326
  });
245
327
  }
246
- return { payload, streamComplete };
328
+ return {
329
+ payload,
330
+ streamComplete: streamCompletePromise,
331
+ fullyPrefetched,
332
+ };
247
333
  } catch (error) {
248
334
  // Convert network-level errors to NetworkError for proper handling
249
335
  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
+ }