@rangojs/router 0.20.0 → 0.22.0

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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -125,6 +125,16 @@
125
125
  "react-server": "./src/cloudflare/index.ts",
126
126
  "default": "./src/cloudflare/index.ts"
127
127
  },
128
+ "./prerender": {
129
+ "types": "./dist/types/prerender/index.d.ts",
130
+ "react-server": "./src/prerender/index.ts",
131
+ "default": "./src/prerender/index.ts"
132
+ },
133
+ "./prerender/cloudflare": {
134
+ "types": "./dist/types/prerender/cloudflare.d.ts",
135
+ "react-server": "./src/prerender/cloudflare.ts",
136
+ "default": "./src/prerender/cloudflare.ts"
137
+ },
128
138
  "./vercel": {
129
139
  "types": "./dist/types/vercel/index.d.ts",
130
140
  "react-server": "./src/vercel/index.ts",
@@ -208,7 +218,7 @@
208
218
  "jiti": "^2.7.0",
209
219
  "react": "^19.3.0",
210
220
  "react-dom": "^19.3.0",
211
- "vitest": "^4.1.9",
221
+ "vitest": "^4.1.11",
212
222
  "@shared/e2e": "0.0.1"
213
223
  },
214
224
  "peerDependencies": {
@@ -217,7 +227,6 @@
217
227
  "@playwright/test": "^1.49.1",
218
228
  "@testing-library/react": ">=16",
219
229
  "@vercel/functions": "^3.0.0",
220
- "@vitejs/plugin-rsc": "^0.5.35",
221
230
  "react": ">=19.2.8 <20",
222
231
  "react-dom": ">=19.2.8 <20",
223
232
  "vite": "^8.0.16",
@@ -166,24 +166,87 @@ exemption there). The route then serves uncached; see `/ppr`.
166
166
 
167
167
  ### Cross-deploy safety: version-segmented store keys
168
168
 
169
- `CFCacheStore` prefixes every **physical** store key (the CF Cache API URL and
170
- the KV key) with the build version — auto-generated from the
171
- `@rangojs/router:version` virtual module, overridable via the store's `version`
172
- option. A new deploy reads under a new prefix, so it can **never** read a
173
- previous build's entries: no cross-deploy shape drift, and no dead client-chunk
174
- references baked into cached RSC.
175
-
176
- The tradeoff to know: **loader/data caches use the same store**, so they're
177
- version-segmented too. Every deploy is therefore a _cold data cache_ — SWR can't
178
- soften it, because no stale entry exists under the new key. For high-traffic,
179
- frequently-deploying, data-bound apps that's a deploy-time origin warm-up. Decide
180
- deliberately: accept it (correctness over hit-rate), or split the policy — let
181
- the render/edge cache auto-version while a separate data store gets a stable
182
- `version` so its entries survive deploys. (Per-process stores like
183
- `MemorySegmentCacheStore` are cold on every restart anyway; this matters for
184
- persistent stores.) `VercelCacheStore` does not version automatically: pass a
185
- per-deploy `getCache({ namespace })` or its `version` option (see `/vercel`).
186
- See `/caching` for store setup.
169
+ `CFCacheStore` and `VercelCacheStore` prefix every **physical** store key with a
170
+ version, so a build only reads entries that code like its own wrote. The version
171
+ is a hash of the code, computed per `createRouter()` by `vite build`, not the
172
+ build time. Each router has two:
173
+
174
+ - The **data version** (a hash of the router's server code) keys cached RSC
175
+ data: segment entries from `cache()`, `"use cache"` values, loader data.
176
+ - The **document version** (the data version plus the SSR output, the client
177
+ asset file names, `base`, and the router's `Prerender` payloads) keys stored
178
+ HTML (PPR shells, document-cache responses). It is also sent to the browser,
179
+ and a mismatch with the tab's `_rsc_v` reloads the tab.
180
+
181
+ Same code builds to the same versions, so a rebuild or redeploy of unchanged
182
+ code keeps the cache, and a deploy that changes app A keeps app B's cache.
183
+
184
+ | Deploy | Cached data | Stored HTML | Open tabs |
185
+ | ------------------------------------- | ------------------- | --------------------- | ----------------------- |
186
+ | Rebuild, no code change | kept | kept | untouched |
187
+ | Server code of app A changes | cleared for A only | cleared for A only | A's tabs reload |
188
+ | Client code of any app changes | kept for every app | cleared for every app | every app's tabs reload |
189
+ | Server code shared by A and B changes | cleared for A and B | cleared for A and B | A's and B's tabs reload |
190
+
191
+ "Server code" is everything the app's server can run: its own modules, the
192
+ dependencies it imports (bundled, or on the node preset left external and
193
+ resolved from `node_modules`), and a stylesheet its server code links
194
+ (`import "./x.css"` in a server component, or `import href from "./x.css?url"`
195
+ for a document `<link>`): the stylesheet's hashed URL is in what the server
196
+ renders, so a change to the compiled CSS clears that app's cached data. A
197
+ `clientUrls()` module is client code that also tells the server what to run:
198
+ changing which loaders a route declares, its `loading` or its transition
199
+ counts as a server change; changing the components it renders does not. Under
200
+ a host router, apps are independent when the host mounts them lazily
201
+ (`.lazy(() => import(...))`); routers one module imports statically share
202
+ that module's code.
203
+
204
+ The key families: `CFCacheStore` keys segment entries and `fn:` items with the
205
+ data version and `doc:` responses and `shell2:` shells with the document
206
+ version. `VercelCacheStore` does the same by default (families `s`, `i` data;
207
+ `r`, `h` document). `MemorySegmentCacheStore` has no version; the process is its
208
+ scope.
209
+
210
+ Tag invalidation markers are stored without a version, so `updateTag()` and
211
+ `revalidateTag()` apply to entries of every version, including a version that
212
+ comes back in a rollback.
213
+
214
+ - **Force a clear:** change `version` (`createRouter({ version })`,
215
+ `createRSCHandler({ version })`, `new CFCacheStore({ version })` or
216
+ `new VercelCacheStore({ version })` all use that exact value for both
217
+ versions), or invalidate tags.
218
+ - **Keep the old "every deploy clears the cache" behavior:** set `version` to a
219
+ per-deploy value (a build id), or on Vercel keep a deployment-scoped
220
+ `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`.
221
+ - **Separate environments:** two deployments built from the same code have the
222
+ same versions. If they share a store (the same KV namespace), they share
223
+ entries. Give each environment its own KV namespace or cache namespace, or its
224
+ own `version`.
225
+
226
+ Two things give a router a new version on every build, and with it a cold
227
+ cache on every deploy:
228
+
229
+ - **No stable encryption key.** The key that encrypts inline server-action
230
+ bound arguments is part of the version of every router whose code encrypts
231
+ with it. Pass `rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY })`
232
+ (see `/use-cache`); the build prints a note when a router needs it.
233
+ - **A `Prerender` or `Static()` handler whose output differs per build**: a
234
+ timestamp, a random id, or an inline action with bound arguments rendered at
235
+ build time (its arguments are encrypted with a random IV, stable key or not).
236
+ - **An import the server build leaves external and cannot read**: a package
237
+ that is not in `node_modules` at build time, a path that does not resolve, a
238
+ URL. The build names it.
239
+
240
+ Every build prints each router's versions and writes what they were computed
241
+ from to `node_modules/.rangojs-router-build/cache-versions.json`. Diff that
242
+ file between two builds to see which chunk or payload moved a version; its
243
+ `unownedFiles` lists the server files in no router's version (the host entry
244
+ of lazily mounted apps), where a change keeps every app's cache.
245
+
246
+ A custom persistent store keys the same way with `getCacheVersions()` from
247
+ `@rangojs/router/cache`, called per operation inside the request: `data` for
248
+ segment entries and items, `document` for responses and shells, no version on
249
+ its tag-invalidation records. See `/caching` for store setup.
187
250
 
188
251
  ### Client cache: forward/back is mutation-aware
189
252
 
@@ -226,8 +289,9 @@ recompute for a merely-aging entry.
226
289
  `/document-cache`. A platform CDN may independently consume the same header
227
290
  and bypass the function on hits; see `/deployment-caching`.
228
291
 
229
- SWR softens normal TTL expiry, **not** a cross-deploy cold cache — a new build
230
- has no stale entry to serve (see version-segmented store keys above).
292
+ SWR softens normal TTL expiry, **not** a version change — a deploy that changes
293
+ a router's code reads under a new version and has no stale entry to serve (see
294
+ version-segmented store keys above).
231
295
 
232
296
  Store support is layer-specific. `CFCacheStore` and `VercelCacheStore` support
233
297
  SWR for segment, document/response, item (`"use cache"` and cached loaders), and
@@ -238,25 +302,26 @@ local/dev behavior, not as proof that segment SWR is active.
238
302
 
239
303
  ## Key Differences
240
304
 
241
- | | `cache()` DSL | `"use cache"` directive |
242
- | -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------- |
243
- | **Scope** | Route segment tree (handler + children + parallels) | Single function return value |
244
- | **Defined at** | Route definition site (`urls.ts`) | Inside function body or at file top |
245
- | **Cache key** | Request type + host + pathname + params + search | Function identity + serialized non-tainted args |
246
- | **Execution on hit** | All-or-nothing: entire handler skipped | Partial: function body skipped, calling code runs |
247
- | **Runtime control** | `condition` to disable, custom `key` function | None — if the directive is present, it caches |
248
- | **Side effects** | Response side effects throw inside the boundary | `ctx.headers.set()`, `ctx.set()`, etc. throw |
249
- | **Handle data** | Handler pushes replayed; loader pushes re-run live | Captured and replayed when it receives `ctx` |
250
- | **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
251
- | **Nesting** | Inner TTLs override; `key()` partitions compose | Compose by calling cached functions from uncached; inner tags reach the outer entry |
305
+ | | `cache()` DSL | `"use cache"` directive |
306
+ | -------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------- |
307
+ | **Scope** | Route segment tree (handler + children + parallels) | Single function return value |
308
+ | **Defined at** | Route definition site (`urls.ts`) | Inside function body or at file top |
309
+ | **Cache key** | Request type + router + host + pathname + params + search | Function identity + serialized non-tainted args |
310
+ | **Execution on hit** | All-or-nothing: entire handler skipped | Partial: function body skipped, calling code runs |
311
+ | **Runtime control** | `condition` to disable, custom `key` function | None — if the directive is present, it caches |
312
+ | **Side effects** | Response side effects throw inside the boundary | `ctx.headers.set()`, `ctx.set()`, etc. throw |
313
+ | **Handle data** | Handler pushes replayed; loader pushes re-run live | Captured and replayed when it receives `ctx` |
314
+ | **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
315
+ | **Nesting** | Inner TTLs override; `key()` partitions compose | Compose by calling cached functions from uncached; inner tags reach the outer entry |
252
316
 
253
317
  ### cache() Cache Key
254
318
 
255
- The default key is `{requestType}:{host}{pathname}[:params][?search]`, where
256
- requestType is `doc`, `partial`, or `intercept`. The same URL is therefore cached
257
- separately for full document loads, client navigations, and intercept
319
+ The default key is `{requestType}:{routerId}@{host}{pathname}[:params][?search]`,
320
+ where requestType is `doc`, `partial`, or `intercept`. The same URL is therefore
321
+ cached separately for full document loads, client navigations, and intercept
258
322
  navigations. The host keeps tenants apart when one deployment serves several
259
- domains; the search part is sorted, excludes the router's internal params, and
323
+ domains, and the router id keeps two routers on one store apart when both
324
+ serve the same host and path (see `/host-router`, "Shared cache store"); the search part is sorted, excludes the router's internal params, and
260
325
  honors `createRouter({ cache: { searchParams } })`.
261
326
 
262
327
  A custom `key` function replaces the whole default key (e.g., to key by user role
@@ -280,8 +345,8 @@ The key is `use-cache:{functionId}:{serializedArgs}` where functionId is a stabl
280
345
  ID from the Vite transform (module path + export name) and args are serialized
281
346
  (stable JSON when every arg is JSON-safe, RSC `encodeReply()` otherwise).
282
347
  Request-scoped arguments are excluded: route fields read off `ctx` (handler,
283
- loader or middleware: host, route name, pathname, params, search) and a
284
- `Request`'s URL are folded in, and `env`
348
+ loader or middleware: router id, host, route name, pathname, params, search) and
349
+ a `Request`'s URL are folded in, and `env`
285
350
  is left out. React element slots stay out of the key; any other argument that
286
351
  cannot be serialized (a function, a class instance) runs the call uncached. See
287
352
  `/use-cache`.
@@ -164,9 +164,10 @@ cache(
164
164
  );
165
165
  ```
166
166
 
167
- - **Default key**: `{doc|partial|intercept}:{host}{pathname}[:params][?search]`
167
+ - **Default key**: `{doc|partial|intercept}:{routerId}@{host}{pathname}[:params][?search]`
168
168
  — document requests, client navigations, and intercept navigations are cached
169
- separately. The search part honors `cache.searchParams` (below).
169
+ separately, and so are two routers that share a store (see `/host-router`,
170
+ "Shared cache store"). The search part honors `cache.searchParams` (below).
170
171
  - **`key`** is a full override: it bypasses the default key, the store's
171
172
  `keyGenerator`, and the search-param filter. The result is stored
172
173
  namespaced (`key:` plus its URI encoding), so whatever it returns, request
@@ -300,8 +301,13 @@ is not stored after it. A `"use cache"` call, a stale entry's background
300
301
  refresh, a loader's own `cache()`, a route `cache()` render or a
301
302
  document-cache render that read its data before one of its tags was
302
303
  invalidated still returns what it read, but its store write is skipped:
303
- every store stamps an entry when it is written, so the old value would be
304
- served as newer than the invalidation. The next read runs it again. A
304
+ the old value would be served as newer than the invalidation. The next read
305
+ runs it again. A write that gets through anyway (another colo's marker not
306
+ visible yet) is still rejected on later reads by `CFCacheStore` with KV, which
307
+ stamps an entry with the start of the execution that produced it, not its
308
+ write time; `MemorySegmentCacheStore` refuses such a write. `VercelCacheStore`
309
+ and `CFCacheStore` L1 hits in purge mode or without KV read no marker for
310
+ data, so the write gate is their only guard across requests. A
305
311
  `"use cache"` call already running when one of its tags is invalidated is
306
312
  not joined by later calls either. Its tags include those of the
307
313
  `"use cache"` functions it calls. Another isolate's invalidation is caught
@@ -523,6 +529,24 @@ already expressible with `cache({ key })`.
523
529
  | `CFCacheStore` | `@rangojs/router/cache` | Cloudflare Workers (Cache API L1 + optional KV L2) |
524
530
  | `VercelCacheStore` | `@rangojs/router/cache` | Vercel Functions (Vercel Runtime Cache) |
525
531
 
532
+ ### Filling a cache before traffic
533
+
534
+ Every cache above fills from the first request that misses. To pay that
535
+ render before a visitor does (after a deploy, after an `updateTag()`), warm
536
+ the URL: `router.prerender({ env, ctx })(url)` sends it through the router's
537
+ handler in a mode where every cache read misses and every write replaces the
538
+ entry, so the route's `ppr` shell, `cache()` records, `"use cache"` results,
539
+ loader caches and document cache entry are rewritten under the keys a
540
+ visitor reads, with the old entries serving until then. See `/prerender` →
541
+ "Warm any route before traffic".
542
+
543
+ A warm only writes to a store that is shared beyond the place the call runs.
544
+ Each store says where its entries can be read (`scope`): `CFCacheStore` with
545
+ `kv` is `"global"`, without `kv` `"local"`; `VercelCacheStore` is `"regional"`
546
+ (a warm fills the region it runs in); `MemorySegmentCacheStore` is `"local"`
547
+ (refused in production, allowed under the dev server; a single-process server
548
+ opts in with `new MemorySegmentCacheStore({ scope: "global" })`).
549
+
526
550
  ### Memory Store
527
551
 
528
552
  For development, tests, and single-instance deployments. Entries live in
@@ -535,6 +559,7 @@ const store = new MemorySegmentCacheStore({
535
559
  defaults: { ttl: 60 }, // segments ignore swr on this store
536
560
  maxEntries: 1000, // per-family FIFO cap (default 1000)
537
561
  name: "app", // optional: keep entries across Vite HMR module reloads
562
+ scope: "local", // default; "global" only when ONE process serves the app (see /prerender warming)
538
563
  });
539
564
  ```
540
565
 
@@ -564,9 +589,13 @@ const router = createRouter<AppBindings>({
564
589
  });
565
590
  ```
566
591
 
567
- `CFCacheStore` prefixes every key with the build version, so a deploy never
568
- reads the previous build's entries (override with `version`). See `/cache-guide`
569
- → "Cross-deploy safety" and `/cloudflare` for bindings.
592
+ `CFCacheStore` prefixes every key with a version computed per `createRouter()`
593
+ from that router's built code: segment entries and `fn:` items with the data
594
+ version, `doc:` responses and `shell2:` shells with the document version. A
595
+ deploy that changes a router's code reads under a new version; a deploy that does
596
+ not keeps the entries. Tag invalidation markers carry no version, so `updateTag()`
597
+ reaches entries of every version. `version` replaces both versions with one
598
+ value. See `/cache-guide` → "Cross-deploy safety" and `/cloudflare` for bindings.
570
599
 
571
600
  ### With KV L2 Persistence
572
601
 
@@ -755,10 +784,11 @@ the store adds SWR, tag invalidation, and the family split on top of it.
755
784
  import { getCache, waitUntil } from "@vercel/functions";
756
785
  import { VercelCacheStore } from "@rangojs/router/cache";
757
786
 
758
- // A per-deploy namespace: Vercel does not reconcile entries across deploys.
787
+ // No deployment id in the namespace: the store versions its keys itself, so a
788
+ // deploy that does not change a router keeps that router's entries.
759
789
  // One handle per process: the store keeps its PPR shell and tag-marker
760
790
  // memos per handle.
761
- const runtimeCache = getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID });
791
+ const runtimeCache = getCache();
762
792
 
763
793
  const router = createRouter({
764
794
  document: Document,
@@ -794,6 +824,12 @@ beyond the per-item cap are dropped with a warning. The official client swallows
794
824
  `expireTag` failures, so treat `updateTag()` on Vercel as best-effort. See
795
825
  `/vercel` for the preset and the remaining options (`version`, `name`, `debug`).
796
826
 
827
+ `VercelCacheStore` versions its keys by default: families `s` and `i` take the
828
+ router's data version, `r` and `h` its document version, and tag markers
829
+ (`rg:tm:{tag}`) take none. `version` replaces both. To clear the cache on every
830
+ deploy, set `version` to a per-deploy value or keep a deployment-scoped
831
+ `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`.
832
+
797
833
  ## Cache purity & tainted objects
798
834
 
799
835
  A `cache()` boundary caches everything except loaders, so anything read inside a
@@ -1083,6 +1119,17 @@ A per-boundary store becomes reachable by `updateTag()`/`revalidateTag()` only
1083
1119
  once that boundary has been matched in the current process. For data you
1084
1120
  invalidate by tag, prefer the app-level store.
1085
1121
 
1122
+ A store you implement declares where its entries can be read, or
1123
+ `router.prerender()` refuses to warm into it (a store that declares nothing is
1124
+ treated as `"local"`):
1125
+
1126
+ ```typescript
1127
+ const store: SegmentCacheStore = {
1128
+ scope: "global", // every location reads the same entries; or "regional"
1129
+ // get, set, delete, ...
1130
+ };
1131
+ ```
1132
+
1086
1133
  If you implement `SegmentCacheStore` yourself, `invalidateTags(tags)` is called
1087
1134
  synchronously inside the invalidating request, and `revalidateTag()` does not
1088
1135
  await it. For that request to read its own writes, record the tags and the
@@ -185,7 +185,7 @@
185
185
  },
186
186
  {
187
187
  "name": "prerender",
188
- "description": "Pre-render route segments at build time with Prerender (with getParams), Passthrough live fallback, Static segments, and Skip. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, reading build-only data (files, build env), or deciding which routes to prerender vs render live.",
188
+ "description": "Pre-render route segments at build time with Prerender (with getParams), Passthrough live fallback, Static segments, and Skip, refresh them from a running app with on-demand prerender (Prerender onDemand + router.prerender, ISR-style), and warm any other route's runtime caches (ppr shell, cache(), \"use cache\", document cache) before traffic with the same router.prerender. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, reading build-only data (files, build env), refreshing one page from a webhook, cron or queue without a redeploy, removing a deleted item's page (prerender.remove, or notFound() in a refresh), making routes ready after a deploy or an updateTag so no visitor pays the first render, or deciding which routes to prerender vs render live.",
189
189
  "argumentHint": "[passthrough]",
190
190
  "path": "skills/prerender/SKILL.md"
191
191
  },
@@ -376,6 +376,22 @@ a hard load of the target URL renders the full route.
376
376
  throwers in the client `ErrorBoundary`; `useFetchLoader` works unchanged
377
377
  (route-independent lane; `revalidate()` deliberately not consulted).
378
378
  Pinned in the router repo's client-urls hook-probe e2e.
379
+ - **Location state follows the same split as the route hooks.** Inside the
380
+ optimistic branch (the destination and the group's client layouts)
381
+ `useLocationState` reads the state the navigation carries (`<Link state>`,
382
+ `router.push(url, { state })`) from the first optimistic render, before the
383
+ server answers: a product page can show the name its link carried while
384
+ its loader streams. Chrome outside the group, and content a navigation
385
+ keeps on screen (a same-route navigation, a destination that suspends with
386
+ no boundary), keep the committed entry's state until the canonical commit.
387
+ State a redirect carries arrives with that commit (an action's
388
+ `setLocationState` without a redirect is applied when its response
389
+ arrives), and a navigation that is superseded or cancelled leaves none
390
+ behind. A `flash` slot read in the branch is cleared from history once the
391
+ navigation commits, not before. In the branch the value is the object the
392
+ navigation passed; from the commit on it is the entry's copy (a structured
393
+ clone), so an object-valued slot is a new, equal object at that point.
394
+ Do not key an effect on its identity across the two.
379
395
  - **Editing the module in dev:** projections refresh via HMR discovery; if a
380
396
  route-shape edit ever serves stale routes, restart dev — and if you are
381
397
  developing the router itself, rebuild the router dist before `pnpm dev`
@@ -48,7 +48,8 @@ export default defineConfig({
48
48
  Worker runs the RSC entry and loads SSR inside the same isolate.
49
49
 
50
50
  `buildEnv: "auto"` gives `Prerender` and `Static` handlers a real `ctx.env` at
51
- build time through Wrangler's `getPlatformProxy()` (local bindings). Omit it
51
+ build time through Wrangler's `getPlatformProxy()` (local bindings), reading
52
+ the wrangler config from the Vite root whatever the working directory. Omit it
52
53
  (default `false`) when build-time rendering must not touch bindings; `ctx.env`
53
54
  then throws during build rendering. See `/prerender`.
54
55
 
@@ -183,6 +184,137 @@ keeps serving a purged shell until `memo.shellMs` passes. Set
183
184
  user's next request; cross-colo KV propagation (and `tagCacheTtl`) still
184
185
  applies. Details: `/caching` → "The fresh-reads cookie".
185
186
 
187
+ ## On-demand prerender on Workers
188
+
189
+ A `Prerender(..., { onDemand })` route (`/prerender` → "On-demand refresh")
190
+ needs a durable store for its refreshed entries. On Workers that is KV:
191
+
192
+ ```typescript
193
+ import { createKVPrerenderStore } from "@rangojs/router/prerender/cloudflare";
194
+
195
+ export const router = createRouter<AppBindings>({
196
+ document: Document,
197
+ urls: urlpatterns,
198
+ prerender: (env) => ({
199
+ store: createKVPrerenderStore(env.PRERENDER_KV),
200
+ ttl: 3600, // soft: a stale entry still serves
201
+ // Present = stale-while-revalidate: a stale hit schedules this via waitUntil.
202
+ onRevalidate: (target, liveEnv) => liveEnv.PRERENDER_QUEUE.send({ target }),
203
+ }),
204
+ });
205
+ ```
206
+
207
+ Add the `PRERENDER_KV` namespace (and the queue, if you use one) to
208
+ `wrangler.jsonc` and to `AppBindings`. Refreshes run from the Worker's other
209
+ handlers, with the live `env`:
210
+
211
+ ```typescript
212
+ export default {
213
+ fetch: (request: Request, env: AppBindings, ctx: ExecutionContext) =>
214
+ router.fetch(request, { env, ctx }),
215
+
216
+ // Cron sweep: re-render only what went stale.
217
+ async scheduled(
218
+ _event: ScheduledEvent,
219
+ env: AppBindings,
220
+ ctx: ExecutionContext,
221
+ ) {
222
+ const ids = await env.CMS.listProductIds();
223
+ const prerender = router.prerender({ env, ctx });
224
+ ctx.waitUntil(
225
+ prerender.many(
226
+ ids.map((id) => ({ route: "products.detail", params: { id } })),
227
+ { concurrency: 4, onlyIfStale: true },
228
+ ),
229
+ );
230
+ },
231
+
232
+ // Queue consumer: the queue owns dedup across isolates. onlyIfStale: a
233
+ // message can run after another one refreshed the page, or after
234
+ // prerender.remove() removed it; then it renders nothing.
235
+ async queue(
236
+ batch: MessageBatch<{ target: PrerenderTargetObject }>,
237
+ env: AppBindings,
238
+ ctx: ExecutionContext,
239
+ ) {
240
+ const targets = batch.messages.map((m) => m.body.target);
241
+ ctx.waitUntil(
242
+ router
243
+ .prerender({ env, ctx })
244
+ .many(targets, { concurrency: 8, onlyIfStale: true }),
245
+ );
246
+ },
247
+ };
248
+ ```
249
+
250
+ `PrerenderTargetObject` comes from `@rangojs/router/prerender`. It is what
251
+ `onRevalidate` receives, and the `router.prerender({ env, ctx })` runner (and its
252
+ `.many()`) accepts it as is on a router typed with named routes. A queue hands it back as JSON, so type the
253
+ message body with it (as above) or assert it: `JSON.parse(raw) as
254
+ PrerenderTargetObject`.
255
+
256
+ The KV store writes no `expirationTtl` (entries never expire; `ttl` is soft)
257
+ and keys entries by router id and the router's data version, so a deploy that
258
+ changes server code starts from the build entries again.
259
+ `router.prerender({ env, ctx }).markStale(tags)` writes per-tag markers under
260
+ `__rango_pr_tag__/{encoded routerId}/`, a namespace separate from `updateTag()`'s and
261
+ scoped to the router that marked (routers sharing one KV namespace do not mark
262
+ each other's entries); a read checks one marker per tag on the entry,
263
+ uncached, so keep tag counts small. Markers have no expiry (`/prerender` ->
264
+ "KV tag markers"). Neither `updateTag()` nor `revalidateTag()` reaches this
265
+ store.
266
+
267
+ ## Warm routes on Workers
268
+
269
+ `router.prerender()` also warms a route that is not on-demand: its `ppr`
270
+ shell, `cache()` records, `"use cache"` results and document cache entry are
271
+ rewritten before a visitor asks (`/prerender` → "Warm any route before
272
+ traffic"). On Workers:
273
+
274
+ - **`CFCacheStore` needs `kv`.** With KV the store is shared by every colo
275
+ (`scope: "global"`) and a warm is allowed; without it entries live in one
276
+ colo's Cache API and the call returns `skipped-store-not-shared`.
277
+ - **Give `scheduled` and `queue` an origin.** Cache keys carry the host, and
278
+ those handlers have no request to take it from:
279
+
280
+ ```typescript
281
+ async scheduled(_event: ScheduledEvent, env: AppBindings, ctx: ExecutionContext) {
282
+ const prerender = router.prerender({
283
+ env,
284
+ ctx,
285
+ origin: "https://shop.example",
286
+ });
287
+ ctx.waitUntil(prerender.many(["/", "/products/1"], { onlyIfStale: true }));
288
+ },
289
+ ```
290
+
291
+ A webhook route or a server action needs none: the warm uses the origin of
292
+ the request it is called from.
293
+
294
+ - **A warm fills the colo it runs in, and KV.** Another colo that already
295
+ holds its own Cache API copy keeps serving it until it expires; an L1 hit
296
+ never consults KV. After a deploy no colo holds a copy, so the warm's KV
297
+ entry is what each one reads next. For a content change, invalidate and then
298
+ warm, in this order:
299
+
300
+ ```typescript
301
+ await updateTag(`product:${id}`); // no colo serves the old copy any more
302
+ await router.prerender({ env, ctx })(`/products/${id}`); // writes the new one
303
+ ```
304
+
305
+ A colo with no copy of its own reads the warmed entry from KV. A colo that
306
+ was still holding the old copy treats it as a miss and renders once for
307
+ itself (it does not read the fresher KV entry). With `tagPurge` the purge
308
+ evicts every colo's copy, so they all read the warmed entry.
309
+
310
+ - **Entries under 60 seconds stay in one colo.** KV's minimum `expirationTtl`
311
+ is 60 s, so a segment, item or response entry whose `ttl + swr` is shorter
312
+ is written to the Cache API only, and a warm of it reaches the calling colo
313
+ alone. Shells always reach KV.
314
+ - **KV writes take time to spread.** Cloudflare documents up to 60 seconds or
315
+ more before a write is visible in other locations; a visitor elsewhere in
316
+ that window renders the page as before.
317
+
186
318
  ## Tracing on Workers
187
319
 
188
320
  `createCloudflareTracing()` from `@rangojs/router/cloudflare` emits the
@@ -57,7 +57,7 @@ and multi-region composition without replacing its routing or data model.
57
57
  | Middleware | Global + segment-scoped subtree | Single root Proxy (matcher filters paths); no subtree scope | Request/server-function middleware | [Hono middleware + handler interceptors](https://waku.gg/#interceptors) |
58
58
  | Observability | Built-in CF + Vercel OTel phase spans, `Server-Timing`, perf waterfall | Built-in OTel spans; no equivalent router-phase waterfall | Client/data devtools | No equivalent built-in phase model |
59
59
  | Deploy targets | Node, Cloudflare, Vercel — all with presets | Node/Vercel first; other platforms through adapters | Broad Vite/Rsbuild runtime support | Node plus adapters |
60
- | Deployment skew | Automatic build-version handshake for navigation, prefetch, and actions; safe reload on mismatch | `deploymentId` mismatch reload; Vercel can pin clients to an immutable deploy | Application/platform policy | Build-id mismatch reload (v1.0-beta) |
60
+ | Deployment skew | Automatic version handshake for navigation, prefetch, and actions; safe reload on mismatch | `deploymentId` mismatch reload; Vercel can pin clients to an immutable deploy | Application/platform policy | Build-id mismatch reload (v1.0-beta) |
61
61
  | Multi-tenant | `createHostRouter()` built-in | Roll your own | Roll your own | Roll your own |
62
62
  | Testing primitives | Ships `@rangojs/router/testing` (handlers/loaders/mw/Flight/e2e) | No comparable handler/Flight primitives | Documented patterns; no shipped utils | No comparable handler/Flight primitives |
63
63
  | Client runtime | ~50 KB Rango + ~115 KB React/RSC, per-route chunks | Configuration-dependent | Configuration-dependent | Deliberately light |
@@ -336,8 +336,9 @@ first-class primitive.
336
336
 
337
337
  ### Deployment skew is detected before stale code executes
338
338
 
339
- The Vite plugin generates a build version and injects it into the RSC handler and
340
- initial payload. The browser returns that version on every partial navigation,
339
+ `vite build` computes a document version per `createRouter()` from that router's
340
+ built code (the server code, the SSR output, the client asset file names, `base`,
341
+ and its Prerender payloads) and ships it in the initial payload metadata. The browser returns that version on every partial navigation,
341
342
  prefetch, and Server Action. If an old tab reaches a newer server, request
342
343
  classification detects the mismatch before resolving the route or executing the
343
344
  action and responds with `X-RSC-Reload`. The browser then performs a clean document
@@ -348,13 +349,16 @@ falling through to a misleading 404.
348
349
  The protection covers fresh, completed-prefetch, and in-flight-prefetch responses.
349
350
  For action requests, the reload returns to the same-origin referrer rather than the
350
351
  internal action URL. The cache side follows the same correctness rule:
351
- `CFCacheStore` automatically versions its physical Cache API and KV keys, so a new
352
- build cannot replay Flight containing an old component shape or dead client-chunk
353
- reference. `VercelCacheStore` exposes the same version segmentation, but — unlike
354
- `CFCacheStore` — does not default `version` to the build version: skew safety is
355
- opt-in via the `version` option or the recommended deployment-specific Runtime
356
- Cache namespace. The browser's Rango state also includes
357
- the build version, rotating HTTP and in-memory prefetch cache identity on boot.
352
+ `CFCacheStore` and `VercelCacheStore` automatically version their physical keys
353
+ (Cache API and KV keys on Cloudflare): cached RSC data (segment entries, `"use
354
+ cache"` values, loader data) by a data version, a hash of the router's server
355
+ code, and stored HTML (PPR shells, document-cache responses) by the document
356
+ version. A build whose router code changed cannot replay Flight containing an old
357
+ component shape or dead client-chunk reference, and a rebuild of unchanged code
358
+ keeps its cache. Tag invalidation markers carry no version, so `updateTag()`
359
+ reaches entries of every version. The `version` option replaces both versions.
360
+ The browser's Rango state also includes the document version, rotating HTTP and
361
+ in-memory prefetch cache identity on boot.
358
362
 
359
363
  Next.js deserves explicit credit here: its
360
364
  [`deploymentId`](https://nextjs.org/docs/app/api-reference/config/next-config-js/deploymentId)
@@ -587,10 +591,10 @@ dropped rather than warmed. `useLinkStatus()` exposes `{ pending }` for the owni
587
591
  link.
588
592
 
589
593
  **Rango State ties prefetch and client-cache invalidation together.** It is a
590
- session-cookie value shaped as `{buildVersion}:{invalidationTimestamp}`. Navigation
594
+ session-cookie value shaped as `{documentVersion}:{invalidationTimestamp}`. Navigation
591
595
  and prefetch requests send it as `X-Rango-State`, and RSC responses vary on that
592
596
  header, so the browser HTTP cache and Rango's decoded prefetch map share one cache
593
- identity. A deployment changes the version; a mutation rotates the timestamp. Old
597
+ identity. A deployment that changes the router's code or client assets changes the version; a mutation rotates the timestamp. Old
594
598
  responses become unreachable under the retired identity instead of requiring every
595
599
  cache layer to delete the same entries successfully.
596
600
 
@@ -751,7 +755,7 @@ applications do not have to remember to mark every context manually.
751
755
 
752
756
  For a `"use cache"` function, a tainted context object is not serialized wholesale
753
757
  into the cache key. Rango instead extracts the safe identity dimensions that make
754
- the call reusable without collisions: host, route name, pathname, params, response
758
+ the call reusable without collisions: router, host, route name, pathname, params, response
755
759
  type, and normalized user-facing search params. A shared cached function called
756
760
  from two hosts, route scopes, param sets, or query variants therefore does not
757
761
  collapse into one entry just because both calls received a `ctx` object.