@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
@@ -373,15 +373,17 @@ old-deployment request pinning.
373
373
  The `@rangojs/router/testing/*` entry points give you `runLoader`,
374
374
  `runMiddleware`, `runInRequestContext` (server actions), `dispatch` (base
375
375
  `./testing`), `renderHandler` and real Flight rendering (`renderServerTree`,
376
- `findClientBoundaries`, `findElements`) from `./testing/flight`, `renderRoute`
377
- from `./testing/dom`, a vitest preset from `./testing/vitest`, and a Playwright
378
- e2e harness with dev/prod parity helpers (`parityDescribe`, `expectParity`) from
379
- `./testing/e2e`. You can unit-test a loader, a middleware, an action, or an RSC
380
- handler in isolation. Next.js and Waku ship no official primitives for testing
381
- server components/handlers; TanStack documents testing patterns (build your own
382
- harness from `createRouter`/`createMemoryHistory`) but ships no testing package,
383
- and nothing at the RSC-handler level. See the [testing skill](../../testing/SKILL.md)
384
- and [testing.md](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/testing.md).
376
+ `findClientBoundaries`, `findElements`) plus a real PPR shell capture and HIT
377
+ (`serveShellRequest`, `resetShellTestState`) from `./testing/flight`,
378
+ `renderRoute` from `./testing/dom`, a vitest preset from `./testing/vitest`, and
379
+ a Playwright e2e harness with dev/prod parity helpers (`parityDescribe`,
380
+ `expectParity`) from `./testing/e2e`. You can unit-test a loader, a middleware,
381
+ an action, or an RSC handler in isolation. Next.js and Waku ship no official
382
+ primitives for testing server components/handlers; TanStack documents testing
383
+ patterns (build your own harness from `createRouter`/`createMemoryHistory`) but
384
+ ships no testing package, and nothing at the RSC-handler level. See the
385
+ [testing skill](../../testing/SKILL.md) and
386
+ [testing.md](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/testing.md).
385
387
 
386
388
  ### Bundle discipline
387
389
 
@@ -487,7 +489,7 @@ layout(<ShopLayout />, () => [
487
489
  "@modal",
488
490
  "product.detail",
489
491
  ProductQuickView,
490
- { when: ({ from }) => from.pathname === "/products" },
492
+ { when: ({ from }) => from.url.pathname === "/products" },
491
493
  () => [
492
494
  loader(ProductLoader),
493
495
  loading(<QuickViewSkeleton />),
@@ -506,8 +508,10 @@ middleware, layout, loaders, loading state, and loader-level caching and
506
508
  `intercept()`) can choose by navigation source.
507
509
 
508
510
  This is integrated with the rest of the runtime rather than implemented as URL
509
- masking plus local component state. Intercepts get source-scoped prefetch entries
510
- so a modal Flight payload cannot leak into direct navigation. A prerendered target
511
+ masking plus local component state. A route an intercept targets gets
512
+ source-scoped prefetch entries whether or not the intercept applied, so neither a
513
+ modal Flight payload nor a full page prefetched from another page is reused where
514
+ the intercept decides differently. A prerendered target
511
515
  can store a separate intercept variant while its loaders still run live at
512
516
  request time. An action can revalidate the open intercept without remounting its
513
517
  subtree or forcing the preserved background to render. See the
@@ -534,10 +538,13 @@ There are two distinct things named "revalidate", and the split is deliberate:
534
538
  tabs, and pagination. See
535
539
  [shallow-navigation.md](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/design/shallow-navigation.md).
536
540
 
537
- Both are separate from `revalidateTag()`/`updateTag()`, which hard-purge tagged
538
- cache entries. `updateTag()` is awaitable for read-your-own-writes;
539
- `revalidateTag()` schedules the same invalidation in the background. Neither API
540
- selects a client segment to render. Next.js offers `router.refresh()` (and
541
+ Both are separate from `revalidateTag()`/`updateTag()`, which evict tagged
542
+ cache entries; the request that calls either reads its own writes.
543
+ `updateTag()` also awaits the durable write; `revalidateTag()` schedules it in
544
+ the background. The mutating user's next requests skip the stores' per-isolate
545
+ PPR shell memos via a fresh-reads cookie, and other users converge within the
546
+ memo window and the platform's own propagation. Neither API selects a client
547
+ segment to render. Next.js offers `router.refresh()` (and
541
548
  `refresh()` from `next/cache`) plus path/tag cache invalidation, but no per-segment
542
549
  render predicate and no typed action discrimination — and its invalidations clear
543
550
  the whole client cache rather than selecting a segment. TanStack's `shouldReload`/`router.invalidate()` is the nearest
@@ -573,7 +580,8 @@ the bounded cache uses a configurable TTL. `Save-Data`,
573
580
  Correctness is part of the prefetch contract. Keys include Rango state, destination
574
581
  URL, and the mounted segment set used to compute the diff. Server actions rotate
575
582
  state and abort stale speculative work; a generation check prevents a late result
576
- from repopulating an invalidated cache. Intercepts are automatically source-scoped,
583
+ from repopulating an invalidated cache. Routes an intercept targets are automatically
584
+ source-scoped, matched or not,
577
585
  and malformed, cross-origin, foreign-router, redirect, and reload responses are
578
586
  dropped rather than warmed. `useLinkStatus()` exposes `{ pending }` for the owning
579
587
  link.
@@ -620,18 +628,22 @@ this resource-aware policy layer.
620
628
 
621
629
  `createLoader(fn)` / `loader()` define **live-by-default** data units: they are
622
630
  excluded from an enclosing segment cache and resolve on every request unless the
623
- loader itself explicitly opts into `cache()`. They may safely read `cookies()`,
624
- `headers()`, request context, and `env` because loader execution is outside the
625
- cached shell. Loaders run in parallel, stream independently under `loading()`
626
- boundaries, compose server-side via `ctx.use(OtherLoader)`, and carry
627
- route-level authority: they can throw `notFound()`/`redirect()`, WRITE handles
628
- (`ctx.use(Meta)({ title })` — data-derived page metadata pushed from the data's
629
- producer), read handle data after the render settles (`ctx.get(handle)` behind
630
- `await ctx.rendered()`), and opt into `loader(Def, { ssr: false })`
631
- so a document render awaits them before first flush (deterministic SSR'd data,
632
- meta, and 404 status). Reads happen through `useLoader` in a client component
633
- (including its SSR pass) or `useFetchLoader` for standalone client fetches. "Fetchable" loaders are callable
634
- endpoints with their own middleware and GET/POST/PUT/PATCH/DELETE bodies.
631
+ loader itself explicitly opts into `cache()`. A loader without `ssr: false` may
632
+ safely read `cookies()`, `headers()`, request context, and `env` because its
633
+ execution is outside the cached shell; a loader with its own `cache()` must put
634
+ those reads in its `key()`, or its miss fails and stores nothing. On a `ppr`
635
+ route an `ssr: false` loader bakes into the shell, so an identity read there
636
+ refuses the capture. Loaders run in parallel, stream independently under
637
+ `loading()` boundaries, compose server-side via `ctx.use(OtherLoader)`, and
638
+ carry route-level authority: they can throw `notFound()`/`redirect()`, WRITE
639
+ handles (`ctx.use(Meta)({ title })` — data-derived page metadata pushed from the
640
+ data's producer), read handle data after the render settles (`ctx.get(handle)`
641
+ behind `await ctx.rendered()`), and opt into `loader(Def, { ssr: false })` so a
642
+ document render awaits them before first flush (deterministic SSR'd data, meta,
643
+ and 404 status). Reads happen through `useLoader` in a client component
644
+ (including its SSR pass) or `useFetchLoader` for standalone client fetches.
645
+ "Fetchable" loaders are callable endpoints with their own middleware and
646
+ GET/POST/PUT/PATCH/DELETE bodies.
635
647
 
636
648
  One sharp edge is worth stating because the distinction matters: a cached handler
637
649
  can call `await ctx.use(Loader)`, but if it renders that result inline, it bakes the
@@ -681,7 +693,7 @@ a fully prerendered route. Caching loader data is a separate, explicit opt-in on
681
693
  that loader.
682
694
 
683
695
  The mechanism is concrete: a prerendered route serves its baked Flight shell from
684
- the store, then `resolveLoadersOnly()` resolves the loaders through their own
696
+ the store, then the router resolves the loaders through their own
685
697
  freshness policy at request time and merges their segments into that replayed
686
698
  shell. With the default policy you get a cached/prerendered shell plus live data,
687
699
  automatically, with no per-route static-vs-dynamic decision. The loader is the
@@ -692,7 +704,8 @@ designated live data slot in a cache-first RSC tree.** Several properties follow
692
704
  that no loader-before-render has:
693
705
 
694
706
  - it is the **cache-safety escape hatch** — the only place request-coupled reads
695
- are allowed inside a cache scope, because it is the part guaranteed to re-run;
707
+ are allowed inside a cache scope, because it is the part guaranteed to re-run
708
+ (a loader without `ssr: false` and without its own unkeyed `cache()`);
696
709
  - it is **standalone and composable**, not route-coupled — one `createLoader()`
697
710
  read by many segments, composed via `ctx.use`, or exposed as a fetchable endpoint;
698
711
  - it **streams as a hole, not a gate** — concurrent, Suspense-resolved under
@@ -744,7 +757,7 @@ from two hosts, route scopes, param sets, or query variants therefore does not
744
757
  collapse into one entry just because both calls received a `ctx` object.
745
758
 
746
759
  The same boundary guards operations whose meaning cannot survive a hit. Direct
747
- `cookies()`/`headers()` reads and request/response mutations such as `ctx.set()`,
760
+ `cookies()`/`headers()`/`ctx.request.headers` reads and request/response mutations such as `ctx.set()`,
748
761
  `ctx.header()`, status, theme, and location-state writes throw inside
749
762
  `"use cache"`. If a cached function pushes typed handles through
750
763
  `ctx.use(Breadcrumbs)` or `ctx.use(Meta)`, Rango captures those pushes on a miss and
@@ -773,11 +786,13 @@ safe rule remains simple: read request-specific context at the live point of use
773
786
  normally a loader, rather than deriving it outside and carrying it into a cached
774
787
  shell.
775
788
 
776
- This is where the taint and loader designs meet. Loaders are the sanctioned dynamic
777
- holes: they run outside the enclosing segment cache and may read the current
778
- request, while the shared shell remains protected. Next.js also isolates runtime
779
- APIs from ordinary `"use cache"`, and React's optional taint APIs protect values at
780
- the server-to-client serialization boundary. Those are useful but different
789
+ This is where the taint and loader designs meet. Loaders are the sanctioned
790
+ dynamic holes: they run outside the enclosing segment cache and may read the
791
+ current request (except, on a `ppr` route, an `ssr: false` loader or a loader a
792
+ handler awaits, which the capture runs and refuses on such a read), while the
793
+ shared shell remains protected. Next.js also isolates runtime APIs from ordinary
794
+ `"use cache"`, and React's optional taint APIs protect values at the
795
+ server-to-client serialization boundary. Those are useful but different
781
796
  contracts; they do not provide Rango's route-aware tainted-argument keying,
782
797
  non-cacheable typed context variables, handle capture/replay, and loader escape
783
798
  path as one system. TanStack Start and Waku leave this boundary primarily to
@@ -15,7 +15,7 @@ storefront homepage (shell HIT, production build, M-series laptop): a single
15
15
  was dead for ~11s. Fast paint, frozen page.
16
16
 
17
17
  The obvious fix — wrap the heavy subtree in `<Suspense>` so it hydrates later —
18
- trades the paint away: under shell capture that boundary postpones, and the
18
+ can trade the paint away: when that boundary postpones under shell capture, the
19
19
  frozen prelude ships an empty `<main>`. This recipe gets both: the full body
20
20
  HTML in the prelude AND its hydration off the initial task, released on first
21
21
  idle at retry-lane priority.
@@ -28,7 +28,9 @@ idle at retry-lane priority.
28
28
  | interactive (menu click works) | ~3.5s | ~1.6s | ~1.6s |
29
29
 
30
30
  Measure your own page before and after — the win depends on how much of the
31
- hydration cost lives under the boundary (see "Verifying and measuring").
31
+ hydration cost lives under the boundary (see "Verifying and measuring"). These
32
+ numbers were measured before PPR captures baked handler output; see the next
33
+ section for what that changed.
32
34
 
33
35
  ## The recipe
34
36
 
@@ -102,23 +104,39 @@ inside hydrates after first idle.
102
104
 
103
105
  ## Why `fallback={children}` is load-bearing
104
106
 
105
- This is not a style choice; it is the half of the recipe that makes it
106
- PPR-compatible.
107
-
108
- Shell capture aborts on flight byte-quiet (`src/rsc/shell-capture.ts`,
109
- `FLIGHT_QUIET_HOPS`): once the Flight source has been byte-silent for the
110
- quiet window, the fizz render freezes. A big HTML subtree under _any_
111
- `<Suspense>` boundary cannot finish inside that window, so the boundary always
112
- postpones — boundary placement cannot fix it. Verified both ways: wrapping the
113
- client island from outside AND placing the boundary inside the island both
114
- baked `<!--$?--><template id="B:…">` into `<main>`, i.e. an empty body in the
115
- frozen prelude.
116
-
117
- With the content as the fallback, the unavoidable postpone _becomes the
118
- delivery mechanism_: the shell bakes the fallback, and the fallback IS the
119
- body. In `/ppr` hole-doctrine terms, this is the PHYSICS class exploited
120
- deliberately — you cannot stop the boundary from becoming a hole, so you make
121
- the hole's baked fallback carry the real markup.
107
+ This is not a style choice. The recipe was built when the shell capture
108
+ postponed any big `<Suspense>` subtree: the capture froze on Flight byte-quiet
109
+ (`FLIGHT_QUIET_HOPS` in `src/rsc/shell-capture.ts`) while the handler output
110
+ was still rendering, so a boundary over the page body became a hole no matter
111
+ where it sat. Verified both ways at the time: wrapping the client island from
112
+ outside AND placing the boundary inside the island both baked
113
+ `<!--$?--><template id="B:…">` into `<main>`, i.e. an empty body in the frozen
114
+ prelude. With the content as the fallback, the postpone became the delivery
115
+ mechanism: the shell baked the fallback, and the fallback was the body.
116
+
117
+ PPR captures now wait for the whole handler layer before freezing anything:
118
+ the capture records the page's segments first (async server components
119
+ rendered, every promise in the handler output settled) and renders its Flight
120
+ payload from that record (`settleCaptureRecord` in `src/rsc/shell-capture.ts`).
121
+ A boundary whose content is only handler output is therefore expected to bake
122
+ its content as a completed boundary instead of postponing. What still
123
+ postpones at capture is a boundary over live data: a loader without
124
+ `ssr: false` read with `useLoader` under `loading()` or an inline
125
+ `<Suspense>`, or a promise nested in an `ssr: false` loader's value. Run the
126
+ prelude check in "Verifying and measuring" on your page rather than assuming
127
+ either shape.
128
+
129
+ `fallback={children}` stays the safe choice either way:
130
+
131
+ - If the boundary still postpones (live data inside it), the fallback is what
132
+ the prelude shows, so it must be the content.
133
+ - The sync-update trap below: a client render of the boundary shows the
134
+ fallback. With `fallback={children}` that is a visual no-op; with
135
+ `fallback={null}` it blanks the page.
136
+
137
+ The hydration deferral does not depend on which shape you get: `HydrationGate`
138
+ suspends during hydration, and React keeps the server DOM of the boundary in
139
+ place until the gate releases.
122
140
 
123
141
  ## Why the client gate is free
124
142
 
@@ -164,13 +182,14 @@ release on interaction instead (see Variations).
164
182
 
165
183
  ## Known cost: the body rides twice (measure it, don't guess)
166
184
 
167
- On a shell HIT the gated subtree's HTML is in the response twice — once as the
168
- baked fallback in the prelude, once as the resumed hole content (the resume
169
- re-renders and re-ships it; there is no bake-through). Homepage measurement:
170
- 234KB → 302KB gzipped (+68KB, +29%; raw +1.05MB). It is post-paint bandwidth,
171
- not render-blocking — the visible prelude streams first — but it is real bytes
172
- on every document GET. Weigh it per page; on a small body the recipe may not
173
- pay for itself.
185
+ When the boundary postpones at capture, a shell HIT carries the gated
186
+ subtree's HTML twice — once as the baked fallback in the prelude, once as the
187
+ resumed hole content (the resume re-renders and re-ships it; there is no
188
+ bake-through). Homepage measurement (postponing boundary): 234KB → 302KB
189
+ gzipped (+68KB, +29%; raw +1.05MB). It is post-paint bandwidth, not
190
+ render-blocking — the visible prelude streams first — but it is real bytes on
191
+ every document GET. Weigh it per page; on a small body the recipe may not pay
192
+ for itself. A boundary that bakes its content ships it once.
174
193
 
175
194
  ## Verifying and measuring
176
195
 
@@ -230,7 +249,8 @@ this exact space. The recipe survives that future; a primitive might not.
230
249
 
231
250
  ## Related
232
251
 
233
- - `/ppr` — the shell/hole mechanics this recipe rides on (hole doctrine:
234
- PHYSICS class), and why the capture postpones any big Suspense subtree
235
- - `src/rsc/shell-capture.ts` — the byte-quiet capture window
236
- (`FLIGHT_QUIET_HOPS`) that makes `fallback={children}` mandatory
252
+ - `/ppr` — the shell/hole mechanics this recipe rides on: what bakes into the
253
+ shell, and which boundaries stay holes
254
+ - `src/rsc/shell-capture.ts` — the record-first capture (`settleCaptureRecord`)
255
+ and the byte-quiet window (`FLIGHT_QUIET_HOPS`) that decide what the prelude
256
+ holds
@@ -56,8 +56,11 @@ There are two shell producers:
56
56
  Both producers feed the same in-function serve path. Build-time shells are
57
57
  content-hashed modules in the server bundle, not CDN-served HTML files.
58
58
 
59
- `ppr.ttl`, `ppr.swr`, and `ppr.tags` govern that shell entry. They do **not**
60
- emit HTTP `Cache-Control` and do not configure a platform CDN.
59
+ `ppr.ttl` and `ppr.swr` set the shell entry's lifetime, capped by the route
60
+ `cache()` entry it was captured from (the shorter wins). Its tags are
61
+ `ppr.tags` plus the tags its content recorded. A route `cache({ key })` or a
62
+ store `keyGenerator` stores one shell per partition value. None of this emits
63
+ HTTP `Cache-Control` or configures a platform CDN.
61
64
 
62
65
  ## Platform deployment shapes
63
66
 
@@ -205,7 +205,10 @@ the variant suffixes are still appended.
205
205
  - Responses whose `Cache-Control` also contains `private`, `no-store`, or an
206
206
  unqualified `no-cache` (those win over `s-maxage` in a shared cache)
207
207
  - Responses carrying `Set-Cookie` (a per-client value must not be replayed to
208
- everyone)
208
+ everyone). That includes a response whose request called
209
+ `updateTag()`/`revalidateTag()` on a store that keeps per-isolate memos
210
+ (`CFCacheStore`, `VercelCacheStore`): it carries the fresh-reads cookie
211
+ (`rango-state-fresh` by default), so it is not stored.
209
212
  - Non-200 responses
210
213
  - A 200 whose render reported an error: a component that threw after the
211
214
  response started streaming, such as an async server component whose fetch
@@ -216,6 +219,30 @@ the variant suffixes are still appended.
216
219
  error during the render. The current request still gets the errored render,
217
220
  the next request renders fresh, and on a stale refresh the stale entry keeps
218
221
  serving.
222
+ - A page rendered with a visitor's stored theme other than `defaultTheme`.
223
+ That happens only when `Cache-Control` was set after the render (see
224
+ "Theme" below).
225
+
226
+ ## Theme
227
+
228
+ With `theme` enabled, the page payload carries the theme `useTheme()` starts
229
+ at (`initialTheme`). A page whose response opted in to this cache before
230
+ `next()` starts it at the no-cookie default (`defaultTheme`), whoever rendered
231
+ it (#978), so a visitor with no stored theme never inherits the theme of the
232
+ visitor who warmed the entry. A visitor with a stored theme sees theirs after
233
+ hydration, and the `<html>` class is right before paint either way (the theme
234
+ script reads the cookie). Any other render, a 404 included, starts at the
235
+ visitor's own theme.
236
+
237
+ The render decides from the `Cache-Control` the handler or middleware set
238
+ before `await next()`. A header set after `next()` (or in `onResponse`) arrives
239
+ once the page is rendered, so the cache stores that response only when it was
240
+ rendered with the default theme. Set `Cache-Control` in the handler or before
241
+ `next()` so every visitor's miss can fill the entry.
242
+
243
+ A handler that reads `ctx.theme` on a stored route is not guarded: the stored
244
+ document shows that visitor's theme to everyone. Read the theme with
245
+ `useTheme()` instead (see `/theme`).
219
246
 
220
247
  ## Invalidation
221
248
 
@@ -224,7 +251,10 @@ with the request's tag set, so `updateTag()`/`revalidateTag()` evict them:
224
251
 
225
252
  - a render-callable `cacheTag("...")` in any server component on the page;
226
253
  - tags of `cache()` segments, cached loaders, and `"use cache"` functions the
227
- render read.
254
+ render read. A `cache()` segment HIT contributes the tags its content
255
+ recorded when it was written (render-callable calls and `"use cache"` reads
256
+ inside it), so a document stored over the HIT stays evictable by them. A
257
+ cached loader's HIT likewise contributes the tags its body recorded.
228
258
 
229
259
  Tags are snapshotted after the response body has fully streamed, so tags from
230
260
  Suspense-streamed content are included. A `Prerender()` route's build-time
@@ -184,6 +184,8 @@ parallel({
184
184
  });
185
185
  ```
186
186
 
187
+ On a `ppr` route, a slot's `loading()` over handler output does not make a hole. A slot handler that awaits `ctx.use(CartLoader)` is handler output: it runs at capture, its value is baked into the shared shell, and every HIT replays it without running the handler. A `cookies()`/`headers()` read in that loader refuses the capture, so the route stays on MISS. For a per-request slot, read the loader with `useLoader()` in a `"use client"` component under the slot's `loading()` (see `/ppr` → "The layout-with-loaders playbook").
188
+
187
189
  ### Two scopes for explicit `use` at the mount site: shared (broadcast) and slot-local
188
190
 
189
191
  `parallel()` accepts an explicit `use()` callback that **broadcasts** to every slot in the call ([dsl-helpers.ts](../../src/route-definition/dsl-helpers.ts)). That's the right behavior for the items the parallel allow-list permits and that accumulate (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`) — every slot gets them. (Note: `middleware` is not allowed inside `parallel()`; see the allowed-types table above.)
@@ -66,7 +66,9 @@ the one for your case.
66
66
  Rango-specific hook for tracking actions called outside a
67
67
  `<form action={...}>` flow.
68
68
  - [`./state.md`](./state.md) — `useLocationState` (persistent + flash state,
69
- `.read()`/`.write()`/`.delete()`), `invalidateClientCache()`.
69
+ `.read()`/`.read(location)`/`.write()`/`.delete()`, typed `state` on `router.push()` /
70
+ `router.replace()`, serializability errors that name the failing field),
71
+ `invalidateClientCache()`.
70
72
  - [`./outlets.md`](./outlets.md) — `Outlet`, `ParallelOutlet`, `useOutlet`.
71
73
  - [`./urls.md`](./urls.md) — `useParams`, `usePathname`, `useSearchParams`,
72
74
  `useHref`, `useMount`, `useReverse`.
@@ -34,7 +34,9 @@ Writers: handlers AND loader bodies — `ctx.use(Handle)` returns the push
34
34
  function in each (middleware has no handle APIs). Loader pushes follow a delivery race (settle before
35
35
  the handler barrier → in the SSR'd document; later → applied client-side), and
36
36
  `loader(Def, { ssr: false })` makes them deterministic in the
37
- document. See `/loader` → "Writing Handles from Loaders". `useHandle`
37
+ document. On a `ppr` shell HIT, a loader without `ssr: false` never ran during
38
+ the capture, so its pushes always apply client-side; with `ssr: false` they
39
+ bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
38
40
  re-renders on every handle update, late loader pushes included.
39
41
 
40
42
  Handles can be passed as props from server to client components:
@@ -67,8 +67,10 @@ function NavigationControls() {
67
67
  | `forward()` | `history.forward()`. |
68
68
 
69
69
  `push` / `replace` options: `scroll` (`false` keeps the current scroll
70
- position), `revalidate` (default `true`, see below), and `state` (location
71
- state, see [`./state.md`](./state.md)).
70
+ position), `revalidate` (default `true`, see below), `transition` (`false`
71
+ skips the route's `transition()` for this navigation, see below), and `state`
72
+ (location state: `[MyState(value)]` for typed entries, or plain
73
+ structured-clone-safe data; see [`./state.md`](./state.md)).
72
74
 
73
75
  Target resolution: a path starting with `/` is app-absolute and gets the
74
76
  router `basename` prefixed. A relative path (`"cart"`, `"./cart"`) resolves
@@ -95,6 +97,25 @@ If the pathname changes, `revalidate: false` is silently ignored and a full navi
95
97
 
96
98
  Plain `<a>` tags can opt in via `data-revalidate="false"`.
97
99
 
100
+ #### Skipping the transition
101
+
102
+ Pass `transition: false` to present one navigation without the route's
103
+ `transition()`: the commit is urgent (the `loading()` skeleton streams) and no
104
+ view transition runs, the same result as a `transition({ when })` predicate
105
+ returning `false`. No predicate is called. Back/forward, actions and
106
+ revalidation are unaffected.
107
+
108
+ ```tsx
109
+ router.push("/photos/2", { transition: false });
110
+
111
+ <Link to="/photos/2" transition={false}>
112
+ Next
113
+ </Link>;
114
+ ```
115
+
116
+ Plain `<a>` tags can opt in via `data-transition="false"`. See
117
+ `/view-transitions` → "Conditional transitions".
118
+
98
119
  ### useSegments()
99
120
 
100
121
  Access current URL path and matched route segments:
@@ -12,6 +12,9 @@ import { useLocationState, createLocationState } from "@rangojs/router/client";
12
12
 
13
13
  // Define typed state (all export patterns supported)
14
14
  // Keys are auto-injected by the Vite plugin -- no manual key needed.
15
+ // In a unit test (no plugin), key it with withLocationStateKey(ProductState,
16
+ // "ProductState") from @rangojs/router/testing: outside production, an
17
+ // unkeyed definition throws on first use.
15
18
  export const ProductState = createLocationState<{
16
19
  name: string;
17
20
  price: number;
@@ -81,8 +84,87 @@ definition:
81
84
  const state = useLocationState<{ from?: string }>(); // { from?: string } | undefined
82
85
  ```
83
86
 
87
+ ### State on router.push() / router.replace()
88
+
84
89
  The same `state` option exists on `router.push()` / `router.replace()` (see
85
- [`./navigation.md`](./navigation.md)).
90
+ [`./navigation.md`](./navigation.md)), with the same type as `<Link state>`
91
+ (minus the click-time getter): `HistoryState`, which is
92
+ `readonly LocationStateEntry[]` (typed entries) or `PlainHistoryState` (plain,
93
+ structured-clone-safe data). Both types are exported from
94
+ `@rangojs/router/client`.
95
+
96
+ ```tsx
97
+ "use client";
98
+ import { useLocationState, useRouter } from "@rangojs/router/client";
99
+ import { GridState } from "./state"; // createLocationState<{ count: number }>()
100
+
101
+ function LoadMore() {
102
+ const router = useRouter();
103
+ const grid = useLocationState(GridState);
104
+ const count = grid?.count ?? 20;
105
+ return (
106
+ <button
107
+ onClick={() =>
108
+ router.replace("?page=2", { state: [GridState({ count: count + 20 })] })
109
+ }
110
+ >
111
+ Load more
112
+ </button>
113
+ );
114
+ }
115
+ ```
116
+
117
+ Typed entries always go in an array, with nothing else in it. Three mistakes
118
+ are compile errors:
119
+
120
+ ```tsx
121
+ router.push(url, { state: GridState({ count: 3 }) }); // entry without the array
122
+ router.push(url, { state: [GridState] }); // definition, not called
123
+ router.push(url, { state: [GridState({ count: 3 }), { from: "list" }] }); // mixed
124
+ ```
125
+
126
+ Without the array, the entry's own `__rsc_ls_*` fields would land on
127
+ `history.state` and `useLocationState(GridState)` would read `undefined`; an
128
+ uncalled definition throws `DataCloneError` in `history.pushState`; in a mixed
129
+ array only the first element picks the format, so the rest are lost. For
130
+ untyped (JS) callers, development builds throw an error for each that names
131
+ the definition's key (or the offending index) and the fix.
132
+
133
+ Plain state is checked at the top level only. It rejects functions, symbols,
134
+ `Promise`, `WeakMap`, `WeakSet`, and values typed `unknown` (narrow an
135
+ `unknown` value, or annotate it as `PlainHistoryState`). Array, `Map`, and
136
+ `Set` elements are checked the same way, but plain object members are not.
137
+ These still compile and still throw `DataCloneError` at `history.pushState`:
138
+
139
+ - a function, symbol, or React element inside a plain object
140
+ (`{ onClose: () => {} }`);
141
+ - a top-level `ReactElement` or DOM node (`HTMLElement`), which look like
142
+ plain objects to the type.
143
+
144
+ Store plain data and rebuild the rest on arrival, or use a typed
145
+ `createLocationState<T>()` definition, whose `T` is checked all the way down.
146
+
147
+ ### When a type fails the serializability check
148
+
149
+ `createLocationState<T>()` rejects values that cannot survive
150
+ `history.state`'s structured clone: functions, class constructors, symbols,
151
+ React/RSC content, and fields typed `unknown`. The compile error names the
152
+ failing field and the reason:
153
+
154
+ ```
155
+ Argument of type '{ items: never[]; cursor: string; }' is not assignable to
156
+ parameter of type '... & LocationStateUnsafe<"`unknown` cannot be verified as
157
+ serializable; give it a concrete type", "items[].info.values">'.
158
+ ```
159
+
160
+ The path uses `.` for fields, `[string]` / `[number]` for index signatures
161
+ (`Record<string, T>`), `[]` for array and `Set` elements, `[0]` for tuple
162
+ elements, `<key>` / `<value>` for `Map` keys and values, and `<root>` when the
163
+ value itself is unsafe. A field typed `any` is never reported. When several fields fail, the error lists one `LocationStateUnsafe` per
164
+ path. An object's own unsafe fields are reported before anything nested
165
+ deeper, so a class instance or DOM node stops at its methods
166
+ (`row.el.click`); fix those and the next compile names the rest. The walk
167
+ stops 8 levels deep, reporting the deepest path it reached.
86
168
 
87
169
  ### Flash State (read-once)
88
170
 
@@ -160,6 +242,15 @@ const flash = FlashMessage.read();
160
242
  const product = ProductState.read();
161
243
  ```
162
244
 
245
+ `.read(location)` reads this definition's slot from any `{ state }` snapshot
246
+ instead of the current entry, such as the `from` / `to` of a
247
+ `transition({ when })` context. It works without `window` and never clears
248
+ flash state:
249
+
250
+ ```tsx
251
+ transition({ when: ({ to }) => ProductState.read(to) !== undefined });
252
+ ```
253
+
163
254
  > **Hydration:** `.read()` returns `undefined` on the server but may return
164
255
  > a real value on the first client render (history state survives reload).
165
256
  > Do not call `.read()` directly during the initial render of a component;
@@ -191,11 +282,12 @@ const current = ProductState.read();
191
282
  ProductState.delete();
192
283
  ```
193
284
 
194
- | Method | Updates `history.state` | Fires `useLocationState` rerender | SSR behavior |
195
- | ----------- | ----------------------- | --------------------------------- | ------------------- |
196
- | `.read()` | no | n/a (returns snapshot) | returns `undefined` |
197
- | `.write()` | yes (replace this slot) | no | throws |
198
- | `.delete()` | yes (remove this slot) | no | throws |
285
+ | Method | Updates `history.state` | Fires `useLocationState` rerender | SSR behavior |
286
+ | ----------------- | ----------------------- | --------------------------------- | ------------------- |
287
+ | `.read()` | no | n/a (returns snapshot) | returns `undefined` |
288
+ | `.read(location)` | no | n/a (reads the given snapshot) | reads the snapshot |
289
+ | `.write()` | yes (replace this slot) | no | throws |
290
+ | `.delete()` | yes (remove this slot) | no | throws |
199
291
 
200
292
  ## Cache Control
201
293
 
@@ -214,7 +306,9 @@ it is selected by export conditions:
214
306
  a rotated state `Set-Cookie` for the responding client, which marks its
215
307
  caches stale when it next reads them. Idempotent within a request; throws
216
308
  inside a `cache()` / `"use cache"` boundary; a no-op with a dev warning
217
- outside a request.
309
+ outside a request. On a `ppr` route a shell HIT runs no handler, so a call
310
+ from handler code writes the cookie only on the renders that run it (a
311
+ MISS); call it from middleware, which runs on every request, HITs included.
218
312
 
219
313
  Server actions already invalidate automatically; to suppress that for a no-op
220
314
  action, call `keepClientCache()` inside it (see `/server-actions` → "Client