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