@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -5,8 +5,8 @@ import type {
5
5
  ResolvedSegment,
6
6
  } from "./types.js";
7
7
  import { setAppVersion } from "./app-version.js";
8
- import { setRangoStateLocal } from "./rango-state.js";
9
- import type { AppShell, AppShellRef } from "./app-shell.js";
8
+ import { isActionFenceActive } from "./action-fence.js";
9
+ import { getRangoState } from "./rango-state.js";
10
10
  import * as React from "react";
11
11
  import { startTransition } from "react";
12
12
  import {
@@ -32,6 +32,7 @@ import { isInterceptOnlyCache } from "./intercept-utils.js";
32
32
  import {
33
33
  toNetworkError,
34
34
  emitNetworkError,
35
+ emitNavigationError,
35
36
  isBackgroundSuppressible,
36
37
  } from "./network-error-handler.js";
37
38
  import { debugLog } from "./logging.js";
@@ -50,13 +51,8 @@ export { createNavigationTransaction };
50
51
  */
51
52
  export interface NavigationBridgeConfigWithController extends NavigationBridgeConfig {
52
53
  eventController: EventController;
53
- /** RSC version from initial payload metadata (fallback when appShellRef is not provided) */
54
+ /** RSC version from initial payload metadata. */
54
55
  version?: string;
55
- /**
56
- * Live app-shell ref. When supplied, the bridge reads version/basename
57
- * from this ref so cross-app navigations propagate correctly.
58
- */
59
- appShellRef?: AppShellRef;
60
56
  }
61
57
 
62
58
  /**
@@ -75,46 +71,9 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
75
71
  export function createNavigationBridge(
76
72
  config: NavigationBridgeConfigWithController,
77
73
  ): NavigationBridge {
78
- const {
79
- store,
80
- client,
81
- eventController,
82
- onUpdate,
83
- renderSegments,
84
- appShellRef,
85
- } = config;
74
+ const { store, client, eventController, onUpdate, renderSegments } = config;
86
75
  let version = config.version;
87
76
 
88
- /**
89
- * Replace the active app-shell snapshot atomically. Called by the partial
90
- * updater when a response's routerId indicates the navigation crossed
91
- * into a different app. Runs the local-only side-effects tied to
92
- * app-shell fields (app version, rango-state namespace) so the new app
93
- * owns them after the swap. Theme, warmup, and prefetch TTL are
94
- * document-lifetime and are NOT touched here.
95
- */
96
- function applyAppShell(next: AppShell): void {
97
- if (appShellRef) {
98
- appShellRef.update(next);
99
- }
100
- if (next.version !== undefined) {
101
- version = next.version;
102
- setAppVersion(next.version);
103
- // Use the local-only setter — initRangoState writes the shared
104
- // localStorage key and fires a storage event in other tabs still in
105
- // the old app. setRangoStateLocal only mutates this tab's in-memory
106
- // cache and rebinds it to the target app's routerId-scoped key,
107
- // preserving the "local-only, no broadcast/rotation" contract for
108
- // smooth app-switch transitions.
109
- setRangoStateLocal(next.version, next.routerId);
110
- }
111
- // Cross-app: prior cache entries belong to a different app's segments.
112
- // Drop them locally only — do NOT broadcast invalidation or rotate the
113
- // shared X-Rango-State token, since other tabs still in the old app are
114
- // unaffected by this tab's transition.
115
- store.clearHistoryCacheLocal();
116
- }
117
-
118
77
  // Create shared partial updater
119
78
  const fetchPartialUpdate = createPartialUpdater({
120
79
  store,
@@ -122,7 +81,6 @@ export function createNavigationBridge(
122
81
  onUpdate,
123
82
  renderSegments,
124
83
  getVersion: () => version,
125
- applyAppShell,
126
84
  });
127
85
 
128
86
  return {
@@ -258,17 +216,20 @@ export function createNavigationBridge(
258
216
  store.setInterceptSourceUrl(null);
259
217
  }
260
218
 
261
- // Before navigating away, update the source page's cache with the latest handleData.
262
- // This ensures the cache has correct handleData even if handles were streaming.
219
+ // Before navigating away, update the source page's cache with the latest
220
+ // handleData. This ensures the cache has correct handleData even if handles
221
+ // were streaming. Use updateCacheHandleData (not cacheSegmentsForHistory):
222
+ // the source page's segments are unchanged, so this is a handleData refresh,
223
+ // not a commit. Critically it PRESERVES the entry's stale flag — when the
224
+ // source page has a deferred Meta still pending, its entry was marked stale
225
+ // (invalidate-on-pending) so a popstate return revalidates; re-committing it
226
+ // here would reset stale to false and serve the carried (pre-resolution)
227
+ // title as fresh. It also leaves the nav-instance token intact.
263
228
  const sourceHistoryKey = store.getHistoryKey();
264
229
  const sourceCached = store.getCachedSegments(sourceHistoryKey);
265
230
  if (sourceCached?.segments && sourceCached.segments.length > 0) {
266
231
  const currentHandleData = eventController.getHandleState().data;
267
- store.cacheSegmentsForHistory(
268
- sourceHistoryKey,
269
- sourceCached.segments,
270
- currentHandleData,
271
- );
232
+ store.updateCacheHandleData(sourceHistoryKey, currentHandleData);
272
233
  }
273
234
 
274
235
  // Check if we have cached segments for target URL
@@ -368,8 +329,15 @@ export function createNavigationBridge(
368
329
  } as NavigateOptionsInternal);
369
330
  }
370
331
 
371
- if (error instanceof DOMException && error.name === "AbortError") {
372
- debugLog("[Browser] Navigation aborted by newer navigation");
332
+ // Aborted, or superseded by a newer navigation. A superseded nav may
333
+ // reject with a non-AbortError (e.g. a Flight decode that fails after its
334
+ // signal was aborted), so check the signal too -- otherwise we would
335
+ // render a boundary that clobbers the newer navigation's content.
336
+ if (
337
+ (error instanceof DOMException && error.name === "AbortError") ||
338
+ tx.handle.signal.aborted
339
+ ) {
340
+ debugLog("[Browser] Navigation aborted or superseded");
373
341
  return;
374
342
  }
375
343
 
@@ -386,7 +354,13 @@ export function createNavigationBridge(
386
354
  return;
387
355
  }
388
356
 
389
- throw error;
357
+ // A response we could not process (undecodable Flight body, or an
358
+ // unanticipated failure building the response). Surface the route's
359
+ // error boundary rather than let the rejection abort the navigation
360
+ // silently. Prefetched responses funnel here too: a failed warm-prefetch
361
+ // payload rejects on consumption and propagates to this catch.
362
+ console.error("[Browser] Unprocessable navigation response:", error);
363
+ emitNavigationError(onUpdate, error, url);
390
364
  } finally {
391
365
  tx[Symbol.dispose]();
392
366
  }
@@ -415,6 +389,14 @@ export function createNavigationBridge(
415
389
  tx.with({ url: window.location.href, replace: true, scroll: false }),
416
390
  );
417
391
  } catch (error) {
392
+ // Aborted or superseded: bail without rendering a boundary (see navigate()).
393
+ if (
394
+ (error instanceof DOMException && error.name === "AbortError") ||
395
+ tx.handle.signal.aborted
396
+ ) {
397
+ return;
398
+ }
399
+
418
400
  const networkError = toNetworkError(error, {
419
401
  url: window.location.href,
420
402
  operation: "revalidation",
@@ -427,7 +409,12 @@ export function createNavigationBridge(
427
409
  emitNetworkError(onUpdate, networkError, window.location.href);
428
410
  return;
429
411
  }
430
- throw error;
412
+
413
+ // refresh() shares the fetchPartialUpdate chokepoint with navigate()/
414
+ // popstate, so an unprocessable response must surface the error boundary
415
+ // here too rather than become an uncaught rejection.
416
+ console.error("[Browser] Unprocessable refresh response:", error);
417
+ emitNavigationError(onUpdate, error, window.location.href);
431
418
  } finally {
432
419
  tx[Symbol.dispose]();
433
420
  }
@@ -491,11 +478,22 @@ export function createNavigationBridge(
491
478
  // Helper to check if streaming is in progress
492
479
  const isStreaming = () => eventController.getState().isStreaming;
493
480
 
481
+ // Surface any external rotation of the rango state cookie (a server
482
+ // Set-Cookie, a sibling tab, a cookie clear) BEFORE reading the stale bit.
483
+ // The divergence observer only runs inside getRangoState() — fetch-time —
484
+ // so a popstate-first interaction would otherwise serve a pre-mutation
485
+ // page as fresh and never fetch to trigger the observer. Reading here lets
486
+ // the observer mark the history cache stale so getCachedSegments sees it.
487
+ getRangoState();
488
+
494
489
  // Check if we can restore from history cache
495
490
  const cached = store.getCachedSegments(historyKey);
496
491
  const cachedSegments = cached?.segments;
497
492
  const cachedHandleData = cached?.handleData;
498
- const isStale = cached?.stale ?? false;
493
+ // While an action is in flight the fence persists no stale flag, so OR it
494
+ // in here: a popstate during the flight serves the cached entry AND
495
+ // revalidates (SWR) instead of serving it as fresh.
496
+ const isStale = (cached?.stale ?? false) || isActionFenceActive();
499
497
 
500
498
  if (cachedSegments && cachedSegments.length > 0) {
501
499
  // Update store to point to this history entry
@@ -559,8 +557,19 @@ export function createNavigationBridge(
559
557
  // SWR: If stale, trigger background revalidation
560
558
  if (isStale) {
561
559
  debugLog("[Browser] Cache is stale, background revalidating...");
562
- // Background revalidation - don't await, just fire and forget
563
- const segmentIds = cachedSegments.map((s) => s.id);
560
+ // Background revalidation - don't await, just fire and forget.
561
+ // When the entry's handles are incomplete (a deferred Meta was still
562
+ // pending when the user navigated away — see handlesPending), send NO
563
+ // segment IDs so the server returns a FULL re-render with the handle
564
+ // stream. A normal stale revalidation sends the cached IDs and the
565
+ // server returns a diff-only payload that omits unchanged segments'
566
+ // handles, so a deferred Meta would never re-stream and the title
567
+ // would stay the pre-resolution carry. handlesPending is set only for
568
+ // the deferred-Meta-aborted case, so action/cross-tab SWR keeps the
569
+ // cheap diff path.
570
+ const segmentIds = cached?.handlesPending
571
+ ? []
572
+ : cachedSegments.map((s) => s.id);
564
573
 
565
574
  const tx = createNavigationTransaction(
566
575
  store,
@@ -634,8 +643,13 @@ export function createNavigationBridge(
634
643
  // Restore scroll position after fetch completes
635
644
  handleNavigationEnd({ restore: true, isStreaming });
636
645
  } catch (error) {
637
- if (error instanceof DOMException && error.name === "AbortError") {
638
- debugLog("[Browser] Popstate navigation aborted");
646
+ // Aborted or superseded by a newer navigation: bail without clobbering
647
+ // content with a boundary (see navigate()).
648
+ if (
649
+ (error instanceof DOMException && error.name === "AbortError") ||
650
+ tx.handle.signal.aborted
651
+ ) {
652
+ debugLog("[Browser] Popstate navigation aborted or superseded");
639
653
  return;
640
654
  }
641
655
 
@@ -652,7 +666,10 @@ export function createNavigationBridge(
652
666
  return;
653
667
  }
654
668
 
655
- throw error;
669
+ // Unprocessable response on a back/forward navigation: surface the
670
+ // error boundary instead of an uncaught rejection (see navigate()).
671
+ console.error("[Browser] Unprocessable popstate response:", error);
672
+ emitNavigationError(onUpdate, error, url);
656
673
  } finally {
657
674
  tx[Symbol.dispose]();
658
675
  }
@@ -716,10 +733,6 @@ export function createNavigationBridge(
716
733
  setAppVersion(newVersion);
717
734
  store.clearHistoryCache();
718
735
  },
719
-
720
- updateAppShell(next: AppShell): void {
721
- applyAppShell(next);
722
- },
723
736
  };
724
737
  }
725
738
 
@@ -8,21 +8,25 @@ import type {
8
8
  import { NetworkError, ServerRedirect, isNetworkError } from "../errors.js";
9
9
  import {
10
10
  browserDebugLog,
11
+ debugLog,
11
12
  isBrowserDebugEnabled,
12
13
  startBrowserTransaction,
13
14
  } from "./logging.js";
14
15
  import { getRangoState } from "./rango-state.js";
16
+ import { isActionFenceActive } from "./action-fence.js";
15
17
  import {
16
18
  extractRscHeaderUrl,
17
19
  emptyResponse,
18
20
  handleReloadHeader,
19
21
  teeWithCompletion,
22
+ isForeignRouterId,
20
23
  } from "./response-adapter.js";
21
24
  import {
22
25
  buildPrefetchKey,
23
26
  buildSourceKey,
24
27
  consumeInflightPrefetch,
25
28
  consumePrefetch,
29
+ type DecodedPrefetch,
26
30
  } from "./prefetch/cache.js";
27
31
 
28
32
  /**
@@ -106,31 +110,38 @@ export function createNavigationClient(
106
110
  // server-action invalidation) auto-invalidates both scopes.
107
111
  // Skip cache for stale revalidation (needs fresh data), HMR (needs
108
112
  // fresh modules), and intercept contexts (source-dependent responses).
109
- const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
113
+ // Suspend prefetch consumption while an action is in flight: a queued
114
+ // prefetch holds pre-mutation data and must not be served until the
115
+ // action's response decides whether anything changed.
116
+ const canUsePrefetch =
117
+ !staleRevalidation &&
118
+ !hmr &&
119
+ !interceptSourceUrl &&
120
+ !isActionFenceActive();
110
121
  const rangoState = getRangoState();
111
122
  const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
112
123
  const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
113
124
 
114
- let cachedResponse: Response | null = null;
125
+ let cachedEntry: DecodedPrefetch | null = null;
115
126
  let hitKey: string | null = null;
116
127
  if (canUsePrefetch) {
117
- cachedResponse = consumePrefetch(cacheKey);
118
- if (cachedResponse) {
128
+ cachedEntry = consumePrefetch(cacheKey);
129
+ if (cachedEntry) {
119
130
  hitKey = cacheKey;
120
131
  } else {
121
- cachedResponse = consumePrefetch(wildcardKey);
122
- if (cachedResponse) hitKey = wildcardKey;
132
+ cachedEntry = consumePrefetch(wildcardKey);
133
+ if (cachedEntry) hitKey = wildcardKey;
123
134
  }
124
135
  }
125
136
 
126
- let inflightResponsePromise: Promise<Response | null> | null = null;
127
- if (canUsePrefetch && !cachedResponse) {
128
- inflightResponsePromise = consumeInflightPrefetch(cacheKey);
129
- if (inflightResponsePromise) {
137
+ let inflightEntryPromise: Promise<DecodedPrefetch | null> | null = null;
138
+ if (canUsePrefetch && !cachedEntry) {
139
+ inflightEntryPromise = consumeInflightPrefetch(cacheKey);
140
+ if (inflightEntryPromise) {
130
141
  hitKey = cacheKey;
131
142
  } else {
132
- inflightResponsePromise = consumeInflightPrefetch(wildcardKey);
133
- if (inflightResponsePromise) hitKey = wildcardKey;
143
+ inflightEntryPromise = consumeInflightPrefetch(wildcardKey);
144
+ if (inflightEntryPromise) hitKey = wildcardKey;
134
145
  }
135
146
  }
136
147
  // Track when the stream completes
@@ -180,6 +191,19 @@ export function createNavigationClient(
180
191
  throw new ServerRedirect(redirect.url, undefined);
181
192
  }
182
193
 
194
+ // Integrity check (pre-decode): refuse a foreign app's content response
195
+ // before createFromFetch imports its chunks. Ordered AFTER the reload
196
+ // and redirect handlers — control responses are never stamped with
197
+ // X-RSC-Router-Id, so they are steered first and never reach here.
198
+ if (isForeignRouterId(response, routerId)) {
199
+ if (tx) {
200
+ browserDebugLog(tx, `router id mismatch, reloading (${source})`);
201
+ }
202
+ resolveStreamComplete();
203
+ window.location.href = targetUrl;
204
+ return new Promise<Response>(() => {});
205
+ }
206
+
183
207
  return response;
184
208
  };
185
209
 
@@ -192,9 +216,18 @@ export function createNavigationClient(
192
216
  }
193
217
 
194
218
  return fetch(fetchUrl, {
219
+ // During an action's flight the state is not rotated, so the old
220
+ // X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass
221
+ // it so a genuine mid-action navigation fetches fresh instead of being
222
+ // served the stale prefetched bytes.
223
+ ...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
195
224
  headers: {
196
225
  "X-RSC-Router-Client-Path": previousUrl,
197
- "X-Rango-State": getRangoState(),
226
+ // Reuse the single per-operation read (see rangoState above): the
227
+ // cache-key lookup and this header must agree on one value, and the
228
+ // cookie read has side effects (external-rotation notify) we do not
229
+ // want to fire twice per navigation.
230
+ "X-Rango-State": rangoState,
198
231
  ...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
199
232
  ...(interceptSourceUrl && {
200
233
  "X-RSC-Router-Intercept-Source": interceptSourceUrl,
@@ -217,29 +250,39 @@ export function createNavigationClient(
217
250
  });
218
251
  };
219
252
 
220
- let responsePromise: Promise<Response>;
253
+ // A warm prefetch hit returns its eagerly-decoded payload directly: the
254
+ // route's chunks were imported during the prefetch, so this click runs
255
+ // no decode and no network. Only the fresh path runs createFromFetch and
256
+ // resolves the local streamComplete (via doFreshFetch's teeWithCompletion
257
+ // and the control-header short-circuits in validateRscHeaders).
258
+ const freshResult = (): {
259
+ payload: Promise<RscPayload>;
260
+ streamComplete: Promise<void>;
261
+ } => ({
262
+ payload: deps.createFromFetch<RscPayload>(doFreshFetch()),
263
+ streamComplete,
264
+ });
265
+
266
+ let payloadPromise: Promise<RscPayload>;
267
+ let streamCompletePromise: Promise<void>;
268
+ // True only for a prefetch-cache hit whose stream had already fully drained
269
+ // (complete === true). A still-streaming hit and the fresh path stay false,
270
+ // so only a fully-warmed prefetch commits in a transition (no fallback flash).
271
+ let fullyPrefetched = false;
221
272
 
222
- if (cachedResponse) {
273
+ if (cachedEntry) {
223
274
  if (tx) {
224
- browserDebugLog(tx, "prefetch cache hit", {
275
+ browserDebugLog(tx, "prefetch cache hit (warm)", {
225
276
  key: hitKey,
226
277
  wildcard: hitKey === wildcardKey,
227
278
  });
228
279
  }
229
- responsePromise = Promise.resolve(cachedResponse).then((response) => {
230
- const validated = validateRscHeaders(response, "prefetch cache");
231
- if (validated instanceof Promise) return validated;
232
-
233
- return teeWithCompletion(
234
- validated,
235
- () => {
236
- if (tx) browserDebugLog(tx, "stream complete (from cache)");
237
- resolveStreamComplete();
238
- },
239
- signal,
240
- );
241
- });
242
- } else if (inflightResponsePromise) {
280
+ payloadPromise = cachedEntry.payload;
281
+ streamCompletePromise = cachedEntry.streamComplete;
282
+ // Only a hit whose stream already fully drained is "fully prefetched";
283
+ // a still-streaming hit must keep streaming its fallbacks like a cold load.
284
+ fullyPrefetched = cachedEntry.complete;
285
+ } else if (inflightEntryPromise) {
243
286
  if (tx) {
244
287
  browserDebugLog(tx, "reusing inflight prefetch", {
245
288
  key: hitKey,
@@ -247,51 +290,49 @@ export function createNavigationClient(
247
290
  });
248
291
  }
249
292
  const adoptedViaWildcard = hitKey === wildcardKey;
250
- responsePromise = inflightResponsePromise.then(async (response) => {
251
- if (!response) {
252
- if (tx) {
253
- browserDebugLog(tx, "inflight prefetch unavailable, refetching");
254
- }
255
- return doFreshFetch();
293
+ const entry = await inflightEntryPromise;
294
+ if (!entry) {
295
+ if (tx) {
296
+ browserDebugLog(tx, "inflight prefetch unavailable, refetching");
256
297
  }
257
-
258
- // Cross-source safety: an inflight promise adopted via the
259
- // wildcard key may turn out to be source-scoped (server emitted
260
- // `X-RSC-Prefetch-Scope: source`), which means it was built for
261
- // a different source page. Discard and refetch.
262
- if (
263
- adoptedViaWildcard &&
264
- response.headers.get("x-rsc-prefetch-scope") === "source"
265
- ) {
266
- if (tx) {
267
- browserDebugLog(
268
- tx,
269
- "wildcard inflight turned out source-scoped, refetching",
270
- );
271
- }
272
- return doFreshFetch();
298
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
299
+ freshResult());
300
+ } else if (adoptedViaWildcard && entry.scope === "source") {
301
+ // A wildcard-adopted inflight that turned out source-scoped was
302
+ // built for a different source page. Discard and refetch.
303
+ if (tx) {
304
+ browserDebugLog(
305
+ tx,
306
+ "wildcard inflight turned out source-scoped, refetching",
307
+ );
273
308
  }
274
-
275
- const validated = validateRscHeaders(response, "inflight prefetch");
276
- if (validated instanceof Promise) return validated;
277
-
278
- return teeWithCompletion(
279
- validated,
280
- () => {
281
- if (tx) {
282
- browserDebugLog(tx, "stream complete (from inflight prefetch)");
283
- }
284
- resolveStreamComplete();
285
- },
286
- signal,
287
- );
288
- });
309
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
310
+ freshResult());
311
+ } else {
312
+ payloadPromise = entry.payload;
313
+ streamCompletePromise = entry.streamComplete;
314
+ // Adopted inflight is normally still streaming (false), but read the
315
+ // flag in case it completed between publish and adoption.
316
+ fullyPrefetched = entry.complete;
317
+ }
289
318
  } else {
290
- responsePromise = doFreshFetch();
319
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
320
+ freshResult());
291
321
  }
292
322
 
293
323
  try {
294
- const payload = await deps.createFromFetch<RscPayload>(responsePromise);
324
+ // [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. Times how long the RSC
325
+ // payload ROOT takes to resolve: ~full-stream duration means the root
326
+ // model is not flushed early (server/runtime buffering, e.g. wrangler
327
+ // dev gzip); fast means the block, if any, is downstream in render.
328
+ const vtDebugStart = isBrowserDebugEnabled() ? performance.now() : 0;
329
+ const payload = await payloadPromise;
330
+ if (isBrowserDebugEnabled()) {
331
+ debugLog("[VT-DIAG] payloadResolved", {
332
+ ms: Math.round(performance.now() - vtDebugStart),
333
+ isPartial: payload.metadata?.isPartial,
334
+ });
335
+ }
295
336
 
296
337
  if (tx) {
297
338
  browserDebugLog(tx, "response received", {
@@ -300,7 +341,11 @@ export function createNavigationClient(
300
341
  diffCount: payload.metadata?.diff?.length ?? 0,
301
342
  });
302
343
  }
303
- return { payload, streamComplete };
344
+ return {
345
+ payload,
346
+ streamComplete: streamCompletePromise,
347
+ fullyPrefetched,
348
+ };
304
349
  } catch (error) {
305
350
  // Convert network-level errors to NetworkError for proper handling
306
351
  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
+ }