@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
@@ -12,10 +12,7 @@ import type {
12
12
  ActionStateListener,
13
13
  HandleData,
14
14
  } from "./types.js";
15
- import {
16
- clearPrefetchCache,
17
- clearPrefetchCacheLocal,
18
- } from "./prefetch/cache.js";
15
+ import { clearPrefetchCache } from "./prefetch/cache.js";
19
16
 
20
17
  /**
21
18
  * Default action state (idle with no payload)
@@ -31,29 +28,49 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
31
28
  // Maximum number of history entries to cache (URLs visited)
32
29
  const HISTORY_CACHE_SIZE = 20;
33
30
 
34
- // Cache entry: [url-key, segments, stale, handleData?, routerId?]
35
- // stale=true means the data may be outdated and should be revalidated on access
31
+ // Cache entry:
32
+ // [url-key, segments, stale, handleData?, routerId?, navInstance?, handlesPending?]
33
+ // stale=true means the data may be outdated and should be revalidated on access.
34
+ // navInstance is the monotonic nav-instance token (see navInstance below): it
35
+ // identifies the per-commit visit that owns this entry. generateHistoryKey is
36
+ // URL-only, so A->B->A reuses the same key; the token lets a late async
37
+ // resolution tell its own visit's entry apart from a newer same-URL visit's, so
38
+ // a stale nav can never clobber a fresher one.
39
+ // handlesPending=true means the entry's handle data is INCOMPLETE (a deferred
40
+ // Meta was still pending when the user navigated away, so it never streamed). A
41
+ // popstate return must REVALIDATE WITH A FULL RE-RENDER (no client segment IDs)
42
+ // to re-stream the handles — a diff-only revalidation omits unchanged segments'
43
+ // handles, so the deferred Meta would never land. Cleared once the deferred Meta
44
+ // resolves while the entry is still owned.
36
45
  type HistoryCacheEntry = [
37
46
  string,
38
47
  ResolvedSegment[],
39
48
  boolean,
40
49
  HandleData?,
41
50
  string?,
51
+ number?,
52
+ boolean?,
42
53
  ];
43
54
 
44
55
  /**
45
- * Shallow clone handleData to avoid reference sharing between cache entries.
46
- * Only clones the structure (objects and arrays), not the data items themselves,
47
- * since mutations happen at the array level, not on individual data objects.
48
- * This preserves any non-serializable types (React elements, functions, etc.)
56
+ * Clone the handleData CONTAINERS (the handle-name map and each segment map) so
57
+ * a cache entry is decoupled from the live map that eventController mutates — it
58
+ * adds/deletes segment keys and REPLACES bucket arrays in place. The bucket
59
+ * arrays themselves are shared by reference, NOT copied: a bucket array is only
60
+ * ever replaced wholesale (eventController.setHandleData reassigns it,
61
+ * resolveDeferredHandleValues builds a fresh one) and collect functions read it
62
+ * without mutating, so sharing is safe and skips an O(elements) copy on every
63
+ * cache write — the per-yield streaming hot path. This also preserves any
64
+ * non-serializable bucket contents (React elements, functions, etc.).
49
65
  */
50
- function cloneHandleData(handleData: HandleData): HandleData {
66
+ export function cloneHandleData(handleData: HandleData): HandleData {
51
67
  const cloned: HandleData = {};
52
68
  for (const [handleKey, segmentMap] of Object.entries(handleData)) {
53
- cloned[handleKey] = {};
69
+ const clonedMap: Record<string, unknown[]> = {};
54
70
  for (const [segmentId, dataArray] of Object.entries(segmentMap)) {
55
- cloned[handleKey][segmentId] = [...dataArray];
71
+ clonedMap[segmentId] = dataArray;
56
72
  }
73
+ cloned[handleKey] = clonedMap;
57
74
  }
58
75
  return cloned;
59
76
  }
@@ -133,14 +150,14 @@ export interface NavigationStoreConfig {
133
150
 
134
151
  /**
135
152
  * Enable cross-tab cache invalidation via BroadcastChannel (default: true)
136
- * When cache is cleared (via server actions or useClientCache().clear()),
153
+ * When cache is cleared (via server actions or invalidateClientCache()),
137
154
  * other tabs will also clear their cache
138
155
  */
139
156
  crossTabSync?: boolean;
140
157
 
141
158
  /**
142
159
  * Auto-refresh when another tab mutates data on the same path (default: true)
143
- * Triggered when cache is cleared via server actions or useClientCache().clear()
160
+ * Triggered when cache is cleared via server actions or invalidateClientCache()
144
161
  * Requires crossTabSync to be enabled
145
162
  */
146
163
  crossTabAutoRefresh?: boolean;
@@ -242,6 +259,14 @@ export function createNavigationStore(
242
259
  // Oldest entries (at front) are removed when over cacheSize limit
243
260
  const historyCache: HistoryCacheEntry[] = [];
244
261
 
262
+ // Monotonic nav-instance token. Bumped each time a cache entry is created or
263
+ // replaced in cacheSegmentsForHistory (i.e. once per commit). Because
264
+ // generateHistoryKey is URL-only, two visits to the same URL share a key; this
265
+ // token gives each visit a distinct identity so a late async handle resolution
266
+ // can tell whether it still owns the live page / the target cache entry, and
267
+ // never overwrite a newer same-URL visit's state.
268
+ let navInstance = 0;
269
+
245
270
  // Current history key (set on navigation, stored in history.state)
246
271
  let currentHistoryKey = config?.initialHistoryKey || generateHistoryKey();
247
272
 
@@ -251,6 +276,10 @@ export function createNavigationStore(
251
276
  config.initialHistoryKey,
252
277
  config.initialSegments,
253
278
  false,
279
+ undefined,
280
+ undefined,
281
+ ++navInstance,
282
+ false,
254
283
  ]);
255
284
  }
256
285
 
@@ -338,24 +367,24 @@ export function createNavigationStore(
338
367
  }
339
368
 
340
369
  /**
341
- * Drop this tab's navigation + prefetch caches without broadcasting or
342
- * rotating shared state. Used when the local session changes in a way that
343
- * doesn't affect other tabs e.g. this tab crosses into a different app
344
- * via a cross-router navigation. Other tabs in the old app keep their
345
- * caches and their X-Rango-State token.
370
+ * Mark every history entry stale WITHOUT touching the prefetch caches or the
371
+ * rango state. Used by the jar-divergence observer: an external rotation has
372
+ * already changed the state value (so prefetch/HTTP entries strand under the
373
+ * retired key), and this tab must NOT re-rotate only the history cache,
374
+ * which is not state-keyed, needs marking.
346
375
  */
347
- function clearCacheInternalLocal(): void {
348
- historyCache.length = 0;
349
- clearPrefetchCacheLocal();
376
+ function markHistoryStale(): void {
377
+ for (let i = 0; i < historyCache.length; i++) {
378
+ historyCache[i][2] = true;
379
+ }
350
380
  }
351
381
 
352
382
  /**
353
- * Mark all cache entries as stale (internal - does not broadcast)
383
+ * Mark all cache entries as stale (internal - does not broadcast). Also
384
+ * clears the prefetch caches, which rotates the rango state.
354
385
  */
355
386
  function markCacheAsStaleInternal(): void {
356
- for (let i = 0; i < historyCache.length; i++) {
357
- historyCache[i][2] = true;
358
- }
387
+ markHistoryStale();
359
388
  clearPrefetchCache();
360
389
  }
361
390
 
@@ -568,6 +597,29 @@ export function createNavigationStore(
568
597
  currentHistoryKey = key;
569
598
  },
570
599
 
600
+ /**
601
+ * Current nav-instance token: the instance of the most recently committed
602
+ * navigation (the value last written by cacheSegmentsForHistory). A late
603
+ * async handle resolution captures this at the start of its own nav and
604
+ * compares it back here to detect whether a NEWER navigation has since
605
+ * committed (token advanced), guarding against a stale nav writing a fresher
606
+ * nav's live state.
607
+ */
608
+ getNavInstance(): number {
609
+ return navInstance;
610
+ },
611
+
612
+ /**
613
+ * The nav-instance token recorded on a specific cache entry, or undefined if
614
+ * no entry exists for that key. Because the history key is URL-only, this is
615
+ * how a late resolution tells "the entry I seeded is still mine" from "a
616
+ * newer same-URL visit replaced my entry".
617
+ */
618
+ getCacheEntryInstance(historyKey: string): number | undefined {
619
+ const entry = historyCache.find(([key]) => key === historyKey);
620
+ return entry ? entry[5] : undefined;
621
+ },
622
+
571
623
  /**
572
624
  * Store segments for a history entry
573
625
  * Updates existing entry if key exists, otherwise adds new entry
@@ -586,6 +638,11 @@ export function createNavigationStore(
586
638
  ? cloneHandleData(handleData)
587
639
  : undefined;
588
640
 
641
+ // Each commit (create or replace) is a new nav instance. The bump happens
642
+ // here, exactly once per cacheSegmentsForHistory call, so getNavInstance()
643
+ // reflects the visit whose entry this is.
644
+ const instance = ++navInstance;
645
+
589
646
  // Check if entry already exists and update it
590
647
  const existingIndex = historyCache.findIndex(
591
648
  ([key]) => key === historyKey,
@@ -597,6 +654,8 @@ export function createNavigationStore(
597
654
  false,
598
655
  clonedHandleData,
599
656
  currentRouterId,
657
+ instance,
658
+ false, // fresh commit: handles complete unless a deferred apply marks it
600
659
  ];
601
660
  } else {
602
661
  // Add new entry at the end (not stale)
@@ -606,6 +665,8 @@ export function createNavigationStore(
606
665
  false,
607
666
  clonedHandleData,
608
667
  currentRouterId,
668
+ instance,
669
+ false,
609
670
  ]);
610
671
  // Remove oldest entries if over limit
611
672
  while (historyCache.length > cacheSize) {
@@ -624,6 +685,7 @@ export function createNavigationStore(
624
685
  stale: boolean;
625
686
  handleData?: HandleData;
626
687
  routerId?: string;
688
+ handlesPending?: boolean;
627
689
  }
628
690
  | undefined {
629
691
  const entry = historyCache.find(([key]) => key === historyKey);
@@ -633,6 +695,7 @@ export function createNavigationStore(
633
695
  stale: entry[2],
634
696
  handleData: entry[3],
635
697
  routerId: entry[4],
698
+ handlesPending: entry[6],
636
699
  };
637
700
  },
638
701
 
@@ -644,11 +707,23 @@ export function createNavigationStore(
644
707
  },
645
708
 
646
709
  /**
647
- * Update only the handleData for an existing cache entry
648
- * Does nothing if the cache entry doesn't exist
649
- * This is used to fix stale handleData after async handles processing
710
+ * Update only the handleData (and optionally the stale flag) for an existing
711
+ * cache entry. Does nothing if the cache entry doesn't exist.
712
+ *
713
+ * Used to fix stale handleData after async handles processing AND to flip an
714
+ * entry's stale / handlesPending bits for the deferred-Meta
715
+ * invalidate+revalidate path: while a nav's Meta is deferred-pending its
716
+ * entry is marked stale + handlesPending (a popstate return then revalidates
717
+ * with a full re-render instead of serving the carry/seed as fresh), and once
718
+ * the deferred Meta resolves both are cleared. When a flag is omitted the
719
+ * entry's current value is preserved.
650
720
  */
651
- updateCacheHandleData(historyKey: string, handleData: HandleData): void {
721
+ updateCacheHandleData(
722
+ historyKey: string,
723
+ handleData: HandleData,
724
+ stale?: boolean,
725
+ handlesPending?: boolean,
726
+ ): void {
652
727
  const existingIndex = historyCache.findIndex(
653
728
  ([key]) => key === historyKey,
654
729
  );
@@ -659,13 +734,49 @@ export function createNavigationStore(
659
734
  historyCache[existingIndex] = [
660
735
  entry[0],
661
736
  entry[1],
662
- entry[2],
737
+ stale ?? entry[2], // set stale when provided, else preserve current
663
738
  clonedHandleData,
664
739
  entry[4], // preserve routerId
740
+ entry[5], // preserve navInstance (entry ownership identity)
741
+ handlesPending ?? entry[6], // set when provided, else preserve current
665
742
  ];
666
743
  }
667
744
  },
668
745
 
746
+ /**
747
+ * Owner-guarded handle-data write: locate the entry, and write ONLY when it
748
+ * is still owned by `ownerInstance` (the nav-instance token that seeded it).
749
+ * Folds the streaming hot path's separate getCacheEntryInstance() ownership
750
+ * probe and updateCacheHandleData() write into a SINGLE historyCache scan
751
+ * (processHandles calls this per yield). Semantics otherwise match
752
+ * updateCacheHandleData: no-op on a missing entry, clone the handleData
753
+ * containers, and preserve stale / handlesPending when the flag is omitted.
754
+ */
755
+ updateCacheHandleDataIfOwned(
756
+ historyKey: string,
757
+ handleData: HandleData,
758
+ ownerInstance: number,
759
+ stale?: boolean,
760
+ handlesPending?: boolean,
761
+ ): void {
762
+ const existingIndex = historyCache.findIndex(
763
+ ([key]) => key === historyKey,
764
+ );
765
+ if (existingIndex === -1) return;
766
+ const entry = historyCache[existingIndex];
767
+ if (entry[5] !== ownerInstance) return;
768
+ const clonedHandleData = cloneHandleData(handleData);
769
+ historyCache[existingIndex] = [
770
+ entry[0],
771
+ entry[1],
772
+ stale ?? entry[2],
773
+ clonedHandleData,
774
+ entry[4],
775
+ entry[5],
776
+ handlesPending ?? entry[6],
777
+ ];
778
+ },
779
+
669
780
  /**
670
781
  * Mark all cache entries as stale
671
782
  * Called after server actions to indicate data may be outdated
@@ -675,20 +786,21 @@ export function createNavigationStore(
675
786
  },
676
787
 
677
788
  /**
678
- * Clear the history cache and broadcast to other tabs
679
- * Use this for hard invalidation when data is definitely stale
789
+ * Mark every history entry stale WITHOUT clearing the prefetch caches or
790
+ * rotating the rango state. The jar-divergence observer calls this after an
791
+ * external rotation has already changed the state value, so re-rotating
792
+ * here would ping-pong with the tab that rotated.
680
793
  */
681
- clearHistoryCache(): void {
682
- clearCacheAndBroadcast();
794
+ markHistoryCacheStale(): void {
795
+ markHistoryStale();
683
796
  },
684
797
 
685
798
  /**
686
- * Drop this tab's navigation + prefetch caches locally without
687
- * broadcasting or rotating shared state. Intended for cross-app
688
- * transitions where the session state diverges for this tab only.
799
+ * Clear the history cache and broadcast to other tabs
800
+ * Use this for hard invalidation when data is definitely stale
689
801
  */
690
- clearHistoryCacheLocal(): void {
691
- clearCacheInternalLocal();
802
+ clearHistoryCache(): void {
803
+ clearCacheAndBroadcast();
692
804
  },
693
805
 
694
806
  /**
@@ -699,14 +811,6 @@ export function createNavigationStore(
699
811
  markStaleAndBroadcast();
700
812
  },
701
813
 
702
- /**
703
- * Broadcast cache invalidation to other tabs without clearing local cache
704
- * Used after consolidation fetch where local cache has fresh data
705
- */
706
- broadcastCacheInvalidation(): void {
707
- broadcastInvalidation();
708
- },
709
-
710
814
  /**
711
815
  * Set the callback to invoke when cross-tab refresh is triggered
712
816
  * Called by navigation bridge during initialization
@@ -823,42 +927,3 @@ export function createNavigationStore(
823
927
  },
824
928
  };
825
929
  }
826
-
827
- // Singleton store instance
828
- let storeInstance: NavigationStore | null = null;
829
-
830
- /**
831
- * Initialize the global navigation store
832
- *
833
- * Should be called once during app initialization.
834
- * Subsequent calls return the existing instance.
835
- */
836
- export function initNavigationStore(
837
- config?: NavigationStoreConfig,
838
- ): NavigationStore {
839
- if (!storeInstance) {
840
- storeInstance = createNavigationStore(config);
841
- }
842
- return storeInstance;
843
- }
844
-
845
- /**
846
- * Get the global navigation store
847
- *
848
- * Throws if store hasn't been initialized.
849
- */
850
- export function getNavigationStore(): NavigationStore {
851
- if (!storeInstance) {
852
- throw new Error(
853
- "Navigation store not initialized. Call initNavigationStore first.",
854
- );
855
- }
856
- return storeInstance;
857
- }
858
-
859
- /**
860
- * Reset the store instance (for testing)
861
- */
862
- export function resetNavigationStore(): void {
863
- storeInstance = null;
864
- }
@@ -13,7 +13,6 @@ import type { EventController, NavigationHandle } from "./event-controller.js";
13
13
  import { debugLog } from "./logging.js";
14
14
  import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
15
15
 
16
- // Re-export for consumers that import from navigation-transaction
17
16
  export { resolveNavigationState } from "./history-state.js";
18
17
 
19
18
  /** Check if a history state object contains location state keys. */
@@ -25,7 +24,6 @@ function hasLocationState(state: unknown): boolean {
25
24
  );
26
25
  }
27
26
 
28
- // Polyfill Symbol.dispose for Safari and older browsers
29
27
  if (typeof Symbol.dispose === "undefined") {
30
28
  (Symbol as any).dispose = Symbol("Symbol.dispose");
31
29
  }
@@ -114,7 +112,6 @@ export function createNavigationTransaction(
114
112
  let committed = false;
115
113
  const currentUrl = window.location.href;
116
114
 
117
- // Start navigation in event controller (this sets loading state)
118
115
  const handle = eventController.startNavigation(url, options);
119
116
 
120
117
  /**
@@ -138,72 +135,50 @@ export function createNavigationTransaction(
138
135
 
139
136
  const parsedUrl = new URL(url, window.location.origin);
140
137
 
141
- // Generate history key from URL (with intercept suffix for separate caching)
142
138
  const historyKey = generateHistoryKey(url, { intercept });
143
139
 
144
- // For cache-only commits (stale revalidation), only update cache and return
145
- // Don't touch store state or history - user may have navigated elsewhere
146
140
  if (cacheOnly) {
147
141
  const currentHandleData = eventController.getHandleState().data;
148
142
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
149
- // Complete the navigation handle so currentNavigation is cleared.
150
- // Without this, the entry lingers and weakens state-machine invariants.
151
143
  handle.complete(parsedUrl);
152
144
  debugLog("[Browser] Cache-only commit, historyKey:", historyKey);
153
145
  return { scroll: false };
154
146
  }
155
147
 
156
- // Save current scroll position before navigating
157
148
  handleNavigationStart();
158
149
 
159
- // Update segment state atomically
160
150
  store.setSegmentIds(segmentIds);
161
151
  store.setCurrentUrl(url);
162
152
  store.setPath(parsedUrl.pathname);
163
153
 
164
154
  store.setHistoryKey(historyKey);
165
155
 
166
- // Cache segments with current handleData for this history entry
167
156
  const currentHandleData = eventController.getHandleState().data;
168
157
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
169
158
 
170
- // For server actions, skip URL/history updates but still complete navigation
171
159
  if (storeOnly) {
172
160
  debugLog("[Browser] Store updated (action)");
173
- // Complete navigation to clear loading state
174
161
  handle.complete(parsedUrl);
175
162
  return { scroll: false };
176
163
  }
177
164
 
178
- // Build history state - include user state, intercept info, and server-set state
179
165
  const historyState = buildHistoryState(
180
166
  opts.state,
181
167
  { intercept, sourceUrl: interceptSourceUrl },
182
168
  serverState,
183
169
  );
184
170
 
185
- // Snapshot old state before pushState/replaceState overwrites it.
186
- // Used to detect when location state is being cleared.
187
171
  const oldState = window.history.state;
188
172
 
189
- // Update browser URL (stamps history.state.idx for back() first-entry detection)
190
173
  pushHistoryWithIdx(historyState, url, replace ?? false);
191
- // Ensure new history entry has a scroll restoration key
192
174
  ensureHistoryKey();
193
175
 
194
- // Notify location state hooks when either old or new state carries
195
- // location state. This covers both "set new state" and "clear old state"
196
- // for same-page navigations where components don't remount.
197
176
  if (hasLocationState(oldState) || hasLocationState(historyState)) {
198
177
  window.dispatchEvent(new Event("__rsc_locationstate"));
199
178
  }
200
179
 
201
- // Complete the navigation in event controller (sets idle state, updates location)
202
180
  handle.complete(parsedUrl);
203
181
 
204
- // NOTE: Scroll is NOT handled here. The caller (partial-update.ts) handles
205
- // scroll AFTER onUpdate() so React has the new content before we scroll.
206
-
207
182
  debugLog(
208
183
  "[Browser] Navigation committed, historyKey:",
209
184
  historyKey,
@@ -217,10 +192,6 @@ export function createNavigationTransaction(
217
192
  handle,
218
193
  commit,
219
194
 
220
- /**
221
- * Create a bound transaction with pre-configured URL options
222
- * segmentIds and segments provided at commit time (after they're resolved)
223
- */
224
195
  with(
225
196
  opts: Omit<CommitOptions, "segmentIds" | "segments">,
226
197
  ): BoundTransaction {
@@ -264,13 +235,10 @@ export function createNavigationTransaction(
264
235
  },
265
236
 
266
237
  [Symbol.dispose]() {
267
- // Superseded: another navigation took over.
268
238
  if (handle.signal.aborted) {
269
239
  return;
270
240
  }
271
241
 
272
- // Failed (not committed): keep the target URL -- the error UI owns it.
273
- // Just reset the event controller to idle.
274
242
  if (!committed) {
275
243
  handle[Symbol.dispose]();
276
244
  }
@@ -1,5 +1,5 @@
1
1
  import { NetworkError, isNetworkError } from "../errors.js";
2
- import { NetworkErrorThrower } from "../network-error-thrower.js";
2
+ import { RenderErrorThrower } from "../render-error-thrower.js";
3
3
  import type { UpdateSubscriber } from "./types.js";
4
4
  import { createElement, startTransition } from "react";
5
5
 
@@ -24,18 +24,18 @@ export function toNetworkError(
24
24
  }
25
25
 
26
26
  /**
27
- * Emit a NetworkError to the UI via the onUpdate subscriber.
28
- * Wraps in startTransition and renders a NetworkErrorThrower component
29
- * that throws during render to trigger the nearest error boundary.
27
+ * Render an error into the segment tree via the onUpdate subscriber so the
28
+ * nearest error boundary catches it. Wrapped in startTransition; RenderErrorThrower
29
+ * throws during render (async rejections do not reach boundaries on their own).
30
30
  */
31
- export function emitNetworkError(
31
+ function emitErrorToBoundary(
32
32
  onUpdate: UpdateSubscriber,
33
- error: NetworkError,
33
+ error: unknown,
34
34
  pathname: string,
35
35
  ): void {
36
36
  startTransition(() => {
37
37
  onUpdate({
38
- root: createElement(NetworkErrorThrower, { error }),
38
+ root: createElement(RenderErrorThrower, { error }),
39
39
  metadata: {
40
40
  pathname,
41
41
  segments: [],
@@ -45,6 +45,33 @@ export function emitNetworkError(
45
45
  });
46
46
  }
47
47
 
48
+ /**
49
+ * Emit a NetworkError to the nearest error boundary (offline, failed fetch).
50
+ */
51
+ export function emitNetworkError(
52
+ onUpdate: UpdateSubscriber,
53
+ error: NetworkError,
54
+ pathname: string,
55
+ ): void {
56
+ emitErrorToBoundary(onUpdate, error, pathname);
57
+ }
58
+
59
+ /**
60
+ * Emit a navigation processing error to the nearest error boundary. Used when a
61
+ * navigation response cannot be processed (an undecodable Flight body, or any
62
+ * unanticipated failure while building the response) -- for both fresh and
63
+ * prefetched responses, since both funnel through the navigation catch. Without
64
+ * this, such a failure becomes an uncaught rejection that silently aborts the
65
+ * navigation instead of surfacing the route's error boundary.
66
+ */
67
+ export function emitNavigationError(
68
+ onUpdate: UpdateSubscriber,
69
+ error: unknown,
70
+ pathname: string,
71
+ ): void {
72
+ emitErrorToBoundary(onUpdate, error, pathname);
73
+ }
74
+
48
75
  /**
49
76
  * Check if an error is safe to suppress in background operations.
50
77
  *