@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
@@ -51,7 +51,9 @@ only prerequisite is an app-level `createRouter({ cache })` store that
51
51
  implements the shell family (`getShell`/`putShell`): `MemorySegmentCacheStore`
52
52
  (dev/tests), `CFCacheStore` (Cache API L1 + optional KV L2), or
53
53
  `VercelCacheStore` (Vercel Runtime Cache). A ppr route on a store without the
54
- family renders normally (no shell) with a once-per-key warning.
54
+ family renders normally (no shell) with a once-per-key warning. A custom store
55
+ returns each `ShellCacheEntry` field as `putShell` received it, including
56
+ `buildVersion` and `snapshot` (both required; the snapshot can be empty).
55
57
 
56
58
  ```typescript
57
59
  import { createRouter, urls } from "@rangojs/router";
@@ -138,7 +140,8 @@ lifetime. A HIT never runs a handler. To keep a value fresh per request, load
138
140
  it in a loader without `ssr: false` and read it with `useLoader` under
139
141
  `loading()` or an inline `<Suspense>`; inside an `ssr: false` loader, return
140
142
  that part as a nested promise. Request-scoped reads the capture now waits for
141
- refuse it: `cookies()`, `headers()`, a `{ cache: false }` variable, and
143
+ refuse it: `cookies()`, `headers()`, `ctx.request.headers`, a
144
+ `{ cache: false }` variable, and
142
145
  `ctx.dynamic()` inside a handler promise, an async component, a handle push, or
143
146
  a loader a handler awaits. A normal `ctx.get()` value is not guarded: shell
144
147
  material is shared per host+URL and request partition. `cache(false)`, or a
@@ -163,12 +166,17 @@ cache(
163
166
 
164
167
  The partition follows nested scopes (issue #970): a `ppr` route under a
165
168
  `cache()` nested in a keyed one is partitioned by the outer `key()` when the
166
- inner boundary has none, and by both composed (outermost first, each
167
- URI-encoded, joined by `|`) when it has its own. The route's record adds the
168
- inner boundary's own default key when it has no `key`; the shell partition
169
- does not, since the shell key already carries the URL (a store
170
- `keyGenerator`'s result is kept when it adds anything). See `/caching`,
171
- "Keys nest".
169
+ inner boundary has none, and by both composed (outermost first, joined by
170
+ `|`) when it has its own. Each `key()` result is namespaced as `key:` plus
171
+ its URI encoding (issue #975), exactly as in the record key, so the shell of
172
+ `key: () => "tier:gold"` is `<host><path>:shell|key%3Atier%253Agold`. The
173
+ route's record adds the inner boundary's own default key when it has no
174
+ `key`; the shell partition does not, since the shell key already carries the
175
+ URL (a store `keyGenerator`'s result is kept when it adds anything, an
176
+ enclosing `cache({ store })`'s on another store included, issue #974). An
177
+ enclosing `condition()` that refuses the request means no shell, as the
178
+ route's own does. See `/caching`, "Keys nest" and "Conditions and tags
179
+ inherit".
172
180
 
173
181
  ```tsx
174
182
  cache({ ttl: 300, key: (ctx) => `tier:${tierOf(ctx)}` }, () => [
@@ -180,8 +188,11 @@ cache({ ttl: 300, key: (ctx) => `tier:${tierOf(ctx)}` }, () => [
180
188
  ```
181
189
 
182
190
  This is the sanctioned way to vary shell material by request: the capture
183
- guards still refuse `cookies()`/`headers()` in handler work, while the key
184
- function reads what it needs (`cookies()` included). A `key()` runs once per
191
+ guards still refuse `cookies()`/`headers()`/`ctx.request.headers` in handler
192
+ work, while the key function, a store `keyGenerator`, a `condition()` and a `tags()` function read
193
+ what they need (`cookies()` and `ctx.request.headers` included, at capture
194
+ too). To render the partition value, copy it in middleware with `ctx.set()`
195
+ and read `ctx.get()` in the handler (see `/middleware`). A `key()` runs once per
185
196
  request: the shell read, the record lookup and the capture share its result
186
197
  (each `key()` of a nested chain once).
187
198
  One that throws serves no shell (like the record path renders uncached); a
@@ -209,9 +220,11 @@ value of it.
209
220
 
210
221
  In tests, `serveShellRequest` reports the key the serve path resolved
211
222
  (`result.key`, partition included); `shellCacheKey(url, searchParams,
212
- partition)` builds the same key from the partition. Under nested keyed
213
- scopes, pass the `key()` results as an array, outermost first
214
- (`shellCacheKey(url, undefined, ["tier:gold", "v:a"])`).
223
+ partition)` builds the same key from the `key()` result, namespacing it as
224
+ production does. Under nested keyed scopes, pass the `key()` results as an
225
+ array, outermost first (`shellCacheKey(url, undefined, ["tier:gold",
226
+ "v:a"])`); for a store `keyGenerator` partition pass
227
+ `{ keys, generated }` (`/testing`, `cache-prerender.md`).
215
228
 
216
229
  ## Where PPR sits: the cache onion
217
230
 
@@ -321,8 +334,8 @@ replay its eligible snapshot. In both cases `matchPartial()`:
321
334
  - keeps the existing prefetch key, source scope, and in-flight lock unchanged.
322
335
 
323
336
  This is deliberately invisible to the browser: the response is the same
324
- `RscPayload` shape as any other partial navigation. Captured item/response values
325
- are NOT replayed on this path. Bake-lane loader pins are, when the replayed
337
+ `RscPayload` shape as any other partial navigation. A shell snapshot holds no
338
+ `"use cache"` or loader `cache()` values. Bake-lane loader pins are, when the replayed
326
339
  snapshot is a shell a document request captured, as on a document HIT: a
327
340
  promise-free `ssr: false` loader is served from its pin without running
328
341
  (unless the capture marked it to run: see "On a shell HIT"), and one whose
@@ -352,13 +365,20 @@ backend failure (`CACHE_READ_ERROR`) — also never falls back: it renders
352
365
  uncached, exactly as without ppr. (Document requests treat the same two
353
366
  opt-outs as a cache miss before any shell read: see the serve pipeline above.)
354
367
 
355
- `transition({ when })` is evaluated from the
356
- matched manifest before route handlers on every PPR match, so it can vary by
357
- URL/params/action or middleware context without disabling replay; handler-set
358
- context is unavailable by design. Intercepts, an active nonce, and an
368
+ `transition({ when })` is a browser predicate: the server never evaluates
369
+ it, so a shell, a replay and a cache entry never hold a decision. The route
370
+ carries it as a client reference (attached right before Flight on every
371
+ match, never stored), and the browser decides each navigation from
372
+ `{ kind, from, to }`. Location state middleware sets reaches `to.state` on a
373
+ replay `HIT` (middleware runs on every HIT); a handler's does not (a HIT runs
374
+ no handler). The decision gates only how the browser commits; it never makes
375
+ a replay send a segment. A replay `HIT`
376
+ makes the same segment decision as the live partial path: a segment the client
377
+ holds that the navigation does not re-send (`revalidate()` false, or a layout
378
+ the default keeps) is omitted from the response, so the client keeps its tree
379
+ (a list the client built up, "Load more" style, stays as it is). Intercepts, an active nonce, and an
359
380
  absent/corrupt segment snapshot fall open to the ordinary partial path when
360
- encountered by the shell capture. A transition already replayed from an
361
- explicit cache tier remains frozen by that tier's normal semantics.
381
+ encountered by the shell capture.
362
382
 
363
383
  Two more decisions are made before any shell-store read, so probes and
364
384
  prerendered routes never spend passive `getShell` I/O:
@@ -413,17 +433,17 @@ consumer-invisible.
413
433
 
414
434
  The bounded bypass tokens, grouped by when they are decided:
415
435
 
416
- | Token | Decided | Meaning |
417
- | --------------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
418
- | `method`, `dynamic`, `nonce`, `store-unavailable`, `passive-read-unsupported` | pre-read | request/route/store ineligible for replay |
419
- | `no-navigation-context` | pre-read | no `X-RSC-Router-Client-Path`/`Referer`; a partial match is impossible |
420
- | `prerender-store` | pre-read or match | `Prerender()` route served by its baked artifact (pre-read probe of the normal variant; reclassified post-match when the store actually served, either variant) |
421
- | `intercept` | match | the navigation resolved to an intercept — replay is never armed for intercepts (they keep their normal cache path); no heal capture |
422
- | `cache-disabled` | pre-read or match | `cache(false)` (pre-read, static) or `condition()` false (decided at the lookup); consumer opt-out is absolute |
423
- | `read-error`, `no-entry`, `invalid-version`, `corrupt-entry`, `stale-build-entry` | shell read | no usable shell entry (`no-entry`/`invalid-version`/`corrupt-entry`/`stale-build-entry` schedule the navigation-only heal capture) |
424
- | `no-segment-snapshot` | eligibility | entry exists but its snapshot has no recorded segments for the page; on a route with an enabled `cache()` scope, it heals when the lookup did not refuse (a `condition()` false-at-capture entry becomes replayable) |
425
- | `explicit-cache-hit` | match | the route's own `cache()` tier supplied the match |
426
- | `snapshot-miss` | match | an eligible snapshot was seeded but matching did not consume it |
436
+ | Token | Decided | Meaning |
437
+ | ----------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
438
+ | `method`, `dynamic`, `nonce`, `store-unavailable`, `passive-read-unsupported` | pre-read | request/route/store ineligible for replay |
439
+ | `no-navigation-context` | pre-read | no `X-RSC-Router-Client-Path`/`Referer`; a partial match is impossible |
440
+ | `prerender-store` | pre-read or match | `Prerender()` route served by its baked artifact (pre-read probe of the normal variant; reclassified post-match when the store actually served, either variant) |
441
+ | `intercept` | match | the navigation resolved to an intercept — replay is never armed for intercepts (they keep their normal cache path); no heal capture |
442
+ | `cache-disabled` | pre-read or match | `cache(false)` (pre-read, static) or `condition()` false (decided at the lookup); consumer opt-out is absolute |
443
+ | `read-error`, `no-entry`, `invalid-version`, `corrupt-entry` | shell read | no usable shell entry (`no-entry`/`invalid-version`/`corrupt-entry` schedule the navigation-only heal capture) |
444
+ | `no-segment-snapshot` | eligibility | entry exists but its snapshot has no recorded segments for the page; on a route with an enabled `cache()` scope, it heals when the lookup did not refuse (a `condition()` false-at-capture entry becomes replayable) |
445
+ | `explicit-cache-hit` | match | the route's own `cache()` tier supplied the match |
446
+ | `snapshot-miss` | match | an eligible snapshot was seeded but matching did not consume it |
427
447
 
428
448
  ### Capture-generation invalidation
429
449
 
@@ -522,10 +542,9 @@ curl -s -D - -o /dev/null https://app.example.com/products/1 | grep -i x-rango-s
522
542
  capture diagnoses itself. A stored attempt reports `recordSettleMs`
523
543
  (`record=` in the log line): how long the capture waited for the handler
524
544
  layer to settle (promises it passes or pushes, async server components,
525
- loaders it awaits) and its segment record to encode; `bakeWaitMs` (`bake=`):
526
- how long the slowest remaining bake source (a top-level pushed handle promise
527
- or a bake-lane loader container) held the capture gate; and `entryBytes`
528
- (`entry=`): the stored entry's size. In dev, past 2s, the slow source is also
545
+ loaders it awaits, top-level pushed handle promises and bake-lane loader
546
+ containers) and its segment record to encode; and `entryBytes` (`entry=`):
547
+ the stored entry's size. In dev, past 2s, the settle is also
529
548
  named once per key in a console warning with the remedies (`cache()` the
530
549
  work / move it into a live loader / return it as a nested promise or drop
531
550
  `ssr: false` in a bake-lane loader). In dev, with
@@ -776,7 +795,9 @@ identifiers — served to anonymous visitors.)
776
795
  consumer's `<Suspense>` on every HIT (the same mask as bake-lane loader data,
777
796
  at any depth of plain objects, arrays and JSX props; a promise inside a
778
797
  `Map`, `Set`, or class instance is not masked). A live-lane
779
- loader never runs at capture, so all of its pushes happen per request.
798
+ loader never runs at capture unless a handler or an `ssr: false` loader
799
+ awaits it; either
800
+ way its pushes, and those of the loaders it awaits, are live on every HIT.
780
801
 
781
802
  ### Want a hole for already-resolved data?
782
803
 
@@ -865,18 +886,41 @@ loader(ProductLoader, { ssr: false }), // promise-free: baked, not run on a HIT
865
886
  loader(ReviewsLoader), // live: the hole, under loading()
866
887
  ```
867
888
 
889
+ **A hole never reads the shell's copy of a value.** The shell stores its
890
+ recorded segments and the `ssr: false` loaders' baked containers, and no
891
+ `"use cache"` or loader `cache()` value. Every cache read on a HIT goes to
892
+ the store: a live loader's, a nested promise's, and the body of an
893
+ `ssr: false` loader that runs on the HIT. So a live loader that reads the
894
+ same `"use cache"` key as an `ssr: false` loader shows the store's current
895
+ value once it refreshes, while the baked container keeps the capture's. If a
896
+ hole needs a stable value, give it its own cache (`"use cache"` with a
897
+ profile, or a loader `cache()`); the shell never provides one.
898
+
868
899
  **Handle pushes from a bake-lane loader appear once.** The capture records
869
900
  the settled, thenable-free handle pushes of every loader body it runs (the
870
901
  prelude rendered them): the `ssr: false` loader's own, those of the loaders
871
- it awaits with `ctx.use()`, and those its own `cache()` entry replays. Every HIT restores them with the handler layer. A
872
- loader that does run on the HIT (a hole-carrying bake-lane loader, or a live
873
- loader awaiting the same loader) replaces its restored pushes in place, so
874
- any handle, deduping or not, shows each value once; a push that lands after
875
- the document's handle snapshot reaches the client after hydration. A push the
876
- capture cannot record (a deferred push, or one holding a promise) marks the
877
- loader records to run: those
878
- bodies then still run on each HIT, in the background, so the push reaches the
879
- page.
902
+ it awaits with `ctx.use()`, and those its own `cache()` entry replays. Every
903
+ HIT restores them with the handler layer, and they stand: a bake-lane loader
904
+ that does run on the HIT (a hole-carrying one) reads the store, so its settled
905
+ pushes, and those a `"use cache"` hit inside its body replays, are dropped
906
+ instead of replacing the restored ones. Any handle, deduping or not, shows
907
+ each value once, and it is the value the prelude rendered. A loader the route
908
+ also registers on the live lane is a hole: its live run replaces its restored
909
+ pushes, and those of the loaders it awaits, so they show its live value, even
910
+ when a bake-lane loader running on the HIT awaits it. A run that throws or
911
+ makes no push shows none of the capture's once its run ends; a hole slower
912
+ than the handler barrier shows the shell's copy in the first snapshot until
913
+ then. A hole with its own loader `cache()` that hits shows the pushes that
914
+ entry recorded, none if it recorded none, matching its data. A dependency the route does not
915
+ register, which an `ssr: false` loader awaited at capture outside any live
916
+ loader, is on neither lane: its settled pushes stay as the prelude rendered
917
+ them, even where a live loader also awaits it, while its data is fresh. To
918
+ keep its pushes live, declare it as its own `loader()` on the route. A push the capture cannot record (a
919
+ deferred push, or one holding a promise) marks every loader record of the
920
+ page to run: those bodies then still run on each HIT, in the background, and
921
+ that push reaches the page (from the loader's own `cache()` entry when it
922
+ hits) with the run's value; a push that lands after the document's handle snapshot
923
+ reaches the client after hydration.
880
924
 
881
925
  Four hard edges (each e2e/unit-pinned):
882
926
 
@@ -893,7 +937,7 @@ Four hard edges (each e2e/unit-pinned):
893
937
  response. The one exception: a handler that calls `ctx.dynamic()` FIRST
894
938
  re-permits its own header/cookie write (#735) — a dynamic() render never
895
939
  HITs, so the write is deterministic (see "Opting out per request").
896
- - **Identity refuses.** `cookies()`/`headers()` inside a bake-lane loader
940
+ - **Identity refuses.** `cookies()`/`headers()`/`ctx.request.headers` inside a bake-lane loader
897
941
  throws during capture and the capture REFUSES (deterministic, once-per-key
898
942
  warned) — identity can never bake into the shared shell. Drop the loader's
899
943
  `ssr: false` flag (the live lane is exempt; give it a boundary) or move the
@@ -921,7 +965,7 @@ Four hard edges (each e2e/unit-pinned):
921
965
  - **Baked containers show CAPTURE-time data** for the shell's lifetime on
922
966
  document GETs, and on soft navigations that replay a shell captured by a
923
967
  document request (`x-rango-ppr-replay: HIT`). Live loaders and their
924
- item/response reads remain fresh. That IS the bake lane's meaning; if a value
968
+ `"use cache"` reads remain fresh. That IS the bake lane's meaning; if a value
925
969
  must be fresh on every serve, it belongs on the live lane (no `ssr: false`) or
926
970
  in a nested promise.
927
971
 
@@ -939,7 +983,8 @@ their data should bake vs stay live. Your levers, in order of preference:
939
983
  shared/config data returns as plain values (bakes, pinned per shell);
940
984
  per-request data returns as NESTED promises consumed under the widget's own
941
985
  `<Suspense>` (live holes). No `loading()`, no restructuring. One wall: a
942
- bake-lane loader that reads `cookies()`/`headers()` refuses the capture —
986
+ bake-lane loader that reads `cookies()`/`headers()`/`ctx.request.headers`
987
+ refuses the capture —
943
988
  identity belongs on the live lane (a separate unflagged loader); a nested
944
989
  promise inside the flagged loader still runs during capture and trips the
945
990
  guard.
@@ -997,8 +1042,9 @@ The identity rule, stated once: per-user data on a PPR page lives in a
997
1042
  live-lane loader (no `ssr: false`) consumed CLIENT-side with `useLoader` under
998
1043
  `loading()` or an inline `<Suspense>`, or in a NESTED promise inside a
999
1044
  bake-lane loader (a hole, fresh per request). A nested promise's body still
1000
- runs at capture, so it must not call `cookies()`/`headers()` itself — identity
1001
- reads belong in the live-lane loader. Reading `cookies()`, `headers()`, or a
1045
+ runs at capture, so it must not call `cookies()`/`headers()` or read
1046
+ `ctx.request.headers` itself — identity reads belong in the live-lane loader.
1047
+ Reading `cookies()`, `headers()`, `ctx.request.headers`, or a
1002
1048
  `{ cache: false }` variable anywhere the capture waits for refuses the capture
1003
1049
  by construction: handler/render code, a promise the handler passes or pushes,
1004
1050
  an async server component, a bake-lane loader, and a loader a handler awaits
@@ -1037,8 +1083,8 @@ Three levers, in preference order:
1037
1083
 
1038
1084
  Head material (Meta) pushed by HANDLERS cannot be a hole — the head is shell —
1039
1085
  so those promises bake by design; make them cheap with lever 2.
1040
- `recordSettleMs` and `bakeWaitMs` on the capture debug event tell you what each
1041
- capture actually paid. A
1086
+ `recordSettleMs` on the capture debug event tells you what each capture
1087
+ actually paid. A
1042
1088
  Meta pushed by a LIVE-lane loader is different: those loaders are masked at
1043
1089
  capture, so the push happens at request time and applies client-side
1044
1090
  (`metadata.handlesLate`) — it is never in the cached shell's head, by
@@ -1078,11 +1124,14 @@ serve-time: the commit point runs the full chain on EVERY serve.
1078
1124
 
1079
1125
  Because handlers on uncached segments EXECUTE during capture — along with
1080
1126
  everything they produce and every BAKE-lane loader — the capture guard is
1081
- load-bearing: `cookies()`, `headers()`, a theme read, and a `{ cache: false }`
1082
- variable read THROW during a capture render, and the capture
1127
+ load-bearing: `cookies()`, `headers()`, `ctx.request.headers`,
1128
+ `getRequestContext().cookie()` / `.cookies()`, a theme read, and a
1129
+ `{ cache: false }` variable read THROW during a capture render, and the capture
1083
1130
  is refused even when the code catches that throw, so identity can never leak
1084
1131
  into a shared shell through them. Live-lane loaders (every loader without
1085
- `ssr: false`) are exempt: masked at capture, they never run there.
1132
+ `ssr: false`) are exempt when only a client component reads them: masked at
1133
+ capture, they never run there. One a handler or an `ssr: false` loader awaits
1134
+ runs at capture, and its identity reads refuse it.
1086
1135
 
1087
1136
  ## allReady: the SEO/bot story
1088
1137
 
@@ -1102,7 +1151,8 @@ middleware on every serve. A 401/redirect short-circuit returns before any
1102
1151
  shell byte.
1103
1152
 
1104
1153
  **(b) Request-scoped reads refuse the capture.** During the background capture
1105
- render, `cookies()`, `headers()`, the visitor's theme (`ctx.theme`,
1154
+ render, `cookies()`, `headers()`, the raw reads `ctx.request.headers` and
1155
+ `getRequestContext().cookie()` / `.cookies()`, the visitor's theme (`ctx.theme`,
1106
1156
  `getRequestContext().theme`), and `ctx.get()` of a `{ cache: false }` variable
1107
1157
  (`createVar({ cache: false })`, or a value written with
1108
1158
  `ctx.set(..., { cache: false })`) THROW, wherever the capture waits for them:
@@ -1110,14 +1160,15 @@ a handler, a promise it passes or pushes, an async server component, a
1110
1160
  bake-lane loader, and a loader a handler awaits (`await ctx.use(Loader)`). The
1111
1161
  capture is refused even if your code catches the throw, and the route keeps
1112
1162
  serving MISSes with a once-per-key warning. `ctx.dynamic()` called there
1113
- refuses it too. The live lane (no `ssr: false`) stays exempt: masked at capture, it never
1114
- runs there.
1163
+ refuses it too. The live lane (no `ssr: false`) stays exempt when only a client
1164
+ component reads it; one a handler or an `ssr: false` loader awaits runs at
1165
+ capture, and its identity reads refuse it.
1115
1166
 
1116
1167
  **(c) Residual hazard — middleware-derived per-user state.** A NORMAL `ctx`
1117
1168
  variable (no `cache: false`) set by an upstream auth middleware and rendered by
1118
1169
  shell material is photographed into the SHARED shell (the capture inherits
1119
- post-middleware state), and so is a raw `ctx.request.headers` read. The guard
1120
- cannot see either. That is scope fidelity working as designed — for shared
1170
+ post-middleware state). The guard cannot see it. That is scope fidelity
1171
+ working as designed — for shared
1121
1172
  values. It reaches handlers and BAKE-LANE LOADERS alike: a loader reading a
1122
1173
  middleware-provided session object (`ctx.get("session")`) never calls
1123
1174
  `cookies()` itself, so whatever it returns as settled container data is
@@ -1183,13 +1234,13 @@ path(
1183
1234
  );
1184
1235
  ```
1185
1236
 
1186
- | Field | Default | Notes |
1187
- | ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1188
- | `ttl` | `300` | shell freshness window in seconds (`ppr: true` uses the default) |
1189
- | `swr` | — | stale window: serve the stale shell + background recapture |
1190
- | `tags` | — | operational tags UNIONED with the tags the capture collects — see "Invalidation" below |
1191
- | `maxSnapshotBytes` | 8 MiB | cap on the loader pins in the entry's snapshot (bake-lane loader records and the cached items/responses those loaders read). The recorded handler layer is exempt. Over the cap the pins are dropped and the shell is stored with its handler layer (warned once per key); bake-lane loaders then read the live store, and drift is repaired client-side |
1192
- | `captureTimeout` | 15000ms | ONE capture deadline: the handler layer settling (promises it passes or pushes, async server components, loaders it awaits), bake-lane loaders, and the prerender; a capture that misses it stores nothing rather than a partial shell |
1237
+ | Field | Default | Notes |
1238
+ | ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1239
+ | `ttl` | `300` | shell freshness window in seconds (`ppr: true` uses the default) |
1240
+ | `swr` | — | stale window: serve the stale shell + background recapture |
1241
+ | `tags` | — | operational tags UNIONED with the tags the capture collects — see "Invalidation" below |
1242
+ | `maxSnapshotBytes` | 8 MiB | cap on the loader pins in the entry's snapshot (the bake-lane loader records). The recorded handler layer is exempt. Over the cap the pins are dropped and the shell is stored with its handler layer (warned once per key); bake-lane loaders then read the live store, and drift is repaired client-side |
1243
+ | `captureTimeout` | 15000ms | ONE capture deadline: the handler layer settling (promises it passes or pushes, async server components, loaders it awaits), bake-lane loaders, and the prerender; a capture that misses it stores nothing rather than a partial shell |
1193
1244
 
1194
1245
  A shell never outlives the route `cache()` entry it was captured from: under
1195
1246
  a route `cache()`, the shell is fresh no longer than that entry is (at most
@@ -1238,8 +1289,8 @@ render-callable `cacheTag()` in a plain server component (no
1238
1289
  `"use cache"`/`cache()` in its tree) — plus the tags of loaders on the BAKE
1239
1290
  lane (they execute during capture and their data is in the shell). A tag
1240
1291
  recorded outside what the shell renders is not on it. A live-lane loader's tags (every loader without `ssr: false`)
1241
- attach only when a handler consumes its value with `ctx.use()`, which bakes
1242
- it; one read only by the client stays off, since it is masked at capture and
1292
+ attach only when a handler or an `ssr: false` loader consumes its value with
1293
+ `ctx.use()`, which bakes it; one read only by the client stays off, since it is masked at capture and
1243
1294
  its hole is already live. `ppr.tags` adds
1244
1295
  operational tags the render cannot know (a tenant id, a deploy marker).
1245
1296
 
@@ -1267,7 +1318,7 @@ evicted by tag at all — move always-fresh data into a live-lane loader (no
1267
1318
 
1268
1319
  ## Pitfalls
1269
1320
 
1270
- - **A bake-lane loader that reads `cookies()`/`headers()`**: the capture is
1321
+ - **A bake-lane loader that reads `cookies()`/`headers()`/`ctx.request.headers`**: the capture is
1271
1322
  REFUSED (deterministic, once-per-key warned) — the route keeps serving
1272
1323
  MISSes. Move identity onto the live lane (drop `ssr: false`, or split the read into
1273
1324
  a separate unflagged loader; give the reader a boundary). A nested promise
@@ -1299,8 +1350,9 @@ evicted by tag at all — move always-fresh data into a live-lane loader (no
1299
1350
  bake-lane loader data — plain BAKE-lane loader data bakes just like handler
1300
1351
  material.
1301
1352
  - **The session-object bake trap (the guard cannot save you here)**: the
1302
- capture guard sees `cookies()`, `headers()`, and `{ cache: false }` variable
1303
- reads ONLY. A handler or bake-lane loader reading a NORMAL middleware-provided
1353
+ capture guard sees `cookies()`, `headers()`, `ctx.request.headers`, the theme
1354
+ and `{ cache: false }` variable reads ONLY. A handler or bake-lane loader
1355
+ reading a NORMAL middleware-provided
1304
1356
  session object (`ctx.get("session")`) refuses nothing. Inside a bake-lane
1305
1357
  loader, per-user data survives ONLY behind a nested promise — the shape is
1306
1358
  the declaration, and it holds for BOTH branches regardless of settle timing
@@ -1352,10 +1404,10 @@ evicted by tag at all — move always-fresh data into a live-lane loader (no
1352
1404
  same `"use cache"` entry reads it from the store on every HIT. It is still
1353
1405
  cached under its own profile; once the entry expires or is invalidated and
1354
1406
  refreshes, the hole shows the refreshed value next to the shell's old one. The
1355
- capture stores only what a HIT reads: every HIT replays the handler layer
1356
- from the captured segment record, so the `"use cache"` items only handler
1357
- code read are not stored. When a bake-lane loader read the same entry at
1358
- capture, the shell keeps it, and the hole gets that pinned value too.
1407
+ shell stores no `"use cache"` value, only its recorded handler output and the
1408
+ `ssr: false` loaders' baked containers. A hole reads the store even when a
1409
+ bake-lane loader read the same entry at capture: the baked container keeps
1410
+ the capture's value, and the hole shows the current entry.
1359
1411
  - **Uncached nondeterminism in server output is frozen, not drifting**: a raw
1360
1412
  `Date.now()` / `Math.random()` / uncached `fetch` in a handler or server
1361
1413
  component renders once per capture, and the prelude and every HIT show that
@@ -795,8 +795,8 @@ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
795
795
  intercept-resolution middleware runs live. This handles `when` config conditions
796
796
  that prevented pre-rendering.
797
797
 
798
- The `when` config selector receives an `InterceptSelectorContext` with `from.pathname`
799
- which is unknown at build time. All intercepts are pre-rendered unconditionally;
798
+ The `when` config selector receives an `InterceptSelectorContext` whose `from`
799
+ location (the page being left) is unknown at build time. All intercepts are pre-rendered unconditionally;
800
800
  `when` is evaluated at runtime by the intercept-resolution middleware.
801
801
 
802
802
  ### Example: Pre-rendered route with intercept
@@ -820,7 +820,7 @@ layout(ShopLayout, () => [
820
820
  "@modal",
821
821
  ".detail",
822
822
  <ProductModal />,
823
- { when: ({ from }) => from.pathname === "/shop" },
823
+ { when: ({ from }) => from.url.pathname === "/shop" },
824
824
  () => [loader(ProductLoader)],
825
825
  ),
826
826
  ])
@@ -131,11 +131,13 @@ stated, greppable contract.
131
131
  - **The consumption-lane rule.** For every shared artifact (`cache()`,
132
132
  `"use cache"`, the PPR shell): server-side handler consumption
133
133
  (`await ctx.use(loader)`) yields a BAKED copy — the capture-time value
134
- freezes into the shared artifact. Identity reads (`cookies()`/`headers()`,
135
- a `{ cache: false }` variable) inside that loader are permitted under
136
- `cache()`/`"use cache"` (a documented footgun; see `/caching` → "Cache
137
- purity & tainted objects") but REFUSE a PPR capture: the route serves
138
- uncached until the read moves to a live loader. Client-side consumption
134
+ freezes into the shared artifact. Identity reads (`cookies()`/`headers()`/
135
+ `ctx.request.headers`, a `{ cache: false }` variable) inside that loader
136
+ are permitted under a route `cache()` (a documented footgun; see
137
+ `/caching` → "Cache purity & tainted objects"). They throw inside
138
+ `"use cache"` (when the cached function is the first to read the loader in
139
+ the request; an already-started loader's value is stored unchecked) and REFUSE a PPR capture: the route serves uncached until
140
+ the read moves to a live loader. Client-side consumption
139
141
  (`useLoader` in a `"use client"` component) is the LIVE lane. DSL
140
142
  `loader()` segments follow their PPR lane (only `ssr: false` bakes; see
141
143
  `/ppr` → The loader lane rule).
@@ -146,7 +148,7 @@ stated, greppable contract.
146
148
  only when its return carries promises). Live data belongs in a loader without
147
149
  `ssr: false`, read with `useLoader` under `loading()` or an inline
148
150
  `<Suspense>`.
149
- - Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
151
+ - Inside `"use cache"`: `cookies()`/`headers()`/`ctx.request.headers` and `ctx` side-effects
150
152
  (`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
151
153
  is captured on miss and replayed on hit. A non-cacheable variable read
152
154
  (`createVar({ cache: false })`) throws too — see the correctness bullet below.
@@ -163,6 +165,23 @@ stated, greppable contract.
163
165
  - Nested caches: the outer cache window bounds the inner — an inner shorter TTL
164
166
  only applies when the enclosing cache recomputes; put a value in a loader if it
165
167
  must be fresher. See `/cache-guide` → "Combining Both".
168
+ - **A predicate runs where its effect lands.** A server-tree `revalidate()`
169
+ decides what the server re-sends, so it runs on the server; an `intercept({
170
+ when })` selector decides what the server renders, so it runs on the server;
171
+ a `clientUrls()` `revalidate()` and `transition({ when })` decide a browser
172
+ commit, so they run in the browser. A browser predicate sees URLs, params,
173
+ route names, history state and action data, never `get()`, `env` or handler
174
+ state; in a server `urls()` it is written inline (the build hoists it into a
175
+ client module) or imported from a `"use client"` module (`/view-transitions`).
176
+ - **Vocabulary.** `revalidate()` keeps React Router's `shouldRevalidate`
177
+ dialect (`currentUrl`/`nextUrl`, `currentParams`/`nextParams`). Navigation
178
+ selectors (`intercept({ when })`, `transition({ when })`) use `from`/`to`
179
+ locations (`{ url, params, routeName }`, plus `state` in the browser).
180
+ - **A throwing predicate yields the conservative default.** For
181
+ `transition({ when })` that is no hold (an urgent commit) plus
182
+ `console.error`; for a `revalidate()` it is the default decision it would
183
+ have deferred to, logged; for an `intercept({ when })` selector it is no
184
+ intercept (the full page renders), logged with the route name.
166
185
 
167
186
  ## Don't confuse
168
187
 
@@ -608,6 +608,8 @@ whether the routes mount.
608
608
 
609
609
  A route can configure its own `transition()` — the wrap goes around the route's component itself (routes are leaves; they have no separate default outlet channel). If the route component renders a `<ParallelOutlet />` directly, that slot remains inside the route's VT subtree, so prefer mounting parallel slots in a layout when combining intercept modals with route-level transitions. See [skills/view-transitions](../view-transitions/SKILL.md) for examples and the wrap-location rules across layouts, routes, and slots.
610
610
 
611
+ `transition({ when })` and `revalidate()` decide different things: `when` (a browser predicate) gates the hold, `revalidate()` decides whether the route segment is re-sent. A `revalidate()` returning false keeps the client's segment on a navigation whatever `when` returns, including a `ppr` navigation replayed from the shell.
612
+
611
613
  ## Handler-attached `.use`
612
614
 
613
615
  Page handlers can carry their own loader, middleware, error boundaries, parallels, and other defaults via a `.use` callback — so the page is self-contained and reusable across mount sites without re-wiring the same items.
@@ -89,31 +89,32 @@ The single rule that drives everything:
89
89
 
90
90
  Each primitive links to its sub-file (API + recipe + caveats).
91
91
 
92
- | The behavior is… | Layer | Primitive | Import root |
93
- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- | -------------------------------- |
94
- | a `reverse` / `href` / params / env TYPE contract (a wrong name or missing param must not compile) | types (tsc) | [`@ts-expect-error` / `expectTypeOf`](./reverse-and-types.md) | `@rangojs/router/client` + `tsc` |
95
- | one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
96
- | a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
97
- | one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
98
- | a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
99
- | a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ kept, whenContext }`; `{ ppr: true }` for pre-handler timing) | `@rangojs/router/testing` |
100
- | a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | [`runClientRevalidate`](./predicates.md) (production chain evaluator + locked defaults) | `@rangojs/router/testing` |
101
- | a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
102
- | a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
103
- | a stale indicator from `useLoader().isLoading` while a `transition()` navigation re-runs the loader | unit (DOM) | [`renderRoute` + `navigate(url, { loaders })`](./client-components.md#held-navigation) | `@rangojs/router/testing/dom` |
104
- | a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
105
- | a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
106
- | the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
107
- | a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
108
- | navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
109
- | a `"use cache"` hit in a handler (the body runs once across renders; the value round-trips) | RSC unit | [`renderHandler` + seeded `cacheStore`](./render-handler.md#a-use-cache-hit) | `@rangojs/router/testing/flight` |
110
- | a `"use cache"` hit in a loader (the same, in the react-server project) | RSC unit | [`runLoader` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
111
- | a loader's own `cache()` (HIT, `updateTag()` of a tag its body recorded) | RSC unit | [`runLoader` + `cache` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
112
- | cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
113
- | a `ppr` route's shell: MISS -> capture -> HIT, what the shell froze vs what a HIT renders live, tag eviction, navigation replay | RSC unit | [`serveShellRequest`](./serve-shell-request.md) | `@rangojs/router/testing/flight` |
114
- | PPR shell HIT/MISS in real HTML (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-and-navigation-replay) | `@rangojs/router/testing[/e2e]` |
115
- | generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
116
- | a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
92
+ | The behavior is… | Layer | Primitive | Import root |
93
+ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- | -------------------------------- |
94
+ | a `reverse` / `href` / params / env TYPE contract (a wrong name or missing param must not compile) | types (tsc) | [`@ts-expect-error` / `expectTypeOf`](./reverse-and-types.md) | `@rangojs/router/client` + `tsc` |
95
+ | one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
96
+ | a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
97
+ | one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
98
+ | a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
99
+ | a `transition({ when })` browser predicate against `{ kind, from, to, action }` | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ applied, gatedOff, context }`) | `@rangojs/router/testing` |
100
+ | a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | [`runClientRevalidate`](./predicates.md) (production chain evaluator + locked defaults) | `@rangojs/router/testing` |
101
+ | a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
102
+ | a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
103
+ | a stale indicator from `useLoader().isLoading` while a `transition()` navigation re-runs the loader | unit (DOM) | [`renderRoute` + `navigate(url, { loaders })`](./client-components.md#held-navigation) | `@rangojs/router/testing/dom` |
104
+ | a component reading `useLocationState(Def)` after `router.push(url, { state: [Def(value)] })` | unit (DOM) | [`renderRoute` + `withLocationStateKey`](./client-components.md#location-state) | `@rangojs/router/testing[/dom]` |
105
+ | a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
106
+ | a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
107
+ | the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
108
+ | a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
109
+ | navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
110
+ | a `"use cache"` hit in a handler (the body runs once across renders; the value round-trips) | RSC unit | [`renderHandler` + seeded `cacheStore`](./render-handler.md#a-use-cache-hit) | `@rangojs/router/testing/flight` |
111
+ | a `"use cache"` hit in a loader (the same, in the react-server project) | RSC unit | [`runLoader` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
112
+ | a loader's own `cache()` (HIT, `updateTag()` of a tag its body recorded) | RSC unit | [`runLoader` + `cache` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
113
+ | cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
114
+ | a `ppr` route's shell: MISS -> capture -> HIT, what the shell froze vs what a HIT renders live, tag eviction, navigation replay | RSC unit | [`serveShellRequest`](./serve-shell-request.md) | `@rangojs/router/testing/flight` |
115
+ | PPR shell HIT/MISS in real HTML (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-and-navigation-replay) | `@rangojs/router/testing[/e2e]` |
116
+ | generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
117
+ | a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
117
118
 
118
119
  Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
119
120
  `/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
@@ -144,6 +145,7 @@ beside their function):
144
145
  | | `runInRequestContext`, `createTestRequestContext`, `runWithRequestContext`, `toRequest`, `seedVariables` | [`server-actions.md`](./server-actions.md) |
145
146
  | | `runTransitionWhen`, `runClientRevalidate` | [`predicates.md`](./predicates.md) |
146
147
  | | `collectHandle` | [`handles.md`](./handles.md) |
148
+ | | `withLocationStateKey` | [`client-components.md`](./client-components.md) |
147
149
  | | `dispatch` | [`response-routes.md`](./response-routes.md) |
148
150
  | | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
149
151
  | | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |