@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
|
@@ -15,7 +15,7 @@ storefront homepage (shell HIT, production build, M-series laptop): a single
|
|
|
15
15
|
was dead for ~11s. Fast paint, frozen page.
|
|
16
16
|
|
|
17
17
|
The obvious fix — wrap the heavy subtree in `<Suspense>` so it hydrates later —
|
|
18
|
-
|
|
18
|
+
can trade the paint away: when that boundary postpones under shell capture, the
|
|
19
19
|
frozen prelude ships an empty `<main>`. This recipe gets both: the full body
|
|
20
20
|
HTML in the prelude AND its hydration off the initial task, released on first
|
|
21
21
|
idle at retry-lane priority.
|
|
@@ -28,7 +28,9 @@ idle at retry-lane priority.
|
|
|
28
28
|
| interactive (menu click works) | ~3.5s | ~1.6s | ~1.6s |
|
|
29
29
|
|
|
30
30
|
Measure your own page before and after — the win depends on how much of the
|
|
31
|
-
hydration cost lives under the boundary (see "Verifying and measuring").
|
|
31
|
+
hydration cost lives under the boundary (see "Verifying and measuring"). These
|
|
32
|
+
numbers were measured before PPR captures baked handler output; see the next
|
|
33
|
+
section for what that changed.
|
|
32
34
|
|
|
33
35
|
## The recipe
|
|
34
36
|
|
|
@@ -102,23 +104,39 @@ inside hydrates after first idle.
|
|
|
102
104
|
|
|
103
105
|
## Why `fallback={children}` is load-bearing
|
|
104
106
|
|
|
105
|
-
This is not a style choice
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
This is not a style choice. The recipe was built when the shell capture
|
|
108
|
+
postponed any big `<Suspense>` subtree: the capture froze on Flight byte-quiet
|
|
109
|
+
(`FLIGHT_QUIET_HOPS` in `src/rsc/shell-capture.ts`) while the handler output
|
|
110
|
+
was still rendering, so a boundary over the page body became a hole no matter
|
|
111
|
+
where it sat. Verified both ways at the time: wrapping the client island from
|
|
112
|
+
outside AND placing the boundary inside the island both baked
|
|
113
|
+
`<!--$?--><template id="B:…">` into `<main>`, i.e. an empty body in the frozen
|
|
114
|
+
prelude. With the content as the fallback, the postpone became the delivery
|
|
115
|
+
mechanism: the shell baked the fallback, and the fallback was the body.
|
|
116
|
+
|
|
117
|
+
PPR captures now wait for the whole handler layer before freezing anything:
|
|
118
|
+
the capture records the page's segments first (async server components
|
|
119
|
+
rendered, every promise in the handler output settled) and renders its Flight
|
|
120
|
+
payload from that record (`settleCaptureRecord` in `src/rsc/shell-capture.ts`).
|
|
121
|
+
A boundary whose content is only handler output is therefore expected to bake
|
|
122
|
+
its content as a completed boundary instead of postponing. What still
|
|
123
|
+
postpones at capture is a boundary over live data: a loader without
|
|
124
|
+
`ssr: false` read with `useLoader` under `loading()` or an inline
|
|
125
|
+
`<Suspense>`, or a promise nested in an `ssr: false` loader's value. Run the
|
|
126
|
+
prelude check in "Verifying and measuring" on your page rather than assuming
|
|
127
|
+
either shape.
|
|
128
|
+
|
|
129
|
+
`fallback={children}` stays the safe choice either way:
|
|
130
|
+
|
|
131
|
+
- If the boundary still postpones (live data inside it), the fallback is what
|
|
132
|
+
the prelude shows, so it must be the content.
|
|
133
|
+
- The sync-update trap below: a client render of the boundary shows the
|
|
134
|
+
fallback. With `fallback={children}` that is a visual no-op; with
|
|
135
|
+
`fallback={null}` it blanks the page.
|
|
136
|
+
|
|
137
|
+
The hydration deferral does not depend on which shape you get: `HydrationGate`
|
|
138
|
+
suspends during hydration, and React keeps the server DOM of the boundary in
|
|
139
|
+
place until the gate releases.
|
|
122
140
|
|
|
123
141
|
## Why the client gate is free
|
|
124
142
|
|
|
@@ -164,13 +182,14 @@ release on interaction instead (see Variations).
|
|
|
164
182
|
|
|
165
183
|
## Known cost: the body rides twice (measure it, don't guess)
|
|
166
184
|
|
|
167
|
-
|
|
168
|
-
baked fallback in the prelude, once as the
|
|
169
|
-
re-renders and re-ships it; there is no
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
When the boundary postpones at capture, a shell HIT carries the gated
|
|
186
|
+
subtree's HTML twice — once as the baked fallback in the prelude, once as the
|
|
187
|
+
resumed hole content (the resume re-renders and re-ships it; there is no
|
|
188
|
+
bake-through). Homepage measurement (postponing boundary): 234KB → 302KB
|
|
189
|
+
gzipped (+68KB, +29%; raw +1.05MB). It is post-paint bandwidth, not
|
|
190
|
+
render-blocking — the visible prelude streams first — but it is real bytes on
|
|
191
|
+
every document GET. Weigh it per page; on a small body the recipe may not pay
|
|
192
|
+
for itself. A boundary that bakes its content ships it once.
|
|
174
193
|
|
|
175
194
|
## Verifying and measuring
|
|
176
195
|
|
|
@@ -230,7 +249,8 @@ this exact space. The recipe survives that future; a primitive might not.
|
|
|
230
249
|
|
|
231
250
|
## Related
|
|
232
251
|
|
|
233
|
-
- `/ppr` — the shell/hole mechanics this recipe rides on
|
|
234
|
-
|
|
235
|
-
- `src/rsc/shell-capture.ts` — the
|
|
236
|
-
(`FLIGHT_QUIET_HOPS`) that
|
|
252
|
+
- `/ppr` — the shell/hole mechanics this recipe rides on: what bakes into the
|
|
253
|
+
shell, and which boundaries stay holes
|
|
254
|
+
- `src/rsc/shell-capture.ts` — the record-first capture (`settleCaptureRecord`)
|
|
255
|
+
and the byte-quiet window (`FLIGHT_QUIET_HOPS`) that decide what the prelude
|
|
256
|
+
holds
|
|
@@ -56,8 +56,11 @@ There are two shell producers:
|
|
|
56
56
|
Both producers feed the same in-function serve path. Build-time shells are
|
|
57
57
|
content-hashed modules in the server bundle, not CDN-served HTML files.
|
|
58
58
|
|
|
59
|
-
`ppr.ttl
|
|
60
|
-
|
|
59
|
+
`ppr.ttl` and `ppr.swr` set the shell entry's lifetime, capped by the route
|
|
60
|
+
`cache()` entry it was captured from (the shorter wins). Its tags are
|
|
61
|
+
`ppr.tags` plus the tags its content recorded. A route `cache({ key })` or a
|
|
62
|
+
store `keyGenerator` stores one shell per partition value. None of this emits
|
|
63
|
+
HTTP `Cache-Control` or configures a platform CDN.
|
|
61
64
|
|
|
62
65
|
## Platform deployment shapes
|
|
63
66
|
|
|
@@ -205,7 +205,10 @@ the variant suffixes are still appended.
|
|
|
205
205
|
- Responses whose `Cache-Control` also contains `private`, `no-store`, or an
|
|
206
206
|
unqualified `no-cache` (those win over `s-maxage` in a shared cache)
|
|
207
207
|
- Responses carrying `Set-Cookie` (a per-client value must not be replayed to
|
|
208
|
-
everyone)
|
|
208
|
+
everyone). That includes a response whose request called
|
|
209
|
+
`updateTag()`/`revalidateTag()` on a store that keeps per-isolate memos
|
|
210
|
+
(`CFCacheStore`, `VercelCacheStore`): it carries the fresh-reads cookie
|
|
211
|
+
(`rango-state-fresh` by default), so it is not stored.
|
|
209
212
|
- Non-200 responses
|
|
210
213
|
- A 200 whose render reported an error: a component that threw after the
|
|
211
214
|
response started streaming, such as an async server component whose fetch
|
|
@@ -224,7 +227,10 @@ with the request's tag set, so `updateTag()`/`revalidateTag()` evict them:
|
|
|
224
227
|
|
|
225
228
|
- a render-callable `cacheTag("...")` in any server component on the page;
|
|
226
229
|
- tags of `cache()` segments, cached loaders, and `"use cache"` functions the
|
|
227
|
-
render read.
|
|
230
|
+
render read. A `cache()` segment HIT contributes the tags its content
|
|
231
|
+
recorded when it was written (render-callable calls and `"use cache"` reads
|
|
232
|
+
inside it), so a document stored over the HIT stays evictable by them. A
|
|
233
|
+
cached loader's HIT likewise contributes the tags its body recorded.
|
|
228
234
|
|
|
229
235
|
Tags are snapshotted after the response body has fully streamed, so tags from
|
|
230
236
|
Suspense-streamed content are included. A `Prerender()` route's build-time
|
|
@@ -184,6 +184,8 @@ parallel({
|
|
|
184
184
|
});
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
On a `ppr` route, a slot's `loading()` over handler output does not make a hole. A slot handler that awaits `ctx.use(CartLoader)` is handler output: it runs at capture, its value is baked into the shared shell, and every HIT replays it without running the handler. A `cookies()`/`headers()` read in that loader refuses the capture, so the route stays on MISS. For a per-request slot, read the loader with `useLoader()` in a `"use client"` component under the slot's `loading()` (see `/ppr` → "The layout-with-loaders playbook").
|
|
188
|
+
|
|
187
189
|
### Two scopes for explicit `use` at the mount site: shared (broadcast) and slot-local
|
|
188
190
|
|
|
189
191
|
`parallel()` accepts an explicit `use()` callback that **broadcasts** to every slot in the call ([dsl-helpers.ts](../../src/route-definition/dsl-helpers.ts)). That's the right behavior for the items the parallel allow-list permits and that accumulate (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`) — every slot gets them. (Note: `middleware` is not allowed inside `parallel()`; see the allowed-types table above.)
|
|
@@ -34,7 +34,9 @@ Writers: handlers AND loader bodies — `ctx.use(Handle)` returns the push
|
|
|
34
34
|
function in each (middleware has no handle APIs). Loader pushes follow a delivery race (settle before
|
|
35
35
|
the handler barrier → in the SSR'd document; later → applied client-side), and
|
|
36
36
|
`loader(Def, { ssr: false })` makes them deterministic in the
|
|
37
|
-
document.
|
|
37
|
+
document. On a `ppr` shell HIT, a loader without `ssr: false` never ran during
|
|
38
|
+
the capture, so its pushes always apply client-side; with `ssr: false` they
|
|
39
|
+
bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
38
40
|
re-renders on every handle update, late loader pushes included.
|
|
39
41
|
|
|
40
42
|
Handles can be passed as props from server to client components:
|
package/skills/hooks/state.md
CHANGED
|
@@ -214,7 +214,9 @@ it is selected by export conditions:
|
|
|
214
214
|
a rotated state `Set-Cookie` for the responding client, which marks its
|
|
215
215
|
caches stale when it next reads them. Idempotent within a request; throws
|
|
216
216
|
inside a `cache()` / `"use cache"` boundary; a no-op with a dev warning
|
|
217
|
-
outside a request.
|
|
217
|
+
outside a request. On a `ppr` route a shell HIT runs no handler, so a call
|
|
218
|
+
from handler code writes the cookie only on the renders that run it (a
|
|
219
|
+
MISS); call it from middleware, which runs on every request, HITs included.
|
|
218
220
|
|
|
219
221
|
Server actions already invalidate automatically; to suppress that for a no-op
|
|
220
222
|
action, call `keepClientCache()` inside it (see `/server-actions` → "Client
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -7,7 +7,8 @@ argument-hint: "[loader]"
|
|
|
7
7
|
# Data Loaders with loader()
|
|
8
8
|
|
|
9
9
|
A loader is a server function created with `createLoader()` and registered on a
|
|
10
|
-
route segment with `loader()`. It runs fresh on every request
|
|
10
|
+
route segment with `loader()`. It runs fresh on every request (on a `ppr` route,
|
|
11
|
+
one registered with `ssr: false` is baked into the shell instead), streams its
|
|
11
12
|
result to the client, and client components read it with `useLoader()`. This
|
|
12
13
|
skill covers defining, registering, consuming, revalidating, caching, and
|
|
13
14
|
client-fetching loaders.
|
|
@@ -121,25 +122,37 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
121
122
|
> (plain GET only). See the hooks skill ("Scoping refetch with a `key`" and
|
|
122
123
|
> "Refreshing multiple loaders together").
|
|
123
124
|
> - `cache({ key })` — a **server** cache identity (storage hit/miss/ttl/swr).
|
|
125
|
+
> On a route `cache()` over a `ppr` route, each key value also gets its own
|
|
126
|
+
> shell; keep the value set small.
|
|
124
127
|
> - `revalidate()` — which **server** segments/loaders recompute during
|
|
125
128
|
> navigation and action refreshes.
|
|
126
129
|
|
|
127
130
|
DSL loaders are the **live data layer**: they resolve fresh on every request,
|
|
128
131
|
even when the route is inside a `cache()` boundary, so `cache()` gives you
|
|
129
132
|
cached UI + fresh data by default. See "Loaders: The Live Data Layer" below.
|
|
133
|
+
The exception is a `ppr` route's `ssr: false` loader: it bakes into the shell,
|
|
134
|
+
and a shell HIT runs it only when its return carries promises.
|
|
130
135
|
|
|
131
136
|
### Cache safety
|
|
132
137
|
|
|
133
|
-
DSL
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
A DSL loader without `ssr: false` can read `createVar({ cache: false })`
|
|
139
|
+
variables: it runs on every request, `cache()` and PPR shell HITs included, so
|
|
140
|
+
the read guard is bypassed for it. On a `ppr` route the capture runs an
|
|
141
|
+
`ssr: false` loader, and a loader a handler awaits, and bakes their values into
|
|
142
|
+
the shell: a `cookies()`, `headers()` or `{ cache: false }` read there refuses
|
|
143
|
+
the capture, so the route keeps rendering without a shell (warned once per key).
|
|
144
|
+
A loader bound with its own `cache()` is the other exception: its value is
|
|
145
|
+
stored and shared, so a miss whose body read `cookies()`, `headers()`, a
|
|
146
|
+
non-cacheable variable or the theme fails unless the binding has a `key()` or a
|
|
147
|
+
store `keyGenerator`, which must then include them (see "Cache Key"). A
|
|
148
|
+
`"use cache"` function the loader calls is not a loader body: a non-cacheable
|
|
149
|
+
read inside it throws, also through the loader's `ctx` passed in, so read the
|
|
150
|
+
var in the loader and pass the value in as an argument. Passing the loader's
|
|
151
|
+
`ctx` itself is fine: the call keys by route, params and search, like a handler
|
|
152
|
+
`ctx` (`/use-cache`). A fetchable loader called with a request body
|
|
153
|
+
(`load({ method: "POST", body })` or form data) is the exception: the body is
|
|
154
|
+
not in the key, so the call runs uncached. Pass the body fields the function
|
|
155
|
+
needs as arguments instead.
|
|
143
156
|
|
|
144
157
|
### ctx.use(Loader) — escape hatch
|
|
145
158
|
|
|
@@ -157,7 +170,9 @@ path("/product/:slug", async (ctx) => {
|
|
|
157
170
|
])
|
|
158
171
|
```
|
|
159
172
|
|
|
160
|
-
`ctx.use(Loader)` is a
|
|
173
|
+
`ctx.use(Loader)` is a per-request read on any render that runs the handler,
|
|
174
|
+
memoized for the request (a route `cache()` HIT or a `ppr` shell HIT runs no
|
|
175
|
+
handler, so the handler's output from the read is replayed): however many
|
|
161
176
|
times it is called, and whether or not the loader is also registered with
|
|
162
177
|
`loader()`, it runs once per request. It never reads or writes the loader
|
|
163
178
|
store cache on its own: a loader's `cache()` belongs to its DSL binding
|
|
@@ -171,11 +186,15 @@ the tree).
|
|
|
171
186
|
- The handler output depends on the loader data. If the route is inside
|
|
172
187
|
`cache()`, the handler is cached with the loader result baked in —
|
|
173
188
|
defeating the live data guarantee.
|
|
174
|
-
- The same holds under a PPR shell capture (`/ppr`): handler consumption is
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
- The same holds under a PPR shell capture (`/ppr`): handler consumption is the
|
|
190
|
+
BAKED lane — the loader executes at capture and the rendered value is a
|
|
191
|
+
capture-time copy served on every HIT (a HIT never runs the handler);
|
|
192
|
+
`useLoader` of a loader without `ssr: false` is the live lane (an `ssr: false`
|
|
193
|
+
loader is the bake lane however it is read). Unlike `cache()`, an identity
|
|
194
|
+
read inside that loader (`cookies()`, `headers()`, a `{ cache: false }`
|
|
195
|
+
variable) refuses the capture, so the route stays uncached. One rule across
|
|
196
|
+
`cache()`, `"use cache"`, and PPR: the consumption-lane rule (`/rango` →
|
|
197
|
+
Invariants).
|
|
179
198
|
- Non-cacheable variable reads (`createVar({ cache: false })`) inside the
|
|
180
199
|
handler still throw, even if the data came from a loader.
|
|
181
200
|
- Prefer DSL `loader()` + client `useLoader()` for data that depends on
|
|
@@ -491,11 +510,16 @@ Loaders resolve fresh on every request, even when the route's UI segments are
|
|
|
491
510
|
served from cache. Route-level `cache()` caches rendered segments but never
|
|
492
511
|
loader data: loaders are excluded when the segments are stored and re-resolved
|
|
493
512
|
when they are served. Caching a loader's own data is a separate opt-in (see
|
|
494
|
-
"Opting a Loader into Caching").
|
|
513
|
+
"Opting a Loader into Caching"). A `ppr` shell does store some loader data: an
|
|
514
|
+
`ssr: false` loader's settled data bakes into the shell, and a HIT runs that
|
|
515
|
+
loader only when its return carries promises (see "`ssr: false`" below and
|
|
516
|
+
`/ppr`).
|
|
495
517
|
|
|
496
|
-
Pre-rendering follows the same rule:
|
|
497
|
-
(there is no real request context), and at runtime the worker resolves
|
|
498
|
-
fresh against the live database.
|
|
518
|
+
Pre-rendering follows the same rule: the build-time Flight payload skips
|
|
519
|
+
loaders (there is no real request context), and at runtime the worker resolves
|
|
520
|
+
them fresh against the live database. The exception is a `Prerender` + `ppr`
|
|
521
|
+
route: its build-time shell capture runs `ssr: false` loaders and bakes their
|
|
522
|
+
settled data into the shell, like a runtime capture.
|
|
499
523
|
|
|
500
524
|
### Parallel and streaming — latency overlaps first paint
|
|
501
525
|
|
|
@@ -516,8 +540,8 @@ If you come from a framework where the loader is a blocking step that runs
|
|
|
516
540
|
before the response is built, this is the shift to internalize: here the
|
|
517
541
|
response starts streaming first and loader data fills in. (The one deliberate
|
|
518
542
|
exception is per-loader: `loader(Def, { ssr: false })` awaits that
|
|
519
|
-
loader before first flush on document renders
|
|
520
|
-
below.)
|
|
543
|
+
loader before first flush on document renders other than a `ppr` shell HIT,
|
|
544
|
+
where the shell flushes first — see "`ssr: false`" below.)
|
|
521
545
|
|
|
522
546
|
### See it: `debugPerformance`
|
|
523
547
|
|
|
@@ -579,7 +603,11 @@ loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
|
579
603
|
|
|
580
604
|
This works wherever the DSL binding is declared, an intercept's `use()`
|
|
581
605
|
included. A handler's `ctx.use(Loader)` read never caches by itself (see
|
|
582
|
-
"ctx.use(Loader) — escape hatch").
|
|
606
|
+
"ctx.use(Loader) — escape hatch"). Once the binding started, every
|
|
607
|
+
`ctx.use(Loader)` in the request, from the handler or from another loader's
|
|
608
|
+
body, gets the binding's value: one value per loader per request, a HIT
|
|
609
|
+
included. A reader that runs before the binding starts (a layout's loader
|
|
610
|
+
reading a route's cached loader) gets the value of its own run, as before.
|
|
583
611
|
|
|
584
612
|
The loader's data is cached independently from the route's segment cache,
|
|
585
613
|
using the same `SegmentCacheStore` (app-level or per-loader override).
|
|
@@ -648,6 +676,56 @@ loader fails as if its body threw. There is no silent fallback to the default
|
|
|
648
676
|
key, because a personalised key collapsing onto the broad default would share
|
|
649
677
|
one user's data with everyone.
|
|
650
678
|
|
|
679
|
+
**Request-scoped reads in the body must be part of the key.** The default key
|
|
680
|
+
names no user, so one entry serves everyone who requests the same loader, host,
|
|
681
|
+
path and params. A route `cache()` around the route does not partition it
|
|
682
|
+
either: a loader's own `cache()` is an independent layer, keyed only by what it
|
|
683
|
+
declares. On a miss, a body that reads `cookies()`, `headers()` or a
|
|
684
|
+
non-cacheable variable (`createVar({ cache: false })`, or a value written with
|
|
685
|
+
`ctx.set(..., { cache: false })`) therefore **fails**, and nothing is stored,
|
|
686
|
+
unless the binding declares identity with a `key()` or a store `keyGenerator`.
|
|
687
|
+
The check tests only that one of them is there, not what it contains: any
|
|
688
|
+
`key()` or store `keyGenerator` switches it off, so it must itself include the
|
|
689
|
+
value. That includes a store-wide `keyGenerator` that only adds a region
|
|
690
|
+
prefix, like the one described above.
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
export const SessionLoader = createLoader(async () =>
|
|
694
|
+
getAccount(cookies().get("session")?.value),
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// The miss fails: cookies() cannot be called inside loader "…SessionLoader",
|
|
698
|
+
// whose own cache() has no key().
|
|
699
|
+
loader(SessionLoader, () => [cache({ ttl: 60 })]),
|
|
700
|
+
|
|
701
|
+
// One entry per session.
|
|
702
|
+
loader(SessionLoader, () => [
|
|
703
|
+
cache({
|
|
704
|
+
ttl: 60,
|
|
705
|
+
key: (ctx) => `account:${cookies().get("session")?.value ?? "anon"}`,
|
|
706
|
+
}),
|
|
707
|
+
]),
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
The same holds in a stale entry's background refresh (the refresh fails and the
|
|
711
|
+
stale entry keeps serving) and for the loaders the body reads with `ctx.use()`,
|
|
712
|
+
because their values land in the entry. That includes a keyed cached loader
|
|
713
|
+
served from its own cache: its entry keeps the read its miss made. It also holds
|
|
714
|
+
when something else ran the loader first, such as a parent layout's handler
|
|
715
|
+
calling `ctx.use(SessionLoader)`: the miss reuses that run and fails the same
|
|
716
|
+
way. A read that settles after the value, in a promise inside it or in a
|
|
717
|
+
handle push that settles within the 5 s handle-encode timeout, can't fail a
|
|
718
|
+
value that was already served: that user gets their own value, nothing is
|
|
719
|
+
stored, and `onError` gets the same error. A push still pending after the
|
|
720
|
+
timeout drops the entry's handles, and the value is stored without them. A loader with no `cache()` of its own
|
|
721
|
+
reads request data freely.
|
|
722
|
+
|
|
723
|
+
Two kinds of read are not seen, the same as in `"use cache"`: raw reads such
|
|
724
|
+
as `ctx.request.headers` or `getRequestContext().cookie()`, and a value
|
|
725
|
+
computed outside the loader and handed in, such as a per-request memo a handler
|
|
726
|
+
filled from `cookies()` before the loader awaited it. Key what those carry
|
|
727
|
+
too. If the value must stay per request, drop the loader's `cache()` instead.
|
|
728
|
+
|
|
651
729
|
### Tags for Invalidation
|
|
652
730
|
|
|
653
731
|
```typescript
|
|
@@ -665,6 +743,34 @@ loader(ProductLoader, () => [
|
|
|
665
743
|
]),
|
|
666
744
|
```
|
|
667
745
|
|
|
746
|
+
The tags the loader body records tag the entry too: `cacheTag()` calls, the
|
|
747
|
+
tags of `"use cache"` reads, render-time `cacheTag()` in server components of
|
|
748
|
+
the value or a handle push, and the tags the loaders it reads with `ctx.use`
|
|
749
|
+
record, including one the route, a layout or the handler started first. A
|
|
750
|
+
loader it reads that has its own `cache()` also brings its `cache({ tags })`
|
|
751
|
+
and, on a HIT, its entry's tags. `updateTag()` of any of them drops the entry,
|
|
752
|
+
and a HIT records them again, so a route `cache()`, ppr shell or document built
|
|
753
|
+
over the HIT stays evictable. A stale hit's background refresh stores the
|
|
754
|
+
refreshed body's tags; it runs the loaders it reads again rather than reusing
|
|
755
|
+
the page's (possibly stale) values.
|
|
756
|
+
|
|
757
|
+
One case keeps a `cache({ tags })` off: a cached loader that reads another
|
|
758
|
+
loader's `cache()` binding before that binding starts (for example a cached
|
|
759
|
+
layout loader reading a route's cached loader), and writes its entry before
|
|
760
|
+
it. That reader's entry has the tags the loader's body records, but not the
|
|
761
|
+
tags only its `cache({ tags })` lists. If the reader must drop with them, record
|
|
762
|
+
them in the loader body with `cacheTag()` too.
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
766
|
+
cacheTag(`product:${ctx.params.slug}`);
|
|
767
|
+
return db.product(ctx.params.slug);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
loader(ProductLoader, () => [cache({ ttl: 600 })]),
|
|
771
|
+
// updateTag(`product:${slug}`) drops this entry.
|
|
772
|
+
```
|
|
773
|
+
|
|
668
774
|
### Stale-While-Revalidate
|
|
669
775
|
|
|
670
776
|
```typescript
|
|
@@ -818,7 +924,10 @@ flushes, so both signals are resolved server-side while the tree is built:
|
|
|
818
924
|
`redirect()` replaces the whole page with the redirect carrier (still a 200
|
|
819
925
|
document, the client replaces to the target on hydration) and `notFound()`
|
|
820
926
|
renders the not-found UI at the owning segment with a real 404. No read site
|
|
821
|
-
runs, so a `useLoader` in a layout above every Suspense boundary is safe.
|
|
927
|
+
runs, so a `useLoader` in a layout above every Suspense boundary is safe. On a
|
|
928
|
+
`ppr` route, a flagged loader that settles with `notFound()` or `redirect()`
|
|
929
|
+
during the shell capture refuses the capture (warned once per key): that URL
|
|
930
|
+
gets no shell, and its renders deliver the signal as above.
|
|
822
931
|
|
|
823
932
|
Session/auth gates belong in middleware (they are request-shaped, not
|
|
824
933
|
data-shaped, and middleware CAN emit a real pre-stream 302). Data-dependent
|
|
@@ -857,7 +966,9 @@ they are replayed from the entry (see "Opting a Loader into Caching").
|
|
|
857
966
|
|
|
858
967
|
Under `cache()`, loader pushes are not stored with the cached segments: the
|
|
859
968
|
loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
|
|
860
|
-
settled `ssr: false`
|
|
969
|
+
the settled pushes of an `ssr: false` loader, and of the loaders it awaits, in
|
|
970
|
+
the shell, and a promise-free `ssr: false` loader does not run on a shell HIT —
|
|
971
|
+
see `/ppr`.)
|
|
861
972
|
|
|
862
973
|
Reads are the other direction and gated: `ctx.get(handle)` throws unless the
|
|
863
974
|
loader first does `await ctx.rendered()` (DSL-registered loaders only —
|
|
@@ -883,14 +994,17 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
883
994
|
The options object is typed as `LoaderOptions`
|
|
884
995
|
(`import type { LoaderOptions } from "@rangojs/router"`).
|
|
885
996
|
|
|
886
|
-
The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
997
|
+
The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off for
|
|
998
|
+
this loader, so nothing of it is left to stream in the document: document
|
|
999
|
+
renders await this loader before first flush — data is settled (`useLoader`
|
|
1000
|
+
reads it synchronously, no fallback paints), handle pushes beat the barrier
|
|
1001
|
+
snapshot, and a thrown `notFound()` deterministically precedes Response
|
|
1002
|
+
construction (real 404, no warm-up race). Client navigations stream exactly as
|
|
1003
|
+
before (on a `ppr` route, a navigation that replays a shell captured by a
|
|
1004
|
+
document request serves a promise-free flagged loader from its pin, as a
|
|
1005
|
+
document HIT does; a snapshot captured by a navigation alone carries no pins,
|
|
1006
|
+
and its replay runs the loader fresh). Scoped per LOADER: the flagged loader
|
|
1007
|
+
awaits only itself; siblings keep streaming.
|
|
894
1008
|
|
|
895
1009
|
Delivery is in-place, not merely in-document. React's Fizz outlines any
|
|
896
1010
|
COMPLETED Suspense boundary over ~500 bytes to an end-of-stream
|
|
@@ -906,17 +1020,25 @@ still outline; their reveal must wait for the CSS.
|
|
|
906
1020
|
|
|
907
1021
|
The costs and constraints:
|
|
908
1022
|
|
|
909
|
-
- Every document
|
|
910
|
-
That is the point — but keep flagged
|
|
911
|
-
routes.
|
|
1023
|
+
- Every document render that is not a `ppr` shell HIT pays the flagged
|
|
1024
|
+
loader's latency before first byte. That is the point — but keep flagged
|
|
1025
|
+
loaders fast, and flag loaders, not routes. On a HIT the shell flushes
|
|
1026
|
+
first: a flagged loader whose return has no promises does not run (unless
|
|
1027
|
+
the capture marked it to run: see `/ppr` → On a shell HIT), and one whose
|
|
1028
|
+
return carries promises runs to fill its holes.
|
|
912
1029
|
- A flagged loader must not `await ctx.rendered()` / `ctx.get(handle)` — the
|
|
913
1030
|
document render awaits the loader before the render barrier resolves, so
|
|
914
1031
|
that wait is a cycle by construction; it throws a deadlock error naming the
|
|
915
1032
|
fix.
|
|
916
1033
|
- Under PPR the flag is also the bake lane: the settled non-promise data
|
|
917
1034
|
(handle pushes included) freezes into the stored shell — see `/ppr` → The
|
|
918
|
-
loader lane rule.
|
|
919
|
-
|
|
1035
|
+
loader lane rule. On a HIT, a flagged loader whose return has no promises is
|
|
1036
|
+
served from the shell and does not run (unless the capture marked it to run:
|
|
1037
|
+
see `/ppr` → On a shell HIT); one whose return carries promises
|
|
1038
|
+
runs on every HIT, with its baked parts overlaid. A `cookies()`, `headers()`
|
|
1039
|
+
or `{ cache: false }` read in it refuses the capture. The
|
|
1040
|
+
`progressiveChunkSize` auto-raise is live-document only; captured shells
|
|
1041
|
+
outline per the explicit option or React's default.
|
|
920
1042
|
|
|
921
1043
|
Also available in `clientUrls()` route groups (`/client-urls`), where the
|
|
922
1044
|
loader-heavy shape makes it most useful.
|
|
@@ -75,7 +75,10 @@ render pass — normal renders, post-action revalidation, PE re-renders — its
|
|
|
75
75
|
variables are never stale: middleware is the safest `ctx.set` rung on the
|
|
76
76
|
data-passing ladder (`/rango` → "Passing data down the tree"). But it does
|
|
77
77
|
not change partial revalidation boundaries between handler/layout/parallel
|
|
78
|
-
segments.
|
|
78
|
+
segments. On a PPR shell HIT no handler or layout runs; on a route `cache()`
|
|
79
|
+
HIT the segments inside the `cache()` boundary don't run, and those above it
|
|
80
|
+
do. Output that didn't run shows the values read when the shell was captured
|
|
81
|
+
or the entry written.
|
|
79
82
|
|
|
80
83
|
For shared segment data, use named revalidation contracts on both the producer
|
|
81
84
|
and consumer segments, even when middleware is present in the chain.
|
|
@@ -334,19 +337,23 @@ the token (`createVar<T>({ cache: false })`) or per write
|
|
|
334
337
|
(`ctx.set("user", user, { cache: false })`). Reading it with `ctx.get()` inside
|
|
335
338
|
a `cache()` boundary or a `"use cache"` function then throws instead of baking
|
|
336
339
|
one user's data into a shared entry (inside `"use cache"`, also through a
|
|
337
|
-
middleware `ctx` passed in); loaders can still read it because
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
middleware `ctx` passed in); under `cache()` loaders can still read it because a
|
|
341
|
+
DSL loader re-runs on every hit (a `"use cache"` function a loader calls cannot,
|
|
342
|
+
and a loader bound with its own `cache()` only with a `key()`, which switches
|
|
343
|
+
the check off, so the key must include the value). On a `ppr` route only a
|
|
344
|
+
loader without `ssr: false` that no handler awaits may read it: a read the shell
|
|
345
|
+
capture waits for refuses the capture. A middleware `ctx` passed to a
|
|
346
|
+
`"use cache"` function keys the call by host, pathname, params and search, plus
|
|
347
|
+
the route name once the route is matched (a global middleware before `next()`
|
|
348
|
+
has none yet), and its `set()`, `header()` and `headers` writes throw inside the
|
|
349
|
+
function (`/use-cache`).
|
|
343
350
|
|
|
344
351
|
## Build-Time PPR Middleware
|
|
345
352
|
|
|
346
353
|
Normal `Prerender` Flight payload collection does not run middleware: there is
|
|
347
354
|
no request to wrap. The exception is `Prerender` + `ppr` build-shell capture.
|
|
348
|
-
After the Flight payload exists, the
|
|
349
|
-
|
|
355
|
+
After the Flight payload exists, the build replays global and route middleware
|
|
356
|
+
for each generated URL before capturing the shell.
|
|
350
357
|
|
|
351
358
|
In that build-shell pass:
|
|
352
359
|
|