@rangojs/router 0.17.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -214,24 +214,41 @@ intercept(
214
214
  "product",
215
215
  <ProductModal />,
216
216
  // Only intercept when coming from a different section
217
- { when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
217
+ { when: ({ from }) => !from.url.pathname.startsWith("/shop/product/") },
218
218
  () => [
219
219
  loader(ProductLoader),
220
220
  ]
221
221
  )
222
222
  ```
223
223
 
224
- `when` is a synchronous match-time selector. It receives `from` / `to` (URLs),
225
- `fromRouteName` / `toRouteName` (named routes only), `params` (the target's),
226
- `segments` (the client's current segment path and ids), `request`, and `env`.
227
- Pass an array of predicates for AND logic (all must return true). Omit `when`
228
- entirely and the intercept always activates. `when` is not re-evaluated during
229
- action revalidation, so an open modal stays open after an action.
224
+ `when` is a synchronous match-time selector that runs on the server. It
225
+ receives `from` and `to` (the destination), each `{ url, params, routeName }`
226
+ — the shape `transition({ when })` sees, without `state`, because history
227
+ state never reaches the server — plus `segments` (the client's current
228
+ segment path and ids), `request`, and `env`. `from` is the page the
229
+ navigation leaves, except while an intercept is open: then it is the
230
+ intercept's source page, the page under the modal (`match-api.ts`,
231
+ `navigation-snapshot.ts` `effectiveFromUrl`). Opening `/item/1` as a modal
232
+ over `/list/open` and then navigating to `/item/2` from inside the modal
233
+ gives `from.url.pathname === "/list/open"`, not `/item/1`. That differs from
234
+ `transition({ when })`'s `from`, which is always the committed location on
235
+ screen. `routeName` is set for named routes only. Pass an array of predicates
236
+ for AND logic (all must return true). Omit `when` entirely and the intercept
237
+ always activates. `when` is not re-evaluated during action revalidation, so
238
+ an open modal stays open after an action. A selector that throws does not
239
+ intercept: the full page renders and the error is logged with the route name.
240
+
241
+ A prefetch of a route an intercept targets is stored per source page, whether
242
+ or not the intercept applies from that page (`rsc-rendering.ts`
243
+ `x-rsc-prefetch-scope: source`, set from `MatchResult.interceptTargeted`). A
244
+ full page prefetched from a page where `when` is false never serves a click
245
+ from a page where it is true. Routes no intercept targets keep one shared
246
+ prefetch entry.
230
247
 
231
248
  ```typescript
232
249
  // Intercept only when opened from the shop index
233
250
  intercept("@modal", "product", <ProductModal />, {
234
- when: ({ fromRouteName }) => fromRouteName === "index",
251
+ when: ({ from }) => from.routeName === "index",
235
252
  })
236
253
  ```
237
254
 
@@ -242,8 +259,8 @@ intercept(
242
259
  <ProductModal />,
243
260
  {
244
261
  when: [
245
- ({ from }) => from.pathname.startsWith("/shop"),
246
- ({ params }) => params.slug !== "featured",
262
+ ({ from }) => from.url.pathname.startsWith("/shop"),
263
+ ({ to }) => to.params.slug !== "featured",
247
264
  ],
248
265
  },
249
266
  () => [
@@ -401,7 +418,7 @@ layout(ShopLayout, () => [
401
418
  "@modal",
402
419
  ".detail",
403
420
  <ProductModal />,
404
- { when: ({ from }) => from.pathname.startsWith("/shop") },
421
+ { when: ({ from }) => from.url.pathname.startsWith("/shop") },
405
422
  () => [loader(ProductLoader)],
406
423
  ),
407
424
  ])
@@ -482,7 +499,7 @@ export const shopPatterns = urls(({
482
499
  "@modal",
483
500
  ".product", // dot-local: resolves to "shop.product" in this include
484
501
  <ProductModalContent />,
485
- { when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
502
+ { when: ({ from }) => !from.url.pathname.startsWith("/shop/product/") },
486
503
  () => [
487
504
  layout(<ModalWrapper />),
488
505
  loading(<ProductModalSkeleton />),
@@ -7,7 +7,8 @@ argument-hint: "[loader]"
7
7
  # Data Loaders with loader()
8
8
 
9
9
  A loader is a server function created with `createLoader()` and registered on a
10
- route segment with `loader()`. It runs fresh on every request, streams its
10
+ route segment with `loader()`. It runs fresh on every request (on a `ppr` route,
11
+ one registered with `ssr: false` is baked into the shell instead), streams its
11
12
  result to the client, and client components read it with `useLoader()`. This
12
13
  skill covers defining, registering, consuming, revalidating, caching, and
13
14
  client-fetching loaders.
@@ -121,25 +122,39 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
121
122
  > (plain GET only). See the hooks skill ("Scoping refetch with a `key`" and
122
123
  > "Refreshing multiple loaders together").
123
124
  > - `cache({ key })` — a **server** cache identity (storage hit/miss/ttl/swr).
125
+ > On a route `cache()` over a `ppr` route, each key value also gets its own
126
+ > shell; keep the value set small.
124
127
  > - `revalidate()` — which **server** segments/loaders recompute during
125
128
  > navigation and action refreshes.
126
129
 
127
130
  DSL loaders are the **live data layer**: they resolve fresh on every request,
128
131
  even when the route is inside a `cache()` boundary, so `cache()` gives you
129
132
  cached UI + fresh data by default. See "Loaders: The Live Data Layer" below.
133
+ The exception is a `ppr` route's `ssr: false` loader: it bakes into the shell,
134
+ and a shell HIT runs it only when its return carries promises.
130
135
 
131
136
  ### Cache safety
132
137
 
133
- DSL loaders can safely read `createVar({ cache: false })` variables
134
- because they are always resolved fresh. The read guard is bypassed for
135
- loader functions — they never produce stale data. A `"use cache"` function
136
- the loader calls is not a loader body: a non-cacheable read inside it throws,
137
- also through the loader's `ctx` passed in, so read the var in the loader and
138
- pass the value in as an argument. Passing the loader's `ctx` itself is fine:
139
- the call keys by route, params and search, like a handler `ctx` (`/use-cache`).
140
- A fetchable loader called with a request body (`load({ method: "POST", body })`
141
- or form data) is the exception: the body is not in the key, so the call runs
142
- uncached. Pass the body fields the function needs as arguments instead.
138
+ A DSL loader without `ssr: false` can read `createVar({ cache: false })`
139
+ variables: it runs on every request, `cache()` and PPR shell HITs included, so
140
+ the read guard is bypassed for it. On a `ppr` route the capture runs an
141
+ `ssr: false` loader, and a loader a handler awaits, and bakes their values into
142
+ the shell: a `cookies()`, `headers()`, `ctx.request.headers` or
143
+ `{ cache: false }` read there refuses the capture, so the route keeps
144
+ rendering without a shell (warned once per key).
145
+ A loader bound with its own `cache()` is the other exception: its value is
146
+ stored and shared, so a miss whose body read `cookies()`, `headers()`,
147
+ `ctx.request.headers`, a non-cacheable variable or the theme fails unless the
148
+ binding has a `key()` or a
149
+ store `keyGenerator`, which must then include them (see "Cache Key"). A
150
+ `"use cache"` function the loader calls is not a loader body: a non-cacheable
151
+ read inside it throws, also through the loader's `ctx` passed in, so read the
152
+ var in the loader and pass the value in as an argument. Passing the loader's
153
+ `ctx` itself is fine: the call keys by route, params and search, like a handler
154
+ `ctx` (`/use-cache`). A fetchable loader called with a request body
155
+ (`load({ method: "POST", body })` or form data) is the exception: the body is
156
+ not in the key, so the call runs uncached. Pass the body fields the function
157
+ needs as arguments instead.
143
158
 
144
159
  ### ctx.use(Loader) — escape hatch
145
160
 
@@ -157,7 +172,9 @@ path("/product/:slug", async (ctx) => {
157
172
  ])
158
173
  ```
159
174
 
160
- `ctx.use(Loader)` is a live read, memoized for the request: however many
175
+ `ctx.use(Loader)` is a per-request read on any render that runs the handler,
176
+ memoized for the request (a route `cache()` HIT or a `ppr` shell HIT runs no
177
+ handler, so the handler's output from the read is replayed): however many
161
178
  times it is called, and whether or not the loader is also registered with
162
179
  `loader()`, it runs once per request. It never reads or writes the loader
163
180
  store cache on its own: a loader's `cache()` belongs to its DSL binding
@@ -171,11 +188,16 @@ the tree).
171
188
  - The handler output depends on the loader data. If the route is inside
172
189
  `cache()`, the handler is cached with the loader result baked in —
173
190
  defeating the live data guarantee.
174
- - The same holds under a PPR shell capture (`/ppr`): handler consumption is
175
- the BAKED lane — the loader executes at capture (identity reads permitted)
176
- and the rendered value is a capture-time copy; `useLoader` client-side is
177
- the live lane. One rule across `cache()`, `"use cache"`, and PPR: the
178
- consumption-lane rule (`/rango` → Invariants).
191
+ - The same holds under a PPR shell capture (`/ppr`): handler consumption is the
192
+ BAKED lane — the loader executes at capture and the rendered value is a
193
+ capture-time copy served on every HIT (a HIT never runs the handler);
194
+ `useLoader` of a loader without `ssr: false` is the live lane (an `ssr: false`
195
+ loader is the bake lane however it is read). Unlike `cache()`, an identity
196
+ read inside that loader (`cookies()`, `headers()`, `ctx.request.headers`, a
197
+ `{ cache: false }` variable) refuses the capture, so the route stays
198
+ uncached. One rule across
199
+ `cache()`, `"use cache"`, and PPR: the consumption-lane rule (`/rango` →
200
+ Invariants).
179
201
  - Non-cacheable variable reads (`createVar({ cache: false })`) inside the
180
202
  handler still throw, even if the data came from a loader.
181
203
  - Prefer DSL `loader()` + client `useLoader()` for data that depends on
@@ -237,7 +259,8 @@ export const ProductLoader = createLoader(async (ctx) => {
237
259
  // Platform bindings (DB, KV, etc.) — plain bindings from createRouter<TEnv>()
238
260
  const db = ctx.env.DB;
239
261
 
240
- // Request headers
262
+ // Request headers (guarded like headers(): an unkeyed loader cache() fill
263
+ // or a ppr capture of this loader refuses the read)
241
264
  const auth = ctx.request.headers.get("Authorization");
242
265
 
243
266
  // Variables set by middleware (from Rango.Vars augmentation)
@@ -491,11 +514,16 @@ Loaders resolve fresh on every request, even when the route's UI segments are
491
514
  served from cache. Route-level `cache()` caches rendered segments but never
492
515
  loader data: loaders are excluded when the segments are stored and re-resolved
493
516
  when they are served. Caching a loader's own data is a separate opt-in (see
494
- "Opting a Loader into Caching").
517
+ "Opting a Loader into Caching"). A `ppr` shell does store some loader data: an
518
+ `ssr: false` loader's settled data bakes into the shell, and a HIT runs that
519
+ loader only when its return carries promises (see "`ssr: false`" below and
520
+ `/ppr`).
495
521
 
496
- Pre-rendering follows the same rule: at build time loaders are skipped entirely
497
- (there is no real request context), and at runtime the worker resolves them
498
- fresh against the live database.
522
+ Pre-rendering follows the same rule: the build-time Flight payload skips
523
+ loaders (there is no real request context), and at runtime the worker resolves
524
+ them fresh against the live database. The exception is a `Prerender` + `ppr`
525
+ route: its build-time shell capture runs `ssr: false` loaders and bakes their
526
+ settled data into the shell, like a runtime capture.
499
527
 
500
528
  ### Parallel and streaming — latency overlaps first paint
501
529
 
@@ -516,8 +544,8 @@ If you come from a framework where the loader is a blocking step that runs
516
544
  before the response is built, this is the shift to internalize: here the
517
545
  response starts streaming first and loader data fills in. (The one deliberate
518
546
  exception is per-loader: `loader(Def, { ssr: false })` awaits that
519
- loader before first flush on document renders — see "`ssr: false`"
520
- below.)
547
+ loader before first flush on document renders other than a `ppr` shell HIT,
548
+ where the shell flushes first — see "`ssr: false`" below.)
521
549
 
522
550
  ### See it: `debugPerformance`
523
551
 
@@ -579,7 +607,11 @@ loader(ProductLoader, () => [cache({ ttl: 300 })]),
579
607
 
580
608
  This works wherever the DSL binding is declared, an intercept's `use()`
581
609
  included. A handler's `ctx.use(Loader)` read never caches by itself (see
582
- "ctx.use(Loader) — escape hatch").
610
+ "ctx.use(Loader) — escape hatch"). Once the binding started, every
611
+ `ctx.use(Loader)` in the request, from the handler or from another loader's
612
+ body, gets the binding's value: one value per loader per request, a HIT
613
+ included. A reader that runs before the binding starts (a layout's loader
614
+ reading a route's cached loader) gets the value of its own run, as before.
583
615
 
584
616
  The loader's data is cached independently from the route's segment cache,
585
617
  using the same `SegmentCacheStore` (app-level or per-loader override).
@@ -626,6 +658,7 @@ at two levels:
626
658
  import { cookies } from "@rangojs/router";
627
659
 
628
660
  // Full override — key function replaces the default entirely
661
+ // Stored as "loader:{loaderId}:key:product%3Ashoe%3Aen"
629
662
  loader(ProductLoader, () => [
630
663
  cache({
631
664
  ttl: 300,
@@ -637,17 +670,92 @@ loader(ProductLoader, () => [
637
670
  // Set in the store configuration, applies to all entries in that store
638
671
  ```
639
672
 
640
- Resolution priority (same as route-level `cache()`):
673
+ Resolution priority (same order as route-level `cache()`):
641
674
 
642
- 1. `key(ctx)` from cache options — full override
643
- 2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
675
+ 1. `key(ctx)` from cache options — full override, stored namespaced as
676
+ `loader:{loaderId}:key:` plus the result's URI encoding
677
+ 2. `store.keyGenerator(ctx, defaultKey)` — store-level modification, stored
678
+ as returned
644
679
  3. Default key — `loader:{loaderId}:{host}{pathname}:{sortedParams}`
645
680
 
681
+ Because the `key()` result is namespaced by the loader id, no value it
682
+ returns, request input included, can name another loader's entry or a
683
+ default key: two loaders whose `key()` returns the same value keep separate
684
+ entries. A readable prefix is still good practice. A store `keyGenerator`
685
+ result is stored as returned, so build it on the `defaultKey` it receives
686
+ (which carries the loader id): a generator that returns raw request input
687
+ can still name another entry.
688
+
646
689
  A `key` function (or store `keyGenerator`) that throws is **not** caught: the
647
690
  loader fails as if its body threw. There is no silent fallback to the default
648
691
  key, because a personalised key collapsing onto the broad default would share
649
692
  one user's data with everyone.
650
693
 
694
+ **Request-scoped reads in the body must be part of the key.** The default key
695
+ names no user, so one entry serves everyone who requests the same loader, host,
696
+ path and params. A route `cache()` around the route does not partition it
697
+ either: a loader's own `cache()` is an independent layer, keyed only by what it
698
+ declares. On a miss, a body that reads `cookies()`, `headers()`,
699
+ `ctx.request.headers` or a non-cacheable variable (`createVar({ cache: false })`,
700
+ or a value written with `ctx.set(..., { cache: false })`) therefore **fails**,
701
+ and nothing is stored,
702
+ unless the binding declares identity with a `key()` or a store `keyGenerator`.
703
+ The check tests only that one of them is there, not what it contains: any
704
+ `key()` or store `keyGenerator` switches it off, so it must itself include the
705
+ value. That includes a store-wide `keyGenerator` that only adds a region
706
+ prefix, like the one described above.
707
+
708
+ ```typescript
709
+ export const SessionLoader = createLoader(async () =>
710
+ getAccount(cookies().get("session")?.value),
711
+ );
712
+
713
+ // The miss fails: cookies() cannot be called inside loader "…SessionLoader",
714
+ // whose own cache() has no key().
715
+ loader(SessionLoader, () => [cache({ ttl: 60 })]),
716
+
717
+ // One entry per session.
718
+ loader(SessionLoader, () => [
719
+ cache({
720
+ ttl: 60,
721
+ key: (ctx) => `account:${cookies().get("session")?.value ?? "anon"}`,
722
+ }),
723
+ ]),
724
+ ```
725
+
726
+ The same holds in a stale entry's background refresh (the refresh fails and the
727
+ stale entry keeps serving) and for the loaders the body reads with `ctx.use()`,
728
+ because their values land in the entry. That includes a keyed cached loader
729
+ served from its own cache: its entry keeps the read its miss made. It also holds
730
+ when something else ran the loader first, such as a parent layout's handler
731
+ calling `ctx.use(SessionLoader)`: the miss reuses that run and fails the same
732
+ way. A read that settles after the value, in a promise inside it or in a
733
+ handle push that settles within the 5 s handle-encode timeout, can't fail a
734
+ value that was already served: that user gets their own value, nothing is
735
+ stored, and `onError` gets the same error. A push still pending after the
736
+ timeout drops the entry's handles, and the value is stored without them. A loader with no `cache()` of its own
737
+ reads request data freely.
738
+
739
+ The raw reads count the same: `ctx.request.headers` (the loader's
740
+ `ctx.request` is the request's own `Request`) and `getRequestContext().cookie()`
741
+ / `.cookies()` fail an unkeyed miss exactly like `cookies()`. A `key()` or a
742
+ store `keyGenerator` reads them freely, since that is how the entry is
743
+ partitioned:
744
+
745
+ ```typescript
746
+ loader(LocaleLoader, () => [
747
+ cache({
748
+ ttl: 60,
749
+ key: (ctx) => `lang:${ctx.request.headers.get("accept-language") ?? "en"}`,
750
+ }),
751
+ ]),
752
+ ```
753
+
754
+ One kind of read is not seen, the same as in `"use cache"`: a value computed
755
+ outside the loader and handed in, such as a per-request memo a handler filled
756
+ from `cookies()` before the loader awaited it. Key what it carries too. If the
757
+ value must stay per request, drop the loader's `cache()` instead.
758
+
651
759
  ### Tags for Invalidation
652
760
 
653
761
  ```typescript
@@ -665,6 +773,34 @@ loader(ProductLoader, () => [
665
773
  ]),
666
774
  ```
667
775
 
776
+ The tags the loader body records tag the entry too: `cacheTag()` calls, the
777
+ tags of `"use cache"` reads, render-time `cacheTag()` in server components of
778
+ the value or a handle push, and the tags the loaders it reads with `ctx.use`
779
+ record, including one the route, a layout or the handler started first. A
780
+ loader it reads that has its own `cache()` also brings its `cache({ tags })`
781
+ and, on a HIT, its entry's tags. `updateTag()` of any of them drops the entry,
782
+ and a HIT records them again, so a route `cache()`, ppr shell or document built
783
+ over the HIT stays evictable. A stale hit's background refresh stores the
784
+ refreshed body's tags; it runs the loaders it reads again rather than reusing
785
+ the page's (possibly stale) values.
786
+
787
+ One case keeps a `cache({ tags })` off: a cached loader that reads another
788
+ loader's `cache()` binding before that binding starts (for example a cached
789
+ layout loader reading a route's cached loader), and writes its entry before
790
+ it. That reader's entry has the tags the loader's body records, but not the
791
+ tags only its `cache({ tags })` lists. If the reader must drop with them, record
792
+ them in the loader body with `cacheTag()` too.
793
+
794
+ ```typescript
795
+ export const ProductLoader = createLoader(async (ctx) => {
796
+ cacheTag(`product:${ctx.params.slug}`);
797
+ return db.product(ctx.params.slug);
798
+ });
799
+
800
+ loader(ProductLoader, () => [cache({ ttl: 600 })]),
801
+ // updateTag(`product:${slug}`) drops this entry.
802
+ ```
803
+
668
804
  ### Stale-While-Revalidate
669
805
 
670
806
  ```typescript
@@ -691,7 +827,8 @@ loader(ProductLoader, () => [
691
827
  ```
692
828
 
693
829
  When `condition` returns false, the loader runs fresh and the cache is bypassed
694
- entirely (no read, no write).
830
+ entirely (no read, no write). Like `key()`, a `condition()` reads request
831
+ identity freely: it decides whether the entry is used, it is not stored in it.
695
832
 
696
833
  ### Per-Loader Store Override
697
834
 
@@ -818,7 +955,10 @@ flushes, so both signals are resolved server-side while the tree is built:
818
955
  `redirect()` replaces the whole page with the redirect carrier (still a 200
819
956
  document, the client replaces to the target on hydration) and `notFound()`
820
957
  renders the not-found UI at the owning segment with a real 404. No read site
821
- runs, so a `useLoader` in a layout above every Suspense boundary is safe.
958
+ runs, so a `useLoader` in a layout above every Suspense boundary is safe. On a
959
+ `ppr` route, a flagged loader that settles with `notFound()` or `redirect()`
960
+ during the shell capture refuses the capture (warned once per key): that URL
961
+ gets no shell, and its renders deliver the signal as above.
822
962
 
823
963
  Session/auth gates belong in middleware (they are request-shaped, not
824
964
  data-shaped, and middleware CAN emit a real pre-stream 302). Data-dependent
@@ -857,7 +997,9 @@ they are replayed from the entry (see "Opting a Loader into Caching").
857
997
 
858
998
  Under `cache()`, loader pushes are not stored with the cached segments: the
859
999
  loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
860
- settled `ssr: false` pushes in the shell record — see `/ppr`.)
1000
+ the settled pushes of an `ssr: false` loader, and of the loaders it awaits, in
1001
+ the shell, and a promise-free `ssr: false` loader does not run on a shell HIT —
1002
+ see `/ppr`.)
861
1003
 
862
1004
  Reads are the other direction and gated: `ctx.get(handle)` throws unless the
863
1005
  loader first does `await ctx.rendered()` (DSL-registered loaders only —
@@ -883,14 +1025,17 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
883
1025
  The options object is typed as `LoaderOptions`
884
1026
  (`import type { LoaderOptions } from "@rangojs/router"`).
885
1027
 
886
- The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off
887
- for this loader, so nothing of it is left to stream in the document:
888
- document renders await this loader before first flush — data is settled
889
- (`useLoader` reads it synchronously, no fallback paints), handle pushes beat
890
- the barrier snapshot, and a thrown `notFound()` deterministically precedes
891
- Response construction (real 404, no warm-up race). Client navigations stream
892
- exactly as before. Scoped per LOADER: the flagged loader awaits only itself;
893
- siblings keep streaming.
1028
+ The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off for
1029
+ this loader, so nothing of it is left to stream in the document: document
1030
+ renders await this loader before first flush — data is settled (`useLoader`
1031
+ reads it synchronously, no fallback paints), handle pushes beat the barrier
1032
+ snapshot, and a thrown `notFound()` deterministically precedes Response
1033
+ construction (real 404, no warm-up race). Client navigations stream exactly as
1034
+ before (on a `ppr` route, a navigation that replays a shell captured by a
1035
+ document request serves a promise-free flagged loader from its pin, as a
1036
+ document HIT does; a snapshot captured by a navigation alone carries no pins,
1037
+ and its replay runs the loader fresh). Scoped per LOADER: the flagged loader
1038
+ awaits only itself; siblings keep streaming.
894
1039
 
895
1040
  Delivery is in-place, not merely in-document. React's Fizz outlines any
896
1041
  COMPLETED Suspense boundary over ~500 bytes to an end-of-stream
@@ -906,17 +1051,25 @@ still outline; their reveal must wait for the CSS.
906
1051
 
907
1052
  The costs and constraints:
908
1053
 
909
- - Every document load pays the flagged loader's latency before first byte.
910
- That is the point — but keep flagged loaders fast, and flag loaders, not
911
- routes.
1054
+ - Every document render that is not a `ppr` shell HIT pays the flagged
1055
+ loader's latency before first byte. That is the point — but keep flagged
1056
+ loaders fast, and flag loaders, not routes. On a HIT the shell flushes
1057
+ first: a flagged loader whose return has no promises does not run (unless
1058
+ the capture marked it to run: see `/ppr` → On a shell HIT), and one whose
1059
+ return carries promises runs to fill its holes.
912
1060
  - A flagged loader must not `await ctx.rendered()` / `ctx.get(handle)` — the
913
1061
  document render awaits the loader before the render barrier resolves, so
914
1062
  that wait is a cycle by construction; it throws a deadlock error naming the
915
1063
  fix.
916
1064
  - Under PPR the flag is also the bake lane: the settled non-promise data
917
1065
  (handle pushes included) freezes into the stored shell — see `/ppr` → The
918
- loader lane rule. The `progressiveChunkSize` auto-raise is live-document
919
- only; captured shells outline per the explicit option or React's default.
1066
+ loader lane rule. On a HIT, a flagged loader whose return has no promises is
1067
+ served from the shell and does not run (unless the capture marked it to run:
1068
+ see `/ppr` → On a shell HIT); one whose return carries promises
1069
+ runs on every HIT, with its baked parts overlaid. A `cookies()`, `headers()`,
1070
+ `ctx.request.headers` or `{ cache: false }` read in it refuses the capture. The
1071
+ `progressiveChunkSize` auto-raise is live-document only; captured shells
1072
+ outline per the explicit option or React's default.
920
1073
 
921
1074
  Also available in `clientUrls()` route groups (`/client-urls`), where the
922
1075
  loader-heavy shape makes it most useful.
@@ -75,7 +75,10 @@ render pass — normal renders, post-action revalidation, PE re-renders — its
75
75
  variables are never stale: middleware is the safest `ctx.set` rung on the
76
76
  data-passing ladder (`/rango` → "Passing data down the tree"). But it does
77
77
  not change partial revalidation boundaries between handler/layout/parallel
78
- segments.
78
+ segments. On a PPR shell HIT no handler or layout runs; on a route `cache()`
79
+ HIT the segments inside the `cache()` boundary don't run, and those above it
80
+ do. Output that didn't run shows the values read when the shell was captured
81
+ or the entry written.
79
82
 
80
83
  For shared segment data, use named revalidation contracts on both the producer
81
84
  and consumer segments, even when middleware is present in the chain.
@@ -271,6 +274,24 @@ export const myMiddleware: Middleware = async (ctx, next) => {
271
274
  Cookies and request headers are not `ctx` members; use the free functions
272
275
  `cookies()` and `headers()` from `@rangojs/router`.
273
276
 
277
+ Middleware reads request identity freely: `cookies()`, `headers()` and
278
+ `ctx.request.headers` alike. The same `Request` reaches handlers and loaders
279
+ as `ctx.request`, and there `ctx.request.headers` answers like `headers()`: it
280
+ throws inside a `cache()` boundary or a `"use cache"` function and refuses a
281
+ `ppr` shell capture (#976). To render a header on a cached or `ppr` route,
282
+ copy it here with `ctx.set()` (a cacheable variable) and include it in the
283
+ route's `cache()` `key()`, so each value gets its own record and shell:
284
+
285
+ ```typescript
286
+ const Tier = createVar<string>();
287
+
288
+ export const tierMiddleware: Middleware = async (ctx, next) => {
289
+ ctx.set(Tier, ctx.request.headers.get("x-tier") ?? "free");
290
+ return next();
291
+ };
292
+ // cache({ key: (ctx) => `tier:${ctx.request.headers.get("x-tier") ?? "free"}` }, ...)
293
+ ```
294
+
274
295
  ### Changing the response after `next()`
275
296
 
276
297
  After `await next()`, `ctx.header()` and `ctx.headers` write to the real
@@ -334,19 +355,23 @@ the token (`createVar<T>({ cache: false })`) or per write
334
355
  (`ctx.set("user", user, { cache: false })`). Reading it with `ctx.get()` inside
335
356
  a `cache()` boundary or a `"use cache"` function then throws instead of baking
336
357
  one user's data into a shared entry (inside `"use cache"`, also through a
337
- middleware `ctx` passed in); loaders can still read it because they always run
338
- fresh (a `"use cache"` function a loader calls cannot). A middleware `ctx`
339
- passed to a `"use cache"` function keys the call by host, pathname, params and
340
- search, plus the route name once the route is matched (a global middleware
341
- before `next()` has none yet), and its `set()`, `header()` and `headers` writes
342
- throw inside the function (`/use-cache`).
358
+ middleware `ctx` passed in); under `cache()` loaders can still read it because a
359
+ DSL loader re-runs on every hit (a `"use cache"` function a loader calls cannot,
360
+ and a loader bound with its own `cache()` only with a `key()`, which switches
361
+ the check off, so the key must include the value). On a `ppr` route only a
362
+ loader without `ssr: false` that no handler awaits may read it: a read the shell
363
+ capture waits for refuses the capture. A middleware `ctx` passed to a
364
+ `"use cache"` function keys the call by host, pathname, params and search, plus
365
+ the route name once the route is matched (a global middleware before `next()`
366
+ has none yet), and its `set()`, `header()` and `headers` writes throw inside the
367
+ function (`/use-cache`).
343
368
 
344
369
  ## Build-Time PPR Middleware
345
370
 
346
371
  Normal `Prerender` Flight payload collection does not run middleware: there is
347
372
  no request to wrap. The exception is `Prerender` + `ppr` build-shell capture.
348
- After the Flight payload exists, the shell producer replays global and route
349
- middleware for each generated URL before it captures HTML.
373
+ After the Flight payload exists, the build replays global and route middleware
374
+ for each generated URL before capturing the shell.
350
375
 
351
376
  In that build-shell pass:
352
377