@rangojs/router 0.21.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 (243) hide show
  1. package/dist/bin/rango.js +13 -1
  2. package/dist/types/browser/event-controller.d.ts +30 -13
  3. package/dist/types/browser/navigation-store.d.ts +9 -0
  4. package/dist/types/browser/partial-update.d.ts +2 -1
  5. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  6. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  7. package/dist/types/browser/react/context.d.ts +14 -0
  8. package/dist/types/browser/rsc-router.d.ts +13 -2
  9. package/dist/types/browser/types.d.ts +10 -0
  10. package/dist/types/build/generate-manifest.d.ts +2 -0
  11. package/dist/types/cache/background-task.d.ts +22 -0
  12. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  13. package/dist/types/cache/cache-policy.d.ts +17 -0
  14. package/dist/types/cache/cache-scope.d.ts +1 -1
  15. package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
  16. package/dist/types/cache/document-cache.d.ts +8 -5
  17. package/dist/types/cache/handle-snapshot.d.ts +23 -0
  18. package/dist/types/cache/index.d.ts +1 -1
  19. package/dist/types/cache/invalidation-order.d.ts +14 -0
  20. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  21. package/dist/types/cache/shell-snapshot.d.ts +10 -1
  22. package/dist/types/cache/store-scope.d.ts +22 -0
  23. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  24. package/dist/types/cache/types.d.ts +63 -14
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
  26. package/dist/types/errors.d.ts +6 -0
  27. package/dist/types/handles/deferred-resolution.d.ts +7 -0
  28. package/dist/types/internal-suspense-audit.d.ts +1 -0
  29. package/dist/types/loader-store.d.ts +4 -8
  30. package/dist/types/outlet-provider.d.ts +2 -1
  31. package/dist/types/prerender/cloudflare.d.ts +49 -0
  32. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  33. package/dist/types/prerender/index.d.ts +12 -0
  34. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  35. package/dist/types/prerender/on-demand.d.ts +272 -0
  36. package/dist/types/prerender/producer-guard.d.ts +33 -0
  37. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  38. package/dist/types/prerender/warm-request.d.ts +65 -0
  39. package/dist/types/prerender/warm.d.ts +65 -0
  40. package/dist/types/prerender/writable-store.d.ts +170 -0
  41. package/dist/types/prerender.d.ts +27 -0
  42. package/dist/types/reverse.d.ts +1 -1
  43. package/dist/types/route-content-wrapper.d.ts +3 -1
  44. package/dist/types/router/error-handling.d.ts +2 -0
  45. package/dist/types/router/handler-context.d.ts +1 -1
  46. package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
  47. package/dist/types/router/pattern-matching.d.ts +2 -0
  48. package/dist/types/router/prefetch-default.d.ts +4 -4
  49. package/dist/types/router/prerender-match.d.ts +23 -1
  50. package/dist/types/router/route-trie-builder.d.ts +8 -1
  51. package/dist/types/router/router-context.d.ts +1 -0
  52. package/dist/types/router/router-interfaces.d.ts +30 -0
  53. package/dist/types/router/router-options.d.ts +24 -0
  54. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
  56. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  57. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  58. package/dist/types/router/trie-matching.d.ts +2 -0
  59. package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
  60. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  61. package/dist/types/rsc/shell-capture.d.ts +14 -1
  62. package/dist/types/rsc/shell-serve.d.ts +2 -2
  63. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  64. package/dist/types/rsc/types.d.ts +8 -0
  65. package/dist/types/segment-boundary-content.d.ts +17 -0
  66. package/dist/types/segment-loader-promise.d.ts +2 -6
  67. package/dist/types/server/build-version-table.d.ts +9 -0
  68. package/dist/types/server/context.d.ts +12 -4
  69. package/dist/types/server/handle-store.d.ts +2 -0
  70. package/dist/types/server/request-context.d.ts +63 -2
  71. package/dist/types/suspense-audit-react.d.ts +26 -0
  72. package/dist/types/suspense-audit.d.ts +157 -0
  73. package/dist/types/testing/action-form.d.ts +34 -0
  74. package/dist/types/testing/flight.entry.d.ts +2 -0
  75. package/dist/types/testing/index.d.ts +4 -0
  76. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  77. package/dist/types/testing/serve-shell-request.d.ts +34 -4
  78. package/dist/types/testing/shell-status.d.ts +14 -6
  79. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  80. package/dist/types/thenable-status.d.ts +6 -0
  81. package/dist/types/types/handler-context.d.ts +7 -0
  82. package/dist/types/types/route-entry.d.ts +5 -0
  83. package/dist/types/urls/path-helper.d.ts +18 -0
  84. package/dist/types/vite/discovery/state.d.ts +11 -0
  85. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  86. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  87. package/dist/vite/index.js +179 -496
  88. package/package.json +12 -2
  89. package/skills/cache-guide/SKILL.md +18 -17
  90. package/skills/caching/SKILL.md +40 -4
  91. package/skills/catalog.json +1 -1
  92. package/skills/cloudflare/SKILL.md +131 -0
  93. package/skills/comparison/references/framework-comparison.md +1 -1
  94. package/skills/deployment-caching/SKILL.md +33 -12
  95. package/skills/document-cache/SKILL.md +16 -14
  96. package/skills/hooks/SKILL.md +22 -0
  97. package/skills/hooks/handle-and-actions.md +3 -4
  98. package/skills/hooks/navigation.md +10 -0
  99. package/skills/hooks/state.md +5 -4
  100. package/skills/host-router/SKILL.md +18 -0
  101. package/skills/loader/SKILL.md +6 -4
  102. package/skills/middleware/SKILL.md +1 -1
  103. package/skills/migrate-nextjs/SKILL.md +82 -9
  104. package/skills/ppr/SKILL.md +31 -8
  105. package/skills/prerender/SKILL.md +645 -8
  106. package/skills/rango/SKILL.md +1 -0
  107. package/skills/response-routes/SKILL.md +5 -5
  108. package/skills/route/SKILL.md +29 -0
  109. package/skills/testing/SKILL.md +2 -1
  110. package/skills/testing/cache-prerender.md +18 -17
  111. package/skills/testing/serve-shell-request.md +104 -8
  112. package/skills/use-cache/SKILL.md +9 -5
  113. package/skills/vercel/SKILL.md +2 -0
  114. package/skills/view-transitions/SKILL.md +9 -4
  115. package/src/browser/event-controller.ts +69 -25
  116. package/src/browser/link-interceptor.ts +2 -2
  117. package/src/browser/navigation-bridge.ts +75 -35
  118. package/src/browser/navigation-store.ts +26 -0
  119. package/src/browser/navigation-transaction.ts +9 -2
  120. package/src/browser/network-error-handler.ts +2 -0
  121. package/src/browser/partial-update.ts +48 -12
  122. package/src/browser/prefetch/default-strategy.ts +22 -42
  123. package/src/browser/react/Link.tsx +37 -42
  124. package/src/browser/react/NavigationProvider.tsx +63 -3
  125. package/src/browser/react/context.ts +17 -0
  126. package/src/browser/react/location-state.ts +5 -19
  127. package/src/browser/react/use-action.ts +4 -1
  128. package/src/browser/react/use-handle.ts +4 -20
  129. package/src/browser/react/use-link-status.ts +4 -3
  130. package/src/browser/react/use-navigation.ts +4 -1
  131. package/src/browser/react/use-params.ts +4 -1
  132. package/src/browser/react/use-pathname.ts +4 -1
  133. package/src/browser/react/use-search-params.ts +5 -3
  134. package/src/browser/react/use-segments.ts +36 -54
  135. package/src/browser/rsc-router.tsx +79 -39
  136. package/src/browser/segment-reconciler.ts +3 -3
  137. package/src/browser/server-action-bridge.ts +26 -6
  138. package/src/browser/types.ts +11 -0
  139. package/src/build/generate-manifest.ts +17 -0
  140. package/src/build/merge-full-manifests.ts +5 -0
  141. package/src/build/route-trie.ts +1 -0
  142. package/src/cache/background-task.ts +61 -0
  143. package/src/cache/cache-key-utils.ts +91 -12
  144. package/src/cache/cache-policy.ts +11 -2
  145. package/src/cache/cache-runtime.ts +23 -23
  146. package/src/cache/cache-scope.ts +38 -36
  147. package/src/cache/cf/cf-cache-store.ts +16 -11
  148. package/src/cache/document-cache.ts +35 -14
  149. package/src/cache/handle-snapshot.ts +50 -0
  150. package/src/cache/index.ts +1 -0
  151. package/src/cache/invalidation-order.ts +53 -0
  152. package/src/cache/memory-segment-store.ts +52 -1
  153. package/src/cache/shell-snapshot.ts +17 -0
  154. package/src/cache/store-scope.ts +36 -0
  155. package/src/cache/tag-invalidation.ts +7 -5
  156. package/src/cache/types.ts +69 -14
  157. package/src/cache/vercel/vercel-cache-store.ts +20 -6
  158. package/src/client.tsx +40 -3
  159. package/src/errors.ts +13 -6
  160. package/src/handles/deferred-resolution.ts +7 -2
  161. package/src/internal-suspense-audit.ts +12 -0
  162. package/src/loader-store.ts +4 -9
  163. package/src/outlet-provider.tsx +25 -1
  164. package/src/prerender/cloudflare.ts +130 -0
  165. package/src/prerender/create-prerender-trigger.ts +780 -0
  166. package/src/prerender/index.ts +39 -0
  167. package/src/prerender/memory-prerender-store.ts +115 -0
  168. package/src/prerender/on-demand.ts +339 -0
  169. package/src/prerender/producer-guard.ts +66 -0
  170. package/src/prerender/shell-manifest-key.ts +12 -17
  171. package/src/prerender/warm-request.ts +154 -0
  172. package/src/prerender/warm.ts +193 -0
  173. package/src/prerender/writable-store.ts +278 -0
  174. package/src/prerender.ts +29 -0
  175. package/src/reverse.ts +1 -1
  176. package/src/route-content-wrapper.tsx +107 -13
  177. package/src/router/error-handling.ts +12 -12
  178. package/src/router/find-match.ts +1 -0
  179. package/src/router/handler-context.ts +2 -0
  180. package/src/router/intercept-resolution.ts +15 -8
  181. package/src/router/loader-resolution.ts +2 -10
  182. package/src/router/match-middleware/background-revalidation.ts +2 -0
  183. package/src/router/match-middleware/cache-lookup.ts +230 -38
  184. package/src/router/pattern-matching.ts +14 -0
  185. package/src/router/prefetch-default.ts +4 -4
  186. package/src/router/prerender-match.ts +460 -352
  187. package/src/router/route-trie-builder.ts +9 -0
  188. package/src/router/router-context.ts +1 -0
  189. package/src/router/router-interfaces.ts +33 -0
  190. package/src/router/router-options.ts +30 -0
  191. package/src/router/segment-resolution/fresh.ts +20 -19
  192. package/src/router/segment-resolution/loader-cache.ts +15 -17
  193. package/src/router/segment-resolution/revalidation.ts +34 -24
  194. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  195. package/src/router/trie-matching.ts +3 -0
  196. package/src/router.ts +107 -1
  197. package/src/rsc/handler.ts +51 -4
  198. package/src/rsc/progressive-enhancement.ts +5 -1
  199. package/src/rsc/response-cache-serve.ts +10 -4
  200. package/src/rsc/rsc-rendering.ts +53 -11
  201. package/src/rsc/shell-build-manifest.ts +10 -8
  202. package/src/rsc/shell-capture-constants.ts +39 -7
  203. package/src/rsc/shell-capture.ts +108 -19
  204. package/src/rsc/shell-serve.ts +12 -6
  205. package/src/rsc/ssr-module-loader.ts +26 -0
  206. package/src/rsc/types.ts +8 -0
  207. package/src/segment-boundary-content.ts +25 -0
  208. package/src/segment-loader-promise.ts +28 -38
  209. package/src/segment-system.tsx +117 -57
  210. package/src/server/build-version-table.ts +15 -0
  211. package/src/server/context.ts +40 -17
  212. package/src/server/cookie-store.ts +5 -0
  213. package/src/server/handle-store.ts +13 -0
  214. package/src/server/request-context.ts +87 -1
  215. package/src/ssr/ssr-root.tsx +20 -13
  216. package/src/suspense-audit-react.tsx +98 -0
  217. package/src/suspense-audit.ts +1030 -0
  218. package/src/testing/action-form.ts +66 -0
  219. package/src/testing/flight-runtime.d.ts +27 -0
  220. package/src/testing/flight.entry.ts +3 -0
  221. package/src/testing/index.ts +19 -0
  222. package/src/testing/internal/server-modules.ts +32 -0
  223. package/src/testing/render-route.tsx +25 -12
  224. package/src/testing/serve-shell-request.ts +190 -34
  225. package/src/testing/shell-status.ts +17 -6
  226. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  227. package/src/thenable-status.ts +10 -0
  228. package/src/types/handler-context.ts +7 -0
  229. package/src/types/route-entry.ts +6 -0
  230. package/src/urls/path-helper.ts +69 -9
  231. package/src/use-loader.tsx +57 -30
  232. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  233. package/src/vite/discovery/discover-routers.ts +15 -0
  234. package/src/vite/discovery/shell-prerender-phase.ts +5 -5
  235. package/src/vite/discovery/state.ts +17 -1
  236. package/src/vite/inject-client-debug.ts +42 -20
  237. package/src/vite/plugins/virtual-entries.ts +8 -1
  238. package/src/vite/router-discovery.ts +14 -0
  239. package/src/vite/utils/bundle-analysis.ts +39 -7
  240. package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
  241. package/dist/types/segment-content-promise.d.ts +0 -13
  242. package/src/browser/react/subscribe-to-nothing.ts +0 -7
  243. 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.21.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": {
@@ -302,25 +302,26 @@ local/dev behavior, not as proof that segment SWR is active.
302
302
 
303
303
  ## Key Differences
304
304
 
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 + 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 |
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 |
316
316
 
317
317
  ### cache() Cache Key
318
318
 
319
- The default key is `{requestType}:{host}{pathname}[:params][?search]`, where
320
- requestType is `doc`, `partial`, or `intercept`. The same URL is therefore cached
321
- 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
322
322
  navigations. The host keeps tenants apart when one deployment serves several
323
- 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
324
325
  honors `createRouter({ cache: { searchParams } })`.
325
326
 
326
327
  A custom `key` function replaces the whole default key (e.g., to key by user role
@@ -344,8 +345,8 @@ The key is `use-cache:{functionId}:{serializedArgs}` where functionId is a stabl
344
345
  ID from the Vite transform (module path + export name) and args are serialized
345
346
  (stable JSON when every arg is JSON-safe, RSC `encodeReply()` otherwise).
346
347
  Request-scoped arguments are excluded: route fields read off `ctx` (handler,
347
- loader or middleware: host, route name, pathname, params, search) and a
348
- `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`
349
350
  is left out. React element slots stay out of the key; any other argument that
350
351
  cannot be serialized (a function, a class instance) runs the call uncached. See
351
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
 
@@ -1094,6 +1119,17 @@ A per-boundary store becomes reachable by `updateTag()`/`revalidateTag()` only
1094
1119
  once that boundary has been matched in the current process. For data you
1095
1120
  invalidate by tag, prefer the app-level store.
1096
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
+
1097
1133
  If you implement `SegmentCacheStore` yourself, `invalidateTags(tags)` is called
1098
1134
  synchronously inside the invalidating request, and `revalidateTag()` does not
1099
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
  },
@@ -184,6 +184,137 @@ keeps serving a purged shell until `memo.shellMs` passes. Set
184
184
  user's next request; cross-colo KV propagation (and `tagCacheTtl`) still
185
185
  applies. Details: `/caching` → "The fresh-reads cookie".
186
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
+
187
318
  ## Tracing on Workers
188
319
 
189
320
  `createCloudflareTracing()` from `@rangojs/router/cloudflare` emits the
@@ -755,7 +755,7 @@ applications do not have to remember to mark every context manually.
755
755
 
756
756
  For a `"use cache"` function, a tainted context object is not serialized wholesale
757
757
  into the cache key. Rango instead extracts the safe identity dimensions that make
758
- the call reusable without collisions: host, route name, pathname, params, response
758
+ the call reusable without collisions: router, host, route name, pathname, params, response
759
759
  type, and normalized user-facing search params. A shared cached function called
760
760
  from two hosts, route scopes, param sets, or query variants therefore does not
761
761
  collapse into one entry just because both calls received a `ctx` object.
@@ -20,11 +20,12 @@ To choose between the in-app layers themselves, start at `/cache-guide`.
20
20
  | `"use cache"` | one function result | yes | yes | caller, handlers, loaders, rendering |
21
21
  | `cache()` | serialized Flight segments | yes | yes | middleware, loaders, HTML rendering |
22
22
  | `Prerender()` | build-time Flight segments in the server bundle | yes | yes | middleware, loaders, HTML rendering |
23
+ | `Prerender(..., { onDemand })` | refreshed Flight segments in a writable store | yes | yes | middleware, loaders, HTML rendering |
23
24
  | `ppr` | HTML prelude + React postponed state | yes | **yes, before shell commit** | middleware, holes, hydration payload |
24
25
  | `createDocumentCacheMiddleware()` | complete response in the app cache store | yes | outer middleware runs; the hit skips its downstream pipeline | nothing downstream |
25
26
  | HTTP CDN cache (`s-maxage`) | complete HTTP response outside the app | **no** | **no** | nothing |
26
27
 
27
- The first five rows enter the app. The first four preserve the complete request
28
+ The first six rows enter the app. The first five preserve the complete request
28
29
  model; a document-cache middleware hit intentionally short-circuits its
29
30
  downstream pipeline. The last row is a platform cache: on a hit it serves bytes
30
31
  without invoking Rango at all.
@@ -71,12 +72,31 @@ Worker at the edge, middleware runs there, and PPR shell lookup/resume stays in
71
72
  that Worker. `CFCacheStore` supplies the app cache families; client assets are
72
73
  served separately as assets.
73
74
 
75
+ An on-demand prerender store (`/prerender` → "On-demand refresh") on Workers
76
+ is `createKVPrerenderStore(env.PRERENDER_KV)` from
77
+ `@rangojs/router/prerender/cloudflare`: refreshes run from `scheduled`,
78
+ `queue` or a webhook route, and every request to an on-demand route pays one
79
+ KV read plus one per tag on the entry.
80
+
81
+ The same `router.prerender()` call warms any other route's caches before
82
+ traffic, when `CFCacheStore` has `kv` (shared by every colo). It fills the
83
+ calling colo and KV; a colo that already holds its own copy keeps it until it
84
+ expires, so a content refresh is `updateTag()` and then the warm: no colo
85
+ serves the old copy, and a colo without one reads the warmed entry
86
+ (`/cloudflare` → "Warm routes on Workers").
87
+
74
88
  ### Vercel
75
89
 
76
90
  The Vercel preset emits static client assets plus one streaming Node Function.
77
91
  HTML, Flight, prerender payloads, and PPR shells are served from that function.
78
92
  `VercelCacheStore` uses Runtime Cache inside the function; Runtime Cache is not
79
- Vercel's CDN/ISR cache.
93
+ Vercel's CDN/ISR cache. Rango's on-demand prerender does not use Vercel ISR
94
+ either: it needs a durable `WritablePrerenderStore` shared by every function
95
+ instance, and v1 ships none for Vercel (the in-memory store is per process),
96
+ so a Vercel app writes a small get/set adapter over its own storage.
97
+ Warming a plain route (`/prerender` → "Warm any route before traffic") needs
98
+ no such store: it writes to `VercelCacheStore`, in the region the call runs
99
+ in.
80
100
 
81
101
  The preset deliberately emits no Vercel `.prerender-config.json`, response
82
102
  `chain`, or CDN-stitched resume function. Vercel's open-source Build Output
@@ -155,15 +175,16 @@ the app store, should own the complete response.
155
175
 
156
176
  ## Decision guide
157
177
 
158
- | Requirement | Choose |
159
- | --------------------------------------------------- | ------------------------------------------ |
160
- | Per-request auth or request shaping | in-function caching; never shared CDN HTML |
161
- | Stable shell with cart/session/live prices | `ppr` with live holes |
162
- | Build-known segments with live loaders | `Prerender()` |
163
- | Fully public response, whole-page TTL is acceptable | HTTP `s-maxage` + SWR |
164
- | Whole response reused inside the app store | `createDocumentCacheMiddleware()` |
165
- | One query or component is expensive | `"use cache"` |
166
- | One route subtree is expensive | `cache()` |
178
+ | Requirement | Choose |
179
+ | --------------------------------------------------- | ----------------------------------------------------- |
180
+ | Per-request auth or request shaping | in-function caching; never shared CDN HTML |
181
+ | Stable shell with cart/session/live prices | `ppr` with live holes |
182
+ | Build-known segments with live loaders | `Prerender()` |
183
+ | Prerendered page refreshed on a CMS edit, no deploy | `Prerender(..., { onDemand })` + `router.prerender()` |
184
+ | Fully public response, whole-page TTL is acceptable | HTTP `s-maxage` + SWR |
185
+ | Whole response reused inside the app store | `createDocumentCacheMiddleware()` |
186
+ | One query or component is expensive | `"use cache"` |
187
+ | One route subtree is expensive | `cache()` |
167
188
 
168
189
  For a large dynamic app, CDN-stitched PPR would mainly improve shell first-byte
169
190
  latency and avoid sending the prelude from the function. It would not remove the
@@ -175,7 +196,7 @@ provably public and shared.
175
196
  ## Related skills
176
197
 
177
198
  - `/ppr` — shell capture, holes, middleware commit point, invalidation
178
- - `/prerender` — build-time Flight segments and `Prerender + ppr`
199
+ - `/prerender` — build-time Flight segments, `Prerender + ppr`, and on-demand refresh
179
200
  - `/document-cache` — store-backed complete-response middleware
180
201
  - `/vercel` — Vercel Build Output preset and Runtime Cache wiring
181
202
  - `/cloudflare` — Worker deployment and bindings
@@ -175,20 +175,22 @@ header or logs to identify an actual CDN hit.
175
175
 
176
176
  ## Cache Key Generation
177
177
 
178
- The default key is the host, pathname, and sorted search string (internal
179
- `_rsc*` params removed, `cache.searchParams` filtering applied), followed by
180
- suffixes that separate response variants:
178
+ The default key is the serving router's id, the host, pathname, and sorted
179
+ search string (internal `_rsc*` params removed, `cache.searchParams` filtering
180
+ applied), followed by suffixes that separate response variants:
181
181
 
182
- - HTML document requests: `{host}{pathname}[?search]:html`
182
+ - HTML document requests: `{routerId}@{host}{pathname}[?search]:html`
183
183
  - RSC requests (client navigations and full-document Flight fetches):
184
- `{host}{pathname}[?search]:rsc`
184
+ `{routerId}@{host}{pathname}[?search]:rsc`
185
185
  - Client navigations additionally add a hash of the segments the client already
186
186
  has (`:{segmentHash}`), so navigations from pages with different layouts get
187
187
  different cached payloads, and a `:fragments` marker when the client accepts
188
188
  fragment envelopes.
189
189
 
190
- A custom `keyGenerator(url)` replaces only the `{host}{pathname}[?search]` part;
191
- the variant suffixes are still appended.
190
+ A custom `keyGenerator(url)` replaces the whole
191
+ `{routerId}@{host}{pathname}[?search]` part, router id included, so a generator
192
+ used by routers that share a store must keep them apart itself; the variant
193
+ suffixes are still appended.
192
194
 
193
195
  ## What Gets Cached
194
196
 
@@ -320,13 +322,13 @@ everything else.
320
322
 
321
323
  ## Document Cache vs Segment Cache
322
324
 
323
- | Feature | Document Cache | Segment Cache (`cache()`) |
324
- | ------------ | ------------------------------------- | --------------------------------- |
325
- | Granularity | Full response | Individual segments |
326
- | Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
327
- | Loaders | Frozen with the response | Always run fresh |
328
- | Use case | Static pages | Dynamic compositions |
329
- | Key includes | host + URL + variant (+ segment hash) | host + pathname + params + search |
325
+ | Feature | Document Cache | Segment Cache (`cache()`) |
326
+ | ------------ | ---------------------------------------------- | ------------------------------------------ |
327
+ | Granularity | Full response | Individual segments |
328
+ | Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
329
+ | Loaders | Frozen with the response | Always run fresh |
330
+ | Use case | Static pages | Dynamic compositions |
331
+ | Key includes | router + host + URL + variant (+ segment hash) | router + host + pathname + params + search |
330
332
 
331
333
  Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
332
334
 
@@ -97,3 +97,25 @@ the one for your case.
97
97
  | `useAction()` | Server action state | state, actionId, payload, error, result |
98
98
  | `useLocationState()` | History state (persists or flash) | T \| undefined |
99
99
  | `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
100
+
101
+ ## Hydration
102
+
103
+ On a document load every router hook renders what the server rendered until
104
+ the document has finished streaming, wherever the component sits. A component
105
+ in a `<Suspense>` or `loading()` boundary that streams in after the rest of
106
+ the page hydrates with the document's pathname, params, search params and
107
+ segments, an idle `useNavigation()` and `useAction()`, a `useLinkStatus()` that
108
+ is not pending, the handle data the HTML was rendered from, and `undefined`
109
+ location state, even when a navigation or an action has started meanwhile. It
110
+ cannot mismatch its server HTML.
111
+
112
+ - Navigation state, params and action state: the component shows the live
113
+ value right after it hydrates.
114
+ - Handle data a loader pushed late, and the entry's location state: every
115
+ reader gets them once the whole document has streamed and hydrated, in one
116
+ transition. A reader re-renders only when its selection changes.
117
+ - A navigation that commits before then mounts its page with the live values
118
+ from its first render. A boundary of the document that persists across that
119
+ navigation and has not hydrated yet then hydrates against the live values;
120
+ where they differ from its HTML, React reports the mismatch and renders that
121
+ boundary on the client.
@@ -40,10 +40,9 @@ bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandl
40
40
  re-renders on every handle update, late loader pushes included.
41
41
 
42
42
  On a document load `useHandle` hydrates with the handle data its HTML was
43
- rendered from, then takes whatever arrived since. That holds wherever the
44
- reader sits: a reader inside a `<Suspense>` or `loading()` boundary, which
45
- hydrates after the rest of the page, does not see a late loader push during
46
- its hydration render, so it cannot mismatch its server HTML.
43
+ rendered from, wherever the reader sits; late loader pushes reach every reader
44
+ once the document has finished streaming and every boundary has hydrated (see
45
+ `SKILL.md`, "Hydration").
47
46
 
48
47
  Handles can be passed as props from server to client components:
49
48
 
@@ -166,6 +166,16 @@ function Breadcrumbs() {
166
166
  const isShopRoute = useSegments((s) => s.path[0] === "shop");
167
167
  ```
168
168
 
169
+ The selector is applied when the router store changes (a navigation or new
170
+ handle data), as with `useNavigation` and `useAction`. A new selector function
171
+ on a re-render takes effect at the next change. To derive from a prop, select
172
+ the state and derive in render:
173
+
174
+ ```tsx
175
+ const segments = useSegments();
176
+ const part = segments.path[depth];
177
+ ```
178
+
169
179
  ### useLinkStatus()
170
180
 
171
181
  Track pending state inside a Link component. `pending` is `true` while a
@@ -251,9 +251,9 @@ function FlashBanner() {
251
251
  ```
252
252
 
253
253
  Flash behavior is determined by the definition (`{ flash: true }`), not by which
254
- hook reads it. `useLocationState` reads the value during render (on the
255
- hydration render it returns `undefined` and reads in a post-mount effect, so
256
- SSR and hydration agree), then clears it from `history.state` via
254
+ hook reads it. `useLocationState` reads the value during render (on a
255
+ document load it returns `undefined` until the document has finished streaming
256
+ and hydrating, so SSR and hydration agree), then clears it from `history.state` via
257
257
  `replaceState` in a `useEffect`. Every component reading the same flash
258
258
  definition sees the value, whenever it mounts, until the next navigation or
259
259
  back/forward commits its entry's state: an entry that carries no flash shows
@@ -342,7 +342,8 @@ development.
342
342
 
343
343
  Location state lives in the browser, so the server renders a document without
344
344
  it. `useLocationState` therefore hydrates as `undefined` and applies the stored
345
- value right after. For most state that is invisible. For state that decides
345
+ value once the document has finished streaming and every boundary has
346
+ hydrated. For most state that is invisible. For state that decides
346
347
  how much content is on the page, it is a layout shift on every refresh.
347
348
 
348
349
  The case this option exists for is a "load more" list. `?page=6` loads that
@@ -383,3 +383,21 @@ Each `createRouter()` app has its own cache version, computed by `vite build` fr
383
383
  | 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 |
384
384
 
385
385
  The host entry (the file that creates the host router and its middleware) is not part of a lazily mounted sub-app's version. If host middleware changes what an app renders from inside a cache boundary, change that app's `version` or invalidate its tags. See `/cache-guide`.
386
+
387
+ ## Shared cache store
388
+
389
+ Sub-apps may share one cache store (one KV namespace, one `MemorySegmentCacheStore`). Every entry that is a router's output for a host and path is keyed by that router as well; its key starts with the router's id (`{routerId}@{host}{path}...`):
390
+
391
+ - PPR shells (document, navigation and build-time);
392
+ - route `cache()` records, document-cache responses and response-route entries;
393
+ - a loader's own `cache()` entry, and a `"use cache"` entry of a function called with a handler or loader `ctx`. A `ctx` is one router's: its context variables, its `env`, its `reverse()` map.
394
+
395
+ So two sub-apps never read each other's entry, including where they serve the same host and path:
396
+
397
+ - with the `hostOverride` cookie, which picks the app and forwards the request unchanged, so every app answers under the one preview origin;
398
+ - after a `fallback()` that hands a request to another app's handler;
399
+ - after a `router.prerender()` warm that resolved another app's host as its origin.
400
+
401
+ There is nothing to configure and no namespace to manage. What it relies on is that each router has a stable id: the one the Vite plugin injects for a `createRouter({ ... })` call written with an object literal, or the `id` you pass (`createRouter({ id: "admin", ... })`). A router the plugin cannot reach, for example `createRouter(options)` with an options variable, falls back to a counter (`router_0`, `router_1`, ...) that follows module evaluation order. One such router is fine. With more than one, the build already warns (`N routers use auto-generated IDs`), because routes and cache versions are matched by the same id; the fix is the same here: pass the object literal inline or set an `id`.
402
+
403
+ Not keyed by router, on purpose: a `"use cache"` entry of a function called with plain arguments or the bare `Request` (the function and its arguments name no router, so both apps share the entry), tag markers (an invalidation reaches every app), and any key you build yourself (`cache({ key })` on a route or a loader, the document cache's `keyGenerator`): keep those apart yourself where two apps must not share them.
@@ -662,9 +662,11 @@ HIT".
662
662
 
663
663
  ### Cache Key
664
664
 
665
- The default cache key is `loader:{loaderId}:{host}{pathname}:{sortedParams}`
666
- (the host keeps multi-tenant hosts from sharing entries). This can be customized
667
- at two levels:
665
+ The default cache key is
666
+ `loader:{loaderId}:{routerId}@{host}{pathname}:{sortedParams}`: the host keeps
667
+ multi-tenant hosts from sharing entries, and the router id keeps two routers
668
+ that mount the loader on one store apart (see `/host-router`, "Shared cache
669
+ store"). This can be customized at two levels:
668
670
 
669
671
  ```typescript
670
672
  import { cookies } from "@rangojs/router";
@@ -688,7 +690,7 @@ Resolution priority (same order as route-level `cache()`):
688
690
  `loader:{loaderId}:key:` plus the result's URI encoding
689
691
  2. `store.keyGenerator(ctx, defaultKey)` — store-level modification, stored
690
692
  as returned
691
- 3. Default key — `loader:{loaderId}:{host}{pathname}:{sortedParams}`
693
+ 3. Default key — `loader:{loaderId}:{routerId}@{host}{pathname}:{sortedParams}`
692
694
 
693
695
  Because the `key()` result is namespaced by the loader id, no value it
694
696
  returns, request input included, can name another loader's entry or a
@@ -426,7 +426,7 @@ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
426
426
 
427
427
  Read the flash on the target page with `useLocationState(FlashMessage)`. The `{ flash: true }` option makes it auto-clear after first render. See `/hooks`.
428
428
 
429
- A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again.
429
+ A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again. The client follows at most 20 redirects in one navigation; a loop (two pages redirecting to each other) stops there with the error boundary and a `[rango] Server redirect loop` console error.
430
430
 
431
431
  ## Authentication Middleware
432
432