@rangojs/router 0.17.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 +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- 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 +98 -14
- 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-snapshot.d.ts +4 -3
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- 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/types.d.ts +207 -43
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
- package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
- package/dist/types/rsc/shell-capture.d.ts +146 -15
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/types.d.ts +7 -0
- package/dist/types/server/context.d.ts +109 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/request-context.d.ts +159 -33
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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/types/handler-context.d.ts +14 -1
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +46 -9
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +123 -51
- package/skills/caching/SKILL.md +253 -23
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +11 -6
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +44 -32
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +8 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/state.md +3 -1
- package/skills/loader/SKILL.md +161 -39
- package/skills/middleware/SKILL.md +16 -9
- package/skills/migrate-nextjs/SKILL.md +67 -36
- 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 +64 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +655 -363
- package/skills/prerender/SKILL.md +27 -14
- package/skills/rango/SKILL.md +52 -33
- package/skills/route/SKILL.md +12 -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 +25 -21
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/loader.md +18 -1
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/theme/SKILL.md +52 -4
- package/skills/typesafety/env-and-bindings.md +10 -2
- package/skills/use-cache/SKILL.md +39 -16
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/rsc-router.tsx +5 -0
- package/src/cache/cache-key-utils.ts +22 -2
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +34 -7
- package/src/cache/cache-scope.ts +345 -33
- 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/handle-snapshot.ts +4 -3
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +104 -19
- package/src/cache/types.ts +211 -43
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/prerender/build-shell-capture.ts +65 -11
- package/src/route-definition/dsl-helpers.ts +4 -3
- package/src/router/handler-context.ts +38 -4
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +19 -7
- package/src/router/loader-resolution.ts +63 -32
- package/src/router/match-api.ts +28 -9
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/cache-lookup.ts +17 -0
- package/src/router/match-middleware/cache-store.ts +31 -11
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +23 -4
- package/src/router/prerender-match.ts +6 -0
- package/src/router/segment-resolution/fresh.ts +18 -9
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +227 -56
- package/src/router/segment-resolution/loader-snapshot.ts +203 -51
- package/src/router/segment-resolution/mask-nested.ts +136 -42
- package/src/router/segment-resolution/revalidation.ts +27 -10
- 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/response-cache-serve.ts +9 -6
- package/src/rsc/rsc-rendering.ts +601 -181
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +60 -6
- package/src/rsc/shell-capture.ts +1001 -250
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/types.ts +7 -0
- package/src/server/context.ts +219 -28
- package/src/server/cookie-store.ts +87 -62
- package/src/server/request-context.ts +263 -38
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/run-loader.ts +63 -5
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/types/handler-context.ts +14 -1
- package/src/urls/pattern-types.ts +22 -18
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/router-discovery.ts +14 -4
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,11 @@ 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 variable belongs in a loader
|
|
93
|
-
(
|
|
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;
|
|
94
98
|
awaiting a loader with `ctx.use()` and rendering the result in a cached handler
|
|
95
99
|
silently bakes per-request data into the shared entry (see "Cache purity &
|
|
96
100
|
tainted objects" below).
|
|
@@ -163,7 +167,12 @@ cache(
|
|
|
163
167
|
— document requests, client navigations, and intercept navigations are cached
|
|
164
168
|
separately. The search part honors `cache.searchParams` (below).
|
|
165
169
|
- **`key`** is a full override: it bypasses the default key, the store's
|
|
166
|
-
`keyGenerator`, and the search-param filter.
|
|
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").
|
|
167
176
|
- **`condition`** returning `false` skips both the cache read and write for that
|
|
168
177
|
request (the boundary renders live).
|
|
169
178
|
- `cache(() => [...])` with no options uses the store defaults.
|
|
@@ -221,15 +230,25 @@ function CampaignBanner() {
|
|
|
221
230
|
|
|
222
231
|
Each tag attaches to one entry, so know which entry you are tagging:
|
|
223
232
|
|
|
224
|
-
- Forms 1 and 2 tag the `cache()` segment entry.
|
|
225
|
-
it: tags recorded by a `"use cache"` function (form 3) stay on that
|
|
226
|
-
function's entry and do not attach to an enclosing `cache()` segment entry.
|
|
233
|
+
- Forms 1 and 2 tag the `cache()` segment entry.
|
|
227
234
|
- Form 3 tags the `"use cache"` entry (plus any profile `tags`).
|
|
228
235
|
- Form 4 tags the request's `ppr` shell entry (`/ppr`) or `/document-cache`
|
|
229
|
-
entry, with no `"use cache"` needed.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
233
252
|
|
|
234
253
|
Invalidate with one of two server-only verbs (both variadic, imported from
|
|
235
254
|
`@rangojs/router`):
|
|
@@ -258,6 +277,25 @@ function onProductsChanged() {
|
|
|
258
277
|
| `updateTag(...tags)` | awaitable (`Promise<void>`) | server actions | immediate; next read is fresh |
|
|
259
278
|
| `revalidateTag(...tags)` | background (`void`) | route handlers / webhooks | background (non-blocking); next read re-renders fresh |
|
|
260
279
|
|
|
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
|
+
|
|
261
299
|
Both must run inside a request (action, handler, middleware, response route).
|
|
262
300
|
Called from a queue consumer or cron job, there is no request context, so they
|
|
263
301
|
warn and invalidate nothing. `updateTag()` rejects when a store's durable write
|
|
@@ -383,8 +421,17 @@ defaults (ttl 60 when the store sets none); it does **not** inherit the options
|
|
|
383
421
|
of an enclosing `cache()` boundary. Inside `loader()` only the direct form
|
|
384
422
|
`cache({...})` is valid — the wrapper form `cache(opts, () => [...])` throws.
|
|
385
423
|
Handle pushes from the loader body (`Meta`, `Breadcrumbs`) are stored with the
|
|
386
|
-
value and replayed on every hit, stale included.
|
|
387
|
-
|
|
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").
|
|
388
435
|
|
|
389
436
|
## Global Cache Configuration
|
|
390
437
|
|
|
@@ -537,8 +584,9 @@ const router = createRouter<AppBindings>({
|
|
|
537
584
|
| Cold colo | MISS | HIT | Serve from KV, promote to L1 |
|
|
538
585
|
| First render | MISS | MISS | Render, write to both L1 + KV |
|
|
539
586
|
|
|
540
|
-
KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total
|
|
541
|
-
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.
|
|
542
590
|
|
|
543
591
|
### Resilience & latency budgets
|
|
544
592
|
|
|
@@ -558,6 +606,74 @@ read. A non-finite value (e.g. `Number(env.UNSET)`) falls back to the default.
|
|
|
558
606
|
The tag-invalidation marker reads inherit these same budgets and **fail open** on
|
|
559
607
|
a KV timeout — the entry is served rather than wrongly treated as invalidated.
|
|
560
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
|
+
|
|
561
677
|
```typescript
|
|
562
678
|
new CFCacheStore({
|
|
563
679
|
ctx,
|
|
@@ -623,13 +739,17 @@ the store adds SWR, tag invalidation, and the family split on top of it.
|
|
|
623
739
|
import { getCache, waitUntil } from "@vercel/functions";
|
|
624
740
|
import { VercelCacheStore } from "@rangojs/router/cache";
|
|
625
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
|
+
|
|
626
747
|
const router = createRouter({
|
|
627
748
|
document: Document,
|
|
628
749
|
urls: urlpatterns,
|
|
629
750
|
cache: () => ({
|
|
630
751
|
store: new VercelCacheStore({
|
|
631
|
-
|
|
632
|
-
cache: getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID }),
|
|
752
|
+
cache: runtimeCache,
|
|
633
753
|
waitUntil,
|
|
634
754
|
defaults: { ttl: 60, swr: 300 },
|
|
635
755
|
}),
|
|
@@ -637,6 +757,22 @@ const router = createRouter({
|
|
|
637
757
|
});
|
|
638
758
|
```
|
|
639
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
|
+
|
|
640
776
|
Writes over 2 MB (the platform limit, `maxItemBytes`) are skipped, and tags
|
|
641
777
|
beyond the per-item cap are dropped with a warning. The official client swallows
|
|
642
778
|
`expireTag` failures, so treat `updateTag()` on Vercel as best-effort. See
|
|
@@ -654,6 +790,7 @@ another, request-scoped APIs are guarded inside a cache scope:
|
|
|
654
790
|
| `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
|
|
655
791
|
| Response writes: `ctx.headers.set()`, `setCookie()`, `setStatus()`, `onResponse()` | **throws** — response side effects lost on a hit |
|
|
656
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 |
|
|
657
794
|
| `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
|
|
658
795
|
| Any of the above **inside a registered loader** (`loader(...)`) | **allowed** — loaders always run fresh |
|
|
659
796
|
|
|
@@ -661,6 +798,12 @@ A loader body invoked from a handler with `await ctx.use(Loader)` may read
|
|
|
661
798
|
`cookies()`/`headers()`, but its response writes still throw: on a hit the
|
|
662
799
|
handler is skipped, so that loader never runs.
|
|
663
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").
|
|
806
|
+
|
|
664
807
|
**Tainted objects.** Request-scoped objects (`ctx`, `env`, `request`) carry an
|
|
665
808
|
internal taint symbol so they are excluded from `"use cache"` cache keys. The
|
|
666
809
|
`cache()` scope is tracked in async-local state and deliberately ends inside
|
|
@@ -680,10 +823,12 @@ is **not** guarded. `ctx.use()` is a server-side escape hatch for non-rendered
|
|
|
680
823
|
uses (set a ctx var, make a routing decision); never render its result inside a
|
|
681
824
|
cached handler.
|
|
682
825
|
|
|
683
|
-
This is the **consumption-lane rule**, and it holds
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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.
|
|
687
832
|
|
|
688
833
|
```typescript
|
|
689
834
|
// WRONG — throws: cookies() read directly in a cached handler
|
|
@@ -729,8 +874,9 @@ See `/cache-guide` for the full decision guide and the `cache()` vs `"use cache"
|
|
|
729
874
|
|
|
730
875
|
## Nested Cache Boundaries
|
|
731
876
|
|
|
732
|
-
An inner boundary overrides the settings of the outer one for its subtree
|
|
733
|
-
`cache(false)` turns caching off for a
|
|
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:
|
|
734
880
|
|
|
735
881
|
```typescript
|
|
736
882
|
cache({ ttl: 300 }, () => [
|
|
@@ -746,6 +892,80 @@ cache({ ttl: 300 }, () => [
|
|
|
746
892
|
]);
|
|
747
893
|
```
|
|
748
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
|
+
]),
|
|
927
|
+
]);
|
|
928
|
+
```
|
|
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
|
+
|
|
749
969
|
## Custom Cache Store
|
|
750
970
|
|
|
751
971
|
Create a dedicated store for specific routes:
|
|
@@ -765,6 +985,16 @@ A per-boundary store becomes reachable by `updateTag()`/`revalidateTag()` only
|
|
|
765
985
|
once that boundary has been matched in the current process. For data you
|
|
766
986
|
invalidate by tag, prefer the app-level store.
|
|
767
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
|
+
|
|
768
998
|
## Complete Example
|
|
769
999
|
|
|
770
1000
|
```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
|
},
|
|
@@ -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
|
|
@@ -373,15 +373,17 @@ old-deployment request pinning.
|
|
|
373
373
|
The `@rangojs/router/testing/*` entry points give you `runLoader`,
|
|
374
374
|
`runMiddleware`, `runInRequestContext` (server actions), `dispatch` (base
|
|
375
375
|
`./testing`), `renderHandler` and real Flight rendering (`renderServerTree`,
|
|
376
|
-
`findClientBoundaries`, `findElements`)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
and
|
|
376
|
+
`findClientBoundaries`, `findElements`) plus a real PPR shell capture and HIT
|
|
377
|
+
(`serveShellRequest`, `resetShellTestState`) from `./testing/flight`,
|
|
378
|
+
`renderRoute` from `./testing/dom`, a vitest preset from `./testing/vitest`, and
|
|
379
|
+
a Playwright e2e harness with dev/prod parity helpers (`parityDescribe`,
|
|
380
|
+
`expectParity`) from `./testing/e2e`. You can unit-test a loader, a middleware,
|
|
381
|
+
an action, or an RSC handler in isolation. Next.js and Waku ship no official
|
|
382
|
+
primitives for testing server components/handlers; TanStack documents testing
|
|
383
|
+
patterns (build your own harness from `createRouter`/`createMemoryHistory`) but
|
|
384
|
+
ships no testing package, and nothing at the RSC-handler level. See the
|
|
385
|
+
[testing skill](../../testing/SKILL.md) and
|
|
386
|
+
[testing.md](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/testing.md).
|
|
385
387
|
|
|
386
388
|
### Bundle discipline
|
|
387
389
|
|
|
@@ -534,10 +536,13 @@ There are two distinct things named "revalidate", and the split is deliberate:
|
|
|
534
536
|
tabs, and pagination. See
|
|
535
537
|
[shallow-navigation.md](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/design/shallow-navigation.md).
|
|
536
538
|
|
|
537
|
-
Both are separate from `revalidateTag()`/`updateTag()`, which
|
|
538
|
-
cache entries
|
|
539
|
-
`
|
|
540
|
-
|
|
539
|
+
Both are separate from `revalidateTag()`/`updateTag()`, which evict tagged
|
|
540
|
+
cache entries; the request that calls either reads its own writes.
|
|
541
|
+
`updateTag()` also awaits the durable write; `revalidateTag()` schedules it in
|
|
542
|
+
the background. The mutating user's next requests skip the stores' per-isolate
|
|
543
|
+
PPR shell memos via a fresh-reads cookie, and other users converge within the
|
|
544
|
+
memo window and the platform's own propagation. Neither API selects a client
|
|
545
|
+
segment to render. Next.js offers `router.refresh()` (and
|
|
541
546
|
`refresh()` from `next/cache`) plus path/tag cache invalidation, but no per-segment
|
|
542
547
|
render predicate and no typed action discrimination — and its invalidations clear
|
|
543
548
|
the whole client cache rather than selecting a segment. TanStack's `shouldReload`/`router.invalidate()` is the nearest
|
|
@@ -620,18 +625,22 @@ this resource-aware policy layer.
|
|
|
620
625
|
|
|
621
626
|
`createLoader(fn)` / `loader()` define **live-by-default** data units: they are
|
|
622
627
|
excluded from an enclosing segment cache and resolve on every request unless the
|
|
623
|
-
loader itself explicitly opts into `cache()`.
|
|
624
|
-
`headers()`, request context, and `env` because
|
|
625
|
-
cached shell
|
|
626
|
-
|
|
627
|
-
route
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
628
|
+
loader itself explicitly opts into `cache()`. A loader without `ssr: false` may
|
|
629
|
+
safely read `cookies()`, `headers()`, request context, and `env` because its
|
|
630
|
+
execution is outside the cached shell; a loader with its own `cache()` must put
|
|
631
|
+
those reads in its `key()`, or its miss fails and stores nothing. On a `ppr`
|
|
632
|
+
route an `ssr: false` loader bakes into the shell, so an identity read there
|
|
633
|
+
refuses the capture. Loaders run in parallel, stream independently under
|
|
634
|
+
`loading()` boundaries, compose server-side via `ctx.use(OtherLoader)`, and
|
|
635
|
+
carry route-level authority: they can throw `notFound()`/`redirect()`, WRITE
|
|
636
|
+
handles (`ctx.use(Meta)({ title })` — data-derived page metadata pushed from the
|
|
637
|
+
data's producer), read handle data after the render settles (`ctx.get(handle)`
|
|
638
|
+
behind `await ctx.rendered()`), and opt into `loader(Def, { ssr: false })` so a
|
|
639
|
+
document render awaits them before first flush (deterministic SSR'd data, meta,
|
|
640
|
+
and 404 status). Reads happen through `useLoader` in a client component
|
|
641
|
+
(including its SSR pass) or `useFetchLoader` for standalone client fetches.
|
|
642
|
+
"Fetchable" loaders are callable endpoints with their own middleware and
|
|
643
|
+
GET/POST/PUT/PATCH/DELETE bodies.
|
|
635
644
|
|
|
636
645
|
One sharp edge is worth stating because the distinction matters: a cached handler
|
|
637
646
|
can call `await ctx.use(Loader)`, but if it renders that result inline, it bakes the
|
|
@@ -681,7 +690,7 @@ a fully prerendered route. Caching loader data is a separate, explicit opt-in on
|
|
|
681
690
|
that loader.
|
|
682
691
|
|
|
683
692
|
The mechanism is concrete: a prerendered route serves its baked Flight shell from
|
|
684
|
-
the store, then
|
|
693
|
+
the store, then the router resolves the loaders through their own
|
|
685
694
|
freshness policy at request time and merges their segments into that replayed
|
|
686
695
|
shell. With the default policy you get a cached/prerendered shell plus live data,
|
|
687
696
|
automatically, with no per-route static-vs-dynamic decision. The loader is the
|
|
@@ -692,7 +701,8 @@ designated live data slot in a cache-first RSC tree.** Several properties follow
|
|
|
692
701
|
that no loader-before-render has:
|
|
693
702
|
|
|
694
703
|
- it is the **cache-safety escape hatch** — the only place request-coupled reads
|
|
695
|
-
are allowed inside a cache scope, because it is the part guaranteed to re-run
|
|
704
|
+
are allowed inside a cache scope, because it is the part guaranteed to re-run
|
|
705
|
+
(a loader without `ssr: false` and without its own unkeyed `cache()`);
|
|
696
706
|
- it is **standalone and composable**, not route-coupled — one `createLoader()`
|
|
697
707
|
read by many segments, composed via `ctx.use`, or exposed as a fetchable endpoint;
|
|
698
708
|
- it **streams as a hole, not a gate** — concurrent, Suspense-resolved under
|
|
@@ -773,11 +783,13 @@ safe rule remains simple: read request-specific context at the live point of use
|
|
|
773
783
|
normally a loader, rather than deriving it outside and carrying it into a cached
|
|
774
784
|
shell.
|
|
775
785
|
|
|
776
|
-
This is where the taint and loader designs meet. Loaders are the sanctioned
|
|
777
|
-
holes: they run outside the enclosing segment cache and may read the
|
|
778
|
-
request,
|
|
779
|
-
|
|
780
|
-
|
|
786
|
+
This is where the taint and loader designs meet. Loaders are the sanctioned
|
|
787
|
+
dynamic holes: they run outside the enclosing segment cache and may read the
|
|
788
|
+
current request (except, on a `ppr` route, an `ssr: false` loader or a loader a
|
|
789
|
+
handler awaits, which the capture runs and refuses on such a read), while the
|
|
790
|
+
shared shell remains protected. Next.js also isolates runtime APIs from ordinary
|
|
791
|
+
`"use cache"`, and React's optional taint APIs protect values at the
|
|
792
|
+
server-to-client serialization boundary. Those are useful but different
|
|
781
793
|
contracts; they do not provide Rango's route-aware tainted-argument keying,
|
|
782
794
|
non-cacheable typed context variables, handle capture/replay, and loader escape
|
|
783
795
|
path as one system. TanStack Start and Waku leave this boundary primarily to
|