@rangojs/router 0.26.6 → 0.27.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/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 +18 -0
- package/dist/types/browser/history-state.d.ts +4 -0
- package/dist/types/browser/intercept-utils.d.ts +23 -0
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/prefetch-markers.d.ts +12 -5
- package/dist/types/router/match-context.d.ts +8 -0
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
- package/dist/types/router/revalidation.d.ts +6 -1
- package/dist/types/router/router-context.d.ts +2 -17
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/testing/serve-shell-request.d.ts +7 -0
- package/dist/types/types/handler-context.d.ts +9 -6
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/client-urls/SKILL.md +2 -1
- package/skills/document-cache/SKILL.md +5 -0
- package/skills/intercept/SKILL.md +36 -6
- package/skills/layout/SKILL.md +11 -7
- package/skills/loader/SKILL.md +30 -13
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +5 -4
- package/skills/parallel/SKILL.md +5 -4
- package/skills/ppr/SKILL.md +1 -0
- package/skills/rango/SKILL.md +18 -10
- package/skills/server-actions/SKILL.md +43 -5
- package/skills/testing/predicates.md +1 -1
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/ux-architecture/SKILL.md +3 -2
- package/src/browser/action-coordinator.ts +2 -13
- package/src/browser/action-fence.ts +11 -3
- package/src/browser/event-controller.ts +37 -0
- package/src/browser/history-state.ts +9 -1
- package/src/browser/intercept-utils.ts +51 -0
- package/src/browser/navigation-bridge.ts +20 -17
- package/src/browser/navigation-client.ts +5 -10
- package/src/browser/navigation-transaction.ts +3 -10
- package/src/browser/partial-update.ts +8 -0
- package/src/browser/rsc-router.tsx +16 -2
- package/src/browser/server-action-bridge.ts +223 -78
- package/src/cache/document-cache.ts +12 -6
- package/src/prefetch-markers.ts +13 -5
- package/src/router/match-context.ts +13 -0
- package/src/router/match-middleware/cache-lookup.ts +21 -10
- package/src/router/match-middleware/cache-store.ts +10 -4
- package/src/router/match-result.ts +1 -20
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +33 -7
- package/src/router/router-context.ts +4 -22
- package/src/rsc/rsc-rendering.ts +12 -4
- package/src/rsc/shell-capture-constants.ts +2 -0
- package/src/ssr/ssr-root.tsx +1 -0
- package/src/testing/serve-shell-request.ts +20 -2
- package/src/types/handler-context.ts +9 -6
|
@@ -93,7 +93,11 @@
|
|
|
93
93
|
*/
|
|
94
94
|
import type { InternalHandlerContext, ResolvedSegment } from "../../types.js";
|
|
95
95
|
import { isPprEntry, type EntryData } from "../../server/context.js";
|
|
96
|
-
import
|
|
96
|
+
import {
|
|
97
|
+
rendersLive,
|
|
98
|
+
type MatchContext,
|
|
99
|
+
type MatchPipelineState,
|
|
100
|
+
} from "../match-context.js";
|
|
97
101
|
import { getRouterContext, type RouterContext } from "../router-context.js";
|
|
98
102
|
import { observeEvent } from "../instrument.js";
|
|
99
103
|
import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
|
|
@@ -280,9 +284,9 @@ async function* yieldFromStore<TEnv>(
|
|
|
280
284
|
/**
|
|
281
285
|
* The entry may be newer than the client's copy: an on-demand overlay entry
|
|
282
286
|
* that a refresh (possibly inside this very action) replaced. On an action
|
|
283
|
-
* the route's own segments are then re-sent, as a live
|
|
284
|
-
* re-sends them (revalidation.ts "action:route-segment");
|
|
285
|
-
* bundled manifest keeps the client's copy.
|
|
287
|
+
* or a stale request the route's own segments are then re-sent, as a live
|
|
288
|
+
* action re-render re-sends them (revalidation.ts "action:route-segment");
|
|
289
|
+
* the immutable bundled manifest keeps the client's copy.
|
|
286
290
|
*/
|
|
287
291
|
replaceable?: boolean,
|
|
288
292
|
): AsyncGenerator<ResolvedSegment> {
|
|
@@ -348,7 +352,9 @@ async function* yieldFromStore<TEnv>(
|
|
|
348
352
|
// different content, so we must NOT nullify.
|
|
349
353
|
const paramsChanged =
|
|
350
354
|
!ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
|
|
351
|
-
|
|
355
|
+
// The action defaults' route segment (revalidation.ts), not a cache skip.
|
|
356
|
+
const resendRouteSegments =
|
|
357
|
+
replaceable === true && (ctx.isAction || ctx.stale);
|
|
352
358
|
for (const segment of segments) {
|
|
353
359
|
if (interceptFillSkips(ctx, segment)) continue;
|
|
354
360
|
if (!ctx.isFullMatch && ctx.clientSegmentSet.has(segment.id)) {
|
|
@@ -679,8 +685,9 @@ export function withCacheLookup<TEnv>(
|
|
|
679
685
|
// its result is applied client-side via useActionState). A plain on-demand
|
|
680
686
|
// route is the same case, served from the overlay first (an overlay entry
|
|
681
687
|
// is newer than the build one, and may be the only one). Passthrough
|
|
682
|
-
// routes keep a liveHandler, so
|
|
683
|
-
|
|
688
|
+
// routes keep a liveHandler, so a live render (rendersLive) re-renders
|
|
689
|
+
// them fresh.
|
|
690
|
+
if (!rendersLive(ctx) || !isPassthroughPrerenderRoute) {
|
|
684
691
|
await ensurePrerenderDeps();
|
|
685
692
|
if (prerenderStoreInstance || prerenderOverlay) {
|
|
686
693
|
const served = yield* tryPrerenderLookup(
|
|
@@ -698,7 +705,11 @@ export function withCacheLookup<TEnv>(
|
|
|
698
705
|
}
|
|
699
706
|
}
|
|
700
707
|
|
|
701
|
-
if (
|
|
708
|
+
if (
|
|
709
|
+
!rendersLive(ctx) &&
|
|
710
|
+
!ctx.matched.pr &&
|
|
711
|
+
globalThis.__PRERENDER_DEV_URL
|
|
712
|
+
) {
|
|
702
713
|
const hasStatic = ctx.entries.some(
|
|
703
714
|
(e) =>
|
|
704
715
|
(e.type === "layout" ||
|
|
@@ -737,7 +748,7 @@ export function withCacheLookup<TEnv>(
|
|
|
737
748
|
// pushes of segments the response does not carry. withCacheStore skips
|
|
738
749
|
// the write for the same request.
|
|
739
750
|
if (
|
|
740
|
-
ctx
|
|
751
|
+
rendersLive(ctx) ||
|
|
741
752
|
!ctx.cacheScope?.enabled ||
|
|
742
753
|
!recordUse(ctx.handlerContext).read
|
|
743
754
|
) {
|
|
@@ -1004,7 +1015,7 @@ export function withCacheLookup<TEnv>(
|
|
|
1004
1015
|
routeKey: ctx.routeKey,
|
|
1005
1016
|
context: ctx.handlerContext,
|
|
1006
1017
|
actionContext: ctx.actionContext,
|
|
1007
|
-
|
|
1018
|
+
cacheStale: cacheResult.shouldRevalidate,
|
|
1008
1019
|
traceSource: "cache-hit",
|
|
1009
1020
|
});
|
|
1010
1021
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* +---------------------+
|
|
20
20
|
* | Should skip cache? |
|
|
21
21
|
* | - !cacheScope |──yes──> return
|
|
22
|
-
* | -
|
|
22
|
+
* | - rendersLive |
|
|
23
23
|
* | - cacheHit |
|
|
24
24
|
* | - method !== GET |
|
|
25
25
|
* +---------------------+
|
|
@@ -105,7 +105,11 @@ import {
|
|
|
105
105
|
getRequestContext,
|
|
106
106
|
type RequestContext,
|
|
107
107
|
} from "../../server/request-context.js";
|
|
108
|
-
import
|
|
108
|
+
import {
|
|
109
|
+
rendersLive,
|
|
110
|
+
type MatchContext,
|
|
111
|
+
type MatchPipelineState,
|
|
112
|
+
} from "../match-context.js";
|
|
109
113
|
import { createShellImplicitDocScope } from "../../cache/cache-scope.js";
|
|
110
114
|
import { recordSegmentTags } from "../../cache/cache-tag.js";
|
|
111
115
|
import { executionStart } from "../../cache/tag-invalidation.js";
|
|
@@ -155,7 +159,7 @@ export function withCacheStore<TEnv>(
|
|
|
155
159
|
// and could only write an incomplete one.
|
|
156
160
|
if (
|
|
157
161
|
!ctx.cacheScope?.enabled ||
|
|
158
|
-
ctx
|
|
162
|
+
rendersLive(ctx) ||
|
|
159
163
|
state.cacheHit ||
|
|
160
164
|
ctx.request.method !== "GET" ||
|
|
161
165
|
!recordUse(ctx.handlerContext).write
|
|
@@ -355,7 +359,9 @@ function recordShellCaptureDocRecord<TEnv>(
|
|
|
355
359
|
if (!marker?.store) return;
|
|
356
360
|
const scope = ctx.cacheScope;
|
|
357
361
|
if (!scope || scope.isShellImplicitDocScope) return;
|
|
358
|
-
if (ctx
|
|
362
|
+
if (rendersLive(ctx) || ctx.isIntercept || ctx.request.method !== "GET") {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
359
365
|
if (state.cacheSource === "prerender") return;
|
|
360
366
|
if (!scope.allowsCache("write")) return;
|
|
361
367
|
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
*/
|
|
110
110
|
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
111
111
|
import { isPlaceholder } from "../types/segments.js";
|
|
112
|
-
import {
|
|
112
|
+
import { interceptMatched } from "../browser/intercept-utils.js";
|
|
113
113
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
114
114
|
import { debugLog } from "./logging.js";
|
|
115
115
|
import { appendMetric } from "./metrics.js";
|
|
@@ -196,25 +196,6 @@ function deduplicateLoaderSegments(
|
|
|
196
196
|
return { segments: result, removedIds };
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
/**
|
|
200
|
-
* An intercept's `matched`: the client's ids without any of the slot's (its
|
|
201
|
-
* id and every loader segment on it, another intercept's into the same slot
|
|
202
|
-
* included), then the slot's (interceptSegmentIds). One order for the
|
|
203
|
-
* prefetch, its fill, an action and a revisit: a fill whose `matched` differs
|
|
204
|
-
* from its tree's is rendered as a new tree (browser/partial-update.ts
|
|
205
|
-
* fillHoles).
|
|
206
|
-
*/
|
|
207
|
-
function interceptMatched(
|
|
208
|
-
clientIds: readonly string[],
|
|
209
|
-
slotIds: readonly string[],
|
|
210
|
-
): string[] {
|
|
211
|
-
// loaderParentId leaves a non-loader id as it is: the slot's own id goes too.
|
|
212
|
-
return [
|
|
213
|
-
...clientIds.filter((id) => loaderParentId(id) !== slotIds[0]),
|
|
214
|
-
...slotIds,
|
|
215
|
-
];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
199
|
export function buildMatchResult<TEnv>(
|
|
219
200
|
allSegments: ResolvedSegment[],
|
|
220
201
|
ctx: MatchContext<TEnv>,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isLoaderSegmentId } from "../browser/react/filter-segment-order.js";
|
|
2
2
|
import { requestHeaders } from "../server/request-headers.js";
|
|
3
|
+
import { STALE_PARAM } from "../prefetch-markers.js";
|
|
3
4
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
4
5
|
|
|
5
6
|
export interface NavigationSnapshot {
|
|
@@ -134,7 +135,7 @@ export async function resolveNavigation(
|
|
|
134
135
|
): Promise<NavigationSnapshot | null> {
|
|
135
136
|
const clientSegmentIds =
|
|
136
137
|
url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
|
|
137
|
-
const stale = url.searchParams.get(
|
|
138
|
+
const stale = url.searchParams.get(STALE_PARAM) === "true";
|
|
138
139
|
const previousUrl = getNavigationContextHeader(request, url.origin);
|
|
139
140
|
const interceptSourceUrl = getInterceptSourceHeader(request);
|
|
140
141
|
const isHmr = !!requestHeaders(request).get("X-RSC-HMR");
|
|
@@ -24,6 +24,22 @@ import { makeIsAction } from "./is-action.js";
|
|
|
24
24
|
const REVALIDATE_ALWAYS: ShouldRevalidateFn<any, any> = () => true;
|
|
25
25
|
const REVALIDATE_NEVER: ShouldRevalidateFn<any, any> = () => false;
|
|
26
26
|
|
|
27
|
+
/** Trace reasons of the action defaults, per lane: an action, a stale request. */
|
|
28
|
+
const ACTION_DEFAULT_REASONS = {
|
|
29
|
+
action: {
|
|
30
|
+
route: "action:route-segment",
|
|
31
|
+
loader: "action:loader-segment",
|
|
32
|
+
routeChild: "action:belongs-to-route",
|
|
33
|
+
parentChain: "action:parent-chain-skip",
|
|
34
|
+
},
|
|
35
|
+
stale: {
|
|
36
|
+
route: "stale:route-segment",
|
|
37
|
+
loader: "stale:loader-segment",
|
|
38
|
+
routeChild: "stale:belongs-to-route",
|
|
39
|
+
parentChain: "stale:parent-chain-skip",
|
|
40
|
+
},
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
27
43
|
/**
|
|
28
44
|
* The predicate `revalidate(true)` / `revalidate(false)` stands for: the
|
|
29
45
|
* constant hard decision of `revalidate(() => true)` / `() => false`.
|
|
@@ -61,8 +77,13 @@ export interface EvaluateRevalidationOptions<TEnv> {
|
|
|
61
77
|
context: HandlerContext<any, TEnv>;
|
|
62
78
|
/** Action context if triggered by action */
|
|
63
79
|
actionContext?: ActionContext;
|
|
64
|
-
/**
|
|
80
|
+
/** The browser's `_rsc_stale` signal: seeds the action defaults. */
|
|
65
81
|
stale?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* The route-cache entry is past its TTL (SWR). Predicates see it as `stale`;
|
|
84
|
+
* it never seeds the action defaults.
|
|
85
|
+
*/
|
|
86
|
+
cacheStale?: boolean;
|
|
66
87
|
/** Trace source hint for the revalidation trace */
|
|
67
88
|
traceSource?: RevalidationTraceEntry["source"];
|
|
68
89
|
/**
|
|
@@ -100,6 +121,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
100
121
|
context,
|
|
101
122
|
actionContext,
|
|
102
123
|
stale,
|
|
124
|
+
cacheStale,
|
|
103
125
|
traceSource,
|
|
104
126
|
defaultOverride,
|
|
105
127
|
} = options;
|
|
@@ -147,19 +169,23 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
147
169
|
if (defaultOverride) {
|
|
148
170
|
defaultShouldRevalidate = defaultOverride.value;
|
|
149
171
|
defaultReason = defaultOverride.reason;
|
|
150
|
-
} else if (request.method === "POST") {
|
|
172
|
+
} else if (request.method === "POST" || stale) {
|
|
173
|
+
// A stale request re-renders a page an action may have changed after the
|
|
174
|
+
// client cached it. Not an action: no actionContext, isAction() is false.
|
|
175
|
+
const reasons =
|
|
176
|
+
ACTION_DEFAULT_REASONS[request.method === "POST" ? "action" : "stale"];
|
|
151
177
|
if (segment.type === "route") {
|
|
152
178
|
defaultShouldRevalidate = true;
|
|
153
|
-
defaultReason =
|
|
179
|
+
defaultReason = reasons.route;
|
|
154
180
|
} else if (segment.type === "loader") {
|
|
155
181
|
defaultShouldRevalidate = true;
|
|
156
|
-
defaultReason =
|
|
182
|
+
defaultReason = reasons.loader;
|
|
157
183
|
} else if (segment.belongsToRoute) {
|
|
158
184
|
defaultShouldRevalidate = true;
|
|
159
|
-
defaultReason =
|
|
185
|
+
defaultReason = reasons.routeChild;
|
|
160
186
|
} else {
|
|
161
187
|
defaultShouldRevalidate = false;
|
|
162
|
-
defaultReason =
|
|
188
|
+
defaultReason = reasons.parentChain;
|
|
163
189
|
}
|
|
164
190
|
} else {
|
|
165
191
|
if (segment.type === "route") {
|
|
@@ -251,7 +277,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
251
277
|
routeName: toRouteName,
|
|
252
278
|
fromRouteName,
|
|
253
279
|
toRouteName,
|
|
254
|
-
stale,
|
|
280
|
+
stale: cacheStale ? true : stale,
|
|
255
281
|
});
|
|
256
282
|
} catch (error) {
|
|
257
283
|
// A thrown Response is control flow (e.g. `throw redirect(...)`), not a
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
21
21
|
import type { TelemetrySink } from "./telemetry.js";
|
|
22
22
|
import type { ResolveSegmentOptions } from "./segment-resolution.js";
|
|
23
|
+
import type { EvaluateRevalidationOptions } from "./revalidation.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Revalidation context passed to segment resolution
|
|
@@ -166,28 +167,9 @@ export interface RouterContext<TEnv = any> {
|
|
|
166
167
|
gen: AsyncGenerator<T | { __type: "id"; id: string }>,
|
|
167
168
|
) => Promise<{ items: T[]; matchedIds: string[] }>;
|
|
168
169
|
|
|
169
|
-
evaluateRevalidation: (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
getPrevSegment: (() => Promise<ResolvedSegment | undefined>) | null;
|
|
173
|
-
request: Request;
|
|
174
|
-
prevUrl: URL;
|
|
175
|
-
nextUrl: URL;
|
|
176
|
-
revalidations: Array<{ name: string; fn: any }>;
|
|
177
|
-
routeKey: string;
|
|
178
|
-
context: HandlerContext<any, TEnv>;
|
|
179
|
-
actionContext?: any;
|
|
180
|
-
stale?: boolean;
|
|
181
|
-
traceSource?:
|
|
182
|
-
| "segment-resolution"
|
|
183
|
-
| "cache-hit"
|
|
184
|
-
| "loader"
|
|
185
|
-
| "parallel"
|
|
186
|
-
| "orphan-layout"
|
|
187
|
-
| "route-handler"
|
|
188
|
-
| "layout-handler"
|
|
189
|
-
| "intercept-loader";
|
|
190
|
-
}) => Promise<boolean>;
|
|
170
|
+
evaluateRevalidation: (
|
|
171
|
+
params: EvaluateRevalidationOptions<TEnv>,
|
|
172
|
+
) => Promise<boolean>;
|
|
191
173
|
|
|
192
174
|
getRequestContext: () =>
|
|
193
175
|
| {
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -99,7 +99,7 @@ import {
|
|
|
99
99
|
import { contextGet } from "../context-var.js";
|
|
100
100
|
import { rewriteShellAudience, rewriteTrail } from "../server/rewrite-trail.js";
|
|
101
101
|
import { visitorShellKey } from "./shell-capture-constants.js";
|
|
102
|
-
import { PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
102
|
+
import { PREFETCH_HEADER, STALE_PARAM } from "../prefetch-markers.js";
|
|
103
103
|
import {
|
|
104
104
|
isWarmReplace,
|
|
105
105
|
noteWarmShellEvent,
|
|
@@ -1268,11 +1268,14 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1268
1268
|
// carries deferred units answers one page's prefetch, and a fill is the
|
|
1269
1269
|
// second half of one adoption: neither is reused by a shared cache or the
|
|
1270
1270
|
// document cache (which also refuses a payload marked `_payloadDeferred`),
|
|
1271
|
-
// whatever Cache-Control the route set
|
|
1272
|
-
//
|
|
1271
|
+
// whatever Cache-Control the route set; so is a stale partial render, which
|
|
1272
|
+
// a shared cache keyed by URL must not store or serve. A route with no flag
|
|
1273
|
+
// keeps its headers.
|
|
1273
1274
|
if (
|
|
1274
1275
|
reqCtx._payloadDeferred ||
|
|
1275
|
-
(isPartial &&
|
|
1276
|
+
(isPartial &&
|
|
1277
|
+
(reqCtx._requestKind === "fill" ||
|
|
1278
|
+
url.searchParams.get(STALE_PARAM) === "true"))
|
|
1276
1279
|
) {
|
|
1277
1280
|
rscHeaders["cache-control"] = NOT_REUSED;
|
|
1278
1281
|
}
|
|
@@ -1460,6 +1463,11 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1460
1463
|
if (!getNavigationContextHeader(request, url.origin)) {
|
|
1461
1464
|
return runMatch({ outcome: "BYPASS", reason: "no-navigation-context" });
|
|
1462
1465
|
}
|
|
1466
|
+
// A stale partial renders live, as an action does (rendersLive,
|
|
1467
|
+
// router/match-context.ts): the record may predate the action's write.
|
|
1468
|
+
if (url.searchParams.get(STALE_PARAM) === "true") {
|
|
1469
|
+
return runMatch({ outcome: "BYPASS", reason: "stale" });
|
|
1470
|
+
}
|
|
1463
1471
|
// Fragment passthrough (#700), extended to partial replay. Navigation
|
|
1464
1472
|
// context proves the request can be matched; the separate capability header
|
|
1465
1473
|
// proves its payload consumer can expand envelopes. Older clients and the
|
|
@@ -52,6 +52,8 @@ export const PPR_REPLAY_STATUS_HEADER: string = "x-rango-ppr-replay";
|
|
|
52
52
|
*/
|
|
53
53
|
export const PPR_REPLAY_BYPASS_REASONS = [
|
|
54
54
|
"method",
|
|
55
|
+
// A stale request (`_rsc_stale`) renders live, as an action does.
|
|
56
|
+
"stale",
|
|
55
57
|
"dynamic",
|
|
56
58
|
"nonce",
|
|
57
59
|
"store-unavailable",
|
package/src/ssr/ssr-root.tsx
CHANGED
|
@@ -58,7 +58,11 @@ import {
|
|
|
58
58
|
SHELL_CAPTURE_MAX_WAIT_MS,
|
|
59
59
|
} from "../rsc/shell-capture-constants.js";
|
|
60
60
|
import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../segment-fragments.js";
|
|
61
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
FILL_PARAM,
|
|
63
|
+
PREFETCH_HEADER,
|
|
64
|
+
STALE_PARAM,
|
|
65
|
+
} from "../prefetch-markers.js";
|
|
62
66
|
import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
|
|
63
67
|
import { withoutPushedBy } from "../handles/pushed-by.js";
|
|
64
68
|
import type { HandleData } from "../server/handle-store.js";
|
|
@@ -105,6 +109,12 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
105
109
|
* that source. `rewriteTarget` sends the `metadata.rewriteTarget` the page at
|
|
106
110
|
* `from` carried when middleware rewrote it (`_rsc_rt`), as the browser
|
|
107
111
|
* does: the server then diffs against the route that rendered that page.
|
|
112
|
+
* `stale: true` sends a stale request (`_rsc_stale`), as the browser sends
|
|
113
|
+
* it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`)
|
|
114
|
+
* and segments take an action's revalidation defaults; it renders live, as
|
|
115
|
+
* an action does, so neither the route cache nor the document cache answers
|
|
116
|
+
* or stores it, and a `ppr` route's `replayStatus` is
|
|
117
|
+
* `{ outcome: "BYPASS", reason: "stale" }`.
|
|
108
118
|
*/
|
|
109
119
|
partial?:
|
|
110
120
|
| true
|
|
@@ -115,6 +125,7 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
115
125
|
prefetch?: boolean;
|
|
116
126
|
fill?: boolean;
|
|
117
127
|
interceptSource?: string;
|
|
128
|
+
stale?: boolean;
|
|
118
129
|
};
|
|
119
130
|
/**
|
|
120
131
|
* POST this form as a submit before hydration: the no-JS request the
|
|
@@ -628,14 +639,21 @@ async function buildRequest(
|
|
|
628
639
|
prefetch = false,
|
|
629
640
|
fill = false,
|
|
630
641
|
interceptSource,
|
|
642
|
+
stale = false,
|
|
631
643
|
} = partial === true ? {} : partial;
|
|
644
|
+
if (stale && prefetch) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
"serveShellRequest: partial.stale cannot be combined with partial.prefetch",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
632
649
|
const target = new URL(url);
|
|
633
650
|
target.searchParams.set("_rsc_partial", "true");
|
|
634
651
|
target.searchParams.set("_rsc_segments", segments.join(","));
|
|
635
652
|
if (rewriteTarget) target.searchParams.set("_rsc_rt", rewriteTarget);
|
|
636
|
-
// As the browser sends them: browser/navigation-client.ts (fill) and
|
|
653
|
+
// As the browser sends them: browser/navigation-client.ts (fill, stale) and
|
|
637
654
|
// browser/prefetch/fetch.ts (prefetch).
|
|
638
655
|
if (fill) target.searchParams.set(FILL_PARAM, "1");
|
|
656
|
+
if (stale) target.searchParams.set(STALE_PARAM, "true");
|
|
639
657
|
if (prefetch && !headers.has(PREFETCH_HEADER)) {
|
|
640
658
|
headers.set(PREFETCH_HEADER, "1");
|
|
641
659
|
}
|
|
@@ -637,10 +637,10 @@ export type IsActionFn = (...actions: ActionRef[]) => boolean;
|
|
|
637
637
|
* ```ts
|
|
638
638
|
* import * as CartActions from "./actions/cart";
|
|
639
639
|
*
|
|
640
|
-
* // Idiom A — "mine, else defer": re-render on my actions
|
|
641
|
-
* // undefined so the segment's default decision
|
|
642
|
-
* // revalidators get a say).
|
|
643
|
-
* revalidate((ctx) => ctx.isAction(CartActions) || undefined)
|
|
640
|
+
* // Idiom A — "mine, else defer": re-render on my actions and on Back after
|
|
641
|
+
* // an action, otherwise return undefined so the segment's default decision
|
|
642
|
+
* // still applies (and downstream revalidators get a say).
|
|
643
|
+
* revalidate((ctx) => ctx.isAction(CartActions) || ctx.stale || undefined)
|
|
644
644
|
*
|
|
645
645
|
* // Idiom B — "mine only": re-render on my actions and suppress everything
|
|
646
646
|
* // else (isAction returns a raw boolean, so an unrelated action yields false).
|
|
@@ -768,8 +768,11 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
|
|
|
768
768
|
// ── Staleness signal ─────────────────────────────────────────────────
|
|
769
769
|
|
|
770
770
|
/**
|
|
771
|
-
* `true`
|
|
772
|
-
*
|
|
771
|
+
* `true` on a revalidation of a page the browser marked stale (the
|
|
772
|
+
* `_rsc_stale` query param): Back/Forward onto a history entry after a
|
|
773
|
+
* server action in this or another tab, or while one is in flight. A request
|
|
774
|
+
* that holds no segments (the follow-up after an action, `refresh()`) is
|
|
775
|
+
* marked stale too, but there every segment renders and no predicate runs.
|
|
773
776
|
*
|
|
774
777
|
* This is NOT segment cache staleness (loaders are never segment-cached).
|
|
775
778
|
* Use this to decide whether loader data should be re-fetched after an
|