@rangojs/router 0.27.0 → 0.28.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/dist/bin/rango.js +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/event-controller.d.ts +2 -1
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/segment-reconciler.d.ts +6 -4
- package/dist/types/browser/types.d.ts +2 -1
- package/dist/types/browser/wire-protocol.d.ts +25 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/tag-invalidation.d.ts +10 -0
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +4 -4
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +4 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +10 -0
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +24 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +9 -1
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +34 -0
- package/dist/vite/index.js +3809 -3710
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +17 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +8 -1
- package/skills/testing/response-routes.md +6 -4
- package/skills/use-cache/SKILL.md +46 -8
- package/src/__internal.ts +7 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +5 -3
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +14 -13
- package/src/browser/navigation-client.ts +18 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +66 -53
- package/src/browser/server-action-bridge.ts +50 -57
- package/src/browser/types.ts +2 -1
- package/src/browser/wire-protocol.ts +41 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +110 -50
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +34 -13
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +21 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +4 -4
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +77 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +4 -5
- package/src/router/match-context.ts +20 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +41 -53
- package/src/router/match-middleware/cache-lookup.ts +675 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +4 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/loader-cache.ts +14 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +137 -0
- package/src/router/segment-resolution/revalidation.ts +897 -776
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +10 -0
- package/src/router.ts +12 -13
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +757 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +212 -169
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +26 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +223 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/serve-shell-request.ts +23 -36
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +9 -1
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +35 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
package/package.json
CHANGED
|
@@ -320,11 +320,14 @@ domains, and the router id keeps two routers on one store apart when both
|
|
|
320
320
|
serve the same host and path (see `/host-router`, "Shared cache store"); the search part is sorted, excludes the router's internal params, and
|
|
321
321
|
honors `createRouter({ cache: { searchParams } })`.
|
|
322
322
|
|
|
323
|
-
A custom `key` function replaces the
|
|
324
|
-
or locale)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
323
|
+
A custom `key` function replaces the URL part of the default key (e.g., to
|
|
324
|
+
key by user role or locale), not the request type: document loads, client
|
|
325
|
+
navigations and intercept navigations still keep their own records, and the
|
|
326
|
+
key decides which requests of one type share a record. It also bypasses the
|
|
327
|
+
store's `keyGenerator` and the search-param filter. It runs once per request
|
|
328
|
+
and may read `cookies()`. Its result is stored namespaced (`key:` plus its URI
|
|
329
|
+
encoding, then the request type), so no value it returns, raw request input
|
|
330
|
+
included, names another route's record. A nested `cache()`
|
|
328
331
|
keys its records within the enclosing `key()` partition (`/caching` → "Keys
|
|
329
332
|
nest"). Normalize the result to the values you serve: each distinct value is
|
|
330
333
|
a record. On a `ppr` route, `key()` (and a store `keyGenerator` that returns
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -102,7 +102,9 @@ tainted objects" below).
|
|
|
102
102
|
|
|
103
103
|
Handle pushes follow the same split. Handler pushes (`ctx.use(Meta)`,
|
|
104
104
|
`ctx.use(Breadcrumbs)`, custom handles) are stored with the segments and
|
|
105
|
-
replayed on a hit.
|
|
105
|
+
replayed on a hit. On a navigation the hit replays them only for the segments
|
|
106
|
+
it sends: a layout the browser keeps keeps its values on screen, as on an
|
|
107
|
+
uncached navigation. Pushes from a `loader()` body are not stored: the loader
|
|
106
108
|
re-runs on the hit and pushes again, so each value appears once. A loader that
|
|
107
109
|
runs only because a cached handler awaited it with `ctx.use(Loader)` does not
|
|
108
110
|
re-run on a hit, so its pushes are stored with the handler's.
|
|
@@ -154,7 +156,7 @@ cache(
|
|
|
154
156
|
swr: 300, // seconds a stale entry is served while it refreshes.
|
|
155
157
|
// Default: store.defaults.swr, else no SWR window
|
|
156
158
|
tags: ["blog"], // or (ctx) => string[]; see "Tag-Based Invalidation"
|
|
157
|
-
key: (ctx) => `blog:${ctx.pathname}`, // replaces the default key
|
|
159
|
+
key: (ctx) => `blog:${ctx.pathname}`, // replaces the URL part of the default key
|
|
158
160
|
condition: (ctx) => !ctx.request.headers.has("x-preview"), // false = bypass
|
|
159
161
|
store: blogStore, // per-boundary store; see "Custom Cache Store"
|
|
160
162
|
},
|
|
@@ -164,15 +166,26 @@ cache(
|
|
|
164
166
|
);
|
|
165
167
|
```
|
|
166
168
|
|
|
167
|
-
- **Default key**: `{doc|partial|intercept}:{routerId}@{host}{pathname}[:params][?search]`
|
|
168
|
-
— document requests, client navigations, and intercept
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
- **Default key**: `{doc|partial|intercept:<intercept>}:{routerId}@{host}{pathname}[:params][?search]`
|
|
170
|
+
— under this key, document requests, client navigations, and intercept
|
|
171
|
+
navigations are cached separately, each intercept that targets the route
|
|
172
|
+
apart (`<intercept>` names the one the navigation selected), and so are two
|
|
173
|
+
routers that share a store (see `/host-router`, "Shared cache store"). The
|
|
174
|
+
search part honors `cache.searchParams` (below).
|
|
175
|
+
- **`key`** replaces the URL part of the default key (host, path, params,
|
|
176
|
+
search): it bypasses those, the store's `keyGenerator`, and the
|
|
177
|
+
search-param filter. The request kind stays: document requests, client
|
|
178
|
+
navigations and each intercept navigation keep their own record under one
|
|
179
|
+
`key()` result, so `key()` decides which requests of one kind share a
|
|
180
|
+
record (a `key()` that ignores `:id` shares one document record across
|
|
181
|
+
ids), never that a document request reads a navigation's record (which
|
|
182
|
+
keeps the fallback of a `loading(..., { ssr: false })` that a document
|
|
183
|
+
leaves out) or an intercept's. The
|
|
184
|
+
result is stored namespaced (`key:` plus its URI encoding, then the kind:
|
|
185
|
+
`tier:gold` is `key:tier%3Agold|doc` for a document request and
|
|
186
|
+
`key:tier%3Agold|partial` for a navigation), so whatever it returns,
|
|
187
|
+
request input included, never names another route's default-keyed record
|
|
188
|
+
or a nested partition's. A nested `cache()` keys its records within that
|
|
176
189
|
partition (see "Keys nest" under "Nested Cache Boundaries"). On a `ppr`
|
|
177
190
|
route the same key (or, without one, the store's `keyGenerator`)
|
|
178
191
|
partitions the PPR shell: each partition captures and serves its own shell
|
|
@@ -532,7 +545,7 @@ const router = createRouter({
|
|
|
532
545
|
baked-shell manifest gate (a URL whose only params are excluded ones now
|
|
533
546
|
matches the prerendered shell), and `"use cache"` ctx key normalization.
|
|
534
547
|
- **`cache({ key })` override wins**: a custom key bypasses the filter along
|
|
535
|
-
with the rest of default key
|
|
548
|
+
with the rest of the default key's URL part.
|
|
536
549
|
- **The footgun**: excluding a param promises the rendered output does not
|
|
537
550
|
depend on it. If it does, the first variant is cached and served to everyone.
|
|
538
551
|
That is why the default is `"all"`.
|
|
@@ -725,7 +738,9 @@ default, shared by every router on the host with that prefix; `HttpOnly`,
|
|
|
725
738
|
`SameSite=Lax`, `Path=/`, `Secure` on https, `Max-Age` 11 s with KV and 3 s
|
|
726
739
|
without at the defaults: the longest memo staleness plus 1 s). It is strictly
|
|
727
740
|
functional: its value is `1`, it identifies no one, and it only sends that
|
|
728
|
-
browser's reads past the memos.
|
|
741
|
+
browser's reads past the memos. A stale entry's background refresh (`cache()`,
|
|
742
|
+
`"use cache"`, a cached loader, the document cache) sets none: no visitor made
|
|
743
|
+
that invalidation. The same user's requests that carry it skip
|
|
729
744
|
both memos on every isolate, so a mutating user never sees a memoized shell or
|
|
730
745
|
marker from before their write. What they read past the memos is the store's
|
|
731
746
|
own consistency. `VercelCacheStore`: `expireTag` is global (about 300 ms), so
|
|
@@ -1084,7 +1099,8 @@ cache({ ttl: 300, key: (ctx) => `tier:${tier(ctx)}` }, () => [
|
|
|
1084
1099
|
path("/faq", FaqPage, { name: "faq" }),
|
|
1085
1100
|
]),
|
|
1086
1101
|
|
|
1087
|
-
// Its own key() composes with the outer one
|
|
1102
|
+
// Its own key() composes with the outer one, then the request kind
|
|
1103
|
+
// ("key:tier%3Agold|key:v%3Ab|doc")
|
|
1088
1104
|
cache(
|
|
1089
1105
|
{ ttl: 60, key: (ctx) => `v:${ctx.searchParams.get("v") ?? "a"}` },
|
|
1090
1106
|
() => [path("/plans", PlansPage, { name: "plans" })],
|
|
@@ -1099,10 +1115,10 @@ results first, outermost first:
|
|
|
1099
1115
|
|
|
1100
1116
|
| Boundary the route sits in | Record key |
|
|
1101
1117
|
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1102
|
-
| a single keyed `cache()` | Its `key()` result, namespaced (`tier:gold` is stored as `key:tier%3Agold`)
|
|
1118
|
+
| a single keyed `cache()` | Its `key()` result, namespaced, then the request kind (`tier:gold` is stored as `key:tier%3Agold\|doc` for a document request) |
|
|
1103
1119
|
| `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 |
|
|
1104
|
-
| `cache({ key })` under a keyed one | The chain's `key()` results
|
|
1105
|
-
| deeper nesting | Composes the same way;
|
|
1120
|
+
| `cache({ key })` under a keyed one | The chain's `key()` results, then the request kind |
|
|
1121
|
+
| deeper nesting | Composes the same way; the innermost boundary adds its default key without a `key` of its own, its request kind with one |
|
|
1106
1122
|
| 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 |
|
|
1107
1123
|
| `cache(false)` | No caching, as before; a `cache()` re-enabled below it stays in the partition |
|
|
1108
1124
|
| no `key` anywhere | The default key, or the store's `keyGenerator` result, as before |
|
|
@@ -1134,7 +1150,10 @@ results first, outermost first:
|
|
|
1134
1150
|
empty result there fails key resolution, so the request renders uncached,
|
|
1135
1151
|
and the router warns once naming the store. On the same store the inner
|
|
1136
1152
|
boundary's own default key already carries it (or its own `key()`
|
|
1137
|
-
overrides it).
|
|
1153
|
+
overrides it). Keep the default key in the result
|
|
1154
|
+
(`` `${locale}:${defaultKey}` ``): its prefix is the request kind, and a
|
|
1155
|
+
result without it gives a document request, a navigation and each
|
|
1156
|
+
intercept navigation one record.
|
|
1138
1157
|
- A response route's entry (`path.json()` and the other response routes) is
|
|
1139
1158
|
keyed the same way, behind a `response:` prefix. A `ppr` route's shell is
|
|
1140
1159
|
partitioned by the chain's `key()` results, namespaced the same way, since
|
|
@@ -161,6 +161,16 @@ short-circuits the middleware's downstream pipeline; global middleware that
|
|
|
161
161
|
wraps it can still run. Route middleware, handlers, and loaders below it do not.
|
|
162
162
|
An external CDN hit is different: the function never runs at all.
|
|
163
163
|
|
|
164
|
+
The background revalidation re-runs that downstream pipeline for the stale
|
|
165
|
+
entry. Its cookies, and the headers its route handlers and the middleware
|
|
166
|
+
after the cache write, go on its own response, never on the visitor's `STALE`
|
|
167
|
+
response; middleware ahead of the cache gets the `STALE` response from
|
|
168
|
+
`next()`, and its writes after `next()` land there. It
|
|
169
|
+
starts from what middleware ahead of the cache wrote to the response before
|
|
170
|
+
`next()`, as a `MISS` does, and its response is stored by the rules below:
|
|
171
|
+
its headers are kept, and a cookie it sets keeps the refreshed page out of the
|
|
172
|
+
cache.
|
|
173
|
+
|
|
164
174
|
## Cache Status Header
|
|
165
175
|
|
|
166
176
|
Response includes `x-document-cache-status`:
|
|
@@ -215,7 +225,9 @@ suffixes are still appended.
|
|
|
215
225
|
everyone). That includes a response whose request called
|
|
216
226
|
`updateTag()`/`revalidateTag()` on a store that keeps per-isolate memos
|
|
217
227
|
(`CFCacheStore`, `VercelCacheStore`): it carries the fresh-reads cookie
|
|
218
|
-
(`rango-state-fresh` by default), so it is not stored.
|
|
228
|
+
(`rango-state-fresh` by default), so it is not stored. A stale entry's
|
|
229
|
+
background re-run sets no fresh-reads cookie (no visitor made that
|
|
230
|
+
invalidation), so its invalidation does not keep the refreshed page out.
|
|
219
231
|
- Non-200 responses
|
|
220
232
|
- A 200 whose render reported an error: a component that threw after the
|
|
221
233
|
response started streaming, such as an async server component whose fetch
|
|
@@ -368,7 +368,7 @@ Per-slot merge order is **handler.use → shared use → slot-local use**. Slot-
|
|
|
368
368
|
|
|
369
369
|
Other things to keep in mind about `loading()`:
|
|
370
370
|
|
|
371
|
-
- Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading === false` handling in [segment-resolution/
|
|
371
|
+
- Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading === false` handling in [segment-resolution/revalidation.ts](../../src/router/segment-resolution/revalidation.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
|
|
372
372
|
|
|
373
373
|
Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every mount site to stream by default. Use the slot descriptor's `use` for any per-slot intent at a `parallel()` call.
|
|
374
374
|
|
|
@@ -370,9 +370,14 @@ below), and `cache()` onto the target route.
|
|
|
370
370
|
|
|
371
371
|
To cache a modal, put `cache()` on the target route (or a layout above it).
|
|
372
372
|
An intercept navigation is cached under that scope with its own `intercept:`
|
|
373
|
-
key, so it never collides with the full-page render of the same URL.
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
key, so it never collides with the full-page render of the same URL. The key
|
|
374
|
+
names the intercept the navigation selected, so two intercepts that target one
|
|
375
|
+
route with different `when()` conditions each keep their own record. Both
|
|
376
|
+
hold under a `cache({ key })` too: the `key()` replaces the URL part of the
|
|
377
|
+
key, never the request kind, so a document request and a plain navigation
|
|
378
|
+
never read an intercept's record (see `/caching`). For finer control, use
|
|
379
|
+
`"use cache"` in the intercept handler, or cache the intercept's loader:
|
|
380
|
+
`loader(ProductLoader, () => [cache()])`.
|
|
376
381
|
|
|
377
382
|
## Errors in an Intercept
|
|
378
383
|
|
|
@@ -463,7 +468,9 @@ contract and direction-aware examples.
|
|
|
463
468
|
## Interaction with Prerender
|
|
464
469
|
|
|
465
470
|
When the target route of an intercept uses `Prerender`, the intercept handler is
|
|
466
|
-
also resolved at build time and stored
|
|
471
|
+
also resolved at build time and stored in an intercept variant next to the
|
|
472
|
+
route's own pre-rendered page. The variant holds what an intercept navigation
|
|
473
|
+
renders: the layouts above the route and the intercept, not the route's page.
|
|
467
474
|
This means intercept navigations to pre-rendered routes are served from the
|
|
468
475
|
prerender store without executing handler code at runtime.
|
|
469
476
|
|
|
@@ -496,13 +503,16 @@ Build-time behavior:
|
|
|
496
503
|
- The intercept handler (`<ProductModal />`) is resolved with BuildContext
|
|
497
504
|
- Result is stored under the key `"detail/paramHash/i"` (intercept variant)
|
|
498
505
|
- `when` config conditions are skipped at build time (all intercepts pre-rendered unconditionally)
|
|
499
|
-
- `when` is still evaluated at runtime by the intercept-resolution middleware
|
|
506
|
+
- `when` is still evaluated at runtime by the intercept-resolution middleware,
|
|
507
|
+
and a navigation shows only the intercept it selected
|
|
508
|
+
- Two intercepts on one layout that fill the same slot are not pre-rendered:
|
|
509
|
+
they render live
|
|
500
510
|
|
|
501
511
|
Runtime behavior:
|
|
502
512
|
|
|
503
|
-
- Intercept navigation: prerender store serves the `/i` variant (frozen handler + fresh loaders)
|
|
513
|
+
- Intercept navigation: prerender store serves the `/i` variant (frozen handler + fresh loaders). A layout the client holds (`ShopLayout`) stays as it is on screen, as on a live intercept, unless its `revalidate()` asks for it
|
|
504
514
|
- Direct navigation: prerender store serves the main variant (full page)
|
|
505
|
-
- If no intercept prerender entry exists, falls through to live intercept resolution
|
|
515
|
+
- If no intercept prerender entry exists, or it lacks the selected intercept, falls through to live intercept resolution
|
|
506
516
|
|
|
507
517
|
Loaders inside the intercept always run fresh at request time, same as regular
|
|
508
518
|
pre-rendered routes.
|
|
@@ -319,6 +319,24 @@ export const timing: Middleware = async (ctx, next) => {
|
|
|
319
319
|
};
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
+
A middleware may answer without waiting for its `next()`, as a timeout
|
|
323
|
+
fallback does. The middleware ahead then see its `Response` after `next()`
|
|
324
|
+
and write on it. The abandoned render keeps running and builds its own
|
|
325
|
+
response, which nothing serves.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
export const renderTimeout: Middleware = (ctx, next) =>
|
|
329
|
+
Promise.race([
|
|
330
|
+
next(),
|
|
331
|
+
new Promise<Response>((resolve) =>
|
|
332
|
+
setTimeout(
|
|
333
|
+
() => resolve(new Response("Try again", { status: 503 })),
|
|
334
|
+
2000,
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
]);
|
|
338
|
+
```
|
|
339
|
+
|
|
322
340
|
### Typed context variables in middleware
|
|
323
341
|
|
|
324
342
|
Use `createVar<T>()` for type-safe data sharing between middleware and handlers:
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -450,7 +450,7 @@ non-claiming read option.
|
|
|
450
450
|
|
|
451
451
|
The first cold partial request reports `BYPASS; reason=no-entry` because no
|
|
452
452
|
artifact could supply that response; its successful render schedules
|
|
453
|
-
`
|
|
453
|
+
`planShellCapture` with a navigation-only marker. The next request reports
|
|
454
454
|
`HIT` when the capture produced an eligible snapshot. Navigation snapshots use
|
|
455
455
|
a separate shell key, so they cannot overwrite or be served as a document shell.
|
|
456
456
|
The capture runs with the stripped target document URL and loads SSR support in
|
|
@@ -509,7 +509,9 @@ In handlers it is
|
|
|
509
509
|
too late to prevent a MISS render from already happening, but it still prevents
|
|
510
510
|
the follow-up shell capture. A `ctx.dynamic()` call that only happens during the
|
|
511
511
|
capture itself (for example inside a handler promise, after an await) refuses
|
|
512
|
-
that capture: a dynamic render has no shell.
|
|
512
|
+
that capture: a dynamic render has no shell. A cached handler that calls it in
|
|
513
|
+
the background refresh of a stale `cache()` record does not: the capture
|
|
514
|
+
replayed the record, and the refresh renders the record only.
|
|
513
515
|
|
|
514
516
|
During `Prerender` + `ppr` build-shell capture, middleware is replayed with
|
|
515
517
|
`ctx.build === true`, `ctx.waitUntil()` inert, and the same `ctx.dynamic()`
|
|
@@ -1177,7 +1179,7 @@ their data should bake vs stay live. Your levers, in order of preference:
|
|
|
1177
1179
|
3. **Guaranteed-fresh widgets: live loaders with a widget-sized boundary.**
|
|
1178
1180
|
An unflagged loader read under an inline `<Suspense>` in the widget is
|
|
1179
1181
|
enough. Alternatively, a parallel slot with its OWN `loading()`:
|
|
1180
|
-
parallel-owned loaders get their own per-slot boundary (`
|
|
1182
|
+
parallel-owned loaders get their own per-slot boundary (`resolveParallelSegments` tags
|
|
1181
1183
|
them with the slot's loading; `segment-system.tsx` builds a per-slot
|
|
1182
1184
|
LoaderBoundary), so the chrome bakes into the shell and each widget is an
|
|
1183
1185
|
independent, widget-sized hole:
|
|
@@ -1633,6 +1635,19 @@ evicted by tag at all — move always-fresh data into a live-lane loader (no
|
|
|
1633
1635
|
capture is served as before. Only a throw during the capture counts: a
|
|
1634
1636
|
component that failed in the live render and succeeds when captured is
|
|
1635
1637
|
stored.
|
|
1638
|
+
- **A handler that throws during capture refuses it too**: when a route,
|
|
1639
|
+
layout or parallel slot handler throws (or throws `notFound()`) on the
|
|
1640
|
+
capture's own run while the request that scheduled it rendered fine, for
|
|
1641
|
+
example on a flaky upstream, its boundary (a declared `errorBoundary()` or
|
|
1642
|
+
`notFoundBoundary()`, or the default error page) would become the shell.
|
|
1643
|
+
The capture stores nothing, logs `[ShellCache] capture`, reports to
|
|
1644
|
+
`onError` as `cache-write`, and backs the key off; the next capture after
|
|
1645
|
+
the window stores normally. A request whose own render fails answers
|
|
1646
|
+
500/404 and schedules no capture, but a recapture has no visitor render
|
|
1647
|
+
behind it: a stale shell whose recapture fails keeps serving until its
|
|
1648
|
+
`ttl` + `swr` ends (a build-time shell, until a recapture stores), as a
|
|
1649
|
+
failed `cache()` refresh does, logging and calling `onError` once per
|
|
1650
|
+
backoff window. When you delete a page, invalidate its tag.
|
|
1636
1651
|
|
|
1637
1652
|
## Related
|
|
1638
1653
|
|
|
@@ -309,16 +309,16 @@ layout(DocsNav, () => [
|
|
|
309
309
|
|
|
310
310
|
## Interaction with DSL Items
|
|
311
311
|
|
|
312
|
-
| DSL item | Behavior with Prerender
|
|
313
|
-
| -------------- |
|
|
314
|
-
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching.
|
|
315
|
-
| `revalidate()` | Without Passthrough: no
|
|
316
|
-
| `cache()` | Orthogonal -- prerendered segments bypass the runtime cache. Use `cache()` on parent layouts and loaders.
|
|
317
|
-
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live.
|
|
318
|
-
| `parallel()` | Parallel slots inside path are pre-rendered.
|
|
319
|
-
| `middleware()` | Skipped while collecting build-time Flight payloads (no request). For `Prerender` + `ppr`, the build-shell step replays global and route middleware with `ctx.build === true`; `ctx.dynamic()` skips that shell. At request time middleware runs normally on every request.
|
|
320
|
-
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough.
|
|
321
|
-
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally).
|
|
312
|
+
| DSL item | Behavior with Prerender |
|
|
313
|
+
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
|
|
315
|
+
| `revalidate()` | Without Passthrough: re-runs no prerendered handler (segments are re-served from the build artifact); when the params change, a held parent layout whose `revalidate()` asks for it gets its baked copy. Loader revalidation still works. With Passthrough: re-renders through the live handler. |
|
|
316
|
+
| `cache()` | Orthogonal -- prerendered segments bypass the runtime cache. Use `cache()` on parent layouts and loaders. |
|
|
317
|
+
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are pre-rendered too: every hit, intercept variant included, serves them from the build artifact and runs no layout handler. On a navigation a parent layout the client holds stays held, even when the params change, as on a live route. |
|
|
318
|
+
| `parallel()` | Parallel slots inside path are pre-rendered. A parent layout's slots are pre-rendered too: a hit serves them from the build artifact with the layout, and one the client holds stays held, as on a live route. |
|
|
319
|
+
| `middleware()` | Skipped while collecting build-time Flight payloads (no request). For `Prerender` + `ppr`, the build-shell step replays global and route middleware with `ctx.build === true`; `ctx.dynamic()` skips that shell. At request time middleware runs normally on every request. |
|
|
320
|
+
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
|
|
321
|
+
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside the main entry, without the route's own segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally); a navigation serves only the intercept it selected. |
|
|
322
322
|
|
|
323
323
|
When Passthrough revalidation is enabled, remember that revalidation is
|
|
324
324
|
still partial: opting a child segment into revalidation does not
|
|
@@ -1426,19 +1426,24 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
1426
1426
|
## Interaction with intercept()
|
|
1427
1427
|
|
|
1428
1428
|
When a pre-rendered route is also the target of an `intercept()`, the build system
|
|
1429
|
-
resolves the intercept handler at build time and stores
|
|
1430
|
-
|
|
1429
|
+
resolves the intercept handler at build time and stores an intercept variant
|
|
1430
|
+
under an `/i`-suffixed key alongside the main entry. The variant holds what an
|
|
1431
|
+
intercept navigation renders: the main segments without the route's own (its
|
|
1432
|
+
handler, layouts and slots, which a render under an intercept skips), then the
|
|
1433
|
+
intercept segments, with the handle values of exactly those segments:
|
|
1431
1434
|
|
|
1432
1435
|
```
|
|
1433
1436
|
prerender store keys:
|
|
1434
1437
|
"blog.post/a1b2c3" -> main segments (full page)
|
|
1435
|
-
"blog.post/a1b2c3/i" -> main segments + intercept segments
|
|
1438
|
+
"blog.post/a1b2c3/i" -> main segments without the route's own + intercept segments
|
|
1436
1439
|
```
|
|
1437
1440
|
|
|
1438
1441
|
At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
|
|
1439
1442
|
|
|
1440
1443
|
- **Intercept navigation**: looks up `paramHash/i` first. If found, yields
|
|
1441
|
-
the
|
|
1444
|
+
the variant's segments with only the slot of the intercept the navigation
|
|
1445
|
+
selected (`selectInterceptVariant`); a variant without that slot is a miss.
|
|
1446
|
+
`handleCacheHitIntercept()` extracts intercept segments
|
|
1442
1447
|
(filtered by `namespace?.startsWith("intercept:")`) and sets up slots.
|
|
1443
1448
|
- **Direct navigation**: looks up `paramHash` (no suffix). Standard prerender path.
|
|
1444
1449
|
- **Intercept miss (no `/i` entry)**: falls through to the normal pipeline so
|
|
@@ -1447,7 +1452,26 @@ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
|
|
|
1447
1452
|
|
|
1448
1453
|
The `when` config selector receives an `InterceptSelectorContext` whose `from`
|
|
1449
1454
|
location (the page being left) is unknown at build time. All intercepts are pre-rendered unconditionally;
|
|
1450
|
-
`when` is evaluated at runtime by the intercept-resolution middleware
|
|
1455
|
+
`when` is evaluated at runtime by the intercept-resolution middleware, and a
|
|
1456
|
+
navigation shows only the intercept it selected: the innermost whose `when`
|
|
1457
|
+
passes, as on a route that is not pre-rendered. Two intercepts on one layout
|
|
1458
|
+
that fill the same slot are not pre-rendered: the variant could not tell them
|
|
1459
|
+
apart, so they render live. Intercepts declared in the route's own layouts are
|
|
1460
|
+
never selected and are not pre-rendered.
|
|
1461
|
+
|
|
1462
|
+
The build warns once per route and slot for an intercept a navigation can
|
|
1463
|
+
select that is not pre-rendered: two intercepts that fill one slot, a handler
|
|
1464
|
+
or layout that returned a `Response` at build time, or an intercept that
|
|
1465
|
+
targets the route by its name inside an `include()` (`"item"` for the route
|
|
1466
|
+
`"shop.item"`, or for a route named `"item"` in an unnamed `include()`), which
|
|
1467
|
+
only the local-name fallback selects (name the `include()` and target the
|
|
1468
|
+
full name, or declare the intercept inside it with `".item"`, to pre-render
|
|
1469
|
+
it). That intercept renders live, and its handler runs per request without
|
|
1470
|
+
`BuildContext`; a route in an unnamed `include()` is named by its URL:
|
|
1471
|
+
|
|
1472
|
+
```
|
|
1473
|
+
[rango] Prerender route "item": intercept @modal is not pre-rendered: two intercepts on one layout fill @modal. A navigation that selects it renders it live, and its handler runs per request without BuildContext. To pre-render it, give each intercept its own slot.
|
|
1474
|
+
```
|
|
1451
1475
|
|
|
1452
1476
|
### Example: Pre-rendered route with intercept
|
|
1453
1477
|
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -249,7 +249,11 @@ identity".
|
|
|
249
249
|
The predicate arg carries the action's full context, not just its identity. Match
|
|
250
250
|
_which_ action with `ctx.isAction(addToCart)` (rename-safe); branch on _what it
|
|
251
251
|
returned_ with `ctx.actionResult` — the value your `"use server"` function
|
|
252
|
-
returned, for outcome-conditional revalidation.
|
|
252
|
+
returned, for outcome-conditional revalidation. When the action threw into an
|
|
253
|
+
`errorBoundary()`, a predicate above the boundary still runs, with
|
|
254
|
+
`actionResult` set to the thrown value: check a field's shape before reading
|
|
255
|
+
it (`Array.isArray(actionResult?.items)`). A `redirect()` it throws then redirects, as
|
|
256
|
+
after a success. The arg also exposes `actionId`
|
|
253
257
|
(raw `path#export`), `actionUrl`, `formData` (form-based actions only), `method`,
|
|
254
258
|
and `stale` (the `_rsc_stale` signal that an action ran in this or another tab;
|
|
255
259
|
that Back/Forward revalidation takes an action's defaults, but `isAction()` is
|
|
@@ -704,7 +704,14 @@ is usually too aggressive.
|
|
|
704
704
|
Throw for genuinely exceptional conditions (network failure, DB outage,
|
|
705
705
|
auth violation). The nearest `errorBoundary()` above the page the action was
|
|
706
706
|
called from catches them, and the boundary renders inside route middleware like
|
|
707
|
-
a normal render.
|
|
707
|
+
a normal render. Above the boundary the response is the action's revalidation,
|
|
708
|
+
as after a success: the layouts' loaders re-run as their `revalidate()`
|
|
709
|
+
decides, so a cart count shows what the action wrote before it threw. A
|
|
710
|
+
route's `errorBoundary()` replaces the route: its handler and loaders do not
|
|
711
|
+
run. A layout's `errorBoundary()` replaces what the layout renders in its
|
|
712
|
+
outlet: the layout's own loaders, slots and orphan layouts revalidate as on a
|
|
713
|
+
success, and nothing inside the outlet runs. An action sent from an intercept
|
|
714
|
+
gets the boundary alone.
|
|
708
715
|
|
|
709
716
|
```typescript
|
|
710
717
|
// urls.tsx — inside the urls() callback
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Layer:** integration (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** response routes (json/text/html/xml/md), redirects, 404 (see `/response-routes`, `/mime-routes`)
|
|
4
4
|
|
|
5
|
-
`dispatch` runs the
|
|
5
|
+
`dispatch` runs the production handler's REAL request stages — classify (matching + content negotiation), the terminal plans (reloads, the rewritten-action rule), and the real global + route-level middleware chain — with no RSC render, so redirects, 404s, reloads, response routes, content negotiation, and middleware short-circuits behave exactly as in production. You SEED the request and `env`; everything else (matching, middleware, header/cookie merge) is real machinery.
|
|
6
6
|
|
|
7
7
|
Matching goes through the route trie, as every request in `vite dev` and production does.
|
|
8
8
|
|
|
@@ -83,8 +83,9 @@ describe("api routes via dispatch", () => {
|
|
|
83
83
|
- trailing-slash redirects from the trie — a route with `trailingSlash: "never"` or `"always"` answers the other form with a 308 and the `Location` (query preserved). A route with no `trailingSlash` serves its URL with and without a trailing slash, with no redirect, as production does;
|
|
84
84
|
- the open-redirect guard on browser-followed redirects (a cross-origin `Location` is rewritten to the basename root unless `redirect(url, { external: true })`);
|
|
85
85
|
- content-negotiated routes (`Vary: Accept` appended);
|
|
86
|
+
- the reloads production answers inside the global middleware chain: a request whose `_rsc_v` names another deploy's document version (`setBuildVersions`, `./serve-shell-request.md`), and a `_rsc_partial` request whose `_rsc_rid` names another router (a cross-app navigation), get an `X-RSC-Reload` at the visitor's URL — on any route, a component route or an unmatched path included;
|
|
86
87
|
- cached response routes (`cache({ ... })` around a `path.json` etc.): hit, SWR, and tag writes. The cache write is scheduled via `waitUntil` (a microtask in tests), so flush microtasks between the seeding `dispatch` and the one that asserts the hit;
|
|
87
|
-
- `createRouter({ telemetry })` `request.start` / `request.end` / `request.error` events, and `createRouter({ onError })` for cache-read/write failures.
|
|
88
|
+
- `createRouter({ telemetry })` `request.start` / `request.end` / `request.error` events, all with one `requestId`, which a response route's `request.timeout` carries too (a rewritten pass's included), and `createRouter({ onError })` for cache-read/write failures.
|
|
88
89
|
|
|
89
90
|
## Caveats
|
|
90
91
|
|
|
@@ -94,9 +95,10 @@ describe("api routes via dispatch", () => {
|
|
|
94
95
|
- Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
|
|
95
96
|
- Building the trie runs route discovery: every `include()`d `urls()` handler runs once more, and every async include provider (`() => import(...)`) is called. Count provider calls or handler runs after the first `dispatch` on that router, not from router construction. A provider that rejects fails the `dispatch`, as it fails `vite dev` and the build.
|
|
96
97
|
- A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`). Importing the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules) — build from a focused include (your API routes) for whole-router dispatch.
|
|
97
|
-
- A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request.
|
|
98
|
+
- A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request. An action request (`?_rsc_action`) a rewrite moves to a response route, a redirect or an unmatched path rejects with production's error, `A rewrite of a server action or form post must target a page route; …`.
|
|
99
|
+
- The route's params and name are set inside the global middleware chain, as production sets them: a global middleware reads `ctx.routeName` (and `getRequestContext().params`) after `await next()`, not before it. A trailing-slash redirect sets none.
|
|
98
100
|
- A `_rsc_partial` request to a response route runs global middleware first (an auth gate can still 401/redirect), then returns `X-RSC-Reload` — route-level middleware is skipped, exactly like production.
|
|
99
|
-
- `dispatch` does NOT execute server actions (`?_rsc_action`), but it DOES run the global middleware chain on an action request — middleware can still 401/redirect it,
|
|
101
|
+
- `dispatch` does NOT execute server actions (`?_rsc_action`), but it DOES run the global middleware chain on an action request — middleware can still 401/redirect it. When a global middleware matches the path, any 3xx that leaves the chain on a partial OR action request (a global or route middleware's redirect, a response route's, a trailing-slash redirect) becomes a `204` + `X-RSC-Redirect` (fetch-safe interception), the raw `Location` dropped. Without one, an action request's 3xx goes out raw (through the open-redirect guard), as production sends it; a trailing-slash redirect on a partial request is a `204` either way. Location state carried on such a redirect is dropped too (production embeds it in a Flight payload); cover that at e2e.
|
|
100
102
|
- `createRouter({ onError })` is NOT invoked for a thrown non-Response HANDLER error here (the typed error Response still matches production), and `cache.decision` / `loader.*` telemetry never fires (no RSC render runs). Cover both at e2e.
|
|
101
103
|
|
|
102
104
|
## See also
|
|
@@ -68,10 +68,17 @@ export async function ProductCard({ id }: { id: string }) {
|
|
|
68
68
|
A result Flight cannot encode cleanly is not cached: an async child component
|
|
69
69
|
that throws, a promise in the result or in a handle push that rejects, a
|
|
70
70
|
function or a class instance. Storing it would bake the failure into every hit
|
|
71
|
-
until expiry. Instead the write is skipped and reported to `onError`
|
|
72
|
-
`"cache"`, category `cache-write`, or `stale-revalidation` for a
|
|
73
|
-
refresh):
|
|
74
|
-
|
|
71
|
+
until expiry. Instead the write is skipped, logged and reported to `onError`
|
|
72
|
+
(phase `"cache"`, category `cache-write`, or `stale-revalidation` for a
|
|
73
|
+
background refresh): the next call runs the function again, and a stale entry
|
|
74
|
+
keeps serving.
|
|
75
|
+
|
|
76
|
+
A miss renders a decoded copy of what it encodes, as a hit renders the stored
|
|
77
|
+
entry, so a child that throws or a promise that rejects throws where the copy
|
|
78
|
+
renders, as it would on a hit. A value Flight cannot encode at all (a
|
|
79
|
+
function, a class instance) in the data the function returns is the
|
|
80
|
+
exception: the call returns the live value, uncached. One inside a promise in
|
|
81
|
+
that data rejects that promise instead, as it would on a hit.
|
|
75
82
|
|
|
76
83
|
## Named Cache Profiles
|
|
77
84
|
|
|
@@ -233,6 +240,35 @@ export const ProductLoader = createLoader(async (ctx) => getProduct(ctx));
|
|
|
233
240
|
per-request values (cookies, headers) are not reflected in the cache key. Without
|
|
234
241
|
this guard, one user's data would be served to another.
|
|
235
242
|
|
|
243
|
+
That includes the server components the function returns. The value is
|
|
244
|
+
encoded once, inside the same guard, and the page renders the decoded copy, as
|
|
245
|
+
on a hit, so the guard refuses there every read and write it refuses in the
|
|
246
|
+
body, and a refused read fails the request as it would in the body: at the
|
|
247
|
+
value's root the call throws, and below the root that part renders as an
|
|
248
|
+
error, to its nearest error boundary. Nothing is stored. The same holds for a
|
|
249
|
+
`ctx.use(Loader)` in the returned tree (the loader runs inside the guard, and
|
|
250
|
+
the request's memo of it rejects, so a later read of it in the request throws
|
|
251
|
+
too) and for a `React.lazy` or a memoizing thenable created in the body.
|
|
252
|
+
|
|
253
|
+
A handle value the function pushes (`ctx.use(Handle)`) is encoded inside the
|
|
254
|
+
guard as well, and one that reads the request is not stored. One handle case
|
|
255
|
+
is not covered yet: a handle value that holds a lazy component or a memoized
|
|
256
|
+
promise. Don't push lazy state that reads the request from a cached function.
|
|
257
|
+
|
|
258
|
+
Two memo gaps remain, where the read happened before the guard could see it:
|
|
259
|
+
|
|
260
|
+
- In the body: a `React.cache`-memoized read of `cookies()` or `headers()` (a
|
|
261
|
+
`getSession = cache(() => cookies()...)` auth helper) that the page called
|
|
262
|
+
before the cached function runs returns the memo without reading again, so
|
|
263
|
+
the guard never sees it and the entry stores that visitor's value.
|
|
264
|
+
- In the body or the returned tree: a per-request memo of your own, keyed by
|
|
265
|
+
`getRequestContext()`, that the page filled and the cached function or a
|
|
266
|
+
component it returns reads. `React.cache` itself is safe in the returned
|
|
267
|
+
tree, which renders in its own Flight request.
|
|
268
|
+
|
|
269
|
+
Pass the value in as an argument instead of reading a memo inside the cached
|
|
270
|
+
function or the tree it returns.
|
|
271
|
+
|
|
236
272
|
The raw reads throw the same way: `ctx.request.headers` on any `ctx` passed in
|
|
237
273
|
(handler, loader, middleware; it is one `Request` object, also
|
|
238
274
|
`getRequestContext().request`), and `getRequestContext().cookie()` /
|
|
@@ -397,9 +433,11 @@ export const Page = Prerender(
|
|
|
397
433
|
|
|
398
434
|
## Performance: waitUntil
|
|
399
435
|
|
|
400
|
-
On cache miss, the function executes and
|
|
401
|
-
|
|
402
|
-
the
|
|
436
|
+
On cache miss, the function executes and its result is encoded; the call
|
|
437
|
+
returns once the decoded copy's root has arrived, so a server component at the
|
|
438
|
+
root of the value renders before the call resolves. The rest of the encode and
|
|
439
|
+
the cache **store write** (`setItem`) are deferred to `waitUntil` and do NOT
|
|
440
|
+
block the response.
|
|
403
441
|
|
|
404
442
|
On stale hit, stale data is returned immediately. Background revalidation
|
|
405
443
|
(re-execute + store write) runs entirely inside `waitUntil`.
|
|
@@ -407,7 +445,7 @@ On stale hit, stale data is returned immediately. Background revalidation
|
|
|
407
445
|
| Phase | Blocks response? |
|
|
408
446
|
| ------------------------------------ | ---------------- |
|
|
409
447
|
| Function execution (miss) | Yes |
|
|
410
|
-
| Result
|
|
448
|
+
| Result encode, to its root (miss) | Yes |
|
|
411
449
|
| Cache store write (miss) | No (waitUntil) |
|
|
412
450
|
| Stale value return (stale hit) | No (immediate) |
|
|
413
451
|
| Background revalidation (stale) | No (waitUntil) |
|
package/src/__internal.ts
CHANGED
|
@@ -85,10 +85,16 @@ export type {
|
|
|
85
85
|
*/
|
|
86
86
|
export type {
|
|
87
87
|
RouterContext,
|
|
88
|
-
RevalidationContext,
|
|
89
88
|
InterceptResult,
|
|
90
89
|
} from "./router/router-context.js";
|
|
91
90
|
|
|
91
|
+
/**
|
|
92
|
+
* @internal
|
|
93
|
+
* The request side of a revalidating resolution pass (RouterContext's
|
|
94
|
+
* revalidating resolvers take it).
|
|
95
|
+
*/
|
|
96
|
+
export type { ResolutionPassInput } from "./router/segment-resolution/resolution-pass.js";
|
|
97
|
+
|
|
92
98
|
/**
|
|
93
99
|
* @internal
|
|
94
100
|
* Route match context during pipeline processing.
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* state regardless of listener ordering.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { WIRE } from "../wire-params.js";
|
|
21
|
+
|
|
20
22
|
const IDLE_TIMEOUT = 60_000;
|
|
21
23
|
const DEBOUNCE_DELAY = 150;
|
|
22
24
|
|
|
@@ -61,7 +63,7 @@ export function startConnectionWarmup(
|
|
|
61
63
|
|
|
62
64
|
function sendWarmup() {
|
|
63
65
|
coldLatch = false;
|
|
64
|
-
env.fetch(
|
|
66
|
+
env.fetch(`/?${WIRE.warmup}`, { method: "HEAD" }).catch(() => {});
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
function triggerWarmup() {
|
|
@@ -258,7 +258,8 @@ export interface EventController {
|
|
|
258
258
|
/** Originating history entry key; scopes location-state arbitration. */
|
|
259
259
|
cohort?: string,
|
|
260
260
|
): ActionHandle;
|
|
261
|
-
|
|
261
|
+
/** Abort every action in flight except `except` (an ActionHandle id). */
|
|
262
|
+
abortAllActions(except?: string): void;
|
|
262
263
|
|
|
263
264
|
// State access
|
|
264
265
|
getState(): DerivedNavigationState;
|
|
@@ -1108,13 +1109,14 @@ export function createEventController(
|
|
|
1108
1109
|
};
|
|
1109
1110
|
}
|
|
1110
1111
|
|
|
1111
|
-
|
|
1112
|
+
/** Abort every action in flight except `except` (an ActionHandle id). */
|
|
1113
|
+
function abortAllActions(except?: string) {
|
|
1112
1114
|
for (const [id, entry] of inflightActions) {
|
|
1113
1115
|
// Preserve settling entries — they have already been handled by
|
|
1114
1116
|
// fail()/complete() and will self-cleanup via the settlement timeout.
|
|
1115
1117
|
// Clearing them here would prevent debounced notifications from
|
|
1116
1118
|
// delivering the error/result state to subscribers.
|
|
1117
|
-
if (entry.phase === "settling") continue;
|
|
1119
|
+
if (entry.phase === "settling" || id === except) continue;
|
|
1118
1120
|
entry.abort.abort();
|
|
1119
1121
|
inflightActions.delete(id);
|
|
1120
1122
|
}
|