@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  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 +270 -30
  18. package/skills/host-router/SKILL.md +82 -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 +294 -30
  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 +203 -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 +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  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 +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  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 +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -4,12 +4,21 @@ import type {
4
4
  NavigateOptionsInternal,
5
5
  ResolvedSegment,
6
6
  } from "./types.js";
7
+ import { setAppVersion } from "./app-version.js";
8
+ import { isActionFenceActive } from "./action-fence.js";
9
+ import { getRangoState } from "./rango-state.js";
7
10
  import * as React from "react";
8
11
  import { startTransition } from "react";
9
12
  import {
10
13
  createNavigationTransaction,
11
14
  resolveNavigationState,
12
15
  } from "./navigation-transaction.js";
16
+ import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
17
+ import {
18
+ handleNavigationStart,
19
+ handleNavigationEnd,
20
+ ensureHistoryKey,
21
+ } from "./scroll-restoration.js";
13
22
 
14
23
  // addTransitionType is only available in React experimental
15
24
  const addTransitionType: ((type: string) => void) | undefined =
@@ -18,7 +27,6 @@ const addTransitionType: ((type: string) => void) | undefined =
18
27
  import { setupLinkInterception } from "./link-interceptor.js";
19
28
  import { createPartialUpdater } from "./partial-update.js";
20
29
  import { generateHistoryKey } from "./navigation-store.js";
21
- import { handleNavigationEnd } from "./scroll-restoration.js";
22
30
  import type { EventController } from "./event-controller.js";
23
31
  import { isInterceptOnlyCache } from "./intercept-utils.js";
24
32
  import {
@@ -35,11 +43,6 @@ if (typeof Symbol.dispose === "undefined") {
35
43
  (Symbol as any).dispose = Symbol("Symbol.dispose");
36
44
  }
37
45
 
38
- /** Get IDs of non-loader segments (layouts, routes, parallels). */
39
- function getNonLoaderSegmentIds(segments: ResolvedSegment[]): string[] {
40
- return segments.filter((s) => s.type !== "loader").map((s) => s.id);
41
- }
42
-
43
46
  export { createNavigationTransaction };
44
47
 
45
48
  /**
@@ -47,7 +50,7 @@ export { createNavigationTransaction };
47
50
  */
48
51
  export interface NavigationBridgeConfigWithController extends NavigationBridgeConfig {
49
52
  eventController: EventController;
50
- /** RSC version from initial payload metadata */
53
+ /** RSC version from initial payload metadata. */
51
54
  version?: string;
52
55
  }
53
56
 
@@ -67,8 +70,8 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
67
70
  export function createNavigationBridge(
68
71
  config: NavigationBridgeConfigWithController,
69
72
  ): NavigationBridge {
70
- const { store, client, eventController, onUpdate, renderSegments, version } =
71
- config;
73
+ const { store, client, eventController, onUpdate, renderSegments } = config;
74
+ let version = config.version;
72
75
 
73
76
  // Create shared partial updater
74
77
  const fetchPartialUpdate = createPartialUpdater({
@@ -76,7 +79,7 @@ export function createNavigationBridge(
76
79
  client,
77
80
  onUpdate,
78
81
  renderSegments,
79
- version,
82
+ getVersion: () => version,
80
83
  });
81
84
 
82
85
  return {
@@ -114,6 +117,81 @@ export function createNavigationBridge(
114
117
  return;
115
118
  }
116
119
 
120
+ // Shallow navigation: skip RSC fetch when revalidate is false
121
+ // and the pathname hasn't changed (search param / hash only change).
122
+ if (
123
+ options?.revalidate === false &&
124
+ targetUrl.pathname === new URL(window.location.href).pathname
125
+ ) {
126
+ // Preserve intercept context from the current history entry so that
127
+ // popstate uses the correct cache key (:intercept suffix) and restores
128
+ // the right full-page vs modal semantics.
129
+ const currentHistoryState = window.history.state;
130
+ const isIntercept = currentHistoryState?.intercept === true;
131
+ const interceptSourceUrl = isIntercept
132
+ ? currentHistoryState?.sourceUrl
133
+ : undefined;
134
+
135
+ const historyKey = generateHistoryKey(url, { intercept: isIntercept });
136
+
137
+ // Copy current segments to the new history key so back/forward restores instantly
138
+ const currentKey = store.getHistoryKey();
139
+ const currentCache = store.getCachedSegments(currentKey);
140
+ if (currentCache?.segments) {
141
+ const currentHandleData = eventController.getHandleState().data;
142
+ store.cacheSegmentsForHistory(
143
+ historyKey,
144
+ currentCache.segments,
145
+ currentHandleData,
146
+ );
147
+ }
148
+
149
+ // Save current scroll position before changing URL
150
+ handleNavigationStart();
151
+
152
+ // Snapshot old state before pushState/replaceState overwrites it
153
+ const oldState = window.history.state;
154
+
155
+ // Update browser URL (carry intercept context into history state)
156
+ const historyState = buildHistoryState(
157
+ resolvedState,
158
+ {
159
+ intercept: isIntercept || undefined,
160
+ sourceUrl: interceptSourceUrl,
161
+ },
162
+ {},
163
+ );
164
+ pushHistoryWithIdx(historyState, url, options?.replace ?? false);
165
+
166
+ // Ensure new history entry has a scroll restoration key
167
+ ensureHistoryKey();
168
+
169
+ // Notify useLocationState() hooks when state changes
170
+ const hasOldState =
171
+ oldState &&
172
+ typeof oldState === "object" &&
173
+ ("state" in oldState ||
174
+ Object.keys(oldState).some((k) => k.startsWith("__rsc_ls_")));
175
+ const hasNewState =
176
+ historyState &&
177
+ ("state" in historyState ||
178
+ Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_")));
179
+ if (hasOldState || hasNewState) {
180
+ window.dispatchEvent(new Event("__rsc_locationstate"));
181
+ }
182
+
183
+ // Update store history key so future navigations reference the right cache
184
+ store.setHistoryKey(historyKey);
185
+ store.setCurrentUrl(url);
186
+
187
+ // Notify hooks — location updates, state stays idle
188
+ eventController.setLocation(targetUrl);
189
+
190
+ // Handle post-navigation scroll
191
+ handleNavigationEnd({ scroll: options.scroll });
192
+ return;
193
+ }
194
+
117
195
  // Only abort pending requests when navigating to a different route
118
196
  // Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
119
197
  const currentPath = new URL(window.location.href).pathname;
@@ -181,18 +259,24 @@ export function createNavigationBridge(
181
259
  // 2. routes that CAN be intercepted - we don't know if this navigation will intercept
182
260
  // 3. when leaving intercept - we need fresh non-intercept segments from server
183
261
  // 4. redirect-with-state - force re-render so hooks read fresh state
262
+ // 5. stale cache - server action invalidated it, need fresh data with loading state
184
263
  const hasUsableCache =
185
264
  cachedSegments &&
186
265
  cachedSegments.length > 0 &&
187
266
  !isInterceptOnlyCache(cachedSegments) &&
188
267
  !hasInterceptCache &&
189
268
  !isLeavingIntercept &&
269
+ !cached?.stale &&
190
270
  !options?._skipCache;
191
271
 
272
+ // Forward navigations always await fetchPartialUpdate before rendering,
273
+ // so useNavigation should always report "loading". skipLoadingState is
274
+ // only used for popstate background revalidation (line ~526) where
275
+ // cached content renders instantly without a network wait.
192
276
  const tx = createNavigationTransaction(store, eventController, url, {
193
277
  ...options,
194
278
  state: resolvedState,
195
- skipLoadingState: hasUsableCache,
279
+ skipLoadingState: false,
196
280
  });
197
281
 
198
282
  // REVALIDATE: Fetch fresh data from server
@@ -200,7 +284,7 @@ export function createNavigationBridge(
200
284
  await fetchPartialUpdate(
201
285
  url,
202
286
  hasUsableCache
203
- ? getNonLoaderSegmentIds(cachedSegments!)
287
+ ? cachedSegments!.map((s) => s.id)
204
288
  : options?._skipCache
205
289
  ? [] // Action redirect: send no segments so server renders everything fresh
206
290
  : undefined,
@@ -332,6 +416,15 @@ export function createNavigationBridge(
332
416
  eventController.abortAllActions();
333
417
  }
334
418
 
419
+ // Popstate that exits an intercept to a non-intercept destination. The
420
+ // fallback fetch path below needs `leave-intercept` mode so it filters
421
+ // the cached @modal segment from the request and forces a re-render —
422
+ // otherwise a cache-miss popstate whose server response has an empty
423
+ // diff hits the "no changes" branch in partial-update and the modal
424
+ // stays on screen.
425
+ const isLeavingIntercept =
426
+ !isIntercept && currentInterceptSource !== null;
427
+
335
428
  // Compute history key from URL (with intercept suffix if applicable)
336
429
  const historyKey = generateHistoryKey(url, { intercept: isIntercept });
337
430
 
@@ -355,11 +448,22 @@ export function createNavigationBridge(
355
448
  // Helper to check if streaming is in progress
356
449
  const isStreaming = () => eventController.getState().isStreaming;
357
450
 
451
+ // Surface any external rotation of the rango state cookie (a server
452
+ // Set-Cookie, a sibling tab, a cookie clear) BEFORE reading the stale bit.
453
+ // The divergence observer only runs inside getRangoState() — fetch-time —
454
+ // so a popstate-first interaction would otherwise serve a pre-mutation
455
+ // page as fresh and never fetch to trigger the observer. Reading here lets
456
+ // the observer mark the history cache stale so getCachedSegments sees it.
457
+ getRangoState();
458
+
358
459
  // Check if we can restore from history cache
359
460
  const cached = store.getCachedSegments(historyKey);
360
461
  const cachedSegments = cached?.segments;
361
462
  const cachedHandleData = cached?.handleData;
362
- const isStale = cached?.stale ?? false;
463
+ // While an action is in flight the fence persists no stale flag, so OR it
464
+ // in here: a popstate during the flight serves the cached entry AND
465
+ // revalidates (SWR) instead of serving it as fresh.
466
+ const isStale = (cached?.stale ?? false) || isActionFenceActive();
363
467
 
364
468
  if (cachedSegments && cachedSegments.length > 0) {
365
469
  // Update store to point to this history entry
@@ -368,6 +472,12 @@ export function createNavigationBridge(
368
472
  store.setCurrentUrl(url);
369
473
  store.setPath(new URL(url).pathname);
370
474
 
475
+ // Restore router identity from cache so subsequent navigations
476
+ // don't falsely detect an app switch.
477
+ if (cached?.routerId) {
478
+ store.setRouterId?.(cached.routerId);
479
+ }
480
+
371
481
  // Render from cache - force await to skip loading fallbacks
372
482
  try {
373
483
  const root = await renderSegments(cachedSegments, {
@@ -393,8 +503,16 @@ export function createNavigationBridge(
393
503
  cachedHandleData,
394
504
  params: cachedParams,
395
505
  },
506
+ scroll: { restore: true, isStreaming },
396
507
  };
397
- const hasTransition = cachedSegments.some((s) => s.transition);
508
+ // Intercept-driven popstate (entering OR leaving an intercept) only
509
+ // mutates the parallel slot; the main outlet shows the same content.
510
+ // Skip startViewTransition in those cases — same rationale as the
511
+ // intercept guard in partial-update.ts's hasTransition computation.
512
+ const hasTransition =
513
+ !isIntercept &&
514
+ !isLeavingIntercept &&
515
+ cachedSegments.some((s) => s.transition);
398
516
  if (hasTransition) {
399
517
  startTransition(() => {
400
518
  if (addTransitionType) {
@@ -406,14 +524,11 @@ export function createNavigationBridge(
406
524
  onUpdate(popstateUpdate);
407
525
  }
408
526
 
409
- // Restore scroll position for back/forward navigation
410
- handleNavigationEnd({ restore: true, isStreaming });
411
-
412
527
  // SWR: If stale, trigger background revalidation
413
528
  if (isStale) {
414
529
  debugLog("[Browser] Cache is stale, background revalidating...");
415
530
  // Background revalidation - don't await, just fire and forget
416
- const segmentIds = getNonLoaderSegmentIds(cachedSegments);
531
+ const segmentIds = cachedSegments.map((s) => s.id);
417
532
 
418
533
  const tx = createNavigationTransaction(
419
534
  store,
@@ -478,7 +593,11 @@ export function createNavigationBridge(
478
593
  intercept: isIntercept,
479
594
  interceptSourceUrl,
480
595
  }),
481
- isIntercept ? { type: "navigate", interceptSourceUrl } : undefined,
596
+ isIntercept
597
+ ? { type: "navigate", interceptSourceUrl }
598
+ : isLeavingIntercept
599
+ ? { type: "leave-intercept" }
600
+ : undefined,
482
601
  );
483
602
  // Restore scroll position after fetch completes
484
603
  handleNavigationEnd({ restore: true, isStreaming });
@@ -555,6 +674,16 @@ export function createNavigationBridge(
555
674
  window.removeEventListener("pageshow", handlePageShow);
556
675
  };
557
676
  },
677
+
678
+ getVersion(): string | undefined {
679
+ return version;
680
+ },
681
+
682
+ updateVersion(newVersion: string): void {
683
+ version = newVersion;
684
+ setAppVersion(newVersion);
685
+ store.clearHistoryCache();
686
+ },
558
687
  };
559
688
  }
560
689
 
@@ -12,12 +12,21 @@ import {
12
12
  startBrowserTransaction,
13
13
  } from "./logging.js";
14
14
  import { getRangoState } from "./rango-state.js";
15
+ import { isActionFenceActive } from "./action-fence.js";
15
16
  import {
16
17
  extractRscHeaderUrl,
17
18
  emptyResponse,
19
+ handleReloadHeader,
18
20
  teeWithCompletion,
21
+ isForeignRouterId,
19
22
  } from "./response-adapter.js";
20
- import { buildPrefetchKey, consumePrefetch } from "./prefetch/cache.js";
23
+ import {
24
+ buildPrefetchKey,
25
+ buildSourceKey,
26
+ consumeInflightPrefetch,
27
+ consumePrefetch,
28
+ type DecodedPrefetch,
29
+ } from "./prefetch/cache.js";
21
30
 
22
31
  /**
23
32
  * Create a navigation client for fetching RSC payloads
@@ -26,8 +35,10 @@ import { buildPrefetchKey, consumePrefetch } from "./prefetch/cache.js";
26
35
  * deserializing the response using the RSC runtime.
27
36
  *
28
37
  * Checks the in-memory prefetch cache before making a network request.
29
- * The cache key is source-dependent (includes the previous URL) so
30
- * prefetch responses match the exact diff the server would produce.
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.
31
42
  *
32
43
  * @param deps - RSC browser dependencies (createFromFetch)
33
44
  * @returns NavigationClient instance
@@ -57,6 +68,7 @@ export function createNavigationClient(
57
68
  staleRevalidation,
58
69
  interceptSourceUrl,
59
70
  version,
71
+ routerId,
60
72
  hmr,
61
73
  } = options;
62
74
 
@@ -84,50 +96,130 @@ export function createNavigationClient(
84
96
  if (version) {
85
97
  fetchUrl.searchParams.set("_rsc_v", version);
86
98
  }
99
+ if (routerId) {
100
+ fetchUrl.searchParams.set("_rsc_rid", routerId);
101
+ }
87
102
 
88
- // Check in-memory prefetch cache before making a network request.
89
- // The cache key includes the source URL (previousUrl) because the
90
- // server's diff response depends on the source page context.
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.
91
110
  // Skip cache for stale revalidation (needs fresh data), HMR (needs
92
111
  // fresh modules), and intercept contexts (source-dependent responses).
93
- const cacheKey = buildPrefetchKey(previousUrl, fetchUrl);
94
- const cachedResponse =
95
- !staleRevalidation && !hmr && !interceptSourceUrl
96
- ? consumePrefetch(cacheKey)
97
- : null;
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);
98
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
+ }
99
146
  // Track when the stream completes
100
147
  let resolveStreamComplete: () => void;
101
148
  const streamComplete = new Promise<void>((resolve) => {
102
149
  resolveStreamComplete = resolve;
103
150
  });
104
151
 
105
- let responsePromise: Promise<Response>;
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;
106
173
 
107
- if (cachedResponse) {
108
- if (tx) {
109
- browserDebugLog(tx, "prefetch cache hit", { key: cacheKey });
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();
110
182
  }
111
- // Cached response body is already fully buffered (arrayBuffer),
112
- // so stream completion is immediate.
113
- responsePromise = Promise.resolve(cachedResponse).then((response) => {
114
- return teeWithCompletion(
115
- response,
116
- () => {
117
- if (tx) browserDebugLog(tx, "stream complete (from cache)");
118
- resolveStreamComplete();
119
- },
120
- signal,
121
- );
122
- });
123
- } else {
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
+ };
208
+
209
+ /** Start a fresh navigation fetch (no cache / inflight hit). */
210
+ const doFreshFetch = (): Promise<Response> => {
124
211
  if (tx) {
125
212
  browserDebugLog(tx, "fetching", {
126
213
  path: `${fetchUrl.pathname}${fetchUrl.search}`,
127
214
  });
128
215
  }
129
216
 
130
- responsePromise = fetch(fetchUrl, {
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 }),
131
223
  headers: {
132
224
  "X-RSC-Router-Client-Path": previousUrl,
133
225
  "X-Rango-State": getRangoState(),
@@ -139,43 +231,11 @@ export function createNavigationClient(
139
231
  },
140
232
  signal,
141
233
  }).then((response) => {
142
- // Check for version mismatch - server wants us to reload
143
- const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
144
- if (reload === "blocked") {
145
- resolveStreamComplete();
146
- return emptyResponse();
147
- }
148
- if (reload) {
149
- if (tx) {
150
- browserDebugLog(tx, "version mismatch, reloading", {
151
- reloadUrl: reload.url,
152
- });
153
- }
154
- window.location.href = reload.url;
155
- return new Promise<Response>(() => {});
156
- }
157
-
158
- // Server-side redirect without state: the server returned 204 with
159
- // X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
160
- // to a URL rendering full HTML). Throw ServerRedirect so the
161
- // navigation bridge catches it and re-navigates with _skipCache.
162
- const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
163
- if (redirect === "blocked") {
164
- resolveStreamComplete();
165
- return emptyResponse();
166
- }
167
- if (redirect) {
168
- if (tx) {
169
- browserDebugLog(tx, "server redirect", {
170
- redirectUrl: redirect.url,
171
- });
172
- }
173
- resolveStreamComplete();
174
- throw new ServerRedirect(redirect.url, undefined);
175
- }
234
+ const validated = validateRscHeaders(response, "fetch");
235
+ if (validated instanceof Promise) return validated;
176
236
 
177
237
  return teeWithCompletion(
178
- response,
238
+ validated,
179
239
  () => {
180
240
  if (tx) browserDebugLog(tx, "stream complete");
181
241
  resolveStreamComplete();
@@ -183,11 +243,71 @@ export function createNavigationClient(
183
243
  signal,
184
244
  );
185
245
  });
246
+ };
247
+
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
+
264
+ if (cachedEntry) {
265
+ if (tx) {
266
+ browserDebugLog(tx, "prefetch cache hit (warm)", {
267
+ key: hitKey,
268
+ wildcard: hitKey === wildcardKey,
269
+ });
270
+ }
271
+ payloadPromise = cachedEntry.payload;
272
+ streamCompletePromise = cachedEntry.streamComplete;
273
+ } else if (inflightEntryPromise) {
274
+ if (tx) {
275
+ browserDebugLog(tx, "reusing inflight prefetch", {
276
+ key: hitKey,
277
+ wildcard: hitKey === wildcardKey,
278
+ });
279
+ }
280
+ const adoptedViaWildcard = hitKey === wildcardKey;
281
+ const entry = await inflightEntryPromise;
282
+ if (!entry) {
283
+ if (tx) {
284
+ browserDebugLog(tx, "inflight prefetch unavailable, refetching");
285
+ }
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
+ }
303
+ } else {
304
+ ({ payload: payloadPromise, streamComplete: streamCompletePromise } =
305
+ freshResult());
186
306
  }
187
307
 
188
308
  try {
189
- // Deserialize RSC payload
190
- const payload = await deps.createFromFetch<RscPayload>(responsePromise);
309
+ const payload = await payloadPromise;
310
+
191
311
  if (tx) {
192
312
  browserDebugLog(tx, "response received", {
193
313
  isPartial: payload.metadata?.isPartial,
@@ -195,7 +315,7 @@ export function createNavigationClient(
195
315
  diffCount: payload.metadata?.diff?.length ?? 0,
196
316
  });
197
317
  }
198
- return { payload, streamComplete };
318
+ return { payload, streamComplete: streamCompletePromise };
199
319
  } catch (error) {
200
320
  // Convert network-level errors to NetworkError for proper handling
201
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
+ }