@rangojs/router 0.16.0 → 0.17.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 (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -1,62 +1,118 @@
1
1
  ---
2
2
  name: caching
3
- description: Configure route/segment-subtree caching with memory, Cloudflare KV, or Vercel cache stores in @rangojs/router. Use when responses should be cached or revalidated, data is stale or not updating after code changes, or you are wiring up a cache store.
3
+ description: Configure segment caching with the cache() DSL in @rangojs/router — TTL/SWR, tags and updateTag/revalidateTag, loader-level caching, and the Memory, Cloudflare (Cache API + optional KV), and Vercel Runtime Cache stores. Use when responses should be cached or revalidated, data is stale or not updating after changes, or you are wiring up a cache store.
4
4
  argument-hint: "[setup]"
5
5
  ---
6
6
 
7
7
  # Caching
8
8
 
9
- @rangojs/router supports segment-level caching with stale-while-revalidate (SWR) for optimal performance.
9
+ This skill covers the route-level `cache()` DSL (segment caching), loader-level
10
+ caching, tag invalidation, and the built-in cache stores. `cache()` stores the
11
+ rendered segments of a route subtree and serves them with a TTL and an optional
12
+ stale-while-revalidate (SWR) window; loaders keep running on every request.
10
13
 
11
- > SWR support is store-specific. `CFCacheStore` revalidates segment, response,
12
- > and `"use cache"` entries in the background. `MemorySegmentCacheStore`
13
- > supports SWR for response and `"use cache"` item entries, but its
14
- > route-segment entries expire at TTL with no background revalidation — use
15
- > `CFCacheStore` for real segment SWR. See `/cache-guide`.
14
+ > SWR support is store-specific. `CFCacheStore` and `VercelCacheStore` serve a
15
+ > stale entry and revalidate it in the background for every entry family
16
+ > (segments, responses, loader and `"use cache"` items, PPR shells).
17
+ > `MemorySegmentCacheStore` does this for responses, items, and shells, but its
18
+ > route-segment entries expire at `ttl` (the `swr` window is ignored) with no
19
+ > background revalidation. Use `CFCacheStore` or `VercelCacheStore` to see
20
+ > segment SWR. See `/cache-guide`.
16
21
 
17
22
  ## Not this skill if…
18
23
 
19
24
  - You want to cache ONE function or component's return value — that is
20
25
  `"use cache"`: see `/use-cache`.
21
- - You want the whole HTTP response frozen at the edge, loader output included —
22
- see `/document-cache`.
23
- - You want the HTML shell cached while loaders stay live per request — see
24
- `/ppr`.
26
+ - You want the whole HTTP response frozen, loader output included — see
27
+ `/document-cache`.
28
+ - You want the rendered HTML shell cached and only the live holes resumed per
29
+ request — that is the `ppr` path option: see `/ppr`.
30
+ - You want segments rendered once at build time — see `/prerender`.
25
31
  - You are unsure which cache layer you need — start at `/cache-guide`.
26
32
 
27
- ## cache() is Partial Prerendering (PPR)
33
+ ## What cache() caches: everything except loaders
28
34
 
29
35
  `cache()` caches **everything except loaders**. On a cache hit, the cached
30
36
  segments (layouts, route components, parallels — including any resolved
31
- Suspense) are served from the store, and **loaders re-run fresh on every
32
- request**, streaming their results into the same response. Loaders are the
33
- dynamic "holes" of an otherwise-cached tree.
37
+ Suspense content) are served from the store, and **loaders re-run fresh on every
38
+ request**, streaming their results into the same response. Loaders are the live
39
+ holes of an otherwise-cached segment tree.
34
40
 
35
- This means a `cache()` boundary at the document root **is** whole-document
36
- Partial Prerendering: the static shell is cached and served instantly while
37
- per-request/per-user data stays live — in one streamed response, no extra round
38
- trip. The browser cannot tell the shell came from cache.
41
+ A `cache()` boundary at the document root therefore gives you a cached shell
42
+ with live loader holes, in one streamed response with no extra round trip. The
43
+ browser cannot tell the segments came from cache. This is a Flight segment
44
+ cache: HTML is still rendered on every request. The `ppr` path option is a
45
+ separate layer that also caches the rendered HTML shell and resumes only the
46
+ holes — see `/ppr`, and `/cache-guide` for how the layers stack.
39
47
 
40
48
  ```typescript
41
49
  cache({ ttl: 60, swr: 300 }, () => [
42
- layout(<RootLayout />), // cached shell
50
+ layout(<RootLayout />), // cached
43
51
  path("/dashboard", Dashboard, { name: "dashboard" }, () => [
44
- loader(StatsLoader), // DYNAMIC HOLE — re-runs every request
52
+ loader(StatsLoader), // live hole — re-runs every request
45
53
  ]),
46
54
  ]);
47
55
  ```
48
56
 
49
- The consumer rule: **want it cached? render it inline. want it dynamic? put it
50
- in a loader and read it with `useLoader()` in a client component.** Anything
51
- read with `cookies()`, `headers()`, or a non-cacheable variable belongs in a
52
- loader (loaders always run fresh). Reading it directly in a cached handler
53
- throws; awaiting it with `ctx.use()` and rendering the result in a cached
54
- handler silently bakes per-request data into the shared shell (see "Cache purity
55
- & tainted objects" below).
57
+ `cache()` covers only its own subtree. A layout declared above the boundary is
58
+ not stored: it renders on every request, hits included, with its parallels and
59
+ loaders, so a per-request shell or a header it writes stays live. On a `ppr`
60
+ route the whole chain bakes into the shell, so there `cache()` covers the whole
61
+ chain.
56
62
 
57
- Pre-rendering (`/prerender`) is the build-time twin: it caches the same shell at
58
- build time instead of on first request. Both feed the segment system
59
- identically, and loaders always run fresh at request time.
63
+ ```typescript
64
+ layout(AccountShell, () => [
65
+ // live: runs on every request, cache hits included
66
+ cache({ ttl: 60 }, () => [
67
+ path("/products/:id", ProductPage, { name: "product" }), // cached
68
+ ]),
69
+ ]);
70
+ ```
71
+
72
+ A `cache()` among a path's own children caches that one path: its handler and
73
+ its own layouts and parallels. Every other item in the list still attaches to
74
+ the path, and everything above the path stays live:
75
+
76
+ ```typescript
77
+ layout(AccountShell, () => [
78
+ // live
79
+ path("/products/:id", ProductPage, { name: "product" }, () => [
80
+ cache({ ttl: 60 }), // caches ProductPage and ProductChrome
81
+ layout(ProductChrome),
82
+ ]),
83
+ ]);
84
+ ```
85
+
86
+ A `cache()` inside a `layout()` or `middleware()` wrapper in the path does the
87
+ same: `layout(ProductChrome, () => [cache({ ttl: 60 })])` caches the whole
88
+ path, not only ProductChrome.
89
+
90
+ The consumer rule: **want it cached? render it inline. Want it live? put it in a
91
+ loader and read it with `useLoader()` in a client component.** Anything read
92
+ with `cookies()`, `headers()`, or a non-cacheable variable belongs in a loader
93
+ (loaders always run fresh). Reading it directly in a cached handler throws;
94
+ awaiting a loader with `ctx.use()` and rendering the result in a cached handler
95
+ silently bakes per-request data into the shared entry (see "Cache purity &
96
+ tainted objects" below).
97
+
98
+ Handle pushes follow the same split. Handler pushes (`ctx.use(Meta)`,
99
+ `ctx.use(Breadcrumbs)`, custom handles) are stored with the segments and
100
+ replayed on a hit. Pushes from a `loader()` body are not stored: the loader
101
+ re-runs on the hit and pushes again, so each value appears once. A loader that
102
+ runs only because a cached handler awaited it with `ctx.use(Loader)` does not
103
+ re-run on a hit, so its pushes are stored with the handler's.
104
+
105
+ A component that throws while the entry is written is not cached. Writing
106
+ re-renders the segments, so an async server component whose fetch fails there
107
+ would otherwise be stored as an error and rendered on every hit. Instead the
108
+ write is skipped and reported to `onError` (phase `"cache"`, category
109
+ `cache-write`): the next request renders fresh, and a stale entry keeps serving.
110
+ The same applies to a stored handle push whose value fails to encode, such as a
111
+ rejected promise: the whole entry is skipped, not just its handles.
112
+
113
+ Pre-rendering (`/prerender`) is the build-time counterpart: it stores the same
114
+ kind of segment payload at build time instead of on first request. Both feed the
115
+ segment system identically, and loaders always run fresh at request time.
60
116
 
61
117
  ## Route-Level Caching with cache()
62
118
 
@@ -66,7 +122,8 @@ Use the `cache()` DSL function to cache routes:
66
122
  import { urls } from "@rangojs/router";
67
123
 
68
124
  export const urlpatterns = urls(({ path, cache }) => [
69
- // Cache these routes for 60 seconds, SWR for 5 minutes
125
+ // Fresh for 60 seconds, then served stale for up to 5 more minutes
126
+ // while it refreshes in the background
70
127
  cache({ ttl: 60, swr: 300 }, () => [
71
128
  path("/blog", BlogIndex, { name: "blog" }),
72
129
  path("/blog/:slug", BlogPost, { name: "blogPost" }),
@@ -77,13 +134,24 @@ export const urlpatterns = urls(({ path, cache }) => [
77
134
  ]);
78
135
  ```
79
136
 
137
+ `cache()` needs a store — the app-level one (see "Global Cache Configuration")
138
+ or a per-boundary `store` option. Without one, the boundary renders live.
139
+
80
140
  ## Cache Options
81
141
 
142
+ Every option is optional. Omitted `ttl`/`swr` fall back to the store's
143
+ `defaults`.
144
+
82
145
  ```typescript
83
146
  cache(
84
147
  {
85
- ttl: 60, // Time-to-live in seconds (default: 60)
86
- swr: 300, // Stale-while-revalidate window (default: 300)
148
+ ttl: 60, // seconds fresh. Default: store.defaults.ttl, else 60
149
+ swr: 300, // seconds a stale entry is served while it refreshes.
150
+ // Default: store.defaults.swr, else no SWR window
151
+ tags: ["blog"], // or (ctx) => string[]; see "Tag-Based Invalidation"
152
+ key: (ctx) => `blog:${ctx.pathname}`, // replaces the default key entirely
153
+ condition: (ctx) => !ctx.request.headers.has("x-preview"), // false = bypass
154
+ store: blogStore, // per-boundary store; see "Custom Cache Store"
87
155
  },
88
156
  () => [
89
157
  // Cached routes
@@ -91,6 +159,17 @@ cache(
91
159
  );
92
160
  ```
93
161
 
162
+ - **Default key**: `{doc|partial|intercept}:{host}{pathname}[:params][?search]`
163
+ — document requests, client navigations, and intercept navigations are cached
164
+ separately. The search part honors `cache.searchParams` (below).
165
+ - **`key`** is a full override: it bypasses the default key, the store's
166
+ `keyGenerator`, and the search-param filter.
167
+ - **`condition`** returning `false` skips both the cache read and write for that
168
+ request (the boundary renders live).
169
+ - `cache(() => [...])` with no options uses the store defaults.
170
+ - `cache(false, () => [...])` disables caching for a subtree (see "Nested Cache
171
+ Boundaries").
172
+
94
173
  ## When cache() does not pay
95
174
 
96
175
  A cache hit is not free: it still runs middleware, the store read, and the
@@ -114,6 +193,8 @@ Caching") over caching the rendered segment.
114
193
  Tag cached entries, then invalidate them on demand. Tags can be attached four ways:
115
194
 
116
195
  ```typescript
196
+ import { cacheTag } from "@rangojs/router";
197
+
117
198
  // 1. Static tags in the cache() DSL
118
199
  cache({ ttl: 300, tags: ["products"] }, () => [path("/products", List)]);
119
200
 
@@ -133,22 +214,29 @@ async function getProduct(id: string) {
133
214
  // 4. Render-callable — a plain server component (no "use cache" in its tree)
134
215
  // records onto the request's document/shell artifact.
135
216
  function CampaignBanner() {
136
- cacheTag("campaign:spring"); // rides ctx._requestTags → shell/document entry
217
+ cacheTag("campaign:spring"); // tags the ppr shell / document-cache entry
137
218
  return <aside>Spring sale</aside>;
138
219
  }
139
220
  ```
140
221
 
141
- Form 4 is how you make a PPR shell or a `/document-cache` page tag-invalidatable
142
- without wrapping anything in `"use cache"`: the tag rides the request's
143
- `_requestTags` onto the shell/document entry, and `revalidateTag` then evicts it.
144
- On a route that is neither PPR nor document-cached the tag records where nothing
145
- reads it — a silent no-op, so don't expect a bare `cacheTag()` to tag an ordinary
146
- uncached page.
222
+ Each tag attaches to one entry, so know which entry you are tagging:
223
+
224
+ - Forms 1 and 2 tag the `cache()` segment entry. They are the only way to tag
225
+ it: tags recorded by a `"use cache"` function (form 3) stay on that
226
+ function's entry and do not attach to an enclosing `cache()` segment entry.
227
+ - Form 3 tags the `"use cache"` entry (plus any profile `tags`).
228
+ - Form 4 tags the request's `ppr` shell entry (`/ppr`) or `/document-cache`
229
+ entry, with no `"use cache"` needed. Called inside a `cache()` segment it
230
+ still tags the document/shell, not the segment entry. On a route that is
231
+ neither `ppr` nor document-cached, nothing reads the tag — a silent no-op, so
232
+ do not expect a bare `cacheTag()` to tag an ordinary uncached page.
147
233
 
148
234
  Invalidate with one of two server-only verbs (both variadic, imported from
149
235
  `@rangojs/router`):
150
236
 
151
237
  ```typescript
238
+ import { updateTag, revalidateTag } from "@rangojs/router";
239
+
152
240
  // Server Action — read-your-own-writes. Await it so the action's own re-render
153
241
  // (and the next navigation) sees fresh data.
154
242
  async function updateProduct(formData: FormData) {
@@ -157,10 +245,9 @@ async function updateProduct(formData: FormData) {
157
245
  await updateTag("products");
158
246
  }
159
247
 
160
- // Route handler / webhook — background, non-blocking (waitUntil). Hard-purge:
161
- // the next read re-renders fresh (NOT stale-while-revalidate).
162
- export async function POST() {
163
- "use server";
248
+ // Webhook or response-route handler — background, non-blocking (waitUntil).
249
+ // Hard purge: the next read re-renders fresh (NOT stale-while-revalidate).
250
+ function onProductsChanged() {
164
251
  revalidateTag("products");
165
252
  return new Response("ok");
166
253
  }
@@ -171,10 +258,19 @@ export async function POST() {
171
258
  | `updateTag(...tags)` | awaitable (`Promise<void>`) | server actions | immediate; next read is fresh |
172
259
  | `revalidateTag(...tags)` | background (`void`) | route handlers / webhooks | background (non-blocking); next read re-renders fresh |
173
260
 
174
- All three built-in stores support tags. For `CFCacheStore`, distributed (cross-colo)
175
- invalidation requires a `kv` namespace — the tag-invalidation markers live in
176
- that same namespace; there is **no** separate tag-invalidation store to wire.
177
- If no tag-capable store is configured, `updateTag`/`revalidateTag` warn and no-op.
261
+ Both must run inside a request (action, handler, middleware, response route).
262
+ Called from a queue consumer or cron job, there is no request context, so they
263
+ warn and invalidate nothing. `updateTag()` rejects when a store's durable write
264
+ fails, so you can retry; `revalidateTag()` reports the failure to `onError`
265
+ (phase `"cache"`, category `cache-invalidate`) instead.
266
+
267
+ All three built-in stores support tags. If no tag-capable store is configured,
268
+ `updateTag`/`revalidateTag` warn and no-op.
269
+
270
+ For `CFCacheStore`, cross-colo invalidation needs either a `kv` namespace
271
+ (marker mode — the tag-invalidation markers live in that same namespace; there
272
+ is **no** separate tag-invalidation store to wire) or `tagPurge` (purge mode,
273
+ below). With neither, invalidation logs a warning and has no effect.
178
274
 
179
275
  By default `CFCacheStore` reads the KV marker on every tagged cache read
180
276
  (strongest invalidation latency). To cut KV reads on hot tagged routes, set
@@ -196,14 +292,37 @@ Purge-by-tag is available on all plans (since April 2025), subject to per-plan
196
292
  rate limits, so the batched single call matters. With a purge wired, `tagCacheTtl`
197
293
  becomes a pure read-cost reducer + fallback window.
198
294
 
295
+ **Purge mode (`tagPurge`).** Instead of checking a KV marker on every L1 read,
296
+ the store can evict tagged L1 entries with Cloudflare's purge-by-tag API:
297
+
298
+ ```typescript
299
+ new CFCacheStore({
300
+ ctx,
301
+ kv: env.CACHE_KV,
302
+ tagPurge: { zoneId: env.CF_ZONE_ID, apiToken: env.CF_PURGE_TOKEN },
303
+ });
304
+ ```
305
+
306
+ The token needs the `Zone.Cache Purge` permission; keep it in a Worker secret.
307
+ You can also pass a `(cacheTags) => Promise<void>` function (for a proxy or a
308
+ test stub); `createCloudflareZonePurge({ zoneId, apiToken })` from
309
+ `@rangojs/router/cache` builds the default one. In purge mode every tagged L1
310
+ entry carries a namespaced `Cache-Tag`, `updateTag()`/`revalidateTag()` await one
311
+ batched purge call, and L1 hits skip the marker lookup. The KV tier and PPR shell
312
+ reads still use KV markers, so keep `kv` configured when you have it; without
313
+ `kv`, purge mode is the only tag invalidation and the store runs L1-only. A
314
+ failed purge makes `updateTag()` reject. Purge-by-tag clears only your zone:
315
+ `workers.dev` previews have an inert Cache API.
316
+
199
317
  ## Named Cache Profiles
200
318
 
201
- Define named profiles in `createRouter({ cacheProfiles })` so the same TTL/SWR
202
- values can be shared across the DSL and `"use cache"` functions without repetition.
203
- Unknown names throw at boot time.
319
+ Named profiles in `createRouter({ cacheProfiles })` are for the
320
+ `"use cache: <name>"` directive only (see `/use-cache`). `default` is built in as
321
+ `{ ttl: 900, swr: 1800 }` and can be overridden. An invalid profile name or
322
+ ttl/swr value throws when the router is created; an unknown profile name throws
323
+ on the first call of a function that uses it.
204
324
 
205
325
  ```typescript
206
- // Define profiles in router
207
326
  createRouter({
208
327
  cacheProfiles: {
209
328
  default: { ttl: 900, swr: 1800 },
@@ -213,7 +332,8 @@ createRouter({
213
332
  });
214
333
  ```
215
334
 
216
- In the DSL, pass the profile's options directly to `cache()`:
335
+ The DSL `cache()` helper does NOT accept a profile name — pass the options
336
+ object directly:
217
337
 
218
338
  ```typescript
219
339
  export const urlpatterns = urls(({ path, cache }) => [
@@ -221,23 +341,34 @@ export const urlpatterns = urls(({ path, cache }) => [
221
341
  path("/blog", BlogIndex, { name: "blog" }),
222
342
  ]),
223
343
 
224
- // Orphan cache boundary (covers subsequent siblings)
344
+ // Orphan cache boundary: with no children callback it covers the
345
+ // siblings that follow it
225
346
  cache({ ttl: 60, swr: 120 }),
226
347
  path("/feed", FeedPage, { name: "feed" }),
227
348
  ]);
228
349
  ```
229
350
 
230
- The DSL `cache()` helper does NOT accept a string profile name — strings are only
231
- valid in the `"use cache: <name>"` directive inside server functions. See
232
- `/use-cache` for function-level caching with named profiles.
351
+ In a layout's children, a `layout()` after the bare `cache()` wraps every
352
+ route of that layout, as it would without the `cache()`. It is cached only with
353
+ the routes after the `cache()`; on a route before it, it renders live:
354
+
355
+ ```typescript
356
+ layout(<AppShell />, () => [
357
+ path("/a", PageA, { name: "a" }), // PromoBanner renders live
358
+ cache({ ttl: 60 }),
359
+ layout(<PromoBanner />),
360
+ path("/b", PageB, { name: "b" }), // PromoBanner cached with PageB
361
+ ]);
362
+ ```
233
363
 
234
364
  ## Loader-Level Caching
235
365
 
236
- Cache individual loaders:
366
+ Loaders are never part of a segment entry. To cache a loader's DATA, give the
367
+ loader its own `cache()`:
237
368
 
238
369
  ```typescript
239
370
  path("/product/:slug", ProductPage, { name: "product" }, () => [
240
- // Cache this loader's results
371
+ // Cache this loader's results for 5 minutes
241
372
  loader(ProductLoader, () => [cache({ ttl: 300 })]),
242
373
 
243
374
  // This loader is not cached
@@ -245,15 +376,29 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
245
376
  ]);
246
377
  ```
247
378
 
379
+ A loader `cache()` takes the same options as the DSL (`ttl`, `swr`, `tags`,
380
+ `key`, `condition`, `store`) and is stored in the store's item family, so SWR
381
+ works on every built-in store. Without options, `cache()` uses the store's
382
+ defaults (ttl 60 when the store sets none); it does **not** inherit the options
383
+ of an enclosing `cache()` boundary. Inside `loader()` only the direct form
384
+ `cache({...})` is valid — the wrapper form `cache(opts, () => [...])` throws.
385
+ Handle pushes from the loader body (`Meta`, `Breadcrumbs`) are stored with the
386
+ value and replayed on every hit, stale included. See `/loader` for the full
387
+ loader reference.
388
+
248
389
  ## Global Cache Configuration
249
390
 
250
- Configure a cache store in the router:
391
+ Configure the app-level store on the router. `cache()` boundaries, cached
392
+ loaders, `"use cache"`, `ppr` shells, and `createDocumentCacheMiddleware()` all
393
+ use it.
251
394
 
252
395
  ```typescript
253
396
  import { createRouter } from "@rangojs/router";
254
397
  import { MemorySegmentCacheStore } from "@rangojs/router/cache";
255
398
 
256
399
  const store = new MemorySegmentCacheStore({
400
+ // On the memory store `swr` applies to loader, "use cache", response and
401
+ // shell entries; route segments expire at ttl.
257
402
  defaults: { ttl: 60, swr: 300 },
258
403
  });
259
404
 
@@ -262,11 +407,14 @@ const router = createRouter({
262
407
  urls: urlpatterns,
263
408
  cache: {
264
409
  store,
265
- enabled: true,
410
+ enabled: true, // false = no app store for the request: every layer above is off
266
411
  },
267
412
  });
268
413
  ```
269
414
 
415
+ `cache` may also be a function `(env, ctx) => ({ store, enabled?, searchParams? })`
416
+ when the store needs runtime bindings (Cloudflare, Vercel).
417
+
270
418
  ### Search param key filtering (`cache.searchParams`)
271
419
 
272
420
  By default every non-reserved query param keys the cache, so
@@ -308,23 +456,32 @@ already expressible with `cache({ key })`.
308
456
 
309
457
  ## Cache Stores
310
458
 
459
+ | Store | Import | Use for |
460
+ | ------------------------- | ----------------------- | -------------------------------------------------- |
461
+ | `MemorySegmentCacheStore` | `@rangojs/router/cache` | dev, tests, single-process Node deployments |
462
+ | `CFCacheStore` | `@rangojs/router/cache` | Cloudflare Workers (Cache API L1 + optional KV L2) |
463
+ | `VercelCacheStore` | `@rangojs/router/cache` | Vercel Functions (Vercel Runtime Cache) |
464
+
311
465
  ### Memory Store
312
466
 
313
- For single-instance deployments:
467
+ For development, tests, and single-instance deployments. Entries live in
468
+ process memory, so each instance has its own cache and a restart empties it.
314
469
 
315
470
  ```typescript
316
471
  import { MemorySegmentCacheStore } from "@rangojs/router/cache";
317
472
 
318
473
  const store = new MemorySegmentCacheStore({
319
- defaults: { ttl: 60, swr: 300 },
474
+ defaults: { ttl: 60 }, // segments ignore swr on this store
320
475
  maxEntries: 1000, // per-family FIFO cap (default 1000)
476
+ name: "app", // optional: keep entries across Vite HMR module reloads
321
477
  });
322
478
  ```
323
479
 
324
480
  Each internal family (segments, responses, `"use cache"` items, PPR shells) is
325
481
  capped at `maxEntries`; on insert past the cap the oldest entry is evicted FIFO
326
482
  and its tag-index entries are cleaned up, so a long-lived process cannot grow
327
- without bound. TTL expiry stays lazy on top of the cap.
483
+ without bound. TTL expiry stays lazy on top of the cap. Two stores with the same
484
+ `name` share their backing maps.
328
485
 
329
486
  ### Cloudflare Edge Cache Store
330
487
 
@@ -338,7 +495,7 @@ const router = createRouter<AppBindings>({
338
495
  urls: urlpatterns,
339
496
  cache: (env, ctx) => ({
340
497
  store: new CFCacheStore({
341
- ctx,
498
+ ctx: ctx!, // ExecutionContext, required (non-blocking writes)
342
499
  defaults: { ttl: 60, swr: 300 },
343
500
  }),
344
501
  enabled: true,
@@ -346,6 +503,10 @@ const router = createRouter<AppBindings>({
346
503
  });
347
504
  ```
348
505
 
506
+ `CFCacheStore` prefixes every key with the build version, so a deploy never
507
+ reads the previous build's entries (override with `version`). See `/cache-guide`
508
+ → "Cross-deploy safety" and `/cloudflare` for bindings.
509
+
349
510
  ### With KV L2 Persistence
350
511
 
351
512
  Add a KV namespace for global cross-colo persistence. On Cache API miss, KV is
@@ -359,7 +520,7 @@ const router = createRouter<AppBindings>({
359
520
  urls: urlpatterns,
360
521
  cache: (env, ctx) => ({
361
522
  store: new CFCacheStore({
362
- ctx,
523
+ ctx: ctx!,
363
524
  kv: env.CACHE_KV, // optional KV namespace binding
364
525
  defaults: { ttl: 60, swr: 300 },
365
526
  }),
@@ -453,6 +614,34 @@ deployment KV keeps markers hot in its per-colo edge cache, so it stays a few
453
614
  milliseconds. `match-error` (a transient `cache.match` rejection that falls
454
615
  through to L2) is kept distinct from a plain `l1-miss`.
455
616
 
617
+ ### Vercel Runtime Cache Store
618
+
619
+ For Vercel Functions. Pass the Runtime Cache handle from `@vercel/functions`;
620
+ the store adds SWR, tag invalidation, and the family split on top of it.
621
+
622
+ ```typescript
623
+ import { getCache, waitUntil } from "@vercel/functions";
624
+ import { VercelCacheStore } from "@rangojs/router/cache";
625
+
626
+ const router = createRouter({
627
+ document: Document,
628
+ urls: urlpatterns,
629
+ cache: () => ({
630
+ store: new VercelCacheStore({
631
+ // A per-deploy namespace: Vercel does not reconcile entries across deploys
632
+ cache: getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID }),
633
+ waitUntil,
634
+ defaults: { ttl: 60, swr: 300 },
635
+ }),
636
+ }),
637
+ });
638
+ ```
639
+
640
+ Writes over 2 MB (the platform limit, `maxItemBytes`) are skipped, and tags
641
+ beyond the per-item cap are dropped with a warning. The official client swallows
642
+ `expireTag` failures, so treat `updateTag()` on Vercel as best-effort. See
643
+ `/vercel` for the preset and the remaining options (`version`, `name`, `debug`).
644
+
456
645
  ## Cache purity & tainted objects
457
646
 
458
647
  A `cache()` boundary caches everything except loaders, so anything read inside a
@@ -460,26 +649,28 @@ cached handler is **frozen into the shared cache entry** and served to every
460
649
  subsequent visitor. To stop one user's request-scoped data from leaking to
461
650
  another, request-scoped APIs are guarded inside a cache scope:
462
651
 
463
- | Inside a `cache()` boundary | Behavior |
464
- | --------------------------------------------------------------- | --------------------------------------------------- |
465
- | `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
466
- | `ctx.header()` / `setCookie()` / `setStatus()` / `onResponse()` | **throws** — response side effects lost on a hit |
467
- | `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
468
- | `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
469
- | Any of the above **inside a loader** | **allowed** — loaders always run fresh |
652
+ | Inside a `cache()` boundary | Behavior |
653
+ | ---------------------------------------------------------------------------------- | --------------------------------------------------- |
654
+ | `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
655
+ | Response writes: `ctx.headers.set()`, `setCookie()`, `setStatus()`, `onResponse()` | **throws** — response side effects lost on a hit |
656
+ | `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
657
+ | `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
658
+ | Any of the above **inside a registered loader** (`loader(...)`) | **allowed** — loaders always run fresh |
659
+
660
+ A loader body invoked from a handler with `await ctx.use(Loader)` may read
661
+ `cookies()`/`headers()`, but its response writes still throw: on a hit the
662
+ handler is skipped, so that loader never runs.
470
663
 
471
664
  **Tainted objects.** Request-scoped objects (`ctx`, `env`, `request`) carry an
472
- internal taint symbol so they are excluded from `"use cache"` cache keys, and
473
- the cache scope is tracked via async-local state. Two flags back the guards:
474
- `INSIDE_CACHE_EXEC` (set while a `"use cache"` function runs) and the `cache()`
475
- DSL scope (`isInsideCacheScope()`). `isInsideCacheScope()` deliberately returns
476
- `false` inside loaders — which is exactly why loaders are the dynamic holes:
477
- they may read `cookies()`/`headers()` and re-run on every request.
665
+ internal taint symbol so they are excluded from `"use cache"` cache keys. The
666
+ `cache()` scope is tracked in async-local state and deliberately ends inside
667
+ loaders — which is exactly why loaders are the live holes: they may read
668
+ `cookies()`/`headers()` and re-run on every request.
478
669
 
479
670
  The fix for "I need request data in a cached route": register a `loader()` and
480
671
  **consume it with `useLoader()` in a client component**. The loader is the
481
- dynamic hole — its data rides the fresh (never-cached) loader segment and is
482
- rendered in the client component, so it never lands in the cached shell.
672
+ live hole — its data rides the fresh (never-cached) loader segment and is
673
+ rendered in the client component, so it never lands in the cached entry.
483
674
 
484
675
  This is NOT the same as awaiting the loader in the handler. A cached handler
485
676
  that does `await ctx.use(Loader)` and renders the result bakes that per-request
@@ -492,17 +683,16 @@ cached handler.
492
683
  This is the **consumption-lane rule**, and it holds identically for every
493
684
  shared artifact — `cache()`, `"use cache"`, and the PPR shell (`/ppr`):
494
685
  handler consumption = baked copy with identity reads permitted; client-side
495
- `useLoader` = live. Stated once in `/rango` → Invariants; pinned by
496
- semantic-matrix row PPR3 and the `e2e/cache.test.ts` "baked copy" case.
686
+ `useLoader` = live. It is stated once in `/rango` → Invariants.
497
687
 
498
688
  ```typescript
499
689
  // WRONG — throws: cookies() read directly in a cached handler
500
690
  cache({ ttl: 60 }, () => [
501
- path("/me", () => <Profile id={cookies().get("uid")?.value} />),
691
+ path("/me", () => <Profile id={cookies().get("uid")?.value} />, { name: "me" }),
502
692
  ]);
503
693
 
504
694
  // ALSO WRONG (unguarded, but leaks) — the awaited loader data is rendered into
505
- // the cached handler, so the user's data is frozen into the shared shell.
695
+ // the cached handler, so the user's data is frozen into the shared entry.
506
696
  cache({ ttl: 60 }, () => [
507
697
  path(
508
698
  "/me",
@@ -522,10 +712,11 @@ cache({ ttl: 60 }, () => [
522
712
  // profile.tsx (client component)
523
713
  "use client";
524
714
  import { useLoader } from "@rangojs/router/client";
715
+ import { MeLoader } from "./loaders";
525
716
 
526
717
  export function Profile() {
527
- const { user } = useLoader(MeLoader); // fresh per request; never cached
528
- return <span>{user.name}</span>;
718
+ const { data } = useLoader(MeLoader); // fresh per request; never cached
719
+ return <span>{data.user.name}</span>;
529
720
  }
530
721
 
531
722
  // urls — register the loader; MeLoader reads cookies() inside the loader (allowed)
@@ -538,17 +729,20 @@ See `/cache-guide` for the full decision guide and the `cache()` vs `"use cache"
538
729
 
539
730
  ## Nested Cache Boundaries
540
731
 
541
- Override cache settings for specific sections:
732
+ An inner boundary overrides the settings of the outer one for its subtree;
733
+ `cache(false)` turns caching off for a subtree:
542
734
 
543
735
  ```typescript
544
- // Global cache
545
736
  cache({ ttl: 300 }, () => [
546
737
  path("/blog", BlogIndex, { name: "blog" }),
547
738
 
548
- // Override: shorter TTL for dynamic content
739
+ // Override: shorter TTL for this section
549
740
  cache({ ttl: 30 }, () => [
550
741
  path("/blog/:slug", BlogPost, { name: "blogPost" }),
551
742
  ]),
743
+
744
+ // Opt out: always rendered live
745
+ cache(false, () => [path("/blog/drafts", Drafts, { name: "drafts" })]),
552
746
  ]);
553
747
  ```
554
748
 
@@ -567,6 +761,10 @@ cache({ store: checkoutCache }, () => [
567
761
  ]);
568
762
  ```
569
763
 
764
+ A per-boundary store becomes reachable by `updateTag()`/`revalidateTag()` only
765
+ once that boundary has been matched in the current process. For data you
766
+ invalidate by tag, prefer the app-level store.
767
+
570
768
  ## Complete Example
571
769
 
572
770
  ```typescript
@@ -591,7 +789,8 @@ export const urlpatterns = urls(({ path, layout, cache, loader, revalidate }) =>
591
789
  layout(<BlogLayout />, () => [
592
790
  path("/blog", BlogIndex, { name: "blog" }),
593
791
  path("/blog/:slug", BlogPost, { name: "blogPost" }, () => [
594
- loader(BlogPostLoader, () => [cache()]), // Use boundary cache settings
792
+ // Uses the app store's defaults, not the enclosing boundary's options
793
+ loader(BlogPostLoader, () => [cache()]),
595
794
  ]),
596
795
  ]),
597
796
  ]),
@@ -601,7 +800,10 @@ export const urlpatterns = urls(({ path, layout, cache, loader, revalidate }) =>
601
800
  path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
602
801
  loader(ProductLoader, () => [cache({ ttl: 120 })]),
603
802
  loader(CartLoader, () => [
604
- revalidate((ctx) => ctx.isAction(CartActions) || undefined),
803
+ // after an action, re-run only for cart actions; on navigation, keep the default
804
+ revalidate((ctx) =>
805
+ ctx.isAction() ? ctx.isAction(CartActions) : undefined,
806
+ ),
605
807
  ]),
606
808
  ]),
607
809
  ]),