@rangojs/router 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/history-state.d.ts +13 -2
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/route-definition/redirect.d.ts +5 -0
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/middleware-types.d.ts +6 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +38 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/history-state.ts +24 -2
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/build/runtime-discovery.ts +46 -8
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +37 -1
- package/src/cache/document-cache.ts +12 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/route-definition/redirect.ts +5 -0
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording what revalidate() rules read of the source page, so a prefetch
|
|
3
|
+
* answer a rule kept is shared between the pages with equal values of what
|
|
4
|
+
* was read (prefetch-source-scope.ts).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createSourceReads,
|
|
9
|
+
formatSourceLabel,
|
|
10
|
+
mergeSourceReads,
|
|
11
|
+
PARAMS_SCOPE_PREFIX,
|
|
12
|
+
shapeOfReads,
|
|
13
|
+
type SourcePage,
|
|
14
|
+
type SourceReads,
|
|
15
|
+
} from "../prefetch-source-scope.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The source group of a match whose diff a source read decided: the reads
|
|
19
|
+
* every kept decision made, unioned, and the source page they read.
|
|
20
|
+
*/
|
|
21
|
+
export interface SourceHeld extends SourcePage {
|
|
22
|
+
reads: SourceReads;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A copy of `params` whose reads land in `reads`: an enumerable getter per
|
|
27
|
+
* key, so a spread, Object.entries, JSON.stringify or structuredClone reads
|
|
28
|
+
* every key. Not a Proxy: structuredClone refuses one. A read of an absent
|
|
29
|
+
* key is not recorded and needs no record: the key set is part of the group
|
|
30
|
+
* (`reads.keys`), so a page in the group lacks that key too.
|
|
31
|
+
*/
|
|
32
|
+
export function recordingParams(
|
|
33
|
+
params: Readonly<Record<string, string>>,
|
|
34
|
+
reads: SourceReads,
|
|
35
|
+
): Record<string, string> {
|
|
36
|
+
reads.keys = true;
|
|
37
|
+
const view: Record<string, string> = {};
|
|
38
|
+
for (const name of Object.keys(params)) {
|
|
39
|
+
Object.defineProperty(view, name, {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
get() {
|
|
43
|
+
reads.params.add(name);
|
|
44
|
+
return params[name];
|
|
45
|
+
},
|
|
46
|
+
// A rule may write to the object, as to the plain one it replaces:
|
|
47
|
+
// what it wrote is its own value, not the source's.
|
|
48
|
+
set(value: string) {
|
|
49
|
+
Object.defineProperty(view, name, {
|
|
50
|
+
value,
|
|
51
|
+
writable: true,
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return view;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add `reads` to the source group of the match `context` decides for. All
|
|
63
|
+
* of one match's decisions read the same source page; a caller that reads
|
|
64
|
+
* no route name passes none.
|
|
65
|
+
*/
|
|
66
|
+
export function markSourceHeld(
|
|
67
|
+
context: { _sourceHeld?: SourceHeld },
|
|
68
|
+
reads: SourceReads,
|
|
69
|
+
page: SourcePage,
|
|
70
|
+
): void {
|
|
71
|
+
const held = (context._sourceHeld ??= {
|
|
72
|
+
reads: createSourceReads(),
|
|
73
|
+
params: page.params,
|
|
74
|
+
routeName: undefined,
|
|
75
|
+
});
|
|
76
|
+
if (page.routeName !== undefined) held.routeName = page.routeName;
|
|
77
|
+
mergeSourceReads(held.reads, reads);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The `x-rsc-prefetch-scope` of a partial response, or undefined when every
|
|
82
|
+
* source page may share it.
|
|
83
|
+
*
|
|
84
|
+
* - `source`: the exact source page's. A route an intercept targets renders
|
|
85
|
+
* the modal or the full page depending on the source, so a full page
|
|
86
|
+
* prefetched where the intercept does not apply would otherwise serve a
|
|
87
|
+
* later click where it does (#1007); a response with deferred units
|
|
88
|
+
* answers one page's prefetch (`sourceOnly`); a kept rule that read
|
|
89
|
+
* `currentUrl` can depend on anything in it; and a source-held answer that
|
|
90
|
+
* sends nothing (`sentNothing`) commits treeless on the page that adopts
|
|
91
|
+
* it, which keeps its whole tree without checking it holds the ids the
|
|
92
|
+
* answer kept (partial-update.ts): only the source is known to hold them.
|
|
93
|
+
* - `params; <label>`: a revalidate() rule read the source's params or route
|
|
94
|
+
* name and kept a segment. A page that adopted the answer would keep its
|
|
95
|
+
* own copy of that segment (#1331), which is right only where the rule
|
|
96
|
+
* decides the same: on pages with equal values of what it read. The label
|
|
97
|
+
* names them (prefetch-source-scope.ts); the browser stores the answer
|
|
98
|
+
* under it. A page that lacks a kept id refetches (the commit's
|
|
99
|
+
* missing-segment check), as for a shared answer.
|
|
100
|
+
*/
|
|
101
|
+
export function prefetchScopeOf(
|
|
102
|
+
sourceOnly: boolean,
|
|
103
|
+
held: SourceHeld | undefined,
|
|
104
|
+
sentNothing: boolean,
|
|
105
|
+
): string | undefined {
|
|
106
|
+
if (sourceOnly) return "source";
|
|
107
|
+
if (!held) return undefined;
|
|
108
|
+
if (held.reads.url || sentNothing) return "source";
|
|
109
|
+
return (
|
|
110
|
+
PARAMS_SCOPE_PREFIX + formatSourceLabel(shapeOfReads(held.reads), held)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Every param of the source read: its params must equal the page's. */
|
|
115
|
+
export function readsOfAllParams(
|
|
116
|
+
params: Readonly<Record<string, string>>,
|
|
117
|
+
): SourceReads {
|
|
118
|
+
const reads = createSourceReads();
|
|
119
|
+
reads.keys = true;
|
|
120
|
+
for (const name of Object.keys(params)) reads.params.add(name);
|
|
121
|
+
return reads;
|
|
122
|
+
}
|
package/src/router/types.ts
CHANGED
|
@@ -91,10 +91,11 @@ export interface MatchApiDeps<TEnv = any> {
|
|
|
91
91
|
findMatch: (pathname: string, ms?: any) => any;
|
|
92
92
|
getMetricsStore: () => any;
|
|
93
93
|
findInterceptForRoute: (
|
|
94
|
-
|
|
94
|
+
routeKeys: string | readonly string[],
|
|
95
95
|
parentEntry: EntryData | null,
|
|
96
96
|
selectorContext: InterceptSelectorContext | null,
|
|
97
97
|
isAction: boolean,
|
|
98
|
+
held?: ReadonlySet<string>,
|
|
98
99
|
) => { intercept: InterceptEntry; entry: EntryData } | null;
|
|
99
100
|
callOnError: SegmentResolutionDeps<TEnv>["callOnError"];
|
|
100
101
|
findNearestErrorBoundary: SegmentResolutionDeps<TEnv>["findNearestErrorBoundary"];
|
package/src/router.ts
CHANGED
|
@@ -654,12 +654,21 @@ export function createRouter<TEnv = any>(
|
|
|
654
654
|
viewTransitionDefault: viewTransitionOption,
|
|
655
655
|
};
|
|
656
656
|
|
|
657
|
+
// Create segment resolution wrappers bound to segmentDeps
|
|
658
|
+
const {
|
|
659
|
+
resolveLoadersOnly,
|
|
660
|
+
buildEntryRevalidateMap,
|
|
661
|
+
resolveAllSegments,
|
|
662
|
+
findInterceptForRoute,
|
|
663
|
+
resolveInterceptEntry,
|
|
664
|
+
resolveInterceptLoadersOnly,
|
|
665
|
+
} = createSegmentWrappers<TEnv>(segmentDeps);
|
|
666
|
+
|
|
657
667
|
// Match API dependencies
|
|
658
668
|
const matchApiDeps: MatchApiDeps<TEnv> = {
|
|
659
669
|
findMatch: (pathname: string, ms?: any) => findMatch(pathname, ms),
|
|
660
670
|
getMetricsStore,
|
|
661
|
-
findInterceptForRoute
|
|
662
|
-
findInterceptForRoute(routeKey, parentEntry, selectorContext, isAction),
|
|
671
|
+
findInterceptForRoute,
|
|
663
672
|
callOnError,
|
|
664
673
|
findNearestErrorBoundary,
|
|
665
674
|
// Use per-router manifest when available, otherwise the static named map
|
|
@@ -667,16 +676,6 @@ export function createRouter<TEnv = any>(
|
|
|
667
676
|
getRouteMap: () => getRouterManifest(routerId) ?? mergedRouteMap,
|
|
668
677
|
};
|
|
669
678
|
|
|
670
|
-
// Create segment resolution wrappers bound to segmentDeps
|
|
671
|
-
const {
|
|
672
|
-
resolveLoadersOnly,
|
|
673
|
-
buildEntryRevalidateMap,
|
|
674
|
-
resolveAllSegments,
|
|
675
|
-
findInterceptForRoute,
|
|
676
|
-
resolveInterceptEntry,
|
|
677
|
-
resolveInterceptLoadersOnly,
|
|
678
|
-
} = createSegmentWrappers<TEnv>(segmentDeps);
|
|
679
|
-
|
|
680
679
|
// Lazy evaluation deps — captures closure state for extracted evaluateLazyEntry
|
|
681
680
|
const lazyEvalDeps: LazyEvalDeps<TEnv> = {
|
|
682
681
|
routesEntries,
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
setRequestContextParams,
|
|
11
11
|
} from "../server/request-context.js";
|
|
12
12
|
import type { RequestContext } from "../server/request-context.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
resolveLocationStateEntries,
|
|
15
|
+
type LocationStateEntry,
|
|
16
|
+
} from "../browser/react/location-state-shared.js";
|
|
14
17
|
import {
|
|
15
18
|
isRedirectResponse,
|
|
16
19
|
isWebSocketUpgradeResponse,
|
|
@@ -340,14 +343,25 @@ export function interceptRedirectForPartial(
|
|
|
340
343
|
/**
|
|
341
344
|
* Attach location state set during a request to a payload's metadata.
|
|
342
345
|
* No-op if no location state was set. Callers must ensure payload.metadata
|
|
343
|
-
* is populated (
|
|
344
|
-
* that
|
|
346
|
+
* is populated. A `document` payload (a page load's or a no-JS form post's
|
|
347
|
+
* bootstrap payload) that carries state marks the request
|
|
348
|
+
* `_documentLocationState`: it is the visitor's, so no shared cache keeps it
|
|
349
|
+
* (server/document-location-state.ts). `locationState` defaults to the
|
|
350
|
+
* current request context's; a shell HIT's tail passes the state set before
|
|
351
|
+
* its render (rsc/rsc-rendering.ts serveShellHit).
|
|
345
352
|
*/
|
|
346
|
-
export function attachLocationStateIfPresent(
|
|
347
|
-
|
|
348
|
-
|
|
353
|
+
export function attachLocationStateIfPresent(
|
|
354
|
+
payload: RscPayload,
|
|
355
|
+
document = false,
|
|
356
|
+
locationState: LocationStateEntry[] | undefined = getLocationState(),
|
|
357
|
+
): void {
|
|
358
|
+
if (locationState && locationState.length > 0) {
|
|
349
359
|
payload.metadata!.locationState =
|
|
350
360
|
resolveLocationStateEntries(locationState);
|
|
361
|
+
if (document) {
|
|
362
|
+
const ctx = _getRequestContext();
|
|
363
|
+
if (ctx) ctx._documentLocationState = true;
|
|
364
|
+
}
|
|
351
365
|
}
|
|
352
366
|
}
|
|
353
367
|
|
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
* So the body ends cleanly with one more row, PREFETCH_FAILED_ROW, which the
|
|
12
12
|
* Flight decoder skips and teeWithCompletion (browser/response-adapter.ts)
|
|
13
13
|
* reads as a stream that did not end cleanly: the client evicts the prefetch
|
|
14
|
-
* and never respawns it.
|
|
15
|
-
*
|
|
14
|
+
* and never respawns it. The router puts no Cache-Control of its own on a
|
|
15
|
+
* prefetch with a live part, the only kind whose render can report an error
|
|
16
|
+
* here, so the browser HTTP cache keeps none of it unless the route sets one;
|
|
17
|
+
* a prefetch with nothing live in it carries `private, max-age`
|
|
18
|
+
* (request-stages.ts applyStoredPrefetchCacheControl); a prefetch
|
|
19
|
+
* scoped to its source page or carrying deferred units is sent
|
|
20
|
+
* `private, no-cache` (rsc-rendering.ts NOT_REUSED).
|
|
16
21
|
*
|
|
17
22
|
* The errors are the render's `_renderErrors`: a Flight error row, a streamed
|
|
18
23
|
* handler recovered into its boundary, a loader that threw (redirect() and
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
setRequestContextParams,
|
|
14
14
|
} from "../server/request-context.js";
|
|
15
15
|
import { printServerError } from "../server/print-server-error.js";
|
|
16
|
+
import { runActionBody } from "../server/cookie-store.js";
|
|
16
17
|
import { createSsrHtmlStage } from "./ssr-setup.js";
|
|
17
18
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
18
19
|
import { executeMiddleware } from "../router/middleware.js";
|
|
@@ -25,6 +26,7 @@ import type { RscPayload, ReactFormState } from "./types.js";
|
|
|
25
26
|
import type { MatchResult } from "../types.js";
|
|
26
27
|
import {
|
|
27
28
|
actionRenderRequest,
|
|
29
|
+
attachLocationStateIfPresent,
|
|
28
30
|
createResponseWithMergedHeaders,
|
|
29
31
|
finalizeResponse,
|
|
30
32
|
buildRouteMiddlewareEntries,
|
|
@@ -186,7 +188,7 @@ async function runFormAction<TEnv>(
|
|
|
186
188
|
// Meter the no-JS form action as the action phase, same as the JS path.
|
|
187
189
|
actionResult = await observePhase(
|
|
188
190
|
PHASES.action(useActionStateId ?? "useActionState"),
|
|
189
|
-
() => boundAction(),
|
|
191
|
+
() => runActionBody(() => boundAction()),
|
|
190
192
|
);
|
|
191
193
|
} catch (error) {
|
|
192
194
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
@@ -215,7 +217,7 @@ async function runFormAction<TEnv>(
|
|
|
215
217
|
try {
|
|
216
218
|
const loadedAction = await ctx.loadServerAction(directActionId);
|
|
217
219
|
actionResult = await observePhase(PHASES.action(directActionId), () =>
|
|
218
|
-
loadedAction.apply(null, args),
|
|
220
|
+
runActionBody(() => loadedAction.apply(null, args)),
|
|
219
221
|
);
|
|
220
222
|
} catch (error) {
|
|
221
223
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
@@ -448,6 +450,10 @@ function renderPeResponse<TEnv>(
|
|
|
448
450
|
// slot instead of failing the payload root and the whole document.
|
|
449
451
|
...(reactFormState ? { formState: Promise.resolve(reactFormState) } : {}),
|
|
450
452
|
};
|
|
453
|
+
// The action's location state rides the re-rendered document's bootstrap
|
|
454
|
+
// payload, as a JS action's rides its response. Not on a failure render,
|
|
455
|
+
// like the JS path.
|
|
456
|
+
if (!failure) attachLocationStateIfPresent(payload, true);
|
|
451
457
|
|
|
452
458
|
const trace = getRequestContext()._activeRoutine;
|
|
453
459
|
|
|
@@ -84,6 +84,14 @@ import {
|
|
|
84
84
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
85
85
|
import type { CacheErrorCategory } from "../cache/cache-error.js";
|
|
86
86
|
import { WIRE } from "../wire-params.js";
|
|
87
|
+
import { isPrefetchKind } from "../router/segment-resolution/prefetch-deferral.js";
|
|
88
|
+
import { storedPrefetchCacheControl } from "../router/prefetch-cache-ttl.js";
|
|
89
|
+
import { carriesClientState } from "../cache/document-cache-policy.js";
|
|
90
|
+
import {
|
|
91
|
+
carryLocationStateAcrossRedirect,
|
|
92
|
+
consumeDocumentLocationState,
|
|
93
|
+
markDocumentResponsePrivate,
|
|
94
|
+
} from "../server/document-location-state.js";
|
|
87
95
|
|
|
88
96
|
/**
|
|
89
97
|
* The router members the stages read. The handler's `RangoInternal` carries
|
|
@@ -96,6 +104,10 @@ export interface StageRouter<TEnv = unknown> {
|
|
|
96
104
|
readonly telemetry?: TelemetrySink;
|
|
97
105
|
readonly timeouts?: ResolvedTimeouts;
|
|
98
106
|
readonly onTimeout?: OnTimeoutCallback<TEnv>;
|
|
107
|
+
/** Names the cookie location state crosses a document redirect in. */
|
|
108
|
+
readonly resolvedStateCookieName: string;
|
|
109
|
+
/** The in-memory prefetch TTL in ms; also a stored prefetch's max-age. */
|
|
110
|
+
readonly prefetchCacheTTL: number;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
/** Report an error through the router's onError. */
|
|
@@ -409,6 +421,14 @@ export async function runRequestPasses<TEnv>(
|
|
|
409
421
|
opts: RequestPassesOptions<TEnv>,
|
|
410
422
|
): Promise<RequestPassesResult> {
|
|
411
423
|
const { ctx, env, variables } = opts;
|
|
424
|
+
// Before any middleware: a document cache lookup, a PPR shell serve and the
|
|
425
|
+
// render all see the state a document redirect carried here.
|
|
426
|
+
consumeDocumentLocationState(
|
|
427
|
+
opts.reqCtx,
|
|
428
|
+
opts.request,
|
|
429
|
+
opts.url,
|
|
430
|
+
ctx.router.resolvedStateCookieName,
|
|
431
|
+
);
|
|
412
432
|
let passIndex = 0;
|
|
413
433
|
let hasMiddleware = false;
|
|
414
434
|
const runChain = (
|
|
@@ -667,6 +687,57 @@ export interface FinalizeRequestOptions<TEnv> {
|
|
|
667
687
|
beforeGuard?: (response: Response) => void;
|
|
668
688
|
}
|
|
669
689
|
|
|
690
|
+
/**
|
|
691
|
+
* A prefetch with nothing live in it (`_servedFromStorage`: a document cache
|
|
692
|
+
* HIT, or a partial whose every sent segment came from storage and ran no
|
|
693
|
+
* loader) leaves with the router's prefetch Cache-Control, so the browser
|
|
694
|
+
* HTTP cache can answer a later prefetch or the click with it.
|
|
695
|
+
* Nothing in such an answer can fail after its 200 left; any other prefetch
|
|
696
|
+
* carries none (#1170). Set here, after every middleware: the document cache
|
|
697
|
+
* reads the route's own header when it stores the response (s-maxage), the
|
|
698
|
+
* browser gets this one, in place of any other the route set.
|
|
699
|
+
*
|
|
700
|
+
* Left as it is: a partial sent NOT_REUSED (scoped to its source page, or
|
|
701
|
+
* carrying deferred units), a header that forbids storing it (`no-store`,
|
|
702
|
+
* `no-cache`, a zero `max-age`), a redirect a middleware returned after the render (`redirect`,
|
|
703
|
+
* converted to a Flight redirect above), and a response carrying one
|
|
704
|
+
* client's state (a per-client signal header, location state), which a
|
|
705
|
+
* stored copy would replay to later requests.
|
|
706
|
+
*/
|
|
707
|
+
function applyStoredPrefetchCacheControl(
|
|
708
|
+
response: Response,
|
|
709
|
+
reqCtx: RequestContext<any>,
|
|
710
|
+
url: URL,
|
|
711
|
+
router: StageRouter<any>,
|
|
712
|
+
redirect: boolean,
|
|
713
|
+
): void {
|
|
714
|
+
if (
|
|
715
|
+
redirect ||
|
|
716
|
+
!reqCtx._servedFromStorage ||
|
|
717
|
+
!isPrefetchKind(reqCtx._requestKind) ||
|
|
718
|
+
!url.searchParams.has(WIRE.partial) ||
|
|
719
|
+
reqCtx._payloadSourceScoped ||
|
|
720
|
+
reqCtx._payloadDeferred ||
|
|
721
|
+
response.status !== 200 ||
|
|
722
|
+
!response.headers.get("content-type")?.startsWith("text/x-component") ||
|
|
723
|
+
FORBIDS_STORE.test(response.headers.get("cache-control") ?? "") ||
|
|
724
|
+
carriesClientState(response, reqCtx)
|
|
725
|
+
) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const cacheControl = storedPrefetchCacheControl(router.prefetchCacheTTL);
|
|
729
|
+
if (cacheControl) trySetHeader(response, "cache-control", cacheControl);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* A Cache-Control directive that forbids reusing the response unvalidated:
|
|
734
|
+
* `no-store`, `no-cache`, or a zero `max-age` (`s-maxage` is the shared
|
|
735
|
+
* caches' and does not match). The route asked for a fresh answer each time;
|
|
736
|
+
* the router's `max-age` would override it.
|
|
737
|
+
*/
|
|
738
|
+
const FORBIDS_STORE =
|
|
739
|
+
/(^|[\s,;])(no-store|no-cache|max-age=["']?0+["']?)(?=$|[\s,;=])/i;
|
|
740
|
+
|
|
670
741
|
/**
|
|
671
742
|
* The response the global chain returned, made ready to leave: a partial or
|
|
672
743
|
* action request's redirect converted, onResponse callbacks drained, the
|
|
@@ -679,6 +750,7 @@ export function finalizeRequestResponse<TEnv>(
|
|
|
679
750
|
const { ctx, reqCtx, request, url, env } = opts;
|
|
680
751
|
const router = ctx.router;
|
|
681
752
|
let response: Response;
|
|
753
|
+
let redirect = false;
|
|
682
754
|
if (opts.hasMiddleware) {
|
|
683
755
|
if (
|
|
684
756
|
url.searchParams.has(WIRE.partial) ||
|
|
@@ -689,6 +761,7 @@ export function finalizeRequestResponse<TEnv>(
|
|
|
689
761
|
ctx.createRedirectFlightResponse,
|
|
690
762
|
{ requestOrigin: url.origin, basename: router.basename },
|
|
691
763
|
);
|
|
764
|
+
redirect = intercepted !== null;
|
|
692
765
|
response = intercepted ?? finalizeResponse(downstream);
|
|
693
766
|
} else {
|
|
694
767
|
response = finalizeResponse(downstream);
|
|
@@ -700,6 +773,7 @@ export function finalizeRequestResponse<TEnv>(
|
|
|
700
773
|
if (opts.rewrittenTo !== undefined) {
|
|
701
774
|
trySetHeader(response, REWRITE_HEADER, opts.rewrittenTo);
|
|
702
775
|
}
|
|
776
|
+
applyStoredPrefetchCacheControl(response, reqCtx, url, router, redirect);
|
|
703
777
|
opts.beforeGuard?.(response);
|
|
704
778
|
|
|
705
779
|
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
@@ -707,7 +781,19 @@ export function finalizeRequestResponse<TEnv>(
|
|
|
707
781
|
// guarding browser-followed (3xx) redirects once covers them all and any
|
|
708
782
|
// future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
|
|
709
783
|
// through untouched (validated client-side instead).
|
|
710
|
-
|
|
784
|
+
// After the guard, so the cookie names the target the browser follows.
|
|
785
|
+
// A document carrying location state, or a redirect carrying it on, leaves
|
|
786
|
+
// private: set after every middleware, so none can make it shared.
|
|
787
|
+
const guarded = markDocumentResponsePrivate(
|
|
788
|
+
carryLocationStateAcrossRedirect(
|
|
789
|
+
guardOutgoingRedirect(response, url.origin, router.basename),
|
|
790
|
+
reqCtx,
|
|
791
|
+
request,
|
|
792
|
+
url,
|
|
793
|
+
router.resolvedStateCookieName,
|
|
794
|
+
),
|
|
795
|
+
reqCtx,
|
|
796
|
+
);
|
|
711
797
|
|
|
712
798
|
// Stream-idle watchdog (opt-in via timeouts.streamIdleMs): bounds
|
|
713
799
|
// end-to-end idle flow on the streamed body — see rsc/stream-idle.ts
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -102,6 +102,7 @@ import { contextGet } from "../context-var.js";
|
|
|
102
102
|
import { rewriteShellAudience, rewriteTrail } from "../server/rewrite-trail.js";
|
|
103
103
|
import { visitorShellKey } from "./shell-capture-constants.js";
|
|
104
104
|
import { PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
105
|
+
import { isPrefetchKind } from "../router/segment-resolution/prefetch-deferral.js";
|
|
105
106
|
import { WIRE } from "../wire-params.js";
|
|
106
107
|
import {
|
|
107
108
|
isWarmReplace,
|
|
@@ -140,6 +141,7 @@ import {
|
|
|
140
141
|
SEGMENT_FRAGMENT_CAPABILITY_HEADER,
|
|
141
142
|
SEGMENT_FRAGMENT_RECOVERY_HEADER,
|
|
142
143
|
} from "../segment-fragments.js";
|
|
144
|
+
import { prefetchScopeOf } from "../router/source-reads.js";
|
|
143
145
|
|
|
144
146
|
type PprReplayStatus =
|
|
145
147
|
| { outcome: "HIT"; freshness: "fresh" | "stale" }
|
|
@@ -361,10 +363,10 @@ type PreparedRender =
|
|
|
361
363
|
kind: "payload";
|
|
362
364
|
payload: RscPayload;
|
|
363
365
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
+
* `x-rsc-prefetch-scope`, when the response depends on the source
|
|
367
|
+
* page (prefetchScopeOf).
|
|
366
368
|
*/
|
|
367
|
-
|
|
369
|
+
prefetchScope: string | undefined;
|
|
368
370
|
pprReplayStatus?: PprReplayStatus;
|
|
369
371
|
/** The key a navigation-only heal capture stores under, when one is needed. */
|
|
370
372
|
healKey?: string;
|
|
@@ -430,6 +432,12 @@ function* requestRenderPlan<TEnv>(
|
|
|
430
432
|
// chain (executeRender wraps it), so no shell byte can precede a guard
|
|
431
433
|
// decision — that ordering is what makes a shared shell safe. Routes
|
|
432
434
|
// without the `ppr` option stay pure axis 1 at zero cost.
|
|
435
|
+
//
|
|
436
|
+
// Location state set by now (middleware, a document redirect's cookie) does
|
|
437
|
+
// not keep a request off the shell: a HIT attaches it to the live tail's
|
|
438
|
+
// payload, and a capture builds its own payload without it, so no shell
|
|
439
|
+
// ever holds a visitor's state (server/document-location-state.ts).
|
|
440
|
+
const stateBeforeRender = reqCtx._locationState?.length ?? 0;
|
|
433
441
|
const shell = yield* scope("shell-serve", shellServePlan(input));
|
|
434
442
|
if (shell.kind === "serve") return shell.response;
|
|
435
443
|
|
|
@@ -441,11 +449,12 @@ function* requestRenderPlan<TEnv>(
|
|
|
441
449
|
);
|
|
442
450
|
if (prepared.kind === "control") return prepared.response;
|
|
443
451
|
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
452
|
+
// Server-set location state rides the payload: a navigation's, merged at
|
|
453
|
+
// its commit, or a document's bootstrap payload, merged when the client
|
|
454
|
+
// starts (browser/rsc-router.tsx). A document carrying it is per visitor
|
|
455
|
+
// (server/document-location-state.ts).
|
|
456
|
+
if (prepared.payload.metadata) {
|
|
457
|
+
attachLocationStateIfPresent(prepared.payload, !isPartial);
|
|
449
458
|
}
|
|
450
459
|
|
|
451
460
|
const response = yield* step("render", () =>
|
|
@@ -457,8 +466,14 @@ function* requestRenderPlan<TEnv>(
|
|
|
457
466
|
// shell). Capture does not flow through the HTTP pipeline — middleware never
|
|
458
467
|
// re-runs (it already ran for this request; guarding is serve-time).
|
|
459
468
|
// reqCtx._dynamic is re-read here on purpose: a loader may have marked the
|
|
460
|
-
// request dynamic while rendering.
|
|
461
|
-
|
|
469
|
+
// request dynamic while rendering. State a handler set in this render is
|
|
470
|
+
// not captured: a HIT replays the handler's output without running it, so
|
|
471
|
+
// the state would be lost on every HIT. The route renders in full instead.
|
|
472
|
+
if (
|
|
473
|
+
shell.kind === "miss" &&
|
|
474
|
+
!reqCtx._dynamic &&
|
|
475
|
+
(reqCtx._locationState?.length ?? 0) <= stateBeforeRender
|
|
476
|
+
) {
|
|
462
477
|
if (
|
|
463
478
|
response.status === 200 &&
|
|
464
479
|
(response.headers.get("content-type") ?? "").includes("text/html")
|
|
@@ -844,7 +859,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
844
859
|
return {
|
|
845
860
|
kind: "payload",
|
|
846
861
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
847
|
-
|
|
862
|
+
prefetchScope: undefined,
|
|
848
863
|
pprReplayStatus,
|
|
849
864
|
healKey,
|
|
850
865
|
};
|
|
@@ -856,6 +871,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
856
871
|
// showed at once, and ran held work again.
|
|
857
872
|
const deferring = result.segments.some((s) => s.deferred);
|
|
858
873
|
if (deferring) reqCtx._payloadDeferred = true;
|
|
874
|
+
if (result.fromStorage) reqCtx._servedFromStorage = true;
|
|
859
875
|
return {
|
|
860
876
|
kind: "payload",
|
|
861
877
|
payload: {
|
|
@@ -885,7 +901,11 @@ function* preparePayloadPlan<TEnv>(
|
|
|
885
901
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
886
902
|
},
|
|
887
903
|
},
|
|
888
|
-
|
|
904
|
+
prefetchScope: prefetchScopeOf(
|
|
905
|
+
result.interceptTargeted === true || deferring,
|
|
906
|
+
result.sourceHeld,
|
|
907
|
+
result.diff.length === 0,
|
|
908
|
+
),
|
|
889
909
|
pprReplayStatus,
|
|
890
910
|
healKey,
|
|
891
911
|
};
|
|
@@ -909,7 +929,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
909
929
|
return {
|
|
910
930
|
kind: "payload",
|
|
911
931
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
912
|
-
|
|
932
|
+
prefetchScope: undefined,
|
|
913
933
|
};
|
|
914
934
|
}
|
|
915
935
|
|
|
@@ -1095,7 +1115,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1095
1115
|
): Promise<Response> {
|
|
1096
1116
|
const { ctx, request, env, url, isPartial, nonce, reqCtx, renderSpan } =
|
|
1097
1117
|
input;
|
|
1098
|
-
const { payload, pprReplayStatus,
|
|
1118
|
+
const { payload, pprReplayStatus, prefetchScope } = prepared;
|
|
1099
1119
|
const metricsStore = reqCtx._metricsStore;
|
|
1100
1120
|
|
|
1101
1121
|
const rscHeaders: Record<string, string> = {
|
|
@@ -1112,22 +1132,24 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1112
1132
|
serializePprReplayStatus(pprReplayStatus);
|
|
1113
1133
|
}
|
|
1114
1134
|
// Tell the client's prefetch cache to scope this response to its source
|
|
1115
|
-
//
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1135
|
+
// (instead of the default source-agnostic wildcard). See prefetchScopeOf.
|
|
1136
|
+
if (prefetchScope !== undefined) {
|
|
1137
|
+
rscHeaders["x-rsc-prefetch-scope"] = prefetchScope;
|
|
1138
|
+
// A shared cache keys a partial by URL and the ids the client holds, not
|
|
1139
|
+
// by its source page (document-cache.ts): stored, a scoped answer would
|
|
1140
|
+
// serve its diff to another page that holds the same ids.
|
|
1141
|
+
reqCtx._payloadSourceScoped = true;
|
|
1122
1142
|
}
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1143
|
+
// Not reused by a shared cache or the document cache (which also refuses a
|
|
1144
|
+
// payload marked `_payloadDeferred` or `_payloadSourceScoped`), whatever
|
|
1145
|
+
// Cache-Control the route set: a partial scoped to its source page (above);
|
|
1146
|
+
// a response that carries deferred units, which answers one page's prefetch,
|
|
1147
|
+
// and a fill, the second half of one adoption (prefetch: false,
|
|
1148
|
+
// docs/design/prefetch-false.md, R7); and a stale partial render, which a
|
|
1149
|
+
// shared cache keyed by URL must not store or serve. Any other route keeps
|
|
1150
|
+
// its headers.
|
|
1130
1151
|
if (
|
|
1152
|
+
reqCtx._payloadSourceScoped ||
|
|
1131
1153
|
reqCtx._payloadDeferred ||
|
|
1132
1154
|
(isPartial &&
|
|
1133
1155
|
(reqCtx._requestKind === "fill" ||
|
|
@@ -1177,7 +1199,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1177
1199
|
},
|
|
1178
1200
|
);
|
|
1179
1201
|
// A prefetch whose render reported an error is not kept (#1170).
|
|
1180
|
-
if (reqCtx._requestKind
|
|
1202
|
+
if (!isPrefetchKind(reqCtx._requestKind) || !isPartial) return rendered;
|
|
1181
1203
|
return rendered.then((response) =>
|
|
1182
1204
|
markPrefetchOnRenderError(response, reqCtx._renderErrors ?? []),
|
|
1183
1205
|
);
|
|
@@ -1712,6 +1734,21 @@ function serveShellHit(
|
|
|
1712
1734
|
// A visitor-keyed shell shows the visitor's URL: the document cache keys
|
|
1713
1735
|
// this response by the target.
|
|
1714
1736
|
if (entry.pathname !== undefined) reqCtx._payloadVisitorLocation = true;
|
|
1737
|
+
// Location state set before the render (middleware, a document redirect's
|
|
1738
|
+
// cookie) rides the tail's payload, not the shell. Marked here, on the
|
|
1739
|
+
// request's own context and before the response leaves (the tail runs on a
|
|
1740
|
+
// derived context, after an await), so the response goes
|
|
1741
|
+
// `private, no-store` and stays out of the document cache
|
|
1742
|
+
// (server/document-location-state.ts). The tail carries exactly this
|
|
1743
|
+
// state: a write during the tail (a loader's, through getRequestContext())
|
|
1744
|
+
// comes after these headers chose the response's caching, so it is not
|
|
1745
|
+
// sent, as the skill documents (skills/hooks/state.md).
|
|
1746
|
+
// Never undefined: attachLocationStateIfPresent would read the tail's
|
|
1747
|
+
// context instead.
|
|
1748
|
+
const tailLocationState = reqCtx._locationState ?? [];
|
|
1749
|
+
if (tailLocationState.length > 0) {
|
|
1750
|
+
reqCtx._documentLocationState = true;
|
|
1751
|
+
}
|
|
1715
1752
|
const metricsStore = reqCtx._metricsStore;
|
|
1716
1753
|
// Per-stage tail timing for the `ppr:tail` Server-Timing mirror and the
|
|
1717
1754
|
// perf console line: always in dev, in production only when this request
|
|
@@ -1739,7 +1776,7 @@ function serveShellHit(
|
|
|
1739
1776
|
reportCacheError(error, "cache-write", "[ShellServe] retire");
|
|
1740
1777
|
}
|
|
1741
1778
|
};
|
|
1742
|
-
/** The first loader pin of the entry that failed to decode
|
|
1779
|
+
/** The first loader pin of the entry that failed to decode. */
|
|
1743
1780
|
let undecodablePin: string | undefined;
|
|
1744
1781
|
|
|
1745
1782
|
const renderTail = async (
|
|
@@ -1768,6 +1805,9 @@ function serveShellHit(
|
|
|
1768
1805
|
| undefined;
|
|
1769
1806
|
// Same for the location: the key's URL, or a visitor-keyed shell's.
|
|
1770
1807
|
payload.metadata.pathname = entry.pathname ?? url.pathname;
|
|
1808
|
+
// The visitor's location state, as a MISS document's payload carries
|
|
1809
|
+
// it. The request was marked per visitor above.
|
|
1810
|
+
attachLocationStateIfPresent(payload, false, tailLocationState);
|
|
1771
1811
|
}
|
|
1772
1812
|
// Full Flight render per request: hydration needs the whole payload (there
|
|
1773
1813
|
// is no Flight-side resume — a React limitation, not ours).
|
|
@@ -2031,7 +2071,13 @@ function serveShellHit(
|
|
|
2031
2071
|
"the doc record could not supply the handler layer",
|
|
2032
2072
|
);
|
|
2033
2073
|
}
|
|
2034
|
-
|
|
2074
|
+
// A document redirect's state cookie: this response's headers
|
|
2075
|
+
// cleared it, and the reload must read it again.
|
|
2076
|
+
controller.enqueue(
|
|
2077
|
+
new TextEncoder().encode(
|
|
2078
|
+
shellReloadScript(reqCtx._carriedLocationStateCookie),
|
|
2079
|
+
),
|
|
2080
|
+
);
|
|
2035
2081
|
if (tailTiming) {
|
|
2036
2082
|
tailTiming.outcome = "error";
|
|
2037
2083
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
@@ -2057,7 +2103,7 @@ function serveShellHit(
|
|
|
2057
2103
|
}
|
|
2058
2104
|
// A pin that failed to decode left its loader to this request's run:
|
|
2059
2105
|
// this response hydrated once over a prelude rendered from the
|
|
2060
|
-
// capture's value
|
|
2106
|
+
// capture's value. Retire the entry once the response is
|
|
2061
2107
|
// closed, and back the key off so a pin that keeps failing costs one
|
|
2062
2108
|
// mismatch per window. No reload: that would cost a second page load.
|
|
2063
2109
|
if (undecodablePin !== undefined) {
|