@rangojs/router 0.26.6 → 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/action-coordinator.d.ts +0 -4
- package/dist/types/browser/action-fence.d.ts +11 -3
- package/dist/types/browser/event-controller.d.ts +20 -1
- package/dist/types/browser/history-state.d.ts +7 -9
- package/dist/types/browser/intercept-utils.d.ts +23 -0
- 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/rsc-router.d.ts +1 -1
- 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 +4 -6
- 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 +12 -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 +35 -87
- package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
- 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/revalidation.d.ts +6 -1
- package/dist/types/router/router-context.d.ts +9 -63
- 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 +2 -2
- 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/testing/serve-shell-request.d.ts +7 -0
- 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 +18 -7
- 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/client-urls/SKILL.md +2 -1
- package/skills/document-cache/SKILL.md +18 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +53 -13
- package/skills/layout/SKILL.md +11 -7
- package/skills/loader/SKILL.md +30 -13
- package/skills/middleware/SKILL.md +19 -1
- package/skills/migrate-nextjs/SKILL.md +5 -4
- package/skills/parallel/SKILL.md +5 -4
- package/skills/ppr/SKILL.md +19 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +23 -11
- package/skills/server-actions/SKILL.md +51 -6
- package/skills/testing/predicates.md +1 -1
- package/skills/testing/response-routes.md +6 -4
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/use-cache/SKILL.md +46 -8
- package/skills/ux-architecture/SKILL.md +3 -2
- package/src/__internal.ts +7 -1
- package/src/browser/action-coordinator.ts +2 -13
- package/src/browser/action-fence.ts +11 -3
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +42 -3
- package/src/browser/history-state.ts +15 -13
- package/src/browser/intercept-utils.ts +51 -0
- package/src/browser/navigation-bridge.ts +34 -30
- package/src/browser/navigation-client.ts +21 -33
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +7 -11
- package/src/browser/partial-update.ts +8 -0
- 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 +17 -2
- package/src/browser/segment-reconciler.ts +66 -53
- package/src/browser/server-action-bridge.ts +265 -127
- 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 +42 -15
- 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 +4 -7
- 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 +33 -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 +680 -720
- package/src/router/match-middleware/cache-store.ts +14 -12
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/match-result.ts +1 -20
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +4 -3
- 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/revalidation.ts +33 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +14 -117
- 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 +172 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +3 -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/ssr/ssr-root.tsx +1 -0
- package/src/testing/dispatch.ts +223 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/serve-shell-request.ts +37 -32
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +9 -1
- package/src/types/handler-context.ts +21 -7
- 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
|
|
@@ -326,7 +326,8 @@ Per request: a JS action POST runs the predicate with `method: "POST"`,
|
|
|
326
326
|
`FormData` argument) and `actionUrl`; the default is true. An in-group
|
|
327
327
|
navigation GET defaults to true when params or search changed. A prefetch
|
|
328
328
|
partial runs the predicate, so a prefetched in-group navigation honours it. A
|
|
329
|
-
stale back/forward revalidation sees `stale: true
|
|
329
|
+
stale back/forward revalidation sees `stale: true` and takes an action's
|
|
330
|
+
defaults (the default is true). Document requests and
|
|
330
331
|
no-JS form posts hold no segments, so every loader renders whatever a predicate
|
|
331
332
|
says. On the action-triggered refetch GET (consolidation) `isAction()` is false,
|
|
332
333
|
as for `urls()`. `currentParams`/`nextParams` are the server's route params,
|
|
@@ -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`:
|
|
@@ -202,6 +212,11 @@ suffixes are still appended.
|
|
|
202
212
|
|
|
203
213
|
- Server actions (`_rsc_action`)
|
|
204
214
|
- Loader requests (`_rsc_loader`)
|
|
215
|
+
- Stale requests (`_rsc_stale`): the client marks stale the follow-up
|
|
216
|
+
revalidation after an action, a Back/Forward entry's revalidation, and any
|
|
217
|
+
request that holds no segments (`router.refresh()`, a cross-tab refresh).
|
|
218
|
+
Each renders at the origin and is not stored, because its data may predate
|
|
219
|
+
a write and the cache cannot tell.
|
|
205
220
|
- Non-GET requests
|
|
206
221
|
- Responses without an `s-maxage` `Cache-Control` directive
|
|
207
222
|
- Responses whose `Cache-Control` also contains `private`, `no-store`, or an
|
|
@@ -210,7 +225,9 @@ suffixes are still appended.
|
|
|
210
225
|
everyone). That includes a response whose request called
|
|
211
226
|
`updateTag()`/`revalidateTag()` on a store that keeps per-isolate memos
|
|
212
227
|
(`CFCacheStore`, `VercelCacheStore`): it carries the fresh-reads cookie
|
|
213
|
-
(`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.
|
|
214
231
|
- Non-200 responses
|
|
215
232
|
- A 200 whose render reported an error: a component that threw after the
|
|
216
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
|
|
|
@@ -64,11 +64,29 @@ export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) =
|
|
|
64
64
|
|
|
65
65
|
## Navigation Behavior
|
|
66
66
|
|
|
67
|
-
| Navigation Type
|
|
68
|
-
|
|
|
69
|
-
| Click link `/shop/product/abc`
|
|
70
|
-
| Direct URL `/shop/product/abc
|
|
71
|
-
|
|
|
67
|
+
| Navigation Type | What Renders |
|
|
68
|
+
| ----------------------------------------------------- | ---------------------------------------------------- |
|
|
69
|
+
| Click link `/shop/product/abc` | `<ProductModal />` in `@modal`, background preserved |
|
|
70
|
+
| Direct URL `/shop/product/abc`, or a reload | Full `<ProductPage />` page |
|
|
71
|
+
| Back from the open intercept | The intercept closes; the source page stays |
|
|
72
|
+
| Back/Forward onto the intercept's entry | The intercept over its source page |
|
|
73
|
+
| The same, uncached, from a page other than the source | Full `<ProductPage />` page, as a document load |
|
|
74
|
+
|
|
75
|
+
Only a client navigation from the source page shows an intercept. Back/Forward
|
|
76
|
+
restores an intercept's entry from the history cache (`initBrowserApp`'s
|
|
77
|
+
`storeOptions.cacheSize`, 20 by default), or fetches it when its source page is
|
|
78
|
+
on screen. From any other page (after a reload, or once the entry was evicted)
|
|
79
|
+
it costs a full page load, from the top, which drops `clearOnReload` location
|
|
80
|
+
state like any document load, and the entry is a full page from then on. In
|
|
81
|
+
dev, an edit that changes the app version clears the history cache, so the
|
|
82
|
+
next Back/Forward onto an intercept entry takes that path.
|
|
83
|
+
|
|
84
|
+
A server action on `/shop` that calls `redirect("/shop/product/abc")` opens the
|
|
85
|
+
intercept too, and so does a link from `/shop` to a route that redirects
|
|
86
|
+
there. The page under it is the browser's copy, as on a link click: it
|
|
87
|
+
keeps its last render, so it shows its pre-action data until the intercept
|
|
88
|
+
closes. An action submitted from inside an open intercept leaves that page the
|
|
89
|
+
same way.
|
|
72
90
|
|
|
73
91
|
## Intercept with Layout
|
|
74
92
|
|
|
@@ -146,6 +164,18 @@ the modal keeps the data of the others, as it does on a link to another
|
|
|
146
164
|
intercepted route that reads the same loader. In `clientUrls()` the predicate
|
|
147
165
|
runs on the server too (`/client-urls`).
|
|
148
166
|
|
|
167
|
+
The page under an intercept does not re-render while the intercept is on
|
|
168
|
+
screen. An action that settles after the intercept opened (one sent from the
|
|
169
|
+
page underneath just before the user opened it) runs no background
|
|
170
|
+
revalidation. The page underneath keeps its data until the user leaves the
|
|
171
|
+
intercept. Leaving it with Back revalidates that page as stale (the
|
|
172
|
+
action marked the history cache stale): loaders that opt in with
|
|
173
|
+
`revalidate(({ stale }) => stale)` re-run. Going Back or Forward out of the
|
|
174
|
+
intercept while an action from it is in flight does not abort the action:
|
|
175
|
+
`useAction()` gets its result, and the page underneath gets the background
|
|
176
|
+
revalidation (`/server-actions`, "When the user navigates while the action
|
|
177
|
+
runs").
|
|
178
|
+
|
|
149
179
|
A `revalidate()` placed directly in the intercept's `use()` (or returned from
|
|
150
180
|
its handler's `.use`) throws at definition time. Attach it to the loader:
|
|
151
181
|
|
|
@@ -177,7 +207,7 @@ import type { Revalidate } from "@rangojs/router";
|
|
|
177
207
|
import * as ProductActions from "./actions/product";
|
|
178
208
|
|
|
179
209
|
export const revalidateProductShell: Revalidate = (ctx) =>
|
|
180
|
-
ctx.isAction(ProductActions) || undefined;
|
|
210
|
+
ctx.stale || ctx.isAction(ProductActions) || undefined;
|
|
181
211
|
|
|
182
212
|
layout(ProductLayout, () => [
|
|
183
213
|
revalidate(revalidateProductShell), // producer reruns
|
|
@@ -340,9 +370,14 @@ below), and `cache()` onto the target route.
|
|
|
340
370
|
|
|
341
371
|
To cache a modal, put `cache()` on the target route (or a layout above it).
|
|
342
372
|
An intercept navigation is cached under that scope with its own `intercept:`
|
|
343
|
-
key, so it never collides with the full-page render of the same URL.
|
|
344
|
-
|
|
345
|
-
|
|
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()])`.
|
|
346
381
|
|
|
347
382
|
## Errors in an Intercept
|
|
348
383
|
|
|
@@ -433,7 +468,9 @@ contract and direction-aware examples.
|
|
|
433
468
|
## Interaction with Prerender
|
|
434
469
|
|
|
435
470
|
When the target route of an intercept uses `Prerender`, the intercept handler is
|
|
436
|
-
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.
|
|
437
474
|
This means intercept navigations to pre-rendered routes are served from the
|
|
438
475
|
prerender store without executing handler code at runtime.
|
|
439
476
|
|
|
@@ -466,13 +503,16 @@ Build-time behavior:
|
|
|
466
503
|
- The intercept handler (`<ProductModal />`) is resolved with BuildContext
|
|
467
504
|
- Result is stored under the key `"detail/paramHash/i"` (intercept variant)
|
|
468
505
|
- `when` config conditions are skipped at build time (all intercepts pre-rendered unconditionally)
|
|
469
|
-
- `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
|
|
470
510
|
|
|
471
511
|
Runtime behavior:
|
|
472
512
|
|
|
473
|
-
- 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
|
|
474
514
|
- Direct navigation: prerender store serves the main variant (full page)
|
|
475
|
-
- 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
|
|
476
516
|
|
|
477
517
|
Loaders inside the intercept always run fresh at request time, same as regular
|
|
478
518
|
pre-rendered routes.
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -238,7 +238,8 @@ A standalone `layout()` renders when it is first mounted and is then kept as-is:
|
|
|
238
238
|
params auto-filled) keep the old params too; render param-dependent nav in a
|
|
239
239
|
client component with `useReverse(routes)` (`/links`).
|
|
240
240
|
- **Actions** skip it too (the revalidation trace calls this
|
|
241
|
-
`action:parent-chain-skip`)
|
|
241
|
+
`action:parent-chain-skip`), and so does Back/Forward onto a page an action
|
|
242
|
+
made stale (`stale:parent-chain-skip`).
|
|
242
243
|
|
|
243
244
|
Orphan layouts inside a `path()` are the opposite: they belong to the route
|
|
244
245
|
entry and re-render with it (on param/search changes and on every action).
|
|
@@ -249,9 +250,9 @@ or `undefined` to defer to the default:
|
|
|
249
250
|
```typescript
|
|
250
251
|
import * as CartActions from "./actions/cart";
|
|
251
252
|
|
|
252
|
-
// Re-render after any cart action
|
|
253
|
+
// Re-render after any cart action, and on Back after an action
|
|
253
254
|
layout(<CartLayout />, () => [
|
|
254
|
-
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
255
|
+
revalidate((ctx) => ctx.isAction(CartActions) || ctx.stale || undefined),
|
|
255
256
|
|
|
256
257
|
path("/cart", CartPage, { name: "cart" }),
|
|
257
258
|
])
|
|
@@ -292,7 +293,7 @@ import type { Revalidate } from "@rangojs/router";
|
|
|
292
293
|
import { addToCart } from "./actions/cart";
|
|
293
294
|
|
|
294
295
|
export const revalidateCartData: Revalidate = (ctx) =>
|
|
295
|
-
ctx.isAction(addToCart) || undefined;
|
|
296
|
+
ctx.stale || ctx.isAction(addToCart) || undefined;
|
|
296
297
|
```
|
|
297
298
|
|
|
298
299
|
```typescript
|
|
@@ -305,8 +306,11 @@ layout(<CartLayout />, () => [
|
|
|
305
306
|
```
|
|
306
307
|
|
|
307
308
|
On the layout the contract adds the re-render the default skips after an
|
|
308
|
-
action
|
|
309
|
-
|
|
309
|
+
action, and on Back/Forward onto a page an action made stale (`ctx.stale`).
|
|
310
|
+
The route segment already re-runs on both, so there it changes nothing by
|
|
311
|
+
default; the shared name documents the dependency. Leave out `ctx.stale` and
|
|
312
|
+
Back after any action re-runs the page without its layout: the page reads
|
|
313
|
+
`undefined`.
|
|
310
314
|
|
|
311
315
|
If a segment depends on multiple upstream domains, compose multiple
|
|
312
316
|
contracts (`revalidateAuthData`, `revalidateCartData`, and so on).
|
|
@@ -319,7 +323,7 @@ import { revalidate, type Revalidate } from "@rangojs/router";
|
|
|
319
323
|
import * as AuthActions from "./actions/auth";
|
|
320
324
|
|
|
321
325
|
export const revalidateAuthData: Revalidate = (ctx) =>
|
|
322
|
-
ctx.isAction(AuthActions) || undefined;
|
|
326
|
+
ctx.stale || ctx.isAction(AuthActions) || undefined;
|
|
323
327
|
export const revalidateAuth = () => [revalidate(revalidateAuthData)];
|
|
324
328
|
```
|
|
325
329
|
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -396,12 +396,26 @@ Segment defaults when no revalidator decides (from
|
|
|
396
396
|
`src/router/revalidation.ts`). A segment the client does not hold yet always
|
|
397
397
|
renders.
|
|
398
398
|
|
|
399
|
-
| Segment | After an action (POST) | Navigation |
|
|
400
|
-
| -------------------------------------------------------------------- |
|
|
401
|
-
| Route (`path()`) | `true`
|
|
402
|
-
| Owned by the route (loaders, layouts, parallels inside the `path()`) | `true`
|
|
403
|
-
| Loader above the route (e.g. on a parent `layout()`) | `true`
|
|
404
|
-
| Layout or parallel above the route | `false`
|
|
399
|
+
| Segment | After an action (POST), or a stale Back/Forward | Navigation |
|
|
400
|
+
| -------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------- |
|
|
401
|
+
| Route (`path()`) | `true` | `true` when params or search change |
|
|
402
|
+
| Owned by the route (loaders, layouts, parallels inside the `path()`) | `true` | `true` when params or search change |
|
|
403
|
+
| Loader above the route (e.g. on a parent `layout()`) | `true` | `false` |
|
|
404
|
+
| Layout or parallel above the route | `false` | `false` |
|
|
405
|
+
|
|
406
|
+
A stale Back/Forward is the background revalidation of a page a server action
|
|
407
|
+
(in this tab or another) marked stale: Back renders the cached page, then
|
|
408
|
+
revalidates it with `stale: true`. It takes an action's defaults, so a layout's
|
|
409
|
+
cart count shows the write with no opt-in. It renders live, as an action does:
|
|
410
|
+
no route-cache record or PPR replay answers it. It is not an action:
|
|
411
|
+
`isAction()` is false and `actionId` is undefined. To keep a loader as it was
|
|
412
|
+
on that request:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
loader(RecommendationsLoader, () => [
|
|
416
|
+
revalidate(({ stale }) => (stale ? false : undefined)),
|
|
417
|
+
]);
|
|
418
|
+
```
|
|
405
419
|
|
|
406
420
|
#### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
|
|
407
421
|
|
|
@@ -421,9 +435,10 @@ Pick the idiom by what the segment's default already does (table above). After
|
|
|
421
435
|
an action, loaders, the route, and segments inside the `path()` already re-run;
|
|
422
436
|
layouts and parallels above the route are skipped:
|
|
423
437
|
|
|
424
|
-
- **Add a signal where the default skips.**
|
|
425
|
-
|
|
426
|
-
|
|
438
|
+
- **Add a signal where the default skips.**
|
|
439
|
+
`ctx.isAction(X) || ctx.stale || undefined` on a parent layout or a
|
|
440
|
+
layout-level parallel re-renders it after `X` and on Back after an action,
|
|
441
|
+
and defers otherwise. On a loader, the route, or a segment inside the `path()` it
|
|
427
442
|
changes nothing: they already re-run after every action, and on navigation it
|
|
428
443
|
defers.
|
|
429
444
|
- **Narrow after actions where the default re-runs.**
|
|
@@ -487,10 +502,12 @@ the same named revalidation contract across producer and consumer segments.
|
|
|
487
502
|
import type { Revalidate } from "@rangojs/router";
|
|
488
503
|
import * as AccountActions from "./actions/account";
|
|
489
504
|
|
|
490
|
-
// Match by reference with ctx.isAction() (rename-safe),
|
|
491
|
-
//
|
|
505
|
+
// Match by reference with ctx.isAction() (rename-safe), true on a stale
|
|
506
|
+
// Back/Forward (the page re-runs, so its producer must), and defer
|
|
507
|
+
// (|| undefined) so these contracts compose — a hard `false` would
|
|
508
|
+
// short-circuit the rest.
|
|
492
509
|
export const revalidateAccountScope: Revalidate = (ctx) =>
|
|
493
|
-
ctx.isAction(AccountActions) || undefined;
|
|
510
|
+
ctx.stale || ctx.isAction(AccountActions) || undefined;
|
|
494
511
|
|
|
495
512
|
// urls.tsx
|
|
496
513
|
layout(AccountLayout, () => [
|
|
@@ -955,7 +972,7 @@ function ProductPage() {
|
|
|
955
972
|
|
|
956
973
|
A skeleton that has appeared stays on screen for at least about 300 ms before React replaces it with what it was waiting for: React does not replace a fallback sooner than 300 ms after the last fallback committed ([#1078](https://github.com/rangojs/rango/issues/1078)). On a cold click that mounts the `loading()` boundary, a loader that finishes sooner than that still shows its skeleton for about 300 ms, while a slower loader is not delayed. The router leaves this to React: an early skeleton is preferred to holding the click, and a fully prefetched click commits complete and shows none. A nested `loading()` whose data lands after the outer one has been revealed shows its own fallback, and the 300 ms starts again. The loaders feature page on the docs site has the measured figures. Under `transition()` a related cost applies; see [view-transitions](../view-transitions/SKILL.md).
|
|
957
974
|
|
|
958
|
-
A loader that throws (anything but `notFound()` or `redirect()`) throws at its `useLoader` read on every client lane: a plain or fully prefetched click, Back/Forward restored from the history cache, and the rerun after a server action. A prefetch in which a loader threw is never used, in memory or from the browser's HTTP cache: the click asks the server again, so a loader that has recovered shows its data. The nearest boundary to the read catches it (your React error boundary around the read, else the route's `errorBoundary()`, else the router's root one), and the layouts above keep their instance. The background refresh after Back/Forward onto a stale page is the exception: a loader that fails there leaves the page on screen and logs `[Browser] Background revalidation failed`. Not covered: an `intercept()`'s loaders, and a parallel slot's loaders under its own `loading()`, which are decoded above the slot's content.
|
|
975
|
+
A loader that throws (anything but `notFound()` or `redirect()`) throws at its `useLoader` read on every client lane: a plain or fully prefetched click, Back/Forward restored from the history cache, and the rerun after a server action. A prefetch in which a loader threw is never used, in memory or from the browser's HTTP cache: the click asks the server again, so a loader that has recovered shows its data. The nearest boundary to the read catches it (your React error boundary around the read, else the route's `errorBoundary()`, else the router's root one), and the layouts above keep their instance. The background refresh after Back/Forward onto a stale page is the exception: it re-runs every loader on the defaults (see the defaults table), and a loader that fails there leaves the page on screen as it was and logs `[Browser] Background revalidation failed`. Not covered: an `intercept()`'s loaders, and a parallel slot's loaders under its own `loading()`, which are decoded above the slot's content.
|
|
959
976
|
|
|
960
977
|
## Loader Authority: notFound() and redirect()
|
|
961
978
|
|
|
@@ -90,7 +90,7 @@ import type { Revalidate } from "@rangojs/router";
|
|
|
90
90
|
import * as CartActions from "./actions/cart";
|
|
91
91
|
|
|
92
92
|
export const revalidateCartData: Revalidate = (ctx) =>
|
|
93
|
-
ctx.isAction(CartActions) || undefined;
|
|
93
|
+
ctx.stale || ctx.isAction(CartActions) || undefined;
|
|
94
94
|
|
|
95
95
|
layout(CartLayout, () => [
|
|
96
96
|
middleware(cartRenderMiddleware),
|
|
@@ -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:
|
|
@@ -633,10 +633,11 @@ re-rendering:
|
|
|
633
633
|
```typescript
|
|
634
634
|
import { updateBlog } from "./actions/blog";
|
|
635
635
|
|
|
636
|
-
// Re-run this layout when a blog action fires
|
|
637
|
-
// skipped after actions by default; undefined
|
|
636
|
+
// Re-run this layout when a blog action fires, and on Back after an action
|
|
637
|
+
// (layouts above the route are skipped after actions by default; undefined
|
|
638
|
+
// defers otherwise)
|
|
638
639
|
layout(BlogLayout, () => [
|
|
639
|
-
revalidate((ctx) => ctx.isAction(updateBlog) || undefined),
|
|
640
|
+
revalidate((ctx) => ctx.isAction(updateBlog) || ctx.stale || undefined),
|
|
640
641
|
path("/blog/:slug", BlogPost, { name: "blogPost" }),
|
|
641
642
|
]);
|
|
642
643
|
|
|
@@ -860,7 +861,7 @@ Server actions work the same way — `"use server"` directive, `useActionState`,
|
|
|
860
861
|
|
|
861
862
|
Key difference: in Rango, route middleware does NOT wrap action execution. Actions only see global middleware context. Use `getRequestContext()` in actions to access `ctx.set()`/`ctx.get()`.
|
|
862
863
|
|
|
863
|
-
Next.js's `revalidateTag()` maps directly: tag entries via `cache({ tags })` / `cacheTag(...)`, then invalidate. **In a server action use `await updateTag(tag)`** — it is read-your-own-writes, so the action's own re-render sees fresh data, and it rejects when the store's durable write fails; `revalidateTag(tag)` is a background (non-blocking) hard-purge for route handlers / webhooks. The request that calls `revalidateTag()` also reads its own writes, but a failed durable write only reaches `onError`. `revalidatePath()` has no path-based equivalent — tag the route's entries instead. Separately, the route, the segments inside its `path()`, and all loaders already re-run after every action; layouts and parallels above the route are skipped. To re-render one of those skipped segments after an action, attach `revalidate((ctx) => ctx.isAction(updateBlog) || undefined)` to it; to narrow a loader or route segment to one action, use `revalidate((ctx) => (ctx.isAction() ? ctx.isAction(updateBlog) : undefined))` (match the imported action by reference, as in §3). See `/server-actions` for the full pattern (validation, error handling, file uploads), `/caching` for tag invalidation, and `/loader` for revalidation rule semantics.
|
|
864
|
+
Next.js's `revalidateTag()` maps directly: tag entries via `cache({ tags })` / `cacheTag(...)`, then invalidate. **In a server action use `await updateTag(tag)`** — it is read-your-own-writes, so the action's own re-render sees fresh data, and it rejects when the store's durable write fails; `revalidateTag(tag)` is a background (non-blocking) hard-purge for route handlers / webhooks. The request that calls `revalidateTag()` also reads its own writes, but a failed durable write only reaches `onError`. `revalidatePath()` has no path-based equivalent — tag the route's entries instead. Separately, the route, the segments inside its `path()`, and all loaders already re-run after every action; layouts and parallels above the route are skipped. To re-render one of those skipped segments after an action, and on Back after one, attach `revalidate((ctx) => ctx.isAction(updateBlog) || ctx.stale || undefined)` to it; to narrow a loader or route segment to one action, use `revalidate((ctx) => (ctx.isAction() ? ctx.isAction(updateBlog) : undefined))` (match the imported action by reference, as in §3). See `/server-actions` for the full pattern (validation, error handling, file uploads), `/caching` for tag invalidation, and `/loader` for revalidation rule semantics.
|
|
864
865
|
|
|
865
866
|
## 8. Metadata / Head
|
|
866
867
|
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -392,8 +392,9 @@ parallel(
|
|
|
392
392
|
() => [
|
|
393
393
|
loader(CartLoader),
|
|
394
394
|
// Layout-level slot (kept after actions by default): also re-render after
|
|
395
|
-
// cart actions. Under a path() the slot
|
|
396
|
-
|
|
395
|
+
// cart actions and on Back after an action. Under a path() the slot
|
|
396
|
+
// re-renders after every action anyway.
|
|
397
|
+
revalidate((ctx) => ctx.isAction(CartActions) || ctx.stale || undefined),
|
|
397
398
|
]
|
|
398
399
|
)
|
|
399
400
|
```
|
|
@@ -456,7 +457,7 @@ import type { Revalidate } from "@rangojs/router";
|
|
|
456
457
|
import * as CartActions from "./actions/cart";
|
|
457
458
|
|
|
458
459
|
export const revalidateCartData: Revalidate = (ctx) =>
|
|
459
|
-
ctx.isAction(CartActions) || undefined;
|
|
460
|
+
ctx.stale || ctx.isAction(CartActions) || undefined;
|
|
460
461
|
|
|
461
462
|
layout(CartLayout, () => [
|
|
462
463
|
revalidate(revalidateCartData), // producer reruns
|
|
@@ -575,7 +576,7 @@ export const shopPatterns = urls(({
|
|
|
575
576
|
() => [
|
|
576
577
|
loader(CartLoader),
|
|
577
578
|
loading(<CartSkeleton />),
|
|
578
|
-
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
579
|
+
revalidate((ctx) => ctx.isAction(CartActions) || ctx.stale || undefined),
|
|
579
580
|
]
|
|
580
581
|
),
|
|
581
582
|
|
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
|
|
@@ -482,6 +482,7 @@ The bounded bypass tokens, grouped by when they are decided:
|
|
|
482
482
|
| Token | Decided | Meaning |
|
|
483
483
|
| ----------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
484
484
|
| `method`, `dynamic`, `nonce`, `store-unavailable`, `passive-read-unsupported` | pre-read | request/route/store ineligible for replay |
|
|
485
|
+
| `stale` | pre-read | a stale request (`_rsc_stale`, Back/Forward onto a page an action made stale) renders live, as an action does: the record may predate the write |
|
|
485
486
|
| `no-navigation-context` | pre-read | no `X-RSC-Router-Client-Path` and no same-origin `Referer`; a partial match is impossible |
|
|
486
487
|
| `prerender-store` | pre-read or match | `Prerender()` route served by its baked artifact (pre-read probe of the normal variant; reclassified post-match when the store actually served, either variant) |
|
|
487
488
|
| `intercept` | match | the navigation resolved to an intercept — replay is never armed for intercepts (they keep their normal cache path); no heal capture |
|
|
@@ -508,7 +509,9 @@ In handlers it is
|
|
|
508
509
|
too late to prevent a MISS render from already happening, but it still prevents
|
|
509
510
|
the follow-up shell capture. A `ctx.dynamic()` call that only happens during the
|
|
510
511
|
capture itself (for example inside a handler promise, after an await) refuses
|
|
511
|
-
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.
|
|
512
515
|
|
|
513
516
|
During `Prerender` + `ppr` build-shell capture, middleware is replayed with
|
|
514
517
|
`ctx.build === true`, `ctx.waitUntil()` inert, and the same `ctx.dynamic()`
|
|
@@ -1176,7 +1179,7 @@ their data should bake vs stay live. Your levers, in order of preference:
|
|
|
1176
1179
|
3. **Guaranteed-fresh widgets: live loaders with a widget-sized boundary.**
|
|
1177
1180
|
An unflagged loader read under an inline `<Suspense>` in the widget is
|
|
1178
1181
|
enough. Alternatively, a parallel slot with its OWN `loading()`:
|
|
1179
|
-
parallel-owned loaders get their own per-slot boundary (`
|
|
1182
|
+
parallel-owned loaders get their own per-slot boundary (`resolveParallelSegments` tags
|
|
1180
1183
|
them with the slot's loading; `segment-system.tsx` builds a per-slot
|
|
1181
1184
|
LoaderBoundary), so the chrome bakes into the shell and each widget is an
|
|
1182
1185
|
independent, widget-sized hole:
|
|
@@ -1632,6 +1635,19 @@ evicted by tag at all — move always-fresh data into a live-lane loader (no
|
|
|
1632
1635
|
capture is served as before. Only a throw during the capture counts: a
|
|
1633
1636
|
component that failed in the live render and succeeds when captured is
|
|
1634
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.
|
|
1635
1651
|
|
|
1636
1652
|
## Related
|
|
1637
1653
|
|