@rangojs/router 0.27.0 → 0.29.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-fence.d.ts +2 -1
- package/dist/types/browser/event-controller.d.ts +8 -2
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/react/use-action.d.ts +7 -2
- package/dist/types/browser/react/use-handle.d.ts +19 -16
- package/dist/types/browser/react/use-link-status.d.ts +5 -2
- package/dist/types/browser/segment-reconciler.d.ts +32 -4
- package/dist/types/browser/types.d.ts +24 -5
- package/dist/types/browser/wire-protocol.d.ts +26 -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/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +11 -5
- 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 +6 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/navigation-snapshot.d.ts +7 -0
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
- 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 +95 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
- 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 +11 -1
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +53 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/testing/render-route.d.ts +43 -8
- package/dist/types/testing/serve-shell-request.d.ts +8 -1
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +30 -3
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +39 -0
- package/dist/vite/index.js +3822 -3727
- 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 +4 -2
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +1 -1
- package/skills/hooks/data.md +8 -1
- package/skills/hooks/handle-and-actions.md +33 -2
- package/skills/hooks/navigation.md +121 -15
- package/skills/hooks/state.md +6 -4
- package/skills/hooks/urls.md +16 -5
- package/skills/intercept/SKILL.md +23 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/observability/SKILL.md +30 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +15 -1
- package/skills/testing/client-components.md +25 -18
- 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/view-transitions/SKILL.md +1 -1
- package/src/__internal.ts +7 -1
- package/src/browser/action-fence.ts +2 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +37 -13
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +59 -26
- package/src/browser/navigation-client.ts +20 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/network-error-handler.ts +4 -0
- package/src/browser/partial-update.ts +33 -6
- 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/react/use-action.ts +24 -48
- package/src/browser/react/use-handle.ts +22 -18
- package/src/browser/react/use-link-status.ts +48 -60
- package/src/browser/react/use-navigation.ts +92 -41
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +109 -53
- package/src/browser/server-action-bridge.ts +81 -61
- package/src/browser/types.ts +25 -5
- package/src/browser/wire-protocol.ts +43 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +132 -75
- 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 +64 -46
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/read-through-swr.ts +20 -18
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +6 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +22 -10
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +82 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +5 -5
- package/src/router/match-context.ts +23 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +46 -57
- package/src/router/match-middleware/cache-lookup.ts +677 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +14 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/helpers.ts +49 -13
- package/src/router/segment-resolution/loader-cache.ts +15 -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 +139 -0
- package/src/router/segment-resolution/revalidation.ts +939 -789
- package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
- 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 +11 -1
- package/src/router.ts +19 -17
- 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 +759 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +275 -227
- 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 +75 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +226 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/render-route.tsx +199 -13
- package/src/testing/serve-shell-request.ts +36 -40
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +30 -3
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +40 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
|
@@ -1,104 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cache Lookup Middleware
|
|
3
3
|
*
|
|
4
|
-
* First middleware in the pipeline.
|
|
4
|
+
* First middleware in the pipeline. Before segment resolution runs, it serves
|
|
5
|
+
* a stored match from the first source that answers:
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* +---------------------+
|
|
13
|
-
* | Is action request? |──yes──> yield* source (pass through)
|
|
14
|
-
* +---------------------+
|
|
15
|
-
* | no
|
|
16
|
-
* v
|
|
17
|
-
* +---------------------+
|
|
18
|
-
* | Cache enabled? |──no───> yield* source (pass through)
|
|
19
|
-
* +---------------------+
|
|
20
|
-
* | yes
|
|
21
|
-
* v
|
|
22
|
-
* +---------------------+
|
|
23
|
-
* | Lookup cache |
|
|
24
|
-
* | (pathname, params) |
|
|
25
|
-
* +---------------------+
|
|
26
|
-
* |
|
|
27
|
-
* +-----+-----+
|
|
28
|
-
* | |
|
|
29
|
-
* miss hit
|
|
30
|
-
* | |
|
|
31
|
-
* v v
|
|
32
|
-
* yield* Set state.cacheHit = true
|
|
33
|
-
* source Set state.shouldRevalidate
|
|
34
|
-
* | |
|
|
35
|
-
* | v
|
|
36
|
-
* | +---------------------------+
|
|
37
|
-
* | | For each cached segment: |
|
|
38
|
-
* | | - Apply revalidation |
|
|
39
|
-
* | | - Set component = null |
|
|
40
|
-
* | | if client has it |
|
|
41
|
-
* | +---------------------------+
|
|
42
|
-
* | |
|
|
43
|
-
* | v
|
|
44
|
-
* | +---------------------------+
|
|
45
|
-
* | | Resolve fresh loaders | <-- Loaders are NEVER cached
|
|
46
|
-
* | | (always fresh data) |
|
|
47
|
-
* | +---------------------------+
|
|
48
|
-
* | |
|
|
49
|
-
* | v
|
|
50
|
-
* | yield cached segments
|
|
51
|
-
* | yield fresh loader segments
|
|
52
|
-
* | |
|
|
53
|
-
* +-----------+
|
|
54
|
-
* |
|
|
55
|
-
* v
|
|
56
|
-
* next middleware
|
|
7
|
+
* 1. Prerender: the writable on-demand overlay, then the bundled build
|
|
8
|
+
* manifest. A live render (an action or a stale partial) reads them too
|
|
9
|
+
* when the route has no live handler (not a Passthrough).
|
|
10
|
+
* 2. The dev Static store (`__PRERENDER_DEV_URL`, isStaticPrerender entries).
|
|
11
|
+
* 3. The route's cache() record (the segment cache). An action or a stale
|
|
12
|
+
* partial renders live (rendersLive) and reads no record.
|
|
57
13
|
*
|
|
14
|
+
* Every source serves through serveStoredSegments: pre-rendering is caching
|
|
15
|
+
* at build time, so a prerender entry is a cache hit through the same segment
|
|
16
|
+
* path. When no source answers, the request passes through to
|
|
17
|
+
* segment-resolution.
|
|
58
18
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
19
|
+
* A STORED HIT
|
|
20
|
+
* ============
|
|
61
21
|
*
|
|
62
|
-
* Cache HIT:
|
|
63
22
|
* - state.cacheHit = true signals downstream middleware to skip
|
|
64
|
-
* -
|
|
65
|
-
* - Loaders are always re-resolved
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* Cache MISS:
|
|
69
|
-
* - Passes through to segment-resolution middleware
|
|
70
|
-
* - No segments yielded from this middleware
|
|
71
|
-
*
|
|
72
|
-
* Loaders:
|
|
73
|
-
* - NEVER cached in the segment cache
|
|
74
|
-
* - Always resolved fresh on every request
|
|
75
|
-
* - Ensures data freshness even with cached UI components
|
|
76
|
-
* - Segment cache staleness does NOT propagate to loader revalidation;
|
|
23
|
+
* - A segment the client already holds and keeps has its component nullified
|
|
24
|
+
* - Loaders are always re-resolved fresh: never cached or pre-rendered.
|
|
25
|
+
* Segment cache staleness does NOT propagate to loader revalidation;
|
|
77
26
|
* loaders use their own revalidation rules (actionId, user-defined)
|
|
27
|
+
* - A cache() record sets state.shouldRevalidate, which triggers background
|
|
28
|
+
* SWR when the record was stale
|
|
78
29
|
*
|
|
30
|
+
* REVALIDATION RULES (cache() record)
|
|
31
|
+
* ===================================
|
|
79
32
|
*
|
|
80
|
-
*
|
|
81
|
-
* ==================
|
|
33
|
+
* Each record segment the client holds is evaluated against its rules:
|
|
82
34
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* 1. No rules defined -> use default (skip if client has segment)
|
|
35
|
+
* 1. No rules defined -> re-render the route's own segments when the search
|
|
36
|
+
* or the route params changed; otherwise skip (nullify component)
|
|
86
37
|
* 2. Rules return false -> skip re-render (nullify component)
|
|
87
38
|
* 3. Rules return true -> re-render (keep component)
|
|
88
39
|
*
|
|
89
|
-
* Revalidation context includes
|
|
90
|
-
*
|
|
91
|
-
* - Request object
|
|
92
|
-
* - Action context (if POST)
|
|
40
|
+
* Revalidation context includes the previous/next URL and params, the request
|
|
41
|
+
* and the action context (if POST).
|
|
93
42
|
*/
|
|
94
43
|
import type { InternalHandlerContext, ResolvedSegment } from "../../types.js";
|
|
95
44
|
import { isPprEntry, type EntryData } from "../../server/context.js";
|
|
96
45
|
import {
|
|
97
46
|
rendersLive,
|
|
47
|
+
resolutionPassInput,
|
|
98
48
|
type MatchContext,
|
|
99
49
|
type MatchPipelineState,
|
|
100
50
|
} from "../match-context.js";
|
|
101
51
|
import { getRouterContext, type RouterContext } from "../router-context.js";
|
|
52
|
+
import { freshPassInput } from "../segment-resolution/resolution-pass.js";
|
|
102
53
|
import { observeEvent } from "../instrument.js";
|
|
103
54
|
import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
|
|
104
55
|
import { treeHasStreaming } from "./segment-resolution.js";
|
|
@@ -106,6 +57,7 @@ import { loaderPins } from "../segment-resolution/loader-cache.js";
|
|
|
106
57
|
import {
|
|
107
58
|
fillSends,
|
|
108
59
|
interceptFillSkips,
|
|
60
|
+
isFillRequest,
|
|
109
61
|
recordUse,
|
|
110
62
|
} from "../segment-resolution/prefetch-deferral.js";
|
|
111
63
|
import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
|
|
@@ -133,29 +85,21 @@ import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
|
|
|
133
85
|
import { paramsEqual } from "../params-util.js";
|
|
134
86
|
import { requestHeaders } from "../../server/request-headers.js";
|
|
135
87
|
import { isInterceptSegment } from "../../browser/intercept-utils.js";
|
|
88
|
+
import {
|
|
89
|
+
interceptRecordKey,
|
|
90
|
+
interceptSlotId,
|
|
91
|
+
} from "../intercept-resolution.js";
|
|
136
92
|
|
|
137
93
|
// Lazily initialized prerender store singleton and dynamically imported deps.
|
|
138
94
|
// Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
|
|
139
95
|
// top-level, which breaks vitest (only URLs with file:, data:, node: schemes).
|
|
140
96
|
let prerenderStoreInstance: PrerenderStore | null | undefined;
|
|
141
|
-
let
|
|
142
|
-
|
|
143
|
-
| undefined;
|
|
144
|
-
let _fragmentSegments:
|
|
145
|
-
| typeof import("../../cache/segment-codec.js").fragmentSegments
|
|
146
|
-
| undefined;
|
|
147
|
-
let _restoreHandles:
|
|
148
|
-
| typeof import("../../cache/handle-snapshot.js").restoreHandles
|
|
149
|
-
| undefined;
|
|
150
|
-
let _decodeHandles:
|
|
151
|
-
| typeof import("../../cache/handle-snapshot.js").decodeHandles
|
|
152
|
-
| undefined;
|
|
153
|
-
let _restoreRecordHandles:
|
|
154
|
-
| typeof import("../../cache/handle-snapshot.js").restoreRecordHandles
|
|
155
|
-
| undefined;
|
|
97
|
+
let codec: typeof import("../../cache/segment-codec.js") | undefined;
|
|
98
|
+
let handleSnapshot: typeof import("../../cache/handle-snapshot.js") | undefined;
|
|
156
99
|
let _hashParams:
|
|
157
100
|
| typeof import("../../prerender/param-hash.js").hashParams
|
|
158
101
|
| undefined;
|
|
102
|
+
let storeModule: typeof import("../../prerender/store.js") | undefined;
|
|
159
103
|
|
|
160
104
|
/**
|
|
161
105
|
* @internal Replace the prerender store this module serves from, and return
|
|
@@ -171,215 +115,540 @@ export function setPrerenderStoreForTests(
|
|
|
171
115
|
return previous;
|
|
172
116
|
}
|
|
173
117
|
|
|
174
|
-
async function ensurePrerenderDeps() {
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (prerenderStoreInstance === undefined) {
|
|
189
|
-
prerenderStoreInstance = store.createPrerenderStore();
|
|
190
|
-
}
|
|
118
|
+
async function ensurePrerenderDeps(): Promise<void> {
|
|
119
|
+
if (codec) return;
|
|
120
|
+
const [segmentCodec, snapshot, paramHash, store] = await Promise.all([
|
|
121
|
+
import("../../cache/segment-codec.js"),
|
|
122
|
+
import("../../cache/handle-snapshot.js"),
|
|
123
|
+
import("../../prerender/param-hash.js"),
|
|
124
|
+
import("../../prerender/store.js"),
|
|
125
|
+
]);
|
|
126
|
+
codec = segmentCodec;
|
|
127
|
+
handleSnapshot = snapshot;
|
|
128
|
+
_hashParams = paramHash.hashParams;
|
|
129
|
+
storeModule = store;
|
|
130
|
+
if (prerenderStoreInstance === undefined) {
|
|
131
|
+
prerenderStoreInstance = store.createPrerenderStore();
|
|
191
132
|
}
|
|
192
133
|
}
|
|
193
134
|
|
|
194
135
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
136
|
+
* Where a stored match is read from. Each source serves through
|
|
137
|
+
* serveStoredSegments: a prerender entry is a cache hit through the same
|
|
138
|
+
* segment path (prerender-api-design.md, "Core Principle").
|
|
139
|
+
*
|
|
140
|
+
* - `segment-cache`: a cache() record, decoded by the lookup, which replays
|
|
141
|
+
* its handles on a document request; on a partial one `restoreHandles`
|
|
142
|
+
* replays those of the segments it sends. `scope` is the scope whose
|
|
143
|
+
* record answered.
|
|
144
|
+
* - `prerender-build`: the bundled build manifest (immutable).
|
|
145
|
+
* - `prerender-overlay`: the writable on-demand overlay. Its entry may be
|
|
146
|
+
* newer than the client's copy: one that a refresh (possibly inside this
|
|
147
|
+
* very action) replaced.
|
|
198
148
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
async function* resolveFreshLoadersAndYield<TEnv>(
|
|
207
|
-
ctx: MatchContext<TEnv>,
|
|
208
|
-
entries: EntryData[],
|
|
209
|
-
state: MatchPipelineState,
|
|
210
|
-
pipelineStart: number,
|
|
211
|
-
ms: MatchContext<TEnv>["metricsStore"],
|
|
212
|
-
resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
|
|
213
|
-
resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
|
|
214
|
-
): AsyncGenerator<ResolvedSegment> {
|
|
215
|
-
const loaderStart = performance.now();
|
|
216
|
-
entries = entries.filter((e) => !interceptFillSkips(ctx, e.shortCode));
|
|
217
|
-
|
|
218
|
-
if (ctx.isFullMatch) {
|
|
219
|
-
if (resolveLoadersOnly) {
|
|
220
|
-
const loaderSegments = await ctx.Store.run(() =>
|
|
221
|
-
resolveLoadersOnly(entries, ctx.handlerContext),
|
|
222
|
-
);
|
|
223
|
-
state.matchedIds = state.cachedMatchedIds!;
|
|
224
|
-
for (const segment of loaderSegments) {
|
|
225
|
-
yield segment;
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
state.matchedIds = state.cachedMatchedIds!;
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
if (resolveLoadersOnlyWithRevalidation) {
|
|
232
|
-
const loaderResult = await ctx.Store.run(() =>
|
|
233
|
-
resolveLoadersOnlyWithRevalidation(
|
|
234
|
-
entries,
|
|
235
|
-
ctx.handlerContext,
|
|
236
|
-
ctx.clientSegmentSet,
|
|
237
|
-
ctx.prevParams,
|
|
238
|
-
ctx.request,
|
|
239
|
-
ctx.prevUrl,
|
|
240
|
-
ctx.url,
|
|
241
|
-
ctx.routeKey,
|
|
242
|
-
ctx.actionContext,
|
|
243
|
-
// Loaders are never cached in the segment cache, so segment staleness
|
|
244
|
-
// must not propagate; browser-sent staleness (ctx.stale) still must.
|
|
245
|
-
ctx.stale || undefined,
|
|
246
|
-
),
|
|
247
|
-
);
|
|
248
|
-
state.matchedIds = [
|
|
249
|
-
...state.cachedMatchedIds!,
|
|
250
|
-
...loaderResult.matchedIds,
|
|
251
|
-
];
|
|
252
|
-
for (const segment of loaderResult.segments) {
|
|
253
|
-
yield segment;
|
|
254
|
-
}
|
|
255
|
-
} else {
|
|
256
|
-
state.matchedIds = state.cachedMatchedIds!;
|
|
149
|
+
type StoredSegmentSource =
|
|
150
|
+
| {
|
|
151
|
+
kind: "segment-cache";
|
|
152
|
+
segments: ResolvedSegment[];
|
|
153
|
+
shouldRevalidate: boolean;
|
|
154
|
+
scope: CacheScope;
|
|
155
|
+
restoreHandles?: (sentIds: ReadonlySet<string>) => Promise<void>;
|
|
257
156
|
}
|
|
258
|
-
}
|
|
157
|
+
| { kind: "prerender-build" | "prerender-overlay"; entry: PrerenderEntry };
|
|
259
158
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
depth: 1,
|
|
267
|
-
});
|
|
268
|
-
ms.metrics.push({
|
|
269
|
-
label: "pipeline:cache-hit",
|
|
270
|
-
duration: loaderEnd - pipelineStart,
|
|
271
|
-
startTime: pipelineStart - ms.requestStart,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function* yieldFromStore<TEnv>(
|
|
159
|
+
/**
|
|
160
|
+
* Decode a prerender entry's segments and replay its handle data into the
|
|
161
|
+
* request's HandleStore, as the cache scope's lookup does for a record; on a
|
|
162
|
+
* partial request, only the buckets of the segments it `sends`.
|
|
163
|
+
*/
|
|
164
|
+
async function decodeStoredEntry<TEnv>(
|
|
277
165
|
entry: PrerenderEntry,
|
|
278
166
|
ctx: MatchContext<TEnv>,
|
|
279
|
-
state: MatchPipelineState,
|
|
280
|
-
pipelineStart: number,
|
|
281
167
|
reqCtx: RequestContext<TEnv> | undefined,
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* The entry may be newer than the client's copy: an on-demand overlay entry
|
|
286
|
-
* that a refresh (possibly inside this very action) replaced. On an action
|
|
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.
|
|
290
|
-
*/
|
|
291
|
-
replaceable?: boolean,
|
|
292
|
-
): AsyncGenerator<ResolvedSegment> {
|
|
293
|
-
if (
|
|
294
|
-
!_deserializeSegments ||
|
|
295
|
-
!_fragmentSegments ||
|
|
296
|
-
!_restoreHandles ||
|
|
297
|
-
!_decodeHandles ||
|
|
298
|
-
!_restoreRecordHandles ||
|
|
299
|
-
!_hashParams
|
|
300
|
-
) {
|
|
301
|
-
throw new Error("yieldFromStore called before ensurePrerenderDeps");
|
|
302
|
-
}
|
|
303
|
-
|
|
168
|
+
sends: (segment: ResolvedSegment) => boolean | Promise<boolean>,
|
|
169
|
+
): Promise<ResolvedSegment[]> {
|
|
304
170
|
// Shell-HIT tail (issue #700): a Prerender+ppr route's tail serves from THIS
|
|
305
171
|
// store (the prerender lookup runs before the cache scope), so the fragment
|
|
306
172
|
// splice must apply here too — otherwise producer B entries re-serialize the
|
|
307
173
|
// whole tree per request while producer A entries do not.
|
|
308
174
|
const segments = reqCtx?._shellFragmentPayload
|
|
309
|
-
? await
|
|
310
|
-
: await
|
|
175
|
+
? await codec!.fragmentSegments(entry.segments)
|
|
176
|
+
: await codec!.deserializeSegments(entry.segments);
|
|
311
177
|
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
178
|
+
// entry.handles is a Flight-encoded string ("" when none) — decode before
|
|
179
|
+
// restore so Promise/ReactNode handle values are revived, not the corrupted
|
|
180
|
+
// JSON form.
|
|
315
181
|
const handleStore = reqCtx?._handleStore;
|
|
316
182
|
if (handleStore && entry.handles) {
|
|
317
|
-
const handlesRecord = await
|
|
183
|
+
const handlesRecord = await handleSnapshot!.decodeHandles(entry.handles);
|
|
318
184
|
if (handlesRecord) {
|
|
319
|
-
|
|
185
|
+
// On a partial request, a bucket is replayed only when a segment with
|
|
186
|
+
// its id is sent: not one a kept segment holds (the client keeps its
|
|
187
|
+
// values, as on a live walk), nor one of an intercept the variant does
|
|
188
|
+
// not serve (selectInterceptVariant).
|
|
189
|
+
if (!ctx.isFullMatch) {
|
|
190
|
+
const sent = new Set<string>();
|
|
191
|
+
for (const segment of segments) {
|
|
192
|
+
let send = sends(segment);
|
|
193
|
+
if (typeof send !== "boolean") send = await send;
|
|
194
|
+
if (send) sent.add(segment.id);
|
|
195
|
+
}
|
|
196
|
+
for (const id of Object.keys(handlesRecord)) {
|
|
197
|
+
if (!sent.has(id)) delete handlesRecord[id];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
handleSnapshot!.restoreHandles(handlesRecord, handleStore);
|
|
320
201
|
}
|
|
321
202
|
}
|
|
322
203
|
|
|
323
204
|
// Before the render barrier freezes the document snapshot.
|
|
324
205
|
const shellHandles = reqCtx?._shellImplicitCache?.prerenderHandles;
|
|
325
206
|
if (handleStore && shellHandles) {
|
|
326
|
-
await
|
|
207
|
+
await handleSnapshot!.restoreRecordHandles(
|
|
327
208
|
handleStore,
|
|
328
209
|
shellHandles,
|
|
329
210
|
loaderPins(ctx.entries, reqCtx),
|
|
330
211
|
);
|
|
331
212
|
}
|
|
332
213
|
|
|
333
|
-
state.cacheHit = true;
|
|
334
|
-
state.cacheSource = "prerender";
|
|
335
|
-
state.cachedSegments = segments;
|
|
336
|
-
state.cachedMatchedIds = segments.map((s) => s.id);
|
|
337
|
-
|
|
338
|
-
// Set streaming flag (once) and resolve render barrier.
|
|
339
214
|
// Post-match serve-source truth for the PPR replay reporter. This overwrites
|
|
340
215
|
// `intercept` because the prerender store is the response source.
|
|
216
|
+
if (reqCtx) reqCtx._pprReplayPostMatchReason = "prerender-store";
|
|
217
|
+
return segments;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Whether a cache() record's segment is sent: one the client lacks, an
|
|
222
|
+
* intercept's, or one its revalidate rules re-render. Without rules, the
|
|
223
|
+
* route's own segments are re-rendered when the search or the route params
|
|
224
|
+
* changed. A segment with rules answers with their pending decision, which
|
|
225
|
+
* serveStoredSegments awaits and observes.
|
|
226
|
+
*/
|
|
227
|
+
function cachedSegmentSends<TEnv>(
|
|
228
|
+
ctx: MatchContext<TEnv>,
|
|
229
|
+
cacheStale: boolean,
|
|
230
|
+
reqCtx: RequestContext<TEnv> | undefined,
|
|
231
|
+
router: RouterContext<TEnv>,
|
|
232
|
+
): (segment: ResolvedSegment) => boolean | Promise<boolean> {
|
|
233
|
+
const entryRevalidateMap =
|
|
234
|
+
!ctx.isFullMatch &&
|
|
235
|
+
ctx.clientSegmentSet.size > 0 &&
|
|
236
|
+
router.buildEntryRevalidateMap
|
|
237
|
+
? router.buildEntryRevalidateMap(ctx.entries)
|
|
238
|
+
: undefined;
|
|
239
|
+
const searchChanged =
|
|
240
|
+
ctx.prevUrl.search !== ctx.url.search || reqCtx?._visitorMoved === true;
|
|
241
|
+
const routeParamsChanged = !paramsEqual(ctx.matched.params, ctx.prevParams);
|
|
242
|
+
const trace = (
|
|
243
|
+
segment: ResolvedSegment,
|
|
244
|
+
shouldRevalidate: boolean,
|
|
245
|
+
reason: string,
|
|
246
|
+
): boolean => {
|
|
247
|
+
if (isTraceActive()) {
|
|
248
|
+
pushRevalidationTraceEntry({
|
|
249
|
+
segmentId: segment.id,
|
|
250
|
+
segmentType: segment.type,
|
|
251
|
+
belongsToRoute: segment.belongsToRoute ?? false,
|
|
252
|
+
source: "cache-hit",
|
|
253
|
+
defaultShouldRevalidate: shouldRevalidate,
|
|
254
|
+
finalShouldRevalidate: shouldRevalidate,
|
|
255
|
+
reason,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return shouldRevalidate;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return (segment) => {
|
|
262
|
+
if (!ctx.clientSegmentSet.has(segment.id)) {
|
|
263
|
+
return trace(segment, true, "new-segment");
|
|
264
|
+
}
|
|
265
|
+
// A fill keeps a held segment outright (fillSends), an intercept's
|
|
266
|
+
// included.
|
|
267
|
+
if (
|
|
268
|
+
fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) === false
|
|
269
|
+
) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
if (isInterceptSegment(segment)) return true;
|
|
273
|
+
|
|
274
|
+
const entryInfo = entryRevalidateMap?.get(segment.id);
|
|
275
|
+
if (!entryInfo || entryInfo.revalidate.length === 0) {
|
|
276
|
+
const shouldDefaultRevalidate =
|
|
277
|
+
(searchChanged || routeParamsChanged) &&
|
|
278
|
+
(segment.type === "route" ||
|
|
279
|
+
(segment.belongsToRoute === true &&
|
|
280
|
+
(segment.type === "layout" || segment.type === "parallel")));
|
|
281
|
+
return trace(
|
|
282
|
+
segment,
|
|
283
|
+
shouldDefaultRevalidate,
|
|
284
|
+
!shouldDefaultRevalidate
|
|
285
|
+
? "cached-no-rules"
|
|
286
|
+
: routeParamsChanged
|
|
287
|
+
? "cached-params-changed"
|
|
288
|
+
: "cached-search-changed",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return router.evaluateRevalidation({
|
|
293
|
+
segment,
|
|
294
|
+
prevParams: ctx.prevParams,
|
|
295
|
+
getPrevSegment: null,
|
|
296
|
+
request: ctx.request,
|
|
297
|
+
prevUrl: ctx.prevUrl,
|
|
298
|
+
nextUrl: ctx.url,
|
|
299
|
+
revalidations: entryInfo.revalidate.map((fn, i) => ({
|
|
300
|
+
name: `revalidate${i}`,
|
|
301
|
+
fn,
|
|
302
|
+
})),
|
|
303
|
+
routeKey: ctx.routeKey,
|
|
304
|
+
context: ctx.handlerContext,
|
|
305
|
+
actionContext: ctx.actionContext,
|
|
306
|
+
cacheStale,
|
|
307
|
+
traceSource: "cache-hit",
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Whether a prerender entry's segment is sent. On a partial navigation the
|
|
314
|
+
* client keeps the segments it holds. The route's own segments (an
|
|
315
|
+
* intercept's included) are sent again when the params changed (e.g., a
|
|
316
|
+
* different guide slug). An overlay entry may be newer than the client's
|
|
317
|
+
* copy: on an action or a stale request they are re-sent too, as a live
|
|
318
|
+
* action re-render re-sends them (revalidation.ts "action:route-segment");
|
|
319
|
+
* the immutable bundled manifest keeps the client's copy. A held segment
|
|
320
|
+
* above the route stays held, as a live walk keeps it ("nav:non-route-skip"),
|
|
321
|
+
* unless its revalidate() rules ask for it when the params changed.
|
|
322
|
+
*
|
|
323
|
+
* Decided once per decoded segment, by object: an intercept variant can hold
|
|
324
|
+
* a layout's parallel and the intercept's slot under one id. decodeStoredEntry
|
|
325
|
+
* asks first, to replay the handle values of the sent segments only.
|
|
326
|
+
*/
|
|
327
|
+
function storedEntrySends<TEnv>(
|
|
328
|
+
ctx: MatchContext<TEnv>,
|
|
329
|
+
replaceable: boolean,
|
|
330
|
+
router: RouterContext<TEnv>,
|
|
331
|
+
): (segment: ResolvedSegment) => boolean | Promise<boolean> {
|
|
332
|
+
if (ctx.isFullMatch) return () => true;
|
|
333
|
+
const paramsChanged = !paramsEqual(ctx.matched.params, ctx.prevParams);
|
|
334
|
+
const resendRouteSegments = replaceable && (ctx.isAction || ctx.stale);
|
|
335
|
+
const entryRevalidateMap =
|
|
336
|
+
paramsChanged && router.buildEntryRevalidateMap
|
|
337
|
+
? router.buildEntryRevalidateMap(ctx.entries)
|
|
338
|
+
: undefined;
|
|
339
|
+
const decide = (segment: ResolvedSegment): boolean | Promise<boolean> => {
|
|
340
|
+
if (!ctx.clientSegmentSet.has(segment.id)) return true;
|
|
341
|
+
// A fill keeps it (fillSends).
|
|
342
|
+
const fill = fillSends(
|
|
343
|
+
ctx.handlerContext,
|
|
344
|
+
segment.id,
|
|
345
|
+
ctx.clientSegmentSet,
|
|
346
|
+
);
|
|
347
|
+
if (fill !== undefined) return fill;
|
|
348
|
+
if (segment.type === "route" || segment.belongsToRoute === true) {
|
|
349
|
+
return paramsChanged || resendRouteSegments;
|
|
350
|
+
}
|
|
351
|
+
const revalidate = entryRevalidateMap?.get(segment.id)?.revalidate;
|
|
352
|
+
if (!revalidate?.length) return false;
|
|
353
|
+
return router.evaluateRevalidation({
|
|
354
|
+
segment,
|
|
355
|
+
prevParams: ctx.prevParams,
|
|
356
|
+
getPrevSegment: null,
|
|
357
|
+
request: ctx.request,
|
|
358
|
+
prevUrl: ctx.prevUrl,
|
|
359
|
+
nextUrl: ctx.url,
|
|
360
|
+
revalidations: revalidate.map((fn, i) => ({
|
|
361
|
+
name: `revalidate${i}`,
|
|
362
|
+
fn,
|
|
363
|
+
})),
|
|
364
|
+
routeKey: ctx.routeKey,
|
|
365
|
+
context: ctx.handlerContext,
|
|
366
|
+
actionContext: ctx.actionContext,
|
|
367
|
+
stale: ctx.stale,
|
|
368
|
+
traceSource: "cache-hit",
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
const decided = new WeakMap<ResolvedSegment, boolean | Promise<boolean>>();
|
|
372
|
+
return (segment) => {
|
|
373
|
+
let send = decided.get(segment);
|
|
374
|
+
if (send === undefined) decided.set(segment, (send = decide(segment)));
|
|
375
|
+
return send;
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Serve a stored match from the first source that answers (the order is in
|
|
381
|
+
* the module header), or pass `upstream` through when none does. Every
|
|
382
|
+
* StoredSegmentSource serves here: the stored segments, minus the ones the
|
|
383
|
+
* client keeps, then the loaders resolved fresh (loaders are never cached or
|
|
384
|
+
* pre-rendered). By source: a prerender entry is decoded here, a cache()
|
|
385
|
+
* record also resolves the entries above its boundary live and sets
|
|
386
|
+
* state.shouldRevalidate, and each decides which held segments are sent
|
|
387
|
+
* again.
|
|
388
|
+
*
|
|
389
|
+
* The reads and the serve share this generator: a HIT is the request hot
|
|
390
|
+
* path, and serving from a second generator behind `yield*` adds a microtask
|
|
391
|
+
* turn and two promises for each segment it yields, and once more at its end.
|
|
392
|
+
*/
|
|
393
|
+
async function* serveStoredSegments<TEnv>(
|
|
394
|
+
ctx: MatchContext<TEnv>,
|
|
395
|
+
state: MatchPipelineState,
|
|
396
|
+
upstream: AsyncGenerator<ResolvedSegment>,
|
|
397
|
+
): AsyncGenerator<ResolvedSegment> {
|
|
398
|
+
const pipelineStart = performance.now();
|
|
399
|
+
|
|
400
|
+
// Eagerly capture the HandleStore before any async operations.
|
|
401
|
+
// In workerd/Cloudflare, dynamic imports and fetch() inside the pipeline
|
|
402
|
+
// can disrupt AsyncLocalStorage, causing getRequestContext() to return
|
|
403
|
+
// undefined afterward. Capturing the reference early ensures handle replay
|
|
404
|
+
// and handler handle-push work regardless of ALS state.
|
|
405
|
+
const reqCtx = _getRequestContext<TEnv>();
|
|
406
|
+
// Only the match can determine interception; the source header proves
|
|
407
|
+
// nothing in either direction. Clear a stale reason on normal matches.
|
|
341
408
|
if (reqCtx) {
|
|
342
|
-
reqCtx._pprReplayPostMatchReason =
|
|
343
|
-
|
|
344
|
-
|
|
409
|
+
reqCtx._pprReplayPostMatchReason = ctx.isIntercept
|
|
410
|
+
? "intercept"
|
|
411
|
+
: undefined;
|
|
412
|
+
}
|
|
413
|
+
// ALS-backed: captured before the first await, like reqCtx.
|
|
414
|
+
const router = getRouterContext<TEnv>();
|
|
415
|
+
let stored: StoredSegmentSource | null = null;
|
|
416
|
+
|
|
417
|
+
// An on-demand route may have no build-baked entry yet still needs a
|
|
418
|
+
// writable-overlay lookup, so od joins pr in the gate. The retained producer
|
|
419
|
+
// is NEVER run by this pipeline: a miss falls through like pr + miss.
|
|
420
|
+
const isPassthroughPrerenderRoute = ctx.entries.some(
|
|
421
|
+
(entry) => entry.type === "route" && entry.isPassthrough === true,
|
|
422
|
+
);
|
|
423
|
+
const overlayEligible =
|
|
424
|
+
ctx.matched.od === true && !requestHeaders(ctx.request).get("X-RSC-HMR");
|
|
425
|
+
// Actions normally re-render fresh and skip the prerender store. But a pure
|
|
426
|
+
// Prerender route's handler is evicted at build, so there is no fresh
|
|
427
|
+
// handler to run on an action re-render -- without the fallback the
|
|
428
|
+
// re-render falls through to the evicted handler and throws "No prerender
|
|
429
|
+
// data found". Serve the prerendered entry instead (the action ran already;
|
|
430
|
+
// its result is applied client-side via useActionState). A plain on-demand
|
|
431
|
+
// route is the same case, served from the overlay first (an overlay entry
|
|
432
|
+
// is newer than the build one, and may be the only one). Passthrough
|
|
433
|
+
// routes keep a liveHandler, so a live render (rendersLive) re-renders
|
|
434
|
+
// them fresh. A pass cut at an error boundary walks live too
|
|
435
|
+
// (ActionContext.boundary, a follow-up's held one): the evicted handler is
|
|
436
|
+
// under the cut.
|
|
437
|
+
if (
|
|
438
|
+
(prerenderStoreShortCircuits(ctx.matched.pr, ctx.request) ||
|
|
439
|
+
overlayEligible) &&
|
|
440
|
+
(!rendersLive(ctx) || !isPassthroughPrerenderRoute) &&
|
|
441
|
+
!ctx.actionContext?.boundary &&
|
|
442
|
+
!ctx.heldBoundary
|
|
443
|
+
) {
|
|
444
|
+
await ensurePrerenderDeps();
|
|
445
|
+
stored = await readPrerenderSource(
|
|
446
|
+
ctx,
|
|
447
|
+
reqCtx,
|
|
448
|
+
isPassthroughPrerenderRoute,
|
|
449
|
+
// Per-request writable prerender overlay (durable), resolved by the handler.
|
|
450
|
+
reqCtx?._prerender,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (
|
|
455
|
+
!stored &&
|
|
456
|
+
!rendersLive(ctx) &&
|
|
457
|
+
!ctx.matched.pr &&
|
|
458
|
+
globalThis.__PRERENDER_DEV_URL &&
|
|
459
|
+
ctx.entries.some(
|
|
460
|
+
(e) =>
|
|
461
|
+
(e.type === "layout" || e.type === "route" || e.type === "parallel") &&
|
|
462
|
+
e.isStaticPrerender,
|
|
463
|
+
)
|
|
464
|
+
) {
|
|
465
|
+
await ensurePrerenderDeps();
|
|
466
|
+
stored = await readPrerenderSource(
|
|
467
|
+
ctx,
|
|
468
|
+
reqCtx,
|
|
469
|
+
isPassthroughPrerenderRoute,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
stored ??= await readSegmentCacheSource(ctx, reqCtx);
|
|
474
|
+
if (!stored) {
|
|
475
|
+
yield* upstream;
|
|
476
|
+
const ms = ctx.metricsStore;
|
|
477
|
+
if (ms) {
|
|
478
|
+
ms.metrics.push({
|
|
479
|
+
label: "pipeline:cache-miss",
|
|
480
|
+
duration: performance.now() - pipelineStart,
|
|
481
|
+
startTime: pipelineStart - ms.requestStart,
|
|
482
|
+
});
|
|
345
483
|
}
|
|
346
|
-
|
|
484
|
+
return;
|
|
347
485
|
}
|
|
486
|
+
const source: StoredSegmentSource = stored;
|
|
487
|
+
|
|
488
|
+
const fromCache = source.kind === "segment-cache";
|
|
489
|
+
const storedSends = fromCache
|
|
490
|
+
? undefined
|
|
491
|
+
: storedEntrySends(ctx, source.kind === "prerender-overlay", router);
|
|
492
|
+
const segments = fromCache
|
|
493
|
+
? source.segments
|
|
494
|
+
: await decodeStoredEntry(source.entry, ctx, reqCtx, storedSends!);
|
|
495
|
+
|
|
496
|
+
// Entries above the cache() boundary are not in the record: resolve them
|
|
497
|
+
// fresh, exactly as an uncached render of this request would.
|
|
498
|
+
const scope = fromCache ? source.scope : undefined;
|
|
499
|
+
const boundary = scope?.boundary;
|
|
500
|
+
const boundaryIndex =
|
|
501
|
+
boundary === undefined
|
|
502
|
+
? 0
|
|
503
|
+
: Math.max(
|
|
504
|
+
0,
|
|
505
|
+
ctx.entries.findIndex((e) => e.shortCode === boundary),
|
|
506
|
+
);
|
|
507
|
+
let liveSegments: ResolvedSegment[] = [];
|
|
508
|
+
let liveMatchedIds: string[] = [];
|
|
509
|
+
if (scope && boundaryIndex > 0) {
|
|
510
|
+
const liveEntries = ctx.entries.slice(0, boundaryIndex);
|
|
511
|
+
const live: { segments: ResolvedSegment[]; matchedIds: string[] } =
|
|
512
|
+
await ctx.Store.run(async () => {
|
|
513
|
+
// The full chain keeps a bare cache() marker, which also sits in
|
|
514
|
+
// its layout's orphan list, out of the live pass: the record
|
|
515
|
+
// supplies it and its subtree (issue #918).
|
|
516
|
+
const result = await router.resolveAllSegments(
|
|
517
|
+
ctx.isFullMatch
|
|
518
|
+
? freshPassInput(
|
|
519
|
+
ctx.handlerContext,
|
|
520
|
+
ctx.routeKey,
|
|
521
|
+
ctx.matched.params,
|
|
522
|
+
)
|
|
523
|
+
: resolutionPassInput(ctx),
|
|
524
|
+
liveEntries,
|
|
525
|
+
{
|
|
526
|
+
interceptResult: ctx.interceptResult,
|
|
527
|
+
localRouteName: ctx.localRouteName,
|
|
528
|
+
pathname: ctx.pathname,
|
|
529
|
+
chain: ctx.entries,
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
return ctx.isFullMatch
|
|
533
|
+
? {
|
|
534
|
+
segments: result.segments,
|
|
535
|
+
matchedIds: result.segments.map((s) => s.id),
|
|
536
|
+
}
|
|
537
|
+
: result;
|
|
538
|
+
});
|
|
539
|
+
// The record supplies every covered segment.
|
|
540
|
+
liveSegments = live.segments.filter(
|
|
541
|
+
(s) => !scope.covers(s.id, s.namespace),
|
|
542
|
+
);
|
|
543
|
+
liveMatchedIds = live.matchedIds.filter((id) => !scope.covers(id));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
state.cacheHit = true;
|
|
547
|
+
state.cacheSource = fromCache ? "runtime" : "prerender";
|
|
548
|
+
if (fromCache) state.shouldRevalidate = source.shouldRevalidate;
|
|
549
|
+
state.cachedSegments = segments;
|
|
550
|
+
const cachedMatchedIds = [...liveMatchedIds, ...segments.map((s) => s.id)];
|
|
551
|
+
state.cachedMatchedIds = cachedMatchedIds;
|
|
552
|
+
|
|
553
|
+
const sends = fromCache
|
|
554
|
+
? cachedSegmentSends(ctx, source.shouldRevalidate, reqCtx, router)
|
|
555
|
+
: storedSends!;
|
|
556
|
+
const restoreHandles = fromCache ? source.restoreHandles : undefined;
|
|
557
|
+
const sentIds = restoreHandles && new Set<string>();
|
|
558
|
+
|
|
559
|
+
yield* liveSegments;
|
|
348
560
|
|
|
349
|
-
// For partial navigation, nullify components the client already has
|
|
350
|
-
// so parent layouts stay live (client keeps its existing versions).
|
|
351
|
-
// When params changed (e.g., different guide slug), the segments have
|
|
352
|
-
// different content, so we must NOT nullify.
|
|
353
|
-
const paramsChanged =
|
|
354
|
-
!ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
|
|
355
|
-
// The action defaults' route segment (revalidation.ts), not a cache skip.
|
|
356
|
-
const resendRouteSegments =
|
|
357
|
-
replaceable === true && (ctx.isAction || ctx.stale);
|
|
358
561
|
for (const segment of segments) {
|
|
359
562
|
if (interceptFillSkips(ctx, segment)) continue;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
563
|
+
let send = sends(segment);
|
|
564
|
+
if (typeof send !== "boolean") {
|
|
565
|
+
// A cache() record segment's revalidate() rules decided. Awaited here,
|
|
566
|
+
// not chained in cachedSegmentSends: a .then() costs a turn and a
|
|
567
|
+
// promise per segment.
|
|
568
|
+
send = await send;
|
|
569
|
+
observeEvent({
|
|
570
|
+
type: "revalidation.decision",
|
|
571
|
+
timestamp: performance.now(),
|
|
572
|
+
segmentId: segment.id,
|
|
573
|
+
pathname: ctx.pathname,
|
|
574
|
+
routeKey: ctx.routeKey,
|
|
575
|
+
shouldRevalidate: send,
|
|
576
|
+
});
|
|
369
577
|
}
|
|
578
|
+
if (!send) keepClientSegment(segment);
|
|
579
|
+
else sentIds?.add(segment.id);
|
|
370
580
|
yield segment;
|
|
371
581
|
}
|
|
372
582
|
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
583
|
+
// A partial cache() HIT replays handler values for the segments it sends
|
|
584
|
+
// only, decided above after the live entries: a kept segment keeps the
|
|
585
|
+
// client's, as on a live walk (handle-snapshot.ts restoreHandles;
|
|
586
|
+
// decodeStoredEntry for a bake).
|
|
587
|
+
if (sentIds) await restoreHandles!(sentIds);
|
|
588
|
+
|
|
589
|
+
// The stored handler values stand where the MISS's handlers pushed them:
|
|
590
|
+
// behind what a segment's loaders push before the walk reaches its handler
|
|
591
|
+
// (HandleStore.holdRecordedValues). A fill holds nothing: on a fill MISS the
|
|
592
|
+
// browser's merge appends what the fill runs to the held handler values.
|
|
593
|
+
if (!isFillRequest(ctx.handlerContext)) {
|
|
594
|
+
reqCtx?._handleStore.holdRecordedValues(segments.map((s) => s.id));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (reqCtx) {
|
|
598
|
+
if (reqCtx._treeHasStreaming === undefined) {
|
|
599
|
+
reqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
|
|
600
|
+
}
|
|
601
|
+
reqCtx._resolveRenderBarrier(
|
|
602
|
+
liveSegments.length > 0 ? [...liveSegments, ...segments] : segments,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Resolve loaders fresh. The live pass already resolved the loaders of the
|
|
607
|
+
// entries above the boundary.
|
|
608
|
+
const loaderStart = performance.now();
|
|
609
|
+
const loaderEntries = ctx.entries.slice(boundaryIndex);
|
|
610
|
+
const { resolveLoadersOnly } = router;
|
|
611
|
+
if (resolveLoadersOnly) {
|
|
612
|
+
const loaderResult = await ctx.Store.run(() =>
|
|
613
|
+
resolveLoadersOnly(
|
|
614
|
+
ctx.isFullMatch
|
|
615
|
+
? freshPassInput(ctx.handlerContext, ctx.routeKey, ctx.matched.params)
|
|
616
|
+
: {
|
|
617
|
+
...resolutionPassInput(ctx),
|
|
618
|
+
// Loaders are never cached in the segment cache, so segment
|
|
619
|
+
// staleness must not propagate; browser-sent staleness
|
|
620
|
+
// (ctx.stale) still must.
|
|
621
|
+
stale: ctx.stale || undefined,
|
|
622
|
+
},
|
|
623
|
+
loaderEntries,
|
|
624
|
+
{ interceptResult: ctx.interceptResult },
|
|
625
|
+
),
|
|
626
|
+
);
|
|
627
|
+
state.matchedIds = ctx.isFullMatch
|
|
628
|
+
? cachedMatchedIds
|
|
629
|
+
: [...cachedMatchedIds, ...loaderResult.matchedIds];
|
|
630
|
+
for (const segment of loaderResult.segments) {
|
|
631
|
+
yield segment;
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
state.matchedIds = cachedMatchedIds;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const ms = ctx.metricsStore;
|
|
638
|
+
if (ms) {
|
|
639
|
+
const loaderEnd = performance.now();
|
|
640
|
+
ms.metrics.push({
|
|
641
|
+
label: "pipeline:loader-resolve",
|
|
642
|
+
duration: loaderEnd - loaderStart,
|
|
643
|
+
startTime: loaderStart - ms.requestStart,
|
|
644
|
+
depth: 1,
|
|
645
|
+
});
|
|
646
|
+
ms.metrics.push({
|
|
647
|
+
label: "pipeline:cache-hit",
|
|
648
|
+
duration: loaderEnd - pipelineStart,
|
|
649
|
+
startTime: pipelineStart - ms.requestStart,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
383
652
|
}
|
|
384
653
|
|
|
385
654
|
/**
|
|
@@ -390,7 +659,7 @@ async function* yieldFromStore<TEnv>(
|
|
|
390
659
|
* misses the store and renders live, and replay — including its heal
|
|
391
660
|
* capture — must stay available for it (withCacheStore records the doc
|
|
392
661
|
* record on that path; state.cacheSource is not "prerender"). The store
|
|
393
|
-
* memoizes per routeKey/paramHash, so this probe and
|
|
662
|
+
* memoizes per routeKey/paramHash, so this probe and readPrerenderSource's
|
|
394
663
|
* subsequent get() share one underlying load.
|
|
395
664
|
*/
|
|
396
665
|
export async function prerenderEntryExists(
|
|
@@ -486,23 +755,17 @@ export function scheduleOverlayRevalidation<TEnv>(
|
|
|
486
755
|
}
|
|
487
756
|
|
|
488
757
|
/**
|
|
489
|
-
*
|
|
490
|
-
* a
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
* through to the normal pipeline so intercept-resolution can run. Callers must
|
|
494
|
-
* guard on `prerenderStoreInstance` after `ensurePrerenderDeps()`.
|
|
758
|
+
* The prerendered (build-time cached) entry for the current route, or null on
|
|
759
|
+
* a miss. Intercept navigations consult only the intercept-specific entry
|
|
760
|
+
* (`paramHash + "/i"`); a miss there falls through to the normal pipeline so
|
|
761
|
+
* intercept-resolution can run. Callers run `ensurePrerenderDeps()` first.
|
|
495
762
|
*/
|
|
496
|
-
async function
|
|
763
|
+
async function readPrerenderSource<TEnv>(
|
|
497
764
|
ctx: MatchContext<TEnv>,
|
|
498
|
-
state: MatchPipelineState,
|
|
499
|
-
pipelineStart: number,
|
|
500
765
|
reqCtx: RequestContext<TEnv> | undefined,
|
|
501
|
-
resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
|
|
502
|
-
resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
|
|
503
766
|
isPassthroughPrerenderRoute: boolean,
|
|
504
767
|
overlay?: ResolvedPrerender,
|
|
505
|
-
):
|
|
768
|
+
): Promise<StoredSegmentSource | null> {
|
|
506
769
|
// 1. Writable durable overlay (per-request, env-scoped), read before the
|
|
507
770
|
// bundled manifest: it is always newer. Only on-demand routes are read
|
|
508
771
|
// (router.prerender() refuses non-od routes, so a pr-only route always
|
|
@@ -551,19 +814,9 @@ async function* tryPrerenderLookup<TEnv>(
|
|
|
551
814
|
(
|
|
552
815
|
ctx.handlerContext as InternalHandlerContext<any, TEnv>
|
|
553
816
|
)._prerenderRemoved = true;
|
|
554
|
-
return
|
|
817
|
+
return null;
|
|
555
818
|
}
|
|
556
|
-
|
|
557
|
-
stored.entry,
|
|
558
|
-
ctx,
|
|
559
|
-
state,
|
|
560
|
-
pipelineStart,
|
|
561
|
-
reqCtx,
|
|
562
|
-
resolveLoadersOnly,
|
|
563
|
-
resolveLoadersOnlyWithRevalidation,
|
|
564
|
-
true,
|
|
565
|
-
);
|
|
566
|
-
return true;
|
|
819
|
+
return { kind: "prerender-overlay", entry: stored.entry };
|
|
567
820
|
}
|
|
568
821
|
}
|
|
569
822
|
|
|
@@ -578,21 +831,143 @@ async function* tryPrerenderLookup<TEnv>(
|
|
|
578
831
|
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
579
832
|
},
|
|
580
833
|
);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
834
|
+
// The variant holds every intercept the build found; serve the one the
|
|
835
|
+
// match selected, or render live.
|
|
836
|
+
const selected = ctx.isIntercept ? ctx.interceptResult : null;
|
|
837
|
+
const served =
|
|
838
|
+
entry && selected
|
|
839
|
+
? storeModule!.selectInterceptVariant(
|
|
840
|
+
entry,
|
|
841
|
+
interceptSlotId(selected.entry, selected.intercept),
|
|
842
|
+
selected.intercept.routeName,
|
|
843
|
+
)
|
|
844
|
+
: entry;
|
|
845
|
+
if (served) return { kind: "prerender-build", entry: served };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* The cache() record that answers this request, or null when it renders: no
|
|
853
|
+
* enabled scope, a live render, a request that reads no record, or a lookup
|
|
854
|
+
* that did not hit.
|
|
855
|
+
*/
|
|
856
|
+
async function readSegmentCacheSource<TEnv>(
|
|
857
|
+
ctx: MatchContext<TEnv>,
|
|
858
|
+
reqCtx: RequestContext<TEnv> | undefined,
|
|
859
|
+
): Promise<StoredSegmentSource | null> {
|
|
860
|
+
// A document HIT tail replays the handler layer through the implicit doc
|
|
861
|
+
// scope and never runs a handler: without that scope (a context that lost
|
|
862
|
+
// the marker before the scope resolved), fail like a lookup miss instead
|
|
863
|
+
// of falling through to the handlers below.
|
|
864
|
+
const tailMarker = reqCtx?._shellImplicitCache;
|
|
865
|
+
if (tailMarker?.docTail && !ctx.cacheScope?.isShellImplicitDocScope) {
|
|
866
|
+
throw new ShellRecordUnavailableError(tailMarker.fixedDocKey);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// A prefetch that defers a layout above the boundary (prefetch: false)
|
|
870
|
+
// emits nothing the record holds: reading it would replay the handle
|
|
871
|
+
// pushes of segments the response does not carry. withCacheStore skips
|
|
872
|
+
// the write for the same request.
|
|
873
|
+
if (
|
|
874
|
+
rendersLive(ctx) ||
|
|
875
|
+
!ctx.cacheScope?.enabled ||
|
|
876
|
+
!recordUse(ctx.handlerContext).read
|
|
877
|
+
) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Only a ppr route's records hold loader pushes, and only its shell pins
|
|
882
|
+
// loaders (CachedEntryData.handleOwners, the loader seed): any other
|
|
883
|
+
// route's record restores as a plain replay.
|
|
884
|
+
const leaf = ctx.entries[ctx.entries.length - 1];
|
|
885
|
+
const ownedPushes =
|
|
886
|
+
leaf !== undefined && isPprEntry(leaf)
|
|
887
|
+
? () => loaderPins(ctx.entries, reqCtx)
|
|
888
|
+
: undefined;
|
|
889
|
+
// A partial request restores the handle values once it decided which
|
|
890
|
+
// segments it sends (serveStoredSegments).
|
|
891
|
+
const deferHandles = !ctx.isFullMatch;
|
|
892
|
+
const explicitLookup = await ctx.cacheScope.lookupRouteDetailed(
|
|
893
|
+
ctx.pathname,
|
|
894
|
+
ctx.matched.params,
|
|
895
|
+
interceptRecordKey(ctx.interceptResult),
|
|
896
|
+
ownedPushes,
|
|
897
|
+
deferHandles,
|
|
898
|
+
);
|
|
899
|
+
let cacheResult =
|
|
900
|
+
explicitLookup.status === "hit" ? explicitLookup.result : null;
|
|
901
|
+
// The scope whose record answered decides which entries it covers.
|
|
902
|
+
let hitScope: CacheScope = ctx.cacheScope;
|
|
903
|
+
|
|
904
|
+
// PPR navigation replay composed with a route-derived cache() scope. The
|
|
905
|
+
// explicit tier stays authoritative: its hit serves under its own
|
|
906
|
+
// key/ttl/swr semantics and reports `explicit-cache-hit` — never a false
|
|
907
|
+
// replay HIT. ONLY a true `miss` lets the seeded doc record supply the
|
|
908
|
+
// match (the marker's onHit observer then reports the true HIT). The
|
|
909
|
+
// other outcomes render fresh: `bypass` (cache(false), a false
|
|
910
|
+
// condition() — absolute opt-outs even when the pre-read gate saw a
|
|
911
|
+
// different condition() result — or no store) and `error` (a throwing
|
|
912
|
+
// key()/keyGenerator/store.get keeps lookupRoute's render-uncached
|
|
913
|
+
// contract; the canonical record must not serve across a broken key
|
|
914
|
+
// partition). The outcome comes from the lookup itself, not a re-run of
|
|
915
|
+
// the condition, so a flapping predicate cannot re-admit the fallback.
|
|
916
|
+
// Gated on the marker's `onExplicitHit`, set ONLY on the
|
|
917
|
+
// navigation-replay serve path: a CAPTURE render must never fall back
|
|
918
|
+
// here — its marker store reads through to the real store, and a
|
|
919
|
+
// doc-keyed hit would replay the previous generation's segments instead
|
|
920
|
+
// of re-running handlers (breaking SWR recapture freshness). Intercepts
|
|
921
|
+
// stay source-dependent on their normal cache path (match-api never arms
|
|
922
|
+
// replay for them).
|
|
923
|
+
const replayMarker = reqCtx?._shellImplicitCache;
|
|
924
|
+
if (
|
|
925
|
+
replayMarker?.onExplicitHit &&
|
|
926
|
+
!ctx.isIntercept &&
|
|
927
|
+
!ctx.cacheScope.isShellImplicitDocScope
|
|
928
|
+
) {
|
|
929
|
+
if (explicitLookup.status === "hit") {
|
|
930
|
+
replayMarker.onExplicitHit();
|
|
931
|
+
} else if (explicitLookup.status === "miss" && replayMarker.store) {
|
|
932
|
+
// The store gate keeps report-only markers (installed on the
|
|
933
|
+
// no-eligible-snapshot path purely for truthful status) inert: a
|
|
934
|
+
// store-less marker minting a doc scope here would resolve the APP
|
|
935
|
+
// store and read the REAL doc: partition — a cross-partition serve.
|
|
936
|
+
hitScope = createShellImplicitDocScope(replayMarker);
|
|
937
|
+
cacheResult = await hitScope.lookupRoute(
|
|
938
|
+
ctx.pathname,
|
|
939
|
+
ctx.matched.params,
|
|
940
|
+
undefined,
|
|
941
|
+
ownedPushes,
|
|
942
|
+
deferHandles,
|
|
590
943
|
);
|
|
591
|
-
|
|
944
|
+
} else if (explicitLookup.status === "bypass") {
|
|
945
|
+
// condition() refused at lookup time (the gate only pre-decides the
|
|
946
|
+
// static cache(false) case) — report cache-disabled truthfully.
|
|
947
|
+
replayMarker.onExplicitBypass?.();
|
|
592
948
|
}
|
|
949
|
+
// "error" stays unreported: the render is fresh and the seeded record
|
|
950
|
+
// was not consulted, which is exactly what snapshot-miss describes; the
|
|
951
|
+
// store already routed the failure through reportCacheError.
|
|
593
952
|
}
|
|
594
953
|
|
|
595
|
-
|
|
954
|
+
if (!cacheResult) {
|
|
955
|
+
// A document shell HIT tail replays the handler layer from the entry's
|
|
956
|
+
// doc record and must never run a handler behind the committed prelude:
|
|
957
|
+
// a record that did not hit (it failed to decode, or the entry lost it)
|
|
958
|
+
// ends the tail here; serveShellHit degrades the response.
|
|
959
|
+
if (replayMarker?.docTail) {
|
|
960
|
+
throw new ShellRecordUnavailableError(replayMarker.fixedDocKey);
|
|
961
|
+
}
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
kind: "segment-cache",
|
|
966
|
+
segments: cacheResult.segments,
|
|
967
|
+
shouldRevalidate: cacheResult.shouldRevalidate,
|
|
968
|
+
scope: hitScope,
|
|
969
|
+
restoreHandles: cacheResult.restoreHandles,
|
|
970
|
+
};
|
|
596
971
|
}
|
|
597
972
|
|
|
598
973
|
/**
|
|
@@ -620,442 +995,18 @@ export type GeneratorMiddleware<T> = (
|
|
|
620
995
|
/**
|
|
621
996
|
* Creates cache lookup middleware
|
|
622
997
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
* -
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
998
|
+
* The middleware is serveStoredSegments. It serves a stored match when one
|
|
999
|
+
* answers, in this order: the prerender overlay and build store (Prerender
|
|
1000
|
+
* and on-demand routes), the dev Static store, then the route's cache()
|
|
1001
|
+
* record. It yields the stored segments and the loaders resolved fresh and
|
|
1002
|
+
* sets state.cacheHit (and state.shouldRevalidate for a cache() record, for
|
|
1003
|
+
* background SWR).
|
|
629
1004
|
*
|
|
630
|
-
* If
|
|
631
|
-
* - Passes through to next middleware
|
|
1005
|
+
* If nothing answers, passes through to the next middleware.
|
|
632
1006
|
*/
|
|
633
1007
|
export function withCacheLookup<TEnv>(
|
|
634
1008
|
ctx: MatchContext<TEnv>,
|
|
635
1009
|
state: MatchPipelineState,
|
|
636
1010
|
): GeneratorMiddleware<ResolvedSegment> {
|
|
637
|
-
return
|
|
638
|
-
source: AsyncGenerator<ResolvedSegment>,
|
|
639
|
-
): AsyncGenerator<ResolvedSegment> {
|
|
640
|
-
const pipelineStart = performance.now();
|
|
641
|
-
const ms = ctx.metricsStore;
|
|
642
|
-
|
|
643
|
-
// Eagerly capture the HandleStore before any async operations.
|
|
644
|
-
// In workerd/Cloudflare, dynamic imports and fetch() inside the pipeline
|
|
645
|
-
// can disrupt AsyncLocalStorage, causing getRequestContext() to return
|
|
646
|
-
// undefined afterward. Capturing the reference early ensures handle replay
|
|
647
|
-
// and handler handle-push work regardless of ALS state.
|
|
648
|
-
const pipelineReqCtx = _getRequestContext<TEnv>();
|
|
649
|
-
// Only the match can determine interception; the source header proves
|
|
650
|
-
// nothing in either direction. Clear a stale reason on normal matches.
|
|
651
|
-
if (pipelineReqCtx) {
|
|
652
|
-
pipelineReqCtx._pprReplayPostMatchReason = ctx.isIntercept
|
|
653
|
-
? "intercept"
|
|
654
|
-
: undefined;
|
|
655
|
-
}
|
|
656
|
-
// Per-request writable prerender overlay (durable), resolved by the handler.
|
|
657
|
-
const prerenderOverlay = pipelineReqCtx?._prerender;
|
|
658
|
-
|
|
659
|
-
const {
|
|
660
|
-
evaluateRevalidation,
|
|
661
|
-
buildEntryRevalidateMap,
|
|
662
|
-
resolveLoadersOnlyWithRevalidation,
|
|
663
|
-
resolveLoadersOnly,
|
|
664
|
-
resolveAllSegments,
|
|
665
|
-
resolveAllSegmentsWithRevalidation,
|
|
666
|
-
} = getRouterContext<TEnv>();
|
|
667
|
-
|
|
668
|
-
// An on-demand route may have no build-baked entry yet still needs a
|
|
669
|
-
// writable-overlay lookup, so od joins pr in the gate. The retained producer
|
|
670
|
-
// is NEVER run by this pipeline: a miss falls through like pr + miss.
|
|
671
|
-
const isPassthroughPrerenderRoute = ctx.entries.some(
|
|
672
|
-
(entry) => entry.type === "route" && entry.isPassthrough === true,
|
|
673
|
-
);
|
|
674
|
-
const overlayEligible =
|
|
675
|
-
ctx.matched.od === true && !requestHeaders(ctx.request).get("X-RSC-HMR");
|
|
676
|
-
if (
|
|
677
|
-
prerenderStoreShortCircuits(ctx.matched.pr, ctx.request) ||
|
|
678
|
-
overlayEligible
|
|
679
|
-
) {
|
|
680
|
-
// Actions normally re-render fresh and skip the prerender store. But a pure
|
|
681
|
-
// Prerender route's handler is evicted at build, so there is no fresh
|
|
682
|
-
// handler to run on an action re-render -- without the fallback the
|
|
683
|
-
// re-render falls through to the evicted handler and throws "No prerender
|
|
684
|
-
// data found". Serve the prerendered entry instead (the action ran already;
|
|
685
|
-
// its result is applied client-side via useActionState). A plain on-demand
|
|
686
|
-
// route is the same case, served from the overlay first (an overlay entry
|
|
687
|
-
// is newer than the build one, and may be the only one). Passthrough
|
|
688
|
-
// routes keep a liveHandler, so a live render (rendersLive) re-renders
|
|
689
|
-
// them fresh.
|
|
690
|
-
if (!rendersLive(ctx) || !isPassthroughPrerenderRoute) {
|
|
691
|
-
await ensurePrerenderDeps();
|
|
692
|
-
if (prerenderStoreInstance || prerenderOverlay) {
|
|
693
|
-
const served = yield* tryPrerenderLookup(
|
|
694
|
-
ctx,
|
|
695
|
-
state,
|
|
696
|
-
pipelineStart,
|
|
697
|
-
pipelineReqCtx,
|
|
698
|
-
resolveLoadersOnly,
|
|
699
|
-
resolveLoadersOnlyWithRevalidation,
|
|
700
|
-
isPassthroughPrerenderRoute,
|
|
701
|
-
prerenderOverlay,
|
|
702
|
-
);
|
|
703
|
-
if (served) return;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
if (
|
|
709
|
-
!rendersLive(ctx) &&
|
|
710
|
-
!ctx.matched.pr &&
|
|
711
|
-
globalThis.__PRERENDER_DEV_URL
|
|
712
|
-
) {
|
|
713
|
-
const hasStatic = ctx.entries.some(
|
|
714
|
-
(e) =>
|
|
715
|
-
(e.type === "layout" ||
|
|
716
|
-
e.type === "route" ||
|
|
717
|
-
e.type === "parallel") &&
|
|
718
|
-
e.isStaticPrerender,
|
|
719
|
-
);
|
|
720
|
-
if (hasStatic) {
|
|
721
|
-
await ensurePrerenderDeps();
|
|
722
|
-
if (prerenderStoreInstance) {
|
|
723
|
-
const served = yield* tryPrerenderLookup(
|
|
724
|
-
ctx,
|
|
725
|
-
state,
|
|
726
|
-
pipelineStart,
|
|
727
|
-
pipelineReqCtx,
|
|
728
|
-
resolveLoadersOnly,
|
|
729
|
-
resolveLoadersOnlyWithRevalidation,
|
|
730
|
-
isPassthroughPrerenderRoute,
|
|
731
|
-
);
|
|
732
|
-
if (served) return;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// A document HIT tail replays the handler layer through the implicit doc
|
|
738
|
-
// scope and never runs a handler: without that scope (a context that lost
|
|
739
|
-
// the marker before the scope resolved), fail like a lookup miss instead
|
|
740
|
-
// of falling through to the handlers below.
|
|
741
|
-
const tailMarker = pipelineReqCtx?._shellImplicitCache;
|
|
742
|
-
if (tailMarker?.docTail && !ctx.cacheScope?.isShellImplicitDocScope) {
|
|
743
|
-
throw new ShellRecordUnavailableError(tailMarker.fixedDocKey);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// A prefetch that defers a layout above the boundary (prefetch: false)
|
|
747
|
-
// emits nothing the record holds: reading it would replay the handle
|
|
748
|
-
// pushes of segments the response does not carry. withCacheStore skips
|
|
749
|
-
// the write for the same request.
|
|
750
|
-
if (
|
|
751
|
-
rendersLive(ctx) ||
|
|
752
|
-
!ctx.cacheScope?.enabled ||
|
|
753
|
-
!recordUse(ctx.handlerContext).read
|
|
754
|
-
) {
|
|
755
|
-
yield* source;
|
|
756
|
-
if (ms) {
|
|
757
|
-
ms.metrics.push({
|
|
758
|
-
label: "pipeline:cache-miss",
|
|
759
|
-
duration: performance.now() - pipelineStart,
|
|
760
|
-
startTime: pipelineStart - ms.requestStart,
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Only a ppr route's records hold loader pushes, and only its shell pins
|
|
767
|
-
// loaders (CachedEntryData.handleOwners, the loader seed): any other
|
|
768
|
-
// route's record restores as a plain replay.
|
|
769
|
-
const leaf = ctx.entries[ctx.entries.length - 1];
|
|
770
|
-
const ownedPushes =
|
|
771
|
-
leaf !== undefined && isPprEntry(leaf)
|
|
772
|
-
? () => loaderPins(ctx.entries, pipelineReqCtx)
|
|
773
|
-
: undefined;
|
|
774
|
-
const explicitLookup = await ctx.cacheScope.lookupRouteDetailed(
|
|
775
|
-
ctx.pathname,
|
|
776
|
-
ctx.matched.params,
|
|
777
|
-
ctx.isIntercept,
|
|
778
|
-
ownedPushes,
|
|
779
|
-
);
|
|
780
|
-
let cacheResult =
|
|
781
|
-
explicitLookup.status === "hit" ? explicitLookup.result : null;
|
|
782
|
-
// The scope whose record answered decides which entries it covers.
|
|
783
|
-
let hitScope: CacheScope = ctx.cacheScope;
|
|
784
|
-
|
|
785
|
-
// PPR navigation replay composed with a route-derived cache() scope. The
|
|
786
|
-
// explicit tier stays authoritative: its hit serves under its own
|
|
787
|
-
// key/ttl/swr semantics and reports `explicit-cache-hit` — never a false
|
|
788
|
-
// replay HIT. ONLY a true `miss` lets the seeded doc record supply the
|
|
789
|
-
// match (the marker's onHit observer then reports the true HIT). The
|
|
790
|
-
// other outcomes render fresh: `bypass` (cache(false), a false
|
|
791
|
-
// condition() — absolute opt-outs even when the pre-read gate saw a
|
|
792
|
-
// different condition() result — or no store) and `error` (a throwing
|
|
793
|
-
// key()/keyGenerator/store.get keeps lookupRoute's render-uncached
|
|
794
|
-
// contract; the canonical record must not serve across a broken key
|
|
795
|
-
// partition). The outcome comes from the lookup itself, not a re-run of
|
|
796
|
-
// the condition, so a flapping predicate cannot re-admit the fallback.
|
|
797
|
-
// Gated on the marker's `onExplicitHit`, set ONLY on the
|
|
798
|
-
// navigation-replay serve path: a CAPTURE render must never fall back
|
|
799
|
-
// here — its marker store reads through to the real store, and a
|
|
800
|
-
// doc-keyed hit would replay the previous generation's segments instead
|
|
801
|
-
// of re-running handlers (breaking SWR recapture freshness). Intercepts
|
|
802
|
-
// stay source-dependent on their normal cache path (match-api never arms
|
|
803
|
-
// replay for them).
|
|
804
|
-
const replayMarker = pipelineReqCtx?._shellImplicitCache;
|
|
805
|
-
if (
|
|
806
|
-
replayMarker?.onExplicitHit &&
|
|
807
|
-
!ctx.isIntercept &&
|
|
808
|
-
!ctx.cacheScope.isShellImplicitDocScope
|
|
809
|
-
) {
|
|
810
|
-
if (explicitLookup.status === "hit") {
|
|
811
|
-
replayMarker.onExplicitHit();
|
|
812
|
-
} else if (explicitLookup.status === "miss" && replayMarker.store) {
|
|
813
|
-
// The store gate keeps report-only markers (installed on the
|
|
814
|
-
// no-eligible-snapshot path purely for truthful status) inert: a
|
|
815
|
-
// store-less marker minting a doc scope here would resolve the APP
|
|
816
|
-
// store and read the REAL doc: partition — a cross-partition serve.
|
|
817
|
-
hitScope = createShellImplicitDocScope(replayMarker);
|
|
818
|
-
cacheResult = await hitScope.lookupRoute(
|
|
819
|
-
ctx.pathname,
|
|
820
|
-
ctx.matched.params,
|
|
821
|
-
ctx.isIntercept,
|
|
822
|
-
ownedPushes,
|
|
823
|
-
);
|
|
824
|
-
} else if (explicitLookup.status === "bypass") {
|
|
825
|
-
// condition() refused at lookup time (the gate only pre-decides the
|
|
826
|
-
// static cache(false) case) — report cache-disabled truthfully.
|
|
827
|
-
replayMarker.onExplicitBypass?.();
|
|
828
|
-
}
|
|
829
|
-
// "error" stays unreported: the render is fresh and the seeded record
|
|
830
|
-
// was not consulted, which is exactly what snapshot-miss describes; the
|
|
831
|
-
// store already routed the failure through reportCacheError.
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (!cacheResult) {
|
|
835
|
-
// A document shell HIT tail replays the handler layer from the entry's
|
|
836
|
-
// doc record and must never run a handler behind the committed prelude:
|
|
837
|
-
// a record that did not hit (it failed to decode, or the entry lost it)
|
|
838
|
-
// ends the tail here; serveShellHit degrades the response.
|
|
839
|
-
if (replayMarker?.docTail) {
|
|
840
|
-
throw new ShellRecordUnavailableError(replayMarker.fixedDocKey);
|
|
841
|
-
}
|
|
842
|
-
yield* source;
|
|
843
|
-
if (ms) {
|
|
844
|
-
ms.metrics.push({
|
|
845
|
-
label: "pipeline:cache-miss",
|
|
846
|
-
duration: performance.now() - pipelineStart,
|
|
847
|
-
startTime: pipelineStart - ms.requestStart,
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Entries above the cache() boundary are not in the record: resolve them
|
|
854
|
-
// fresh, exactly as an uncached render of this request would.
|
|
855
|
-
const boundaryIndex =
|
|
856
|
-
hitScope.boundary === undefined
|
|
857
|
-
? 0
|
|
858
|
-
: Math.max(
|
|
859
|
-
0,
|
|
860
|
-
ctx.entries.findIndex((e) => e.shortCode === hitScope.boundary),
|
|
861
|
-
);
|
|
862
|
-
let liveSegments: ResolvedSegment[] = [];
|
|
863
|
-
let liveMatchedIds: string[] = [];
|
|
864
|
-
if (boundaryIndex > 0) {
|
|
865
|
-
const liveEntries = ctx.entries.slice(0, boundaryIndex);
|
|
866
|
-
const live: { segments: ResolvedSegment[]; matchedIds: string[] } =
|
|
867
|
-
await ctx.Store.run(async () => {
|
|
868
|
-
if (!ctx.isFullMatch) {
|
|
869
|
-
return resolveAllSegmentsWithRevalidation(
|
|
870
|
-
liveEntries,
|
|
871
|
-
ctx.routeKey,
|
|
872
|
-
ctx.matched.params,
|
|
873
|
-
ctx.handlerContext,
|
|
874
|
-
ctx.clientSegmentSet,
|
|
875
|
-
ctx.prevParams,
|
|
876
|
-
ctx.request,
|
|
877
|
-
ctx.prevUrl,
|
|
878
|
-
ctx.url,
|
|
879
|
-
ctx.actionContext,
|
|
880
|
-
ctx.interceptResult,
|
|
881
|
-
ctx.localRouteName,
|
|
882
|
-
ctx.pathname,
|
|
883
|
-
ctx.stale,
|
|
884
|
-
ctx.entries,
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
// The full chain keeps a bare cache() marker, which also sits in
|
|
888
|
-
// its layout's orphan list, out of the live pass: the record
|
|
889
|
-
// supplies it and its subtree (issue #918).
|
|
890
|
-
const segments = await resolveAllSegments(
|
|
891
|
-
liveEntries,
|
|
892
|
-
ctx.routeKey,
|
|
893
|
-
ctx.matched.params,
|
|
894
|
-
ctx.handlerContext,
|
|
895
|
-
ctx.loaderPromises,
|
|
896
|
-
{ chain: ctx.entries },
|
|
897
|
-
);
|
|
898
|
-
return { segments, matchedIds: segments.map((s) => s.id) };
|
|
899
|
-
});
|
|
900
|
-
// The record supplies every covered segment.
|
|
901
|
-
liveSegments = live.segments.filter(
|
|
902
|
-
(s) => !hitScope.covers(s.id, s.namespace),
|
|
903
|
-
);
|
|
904
|
-
liveMatchedIds = live.matchedIds.filter((id) => !hitScope.covers(id));
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
state.cacheHit = true;
|
|
908
|
-
state.cacheSource = "runtime";
|
|
909
|
-
state.shouldRevalidate = cacheResult.shouldRevalidate;
|
|
910
|
-
state.cachedSegments = cacheResult.segments;
|
|
911
|
-
state.cachedMatchedIds = [
|
|
912
|
-
...liveMatchedIds,
|
|
913
|
-
...cacheResult.segments.map((s) => s.id),
|
|
914
|
-
];
|
|
915
|
-
|
|
916
|
-
const canCheckSegmentRevalidation =
|
|
917
|
-
!ctx.isFullMatch &&
|
|
918
|
-
ctx.clientSegmentSet.size > 0 &&
|
|
919
|
-
!!buildEntryRevalidateMap;
|
|
920
|
-
const entryRevalidateMap = canCheckSegmentRevalidation
|
|
921
|
-
? buildEntryRevalidateMap(ctx.entries)
|
|
922
|
-
: undefined;
|
|
923
|
-
const searchChanged =
|
|
924
|
-
ctx.prevUrl.search !== ctx.url.search ||
|
|
925
|
-
pipelineReqCtx?._visitorMoved === true;
|
|
926
|
-
const routeParamsChanged = !paramsEqual(ctx.matched.params, ctx.prevParams);
|
|
927
|
-
|
|
928
|
-
yield* liveSegments;
|
|
929
|
-
|
|
930
|
-
for (const segment of cacheResult.segments) {
|
|
931
|
-
if (interceptFillSkips(ctx, segment)) continue;
|
|
932
|
-
if (!ctx.clientSegmentSet.has(segment.id)) {
|
|
933
|
-
if (isTraceActive()) {
|
|
934
|
-
pushRevalidationTraceEntry({
|
|
935
|
-
segmentId: segment.id,
|
|
936
|
-
segmentType: segment.type,
|
|
937
|
-
belongsToRoute: segment.belongsToRoute ?? false,
|
|
938
|
-
source: "cache-hit",
|
|
939
|
-
defaultShouldRevalidate: true,
|
|
940
|
-
finalShouldRevalidate: true,
|
|
941
|
-
reason: "new-segment",
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
yield segment;
|
|
945
|
-
continue;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// A fill keeps a held segment outright (fillSends), an intercept's
|
|
949
|
-
// included.
|
|
950
|
-
if (
|
|
951
|
-
fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) ===
|
|
952
|
-
false
|
|
953
|
-
) {
|
|
954
|
-
keepClientSegment(segment);
|
|
955
|
-
yield segment;
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
if (isInterceptSegment(segment)) {
|
|
959
|
-
yield segment;
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
const entryInfo = entryRevalidateMap?.get(segment.id);
|
|
964
|
-
const shouldDefaultRevalidate =
|
|
965
|
-
(searchChanged || routeParamsChanged) &&
|
|
966
|
-
(segment.type === "route" ||
|
|
967
|
-
(segment.belongsToRoute &&
|
|
968
|
-
(segment.type === "layout" || segment.type === "parallel")));
|
|
969
|
-
|
|
970
|
-
if (!entryInfo || entryInfo.revalidate.length === 0) {
|
|
971
|
-
if (shouldDefaultRevalidate) {
|
|
972
|
-
if (isTraceActive()) {
|
|
973
|
-
pushRevalidationTraceEntry({
|
|
974
|
-
segmentId: segment.id,
|
|
975
|
-
segmentType: segment.type,
|
|
976
|
-
belongsToRoute: segment.belongsToRoute ?? false,
|
|
977
|
-
source: "cache-hit",
|
|
978
|
-
defaultShouldRevalidate: true,
|
|
979
|
-
finalShouldRevalidate: true,
|
|
980
|
-
reason: routeParamsChanged
|
|
981
|
-
? "cached-params-changed"
|
|
982
|
-
: "cached-search-changed",
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
yield segment;
|
|
986
|
-
continue;
|
|
987
|
-
}
|
|
988
|
-
if (isTraceActive()) {
|
|
989
|
-
pushRevalidationTraceEntry({
|
|
990
|
-
segmentId: segment.id,
|
|
991
|
-
segmentType: segment.type,
|
|
992
|
-
belongsToRoute: segment.belongsToRoute ?? false,
|
|
993
|
-
source: "cache-hit",
|
|
994
|
-
defaultShouldRevalidate: false,
|
|
995
|
-
finalShouldRevalidate: false,
|
|
996
|
-
reason: "cached-no-rules",
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
keepClientSegment(segment);
|
|
1000
|
-
yield segment;
|
|
1001
|
-
continue;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
const shouldRevalidate = await evaluateRevalidation({
|
|
1005
|
-
segment,
|
|
1006
|
-
prevParams: ctx.prevParams,
|
|
1007
|
-
getPrevSegment: null,
|
|
1008
|
-
request: ctx.request,
|
|
1009
|
-
prevUrl: ctx.prevUrl,
|
|
1010
|
-
nextUrl: ctx.url,
|
|
1011
|
-
revalidations: entryInfo.revalidate.map((fn, i) => ({
|
|
1012
|
-
name: `revalidate${i}`,
|
|
1013
|
-
fn,
|
|
1014
|
-
})),
|
|
1015
|
-
routeKey: ctx.routeKey,
|
|
1016
|
-
context: ctx.handlerContext,
|
|
1017
|
-
actionContext: ctx.actionContext,
|
|
1018
|
-
cacheStale: cacheResult.shouldRevalidate,
|
|
1019
|
-
traceSource: "cache-hit",
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
observeEvent({
|
|
1023
|
-
type: "revalidation.decision",
|
|
1024
|
-
timestamp: performance.now(),
|
|
1025
|
-
segmentId: segment.id,
|
|
1026
|
-
pathname: ctx.pathname,
|
|
1027
|
-
routeKey: ctx.routeKey,
|
|
1028
|
-
shouldRevalidate,
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
if (!shouldRevalidate) keepClientSegment(segment);
|
|
1032
|
-
|
|
1033
|
-
yield segment;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
const barrierReqCtx = pipelineReqCtx;
|
|
1037
|
-
if (barrierReqCtx) {
|
|
1038
|
-
if (barrierReqCtx._treeHasStreaming === undefined) {
|
|
1039
|
-
barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
|
|
1040
|
-
}
|
|
1041
|
-
barrierReqCtx._resolveRenderBarrier(
|
|
1042
|
-
liveSegments.length > 0
|
|
1043
|
-
? [...liveSegments, ...cacheResult.segments]
|
|
1044
|
-
: cacheResult.segments,
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
// Resolve loaders fresh (loaders are never cached). Shared with the
|
|
1049
|
-
// prerender-store path via resolveFreshLoadersAndYield. The live pass
|
|
1050
|
-
// already resolved the loaders of the entries above the boundary.
|
|
1051
|
-
yield* resolveFreshLoadersAndYield(
|
|
1052
|
-
ctx,
|
|
1053
|
-
ctx.entries.slice(boundaryIndex),
|
|
1054
|
-
state,
|
|
1055
|
-
pipelineStart,
|
|
1056
|
-
ms,
|
|
1057
|
-
resolveLoadersOnly,
|
|
1058
|
-
resolveLoadersOnlyWithRevalidation,
|
|
1059
|
-
);
|
|
1060
|
-
};
|
|
1011
|
+
return (upstream) => serveStoredSegments(ctx, state, upstream);
|
|
1061
1012
|
}
|