@rangojs/router 0.15.1 → 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.
- package/README.md +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +52 -0
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +36 -10
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +3 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/thenable-status.d.ts +9 -0
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +884 -768
- package/package.json +4 -4
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +43 -1
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +75 -80
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +106 -0
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +134 -42
- package/src/route-definition/helpers-types.ts +36 -10
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +2 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/ssr-suspension-warning.ts +2 -13
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +14 -12
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/thenable-status.ts +16 -0
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +24 -4
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
package/skills/caching/SKILL.md
CHANGED
|
@@ -1,62 +1,118 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: caching
|
|
3
|
-
description: Configure
|
|
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
|
-
|
|
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`
|
|
12
|
-
> and
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
> `
|
|
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
|
|
22
|
-
|
|
23
|
-
- You want the HTML shell cached
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
50
|
+
layout(<RootLayout />), // cached
|
|
43
51
|
path("/dashboard", Dashboard, { name: "dashboard" }, () => [
|
|
44
|
-
loader(StatsLoader), //
|
|
52
|
+
loader(StatsLoader), // live hole — re-runs every request
|
|
45
53
|
]),
|
|
46
54
|
]);
|
|
47
55
|
```
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
//
|
|
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, //
|
|
86
|
-
swr: 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"); //
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
//
|
|
161
|
-
// the next read re-renders fresh (NOT stale-while-revalidate).
|
|
162
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
464
|
-
|
|
|
465
|
-
| `cookies()` / `headers()` (read or write)
|
|
466
|
-
| `ctx.
|
|
467
|
-
| `ctx.get(var)` where the var is `{ cache: false }`
|
|
468
|
-
| `ctx.set(var, value)` for a cacheable var
|
|
469
|
-
| Any of the above **inside a loader**
|
|
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
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
482
|
-
rendered in the client component, so it never lands in the cached
|
|
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.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
]),
|