@rangojs/router 0.18.0 → 0.19.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 (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -123,7 +123,9 @@ else they allow:
123
123
 
124
124
  - **`cache()` boundary guard** (route-level) — fires while the handler runs on a
125
125
  miss. `cookies()` and `headers()` throw (request-scoped data would be baked into
126
- the shared cached shell), `ctx.get(nonCacheableVar)` throws (a tainted value
126
+ the shared cached shell), and so do the raw reads `ctx.request.headers` and
127
+ `getRequestContext().cookie()` / `.cookies()` (#976);
128
+ `ctx.get(nonCacheableVar)` throws (a tainted value
127
129
  would be baked in), and response side effects (`ctx.headers.set()`,
128
130
  `setCookie()`, `setStatus()`, `onResponse()`) throw. `ctx.set()` of a cacheable var is
129
131
  **allowed** — children are cached too and can read it. **Loaders are exempt**
@@ -131,19 +133,33 @@ else they allow:
131
133
  loader.
132
134
  - **Loader `cache()` guard** (a loader bound with its own `cache()`) — its value
133
135
  is stored under a key that names no user, so a miss whose body read
134
- `cookies()`, `headers()`, the theme or a non-cacheable `ctx.get()` fails
136
+ `cookies()`, `headers()`, `ctx.request.headers`, the theme or a
137
+ non-cacheable `ctx.get()` fails
135
138
  unless the binding has a `key()` or its store a `keyGenerator`. Either one
136
139
  switches the check off, so put what the body reads in it (`/loader` →
137
140
  "Cache Key").
138
141
  - **`"use cache"` exec-guard** (function-level) — the same request-scoped APIs
139
- throw inside the cached function (`cookies()`, `headers()`, `ctx.set()`,
140
- `ctx.headers.set()` and other response writes); additionally, tainted
141
- `ctx`/`env`/`req` args are excluded from the cache key. The guard runs only on
142
- the cached path: with no item-capable store configured the function runs
142
+ throw inside the cached function (`cookies()`, `headers()`,
143
+ `ctx.request.headers`, `ctx.set()`, `ctx.headers.set()` and other response
144
+ writes); additionally, tainted `ctx`/`env`/`req` args are excluded from the
145
+ cache key (a `Request` argument keys by its URL only). The guard runs only
146
+ on the cached path: with no item-capable store configured the function runs
143
147
  uncached and nothing throws.
144
148
 
145
- The `ppr` shell capture has its own, stricter guard: `cookies()`, `headers()`, a
146
- theme read, a `{ cache: false }` variable read, and `ctx.dynamic()` refuse the
149
+ All three guards leave a cache's own callbacks alone: a `key()`, a store
150
+ `keyGenerator`, a `condition()` and a `tags()` function may read `cookies()`,
151
+ `headers()` or `ctx.request.headers`, because the value picks or labels the
152
+ entry rather than being rendered into it. So does `onError`, which only
153
+ observes. A `"use cache"` function or loader they call is guarded as usual.
154
+
155
+ `ctx.request.clone()` is guarded like `ctx.request.headers`. The remaining gap:
156
+ `fetch(ctx.request)` and `new Request(ctx.request)` don't throw and aren't
157
+ guarded. A fetch forwards the visitor's `Cookie` and `Authorization`, so its
158
+ response is per visitor; don't make one in a cached body.
159
+
160
+ The `ppr` shell capture has its own, stricter guard: `cookies()`, `headers()`,
161
+ `ctx.request.headers`, a theme read, a `{ cache: false }` variable read, and
162
+ `ctx.dynamic()` refuse the
147
163
  capture anywhere it waits — handlers, promises they pass or push, async server
148
164
  components, `ssr: false` loaders, and loaders a handler awaits (no loader
149
165
  exemption there). The route then serves uncached; see `/ppr`.
@@ -222,17 +238,17 @@ local/dev behavior, not as proof that segment SWR is active.
222
238
 
223
239
  ## Key Differences
224
240
 
225
- | | `cache()` DSL | `"use cache"` directive |
226
- | -------------------- | ----------------------------------------------------- | ------------------------------------------------- |
227
- | **Scope** | Route segment tree (handler + children + parallels) | Single function return value |
228
- | **Defined at** | Route definition site (`urls.ts`) | Inside function body or at file top |
229
- | **Cache key** | Request type + host + pathname + params + search | Function identity + serialized non-tainted args |
230
- | **Execution on hit** | All-or-nothing: entire handler skipped | Partial: function body skipped, calling code runs |
231
- | **Runtime control** | `condition` to disable, custom `key` function | None — if the directive is present, it caches |
232
- | **Side effects** | Response side effects throw inside the boundary | `ctx.headers.set()`, `ctx.set()`, etc. throw |
233
- | **Handle data** | Handler pushes replayed; loader pushes re-run live | Captured and replayed when it receives `ctx` |
234
- | **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
235
- | **Nesting** | Inner TTLs override; `key()` partitions compose | Compose by calling cached functions from uncached |
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 |
236
252
 
237
253
  ### cache() Cache Key
238
254
 
@@ -245,14 +261,18 @@ honors `createRouter({ cache: { searchParams } })`.
245
261
 
246
262
  A custom `key` function replaces the whole default key (e.g., to key by user role
247
263
  or locale); it also bypasses the store's `keyGenerator` and the search-param
248
- filter. It runs once per request and may read `cookies()`. A nested `cache()`
264
+ filter. It runs once per request and may read `cookies()`. Its result is
265
+ stored namespaced (`key:` plus its URI encoding), so no value it returns, raw
266
+ request input included, names another route's record. A nested `cache()`
249
267
  keys its records within the enclosing `key()` partition (`/caching` → "Keys
250
- nest"). Never return raw request input from `key()`: normalize it to the
251
- values you serve or prefix it. On a `ppr` route, `key()` (and a store
252
- `keyGenerator` that returns a non-default key) also partitions the shell: one
253
- shell per value, and a partitioned request never reads a build-time shell.
254
- Keep the values to a small set. `condition` can disable caching entirely at
255
- runtime (e.g., skip for authenticated users).
268
+ nest"). Normalize the result to the values you serve: each distinct value is
269
+ a record. On a `ppr` route, `key()` (and a store `keyGenerator` that returns
270
+ a non-default key) also partitions the shell: one shell per value, and a
271
+ partitioned request never reads a build-time shell. Keep the values to a
272
+ small set. `condition` can disable caching entirely at runtime (e.g., skip
273
+ for authenticated users), for every `cache()` nested under it too, and an
274
+ outer `cache()`'s `tags` tag the nested records (`/caching` → "Conditions
275
+ and tags inherit").
256
276
 
257
277
  ### "use cache" Cache Key
258
278
 
@@ -302,8 +322,9 @@ MISS → function body runs, return value + handle data cached
302
322
 
303
323
  Runtime guards throw if you call `cookies()`, `headers()`, `ctx.set()`,
304
324
  `ctx.headers.set()` (or any response write: cookie writes, `setStatus()`,
305
- `onResponse()`), `ctx.setTheme()`, or `ctx.setLocationState()` inside a
306
- `"use cache"` function. `cookies()` and `headers()` are blocked because
325
+ `onResponse()`), `ctx.setTheme()`, or `ctx.setLocationState()`, or read
326
+ `ctx.request.headers`, inside a `"use cache"` function. `cookies()`,
327
+ `headers()` and `ctx.request.headers` are blocked because
307
328
  per-request data is not in the cache key. Side-effect methods are blocked because
308
329
  their effects are lost on hit. Use `ctx.use(Handle)` instead for data — handle
309
330
  data is captured and replayed.
@@ -432,7 +453,8 @@ Neither mechanism caches response headers or cookies.
432
453
  you need headers or cookies on every response, set them in middleware, a live
433
454
  segment outside the cache boundary, or a loader.
434
455
  - **"use cache"**: `cookies()` and `headers()` throw inside the cached function
435
- (both reads and writes), and so do `ctx.headers` mutations. Move them outside.
456
+ (both reads and writes), and so do `ctx.request.headers` reads and
457
+ `ctx.headers` mutations. Move them outside.
436
458
  - **`ctx.theme`** (handler and middleware) and **`getRequestContext().theme`**
437
459
  are the visitor's theme cookie, so a read throws in both scopes, like
438
460
  `cookies()`. Read it with `useTheme()` in a client component, or in a
@@ -469,26 +491,27 @@ specifies `cache: false`, the value is non-cacheable.
469
491
 
470
492
  **Behavior inside a `cache()` boundary:**
471
493
 
472
- | Operation | Inside a `cache()` boundary |
473
- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
474
- | `cookies()` / `headers()` (read or write) | Throws (request-scoped, would poison the shared entry) |
475
- | `ctx.theme`, `getRequestContext().theme` | Throws, like `cookies()` (#971) |
476
- | `ctx.get(cacheableVar)` | Allowed |
477
- | `ctx.get(nonCacheableVar)` | Throws (would be baked in) |
478
- | `ctx.set(var, value)` (cacheable) | Allowed |
479
- | `ctx.headers.set()` / cookie writes | Throws (response side effect would be lost on hit) |
480
- | Any of the above **inside a loader** | Allowed (loaders always run fresh) |
481
- | The reads above in a loader with its own `cache()` | The miss fails without a `key()`; any `key()` allows them, so it must include them |
494
+ | Operation | Inside a `cache()` boundary |
495
+ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
496
+ | `cookies()` / `headers()` (read or write) | Throws (request-scoped, would poison the shared entry) |
497
+ | `ctx.request.headers`, `getRequestContext().cookie()` / `.cookies()` | Throws, like `headers()` / `cookies()` (#976) |
498
+ | `ctx.theme`, `getRequestContext().theme` | Throws, like `cookies()` (#971) |
499
+ | `ctx.get(cacheableVar)` | Allowed |
500
+ | `ctx.get(nonCacheableVar)` | Throws (would be baked in) |
501
+ | `ctx.set(var, value)` (cacheable) | Allowed |
502
+ | `ctx.headers.set()` / cookie writes | Throws (response side effect would be lost on hit) |
503
+ | Any of the above **inside a loader** | Allowed (loaders always run fresh) |
504
+ | The reads above in a loader with its own `cache()` | The miss fails without a `key()`; any `key()` allows them, so it must include them |
482
505
 
483
506
  (Both scopes block the same request-scoped APIs — `cookies()`, `headers()`,
484
507
  response side effects, and non-cacheable `ctx.get()` — because each would leak
485
508
  per-request data into a shared cache entry. The `cache()` boundary tracks the
486
509
  scope via `isInsideCacheScope()`; `"use cache"` uses the exec guard and also
487
- excludes tainted `ctx`/`env`/`req` args from the cache key. Loaders are exempt in
488
- both — see "Headers and Cookies" and the precise guarantee below. Under `"use
489
- cache"` the exemption covers a loader body the cached function consumes
490
- (`await ctx.use(Loader)`), not a cached function a loader calls: the loader
491
- re-runs on every request, the cached body does not.)
510
+ excludes tainted `ctx`/`env`/`req` args from the cache key. Loaders are exempt
511
+ under `cache()` — see "Headers and Cookies" and the precise guarantee below.
512
+ Under `"use cache"` nothing is exempt: a loader body the cached function
513
+ consumes (`await ctx.use(Loader)`) is part of the cached result, and a cached
514
+ function a loader calls does not re-run with the loader.)
492
515
 
493
516
  Write is dumb — `ctx.set()` stores the cache metadata but does not enforce.
494
517
  Enforcement happens at read time (`ctx.get()`), where ALS detects the cache
@@ -528,14 +551,18 @@ breeds the false confidence that makes the derived leak _more_ likely. The guard
528
551
  is deliberately non-propagating (propagation would cost a wrapper per derivation
529
552
  on the hot path), and it is scoped to the `cache()` segment boundary and the
530
553
  `"use cache"` body. `"use cache"` functions block the same request-scoped reads
531
- (`cookies()` / `headers()` and non-cacheable `ctx.get()` throw inside them) and
554
+ (`cookies()` / `headers()`, `ctx.request.headers` and non-cacheable `ctx.get()` throw inside them) and
532
555
  additionally exclude tainted `ctx`/`env`/`req` args from the cache key — pass a
533
556
  non-cacheable value in as an argument so it becomes part of the key. The pattern that stays safe is also the natural one:
534
557
  **read tainted context at the point of use, in the path that needs it (a loader or
535
558
  live segment) — never extract user data into a plain value and cache that.**
536
- Loaders are exempt because they run outside the cache scope and resolve fresh
537
- every request. A loader with its own `cache()` does not: its value is stored,
538
- so it falls under the loader `cache()` guard above.
559
+ Loaders are exempt from the `cache()` guard because they run outside the cache
560
+ scope and resolve fresh every request. A loader with its own `cache()` does not:
561
+ its value is stored, so it falls under the loader `cache()` guard above. Nor
562
+ does a loader awaited inside a `"use cache"` body: its value is part of what the
563
+ function returns, so the same reads throw there when the cached function is
564
+ the first to read it in the request (an already-started loader's value is
565
+ stored unchecked).
539
566
 
540
567
  ## Loaders Are Always Fresh
541
568
 
@@ -657,7 +684,7 @@ data is cached independently from the route's segment cache, together with
657
684
  the handle pushes its body made (replayed on every hit). Loader caching
658
685
  supports custom keys, tags, SWR, conditional bypass, and per-loader store
659
686
  overrides — see `/loader` for the full reference. A body that reads
660
- `cookies()`, `headers()` or a non-cacheable variable needs a `key()`, which
687
+ `cookies()`, `headers()`, `ctx.request.headers` or a non-cacheable variable needs a `key()`, which
661
688
  must include the value: without one the miss fails, and any `key()` switches
662
689
  that check off. The entry also carries the tags its body recorded
663
690
  (`cacheTag()` calls, `"use cache"` reads, and those of the loaders it reads
@@ -90,7 +90,8 @@ path, not only ProductChrome.
90
90
 
91
91
  The consumer rule: **want it cached? render it inline. Want it live? put it in a
92
92
  loader and read it with `useLoader()` in a client component.** Anything read
93
- with `cookies()`, `headers()`, or a non-cacheable variable belongs in a loader:
93
+ with `cookies()`, `headers()`, `ctx.request.headers`, or a non-cacheable
94
+ variable belongs in a loader:
94
95
  a route `cache()` does not store loader values, so a loader runs on every
95
96
  request. A loader bound with its own `cache()` is stored, so without a `key()`
96
97
  its miss fails; any `key()` switches that check off, so it must include the
@@ -167,14 +168,20 @@ cache(
167
168
  — document requests, client navigations, and intercept navigations are cached
168
169
  separately. The search part honors `cache.searchParams` (below).
169
170
  - **`key`** is a full override: it bypasses the default key, the store's
170
- `keyGenerator`, and the search-param filter. A nested `cache()` keys its
171
- records within that partition (see "Keys nest" under "Nested Cache
172
- Boundaries"). On a `ppr` route the same key
173
- (or, without one, the store's `keyGenerator`) partitions the PPR shell: each
174
- partition captures and serves its own shell (`/ppr`, "Request-partitioned
175
- shells").
171
+ `keyGenerator`, and the search-param filter. The result is stored
172
+ namespaced (`key:` plus its URI encoding), so whatever it returns, request
173
+ input included, never names another route's default-keyed record or a
174
+ nested partition's. A nested `cache()` keys its records within that
175
+ partition (see "Keys nest" under "Nested Cache Boundaries"). On a `ppr`
176
+ route the same key (or, without one, the store's `keyGenerator`)
177
+ partitions the PPR shell: each partition captures and serves its own shell
178
+ (`/ppr`, "Request-partitioned shells").
176
179
  - **`condition`** returning `false` skips both the cache read and write for that
177
- request (the boundary renders live).
180
+ request (the boundary renders live). It gates every `cache()` nested under
181
+ it too: an inner boundary caches only when every enclosing `condition()`
182
+ allows it.
183
+ - **`tags`** tag this boundary's records and those of every `cache()` nested
184
+ under it, so `updateTag()` of an outer tag evicts the inner records too.
178
185
  - `cache(() => [...])` with no options uses the store defaults.
179
186
  - `cache(false, () => [...])` disables caching for a subtree (see "Nested Cache
180
187
  Boundaries").
@@ -230,8 +237,11 @@ function CampaignBanner() {
230
237
 
231
238
  Each tag attaches to one entry, so know which entry you are tagging:
232
239
 
233
- - Forms 1 and 2 tag the `cache()` segment entry.
234
- - Form 3 tags the `"use cache"` entry (plus any profile `tags`).
240
+ - Forms 1 and 2 tag the `cache()` segment entry, and the entries of every
241
+ `cache()` nested under it (see "Conditions and tags inherit").
242
+ - Form 3 tags the `"use cache"` entry (plus any profile `tags`), and every
243
+ `"use cache"` entry that calls that function: the outer entry bakes the
244
+ inner value, so it carries the inner tags.
235
245
  - Form 4 tags the request's `ppr` shell entry (`/ppr`) or `/document-cache`
236
246
  entry, with no `"use cache"` needed. On a route that is neither `ppr` nor
237
247
  document-cached and has no `cache()`, nothing reads the tag — a silent
@@ -283,18 +293,21 @@ request, so an action that calls `revalidateTag()` and then renders gets fresh
283
293
  `"use cache"` and `cache()` reads, as with `await updateTag()`. What
284
294
  `revalidateTag()` does not wait for is the durable write (the KV marker, the
285
295
  tag purge, Vercel's `expireTag`): other requests see the invalidation once it
286
- lands. A `"use cache"` call that this request started before the
287
- invalidation, and that carries one of the invalidated tags, is not reused
288
- after it and does not store its value; neither does a stale entry's
289
- background refresh. The tags are the call's own (its `cacheTag()` calls and
290
- profile `tags`): a nested `"use cache"` function's tags do not reach the
291
- enclosing entry, so invalidating one of them neither evicts the outer entry
292
- nor stops an outer call already running
293
- ([#980](https://github.com/rangojs/rango/issues/980)). Not covered
294
- ([#977](https://github.com/rangojs/rango/issues/977)): such a call started
295
- by another request, or a loader's own `cache()` that started before the
296
- invalidation, still stores its value when it finishes, and reads after that
297
- can get it.
296
+ lands.
297
+
298
+ Work that started before the invalidation, in this request or another one,
299
+ is not stored after it. A `"use cache"` call, a stale entry's background
300
+ refresh, a loader's own `cache()`, a route `cache()` render or a
301
+ document-cache render that read its data before one of its tags was
302
+ 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
305
+ `"use cache"` call already running when one of its tags is invalidated is
306
+ not joined by later calls either. Its tags include those of the
307
+ `"use cache"` functions it calls. Another isolate's invalidation is caught
308
+ through the store's markers (`CFCacheStore` with KV, `VercelCacheStore`); on
309
+ a KV-less `CFCacheStore` only this isolate's invalidations are, and ttl+swr
310
+ bounds the rest.
298
311
 
299
312
  Both must run inside a request (action, handler, middleware, response route).
300
313
  Called from a queue consumer or cron job, there is no request context, so they
@@ -428,8 +441,9 @@ does. See `/loader` for the full loader reference.
428
441
 
429
442
  The entry's default key is the loader, host, path and params. It does not
430
443
  inherit an enclosing `cache()` boundary's `key()` either, so it is shared
431
- across users: a body that reads `cookies()`, `headers()` or a non-cacheable
432
- variable fails on a miss unless the loader `cache()` has a `key()` or its store
444
+ across users: a body that reads `cookies()`, `headers()`, `ctx.request.headers`
445
+ or a non-cacheable variable fails on a miss unless the loader `cache()` has a
446
+ `key()` or its store
433
447
  a `keyGenerator`. Either one switches the check off, so it must itself include
434
448
  the value (`/loader` → "Cache Key").
435
449
 
@@ -615,8 +629,10 @@ times out, or whose snapshot is truncated or corrupt (also evicted), cannot
615
629
  replay the captured segment record, so it degrades: the entry is replaced and
616
630
  recaptured and the page reloads once into a cache-miss render (`/ppr`). The
617
631
  snapshot holds only what a HIT reads: every HIT replays the handler layer from
618
- the captured segment record, so the `"use cache"` items only handler code read
619
- are not stored.
632
+ the captured segment record and serves an `ssr: false` loader's baked
633
+ container, so no `"use cache"` or loader `cache()` item is stored in it. A
634
+ live hole, and any loader that runs on the HIT, reads those from the store
635
+ under their own ttl/swr and tags.
620
636
 
621
637
  **Shell memo.** After a shell read, the same isolate serves that key's next
622
638
  HITs from memory for `memo.shellMs` (default 2000; `{ shellMs: 0 }` turns it
@@ -785,22 +801,49 @@ cached handler is **frozen into the shared cache entry** and served to every
785
801
  subsequent visitor. To stop one user's request-scoped data from leaking to
786
802
  another, request-scoped APIs are guarded inside a cache scope:
787
803
 
788
- | Inside a `cache()` boundary | Behavior |
789
- | ---------------------------------------------------------------------------------- | --------------------------------------------------- |
790
- | `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
791
- | Response writes: `ctx.headers.set()`, `setCookie()`, `setStatus()`, `onResponse()` | **throws** — response side effects lost on a hit |
792
- | `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
793
- | `ctx.theme`, `getRequestContext().theme` | **throws** — the theme is the visitor's cookie |
794
- | `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
795
- | Any of the above **inside a registered loader** (`loader(...)`) | **allowed** — loaders always run fresh |
804
+ | Inside a `cache()` boundary | Behavior |
805
+ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------- |
806
+ | `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
807
+ | `ctx.request.headers`, `getRequestContext().cookie()` / `.cookies()` | **throws** on read, like `headers()` / `cookies()` |
808
+ | Response writes: `ctx.headers.set()`, `setCookie()`, `setStatus()`, `onResponse()` | **throws** — response side effects lost on a hit |
809
+ | `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
810
+ | `ctx.theme`, `getRequestContext().theme` | **throws** — the theme is the visitor's cookie |
811
+ | `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
812
+ | Any of the above **inside a registered loader** (`loader(...)`) | **allowed** — loaders always run fresh |
813
+ | Any read in the boundary's own `key()`, `keyGenerator`, `condition()` or `tags()` | **allowed** — it picks or labels the entry, never rendered |
796
814
 
797
815
  A loader body invoked from a handler with `await ctx.use(Loader)` may read
798
- `cookies()`/`headers()`, but its response writes still throw: on a hit the
816
+ `cookies()`/`headers()`/`ctx.request.headers`, but its response writes still throw: on a hit the
799
817
  handler is skipped, so that loader never runs.
800
818
 
819
+ `ctx.request.clone()` is guarded the same way. `fetch(ctx.request)` and
820
+ `new Request(ctx.request)` don't throw, but the guard can't see through them:
821
+ a fetch forwards the visitor's `Cookie` and `Authorization`, so its response
822
+ is per visitor and must not be rendered inside the boundary. A handler that
823
+ must render a header its `key()` partitions by reads a copy middleware set:
824
+
825
+ ```tsx
826
+ const Tier = createVar<string>();
827
+ const tierOf = (ctx: { request: Request }) =>
828
+ ctx.request.headers.get("x-tier") === "gold" ? "gold" : "silver";
829
+
830
+ middleware(
831
+ async (ctx, next) => {
832
+ ctx.set(Tier, tierOf(ctx)); // middleware runs outside the boundary
833
+ return next();
834
+ },
835
+ () => [
836
+ cache({ ttl: 300, key: (ctx) => `tier:${tierOf(ctx)}` }, () => [
837
+ path("/pricing", (ctx) => <Pricing tier={ctx.get(Tier)} />),
838
+ ]),
839
+ ],
840
+ );
841
+ ```
842
+
801
843
  A loader bound with its **own** `cache()` (`loader(Def, () => [cache({...})])`)
802
844
  stores its value, and its key does not inherit the route's. A miss whose body
803
- read `cookies()`, `headers()` or a non-cacheable `ctx.get()` fails unless the
845
+ read `cookies()`, `headers()`, `ctx.request.headers` or a non-cacheable
846
+ `ctx.get()` fails unless the
804
847
  binding has a `key()` or its store a `keyGenerator`, which must then include
805
848
  the value (`/loader` → "Cache Key").
806
849
 
@@ -826,9 +869,12 @@ cached handler.
826
869
  This is the **consumption-lane rule**, and it holds for every shared
827
870
  artifact — `cache()`, `"use cache"`, and the PPR shell (`/ppr`): handler
828
871
  consumption = baked copy; client-side `useLoader` = live. The tiers differ on
829
- identity reads inside a handler-consumed loader: `cache()` and `"use cache"`
830
- permit them (the leak above), while a PPR shell capture refuses them and the
831
- route stays uncached. It is stated once in `/rango` → Invariants.
872
+ identity reads inside a handler-consumed loader: `cache()` permits them (the
873
+ leak above); `"use cache"` throws when the cached function is the first to
874
+ read the loader in the request, because the loader body then runs inside it
875
+ and its value is part of the result (a loader already started elsewhere
876
+ returns its value unchecked, and that value is stored); a PPR shell capture
877
+ refuses them and the route stays uncached. It is stated once in `/rango` → Invariants.
832
878
 
833
879
  ```typescript
834
880
  // WRONG — throws: cookies() read directly in a cached handler
@@ -874,9 +920,11 @@ See `/cache-guide` for the full decision guide and the `cache()` vs `"use cache"
874
920
 
875
921
  ## Nested Cache Boundaries
876
922
 
877
- An inner boundary overrides the settings of the outer one for its subtree
878
- (`key` composes instead, see below); `cache(false)` turns caching off for a
879
- subtree:
923
+ An inner boundary overrides `ttl`, `swr` and `store` for its subtree;
924
+ `cache(false)` turns caching off for a subtree. The options that decide
925
+ whether and where a record may be shared build up instead: `key` composes
926
+ (see below), every enclosing `condition()` must allow the inner boundary, and
927
+ the inner records carry the enclosing `tags` too:
880
928
 
881
929
  ```typescript
882
930
  cache({ ttl: 300 }, () => [
@@ -898,6 +946,39 @@ cache miss, with no shell served and none captured. A `ppr` route under a
898
946
  layout's `cache()` therefore cannot opt out of that `cache()` and still get a
899
947
  shell (`/ppr`).
900
948
 
949
+ ### Conditions and tags inherit
950
+
951
+ ```typescript
952
+ cache(
953
+ {
954
+ condition: (ctx) => !ctx.request.headers.has("x-preview"),
955
+ tags: ["catalog"],
956
+ },
957
+ () => [
958
+ cache({ ttl: 60, tags: ["prices"] }, () => [
959
+ path("/prices", PricesPage, { name: "prices" }),
960
+ ]),
961
+ ],
962
+ );
963
+ ```
964
+
965
+ - A preview request renders the prices page live: the outer `condition()` bypasses
966
+ the inner record's read and write, and a `ppr` route's shell. Conditions
967
+ combine with AND, through a `cache(false)` with a `cache()` re-enabled
968
+ below it too.
969
+ - The prices record carries `catalog` and `prices` (static and function
970
+ `tags` alike), so `updateTag("catalog")` evicts it, and the `ppr` shells
971
+ and documents built from it.
972
+ - `cache(false)` is unchanged: it caches nothing below it until a `cache()`
973
+ re-enables caching.
974
+ - A loader's own `cache()` and `"use cache"` are separate layers: they take
975
+ neither the route's `condition()` nor its `tags`, nor its `key()`
976
+ partition.
977
+
978
+ Before issue #974, only the innermost boundary's `condition` and `tags`
979
+ applied: a request an outer `condition()` refused still read and wrote the
980
+ inner record, and `updateTag()` of an outer tag left it in place.
981
+
901
982
  ### Keys nest: a partition covers the whole subtree
902
983
 
903
984
  A `key()` partitions every record under its `cache()`. A nested `cache()`
@@ -911,14 +992,14 @@ const tier = (ctx) =>
911
992
  cache({ ttl: 300, key: (ctx) => `tier:${tier(ctx)}` }, () => [
912
993
  layout(TierLayout, () => [
913
994
  // No key() of its own: the outer key() result, then this boundary's own
914
- // default key ("tier%3Agold|doc%3Aexample.com%2Fpricing"), so /pricing
915
- // and /faq keep their own records
995
+ // default key ("key:tier%3Agold|doc%3Aexample.com%2Fpricing"), so
996
+ // /pricing and /faq keep their own records
916
997
  cache({ ttl: 60 }, () => [
917
998
  path("/pricing", PricingPage, { name: "pricing" }),
918
999
  path("/faq", FaqPage, { name: "faq" }),
919
1000
  ]),
920
1001
 
921
- // Its own key() composes with the outer one ("tier%3Agold|v%3Ab")
1002
+ // Its own key() composes with the outer one ("key:tier%3Agold|key:v%3Ab")
922
1003
  cache(
923
1004
  { ttl: 60, key: (ctx) => `v:${ctx.searchParams.get("v") ?? "a"}` },
924
1005
  () => [path("/plans", PlansPage, { name: "plans" })],
@@ -927,26 +1008,29 @@ cache({ ttl: 300, key: (ctx) => `tier:${tier(ctx)}` }, () => [
927
1008
  ]);
928
1009
  ```
929
1010
 
930
- The parts are joined by `|`, outermost first, each URI-encoded:
931
-
932
- | Boundary the route sits in | Record key |
933
- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
934
- | `cache({ ttl })`, no `key`, under a keyed one | The chain's `key()` results, then this boundary's default key (its store's `keyGenerator` result, else the default key) |
935
- | `cache({ key })` under a keyed one | The chain's `key()` results |
936
- | deeper nesting | Composes the same way; only the innermost boundary's missing `key` adds its default key |
937
- | `cache(false)` | No caching, as before; a `cache()` re-enabled below it stays in the partition |
938
- | a single keyed `cache()`, or no `key` anywhere | Its raw `key()` result, or the default key, as before |
939
-
940
- - The encoding is unambiguous among composed keys: an encoded part holds no
941
- `|`, so no two partitions, and no two inner keys, compose to the same
942
- record key.
943
- - A single keyed `cache()` stores its `key()` result as is, so a raw result
944
- can equal a composed key: a header value `gold|doc%3Aexample.com%2Fpricing`
945
- returned raw names the record gold's pricing page keeps under the inner
946
- `cache()` in the example above. Never return raw
947
- request input from `key()`: normalize it to the values you serve, prefix
948
- it (`tier:${value}`), or encode it. A composed key holds no `:` (every
949
- part is URI-encoded), so a prefixed result never equals one.
1011
+ Every `key()` result is stored as `key:` plus its URI encoding; a default
1012
+ key in a composed key is URI-encoded. The parts are joined by `|`, `key()`
1013
+ results first, outermost first:
1014
+
1015
+ | Boundary the route sits in | Record key |
1016
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1017
+ | a single keyed `cache()` | Its `key()` result, namespaced (`tier:gold` is stored as `key:tier%3Agold`) |
1018
+ | `cache({ ttl })`, no `key`, under a keyed one | The chain's `key()` results, then this boundary's default key (its store's `keyGenerator` result, else the default key), encoded |
1019
+ | `cache({ key })` under a keyed one | The chain's `key()` results |
1020
+ | deeper nesting | Composes the same way; only the innermost boundary's missing `key` adds its default key |
1021
+ | under a `cache({ store })` on another store | Also that store's `keyGenerator` result, one part per store: empty for a store that returns the default key while another's differs, none when all return it |
1022
+ | `cache(false)` | No caching, as before; a `cache()` re-enabled below it stays in the partition |
1023
+ | no `key` anywhere | The default key, or the store's `keyGenerator` result, as before |
1024
+
1025
+ - No `key()` result, whatever it returns, equals a default key or a
1026
+ composed key, and no two composed keys are equal: a namespaced result
1027
+ starts with `key:` and holds no `|`, a default key never starts with
1028
+ `key:`, and each part of a composed key is encoded, so the parts and their
1029
+ kinds are recoverable. Returning request input from `key()` cannot name
1030
+ another route's record (before issue #975 a header value
1031
+ `doc:example.com/pricing` returned raw named the pricing page's default-keyed
1032
+ record). A prefix (`tier:${value}`) is still worth it for readable keys,
1033
+ and normalizing to the values you serve keeps the partition count bounded.
950
1034
  - An inner boundary without `key` keeps everything its default key tells
951
1035
  apart (path, params, search, document vs navigation), so its routes never
952
1036
  share a record, even when the outer `key()` names no route.
@@ -954,17 +1038,31 @@ The parts are joined by `|`, outermost first, each URI-encoded:
954
1038
  single `key()` does.
955
1039
  - Each `key()` runs once per request, however many boundaries and lookups use
956
1040
  it.
1041
+ - A store's `keyGenerator` partitions the records of a `cache()` on that
1042
+ store without a `key()` of its own. A `cache({ store })` on another store
1043
+ nested under it keys its records by that `keyGenerator` result too, so a
1044
+ locale-partitioned outer boundary never shares an inner record across
1045
+ locales. With several such stores each keeps its position: one that
1046
+ returns the default key for a request adds an empty part while another's
1047
+ differs, and when all return it the key is unchanged. A `keyGenerator` must
1048
+ return the default key, not `""`, to leave a request unpartitioned: an
1049
+ empty result there fails key resolution, so the request renders uncached,
1050
+ and the router warns once naming the store. On the same store the inner
1051
+ boundary's own default key already carries it (or its own `key()`
1052
+ overrides it).
957
1053
  - A response route's entry (`path.json()` and the other response routes) is
958
- keyed the same way. A `ppr` route's shell is partitioned by the chain's
959
- `key()` results alone, since the shell key already carries the URL (plus the
960
- store `keyGenerator`'s result for an inner boundary without `key`).
1054
+ keyed the same way, behind a `response:` prefix. A `ppr` route's shell is
1055
+ partitioned by the chain's `key()` results, namespaced the same way, since
1056
+ the shell key already carries the URL (plus the `keyGenerator` results
1057
+ above, and the store's for an inner boundary without `key`).
961
1058
  - To share an inner cache across partitions, move it outside the keyed
962
1059
  `cache()`.
963
1060
 
964
1061
  Before issue #970, the innermost boundary alone decided the key: an inner
965
1062
  `cache()` without `key` wrote under the default key and an inner `key()`
966
1063
  dropped the outer partition, so a silver visitor could HIT what a gold
967
- visitor's request had cached.
1064
+ visitor's request had cached. Before issue #974 an outer `cache({ store })`'s
1065
+ `keyGenerator` did not reach an inner boundary on another store.
968
1066
 
969
1067
  ## Custom Cache Store
970
1068
 
@@ -134,7 +134,7 @@ flow into the generated route map, so `href()`, `ctx.reverse()` and
134
134
  | `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false }, use?)` — see below |
135
135
  | `loading()` | Route-level boundary around the optimistic render; inline `<Suspense>` at read sites keeps the destination's chrome visible while only the reads wait |
136
136
  | `revalidate()` | Valid **inside a loader() use callback only**; runs in the browser |
137
- | `transition()` | Inside a `path()` use callback only (at most one). Data-only ViewTransition config — no `when`; same-route navs in a group already hold previous content without it |
137
+ | `transition()` | Inside a `path()` use callback only (at most one). ViewTransition config plus a browser-run `when` predicate (see `/view-transitions`); same-route navs in a group already hold without it |
138
138
  | `intercept()` | Dot-local named target in the SAME definition; use may contain `loader()`/`loading()` |
139
139
 
140
140
  At the top level of the builder only `path()`, `layout()` and `intercept()`