@rangojs/router 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- 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 +224 -72
- 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 +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +9 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- 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 +1094 -376
- 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 +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +47 -3
- package/src/rsc/handler.ts +24 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +89 -9
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
package/skills/caching/SKILL.md
CHANGED
|
@@ -58,7 +58,8 @@ cache({ ttl: 60, swr: 300 }, () => [
|
|
|
58
58
|
not stored: it renders on every request, hits included, with its parallels and
|
|
59
59
|
loaders, so a per-request shell or a header it writes stays live. On a `ppr`
|
|
60
60
|
route the whole chain bakes into the shell, so there `cache()` covers the whole
|
|
61
|
-
chain
|
|
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`).
|
|
62
63
|
|
|
63
64
|
```typescript
|
|
64
65
|
layout(AccountShell, () => [
|
|
@@ -89,8 +90,12 @@ path, not only ProductChrome.
|
|
|
89
90
|
|
|
90
91
|
The consumer rule: **want it cached? render it inline. Want it live? put it in a
|
|
91
92
|
loader and read it with `useLoader()` in a client component.** Anything read
|
|
92
|
-
with `cookies()`, `headers()`, or a non-cacheable
|
|
93
|
-
|
|
93
|
+
with `cookies()`, `headers()`, `ctx.request.headers`, or a non-cacheable
|
|
94
|
+
variable belongs in a loader:
|
|
95
|
+
a route `cache()` does not store loader values, so a loader runs on every
|
|
96
|
+
request. A loader bound with its own `cache()` is stored, so without a `key()`
|
|
97
|
+
its miss fails; any `key()` switches that check off, so it must include the
|
|
98
|
+
value (see `/loader` → "Cache Key"). Reading it directly in a cached handler throws;
|
|
94
99
|
awaiting a loader with `ctx.use()` and rendering the result in a cached handler
|
|
95
100
|
silently bakes per-request data into the shared entry (see "Cache purity &
|
|
96
101
|
tainted objects" below).
|
|
@@ -163,9 +168,20 @@ cache(
|
|
|
163
168
|
— document requests, client navigations, and intercept navigations are cached
|
|
164
169
|
separately. The search part honors `cache.searchParams` (below).
|
|
165
170
|
- **`key`** is a full override: it bypasses the default key, the store's
|
|
166
|
-
`keyGenerator`, and the search-param filter.
|
|
171
|
+
`keyGenerator`, and the search-param filter. The result is stored
|
|
172
|
+
namespaced (`key:` plus its URI encoding), so whatever it returns, request
|
|
173
|
+
input included, never names another route's default-keyed record or a
|
|
174
|
+
nested partition's. A nested `cache()` keys its records within that
|
|
175
|
+
partition (see "Keys nest" under "Nested Cache Boundaries"). On a `ppr`
|
|
176
|
+
route the same key (or, without one, the store's `keyGenerator`)
|
|
177
|
+
partitions the PPR shell: each partition captures and serves its own shell
|
|
178
|
+
(`/ppr`, "Request-partitioned shells").
|
|
167
179
|
- **`condition`** returning `false` skips both the cache read and write for that
|
|
168
|
-
request (the boundary renders live).
|
|
180
|
+
request (the boundary renders live). It gates every `cache()` nested under
|
|
181
|
+
it too: an inner boundary caches only when every enclosing `condition()`
|
|
182
|
+
allows it.
|
|
183
|
+
- **`tags`** tag this boundary's records and those of every `cache()` nested
|
|
184
|
+
under it, so `updateTag()` of an outer tag evicts the inner records too.
|
|
169
185
|
- `cache(() => [...])` with no options uses the store defaults.
|
|
170
186
|
- `cache(false, () => [...])` disables caching for a subtree (see "Nested Cache
|
|
171
187
|
Boundaries").
|
|
@@ -221,15 +237,28 @@ function CampaignBanner() {
|
|
|
221
237
|
|
|
222
238
|
Each tag attaches to one entry, so know which entry you are tagging:
|
|
223
239
|
|
|
224
|
-
- Forms 1 and 2 tag the `cache()` segment entry
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
- Forms 1 and 2 tag the `cache()` segment entry, and the entries of every
|
|
241
|
+
`cache()` nested under it (see "Conditions and tags inherit").
|
|
242
|
+
- Form 3 tags the `"use cache"` entry (plus any profile `tags`), and every
|
|
243
|
+
`"use cache"` entry that calls that function: the outer entry bakes the
|
|
244
|
+
inner value, so it carries the inner tags.
|
|
228
245
|
- Form 4 tags the request's `ppr` shell entry (`/ppr`) or `/document-cache`
|
|
229
|
-
entry, with no `"use cache"` needed.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
246
|
+
entry, with no `"use cache"` needed. On a route that is neither `ppr` nor
|
|
247
|
+
document-cached and has no `cache()`, nothing reads the tag — a silent
|
|
248
|
+
no-op, so do not expect a bare `cacheTag()` to tag an ordinary uncached page.
|
|
249
|
+
- A route `cache()` entry also carries the tags its own content recorded when
|
|
250
|
+
it was written: form 4 calls and form 3 reads in the handlers and server
|
|
251
|
+
components it covers. `updateTag()` of such a tag drops the entry, and a HIT
|
|
252
|
+
re-records the tags, so a ppr shell or document built from the HIT stays
|
|
253
|
+
evictable. `loading()` subtrees count too: a HIT replays their output. A
|
|
254
|
+
loader's tags reach the entry only when a handler consumes its value
|
|
255
|
+
(`await ctx.use(Loader)`); a loader nobody reads on the server runs per
|
|
256
|
+
request and stays off.
|
|
257
|
+
- A loader with its own `cache()` stores the tags its body recorded (form 4
|
|
258
|
+
calls and form 3 reads, including those of the loaders it reads with
|
|
259
|
+
`ctx.use()`) next to its `cache({ tags })`. They invalidate the loader's
|
|
260
|
+
entry, and a HIT records them again, so a route `cache()` entry, ppr shell
|
|
261
|
+
or document built over the HIT carries them.
|
|
233
262
|
|
|
234
263
|
Invalidate with one of two server-only verbs (both variadic, imported from
|
|
235
264
|
`@rangojs/router`):
|
|
@@ -258,6 +287,28 @@ function onProductsChanged() {
|
|
|
258
287
|
| `updateTag(...tags)` | awaitable (`Promise<void>`) | server actions | immediate; next read is fresh |
|
|
259
288
|
| `revalidateTag(...tags)` | background (`void`) | route handlers / webhooks | background (non-blocking); next read re-renders fresh |
|
|
260
289
|
|
|
290
|
+
The request that calls either verb reads its own writes. Before the call
|
|
291
|
+
returns, each built-in store marks the tags as invalidated for the rest of that
|
|
292
|
+
request, so an action that calls `revalidateTag()` and then renders gets fresh
|
|
293
|
+
`"use cache"` and `cache()` reads, as with `await updateTag()`. What
|
|
294
|
+
`revalidateTag()` does not wait for is the durable write (the KV marker, the
|
|
295
|
+
tag purge, Vercel's `expireTag`): other requests see the invalidation once it
|
|
296
|
+
lands.
|
|
297
|
+
|
|
298
|
+
Work that started before the invalidation, in this request or another one,
|
|
299
|
+
is not stored after it. A `"use cache"` call, a stale entry's background
|
|
300
|
+
refresh, a loader's own `cache()`, a route `cache()` render or a
|
|
301
|
+
document-cache render that read its data before one of its tags was
|
|
302
|
+
invalidated still returns what it read, but its store write is skipped:
|
|
303
|
+
every store stamps an entry when it is written, so the old value would be
|
|
304
|
+
served as newer than the invalidation. The next read runs it again. A
|
|
305
|
+
`"use cache"` call already running when one of its tags is invalidated is
|
|
306
|
+
not joined by later calls either. Its tags include those of the
|
|
307
|
+
`"use cache"` functions it calls. Another isolate's invalidation is caught
|
|
308
|
+
through the store's markers (`CFCacheStore` with KV, `VercelCacheStore`); on
|
|
309
|
+
a KV-less `CFCacheStore` only this isolate's invalidations are, and ttl+swr
|
|
310
|
+
bounds the rest.
|
|
311
|
+
|
|
261
312
|
Both must run inside a request (action, handler, middleware, response route).
|
|
262
313
|
Called from a queue consumer or cron job, there is no request context, so they
|
|
263
314
|
warn and invalidate nothing. `updateTag()` rejects when a store's durable write
|
|
@@ -383,8 +434,18 @@ defaults (ttl 60 when the store sets none); it does **not** inherit the options
|
|
|
383
434
|
of an enclosing `cache()` boundary. Inside `loader()` only the direct form
|
|
384
435
|
`cache({...})` is valid — the wrapper form `cache(opts, () => [...])` throws.
|
|
385
436
|
Handle pushes from the loader body (`Meta`, `Breadcrumbs`) are stored with the
|
|
386
|
-
value and replayed on every hit, stale included.
|
|
387
|
-
|
|
437
|
+
value and replayed on every hit, stale included. The tags the body records
|
|
438
|
+
(`cacheTag()`, `"use cache"` reads, and those of the loaders it reads) are
|
|
439
|
+
stored with it too: `updateTag()` of one drops the entry, as `cache({ tags })`
|
|
440
|
+
does. See `/loader` for the full loader reference.
|
|
441
|
+
|
|
442
|
+
The entry's default key is the loader, host, path and params. It does not
|
|
443
|
+
inherit an enclosing `cache()` boundary's `key()` either, so it is shared
|
|
444
|
+
across users: a body that reads `cookies()`, `headers()`, `ctx.request.headers`
|
|
445
|
+
or a non-cacheable variable fails on a miss unless the loader `cache()` has a
|
|
446
|
+
`key()` or its store
|
|
447
|
+
a `keyGenerator`. Either one switches the check off, so it must itself include
|
|
448
|
+
the value (`/loader` → "Cache Key").
|
|
388
449
|
|
|
389
450
|
## Global Cache Configuration
|
|
390
451
|
|
|
@@ -537,8 +598,9 @@ const router = createRouter<AppBindings>({
|
|
|
537
598
|
| Cold colo | MISS | HIT | Serve from KV, promote to L1 |
|
|
538
599
|
| First render | MISS | MISS | Render, write to both L1 + KV |
|
|
539
600
|
|
|
540
|
-
KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total
|
|
541
|
-
are only cached in L1.
|
|
601
|
+
KV entries require `expirationTtl >= 60s`. Short-lived data entries (< 60s total
|
|
602
|
+
TTL) are only cached in L1. A PPR shell is always written to KV, a short one
|
|
603
|
+
with the 60 s minimum; its reads still expire it at its own deadline.
|
|
542
604
|
|
|
543
605
|
### Resilience & latency budgets
|
|
544
606
|
|
|
@@ -558,6 +620,76 @@ read. A non-finite value (e.g. `Number(env.UNSET)`) falls back to the default.
|
|
|
558
620
|
The tag-invalidation marker reads inherit these same budgets and **fail open** on
|
|
559
621
|
a KV timeout — the entry is served rather than wrongly treated as invalidated.
|
|
560
622
|
|
|
623
|
+
A PPR shell HIT reads its entry prelude-first: the body budget (or the KV budget
|
|
624
|
+
on a KV read, counted from opening the value) covers only the entry's head and
|
|
625
|
+
prelude, the tag-marker read runs alongside the prelude read, and the capture
|
|
626
|
+
snapshot behind them is read off the commit path, bounded by `kvReadTimeoutMs`
|
|
627
|
+
(at least 1 s: the tail cannot render without it). A HIT whose snapshot read
|
|
628
|
+
times out, or whose snapshot is truncated or corrupt (also evicted), cannot
|
|
629
|
+
replay the captured segment record, so it degrades: the entry is replaced and
|
|
630
|
+
recaptured and the page reloads once into a cache-miss render (`/ppr`). The
|
|
631
|
+
snapshot holds only what a HIT reads: every HIT replays the handler layer from
|
|
632
|
+
the captured segment record and serves an `ssr: false` loader's baked
|
|
633
|
+
container, so no `"use cache"` or loader `cache()` item is stored in it. A
|
|
634
|
+
live hole, and any loader that runs on the HIT, reads those from the store
|
|
635
|
+
under their own ttl/swr and tags.
|
|
636
|
+
|
|
637
|
+
**Shell memo.** After a shell read, the same isolate serves that key's next
|
|
638
|
+
HITs from memory for `memo.shellMs` (default 2000; `{ shellMs: 0 }` turns it
|
|
639
|
+
off), with no Cache API or KV read. The tag-marker check still runs on every
|
|
640
|
+
HIT, and the isolate that runs `updateTag()`/`revalidateTag()` drops its own
|
|
641
|
+
copies, so that request and later ones on the same isolate never get the
|
|
642
|
+
invalidated shell. What another isolate can serve for up to one window: the
|
|
643
|
+
previous capture of a key another isolate just recaptured, and, without KV in
|
|
644
|
+
purge mode, a purged shell (the purge reaches the stored entry, not other
|
|
645
|
+
isolates' memos). `memo.shellMaxBytes` (default 16 MiB, shared by every
|
|
646
|
+
`CFCacheStore` in the isolate) caps what it holds; only fresh shells are kept.
|
|
647
|
+
|
|
648
|
+
**Tag-marker memo.** With KV, PPR shell reads check their tag markers through
|
|
649
|
+
a per-isolate memo, stale-while-revalidate: a marker read within
|
|
650
|
+
`memo.markerFreshMs` (default 1000) is used as is, one within
|
|
651
|
+
`memo.markerMaxStaleMs` (default 10000) is used while a background read
|
|
652
|
+
refreshes it, an older one waits for KV. The isolate also remembers each
|
|
653
|
+
shell's tag names and starts those marker reads with the Cache API match
|
|
654
|
+
(`ppr.tags` are known up front). `cache()`, `"use cache"` and response entries
|
|
655
|
+
keep reading their markers. `{ markerFreshMs: 0 }` turns the value memo off.
|
|
656
|
+
|
|
657
|
+
**The fresh-reads cookie.** A response whose request ran `updateTag()` or
|
|
658
|
+
`revalidateTag()` sets `<state cookie prefix>-fresh` (`rango-state-fresh` by
|
|
659
|
+
default, shared by every router on the host with that prefix; `HttpOnly`,
|
|
660
|
+
`SameSite=Lax`, `Path=/`, `Secure` on https, `Max-Age` 11 s with KV and 3 s
|
|
661
|
+
without at the defaults: the longest memo staleness plus 1 s). It is strictly
|
|
662
|
+
functional: its value is `1`, it identifies no one, and it only sends that
|
|
663
|
+
browser's reads past the memos. The same user's requests that carry it skip
|
|
664
|
+
both memos on every isolate, so a mutating user never sees a memoized shell or
|
|
665
|
+
marker from before their write. What they read past the memos is the store's
|
|
666
|
+
own consistency. `VercelCacheStore`: `expireTag` is global (about 300 ms), so
|
|
667
|
+
they read fresh in any region. `CFCacheStore` with KV: the shell's tag check
|
|
668
|
+
reads that colo's marker (its Cache API copy when `tagCacheTtl` is set, then
|
|
669
|
+
KV), so in the colo that ran the mutation they read fresh, while a request
|
|
670
|
+
another colo serves can still get the old shell until KV propagates the
|
|
671
|
+
marker there (up to about 60 s) and that colo's cached marker (`tagCacheTtl`)
|
|
672
|
+
expires. `CFCacheStore` without KV in purge mode (`tagPurge`) has no markers:
|
|
673
|
+
the purge is what removes the shell, and they see it once it has reached the
|
|
674
|
+
colo serving them. Other users can see a memoized shell or marker for up to
|
|
675
|
+
`markerMaxStaleMs` (with KV) or the shell window (`VercelCacheStore` in
|
|
676
|
+
another region, `CFCacheStore` in KV-less purge mode, where another isolate's
|
|
677
|
+
memo can keep serving the purged shell). Set `{ shellMs: 0, markerFreshMs: 0 }` where every user's next
|
|
678
|
+
request must see an invalidation (with KV, `{ shellMs: 0 }` alone still leaves
|
|
679
|
+
the marker memo); that also means no cookie. The cookie is not set when the
|
|
680
|
+
invalidation runs after the response headers were sent (a streaming loader or
|
|
681
|
+
render; dev warns): invalidate in a server action, route handler or
|
|
682
|
+
middleware. Any client can send it, which only makes its own requests read
|
|
683
|
+
the store.
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
new CFCacheStore({
|
|
687
|
+
ctx,
|
|
688
|
+
kv: env.CACHE_KV,
|
|
689
|
+
memo: { shellMs: 5000, markerFreshMs: 500 },
|
|
690
|
+
});
|
|
691
|
+
```
|
|
692
|
+
|
|
561
693
|
```typescript
|
|
562
694
|
new CFCacheStore({
|
|
563
695
|
ctx,
|
|
@@ -623,13 +755,17 @@ the store adds SWR, tag invalidation, and the family split on top of it.
|
|
|
623
755
|
import { getCache, waitUntil } from "@vercel/functions";
|
|
624
756
|
import { VercelCacheStore } from "@rangojs/router/cache";
|
|
625
757
|
|
|
758
|
+
// A per-deploy namespace: Vercel does not reconcile entries across deploys.
|
|
759
|
+
// One handle per process: the store keeps its PPR shell and tag-marker
|
|
760
|
+
// memos per handle.
|
|
761
|
+
const runtimeCache = getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID });
|
|
762
|
+
|
|
626
763
|
const router = createRouter({
|
|
627
764
|
document: Document,
|
|
628
765
|
urls: urlpatterns,
|
|
629
766
|
cache: () => ({
|
|
630
767
|
store: new VercelCacheStore({
|
|
631
|
-
|
|
632
|
-
cache: getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID }),
|
|
768
|
+
cache: runtimeCache,
|
|
633
769
|
waitUntil,
|
|
634
770
|
defaults: { ttl: 60, swr: 300 },
|
|
635
771
|
}),
|
|
@@ -637,6 +773,22 @@ const router = createRouter({
|
|
|
637
773
|
});
|
|
638
774
|
```
|
|
639
775
|
|
|
776
|
+
The store keeps the same PPR shell memo as `CFCacheStore` (`memo.shellMs`,
|
|
777
|
+
default 2000; `memo.shellMaxBytes`, default 16 MiB) and the same tag-marker
|
|
778
|
+
memo (`memo.markerFreshMs`, default 300; `memo.markerMaxStaleMs`, default
|
|
779
|
+
2000), one per `cache` handle: create the handle once, as above, or the memos
|
|
780
|
+
never hit. The request that runs `updateTag()`/`revalidateTag()` misses on
|
|
781
|
+
the entries carrying those tags before `expireTag` lands. A shell read's
|
|
782
|
+
marker check sees the invalidation in the process that ran it once its
|
|
783
|
+
markers are written, and in the rest of the region once their marker memo
|
|
784
|
+
refreshes. The tag markers are a regional `cache.set` (only
|
|
785
|
+
`expireTag` is global), so another region serves a shell it memoized before
|
|
786
|
+
the invalidation until its window passes, where without the memo `expireTag`
|
|
787
|
+
removes it within about 300 ms; a platform `expireTag` issued outside rango is
|
|
788
|
+
likewise seen when the window passes. The fresh-reads cookie (`Max-Age` 3 s)
|
|
789
|
+
sends the mutating user's next requests past both memos, and `expireTag` is
|
|
790
|
+
global, so they read fresh in any region.
|
|
791
|
+
|
|
640
792
|
Writes over 2 MB (the platform limit, `maxItemBytes`) are skipped, and tags
|
|
641
793
|
beyond the per-item cap are dropped with a warning. The official client swallows
|
|
642
794
|
`expireTag` failures, so treat `updateTag()` on Vercel as best-effort. See
|
|
@@ -649,18 +801,52 @@ cached handler is **frozen into the shared cache entry** and served to every
|
|
|
649
801
|
subsequent visitor. To stop one user's request-scoped data from leaking to
|
|
650
802
|
another, request-scoped APIs are guarded inside a cache scope:
|
|
651
803
|
|
|
652
|
-
| Inside a `cache()` boundary | Behavior
|
|
653
|
-
| ---------------------------------------------------------------------------------- |
|
|
654
|
-
| `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry
|
|
655
|
-
|
|
|
656
|
-
| `ctx.
|
|
657
|
-
| `ctx.
|
|
658
|
-
|
|
|
804
|
+
| Inside a `cache()` boundary | Behavior |
|
|
805
|
+
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
806
|
+
| `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
|
|
807
|
+
| `ctx.request.headers`, `getRequestContext().cookie()` / `.cookies()` | **throws** on read, like `headers()` / `cookies()` |
|
|
808
|
+
| Response writes: `ctx.headers.set()`, `setCookie()`, `setStatus()`, `onResponse()` | **throws** — response side effects lost on a hit |
|
|
809
|
+
| `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
|
|
810
|
+
| `ctx.theme`, `getRequestContext().theme` | **throws** — the theme is the visitor's cookie |
|
|
811
|
+
| `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
|
|
812
|
+
| Any of the above **inside a registered loader** (`loader(...)`) | **allowed** — loaders always run fresh |
|
|
813
|
+
| Any read in the boundary's own `key()`, `keyGenerator`, `condition()` or `tags()` | **allowed** — it picks or labels the entry, never rendered |
|
|
659
814
|
|
|
660
815
|
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
|
|
816
|
+
`cookies()`/`headers()`/`ctx.request.headers`, but its response writes still throw: on a hit the
|
|
662
817
|
handler is skipped, so that loader never runs.
|
|
663
818
|
|
|
819
|
+
`ctx.request.clone()` is guarded the same way. `fetch(ctx.request)` and
|
|
820
|
+
`new Request(ctx.request)` don't throw, but the guard can't see through them:
|
|
821
|
+
a fetch forwards the visitor's `Cookie` and `Authorization`, so its response
|
|
822
|
+
is per visitor and must not be rendered inside the boundary. A handler that
|
|
823
|
+
must render a header its `key()` partitions by reads a copy middleware set:
|
|
824
|
+
|
|
825
|
+
```tsx
|
|
826
|
+
const Tier = createVar<string>();
|
|
827
|
+
const tierOf = (ctx: { request: Request }) =>
|
|
828
|
+
ctx.request.headers.get("x-tier") === "gold" ? "gold" : "silver";
|
|
829
|
+
|
|
830
|
+
middleware(
|
|
831
|
+
async (ctx, next) => {
|
|
832
|
+
ctx.set(Tier, tierOf(ctx)); // middleware runs outside the boundary
|
|
833
|
+
return next();
|
|
834
|
+
},
|
|
835
|
+
() => [
|
|
836
|
+
cache({ ttl: 300, key: (ctx) => `tier:${tierOf(ctx)}` }, () => [
|
|
837
|
+
path("/pricing", (ctx) => <Pricing tier={ctx.get(Tier)} />),
|
|
838
|
+
]),
|
|
839
|
+
],
|
|
840
|
+
);
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
A loader bound with its **own** `cache()` (`loader(Def, () => [cache({...})])`)
|
|
844
|
+
stores its value, and its key does not inherit the route's. A miss whose body
|
|
845
|
+
read `cookies()`, `headers()`, `ctx.request.headers` or a non-cacheable
|
|
846
|
+
`ctx.get()` fails unless the
|
|
847
|
+
binding has a `key()` or its store a `keyGenerator`, which must then include
|
|
848
|
+
the value (`/loader` → "Cache Key").
|
|
849
|
+
|
|
664
850
|
**Tainted objects.** Request-scoped objects (`ctx`, `env`, `request`) carry an
|
|
665
851
|
internal taint symbol so they are excluded from `"use cache"` cache keys. The
|
|
666
852
|
`cache()` scope is tracked in async-local state and deliberately ends inside
|
|
@@ -680,10 +866,15 @@ is **not** guarded. `ctx.use()` is a server-side escape hatch for non-rendered
|
|
|
680
866
|
uses (set a ctx var, make a routing decision); never render its result inside a
|
|
681
867
|
cached handler.
|
|
682
868
|
|
|
683
|
-
This is the **consumption-lane rule**, and it holds
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
869
|
+
This is the **consumption-lane rule**, and it holds for every shared
|
|
870
|
+
artifact — `cache()`, `"use cache"`, and the PPR shell (`/ppr`): handler
|
|
871
|
+
consumption = baked copy; client-side `useLoader` = live. The tiers differ on
|
|
872
|
+
identity reads inside a handler-consumed loader: `cache()` permits them (the
|
|
873
|
+
leak above); `"use cache"` throws when the cached function is the first to
|
|
874
|
+
read the loader in the request, because the loader body then runs inside it
|
|
875
|
+
and its value is part of the result (a loader already started elsewhere
|
|
876
|
+
returns its value unchecked, and that value is stored); a PPR shell capture
|
|
877
|
+
refuses them and the route stays uncached. It is stated once in `/rango` → Invariants.
|
|
687
878
|
|
|
688
879
|
```typescript
|
|
689
880
|
// WRONG — throws: cookies() read directly in a cached handler
|
|
@@ -729,8 +920,11 @@ See `/cache-guide` for the full decision guide and the `cache()` vs `"use cache"
|
|
|
729
920
|
|
|
730
921
|
## Nested Cache Boundaries
|
|
731
922
|
|
|
732
|
-
An inner boundary overrides
|
|
733
|
-
`cache(false)` turns caching off for a subtree
|
|
923
|
+
An inner boundary overrides `ttl`, `swr` and `store` for its subtree;
|
|
924
|
+
`cache(false)` turns caching off for a subtree. The options that decide
|
|
925
|
+
whether and where a record may be shared build up instead: `key` composes
|
|
926
|
+
(see below), every enclosing `condition()` must allow the inner boundary, and
|
|
927
|
+
the inner records carry the enclosing `tags` too:
|
|
734
928
|
|
|
735
929
|
```typescript
|
|
736
930
|
cache({ ttl: 300 }, () => [
|
|
@@ -746,6 +940,130 @@ cache({ ttl: 300 }, () => [
|
|
|
746
940
|
]);
|
|
747
941
|
```
|
|
748
942
|
|
|
943
|
+
On a `ppr` route the opt-out also means no shell: `cache(false)`, or a
|
|
944
|
+
`condition()` that returns false, renders that route (or that request) like a
|
|
945
|
+
cache miss, with no shell served and none captured. A `ppr` route under a
|
|
946
|
+
layout's `cache()` therefore cannot opt out of that `cache()` and still get a
|
|
947
|
+
shell (`/ppr`).
|
|
948
|
+
|
|
949
|
+
### Conditions and tags inherit
|
|
950
|
+
|
|
951
|
+
```typescript
|
|
952
|
+
cache(
|
|
953
|
+
{
|
|
954
|
+
condition: (ctx) => !ctx.request.headers.has("x-preview"),
|
|
955
|
+
tags: ["catalog"],
|
|
956
|
+
},
|
|
957
|
+
() => [
|
|
958
|
+
cache({ ttl: 60, tags: ["prices"] }, () => [
|
|
959
|
+
path("/prices", PricesPage, { name: "prices" }),
|
|
960
|
+
]),
|
|
961
|
+
],
|
|
962
|
+
);
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
- A preview request renders the prices page live: the outer `condition()` bypasses
|
|
966
|
+
the inner record's read and write, and a `ppr` route's shell. Conditions
|
|
967
|
+
combine with AND, through a `cache(false)` with a `cache()` re-enabled
|
|
968
|
+
below it too.
|
|
969
|
+
- The prices record carries `catalog` and `prices` (static and function
|
|
970
|
+
`tags` alike), so `updateTag("catalog")` evicts it, and the `ppr` shells
|
|
971
|
+
and documents built from it.
|
|
972
|
+
- `cache(false)` is unchanged: it caches nothing below it until a `cache()`
|
|
973
|
+
re-enables caching.
|
|
974
|
+
- A loader's own `cache()` and `"use cache"` are separate layers: they take
|
|
975
|
+
neither the route's `condition()` nor its `tags`, nor its `key()`
|
|
976
|
+
partition.
|
|
977
|
+
|
|
978
|
+
Before issue #974, only the innermost boundary's `condition` and `tags`
|
|
979
|
+
applied: a request an outer `condition()` refused still read and wrote the
|
|
980
|
+
inner record, and `updateTag()` of an outer tag left it in place.
|
|
981
|
+
|
|
982
|
+
### Keys nest: a partition covers the whole subtree
|
|
983
|
+
|
|
984
|
+
A `key()` partitions every record under its `cache()`. A nested `cache()`
|
|
985
|
+
keys its records within that partition, so a request in one partition never
|
|
986
|
+
HITs another partition's inner record:
|
|
987
|
+
|
|
988
|
+
```typescript
|
|
989
|
+
const tier = (ctx) =>
|
|
990
|
+
ctx.request.headers.get("x-tier") === "gold" ? "gold" : "silver";
|
|
991
|
+
|
|
992
|
+
cache({ ttl: 300, key: (ctx) => `tier:${tier(ctx)}` }, () => [
|
|
993
|
+
layout(TierLayout, () => [
|
|
994
|
+
// No key() of its own: the outer key() result, then this boundary's own
|
|
995
|
+
// default key ("key:tier%3Agold|doc%3Aexample.com%2Fpricing"), so
|
|
996
|
+
// /pricing and /faq keep their own records
|
|
997
|
+
cache({ ttl: 60 }, () => [
|
|
998
|
+
path("/pricing", PricingPage, { name: "pricing" }),
|
|
999
|
+
path("/faq", FaqPage, { name: "faq" }),
|
|
1000
|
+
]),
|
|
1001
|
+
|
|
1002
|
+
// Its own key() composes with the outer one ("key:tier%3Agold|key:v%3Ab")
|
|
1003
|
+
cache(
|
|
1004
|
+
{ ttl: 60, key: (ctx) => `v:${ctx.searchParams.get("v") ?? "a"}` },
|
|
1005
|
+
() => [path("/plans", PlansPage, { name: "plans" })],
|
|
1006
|
+
),
|
|
1007
|
+
]),
|
|
1008
|
+
]);
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
Every `key()` result is stored as `key:` plus its URI encoding; a default
|
|
1012
|
+
key in a composed key is URI-encoded. The parts are joined by `|`, `key()`
|
|
1013
|
+
results first, outermost first:
|
|
1014
|
+
|
|
1015
|
+
| Boundary the route sits in | Record key |
|
|
1016
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1017
|
+
| a single keyed `cache()` | Its `key()` result, namespaced (`tier:gold` is stored as `key:tier%3Agold`) |
|
|
1018
|
+
| `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), encoded |
|
|
1019
|
+
| `cache({ key })` under a keyed one | The chain's `key()` results |
|
|
1020
|
+
| deeper nesting | Composes the same way; only the innermost boundary's missing `key` adds its default key |
|
|
1021
|
+
| under a `cache({ store })` on another store | Also that store's `keyGenerator` result, one part per store: empty for a store that returns the default key while another's differs, none when all return it |
|
|
1022
|
+
| `cache(false)` | No caching, as before; a `cache()` re-enabled below it stays in the partition |
|
|
1023
|
+
| no `key` anywhere | The default key, or the store's `keyGenerator` result, as before |
|
|
1024
|
+
|
|
1025
|
+
- No `key()` result, whatever it returns, equals a default key or a
|
|
1026
|
+
composed key, and no two composed keys are equal: a namespaced result
|
|
1027
|
+
starts with `key:` and holds no `|`, a default key never starts with
|
|
1028
|
+
`key:`, and each part of a composed key is encoded, so the parts and their
|
|
1029
|
+
kinds are recoverable. Returning request input from `key()` cannot name
|
|
1030
|
+
another route's record (before issue #975 a header value
|
|
1031
|
+
`doc:example.com/pricing` returned raw named the pricing page's default-keyed
|
|
1032
|
+
record). A prefix (`tier:${value}`) is still worth it for readable keys,
|
|
1033
|
+
and normalizing to the values you serve keeps the partition count bounded.
|
|
1034
|
+
- An inner boundary without `key` keeps everything its default key tells
|
|
1035
|
+
apart (path, params, search, document vs navigation), so its routes never
|
|
1036
|
+
share a record, even when the outer `key()` names no route.
|
|
1037
|
+
- A boundary with its own `key()` skips the store's `keyGenerator`, as a
|
|
1038
|
+
single `key()` does.
|
|
1039
|
+
- Each `key()` runs once per request, however many boundaries and lookups use
|
|
1040
|
+
it.
|
|
1041
|
+
- A store's `keyGenerator` partitions the records of a `cache()` on that
|
|
1042
|
+
store without a `key()` of its own. A `cache({ store })` on another store
|
|
1043
|
+
nested under it keys its records by that `keyGenerator` result too, so a
|
|
1044
|
+
locale-partitioned outer boundary never shares an inner record across
|
|
1045
|
+
locales. With several such stores each keeps its position: one that
|
|
1046
|
+
returns the default key for a request adds an empty part while another's
|
|
1047
|
+
differs, and when all return it the key is unchanged. A `keyGenerator` must
|
|
1048
|
+
return the default key, not `""`, to leave a request unpartitioned: an
|
|
1049
|
+
empty result there fails key resolution, so the request renders uncached,
|
|
1050
|
+
and the router warns once naming the store. On the same store the inner
|
|
1051
|
+
boundary's own default key already carries it (or its own `key()`
|
|
1052
|
+
overrides it).
|
|
1053
|
+
- A response route's entry (`path.json()` and the other response routes) is
|
|
1054
|
+
keyed the same way, behind a `response:` prefix. A `ppr` route's shell is
|
|
1055
|
+
partitioned by the chain's `key()` results, namespaced the same way, since
|
|
1056
|
+
the shell key already carries the URL (plus the `keyGenerator` results
|
|
1057
|
+
above, and the store's for an inner boundary without `key`).
|
|
1058
|
+
- To share an inner cache across partitions, move it outside the keyed
|
|
1059
|
+
`cache()`.
|
|
1060
|
+
|
|
1061
|
+
Before issue #970, the innermost boundary alone decided the key: an inner
|
|
1062
|
+
`cache()` without `key` wrote under the default key and an inner `key()`
|
|
1063
|
+
dropped the outer partition, so a silver visitor could HIT what a gold
|
|
1064
|
+
visitor's request had cached. Before issue #974 an outer `cache({ store })`'s
|
|
1065
|
+
`keyGenerator` did not reach an inner boundary on another store.
|
|
1066
|
+
|
|
749
1067
|
## Custom Cache Store
|
|
750
1068
|
|
|
751
1069
|
Create a dedicated store for specific routes:
|
|
@@ -765,6 +1083,16 @@ A per-boundary store becomes reachable by `updateTag()`/`revalidateTag()` only
|
|
|
765
1083
|
once that boundary has been matched in the current process. For data you
|
|
766
1084
|
invalidate by tag, prefer the app-level store.
|
|
767
1085
|
|
|
1086
|
+
If you implement `SegmentCacheStore` yourself, `invalidateTags(tags)` is called
|
|
1087
|
+
synchronously inside the invalidating request, and `revalidateTag()` does not
|
|
1088
|
+
await it. For that request to read its own writes, record the tags and the
|
|
1089
|
+
time as invalidated in request-scoped state your reads check (a `WeakMap`
|
|
1090
|
+
keyed by the object `getRequestContext()` returns) before the first `await`,
|
|
1091
|
+
then start the durable write. Only let that state turn hits into misses, and
|
|
1092
|
+
only for entries written at or before that time. A store that skips this
|
|
1093
|
+
still works, but the request that ran `revalidateTag()` can read invalidated
|
|
1094
|
+
entries until the durable write lands.
|
|
1095
|
+
|
|
768
1096
|
## Complete Example
|
|
769
1097
|
|
|
770
1098
|
```typescript
|
package/skills/catalog.json
CHANGED
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
},
|
|
252
252
|
{
|
|
253
253
|
"name": "testing",
|
|
254
|
-
"description": "Test @rangojs/router apps — unit (loaders, middleware, server actions, handles, transition/clientUrls predicates), client components (renderRoute), RSC/Flight (renderServerTree, renderHandler), integration (dispatch), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader, middleware, action, or handler, setting up vitest for a Rango app, or testing a route end-to-end in dev and production.",
|
|
254
|
+
"description": "Test @rangojs/router apps — unit (loaders, middleware, server actions, handles, transition/clientUrls predicates), client components (renderRoute), RSC/Flight (renderServerTree, renderHandler, serveShellRequest for PPR shells), integration (dispatch), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader, middleware, action, or handler, setting up vitest for a Rango app, or testing a route end-to-end in dev and production.",
|
|
255
255
|
"argumentHint": "[layer]",
|
|
256
256
|
"path": "skills/testing/SKILL.md"
|
|
257
257
|
},
|
|
@@ -134,7 +134,7 @@ flow into the generated route map, so `href()`, `ctx.reverse()` and
|
|
|
134
134
|
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false }, use?)` — see below |
|
|
135
135
|
| `loading()` | Route-level boundary around the optimistic render; inline `<Suspense>` at read sites keeps the destination's chrome visible while only the reads wait |
|
|
136
136
|
| `revalidate()` | Valid **inside a loader() use callback only**; runs in the browser |
|
|
137
|
-
| `transition()` | Inside a `path()` use callback only (at most one).
|
|
137
|
+
| `transition()` | Inside a `path()` use callback only (at most one). ViewTransition config plus a browser-run `when` predicate (see `/view-transitions`); same-route navs in a group already hold without it |
|
|
138
138
|
| `intercept()` | Dot-local named target in the SAME definition; use may contain `loader()`/`loading()` |
|
|
139
139
|
|
|
140
140
|
At the top level of the builder only `path()`, `layout()` and `intercept()`
|
|
@@ -268,8 +268,9 @@ handler parity — `ctx.use(Meta)({ title })` pushes exactly like a handler
|
|
|
268
268
|
push. Reads moved to `ctx.get(handle)`, gated behind `await ctx.rendered()`.
|
|
269
269
|
Delivery follows the race model: pushes that settle before the handler barrier
|
|
270
270
|
ride the SSR handle snapshot; later ones stream and apply post-hydration
|
|
271
|
-
(document loads) or progressively (navigations).
|
|
272
|
-
|
|
271
|
+
(document loads) or progressively (navigations). On a `ppr` shell HIT a loader
|
|
272
|
+
without `ssr: false` never ran during the capture, so its pushes are never in
|
|
273
|
+
the shell and always apply client-side. See `/loader` for the full contract.
|
|
273
274
|
|
|
274
275
|
## `ssr: false` — the SSR-completeness opt-in
|
|
275
276
|
|
|
@@ -309,10 +310,14 @@ Constraints:
|
|
|
309
310
|
- Under a `ppr` group route the flag is the bake lane, exactly as on a server
|
|
310
311
|
route (`/ppr` → The loader lane rule).
|
|
311
312
|
|
|
312
|
-
Every document
|
|
313
|
-
that is the point, but it is a real cost.
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
Every document render that is not a `ppr` shell HIT pays the flagged
|
|
314
|
+
loader's latency before first byte — that is the point, but it is a real cost.
|
|
315
|
+
On a shell HIT the shell flushes first: a flagged loader whose value has no
|
|
316
|
+
promises is served from the shell and does not run (unless the capture marked
|
|
317
|
+
it to run: see `/ppr` → On a shell HIT), and one whose value
|
|
318
|
+
carries promises runs on every HIT to fill its holes. Keep flagged loaders
|
|
319
|
+
fast (existence checks before expensive fetches) and flag the loaders that
|
|
320
|
+
need it, not the route.
|
|
316
321
|
|
|
317
322
|
Pinned end to end by dev + production e2e in the router repository.
|
|
318
323
|
|
|
@@ -168,6 +168,21 @@ family runs L1-only (each colo captures and serves its own shell). A tagged
|
|
|
168
168
|
shell with neither `kv` nor `tagPurge` logs a warning once and expires only by
|
|
169
169
|
ttl+swr. See `/caching` and `/ppr`.
|
|
170
170
|
|
|
171
|
+
Shell reads are stale-while-revalidate through per-isolate memos: a warm
|
|
172
|
+
isolate serves a shell it read within `memo.shellMs` (default 2000) from
|
|
173
|
+
memory, and with `kv` it checks tag markers through a memo that is used as is
|
|
174
|
+
for `memo.markerFreshMs` (default 1000) and while it refreshes in the
|
|
175
|
+
background up to `memo.markerMaxStaleMs` (default 10000). After
|
|
176
|
+
`updateTag()`/`revalidateTag()` the response sets the fresh-reads cookie
|
|
177
|
+
(`rango-state-fresh` by default), so the mutating user's next requests skip
|
|
178
|
+
the memos. That makes them fresh in the colo that ran the mutation; other
|
|
179
|
+
colos converge once KV propagates the marker and their cached marker
|
|
180
|
+
(`tagCacheTtl`) expires. In purge mode without `kv`, another isolate's memo
|
|
181
|
+
keeps serving a purged shell until `memo.shellMs` passes. Set
|
|
182
|
+
`memo: { shellMs: 0, markerFreshMs: 0 }` to remove the memo delay for every
|
|
183
|
+
user's next request; cross-colo KV propagation (and `tagCacheTtl`) still
|
|
184
|
+
applies. Details: `/caching` → "The fresh-reads cookie".
|
|
185
|
+
|
|
171
186
|
## Tracing on Workers
|
|
172
187
|
|
|
173
188
|
`createCloudflareTracing()` from `@rangojs/router/cloudflare` emits the
|