@rangojs/router 0.16.0 → 0.18.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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -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-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- 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 +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- 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 +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -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/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- 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 +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- 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/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.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-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- 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/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- 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/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -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 +75 -57
- 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 +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -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 +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- 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 +26 -6
- 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/serve-shell-request.md +188 -0
- 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 +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- 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 +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- 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 +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -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 +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- 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 +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -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 +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -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 +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
package/skills/caching/SKILL.md
CHANGED
|
@@ -1,62 +1,122 @@
|
|
|
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
|
-
|
|
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, and a shell never outlives the route `cache()` entry it was captured
|
|
62
|
+
from (it is fresh and served no longer than that entry; `/ppr`).
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
```typescript
|
|
65
|
+
layout(AccountShell, () => [
|
|
66
|
+
// live: runs on every request, cache hits included
|
|
67
|
+
cache({ ttl: 60 }, () => [
|
|
68
|
+
path("/products/:id", ProductPage, { name: "product" }), // cached
|
|
69
|
+
]),
|
|
70
|
+
]);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A `cache()` among a path's own children caches that one path: its handler and
|
|
74
|
+
its own layouts and parallels. Every other item in the list still attaches to
|
|
75
|
+
the path, and everything above the path stays live:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
layout(AccountShell, () => [
|
|
79
|
+
// live
|
|
80
|
+
path("/products/:id", ProductPage, { name: "product" }, () => [
|
|
81
|
+
cache({ ttl: 60 }), // caches ProductPage and ProductChrome
|
|
82
|
+
layout(ProductChrome),
|
|
83
|
+
]),
|
|
84
|
+
]);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A `cache()` inside a `layout()` or `middleware()` wrapper in the path does the
|
|
88
|
+
same: `layout(ProductChrome, () => [cache({ ttl: 60 })])` caches the whole
|
|
89
|
+
path, not only ProductChrome.
|
|
90
|
+
|
|
91
|
+
The consumer rule: **want it cached? render it inline. Want it live? put it in a
|
|
92
|
+
loader and read it with `useLoader()` in a client component.** Anything read
|
|
93
|
+
with `cookies()`, `headers()`, or a non-cacheable variable belongs in a loader:
|
|
94
|
+
a route `cache()` does not store loader values, so a loader runs on every
|
|
95
|
+
request. A loader bound with its own `cache()` is stored, so without a `key()`
|
|
96
|
+
its miss fails; any `key()` switches that check off, so it must include the
|
|
97
|
+
value (see `/loader` → "Cache Key"). Reading it directly in a cached handler throws;
|
|
98
|
+
awaiting a loader with `ctx.use()` and rendering the result in a cached handler
|
|
99
|
+
silently bakes per-request data into the shared entry (see "Cache purity &
|
|
100
|
+
tainted objects" below).
|
|
101
|
+
|
|
102
|
+
Handle pushes follow the same split. Handler pushes (`ctx.use(Meta)`,
|
|
103
|
+
`ctx.use(Breadcrumbs)`, custom handles) are stored with the segments and
|
|
104
|
+
replayed on a hit. Pushes from a `loader()` body are not stored: the loader
|
|
105
|
+
re-runs on the hit and pushes again, so each value appears once. A loader that
|
|
106
|
+
runs only because a cached handler awaited it with `ctx.use(Loader)` does not
|
|
107
|
+
re-run on a hit, so its pushes are stored with the handler's.
|
|
108
|
+
|
|
109
|
+
A component that throws while the entry is written is not cached. Writing
|
|
110
|
+
re-renders the segments, so an async server component whose fetch fails there
|
|
111
|
+
would otherwise be stored as an error and rendered on every hit. Instead the
|
|
112
|
+
write is skipped and reported to `onError` (phase `"cache"`, category
|
|
113
|
+
`cache-write`): the next request renders fresh, and a stale entry keeps serving.
|
|
114
|
+
The same applies to a stored handle push whose value fails to encode, such as a
|
|
115
|
+
rejected promise: the whole entry is skipped, not just its handles.
|
|
116
|
+
|
|
117
|
+
Pre-rendering (`/prerender`) is the build-time counterpart: it stores the same
|
|
118
|
+
kind of segment payload at build time instead of on first request. Both feed the
|
|
119
|
+
segment system identically, and loaders always run fresh at request time.
|
|
60
120
|
|
|
61
121
|
## Route-Level Caching with cache()
|
|
62
122
|
|
|
@@ -66,7 +126,8 @@ Use the `cache()` DSL function to cache routes:
|
|
|
66
126
|
import { urls } from "@rangojs/router";
|
|
67
127
|
|
|
68
128
|
export const urlpatterns = urls(({ path, cache }) => [
|
|
69
|
-
//
|
|
129
|
+
// Fresh for 60 seconds, then served stale for up to 5 more minutes
|
|
130
|
+
// while it refreshes in the background
|
|
70
131
|
cache({ ttl: 60, swr: 300 }, () => [
|
|
71
132
|
path("/blog", BlogIndex, { name: "blog" }),
|
|
72
133
|
path("/blog/:slug", BlogPost, { name: "blogPost" }),
|
|
@@ -77,13 +138,24 @@ export const urlpatterns = urls(({ path, cache }) => [
|
|
|
77
138
|
]);
|
|
78
139
|
```
|
|
79
140
|
|
|
141
|
+
`cache()` needs a store — the app-level one (see "Global Cache Configuration")
|
|
142
|
+
or a per-boundary `store` option. Without one, the boundary renders live.
|
|
143
|
+
|
|
80
144
|
## Cache Options
|
|
81
145
|
|
|
146
|
+
Every option is optional. Omitted `ttl`/`swr` fall back to the store's
|
|
147
|
+
`defaults`.
|
|
148
|
+
|
|
82
149
|
```typescript
|
|
83
150
|
cache(
|
|
84
151
|
{
|
|
85
|
-
ttl: 60, //
|
|
86
|
-
swr: 300, //
|
|
152
|
+
ttl: 60, // seconds fresh. Default: store.defaults.ttl, else 60
|
|
153
|
+
swr: 300, // seconds a stale entry is served while it refreshes.
|
|
154
|
+
// Default: store.defaults.swr, else no SWR window
|
|
155
|
+
tags: ["blog"], // or (ctx) => string[]; see "Tag-Based Invalidation"
|
|
156
|
+
key: (ctx) => `blog:${ctx.pathname}`, // replaces the default key entirely
|
|
157
|
+
condition: (ctx) => !ctx.request.headers.has("x-preview"), // false = bypass
|
|
158
|
+
store: blogStore, // per-boundary store; see "Custom Cache Store"
|
|
87
159
|
},
|
|
88
160
|
() => [
|
|
89
161
|
// Cached routes
|
|
@@ -91,6 +163,22 @@ cache(
|
|
|
91
163
|
);
|
|
92
164
|
```
|
|
93
165
|
|
|
166
|
+
- **Default key**: `{doc|partial|intercept}:{host}{pathname}[:params][?search]`
|
|
167
|
+
— document requests, client navigations, and intercept navigations are cached
|
|
168
|
+
separately. The search part honors `cache.searchParams` (below).
|
|
169
|
+
- **`key`** is a full override: it bypasses the default key, the store's
|
|
170
|
+
`keyGenerator`, and the search-param filter. A nested `cache()` keys its
|
|
171
|
+
records within that partition (see "Keys nest" under "Nested Cache
|
|
172
|
+
Boundaries"). On a `ppr` route the same key
|
|
173
|
+
(or, without one, the store's `keyGenerator`) partitions the PPR shell: each
|
|
174
|
+
partition captures and serves its own shell (`/ppr`, "Request-partitioned
|
|
175
|
+
shells").
|
|
176
|
+
- **`condition`** returning `false` skips both the cache read and write for that
|
|
177
|
+
request (the boundary renders live).
|
|
178
|
+
- `cache(() => [...])` with no options uses the store defaults.
|
|
179
|
+
- `cache(false, () => [...])` disables caching for a subtree (see "Nested Cache
|
|
180
|
+
Boundaries").
|
|
181
|
+
|
|
94
182
|
## When cache() does not pay
|
|
95
183
|
|
|
96
184
|
A cache hit is not free: it still runs middleware, the store read, and the
|
|
@@ -114,6 +202,8 @@ Caching") over caching the rendered segment.
|
|
|
114
202
|
Tag cached entries, then invalidate them on demand. Tags can be attached four ways:
|
|
115
203
|
|
|
116
204
|
```typescript
|
|
205
|
+
import { cacheTag } from "@rangojs/router";
|
|
206
|
+
|
|
117
207
|
// 1. Static tags in the cache() DSL
|
|
118
208
|
cache({ ttl: 300, tags: ["products"] }, () => [path("/products", List)]);
|
|
119
209
|
|
|
@@ -133,22 +223,39 @@ async function getProduct(id: string) {
|
|
|
133
223
|
// 4. Render-callable — a plain server component (no "use cache" in its tree)
|
|
134
224
|
// records onto the request's document/shell artifact.
|
|
135
225
|
function CampaignBanner() {
|
|
136
|
-
cacheTag("campaign:spring"); //
|
|
226
|
+
cacheTag("campaign:spring"); // tags the ppr shell / document-cache entry
|
|
137
227
|
return <aside>Spring sale</aside>;
|
|
138
228
|
}
|
|
139
229
|
```
|
|
140
230
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
231
|
+
Each tag attaches to one entry, so know which entry you are tagging:
|
|
232
|
+
|
|
233
|
+
- Forms 1 and 2 tag the `cache()` segment entry.
|
|
234
|
+
- Form 3 tags the `"use cache"` entry (plus any profile `tags`).
|
|
235
|
+
- Form 4 tags the request's `ppr` shell entry (`/ppr`) or `/document-cache`
|
|
236
|
+
entry, with no `"use cache"` needed. On a route that is neither `ppr` nor
|
|
237
|
+
document-cached and has no `cache()`, nothing reads the tag — a silent
|
|
238
|
+
no-op, so do not expect a bare `cacheTag()` to tag an ordinary uncached page.
|
|
239
|
+
- A route `cache()` entry also carries the tags its own content recorded when
|
|
240
|
+
it was written: form 4 calls and form 3 reads in the handlers and server
|
|
241
|
+
components it covers. `updateTag()` of such a tag drops the entry, and a HIT
|
|
242
|
+
re-records the tags, so a ppr shell or document built from the HIT stays
|
|
243
|
+
evictable. `loading()` subtrees count too: a HIT replays their output. A
|
|
244
|
+
loader's tags reach the entry only when a handler consumes its value
|
|
245
|
+
(`await ctx.use(Loader)`); a loader nobody reads on the server runs per
|
|
246
|
+
request and stays off.
|
|
247
|
+
- A loader with its own `cache()` stores the tags its body recorded (form 4
|
|
248
|
+
calls and form 3 reads, including those of the loaders it reads with
|
|
249
|
+
`ctx.use()`) next to its `cache({ tags })`. They invalidate the loader's
|
|
250
|
+
entry, and a HIT records them again, so a route `cache()` entry, ppr shell
|
|
251
|
+
or document built over the HIT carries them.
|
|
147
252
|
|
|
148
253
|
Invalidate with one of two server-only verbs (both variadic, imported from
|
|
149
254
|
`@rangojs/router`):
|
|
150
255
|
|
|
151
256
|
```typescript
|
|
257
|
+
import { updateTag, revalidateTag } from "@rangojs/router";
|
|
258
|
+
|
|
152
259
|
// Server Action — read-your-own-writes. Await it so the action's own re-render
|
|
153
260
|
// (and the next navigation) sees fresh data.
|
|
154
261
|
async function updateProduct(formData: FormData) {
|
|
@@ -157,10 +264,9 @@ async function updateProduct(formData: FormData) {
|
|
|
157
264
|
await updateTag("products");
|
|
158
265
|
}
|
|
159
266
|
|
|
160
|
-
//
|
|
161
|
-
// the next read re-renders fresh (NOT stale-while-revalidate).
|
|
162
|
-
|
|
163
|
-
"use server";
|
|
267
|
+
// Webhook or response-route handler — background, non-blocking (waitUntil).
|
|
268
|
+
// Hard purge: the next read re-renders fresh (NOT stale-while-revalidate).
|
|
269
|
+
function onProductsChanged() {
|
|
164
270
|
revalidateTag("products");
|
|
165
271
|
return new Response("ok");
|
|
166
272
|
}
|
|
@@ -171,10 +277,38 @@ export async function POST() {
|
|
|
171
277
|
| `updateTag(...tags)` | awaitable (`Promise<void>`) | server actions | immediate; next read is fresh |
|
|
172
278
|
| `revalidateTag(...tags)` | background (`void`) | route handlers / webhooks | background (non-blocking); next read re-renders fresh |
|
|
173
279
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
280
|
+
The request that calls either verb reads its own writes. Before the call
|
|
281
|
+
returns, each built-in store marks the tags as invalidated for the rest of that
|
|
282
|
+
request, so an action that calls `revalidateTag()` and then renders gets fresh
|
|
283
|
+
`"use cache"` and `cache()` reads, as with `await updateTag()`. What
|
|
284
|
+
`revalidateTag()` does not wait for is the durable write (the KV marker, the
|
|
285
|
+
tag purge, Vercel's `expireTag`): other requests see the invalidation once it
|
|
286
|
+
lands. A `"use cache"` call that this request started before the
|
|
287
|
+
invalidation, and that carries one of the invalidated tags, is not reused
|
|
288
|
+
after it and does not store its value; neither does a stale entry's
|
|
289
|
+
background refresh. The tags are the call's own (its `cacheTag()` calls and
|
|
290
|
+
profile `tags`): a nested `"use cache"` function's tags do not reach the
|
|
291
|
+
enclosing entry, so invalidating one of them neither evicts the outer entry
|
|
292
|
+
nor stops an outer call already running
|
|
293
|
+
([#980](https://github.com/rangojs/rango/issues/980)). Not covered
|
|
294
|
+
([#977](https://github.com/rangojs/rango/issues/977)): such a call started
|
|
295
|
+
by another request, or a loader's own `cache()` that started before the
|
|
296
|
+
invalidation, still stores its value when it finishes, and reads after that
|
|
297
|
+
can get it.
|
|
298
|
+
|
|
299
|
+
Both must run inside a request (action, handler, middleware, response route).
|
|
300
|
+
Called from a queue consumer or cron job, there is no request context, so they
|
|
301
|
+
warn and invalidate nothing. `updateTag()` rejects when a store's durable write
|
|
302
|
+
fails, so you can retry; `revalidateTag()` reports the failure to `onError`
|
|
303
|
+
(phase `"cache"`, category `cache-invalidate`) instead.
|
|
304
|
+
|
|
305
|
+
All three built-in stores support tags. If no tag-capable store is configured,
|
|
306
|
+
`updateTag`/`revalidateTag` warn and no-op.
|
|
307
|
+
|
|
308
|
+
For `CFCacheStore`, cross-colo invalidation needs either a `kv` namespace
|
|
309
|
+
(marker mode — the tag-invalidation markers live in that same namespace; there
|
|
310
|
+
is **no** separate tag-invalidation store to wire) or `tagPurge` (purge mode,
|
|
311
|
+
below). With neither, invalidation logs a warning and has no effect.
|
|
178
312
|
|
|
179
313
|
By default `CFCacheStore` reads the KV marker on every tagged cache read
|
|
180
314
|
(strongest invalidation latency). To cut KV reads on hot tagged routes, set
|
|
@@ -196,14 +330,37 @@ Purge-by-tag is available on all plans (since April 2025), subject to per-plan
|
|
|
196
330
|
rate limits, so the batched single call matters. With a purge wired, `tagCacheTtl`
|
|
197
331
|
becomes a pure read-cost reducer + fallback window.
|
|
198
332
|
|
|
333
|
+
**Purge mode (`tagPurge`).** Instead of checking a KV marker on every L1 read,
|
|
334
|
+
the store can evict tagged L1 entries with Cloudflare's purge-by-tag API:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
new CFCacheStore({
|
|
338
|
+
ctx,
|
|
339
|
+
kv: env.CACHE_KV,
|
|
340
|
+
tagPurge: { zoneId: env.CF_ZONE_ID, apiToken: env.CF_PURGE_TOKEN },
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The token needs the `Zone.Cache Purge` permission; keep it in a Worker secret.
|
|
345
|
+
You can also pass a `(cacheTags) => Promise<void>` function (for a proxy or a
|
|
346
|
+
test stub); `createCloudflareZonePurge({ zoneId, apiToken })` from
|
|
347
|
+
`@rangojs/router/cache` builds the default one. In purge mode every tagged L1
|
|
348
|
+
entry carries a namespaced `Cache-Tag`, `updateTag()`/`revalidateTag()` await one
|
|
349
|
+
batched purge call, and L1 hits skip the marker lookup. The KV tier and PPR shell
|
|
350
|
+
reads still use KV markers, so keep `kv` configured when you have it; without
|
|
351
|
+
`kv`, purge mode is the only tag invalidation and the store runs L1-only. A
|
|
352
|
+
failed purge makes `updateTag()` reject. Purge-by-tag clears only your zone:
|
|
353
|
+
`workers.dev` previews have an inert Cache API.
|
|
354
|
+
|
|
199
355
|
## Named Cache Profiles
|
|
200
356
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
357
|
+
Named profiles in `createRouter({ cacheProfiles })` are for the
|
|
358
|
+
`"use cache: <name>"` directive only (see `/use-cache`). `default` is built in as
|
|
359
|
+
`{ ttl: 900, swr: 1800 }` and can be overridden. An invalid profile name or
|
|
360
|
+
ttl/swr value throws when the router is created; an unknown profile name throws
|
|
361
|
+
on the first call of a function that uses it.
|
|
204
362
|
|
|
205
363
|
```typescript
|
|
206
|
-
// Define profiles in router
|
|
207
364
|
createRouter({
|
|
208
365
|
cacheProfiles: {
|
|
209
366
|
default: { ttl: 900, swr: 1800 },
|
|
@@ -213,7 +370,8 @@ createRouter({
|
|
|
213
370
|
});
|
|
214
371
|
```
|
|
215
372
|
|
|
216
|
-
|
|
373
|
+
The DSL `cache()` helper does NOT accept a profile name — pass the options
|
|
374
|
+
object directly:
|
|
217
375
|
|
|
218
376
|
```typescript
|
|
219
377
|
export const urlpatterns = urls(({ path, cache }) => [
|
|
@@ -221,23 +379,34 @@ export const urlpatterns = urls(({ path, cache }) => [
|
|
|
221
379
|
path("/blog", BlogIndex, { name: "blog" }),
|
|
222
380
|
]),
|
|
223
381
|
|
|
224
|
-
// Orphan cache boundary
|
|
382
|
+
// Orphan cache boundary: with no children callback it covers the
|
|
383
|
+
// siblings that follow it
|
|
225
384
|
cache({ ttl: 60, swr: 120 }),
|
|
226
385
|
path("/feed", FeedPage, { name: "feed" }),
|
|
227
386
|
]);
|
|
228
387
|
```
|
|
229
388
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
389
|
+
In a layout's children, a `layout()` after the bare `cache()` wraps every
|
|
390
|
+
route of that layout, as it would without the `cache()`. It is cached only with
|
|
391
|
+
the routes after the `cache()`; on a route before it, it renders live:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
layout(<AppShell />, () => [
|
|
395
|
+
path("/a", PageA, { name: "a" }), // PromoBanner renders live
|
|
396
|
+
cache({ ttl: 60 }),
|
|
397
|
+
layout(<PromoBanner />),
|
|
398
|
+
path("/b", PageB, { name: "b" }), // PromoBanner cached with PageB
|
|
399
|
+
]);
|
|
400
|
+
```
|
|
233
401
|
|
|
234
402
|
## Loader-Level Caching
|
|
235
403
|
|
|
236
|
-
|
|
404
|
+
Loaders are never part of a segment entry. To cache a loader's DATA, give the
|
|
405
|
+
loader its own `cache()`:
|
|
237
406
|
|
|
238
407
|
```typescript
|
|
239
408
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
240
|
-
// Cache this loader's results
|
|
409
|
+
// Cache this loader's results for 5 minutes
|
|
241
410
|
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
242
411
|
|
|
243
412
|
// This loader is not cached
|
|
@@ -245,15 +414,38 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
245
414
|
]);
|
|
246
415
|
```
|
|
247
416
|
|
|
417
|
+
A loader `cache()` takes the same options as the DSL (`ttl`, `swr`, `tags`,
|
|
418
|
+
`key`, `condition`, `store`) and is stored in the store's item family, so SWR
|
|
419
|
+
works on every built-in store. Without options, `cache()` uses the store's
|
|
420
|
+
defaults (ttl 60 when the store sets none); it does **not** inherit the options
|
|
421
|
+
of an enclosing `cache()` boundary. Inside `loader()` only the direct form
|
|
422
|
+
`cache({...})` is valid — the wrapper form `cache(opts, () => [...])` throws.
|
|
423
|
+
Handle pushes from the loader body (`Meta`, `Breadcrumbs`) are stored with the
|
|
424
|
+
value and replayed on every hit, stale included. The tags the body records
|
|
425
|
+
(`cacheTag()`, `"use cache"` reads, and those of the loaders it reads) are
|
|
426
|
+
stored with it too: `updateTag()` of one drops the entry, as `cache({ tags })`
|
|
427
|
+
does. See `/loader` for the full loader reference.
|
|
428
|
+
|
|
429
|
+
The entry's default key is the loader, host, path and params. It does not
|
|
430
|
+
inherit an enclosing `cache()` boundary's `key()` either, so it is shared
|
|
431
|
+
across users: a body that reads `cookies()`, `headers()` or a non-cacheable
|
|
432
|
+
variable fails on a miss unless the loader `cache()` has a `key()` or its store
|
|
433
|
+
a `keyGenerator`. Either one switches the check off, so it must itself include
|
|
434
|
+
the value (`/loader` → "Cache Key").
|
|
435
|
+
|
|
248
436
|
## Global Cache Configuration
|
|
249
437
|
|
|
250
|
-
Configure
|
|
438
|
+
Configure the app-level store on the router. `cache()` boundaries, cached
|
|
439
|
+
loaders, `"use cache"`, `ppr` shells, and `createDocumentCacheMiddleware()` all
|
|
440
|
+
use it.
|
|
251
441
|
|
|
252
442
|
```typescript
|
|
253
443
|
import { createRouter } from "@rangojs/router";
|
|
254
444
|
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
255
445
|
|
|
256
446
|
const store = new MemorySegmentCacheStore({
|
|
447
|
+
// On the memory store `swr` applies to loader, "use cache", response and
|
|
448
|
+
// shell entries; route segments expire at ttl.
|
|
257
449
|
defaults: { ttl: 60, swr: 300 },
|
|
258
450
|
});
|
|
259
451
|
|
|
@@ -262,11 +454,14 @@ const router = createRouter({
|
|
|
262
454
|
urls: urlpatterns,
|
|
263
455
|
cache: {
|
|
264
456
|
store,
|
|
265
|
-
enabled: true,
|
|
457
|
+
enabled: true, // false = no app store for the request: every layer above is off
|
|
266
458
|
},
|
|
267
459
|
});
|
|
268
460
|
```
|
|
269
461
|
|
|
462
|
+
`cache` may also be a function `(env, ctx) => ({ store, enabled?, searchParams? })`
|
|
463
|
+
when the store needs runtime bindings (Cloudflare, Vercel).
|
|
464
|
+
|
|
270
465
|
### Search param key filtering (`cache.searchParams`)
|
|
271
466
|
|
|
272
467
|
By default every non-reserved query param keys the cache, so
|
|
@@ -308,23 +503,32 @@ already expressible with `cache({ key })`.
|
|
|
308
503
|
|
|
309
504
|
## Cache Stores
|
|
310
505
|
|
|
506
|
+
| Store | Import | Use for |
|
|
507
|
+
| ------------------------- | ----------------------- | -------------------------------------------------- |
|
|
508
|
+
| `MemorySegmentCacheStore` | `@rangojs/router/cache` | dev, tests, single-process Node deployments |
|
|
509
|
+
| `CFCacheStore` | `@rangojs/router/cache` | Cloudflare Workers (Cache API L1 + optional KV L2) |
|
|
510
|
+
| `VercelCacheStore` | `@rangojs/router/cache` | Vercel Functions (Vercel Runtime Cache) |
|
|
511
|
+
|
|
311
512
|
### Memory Store
|
|
312
513
|
|
|
313
|
-
For single-instance deployments
|
|
514
|
+
For development, tests, and single-instance deployments. Entries live in
|
|
515
|
+
process memory, so each instance has its own cache and a restart empties it.
|
|
314
516
|
|
|
315
517
|
```typescript
|
|
316
518
|
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
317
519
|
|
|
318
520
|
const store = new MemorySegmentCacheStore({
|
|
319
|
-
defaults: { ttl: 60, swr
|
|
521
|
+
defaults: { ttl: 60 }, // segments ignore swr on this store
|
|
320
522
|
maxEntries: 1000, // per-family FIFO cap (default 1000)
|
|
523
|
+
name: "app", // optional: keep entries across Vite HMR module reloads
|
|
321
524
|
});
|
|
322
525
|
```
|
|
323
526
|
|
|
324
527
|
Each internal family (segments, responses, `"use cache"` items, PPR shells) is
|
|
325
528
|
capped at `maxEntries`; on insert past the cap the oldest entry is evicted FIFO
|
|
326
529
|
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.
|
|
530
|
+
without bound. TTL expiry stays lazy on top of the cap. Two stores with the same
|
|
531
|
+
`name` share their backing maps.
|
|
328
532
|
|
|
329
533
|
### Cloudflare Edge Cache Store
|
|
330
534
|
|
|
@@ -338,7 +542,7 @@ const router = createRouter<AppBindings>({
|
|
|
338
542
|
urls: urlpatterns,
|
|
339
543
|
cache: (env, ctx) => ({
|
|
340
544
|
store: new CFCacheStore({
|
|
341
|
-
ctx,
|
|
545
|
+
ctx: ctx!, // ExecutionContext, required (non-blocking writes)
|
|
342
546
|
defaults: { ttl: 60, swr: 300 },
|
|
343
547
|
}),
|
|
344
548
|
enabled: true,
|
|
@@ -346,6 +550,10 @@ const router = createRouter<AppBindings>({
|
|
|
346
550
|
});
|
|
347
551
|
```
|
|
348
552
|
|
|
553
|
+
`CFCacheStore` prefixes every key with the build version, so a deploy never
|
|
554
|
+
reads the previous build's entries (override with `version`). See `/cache-guide`
|
|
555
|
+
→ "Cross-deploy safety" and `/cloudflare` for bindings.
|
|
556
|
+
|
|
349
557
|
### With KV L2 Persistence
|
|
350
558
|
|
|
351
559
|
Add a KV namespace for global cross-colo persistence. On Cache API miss, KV is
|
|
@@ -359,7 +567,7 @@ const router = createRouter<AppBindings>({
|
|
|
359
567
|
urls: urlpatterns,
|
|
360
568
|
cache: (env, ctx) => ({
|
|
361
569
|
store: new CFCacheStore({
|
|
362
|
-
ctx
|
|
570
|
+
ctx: ctx!,
|
|
363
571
|
kv: env.CACHE_KV, // optional KV namespace binding
|
|
364
572
|
defaults: { ttl: 60, swr: 300 },
|
|
365
573
|
}),
|
|
@@ -376,8 +584,9 @@ const router = createRouter<AppBindings>({
|
|
|
376
584
|
| Cold colo | MISS | HIT | Serve from KV, promote to L1 |
|
|
377
585
|
| First render | MISS | MISS | Render, write to both L1 + KV |
|
|
378
586
|
|
|
379
|
-
KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total
|
|
380
|
-
are only cached in L1.
|
|
587
|
+
KV entries require `expirationTtl >= 60s`. Short-lived data entries (< 60s total
|
|
588
|
+
TTL) are only cached in L1. A PPR shell is always written to KV, a short one
|
|
589
|
+
with the 60 s minimum; its reads still expire it at its own deadline.
|
|
381
590
|
|
|
382
591
|
### Resilience & latency budgets
|
|
383
592
|
|
|
@@ -397,6 +606,74 @@ read. A non-finite value (e.g. `Number(env.UNSET)`) falls back to the default.
|
|
|
397
606
|
The tag-invalidation marker reads inherit these same budgets and **fail open** on
|
|
398
607
|
a KV timeout — the entry is served rather than wrongly treated as invalidated.
|
|
399
608
|
|
|
609
|
+
A PPR shell HIT reads its entry prelude-first: the body budget (or the KV budget
|
|
610
|
+
on a KV read, counted from opening the value) covers only the entry's head and
|
|
611
|
+
prelude, the tag-marker read runs alongside the prelude read, and the capture
|
|
612
|
+
snapshot behind them is read off the commit path, bounded by `kvReadTimeoutMs`
|
|
613
|
+
(at least 1 s: the tail cannot render without it). A HIT whose snapshot read
|
|
614
|
+
times out, or whose snapshot is truncated or corrupt (also evicted), cannot
|
|
615
|
+
replay the captured segment record, so it degrades: the entry is replaced and
|
|
616
|
+
recaptured and the page reloads once into a cache-miss render (`/ppr`). The
|
|
617
|
+
snapshot holds only what a HIT reads: every HIT replays the handler layer from
|
|
618
|
+
the captured segment record, so the `"use cache"` items only handler code read
|
|
619
|
+
are not stored.
|
|
620
|
+
|
|
621
|
+
**Shell memo.** After a shell read, the same isolate serves that key's next
|
|
622
|
+
HITs from memory for `memo.shellMs` (default 2000; `{ shellMs: 0 }` turns it
|
|
623
|
+
off), with no Cache API or KV read. The tag-marker check still runs on every
|
|
624
|
+
HIT, and the isolate that runs `updateTag()`/`revalidateTag()` drops its own
|
|
625
|
+
copies, so that request and later ones on the same isolate never get the
|
|
626
|
+
invalidated shell. What another isolate can serve for up to one window: the
|
|
627
|
+
previous capture of a key another isolate just recaptured, and, without KV in
|
|
628
|
+
purge mode, a purged shell (the purge reaches the stored entry, not other
|
|
629
|
+
isolates' memos). `memo.shellMaxBytes` (default 16 MiB, shared by every
|
|
630
|
+
`CFCacheStore` in the isolate) caps what it holds; only fresh shells are kept.
|
|
631
|
+
|
|
632
|
+
**Tag-marker memo.** With KV, PPR shell reads check their tag markers through
|
|
633
|
+
a per-isolate memo, stale-while-revalidate: a marker read within
|
|
634
|
+
`memo.markerFreshMs` (default 1000) is used as is, one within
|
|
635
|
+
`memo.markerMaxStaleMs` (default 10000) is used while a background read
|
|
636
|
+
refreshes it, an older one waits for KV. The isolate also remembers each
|
|
637
|
+
shell's tag names and starts those marker reads with the Cache API match
|
|
638
|
+
(`ppr.tags` are known up front). `cache()`, `"use cache"` and response entries
|
|
639
|
+
keep reading their markers. `{ markerFreshMs: 0 }` turns the value memo off.
|
|
640
|
+
|
|
641
|
+
**The fresh-reads cookie.** A response whose request ran `updateTag()` or
|
|
642
|
+
`revalidateTag()` sets `<state cookie prefix>-fresh` (`rango-state-fresh` by
|
|
643
|
+
default, shared by every router on the host with that prefix; `HttpOnly`,
|
|
644
|
+
`SameSite=Lax`, `Path=/`, `Secure` on https, `Max-Age` 11 s with KV and 3 s
|
|
645
|
+
without at the defaults: the longest memo staleness plus 1 s). It is strictly
|
|
646
|
+
functional: its value is `1`, it identifies no one, and it only sends that
|
|
647
|
+
browser's reads past the memos. The same user's requests that carry it skip
|
|
648
|
+
both memos on every isolate, so a mutating user never sees a memoized shell or
|
|
649
|
+
marker from before their write. What they read past the memos is the store's
|
|
650
|
+
own consistency. `VercelCacheStore`: `expireTag` is global (about 300 ms), so
|
|
651
|
+
they read fresh in any region. `CFCacheStore` with KV: the shell's tag check
|
|
652
|
+
reads that colo's marker (its Cache API copy when `tagCacheTtl` is set, then
|
|
653
|
+
KV), so in the colo that ran the mutation they read fresh, while a request
|
|
654
|
+
another colo serves can still get the old shell until KV propagates the
|
|
655
|
+
marker there (up to about 60 s) and that colo's cached marker (`tagCacheTtl`)
|
|
656
|
+
expires. `CFCacheStore` without KV in purge mode (`tagPurge`) has no markers:
|
|
657
|
+
the purge is what removes the shell, and they see it once it has reached the
|
|
658
|
+
colo serving them. Other users can see a memoized shell or marker for up to
|
|
659
|
+
`markerMaxStaleMs` (with KV) or the shell window (`VercelCacheStore` in
|
|
660
|
+
another region, `CFCacheStore` in KV-less purge mode, where another isolate's
|
|
661
|
+
memo can keep serving the purged shell). Set `{ shellMs: 0, markerFreshMs: 0 }` where every user's next
|
|
662
|
+
request must see an invalidation (with KV, `{ shellMs: 0 }` alone still leaves
|
|
663
|
+
the marker memo); that also means no cookie. The cookie is not set when the
|
|
664
|
+
invalidation runs after the response headers were sent (a streaming loader or
|
|
665
|
+
render; dev warns): invalidate in a server action, route handler or
|
|
666
|
+
middleware. Any client can send it, which only makes its own requests read
|
|
667
|
+
the store.
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
new CFCacheStore({
|
|
671
|
+
ctx,
|
|
672
|
+
kv: env.CACHE_KV,
|
|
673
|
+
memo: { shellMs: 5000, markerFreshMs: 500 },
|
|
674
|
+
});
|
|
675
|
+
```
|
|
676
|
+
|
|
400
677
|
```typescript
|
|
401
678
|
new CFCacheStore({
|
|
402
679
|
ctx,
|
|
@@ -453,6 +730,54 @@ deployment KV keeps markers hot in its per-colo edge cache, so it stays a few
|
|
|
453
730
|
milliseconds. `match-error` (a transient `cache.match` rejection that falls
|
|
454
731
|
through to L2) is kept distinct from a plain `l1-miss`.
|
|
455
732
|
|
|
733
|
+
### Vercel Runtime Cache Store
|
|
734
|
+
|
|
735
|
+
For Vercel Functions. Pass the Runtime Cache handle from `@vercel/functions`;
|
|
736
|
+
the store adds SWR, tag invalidation, and the family split on top of it.
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
import { getCache, waitUntil } from "@vercel/functions";
|
|
740
|
+
import { VercelCacheStore } from "@rangojs/router/cache";
|
|
741
|
+
|
|
742
|
+
// A per-deploy namespace: Vercel does not reconcile entries across deploys.
|
|
743
|
+
// One handle per process: the store keeps its PPR shell and tag-marker
|
|
744
|
+
// memos per handle.
|
|
745
|
+
const runtimeCache = getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID });
|
|
746
|
+
|
|
747
|
+
const router = createRouter({
|
|
748
|
+
document: Document,
|
|
749
|
+
urls: urlpatterns,
|
|
750
|
+
cache: () => ({
|
|
751
|
+
store: new VercelCacheStore({
|
|
752
|
+
cache: runtimeCache,
|
|
753
|
+
waitUntil,
|
|
754
|
+
defaults: { ttl: 60, swr: 300 },
|
|
755
|
+
}),
|
|
756
|
+
}),
|
|
757
|
+
});
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
The store keeps the same PPR shell memo as `CFCacheStore` (`memo.shellMs`,
|
|
761
|
+
default 2000; `memo.shellMaxBytes`, default 16 MiB) and the same tag-marker
|
|
762
|
+
memo (`memo.markerFreshMs`, default 300; `memo.markerMaxStaleMs`, default
|
|
763
|
+
2000), one per `cache` handle: create the handle once, as above, or the memos
|
|
764
|
+
never hit. The request that runs `updateTag()`/`revalidateTag()` misses on
|
|
765
|
+
the entries carrying those tags before `expireTag` lands. A shell read's
|
|
766
|
+
marker check sees the invalidation in the process that ran it once its
|
|
767
|
+
markers are written, and in the rest of the region once their marker memo
|
|
768
|
+
refreshes. The tag markers are a regional `cache.set` (only
|
|
769
|
+
`expireTag` is global), so another region serves a shell it memoized before
|
|
770
|
+
the invalidation until its window passes, where without the memo `expireTag`
|
|
771
|
+
removes it within about 300 ms; a platform `expireTag` issued outside rango is
|
|
772
|
+
likewise seen when the window passes. The fresh-reads cookie (`Max-Age` 3 s)
|
|
773
|
+
sends the mutating user's next requests past both memos, and `expireTag` is
|
|
774
|
+
global, so they read fresh in any region.
|
|
775
|
+
|
|
776
|
+
Writes over 2 MB (the platform limit, `maxItemBytes`) are skipped, and tags
|
|
777
|
+
beyond the per-item cap are dropped with a warning. The official client swallows
|
|
778
|
+
`expireTag` failures, so treat `updateTag()` on Vercel as best-effort. See
|
|
779
|
+
`/vercel` for the preset and the remaining options (`version`, `name`, `debug`).
|
|
780
|
+
|
|
456
781
|
## Cache purity & tainted objects
|
|
457
782
|
|
|
458
783
|
A `cache()` boundary caches everything except loaders, so anything read inside a
|
|
@@ -460,26 +785,35 @@ cached handler is **frozen into the shared cache entry** and served to every
|
|
|
460
785
|
subsequent visitor. To stop one user's request-scoped data from leaking to
|
|
461
786
|
another, request-scoped APIs are guarded inside a cache scope:
|
|
462
787
|
|
|
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.
|
|
469
|
-
|
|
|
788
|
+
| Inside a `cache()` boundary | Behavior |
|
|
789
|
+
| ---------------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
790
|
+
| `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
|
|
791
|
+
| Response writes: `ctx.headers.set()`, `setCookie()`, `setStatus()`, `onResponse()` | **throws** — response side effects lost on a hit |
|
|
792
|
+
| `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
|
|
793
|
+
| `ctx.theme`, `getRequestContext().theme` | **throws** — the theme is the visitor's cookie |
|
|
794
|
+
| `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
|
|
795
|
+
| Any of the above **inside a registered loader** (`loader(...)`) | **allowed** — loaders always run fresh |
|
|
796
|
+
|
|
797
|
+
A loader body invoked from a handler with `await ctx.use(Loader)` may read
|
|
798
|
+
`cookies()`/`headers()`, but its response writes still throw: on a hit the
|
|
799
|
+
handler is skipped, so that loader never runs.
|
|
800
|
+
|
|
801
|
+
A loader bound with its **own** `cache()` (`loader(Def, () => [cache({...})])`)
|
|
802
|
+
stores its value, and its key does not inherit the route's. A miss whose body
|
|
803
|
+
read `cookies()`, `headers()` or a non-cacheable `ctx.get()` fails unless the
|
|
804
|
+
binding has a `key()` or its store a `keyGenerator`, which must then include
|
|
805
|
+
the value (`/loader` → "Cache Key").
|
|
470
806
|
|
|
471
807
|
**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.
|
|
808
|
+
internal taint symbol so they are excluded from `"use cache"` cache keys. The
|
|
809
|
+
`cache()` scope is tracked in async-local state and deliberately ends inside
|
|
810
|
+
loaders — which is exactly why loaders are the live holes: they may read
|
|
811
|
+
`cookies()`/`headers()` and re-run on every request.
|
|
478
812
|
|
|
479
813
|
The fix for "I need request data in a cached route": register a `loader()` and
|
|
480
814
|
**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
|
|
815
|
+
live hole — its data rides the fresh (never-cached) loader segment and is
|
|
816
|
+
rendered in the client component, so it never lands in the cached entry.
|
|
483
817
|
|
|
484
818
|
This is NOT the same as awaiting the loader in the handler. A cached handler
|
|
485
819
|
that does `await ctx.use(Loader)` and renders the result bakes that per-request
|
|
@@ -489,20 +823,21 @@ is **not** guarded. `ctx.use()` is a server-side escape hatch for non-rendered
|
|
|
489
823
|
uses (set a ctx var, make a routing decision); never render its result inside a
|
|
490
824
|
cached handler.
|
|
491
825
|
|
|
492
|
-
This is the **consumption-lane rule**, and it holds
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
826
|
+
This is the **consumption-lane rule**, and it holds for every shared
|
|
827
|
+
artifact — `cache()`, `"use cache"`, and the PPR shell (`/ppr`): handler
|
|
828
|
+
consumption = baked copy; client-side `useLoader` = live. The tiers differ on
|
|
829
|
+
identity reads inside a handler-consumed loader: `cache()` and `"use cache"`
|
|
830
|
+
permit them (the leak above), while a PPR shell capture refuses them and the
|
|
831
|
+
route stays uncached. It is stated once in `/rango` → Invariants.
|
|
497
832
|
|
|
498
833
|
```typescript
|
|
499
834
|
// WRONG — throws: cookies() read directly in a cached handler
|
|
500
835
|
cache({ ttl: 60 }, () => [
|
|
501
|
-
path("/me", () => <Profile id={cookies().get("uid")?.value}
|
|
836
|
+
path("/me", () => <Profile id={cookies().get("uid")?.value} />, { name: "me" }),
|
|
502
837
|
]);
|
|
503
838
|
|
|
504
839
|
// 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
|
|
840
|
+
// the cached handler, so the user's data is frozen into the shared entry.
|
|
506
841
|
cache({ ttl: 60 }, () => [
|
|
507
842
|
path(
|
|
508
843
|
"/me",
|
|
@@ -522,10 +857,11 @@ cache({ ttl: 60 }, () => [
|
|
|
522
857
|
// profile.tsx (client component)
|
|
523
858
|
"use client";
|
|
524
859
|
import { useLoader } from "@rangojs/router/client";
|
|
860
|
+
import { MeLoader } from "./loaders";
|
|
525
861
|
|
|
526
862
|
export function Profile() {
|
|
527
|
-
const {
|
|
528
|
-
return <span>{user.name}</span>;
|
|
863
|
+
const { data } = useLoader(MeLoader); // fresh per request; never cached
|
|
864
|
+
return <span>{data.user.name}</span>;
|
|
529
865
|
}
|
|
530
866
|
|
|
531
867
|
// urls — register the loader; MeLoader reads cookies() inside the loader (allowed)
|
|
@@ -538,20 +874,98 @@ See `/cache-guide` for the full decision guide and the `cache()` vs `"use cache"
|
|
|
538
874
|
|
|
539
875
|
## Nested Cache Boundaries
|
|
540
876
|
|
|
541
|
-
|
|
877
|
+
An inner boundary overrides the settings of the outer one for its subtree
|
|
878
|
+
(`key` composes instead, see below); `cache(false)` turns caching off for a
|
|
879
|
+
subtree:
|
|
542
880
|
|
|
543
881
|
```typescript
|
|
544
|
-
// Global cache
|
|
545
882
|
cache({ ttl: 300 }, () => [
|
|
546
883
|
path("/blog", BlogIndex, { name: "blog" }),
|
|
547
884
|
|
|
548
|
-
// Override: shorter TTL for
|
|
885
|
+
// Override: shorter TTL for this section
|
|
549
886
|
cache({ ttl: 30 }, () => [
|
|
550
887
|
path("/blog/:slug", BlogPost, { name: "blogPost" }),
|
|
551
888
|
]),
|
|
889
|
+
|
|
890
|
+
// Opt out: always rendered live
|
|
891
|
+
cache(false, () => [path("/blog/drafts", Drafts, { name: "drafts" })]),
|
|
892
|
+
]);
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
On a `ppr` route the opt-out also means no shell: `cache(false)`, or a
|
|
896
|
+
`condition()` that returns false, renders that route (or that request) like a
|
|
897
|
+
cache miss, with no shell served and none captured. A `ppr` route under a
|
|
898
|
+
layout's `cache()` therefore cannot opt out of that `cache()` and still get a
|
|
899
|
+
shell (`/ppr`).
|
|
900
|
+
|
|
901
|
+
### Keys nest: a partition covers the whole subtree
|
|
902
|
+
|
|
903
|
+
A `key()` partitions every record under its `cache()`. A nested `cache()`
|
|
904
|
+
keys its records within that partition, so a request in one partition never
|
|
905
|
+
HITs another partition's inner record:
|
|
906
|
+
|
|
907
|
+
```typescript
|
|
908
|
+
const tier = (ctx) =>
|
|
909
|
+
ctx.request.headers.get("x-tier") === "gold" ? "gold" : "silver";
|
|
910
|
+
|
|
911
|
+
cache({ ttl: 300, key: (ctx) => `tier:${tier(ctx)}` }, () => [
|
|
912
|
+
layout(TierLayout, () => [
|
|
913
|
+
// No key() of its own: the outer key() result, then this boundary's own
|
|
914
|
+
// default key ("tier%3Agold|doc%3Aexample.com%2Fpricing"), so /pricing
|
|
915
|
+
// and /faq keep their own records
|
|
916
|
+
cache({ ttl: 60 }, () => [
|
|
917
|
+
path("/pricing", PricingPage, { name: "pricing" }),
|
|
918
|
+
path("/faq", FaqPage, { name: "faq" }),
|
|
919
|
+
]),
|
|
920
|
+
|
|
921
|
+
// Its own key() composes with the outer one ("tier%3Agold|v%3Ab")
|
|
922
|
+
cache(
|
|
923
|
+
{ ttl: 60, key: (ctx) => `v:${ctx.searchParams.get("v") ?? "a"}` },
|
|
924
|
+
() => [path("/plans", PlansPage, { name: "plans" })],
|
|
925
|
+
),
|
|
926
|
+
]),
|
|
552
927
|
]);
|
|
553
928
|
```
|
|
554
929
|
|
|
930
|
+
The parts are joined by `|`, outermost first, each URI-encoded:
|
|
931
|
+
|
|
932
|
+
| Boundary the route sits in | Record key |
|
|
933
|
+
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
934
|
+
| `cache({ ttl })`, no `key`, under a keyed one | The chain's `key()` results, then this boundary's default key (its store's `keyGenerator` result, else the default key) |
|
|
935
|
+
| `cache({ key })` under a keyed one | The chain's `key()` results |
|
|
936
|
+
| deeper nesting | Composes the same way; only the innermost boundary's missing `key` adds its default key |
|
|
937
|
+
| `cache(false)` | No caching, as before; a `cache()` re-enabled below it stays in the partition |
|
|
938
|
+
| a single keyed `cache()`, or no `key` anywhere | Its raw `key()` result, or the default key, as before |
|
|
939
|
+
|
|
940
|
+
- The encoding is unambiguous among composed keys: an encoded part holds no
|
|
941
|
+
`|`, so no two partitions, and no two inner keys, compose to the same
|
|
942
|
+
record key.
|
|
943
|
+
- A single keyed `cache()` stores its `key()` result as is, so a raw result
|
|
944
|
+
can equal a composed key: a header value `gold|doc%3Aexample.com%2Fpricing`
|
|
945
|
+
returned raw names the record gold's pricing page keeps under the inner
|
|
946
|
+
`cache()` in the example above. Never return raw
|
|
947
|
+
request input from `key()`: normalize it to the values you serve, prefix
|
|
948
|
+
it (`tier:${value}`), or encode it. A composed key holds no `:` (every
|
|
949
|
+
part is URI-encoded), so a prefixed result never equals one.
|
|
950
|
+
- An inner boundary without `key` keeps everything its default key tells
|
|
951
|
+
apart (path, params, search, document vs navigation), so its routes never
|
|
952
|
+
share a record, even when the outer `key()` names no route.
|
|
953
|
+
- A boundary with its own `key()` skips the store's `keyGenerator`, as a
|
|
954
|
+
single `key()` does.
|
|
955
|
+
- Each `key()` runs once per request, however many boundaries and lookups use
|
|
956
|
+
it.
|
|
957
|
+
- A response route's entry (`path.json()` and the other response routes) is
|
|
958
|
+
keyed the same way. A `ppr` route's shell is partitioned by the chain's
|
|
959
|
+
`key()` results alone, since the shell key already carries the URL (plus the
|
|
960
|
+
store `keyGenerator`'s result for an inner boundary without `key`).
|
|
961
|
+
- To share an inner cache across partitions, move it outside the keyed
|
|
962
|
+
`cache()`.
|
|
963
|
+
|
|
964
|
+
Before issue #970, the innermost boundary alone decided the key: an inner
|
|
965
|
+
`cache()` without `key` wrote under the default key and an inner `key()`
|
|
966
|
+
dropped the outer partition, so a silver visitor could HIT what a gold
|
|
967
|
+
visitor's request had cached.
|
|
968
|
+
|
|
555
969
|
## Custom Cache Store
|
|
556
970
|
|
|
557
971
|
Create a dedicated store for specific routes:
|
|
@@ -567,6 +981,20 @@ cache({ store: checkoutCache }, () => [
|
|
|
567
981
|
]);
|
|
568
982
|
```
|
|
569
983
|
|
|
984
|
+
A per-boundary store becomes reachable by `updateTag()`/`revalidateTag()` only
|
|
985
|
+
once that boundary has been matched in the current process. For data you
|
|
986
|
+
invalidate by tag, prefer the app-level store.
|
|
987
|
+
|
|
988
|
+
If you implement `SegmentCacheStore` yourself, `invalidateTags(tags)` is called
|
|
989
|
+
synchronously inside the invalidating request, and `revalidateTag()` does not
|
|
990
|
+
await it. For that request to read its own writes, record the tags and the
|
|
991
|
+
time as invalidated in request-scoped state your reads check (a `WeakMap`
|
|
992
|
+
keyed by the object `getRequestContext()` returns) before the first `await`,
|
|
993
|
+
then start the durable write. Only let that state turn hits into misses, and
|
|
994
|
+
only for entries written at or before that time. A store that skips this
|
|
995
|
+
still works, but the request that ran `revalidateTag()` can read invalidated
|
|
996
|
+
entries until the durable write lands.
|
|
997
|
+
|
|
570
998
|
## Complete Example
|
|
571
999
|
|
|
572
1000
|
```typescript
|
|
@@ -591,7 +1019,8 @@ export const urlpatterns = urls(({ path, layout, cache, loader, revalidate }) =>
|
|
|
591
1019
|
layout(<BlogLayout />, () => [
|
|
592
1020
|
path("/blog", BlogIndex, { name: "blog" }),
|
|
593
1021
|
path("/blog/:slug", BlogPost, { name: "blogPost" }, () => [
|
|
594
|
-
|
|
1022
|
+
// Uses the app store's defaults, not the enclosing boundary's options
|
|
1023
|
+
loader(BlogPostLoader, () => [cache()]),
|
|
595
1024
|
]),
|
|
596
1025
|
]),
|
|
597
1026
|
]),
|
|
@@ -601,7 +1030,10 @@ export const urlpatterns = urls(({ path, layout, cache, loader, revalidate }) =>
|
|
|
601
1030
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
602
1031
|
loader(ProductLoader, () => [cache({ ttl: 120 })]),
|
|
603
1032
|
loader(CartLoader, () => [
|
|
604
|
-
|
|
1033
|
+
// after an action, re-run only for cart actions; on navigation, keep the default
|
|
1034
|
+
revalidate((ctx) =>
|
|
1035
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
1036
|
+
),
|
|
605
1037
|
]),
|
|
606
1038
|
]),
|
|
607
1039
|
]),
|