@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,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The segment walker.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Resolves a matched chain's segments (loaders, layouts, routes, parallels,
|
|
5
|
+
* orphan layouts, error boundaries) in handler-first order. A partial request
|
|
6
|
+
* re-resolves only what the client lacks and what a held segment's
|
|
7
|
+
* revalidate() rules ask for. A full match (a document, the background
|
|
8
|
+
* re-render of a route record, a build-time prerender) walks the same code
|
|
9
|
+
* with `pass.client === FRESH` (resolution-pass.ts): every segment is new,
|
|
10
|
+
* and the branches marked FRESH keep what the full match has always done
|
|
11
|
+
* differently.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import type { ReactNode } from "react";
|
|
@@ -18,6 +23,7 @@ import {
|
|
|
18
23
|
getParallelSlotEntries,
|
|
19
24
|
type EntryData,
|
|
20
25
|
type LoaderEntry,
|
|
26
|
+
type ParallelEntries,
|
|
21
27
|
type ParallelEntryData,
|
|
22
28
|
} from "../../server/context";
|
|
23
29
|
import type {
|
|
@@ -26,18 +32,20 @@ import type {
|
|
|
26
32
|
ResolvedSegment,
|
|
27
33
|
ShouldRevalidateFn,
|
|
28
34
|
} from "../../types";
|
|
29
|
-
import type {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
ActionContext,
|
|
33
|
-
} from "../types.js";
|
|
35
|
+
import type { SegmentRevalidationResult } from "../types.js";
|
|
36
|
+
import type { InterceptResult } from "../router-context.js";
|
|
37
|
+
import { FRESH, type ResolutionPass } from "./resolution-pass.js";
|
|
34
38
|
import {
|
|
35
39
|
debugLog,
|
|
36
40
|
pushRevalidationTraceEntry,
|
|
37
41
|
isTraceActive,
|
|
38
42
|
} from "../logging.js";
|
|
39
43
|
import { resolveLoaderData } from "./loader-cache.js";
|
|
40
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
entryLoadingMasksLoaders,
|
|
46
|
+
isShellCaptureActive,
|
|
47
|
+
} from "./loader-mask.js";
|
|
48
|
+
import { _getRequestContext } from "../../server/request-context.js";
|
|
41
49
|
import {
|
|
42
50
|
defersLoader,
|
|
43
51
|
deferUnit,
|
|
@@ -60,7 +68,6 @@ import { isLayoutLoaderCopy } from "../../client-urls/layout-loader-copies.js";
|
|
|
60
68
|
import { getRouterContext } from "../router-context.js";
|
|
61
69
|
import { observeEvent, observeSegmentHandler } from "../instrument.js";
|
|
62
70
|
import { withReverseScope } from "../handler-context.js";
|
|
63
|
-
import { runInSegmentTagScope } from "../../cache/cache-tag.js";
|
|
64
71
|
import { trackStreamedHandler } from "./streamed-handler-boundary.js";
|
|
65
72
|
import {
|
|
66
73
|
track,
|
|
@@ -116,24 +123,60 @@ function emitRevalidationDecision(
|
|
|
116
123
|
});
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
/** What evaluateRevalidation reads from the pass, the same for every segment. */
|
|
127
|
+
function revalidationInputOf<TEnv>(
|
|
128
|
+
pass: ResolutionPass<TEnv>,
|
|
129
|
+
): Pick<
|
|
130
|
+
EvaluateRevalidationOptions<TEnv>,
|
|
131
|
+
| "prevParams"
|
|
132
|
+
| "getPrevSegment"
|
|
133
|
+
| "request"
|
|
134
|
+
| "prevUrl"
|
|
135
|
+
| "nextUrl"
|
|
136
|
+
| "routeKey"
|
|
137
|
+
| "context"
|
|
138
|
+
| "actionContext"
|
|
139
|
+
| "stale"
|
|
140
|
+
> {
|
|
141
|
+
return {
|
|
142
|
+
prevParams: pass.prevParams,
|
|
143
|
+
getPrevSegment: null,
|
|
144
|
+
// Only a build-time FRESH pass lacks a request, and it decides nothing.
|
|
145
|
+
request: pass.request!,
|
|
146
|
+
prevUrl: pass.prevUrl,
|
|
147
|
+
nextUrl: pass.nextUrl,
|
|
148
|
+
routeKey: pass.routeKey,
|
|
149
|
+
context: pass.context,
|
|
150
|
+
actionContext: pass.actionContext,
|
|
151
|
+
stale: pass.stale,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
119
155
|
/**
|
|
120
|
-
* Whether a segment the client holds (`
|
|
121
|
-
* renders again. A fill keeps it (prefetch-deferral.ts fillSends): no
|
|
156
|
+
* Whether a segment the client holds (`pass.client`, the set resolution works
|
|
157
|
+
* on) renders again. A fill keeps it (prefetch-deferral.ts fillSends): no
|
|
122
158
|
* revalidate() predicate runs and nothing is traced or observed. Any other
|
|
123
159
|
* request asks the segment's revalidate() chain, and `onEvaluated` sees that
|
|
124
160
|
* answer.
|
|
125
161
|
*/
|
|
126
|
-
async function revalidateHeld<TEnv>(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
162
|
+
async function revalidateHeld<TEnv>(
|
|
163
|
+
pass: ResolutionPass<TEnv>,
|
|
164
|
+
{
|
|
165
|
+
onEvaluated,
|
|
166
|
+
...held
|
|
167
|
+
}: Pick<
|
|
168
|
+
EvaluateRevalidationOptions<TEnv>,
|
|
169
|
+
"segment" | "revalidations" | "traceSource"
|
|
170
|
+
> & {
|
|
171
|
+
onEvaluated?: (shouldRevalidate: boolean) => void;
|
|
172
|
+
},
|
|
173
|
+
): Promise<boolean> {
|
|
174
|
+
const fill = fillSends(pass.context, held.segment.id, pass.client);
|
|
135
175
|
if (fill !== undefined) return fill;
|
|
136
|
-
const shouldRevalidate = await evaluateRevalidation(
|
|
176
|
+
const shouldRevalidate = await evaluateRevalidation({
|
|
177
|
+
...revalidationInputOf(pass),
|
|
178
|
+
...held,
|
|
179
|
+
});
|
|
137
180
|
onEvaluated?.(shouldRevalidate);
|
|
138
181
|
return shouldRevalidate;
|
|
139
182
|
}
|
|
@@ -166,7 +209,7 @@ export function deliverStartedLoaders<TEnv>(
|
|
|
166
209
|
if (plan?.mode !== "prefetch") return;
|
|
167
210
|
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
168
211
|
const started = (loaderId: string): boolean =>
|
|
169
|
-
|
|
212
|
+
internal._deferredLoaders?.get(loaderId)?.started === true ||
|
|
170
213
|
internal._loaderStarted?.(loaderId) === true;
|
|
171
214
|
for (const segment of segments) {
|
|
172
215
|
if (!segment.deferred || segment.type !== "loader") continue;
|
|
@@ -178,7 +221,7 @@ export function deliverStartedLoaders<TEnv>(
|
|
|
178
221
|
}
|
|
179
222
|
|
|
180
223
|
// ---------------------------------------------------------------------------
|
|
181
|
-
//
|
|
224
|
+
// Loaders
|
|
182
225
|
// ---------------------------------------------------------------------------
|
|
183
226
|
|
|
184
227
|
/**
|
|
@@ -199,6 +242,11 @@ export interface LoaderOwner {
|
|
|
199
242
|
* False for an intercept: never a document.
|
|
200
243
|
*/
|
|
201
244
|
readonly bakeLane: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* The owner declares loading(false). A FRESH pass then settles every loader
|
|
247
|
+
* before the handler runs (resolveOwnerLoaders). Unset for an intercept.
|
|
248
|
+
*/
|
|
249
|
+
readonly loadingDisabled?: boolean;
|
|
202
250
|
readonly traceSource: "loader" | "intercept-loader";
|
|
203
251
|
}
|
|
204
252
|
|
|
@@ -213,137 +261,185 @@ export function ownerOf(
|
|
|
213
261
|
segmentPrefix: shortCodeOverride ?? entry.shortCode,
|
|
214
262
|
boundary: entry,
|
|
215
263
|
bakeLane: !entryLoadingMasksLoaders(entry.loading),
|
|
264
|
+
loadingDisabled: entry.loading === false,
|
|
216
265
|
traceSource: "loader",
|
|
217
266
|
};
|
|
218
267
|
}
|
|
219
268
|
|
|
220
269
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
270
|
+
* resolveOwnerLoaders has something to resolve for `entry`'s loaders. The
|
|
271
|
+
* walker checks before the call: most entries own none, and the async call
|
|
272
|
+
* would cost a promise and microtask ticks for an empty result.
|
|
273
|
+
*/
|
|
274
|
+
function ownsLoaders(entry: EntryData): boolean {
|
|
275
|
+
return (entry.loader?.length ?? 0) > 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** getParallelSlotEntries(parallels) lists a slot. */
|
|
279
|
+
function declaresSlot(parallels: ParallelEntries): boolean {
|
|
280
|
+
for (const slot in parallels) {
|
|
281
|
+
if (parallels[slot as `@${string}`]) return true;
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Register flagged loader $$ids on the request context and return their
|
|
288
|
+
* indices. Must run BEFORE kickoff: rendered() checks the set to fail
|
|
289
|
+
* fast on the barrier cycle (resolution awaits the loader, the barrier
|
|
290
|
+
* awaits resolution).
|
|
291
|
+
*/
|
|
292
|
+
function registerAwaitBeforeFlushIds(
|
|
293
|
+
loaderEntries: readonly LoaderEntry[],
|
|
294
|
+
): number[] {
|
|
295
|
+
const awaitedIndices: number[] = [];
|
|
296
|
+
for (let i = 0; i < loaderEntries.length; i++) {
|
|
297
|
+
if (loaderEntries[i]!.awaitBeforeFlush) awaitedIndices.push(i);
|
|
298
|
+
}
|
|
299
|
+
if (awaitedIndices.length > 0) {
|
|
300
|
+
const reqCtx = _getRequestContext();
|
|
301
|
+
if (reqCtx) {
|
|
302
|
+
reqCtx._awaitBeforeFlushLoaderIds ??= new Set();
|
|
303
|
+
for (const i of awaitedIndices) {
|
|
304
|
+
reqCtx._awaitBeforeFlushLoaderIds.add(loaderEntries[i]!.loader.$$id);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return awaitedIndices;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const AWAIT_BEFORE_FLUSH = { awaitBeforeFlush: true } as const;
|
|
312
|
+
|
|
313
|
+
/** Spread into a FRESH loader segment: the flag, or nothing. */
|
|
314
|
+
function stampAwaitBeforeFlush(
|
|
315
|
+
loaderEntry: LoaderEntry,
|
|
316
|
+
): typeof AWAIT_BEFORE_FLUSH | undefined {
|
|
317
|
+
return loaderEntry.awaitBeforeFlush === true ? AWAIT_BEFORE_FLUSH : undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The loaders of a partial pass that run, as indices into `owner.loaders`:
|
|
322
|
+
* each one the client lacks, and each held one whose revalidate() rules ask
|
|
323
|
+
* for it (kept outright in a fill, fillSends). Every decision is traced and
|
|
324
|
+
* observed. `segmentIds` are the loaders' segment ids, by index.
|
|
325
|
+
*/
|
|
326
|
+
async function loadersToRerun<TEnv>(
|
|
327
|
+
pass: ResolutionPass<TEnv>,
|
|
328
|
+
owner: LoaderOwner,
|
|
329
|
+
belongsToRoute: boolean,
|
|
330
|
+
segmentIds: readonly string[],
|
|
331
|
+
): Promise<number[]> {
|
|
332
|
+
const ctx = pass.context;
|
|
333
|
+
const checks = await Promise.all(
|
|
334
|
+
owner.loaders.map(async (loaderEntry, index) => {
|
|
335
|
+
const segmentId = segmentIds[index]!;
|
|
336
|
+
const shouldRun = await revalidate(
|
|
337
|
+
async () => {
|
|
338
|
+
if (!pass.client.has(segmentId)) {
|
|
339
|
+
if (isTraceActive()) {
|
|
340
|
+
pushRevalidationTraceEntry({
|
|
341
|
+
segmentId,
|
|
342
|
+
segmentType: "loader",
|
|
343
|
+
belongsToRoute,
|
|
344
|
+
source: owner.traceSource,
|
|
345
|
+
defaultShouldRevalidate: true,
|
|
346
|
+
finalShouldRevalidate: true,
|
|
347
|
+
reason: "new-segment",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const dummySegment: ResolvedSegment = {
|
|
354
|
+
id: segmentId,
|
|
355
|
+
namespace: owner.namespace,
|
|
356
|
+
type: "loader",
|
|
357
|
+
index,
|
|
358
|
+
component: null,
|
|
359
|
+
params: ctx.params,
|
|
360
|
+
loaderId: loaderEntry.loader.$$id,
|
|
361
|
+
// A clientUrls() layout() loader's copy is projected onto the
|
|
362
|
+
// route but owned by a layout: it takes a layout's default.
|
|
363
|
+
belongsToRoute:
|
|
364
|
+
belongsToRoute && !isLayoutLoaderCopy(loaderEntry.loader),
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return await revalidateHeld(pass, {
|
|
368
|
+
segment: dummySegment,
|
|
369
|
+
revalidations: loaderEntry.revalidate.map((fn, j) => ({
|
|
370
|
+
name: `${owner.traceSource}-revalidate${j}`,
|
|
371
|
+
fn,
|
|
372
|
+
})),
|
|
373
|
+
traceSource: owner.traceSource,
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
async () => true,
|
|
377
|
+
() => false,
|
|
378
|
+
);
|
|
379
|
+
emitRevalidationDecision(
|
|
380
|
+
segmentId,
|
|
381
|
+
ctx.pathname,
|
|
382
|
+
pass.routeKey,
|
|
383
|
+
shouldRun,
|
|
384
|
+
);
|
|
385
|
+
return shouldRun;
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
const indices: number[] = [];
|
|
389
|
+
for (let i = 0; i < checks.length; i++) if (checks[i]) indices.push(i);
|
|
390
|
+
return indices;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The walker's one loader funnel: each loader is a segment, run when the
|
|
395
|
+
* client lacks it, asked its revalidate() when held, kept outright in a fill
|
|
396
|
+
* (fillSends), deferred in a prefetch that defers it (defersLoader).
|
|
397
|
+
*
|
|
398
|
+
* Loaders run lazily via ctx.use() and are memoized, so they run in
|
|
399
|
+
* parallel. A FRESH pass kicks every loader off without an await, in the
|
|
400
|
+
* tick the call starts: a reader's ctx.use(Loader) finds the binding only if
|
|
401
|
+
* it registered first (docs/design/handle-push-ownership.md).
|
|
224
402
|
*/
|
|
225
403
|
export async function resolveOwnerLoaders<TEnv>(
|
|
404
|
+
pass: ResolutionPass<TEnv>,
|
|
226
405
|
owner: LoaderOwner,
|
|
227
|
-
ctx: HandlerContext<any, TEnv>,
|
|
228
406
|
belongsToRoute: boolean,
|
|
229
|
-
clientSegmentIds: ReadonlySet<string>,
|
|
230
|
-
prevParams: Record<string, string>,
|
|
231
|
-
request: Request,
|
|
232
|
-
prevUrl: URL,
|
|
233
|
-
nextUrl: URL,
|
|
234
|
-
routeKey: string,
|
|
235
|
-
deps: SegmentResolutionDeps<TEnv>,
|
|
236
|
-
actionContext?: ActionContext,
|
|
237
|
-
stale?: boolean,
|
|
238
407
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
239
408
|
const loaderEntries = owner.loaders;
|
|
240
409
|
if (loaderEntries.length === 0) return { segments: [], matchedIds: [] };
|
|
241
410
|
|
|
411
|
+
const ctx = pass.context;
|
|
242
412
|
const shortCode = owner.segmentPrefix;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
413
|
+
const fresh = pass.client === FRESH;
|
|
414
|
+
|
|
415
|
+
// Pin `_currentSegmentId` to the OWNING entry BEFORE the loader kickoffs:
|
|
416
|
+
// createLoaderExecutor captures it synchronously at kickoff for
|
|
417
|
+
// ctx.use(Handle) push attribution (loader writes land in the same bucket
|
|
418
|
+
// as the entry's handler pushes — shortCode is in matched/segmentOrder, so
|
|
419
|
+
// collectHandleData keeps them; an id outside the order is silently
|
|
420
|
+
// dropped). Loaders resolve BEFORE the handler-resolution sites assign
|
|
421
|
+
// this (handler-first ordering), so without the pin the captured value is
|
|
422
|
+
// a stale sibling's id (document lane) or undefined (navigation lane —
|
|
423
|
+
// pushes silently vanished).
|
|
250
424
|
(ctx as InternalHandlerContext<any, TEnv>)._currentSegmentId = shortCode;
|
|
251
425
|
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
segmentId: `${shortCode}D${i}.${loaderEntry.loader.$$id}`,
|
|
257
|
-
index: i,
|
|
258
|
-
}));
|
|
259
|
-
|
|
260
|
-
const matchedIds = loaderMeta.map((m) => m.segmentId);
|
|
261
|
-
|
|
262
|
-
const revalidationChecks = await Promise.all(
|
|
263
|
-
loaderMeta.map(
|
|
264
|
-
async ({
|
|
265
|
-
loaderEntry,
|
|
266
|
-
loader,
|
|
267
|
-
loaderRevalidateFns,
|
|
268
|
-
segmentId,
|
|
269
|
-
index,
|
|
270
|
-
}) => {
|
|
271
|
-
const shouldRun = await revalidate(
|
|
272
|
-
async () => {
|
|
273
|
-
if (!clientSegmentIds.has(segmentId)) {
|
|
274
|
-
if (isTraceActive()) {
|
|
275
|
-
pushRevalidationTraceEntry({
|
|
276
|
-
segmentId,
|
|
277
|
-
segmentType: "loader",
|
|
278
|
-
belongsToRoute,
|
|
279
|
-
source: owner.traceSource,
|
|
280
|
-
defaultShouldRevalidate: true,
|
|
281
|
-
finalShouldRevalidate: true,
|
|
282
|
-
reason: "new-segment",
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const dummySegment: ResolvedSegment = {
|
|
289
|
-
id: segmentId,
|
|
290
|
-
namespace: owner.namespace,
|
|
291
|
-
type: "loader",
|
|
292
|
-
index,
|
|
293
|
-
component: null,
|
|
294
|
-
params: ctx.params,
|
|
295
|
-
loaderId: loader.$$id,
|
|
296
|
-
// A clientUrls() layout() loader's copy is projected onto the
|
|
297
|
-
// route but owned by a layout: it takes a layout's default.
|
|
298
|
-
belongsToRoute: belongsToRoute && !isLayoutLoaderCopy(loader),
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
return await revalidateHeld({
|
|
302
|
-
segment: dummySegment,
|
|
303
|
-
prevParams,
|
|
304
|
-
getPrevSegment: null,
|
|
305
|
-
request,
|
|
306
|
-
prevUrl,
|
|
307
|
-
nextUrl,
|
|
308
|
-
revalidations: loaderRevalidateFns.map((fn, j) => ({
|
|
309
|
-
name: `${owner.traceSource}-revalidate${j}`,
|
|
310
|
-
fn,
|
|
311
|
-
})),
|
|
312
|
-
routeKey,
|
|
313
|
-
context: ctx,
|
|
314
|
-
actionContext,
|
|
315
|
-
stale,
|
|
316
|
-
traceSource: owner.traceSource,
|
|
317
|
-
held: clientSegmentIds,
|
|
318
|
-
});
|
|
319
|
-
},
|
|
320
|
-
async () => true,
|
|
321
|
-
() => false,
|
|
322
|
-
);
|
|
323
|
-
emitRevalidationDecision(segmentId, ctx.pathname, routeKey, shouldRun);
|
|
324
|
-
return { shouldRun, loaderEntry, loader, segmentId, index };
|
|
325
|
-
},
|
|
326
|
-
),
|
|
327
|
-
);
|
|
426
|
+
const matchedIds: string[] = [];
|
|
427
|
+
for (let i = 0; i < loaderEntries.length; i++) {
|
|
428
|
+
matchedIds.push(`${shortCode}D${i}.${loaderEntries[i]!.loader.$$id}`);
|
|
429
|
+
}
|
|
328
430
|
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
// whole-container-mask every loader on this path (a never-settling promise
|
|
337
|
-
// for a loader that should have executed).
|
|
338
|
-
// Outside capture the key only activates the _shellLoaderSeed overlay,
|
|
339
|
-
// which serveShellHit seeds for a document HIT and the PPR partial replay
|
|
340
|
-
// (matchPartialWithPprReplay) seeds once its doc record hits.
|
|
431
|
+
// PPR bake/seed key (lane rule: see resolveLoaderData, loader-cache.ts).
|
|
432
|
+
// The key must thread through EVERY resolveLoaderData funnel — omitting it
|
|
433
|
+
// made a capture-active context whole-container-mask every loader on the
|
|
434
|
+
// partial path (a never-settling promise for a loader that should have
|
|
435
|
+
// executed). Outside capture the key only activates the _shellLoaderSeed
|
|
436
|
+
// overlay, which serveShellHit seeds for a document HIT and the PPR
|
|
437
|
+
// partial replay (matchPartialWithPprReplay) seeds once its doc record hits.
|
|
341
438
|
const bakeLane = owner.bakeLane;
|
|
342
439
|
|
|
343
440
|
const kickoff = (
|
|
344
441
|
loaderEntry: LoaderEntry,
|
|
345
442
|
segmentId: string,
|
|
346
|
-
loaderId: string,
|
|
347
443
|
): Promise<unknown> =>
|
|
348
444
|
runInsideLoaderScope(
|
|
349
445
|
() =>
|
|
@@ -351,89 +447,164 @@ export async function resolveOwnerLoaders<TEnv>(
|
|
|
351
447
|
loaderEntry,
|
|
352
448
|
ctx,
|
|
353
449
|
ctx.pathname,
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
450
|
+
// The bake key rides for flagged loaders too (`bake`): ssr: false
|
|
451
|
+
// bakes at capture regardless of the entry's loading() lane
|
|
452
|
+
// (loader-cache.ts capture branch), its HIT-tail seed overlay needs
|
|
453
|
+
// the same key, and on a navigation it pins exactly as on the
|
|
454
|
+
// document HIT.
|
|
357
455
|
bakeLane || loaderEntry.bake === true ? segmentId : null,
|
|
358
456
|
),
|
|
359
|
-
|
|
457
|
+
loaderEntry.loader.$$id,
|
|
360
458
|
loaderEntry.reverseScope,
|
|
361
459
|
);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
460
|
+
|
|
461
|
+
// skipLoaders: no loader runs and none is emitted. Each is registered
|
|
462
|
+
// instead, so a reader starts it as a MISS starts it, in its DSL scope
|
|
463
|
+
// under this owner: a route record leaves its pushes out (captureHandles)
|
|
464
|
+
// and a HIT's live run shows them once. The pin above holds as on a MISS:
|
|
465
|
+
// an orphan layout's handler pushes under the orphan.
|
|
466
|
+
if (pass.skipLoaders) {
|
|
467
|
+
for (let i = 0; i < loaderEntries.length; i++) {
|
|
468
|
+
const loaderEntry = loaderEntries[i]!;
|
|
469
|
+
registerDeferredLoader(ctx, loaderEntry.loader.$$id, shortCode, () =>
|
|
470
|
+
kickoff(loaderEntry, matchedIds[i]!),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return { segments: [], matchedIds: [] };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// FRESH: every loader is new; no decision is asked, traced or observed.
|
|
477
|
+
const toRun = fresh
|
|
478
|
+
? undefined
|
|
479
|
+
: await loadersToRerun(pass, owner, belongsToRoute, matchedIds);
|
|
480
|
+
|
|
481
|
+
// Error context for wrapLoaderPromise: without it, a throwing DSL loader never
|
|
482
|
+
// fires createRouter({ onError }) (phase "loader") nor emits the loader.error
|
|
483
|
+
// telemetry event. A partial pass reports with isPartial.
|
|
484
|
+
const errorContext = fresh
|
|
485
|
+
? buildLoaderErrorContext(ctx)
|
|
486
|
+
: { ...buildLoaderErrorContext(ctx), isPartial: true };
|
|
487
|
+
|
|
488
|
+
// FRESH: awaitBeforeFlush (loader(Def, { ssr: false })). A document render
|
|
489
|
+
// awaits these loaders before returning, so their data is settled, their
|
|
490
|
+
// handle pushes beat the barrier snapshot, and a thrown notFound()'s status
|
|
491
|
+
// write deterministically precedes Response construction. The ids must
|
|
492
|
+
// register on the request context BEFORE kickoff — rendered() checks the
|
|
493
|
+
// set to fail fast on the barrier cycle (segment resolution awaits the
|
|
494
|
+
// loader, the barrier awaits segment resolution, rendered() awaits the
|
|
495
|
+
// barrier), and the loader body can call rendered() before the await below
|
|
496
|
+
// is reached. Shell-capture renders await too: flagged loaders execute at
|
|
497
|
+
// capture (lane rule: see resolveLoaderData, loader-cache.ts), so their
|
|
498
|
+
// handle pushes must land in the prelude. A navigation evaluation never
|
|
499
|
+
// carries awaitBeforeFlush.
|
|
500
|
+
const awaitedIndices = fresh
|
|
501
|
+
? registerAwaitBeforeFlushIds(loaderEntries)
|
|
502
|
+
: [];
|
|
503
|
+
|
|
504
|
+
// The wrapped promise is contracted to never reject (wrapLoaderPromise): a
|
|
505
|
+
// failed loader resolves with its error envelope and renders its own
|
|
506
|
+
// per-loader boundary.
|
|
507
|
+
const start = (loaderEntry: LoaderEntry, segmentId: string) =>
|
|
508
|
+
pass.deps.wrapLoaderPromise(
|
|
509
|
+
kickoff(loaderEntry, segmentId),
|
|
369
510
|
owner.boundary,
|
|
370
511
|
segmentId,
|
|
371
512
|
ctx.pathname,
|
|
372
513
|
errorContext,
|
|
514
|
+
// FRESH: a flagged loader's failure falls back to the default boundary.
|
|
515
|
+
fresh ? loaderEntry.awaitBeforeFlush : undefined,
|
|
373
516
|
);
|
|
374
517
|
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
518
|
+
const segments: ResolvedSegment[] = [];
|
|
519
|
+
const count = toRun === undefined ? loaderEntries.length : toRun.length;
|
|
520
|
+
for (let k = 0; k < count; k++) {
|
|
521
|
+
const index = toRun === undefined ? k : toRun[k]!;
|
|
522
|
+
const loaderEntry = loaderEntries[index]!;
|
|
523
|
+
const segmentId = matchedIds[index]!;
|
|
524
|
+
const { loader } = loaderEntry;
|
|
525
|
+
// Only a loader on a segment the client lacks (defersLoader). It
|
|
526
|
+
// would have run. Registered, not started: it runs, and reads its
|
|
527
|
+
// cache(), only if a reader starts it (startDeferredLoader).
|
|
528
|
+
if (defersLoader(ctx, loaderEntry, segmentId)) {
|
|
529
|
+
registerDeferredLoader(ctx, loader.$$id, shortCode, () =>
|
|
530
|
+
kickoff(loaderEntry, segmentId),
|
|
531
|
+
);
|
|
532
|
+
const deferred: ResolvedSegment = {
|
|
379
533
|
id: segmentId,
|
|
380
534
|
namespace: owner.namespace,
|
|
381
|
-
type: "loader"
|
|
535
|
+
type: "loader",
|
|
382
536
|
index,
|
|
383
537
|
component: null,
|
|
384
538
|
params: ctx.params,
|
|
385
539
|
loaderId: loader.$$id,
|
|
386
|
-
};
|
|
387
|
-
// Only a loader on a segment the client lacks (defersLoader). It
|
|
388
|
-
// would have run. Registered, not started: it runs, and reads its
|
|
389
|
-
// cache(), only if a reader starts it (startDeferredLoader).
|
|
390
|
-
if (defersLoader(ctx, loaderEntry, segmentId)) {
|
|
391
|
-
registerDeferredLoader(ctx, loader.$$id, shortCode, () =>
|
|
392
|
-
kickoff(loaderEntry, segmentId, loader.$$id),
|
|
393
|
-
);
|
|
394
|
-
const deferred: ResolvedSegment = {
|
|
395
|
-
...base,
|
|
396
|
-
belongsToRoute,
|
|
397
|
-
deferred: true,
|
|
398
|
-
};
|
|
399
|
-
deferredLoaderRuns.set(deferred, () =>
|
|
400
|
-
start(loaderEntry, segmentId, loader.$$id),
|
|
401
|
-
);
|
|
402
|
-
return deferred;
|
|
403
|
-
}
|
|
404
|
-
return {
|
|
405
|
-
...base,
|
|
406
|
-
loaderData: start(loaderEntry, segmentId, loader.$$id),
|
|
407
540
|
belongsToRoute,
|
|
541
|
+
deferred: true,
|
|
408
542
|
};
|
|
409
|
-
|
|
410
|
-
|
|
543
|
+
deferredLoaderRuns.set(deferred, () => start(loaderEntry, segmentId));
|
|
544
|
+
segments.push(deferred);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
segments.push({
|
|
548
|
+
id: segmentId,
|
|
549
|
+
namespace: owner.namespace,
|
|
550
|
+
type: "loader",
|
|
551
|
+
index,
|
|
552
|
+
component: null,
|
|
553
|
+
params: ctx.params,
|
|
554
|
+
loaderId: loader.$$id,
|
|
555
|
+
// FRESH, document AND capture renders: segment-system's value-delivery
|
|
556
|
+
// await and the dev SSR-suspension diagnostic both key off it.
|
|
557
|
+
...(fresh ? stampAwaitBeforeFlush(loaderEntry) : undefined),
|
|
558
|
+
loaderData: start(loaderEntry, segmentId),
|
|
559
|
+
belongsToRoute,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (fresh) {
|
|
564
|
+
// loading(false): every loader settles before the handler runs, so the
|
|
565
|
+
// handler streams no loading placeholder. A PPR shell capture keeps the
|
|
566
|
+
// streaming shape: it masks LIVE-lane loaders with never-resolving
|
|
567
|
+
// promises (loader-mask.ts), and awaiting them would hang the capture
|
|
568
|
+
// render's match() forever. The `loadingDisabled` short-circuit keeps the
|
|
569
|
+
// ALS check off the hot path. Otherwise only the flagged loaders are
|
|
570
|
+
// awaited; unflagged siblings keep streaming.
|
|
571
|
+
const settled =
|
|
572
|
+
owner.loadingDisabled && !isShellCaptureActive()
|
|
573
|
+
? segments
|
|
574
|
+
: awaitedIndices.length > 0
|
|
575
|
+
? awaitedIndices.map((i) => segments[i]!)
|
|
576
|
+
: undefined;
|
|
577
|
+
if (settled !== undefined && settled.length > 0) {
|
|
578
|
+
await Promise.all(settled.map((s) => s.loaderData));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
411
581
|
|
|
412
582
|
return { segments, matchedIds };
|
|
413
583
|
}
|
|
414
584
|
|
|
415
585
|
/**
|
|
416
|
-
* Resolve only loader segments for all entries
|
|
586
|
+
* Resolve only loader segments for all entries: a cache() or prerender HIT
|
|
587
|
+
* serves the cached non-loader segments, and loaders are never cached. Under
|
|
588
|
+
* an intercept the route's entry, and on an intercept's fill an entry the
|
|
589
|
+
* client lacks, are skipped as resolveAllSegments skips them, after the ppr
|
|
590
|
+
* header latch below has read the chain's leaf.
|
|
417
591
|
*/
|
|
418
|
-
export async function
|
|
592
|
+
export async function resolveLoadersOnly<TEnv>(
|
|
593
|
+
pass: ResolutionPass<TEnv>,
|
|
419
594
|
entries: EntryData[],
|
|
420
|
-
|
|
421
|
-
clientSegmentIds: Set<string>,
|
|
422
|
-
prevParams: Record<string, string>,
|
|
423
|
-
request: Request,
|
|
424
|
-
prevUrl: URL,
|
|
425
|
-
nextUrl: URL,
|
|
426
|
-
routeKey: string,
|
|
427
|
-
deps: SegmentResolutionDeps<TEnv>,
|
|
428
|
-
actionContext?: ActionContext,
|
|
429
|
-
stale?: boolean,
|
|
595
|
+
walk?: Pick<RevalidatingWalkOptions, "interceptResult">,
|
|
430
596
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
431
597
|
const allLoaderSegments: ResolvedSegment[] = [];
|
|
432
598
|
const allMatchedIds: string[] = [];
|
|
433
599
|
const seenIds = new Set<string>();
|
|
434
600
|
|
|
435
|
-
//
|
|
436
|
-
|
|
601
|
+
// Loader-only serves still run loaders live — on a ppr route their header
|
|
602
|
+
// writes are dead letters (headers flushed with the shell), so the guard
|
|
603
|
+
// latches here too (see resolveAllSegments). cache() needs
|
|
604
|
+
// no latch: loader writes are exempt under cache()
|
|
605
|
+
// (assertCachedHeaderWriteAllowed).
|
|
606
|
+
latchPprHeaderScopeForEntries(entries, pass.routeKey);
|
|
607
|
+
const handleStore = _getRequestContext()?._handleStore;
|
|
437
608
|
|
|
438
609
|
async function collectEntryLoaders(
|
|
439
610
|
entry: EntryData,
|
|
@@ -449,20 +620,11 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
449
620
|
loaderEntries.every((le, i) =>
|
|
450
621
|
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
451
622
|
);
|
|
452
|
-
if (!allAlreadySeen) {
|
|
623
|
+
if (!allAlreadySeen && ownsLoaders(entry)) {
|
|
453
624
|
const { segments, matchedIds } = await resolveOwnerLoaders(
|
|
625
|
+
pass,
|
|
454
626
|
ownerOf(entry, shortCodeOverride),
|
|
455
|
-
context,
|
|
456
627
|
belongsToRoute,
|
|
457
|
-
clientSegmentIds,
|
|
458
|
-
prevParams,
|
|
459
|
-
request,
|
|
460
|
-
prevUrl,
|
|
461
|
-
nextUrl,
|
|
462
|
-
routeKey,
|
|
463
|
-
deps,
|
|
464
|
-
actionContext,
|
|
465
|
-
stale,
|
|
466
628
|
);
|
|
467
629
|
for (const seg of segments) {
|
|
468
630
|
if (!seenIds.has(seg.id)) {
|
|
@@ -472,6 +634,9 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
472
634
|
}
|
|
473
635
|
allMatchedIds.push(...matchedIds);
|
|
474
636
|
}
|
|
637
|
+
// Where the MISS runs this entry's handler: before its slots' loaders,
|
|
638
|
+
// which push into its segment too (a slot's own call releases nothing).
|
|
639
|
+
handleStore?.releaseRecordedValues(sc);
|
|
475
640
|
|
|
476
641
|
const seenParallelEntryIds = new Set<string>();
|
|
477
642
|
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
@@ -493,18 +658,9 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
493
658
|
Object.keys(layoutEntry.parallel).length > 0
|
|
494
659
|
) {
|
|
495
660
|
const inherited = await resolveOwnerLoaders(
|
|
661
|
+
pass,
|
|
496
662
|
ownerOf(entry, layoutEntry.shortCode),
|
|
497
|
-
context,
|
|
498
663
|
childBelongsToRoute,
|
|
499
|
-
clientSegmentIds,
|
|
500
|
-
prevParams,
|
|
501
|
-
request,
|
|
502
|
-
prevUrl,
|
|
503
|
-
nextUrl,
|
|
504
|
-
routeKey,
|
|
505
|
-
deps,
|
|
506
|
-
actionContext,
|
|
507
|
-
stale,
|
|
508
664
|
);
|
|
509
665
|
for (const seg of inherited.segments) {
|
|
510
666
|
if (!seenIds.has(seg.id)) {
|
|
@@ -518,11 +674,18 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
518
674
|
}
|
|
519
675
|
}
|
|
520
676
|
|
|
677
|
+
const fillMatch = {
|
|
678
|
+
handlerContext: pass.context,
|
|
679
|
+
isIntercept: !!walk?.interceptResult,
|
|
680
|
+
clientSegmentSet: pass.client,
|
|
681
|
+
};
|
|
521
682
|
for (const entry of entries) {
|
|
683
|
+
if (entry.type === "route" && walk?.interceptResult) continue;
|
|
684
|
+
if (interceptFillSkips(fillMatch, entry.shortCode)) continue;
|
|
522
685
|
await collectEntryLoaders(entry, entry.type === "route");
|
|
523
686
|
}
|
|
524
687
|
|
|
525
|
-
deliverStartedLoaders(context, allLoaderSegments);
|
|
688
|
+
deliverStartedLoaders(pass.context, allLoaderSegments);
|
|
526
689
|
return { segments: allLoaderSegments, matchedIds: allMatchedIds };
|
|
527
690
|
}
|
|
528
691
|
|
|
@@ -540,16 +703,15 @@ export function buildEntryRevalidateMap(
|
|
|
540
703
|
{ entry: EntryData; revalidate: ShouldRevalidateFn<any, any>[] }
|
|
541
704
|
>();
|
|
542
705
|
|
|
543
|
-
function processEntry(entry: EntryData
|
|
706
|
+
function processEntry(entry: EntryData) {
|
|
544
707
|
map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
|
|
545
708
|
|
|
546
709
|
if (entry.type !== "parallel") {
|
|
710
|
+
// The walker's slot id (resolveParallelSegments), an orphan's included.
|
|
547
711
|
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
548
712
|
entry.parallel,
|
|
549
713
|
)) {
|
|
550
|
-
|
|
551
|
-
const parallelId = `${parallelParentShortCode}.${slot}`;
|
|
552
|
-
map.set(parallelId, {
|
|
714
|
+
map.set(`${entry.shortCode}.${slot}`, {
|
|
553
715
|
entry: parallelEntry,
|
|
554
716
|
revalidate: parallelEntry.revalidate,
|
|
555
717
|
});
|
|
@@ -557,7 +719,7 @@ export function buildEntryRevalidateMap(
|
|
|
557
719
|
}
|
|
558
720
|
|
|
559
721
|
for (const layoutEntry of entry.layout) {
|
|
560
|
-
processEntry(layoutEntry
|
|
722
|
+
processEntry(layoutEntry);
|
|
561
723
|
}
|
|
562
724
|
}
|
|
563
725
|
|
|
@@ -569,44 +731,43 @@ export function buildEntryRevalidateMap(
|
|
|
569
731
|
}
|
|
570
732
|
|
|
571
733
|
/**
|
|
572
|
-
* Resolve the component for a single parallel slot
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
* orphan-vs-main
|
|
734
|
+
* Resolve the component for a single parallel slot. Pure component resolution
|
|
735
|
+
* shared by the main path and the orphan path of
|
|
736
|
+
* resolveParallelSegments: try the static slot cache, else run
|
|
737
|
+
* the slot handler (pinning _currentSegmentId to the slot id so handle pushes
|
|
738
|
+
* land in the slot's own bucket, and wrapping a streamed handler). Returns the
|
|
739
|
+
* resolved component and whether the handler actually ran, or undefined when
|
|
740
|
+
* there is nothing to render: the handler was evicted (production static
|
|
741
|
+
* slot) and the static lookup missed. Does NOT touch the revalidate-default
|
|
742
|
+
* policy (the caller decides shouldResolve, including the orphan-vs-main
|
|
743
|
+
* defaultOverride divergence) or loader-resolution ordering.
|
|
581
744
|
*/
|
|
582
|
-
async function resolveParallelSlotComponent<TEnv>(
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
745
|
+
async function resolveParallelSlotComponent<TEnv>(
|
|
746
|
+
pass: ResolutionPass<TEnv>,
|
|
747
|
+
args: {
|
|
748
|
+
shouldResolve: boolean;
|
|
749
|
+
parallelEntry: ParallelEntryData;
|
|
750
|
+
slot: string;
|
|
751
|
+
parallelId: string;
|
|
752
|
+
handler:
|
|
753
|
+
| ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
|
|
754
|
+
| ReactNode
|
|
755
|
+
| undefined;
|
|
756
|
+
/** Matched chain entry (where the sync path catches), else the slot's owner. */
|
|
757
|
+
boundaryEntry: EntryData;
|
|
758
|
+
},
|
|
759
|
+
): Promise<
|
|
760
|
+
{ component: ReactNode | undefined; handlerRan: boolean } | undefined
|
|
761
|
+
> {
|
|
598
762
|
const {
|
|
599
763
|
shouldResolve,
|
|
600
764
|
parallelEntry,
|
|
601
765
|
slot,
|
|
602
766
|
parallelId,
|
|
603
767
|
handler,
|
|
604
|
-
context,
|
|
605
|
-
deps,
|
|
606
|
-
routeKey,
|
|
607
|
-
params,
|
|
608
768
|
boundaryEntry,
|
|
609
769
|
} = args;
|
|
770
|
+
const { context, deps, routeKey, params } = pass;
|
|
610
771
|
|
|
611
772
|
let component: ReactNode | undefined;
|
|
612
773
|
let handlerRan = false;
|
|
@@ -621,9 +782,7 @@ async function resolveParallelSlotComponent<TEnv>(args: {
|
|
|
621
782
|
if (!shouldResolve) {
|
|
622
783
|
component = null;
|
|
623
784
|
} else if (handler === undefined) {
|
|
624
|
-
|
|
625
|
-
// Nothing to render — use null so the client keeps its cached version.
|
|
626
|
-
component = null;
|
|
785
|
+
return undefined;
|
|
627
786
|
} else {
|
|
628
787
|
// Slot-keyed pushes — slot owns its own bucket, parent layout owns its
|
|
629
788
|
// own. On slot-only revalidations the partial merge updates only the
|
|
@@ -632,13 +791,21 @@ async function resolveParallelSlotComponent<TEnv>(args: {
|
|
|
632
791
|
parallelId;
|
|
633
792
|
handlerRan = true;
|
|
634
793
|
const slotContext = withReverseScope(context, parallelEntry.reverseScope);
|
|
794
|
+
const doneHandler = track(`handler:${parallelEntry.id}.${slot}`, 2);
|
|
795
|
+
const run = () =>
|
|
796
|
+
typeof handler !== "function"
|
|
797
|
+
? handler
|
|
798
|
+
: observeSegmentHandler(
|
|
799
|
+
parallelId,
|
|
800
|
+
`${parallelEntry.id}.${slot}`,
|
|
801
|
+
handler,
|
|
802
|
+
slotContext,
|
|
803
|
+
);
|
|
635
804
|
if (hasLoadingFallback) {
|
|
636
|
-
const result =
|
|
637
|
-
typeof handler === "function"
|
|
638
|
-
? runInSegmentTagScope(parallelId, handler, slotContext)
|
|
639
|
-
: handler;
|
|
805
|
+
const result = run();
|
|
640
806
|
if (result instanceof Promise) {
|
|
641
807
|
warnOnStreamedResponse(result, parallelId);
|
|
808
|
+
result.finally(doneHandler).catch(() => {});
|
|
642
809
|
component = trackStreamedHandler(deps, result, {
|
|
643
810
|
boundaryEntry,
|
|
644
811
|
segmentId: parallelId,
|
|
@@ -646,16 +813,16 @@ async function resolveParallelSlotComponent<TEnv>(args: {
|
|
|
646
813
|
context,
|
|
647
814
|
routeKey,
|
|
648
815
|
params,
|
|
649
|
-
recover:
|
|
816
|
+
recover: !pass.throwOnError,
|
|
650
817
|
});
|
|
651
818
|
} else {
|
|
819
|
+
doneHandler();
|
|
652
820
|
component = result as ReactNode;
|
|
653
821
|
}
|
|
654
822
|
} else {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
: handler;
|
|
823
|
+
// A static value (a Promise included) goes to render as is.
|
|
824
|
+
component = typeof handler === "function" ? await run() : handler;
|
|
825
|
+
doneHandler();
|
|
659
826
|
}
|
|
660
827
|
}
|
|
661
828
|
}
|
|
@@ -664,22 +831,12 @@ async function resolveParallelSlotComponent<TEnv>(args: {
|
|
|
664
831
|
}
|
|
665
832
|
|
|
666
833
|
/**
|
|
667
|
-
* Resolve parallel
|
|
834
|
+
* Resolve an entry's parallel slots: each slot's segment and its loaders.
|
|
668
835
|
*/
|
|
669
|
-
export async function
|
|
836
|
+
export async function resolveParallelSegments<TEnv>(
|
|
837
|
+
pass: ResolutionPass<TEnv>,
|
|
670
838
|
entry: EntryData,
|
|
671
|
-
params: Record<string, string>,
|
|
672
|
-
context: HandlerContext<any, TEnv>,
|
|
673
839
|
belongsToRoute: boolean,
|
|
674
|
-
clientSegmentIds: Set<string>,
|
|
675
|
-
prevParams: Record<string, string>,
|
|
676
|
-
request: Request,
|
|
677
|
-
prevUrl: URL,
|
|
678
|
-
nextUrl: URL,
|
|
679
|
-
routeKey: string,
|
|
680
|
-
deps: SegmentResolutionDeps<TEnv>,
|
|
681
|
-
actionContext?: ActionContext,
|
|
682
|
-
stale?: boolean,
|
|
683
840
|
options?: {
|
|
684
841
|
/**
|
|
685
842
|
* Seed for an unknown parent-chain slot (slot not in clientSegmentIds) when
|
|
@@ -698,10 +855,16 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
698
855
|
* to it: loader sub-ids are filtered out and slots are re-grouped by parent).
|
|
699
856
|
*/
|
|
700
857
|
loaderOrder?: "after" | "before";
|
|
701
|
-
/**
|
|
858
|
+
/**
|
|
859
|
+
* The matched chain entry being resolved: where the sync path catches a
|
|
860
|
+
* handler error, so a streamed slot handler (or a slot owned by a
|
|
861
|
+
* routeless layout) recovers into the same boundary. Defaults to `entry`.
|
|
862
|
+
*/
|
|
702
863
|
boundaryEntry?: EntryData;
|
|
703
864
|
},
|
|
704
865
|
): Promise<SegmentRevalidationResult> {
|
|
866
|
+
const { context, client: clientSegmentIds, params, routeKey } = pass;
|
|
867
|
+
const fresh = clientSegmentIds === FRESH;
|
|
705
868
|
const segments: ResolvedSegment[] = [];
|
|
706
869
|
const matchedIds: string[] = [];
|
|
707
870
|
const parentChainDefault = options?.parentChainDefault ?? "type-derived";
|
|
@@ -733,28 +896,31 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
733
896
|
|
|
734
897
|
// A slot's loaders (never cached) are deduped per parallel entry and
|
|
735
898
|
// emitted either before or after the slot segment per loaderOrder.
|
|
736
|
-
const resolveSlotLoaders =
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
899
|
+
const resolveSlotLoaders =
|
|
900
|
+
resolvedParallelEntries.has(parallelEntry.id) ||
|
|
901
|
+
!ownsLoaders(parallelEntry)
|
|
902
|
+
? undefined
|
|
903
|
+
: async () => {
|
|
904
|
+
const loaderResult = await resolveOwnerLoaders(
|
|
905
|
+
pass,
|
|
906
|
+
ownerOf(parallelEntry, entry.shortCode),
|
|
907
|
+
belongsToRoute,
|
|
908
|
+
);
|
|
909
|
+
// FRESH tags a slot's loaders with the slot's loading(), so
|
|
910
|
+
// renderSegments streams them under it instead of awaiting them
|
|
911
|
+
// on the layout. A partial pass's are tagged on render
|
|
912
|
+
// (restoreParallelLoaderMarkers, segment-system.tsx).
|
|
913
|
+
if (fresh && parallelEntry.loading) {
|
|
914
|
+
for (const seg of loaderResult.segments) {
|
|
915
|
+
seg.parallelLoading = parallelEntry.loading;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
segments.push(...loaderResult.segments);
|
|
919
|
+
matchedIds.push(...loaderResult.matchedIds);
|
|
920
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
921
|
+
};
|
|
756
922
|
|
|
757
|
-
if (loaderOrder === "before") {
|
|
923
|
+
if (resolveSlotLoaders && loaderOrder === "before") {
|
|
758
924
|
await resolveSlotLoaders();
|
|
759
925
|
}
|
|
760
926
|
// Always announce the slot in matchedIds — it's unconditionally appended
|
|
@@ -771,7 +937,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
771
937
|
// below (fillSends).
|
|
772
938
|
const fill = fillSends(context, parallelId, clientSegmentIds);
|
|
773
939
|
let shouldResolve: boolean;
|
|
774
|
-
if (
|
|
940
|
+
if (fresh) {
|
|
941
|
+
// FRESH: the slot is new; no trace entry, no decision event.
|
|
942
|
+
shouldResolve = true;
|
|
943
|
+
} else if (fill !== undefined) {
|
|
775
944
|
shouldResolve = fill;
|
|
776
945
|
} else if (deferred) {
|
|
777
946
|
shouldResolve = true;
|
|
@@ -824,47 +993,51 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
824
993
|
};
|
|
825
994
|
|
|
826
995
|
shouldResolve = await evaluateRevalidation({
|
|
996
|
+
...revalidationInputOf(pass),
|
|
827
997
|
segment: dummySegment,
|
|
828
|
-
prevParams,
|
|
829
|
-
getPrevSegment: null,
|
|
830
|
-
request,
|
|
831
|
-
prevUrl,
|
|
832
|
-
nextUrl,
|
|
833
998
|
revalidations: parallelEntry.revalidate.map((fn, i) => ({
|
|
834
999
|
name: `revalidate${i}`,
|
|
835
1000
|
fn,
|
|
836
1001
|
})),
|
|
837
|
-
routeKey,
|
|
838
|
-
context,
|
|
839
|
-
actionContext,
|
|
840
|
-
stale,
|
|
841
1002
|
traceSource: "parallel",
|
|
842
1003
|
defaultOverride,
|
|
843
1004
|
});
|
|
844
1005
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1006
|
+
if (!fresh) {
|
|
1007
|
+
emitRevalidationDecision(
|
|
1008
|
+
parallelId,
|
|
1009
|
+
context.pathname,
|
|
1010
|
+
routeKey,
|
|
1011
|
+
shouldResolve,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
851
1014
|
|
|
852
1015
|
// A slot with its own flagged loading() is its own unit.
|
|
853
1016
|
if (shouldResolve) deferred ??= deferUnit(context, parallelId, "slot");
|
|
854
|
-
const
|
|
1017
|
+
const resolved = deferred
|
|
855
1018
|
? { component: null, handlerRan: false }
|
|
856
|
-
: await resolveParallelSlotComponent({
|
|
1019
|
+
: await resolveParallelSlotComponent(pass, {
|
|
857
1020
|
shouldResolve,
|
|
858
1021
|
parallelEntry,
|
|
859
1022
|
slot,
|
|
860
1023
|
parallelId,
|
|
861
1024
|
handler,
|
|
862
|
-
context,
|
|
863
|
-
deps,
|
|
864
|
-
routeKey,
|
|
865
|
-
params,
|
|
866
1025
|
boundaryEntry: options?.boundaryEntry ?? entry,
|
|
867
1026
|
});
|
|
1027
|
+
if (resolved === undefined && fresh) {
|
|
1028
|
+
// FRESH: an evicted slot handler whose static lookup missed renders no
|
|
1029
|
+
// slot segment; its loaders still resolve.
|
|
1030
|
+
if (resolveSlotLoaders && loaderOrder === "after") {
|
|
1031
|
+
await resolveSlotLoaders();
|
|
1032
|
+
}
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
// Nothing to render on a partial pass: null, so the client keeps its
|
|
1036
|
+
// cached version.
|
|
1037
|
+
const { component, handlerRan } = resolved ?? {
|
|
1038
|
+
component: null,
|
|
1039
|
+
handlerRan: false,
|
|
1040
|
+
};
|
|
868
1041
|
|
|
869
1042
|
segments.push({
|
|
870
1043
|
id: parallelId,
|
|
@@ -875,7 +1048,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
875
1048
|
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
876
1049
|
transition: applyViewTransitionDefault(
|
|
877
1050
|
parallelEntry.transition,
|
|
878
|
-
deps.viewTransitionDefault,
|
|
1051
|
+
pass.deps.viewTransitionDefault,
|
|
879
1052
|
),
|
|
880
1053
|
params,
|
|
881
1054
|
slot,
|
|
@@ -888,7 +1061,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
888
1061
|
...deferred,
|
|
889
1062
|
});
|
|
890
1063
|
|
|
891
|
-
if (loaderOrder === "after") {
|
|
1064
|
+
if (resolveSlotLoaders && loaderOrder === "after") {
|
|
892
1065
|
await resolveSlotLoaders();
|
|
893
1066
|
}
|
|
894
1067
|
}
|
|
@@ -897,190 +1070,179 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
897
1070
|
}
|
|
898
1071
|
|
|
899
1072
|
/**
|
|
900
|
-
*
|
|
1073
|
+
* Whether a partial pass re-renders a chain entry: one the client lacks, or a
|
|
1074
|
+
* held one whose revalidate() rules ask for it (revalidateHeld).
|
|
901
1075
|
*/
|
|
902
|
-
|
|
1076
|
+
async function entryRevalidates<TEnv>(
|
|
1077
|
+
pass: ResolutionPass<TEnv>,
|
|
903
1078
|
entry: Exclude<EntryData, { type: "parallel" }>,
|
|
904
|
-
params: Record<string, string>,
|
|
905
|
-
context: HandlerContext<any, TEnv>,
|
|
906
1079
|
belongsToRoute: boolean,
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
)
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const component = await revalidate(
|
|
922
|
-
async () => {
|
|
923
|
-
const hasSegment = clientSegmentIds.has(entry.shortCode);
|
|
924
|
-
debugLog("segment.revalidate", "entry presence check", {
|
|
1080
|
+
): Promise<boolean> {
|
|
1081
|
+
const { context, params, routeKey } = pass;
|
|
1082
|
+
const hasSegment = pass.client.has(entry.shortCode);
|
|
1083
|
+
debugLog("segment.revalidate", "entry presence check", {
|
|
1084
|
+
segmentId: entry.shortCode,
|
|
1085
|
+
entryType: entry.type,
|
|
1086
|
+
clientHasSegment: hasSegment,
|
|
1087
|
+
belongsToRoute,
|
|
1088
|
+
});
|
|
1089
|
+
if (!hasSegment) {
|
|
1090
|
+
if (isTraceActive()) {
|
|
1091
|
+
const segType =
|
|
1092
|
+
entry.type === "cache" ? "layout" : (entry.type as "layout" | "route");
|
|
1093
|
+
pushRevalidationTraceEntry({
|
|
925
1094
|
segmentId: entry.shortCode,
|
|
926
|
-
|
|
927
|
-
clientHasSegment: hasSegment,
|
|
1095
|
+
segmentType: segType,
|
|
928
1096
|
belongsToRoute,
|
|
1097
|
+
source: "segment-resolution",
|
|
1098
|
+
defaultShouldRevalidate: true,
|
|
1099
|
+
finalShouldRevalidate: true,
|
|
1100
|
+
reason: "new-segment",
|
|
929
1101
|
});
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
entry.type === "cache"
|
|
934
|
-
? "layout"
|
|
935
|
-
: (entry.type as "layout" | "route");
|
|
936
|
-
pushRevalidationTraceEntry({
|
|
937
|
-
segmentId: entry.shortCode,
|
|
938
|
-
segmentType: segType,
|
|
939
|
-
belongsToRoute,
|
|
940
|
-
source: "segment-resolution",
|
|
941
|
-
defaultShouldRevalidate: true,
|
|
942
|
-
finalShouldRevalidate: true,
|
|
943
|
-
reason: "new-segment",
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
return true;
|
|
947
|
-
}
|
|
1102
|
+
}
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
948
1105
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
964
|
-
};
|
|
1106
|
+
const dummySegment: ResolvedSegment = {
|
|
1107
|
+
id: entry.shortCode,
|
|
1108
|
+
namespace: entry.id,
|
|
1109
|
+
type:
|
|
1110
|
+
entry.type === "cache" ? "layout" : (entry.type as "layout" | "route"),
|
|
1111
|
+
index: 0,
|
|
1112
|
+
component: null as any,
|
|
1113
|
+
params,
|
|
1114
|
+
belongsToRoute,
|
|
1115
|
+
...(entry.type === "layout" || entry.type === "cache"
|
|
1116
|
+
? { layoutName: entry.id }
|
|
1117
|
+
: {}),
|
|
1118
|
+
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
1119
|
+
};
|
|
965
1120
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1121
|
+
return revalidateHeld(pass, {
|
|
1122
|
+
segment: dummySegment,
|
|
1123
|
+
revalidations: entry.revalidate.map((fn, i) => ({
|
|
1124
|
+
name: `revalidate${i}`,
|
|
1125
|
+
fn,
|
|
1126
|
+
})),
|
|
1127
|
+
traceSource: entry.type === "route" ? "route-handler" : "layout-handler",
|
|
1128
|
+
onEvaluated: (shouldRevalidate) => {
|
|
1129
|
+
emitRevalidationDecision(
|
|
1130
|
+
entry.shortCode,
|
|
1131
|
+
context.pathname,
|
|
977
1132
|
routeKey,
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
held: clientSegmentIds,
|
|
984
|
-
onEvaluated: (shouldRevalidate) => {
|
|
985
|
-
emitRevalidationDecision(
|
|
986
|
-
entry.shortCode,
|
|
987
|
-
context.pathname,
|
|
988
|
-
routeKey,
|
|
989
|
-
shouldRevalidate,
|
|
990
|
-
);
|
|
991
|
-
debugLog("segment.revalidate", "entry revalidation decision", {
|
|
992
|
-
segmentId: entry.shortCode,
|
|
993
|
-
shouldRevalidate,
|
|
994
|
-
});
|
|
995
|
-
},
|
|
1133
|
+
shouldRevalidate,
|
|
1134
|
+
);
|
|
1135
|
+
debugLog("segment.revalidate", "entry revalidation decision", {
|
|
1136
|
+
segmentId: entry.shortCode,
|
|
1137
|
+
shouldRevalidate,
|
|
996
1138
|
});
|
|
997
1139
|
},
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Run a route entry's handler: a static store hit, else the handler, streamed
|
|
1145
|
+
* under the route's loading() unless an action awaits it.
|
|
1146
|
+
*/
|
|
1147
|
+
async function resolveRouteComponent<TEnv>(
|
|
1148
|
+
pass: ResolutionPass<TEnv>,
|
|
1149
|
+
entry: Extract<EntryData, { type: "route" }>,
|
|
1150
|
+
): Promise<ReactNode | { content: ReactNode }> {
|
|
1151
|
+
const { context, params, routeKey, actionContext } = pass;
|
|
1152
|
+
// A static hit runs no handler: no handler metric, no span.
|
|
1153
|
+
const staticComponent = await tryStaticHandler(entry, entry.shortCode);
|
|
1154
|
+
if (staticComponent !== undefined) return staticComponent;
|
|
1155
|
+
const doneHandler = track(`handler:${entry.id}`, 2);
|
|
1156
|
+
// For Passthrough routes at runtime, use the live handler instead of
|
|
1157
|
+
// the build handler. At build time (context.build === true), always
|
|
1158
|
+
// use the build handler from entry.handler.
|
|
1159
|
+
const handler =
|
|
1160
|
+
!context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
|
|
1161
|
+
if (!entry.loading) {
|
|
1162
|
+
const result = handleHandlerResult(
|
|
1163
|
+
await observeSegmentHandler(entry.shortCode, entry.id, handler, context),
|
|
1164
|
+
);
|
|
1165
|
+
doneHandler();
|
|
1166
|
+
return result;
|
|
1167
|
+
}
|
|
1168
|
+
if (!actionContext) {
|
|
1169
|
+
const result = handleHandlerResult(
|
|
1170
|
+
observeSegmentHandler(entry.shortCode, entry.id, handler, context),
|
|
1171
|
+
);
|
|
1172
|
+
if (result instanceof Promise) {
|
|
1173
|
+
warnOnStreamedResponse(result, entry.id);
|
|
1174
|
+
result.finally(doneHandler).catch(() => {});
|
|
1175
|
+
return {
|
|
1176
|
+
content: trackStreamedHandler(pass.deps, result, {
|
|
1177
|
+
boundaryEntry: entry,
|
|
1178
|
+
segmentId: entry.shortCode,
|
|
1179
|
+
segmentType: entry.type,
|
|
1180
|
+
context,
|
|
1181
|
+
routeKey,
|
|
1182
|
+
params,
|
|
1183
|
+
recover: !pass.throwOnError,
|
|
1184
|
+
}),
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
doneHandler();
|
|
1188
|
+
return { content: result };
|
|
1189
|
+
}
|
|
1190
|
+
debugLog("segment.action", "resolving action route with awaited value", {
|
|
1191
|
+
entryId: entry.id,
|
|
1192
|
+
});
|
|
1193
|
+
const actionResult = handleHandlerResult(
|
|
1194
|
+
await observeSegmentHandler(entry.shortCode, entry.id, handler, context),
|
|
1195
|
+
);
|
|
1196
|
+
doneHandler();
|
|
1197
|
+
return {
|
|
1198
|
+
content: Promise.resolve(actionResult),
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Resolve a chain entry's handler (layout, cache, or route) into its segment.
|
|
1204
|
+
*/
|
|
1205
|
+
export async function resolveEntryHandler<TEnv>(
|
|
1206
|
+
pass: ResolutionPass<TEnv>,
|
|
1207
|
+
entry: Exclude<EntryData, { type: "parallel" }>,
|
|
1208
|
+
belongsToRoute: boolean,
|
|
1209
|
+
): Promise<{ segment: ResolvedSegment; matchedId: string }> {
|
|
1210
|
+
const { context, params } = pass;
|
|
1211
|
+
const fresh = pass.client === FRESH;
|
|
1212
|
+
const matchedId = entry.shortCode;
|
|
1213
|
+
|
|
1214
|
+
let handlerRan = false;
|
|
1215
|
+
let deferred: { readonly deferred: true } | undefined;
|
|
1216
|
+
let component: ReactNode | { content: ReactNode } = null;
|
|
1217
|
+
// FRESH: the entry is new; no decision is asked, traced or observed.
|
|
1218
|
+
if (fresh || (await entryRevalidates(pass, entry, belongsToRoute))) {
|
|
1219
|
+
// Only an entry the client lacks (defersUnit). Its handler would
|
|
1220
|
+
// have run, and a prefetch sends the entry's fallback in its place.
|
|
1221
|
+
deferred = deferUnit(context, entry.shortCode, "chain");
|
|
1222
|
+
if (!deferred) {
|
|
1003
1223
|
handlerRan = true;
|
|
1004
|
-
const doneHandler = track(`handler:${entry.id}`, 2);
|
|
1005
1224
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
1006
1225
|
entry.shortCode;
|
|
1007
|
-
if (entry.type === "
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const staticComponent = await tryStaticHandler(entry, entry.shortCode);
|
|
1013
|
-
if (staticComponent !== undefined) {
|
|
1014
|
-
doneHandler();
|
|
1015
|
-
return staticComponent;
|
|
1016
|
-
}
|
|
1017
|
-
const routeEntry = entry as Extract<EntryData, { type: "route" }>;
|
|
1018
|
-
// For Passthrough routes at runtime, use the live handler instead of
|
|
1019
|
-
// the build handler. At build time (context.build === true), always
|
|
1020
|
-
// use the build handler from routeEntry.handler.
|
|
1021
|
-
const handler =
|
|
1022
|
-
!context.build && routeEntry.liveHandler
|
|
1023
|
-
? routeEntry.liveHandler
|
|
1024
|
-
: routeEntry.handler;
|
|
1025
|
-
if (!routeEntry.loading) {
|
|
1026
|
-
const result = handleHandlerResult(
|
|
1027
|
-
await observeSegmentHandler(
|
|
1028
|
-
entry.shortCode,
|
|
1029
|
-
entry.id,
|
|
1030
|
-
handler,
|
|
1031
|
-
context,
|
|
1032
|
-
),
|
|
1033
|
-
);
|
|
1034
|
-
doneHandler();
|
|
1035
|
-
return result;
|
|
1036
|
-
}
|
|
1037
|
-
if (!actionContext) {
|
|
1038
|
-
const result = handleHandlerResult(
|
|
1039
|
-
observeSegmentHandler(entry.shortCode, entry.id, handler, context),
|
|
1040
|
-
);
|
|
1041
|
-
if (result instanceof Promise) {
|
|
1042
|
-
warnOnStreamedResponse(result, routeEntry.id);
|
|
1043
|
-
result.finally(doneHandler).catch(() => {});
|
|
1044
|
-
return {
|
|
1045
|
-
content: trackStreamedHandler(deps, result, {
|
|
1046
|
-
boundaryEntry: entry,
|
|
1047
|
-
segmentId: entry.shortCode,
|
|
1048
|
-
segmentType: entry.type,
|
|
1049
|
-
context,
|
|
1050
|
-
routeKey,
|
|
1051
|
-
params,
|
|
1052
|
-
recover: true,
|
|
1053
|
-
}),
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1226
|
+
if (entry.type === "route") {
|
|
1227
|
+
component = await resolveRouteComponent(pass, entry);
|
|
1228
|
+
} else {
|
|
1229
|
+
const doneHandler = track(`handler:${entry.id}`, 2);
|
|
1230
|
+
component = await resolveLayoutComponent(entry, context);
|
|
1056
1231
|
doneHandler();
|
|
1057
|
-
return { content: result };
|
|
1058
1232
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
});
|
|
1062
|
-
const actionResult = handleHandlerResult(
|
|
1063
|
-
await observeSegmentHandler(
|
|
1064
|
-
entry.shortCode,
|
|
1065
|
-
entry.id,
|
|
1066
|
-
handler,
|
|
1067
|
-
context,
|
|
1068
|
-
),
|
|
1069
|
-
);
|
|
1070
|
-
doneHandler();
|
|
1071
|
-
return {
|
|
1072
|
-
content: Promise.resolve(actionResult),
|
|
1073
|
-
};
|
|
1074
|
-
},
|
|
1075
|
-
() => null,
|
|
1076
|
-
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1077
1235
|
|
|
1078
1236
|
// Normalize void handlers (undefined) to null so the reconciler's
|
|
1079
|
-
// component === null checks work consistently for both void and explicit
|
|
1237
|
+
// component === null checks work consistently for both void and explicit
|
|
1238
|
+
// null. FRESH keeps a void layout's undefined: cache-scope.ts refuses a
|
|
1239
|
+
// document record whose layout component is null.
|
|
1080
1240
|
const resolvedComponent =
|
|
1081
1241
|
component && typeof component === "object" && "content" in component
|
|
1082
1242
|
? ((component as { content: ReactNode }).content ?? null)
|
|
1083
|
-
:
|
|
1243
|
+
: fresh && entry.type !== "route"
|
|
1244
|
+
? (component as ReactNode)
|
|
1245
|
+
: (component ?? null);
|
|
1084
1246
|
|
|
1085
1247
|
const segment: ResolvedSegment = {
|
|
1086
1248
|
id: entry.shortCode,
|
|
@@ -1092,39 +1254,32 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
1092
1254
|
loading: entry.loading === false ? null : entry.loading,
|
|
1093
1255
|
transition: applyViewTransitionDefault(
|
|
1094
1256
|
entry.transition,
|
|
1095
|
-
deps.viewTransitionDefault,
|
|
1257
|
+
pass.deps.viewTransitionDefault,
|
|
1096
1258
|
),
|
|
1097
1259
|
params,
|
|
1098
1260
|
belongsToRoute,
|
|
1099
|
-
...(entry.clientGroup ? { clientGroup: entry.clientGroup } : {}),
|
|
1100
|
-
...(entry.type === "layout" || entry.type === "cache"
|
|
1101
|
-
? { layoutName: entry.id }
|
|
1102
|
-
: {}),
|
|
1103
|
-
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
1104
|
-
_handlerRan: handlerRan,
|
|
1105
|
-
...deferred,
|
|
1106
1261
|
};
|
|
1262
|
+
// Assigned, not spread: a conditional spread costs a temporary object and a
|
|
1263
|
+
// property copy on every chain entry. The keys keep this order.
|
|
1264
|
+
if (entry.clientGroup) segment.clientGroup = entry.clientGroup;
|
|
1265
|
+
if (entry.type === "layout" || entry.type === "cache") {
|
|
1266
|
+
segment.layoutName = entry.id;
|
|
1267
|
+
}
|
|
1268
|
+
if (entry.mountPath) segment.mountPath = entry.mountPath;
|
|
1269
|
+
segment._handlerRan = handlerRan;
|
|
1270
|
+
if (deferred) segment.deferred = deferred.deferred;
|
|
1107
1271
|
|
|
1108
1272
|
return { segment, matchedId };
|
|
1109
1273
|
}
|
|
1110
1274
|
|
|
1111
1275
|
/**
|
|
1112
|
-
* Resolve segments
|
|
1276
|
+
* Resolve one chain entry's segments: its loaders, its handler, its orphan
|
|
1277
|
+
* layouts and its parallels, handler first.
|
|
1113
1278
|
*/
|
|
1114
|
-
export async function
|
|
1279
|
+
export async function resolveSegment<TEnv>(
|
|
1280
|
+
pass: ResolutionPass<TEnv>,
|
|
1115
1281
|
entry: Exclude<EntryData, { type: "parallel" }>,
|
|
1116
|
-
|
|
1117
|
-
params: Record<string, string>,
|
|
1118
|
-
context: HandlerContext<any, TEnv>,
|
|
1119
|
-
clientSegmentIds: Set<string>,
|
|
1120
|
-
prevParams: Record<string, string>,
|
|
1121
|
-
request: Request,
|
|
1122
|
-
prevUrl: URL,
|
|
1123
|
-
nextUrl: URL,
|
|
1124
|
-
deps: SegmentResolutionDeps<TEnv>,
|
|
1125
|
-
actionContext?: ActionContext,
|
|
1126
|
-
stale?: boolean,
|
|
1127
|
-
/** The matched route's full chain; see ResolveSegmentOptions.chain (fresh.ts). */
|
|
1282
|
+
/** The matched route's full chain; see RevalidatingWalkOptions.chain. */
|
|
1128
1283
|
chain?: readonly EntryData[],
|
|
1129
1284
|
): Promise<SegmentRevalidationResult> {
|
|
1130
1285
|
const segments: ResolvedSegment[] = [];
|
|
@@ -1132,22 +1287,15 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
1132
1287
|
|
|
1133
1288
|
const belongsToRoute = entry.type === "route";
|
|
1134
1289
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
routeKey,
|
|
1145
|
-
deps,
|
|
1146
|
-
actionContext,
|
|
1147
|
-
stale,
|
|
1148
|
-
);
|
|
1149
|
-
segments.push(...loaderResult.segments);
|
|
1150
|
-
matchedIds.push(...loaderResult.matchedIds);
|
|
1290
|
+
if (ownsLoaders(entry)) {
|
|
1291
|
+
const loaderResult = await resolveOwnerLoaders(
|
|
1292
|
+
pass,
|
|
1293
|
+
ownerOf(entry),
|
|
1294
|
+
belongsToRoute,
|
|
1295
|
+
);
|
|
1296
|
+
segments.push(...loaderResult.segments);
|
|
1297
|
+
matchedIds.push(...loaderResult.matchedIds);
|
|
1298
|
+
}
|
|
1151
1299
|
|
|
1152
1300
|
// For route entries, execute the handler BEFORE orphan layouts and parallels
|
|
1153
1301
|
// so ctx.set() data is available to them via ctx.get(). The handler's
|
|
@@ -1156,111 +1304,59 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
1156
1304
|
| { segment: ResolvedSegment; matchedId: string }
|
|
1157
1305
|
| undefined;
|
|
1158
1306
|
if (entry.type === "route") {
|
|
1159
|
-
routeHandlerResult = await
|
|
1160
|
-
entry,
|
|
1161
|
-
params,
|
|
1162
|
-
context,
|
|
1163
|
-
belongsToRoute,
|
|
1164
|
-
clientSegmentIds,
|
|
1165
|
-
prevParams,
|
|
1166
|
-
request,
|
|
1167
|
-
prevUrl,
|
|
1168
|
-
nextUrl,
|
|
1169
|
-
routeKey,
|
|
1170
|
-
deps,
|
|
1171
|
-
actionContext,
|
|
1172
|
-
stale,
|
|
1173
|
-
);
|
|
1307
|
+
routeHandlerResult = await resolveEntryHandler(pass, entry, belongsToRoute);
|
|
1174
1308
|
|
|
1175
1309
|
for (const orphan of entry.layout) {
|
|
1176
|
-
const orphanResult = await
|
|
1177
|
-
orphan,
|
|
1178
|
-
params,
|
|
1179
|
-
context,
|
|
1180
|
-
clientSegmentIds,
|
|
1181
|
-
prevParams,
|
|
1182
|
-
request,
|
|
1183
|
-
prevUrl,
|
|
1184
|
-
nextUrl,
|
|
1185
|
-
routeKey,
|
|
1186
|
-
true,
|
|
1187
|
-
deps,
|
|
1188
|
-
actionContext,
|
|
1189
|
-
stale,
|
|
1190
|
-
entry,
|
|
1191
|
-
);
|
|
1310
|
+
const orphanResult = await resolveOrphanLayout(pass, orphan, true, entry);
|
|
1192
1311
|
segments.push(...orphanResult.segments);
|
|
1193
1312
|
matchedIds.push(...orphanResult.matchedIds);
|
|
1194
1313
|
}
|
|
1195
1314
|
}
|
|
1196
1315
|
|
|
1197
|
-
const
|
|
1198
|
-
resolveParallelSegmentsWithRevalidation(
|
|
1199
|
-
entry,
|
|
1200
|
-
params,
|
|
1201
|
-
context,
|
|
1202
|
-
belongsToRoute,
|
|
1203
|
-
clientSegmentIds,
|
|
1204
|
-
prevParams,
|
|
1205
|
-
request,
|
|
1206
|
-
prevUrl,
|
|
1207
|
-
nextUrl,
|
|
1208
|
-
routeKey,
|
|
1209
|
-
deps,
|
|
1210
|
-
actionContext,
|
|
1211
|
-
stale,
|
|
1212
|
-
);
|
|
1316
|
+
const slotted = declaresSlot(entry.parallel);
|
|
1213
1317
|
|
|
1214
1318
|
if (routeHandlerResult) {
|
|
1215
1319
|
// Route entry: handler already executed above; resolve parallels
|
|
1216
1320
|
// (handler data visible) then push handler segment last for tree order.
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1321
|
+
if (slotted) {
|
|
1322
|
+
const parallelResult = await resolveParallelSegments(
|
|
1323
|
+
pass,
|
|
1324
|
+
entry,
|
|
1325
|
+
belongsToRoute,
|
|
1326
|
+
);
|
|
1327
|
+
segments.push(...parallelResult.segments);
|
|
1328
|
+
matchedIds.push(...parallelResult.matchedIds);
|
|
1329
|
+
}
|
|
1220
1330
|
|
|
1221
1331
|
segments.push(routeHandlerResult.segment);
|
|
1222
1332
|
matchedIds.push(routeHandlerResult.matchedId);
|
|
1223
1333
|
} else {
|
|
1224
1334
|
// Layout/cache entry: handler-first — resolve handler before parallels
|
|
1225
1335
|
// so ctx.set() values are visible to parallel children.
|
|
1226
|
-
const handlerResult = await
|
|
1336
|
+
const handlerResult = await resolveEntryHandler(
|
|
1337
|
+
pass,
|
|
1227
1338
|
entry,
|
|
1228
|
-
params,
|
|
1229
|
-
context,
|
|
1230
1339
|
belongsToRoute,
|
|
1231
|
-
clientSegmentIds,
|
|
1232
|
-
prevParams,
|
|
1233
|
-
request,
|
|
1234
|
-
prevUrl,
|
|
1235
|
-
nextUrl,
|
|
1236
|
-
routeKey,
|
|
1237
|
-
deps,
|
|
1238
|
-
actionContext,
|
|
1239
|
-
stale,
|
|
1240
1340
|
);
|
|
1241
1341
|
segments.push(handlerResult.segment);
|
|
1242
1342
|
matchedIds.push(handlerResult.matchedId);
|
|
1243
1343
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1344
|
+
if (slotted) {
|
|
1345
|
+
const parallelResult = await resolveParallelSegments(
|
|
1346
|
+
pass,
|
|
1347
|
+
entry,
|
|
1348
|
+
belongsToRoute,
|
|
1349
|
+
);
|
|
1350
|
+
segments.push(...parallelResult.segments);
|
|
1351
|
+
matchedIds.push(...parallelResult.matchedIds);
|
|
1352
|
+
}
|
|
1247
1353
|
|
|
1248
1354
|
for (const orphan of entry.layout) {
|
|
1249
1355
|
if (chain?.includes(orphan)) continue;
|
|
1250
|
-
const orphanResult = await
|
|
1356
|
+
const orphanResult = await resolveOrphanLayout(
|
|
1357
|
+
pass,
|
|
1251
1358
|
orphan,
|
|
1252
|
-
params,
|
|
1253
|
-
context,
|
|
1254
|
-
clientSegmentIds,
|
|
1255
|
-
prevParams,
|
|
1256
|
-
request,
|
|
1257
|
-
prevUrl,
|
|
1258
|
-
nextUrl,
|
|
1259
|
-
routeKey,
|
|
1260
1359
|
false,
|
|
1261
|
-
deps,
|
|
1262
|
-
actionContext,
|
|
1263
|
-
stale,
|
|
1264
1360
|
undefined,
|
|
1265
1361
|
entry,
|
|
1266
1362
|
);
|
|
@@ -1273,25 +1369,74 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
1273
1369
|
}
|
|
1274
1370
|
|
|
1275
1371
|
/**
|
|
1276
|
-
*
|
|
1372
|
+
* Whether a partial pass re-renders an orphan layout: one the client lacks,
|
|
1373
|
+
* or a held one whose revalidate() rules ask for it (revalidateHeld).
|
|
1277
1374
|
*/
|
|
1278
|
-
|
|
1375
|
+
async function orphanRevalidates<TEnv>(
|
|
1376
|
+
pass: ResolutionPass<TEnv>,
|
|
1377
|
+
orphan: EntryData,
|
|
1378
|
+
belongsToRoute: boolean,
|
|
1379
|
+
): Promise<boolean> {
|
|
1380
|
+
const { context, params, routeKey } = pass;
|
|
1381
|
+
if (!pass.client.has(orphan.shortCode)) {
|
|
1382
|
+
if (isTraceActive()) {
|
|
1383
|
+
pushRevalidationTraceEntry({
|
|
1384
|
+
segmentId: orphan.shortCode,
|
|
1385
|
+
segmentType: "layout",
|
|
1386
|
+
belongsToRoute,
|
|
1387
|
+
source: "orphan-layout",
|
|
1388
|
+
defaultShouldRevalidate: true,
|
|
1389
|
+
finalShouldRevalidate: true,
|
|
1390
|
+
reason: "new-segment",
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const dummySegment: ResolvedSegment = {
|
|
1397
|
+
id: orphan.shortCode,
|
|
1398
|
+
namespace: orphan.id,
|
|
1399
|
+
type: "layout",
|
|
1400
|
+
index: 0,
|
|
1401
|
+
component: null as any,
|
|
1402
|
+
params,
|
|
1403
|
+
belongsToRoute,
|
|
1404
|
+
layoutName: orphan.id,
|
|
1405
|
+
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
return revalidateHeld(pass, {
|
|
1409
|
+
segment: dummySegment,
|
|
1410
|
+
revalidations: orphan.revalidate.map((fn, i) => ({
|
|
1411
|
+
name: `revalidate${i}`,
|
|
1412
|
+
fn,
|
|
1413
|
+
})),
|
|
1414
|
+
traceSource: "orphan-layout",
|
|
1415
|
+
onEvaluated: (shouldRevalidate) =>
|
|
1416
|
+
emitRevalidationDecision(
|
|
1417
|
+
orphan.shortCode,
|
|
1418
|
+
context.pathname,
|
|
1419
|
+
routeKey,
|
|
1420
|
+
shouldRevalidate,
|
|
1421
|
+
),
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Resolve an orphan layout: its loaders, its handler, its slots and the
|
|
1427
|
+
* routeless entries nested in it.
|
|
1428
|
+
*/
|
|
1429
|
+
export async function resolveOrphanLayout<TEnv>(
|
|
1430
|
+
pass: ResolutionPass<TEnv>,
|
|
1279
1431
|
orphan: EntryData,
|
|
1280
|
-
params: Record<string, string>,
|
|
1281
|
-
context: HandlerContext<any, TEnv>,
|
|
1282
|
-
clientSegmentIds: Set<string>,
|
|
1283
|
-
prevParams: Record<string, string>,
|
|
1284
|
-
request: Request,
|
|
1285
|
-
prevUrl: URL,
|
|
1286
|
-
nextUrl: URL,
|
|
1287
|
-
routeKey: string,
|
|
1288
1432
|
belongsToRoute: boolean,
|
|
1289
|
-
deps: SegmentResolutionDeps<TEnv>,
|
|
1290
|
-
actionContext?: ActionContext,
|
|
1291
|
-
stale?: boolean,
|
|
1292
1433
|
/** Parent route entry — its loaders are inherited so parallel slots can access them. */
|
|
1293
1434
|
parentRouteEntry?: EntryData,
|
|
1294
|
-
/**
|
|
1435
|
+
/**
|
|
1436
|
+
* Matched chain entry being resolved (see
|
|
1437
|
+
* resolveParallelSegments's boundaryEntry); defaults to
|
|
1438
|
+
* parentRouteEntry.
|
|
1439
|
+
*/
|
|
1295
1440
|
boundaryEntry: EntryData | undefined = parentRouteEntry,
|
|
1296
1441
|
): Promise<SegmentRevalidationResult> {
|
|
1297
1442
|
invariant(
|
|
@@ -1299,28 +1444,25 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1299
1444
|
`Expected orphan to be a layout or cache, got: ${orphan.type}`,
|
|
1300
1445
|
);
|
|
1301
1446
|
|
|
1447
|
+
const { context, params } = pass;
|
|
1448
|
+
const fresh = pass.client === FRESH;
|
|
1302
1449
|
const segments: ResolvedSegment[] = [];
|
|
1303
1450
|
const matchedIds: string[] = [];
|
|
1304
1451
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
routeKey,
|
|
1315
|
-
deps,
|
|
1316
|
-
actionContext,
|
|
1317
|
-
stale,
|
|
1318
|
-
);
|
|
1319
|
-
segments.push(...loaderResult.segments);
|
|
1320
|
-
matchedIds.push(...loaderResult.matchedIds);
|
|
1452
|
+
if (ownsLoaders(orphan)) {
|
|
1453
|
+
const loaderResult = await resolveOwnerLoaders(
|
|
1454
|
+
pass,
|
|
1455
|
+
ownerOf(orphan),
|
|
1456
|
+
belongsToRoute,
|
|
1457
|
+
);
|
|
1458
|
+
segments.push(...loaderResult.segments);
|
|
1459
|
+
matchedIds.push(...loaderResult.matchedIds);
|
|
1460
|
+
}
|
|
1321
1461
|
|
|
1322
1462
|
// Inherit parent route's loaders so parallel slots inside this layout
|
|
1323
|
-
// can access them via useLoader().
|
|
1463
|
+
// can access them via useLoader(). Without this, the route's loaders are
|
|
1464
|
+
// only in the route's OutletProvider (rendered as <Outlet /> content),
|
|
1465
|
+
// which is a child — not a parent — of the layout's context.
|
|
1324
1466
|
if (
|
|
1325
1467
|
parentRouteEntry &&
|
|
1326
1468
|
parentRouteEntry.loader &&
|
|
@@ -1328,18 +1470,9 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1328
1470
|
Object.keys(orphan.parallel).length > 0
|
|
1329
1471
|
) {
|
|
1330
1472
|
const inheritedResult = await resolveOwnerLoaders(
|
|
1473
|
+
pass,
|
|
1331
1474
|
ownerOf(parentRouteEntry, orphan.shortCode),
|
|
1332
|
-
context,
|
|
1333
1475
|
belongsToRoute,
|
|
1334
|
-
clientSegmentIds,
|
|
1335
|
-
prevParams,
|
|
1336
|
-
request,
|
|
1337
|
-
prevUrl,
|
|
1338
|
-
nextUrl,
|
|
1339
|
-
routeKey,
|
|
1340
|
-
deps,
|
|
1341
|
-
actionContext,
|
|
1342
|
-
stale,
|
|
1343
1476
|
);
|
|
1344
1477
|
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
1345
1478
|
for (const s of inheritedResult.segments) {
|
|
@@ -1353,70 +1486,30 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1353
1486
|
// so ctx.set() values are visible to parallel children.
|
|
1354
1487
|
matchedIds.push(orphan.shortCode);
|
|
1355
1488
|
|
|
1489
|
+
let handlerRan = false;
|
|
1356
1490
|
let deferred: { readonly deferred: true } | undefined;
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1491
|
+
let component: ReactNode = null;
|
|
1492
|
+
// FRESH: the orphan is new; no decision is asked, traced or observed.
|
|
1493
|
+
if (fresh || (await orphanRevalidates(pass, orphan, belongsToRoute))) {
|
|
1494
|
+
// Below a unit a prefetch skipped: the orphan goes with it.
|
|
1495
|
+
deferred = deferUnit(context, orphan.shortCode, "below");
|
|
1496
|
+
if (!deferred) {
|
|
1497
|
+
handlerRan = true;
|
|
1498
|
+
// Its handler's pushes are its own. Pinned only while it runs: a route
|
|
1499
|
+
// under loading() streams its handler, whose pushes after an await
|
|
1500
|
+
// read the pin after this returns.
|
|
1501
|
+
const internal = context as InternalHandlerContext<any, TEnv>;
|
|
1502
|
+
const pinned = internal._currentSegmentId;
|
|
1503
|
+
internal._currentSegmentId = orphan.shortCode;
|
|
1504
|
+
const doneHandler = track(`handler:${orphan.id}`, 2);
|
|
1505
|
+
try {
|
|
1506
|
+
component = await resolveLayoutComponent(orphan, context);
|
|
1507
|
+
} finally {
|
|
1508
|
+
internal._currentSegmentId = pinned;
|
|
1372
1509
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
namespace: orphan.id,
|
|
1377
|
-
type: "layout",
|
|
1378
|
-
index: 0,
|
|
1379
|
-
component: null as any,
|
|
1380
|
-
params,
|
|
1381
|
-
belongsToRoute,
|
|
1382
|
-
layoutName: orphan.id,
|
|
1383
|
-
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
1384
|
-
};
|
|
1385
|
-
|
|
1386
|
-
return revalidateHeld({
|
|
1387
|
-
segment: dummySegment,
|
|
1388
|
-
prevParams,
|
|
1389
|
-
getPrevSegment: null,
|
|
1390
|
-
request,
|
|
1391
|
-
prevUrl,
|
|
1392
|
-
nextUrl,
|
|
1393
|
-
revalidations: orphan.revalidate.map((fn, i) => ({
|
|
1394
|
-
name: `revalidate${i}`,
|
|
1395
|
-
fn,
|
|
1396
|
-
})),
|
|
1397
|
-
routeKey,
|
|
1398
|
-
context,
|
|
1399
|
-
actionContext,
|
|
1400
|
-
stale,
|
|
1401
|
-
traceSource: "orphan-layout",
|
|
1402
|
-
held: clientSegmentIds,
|
|
1403
|
-
onEvaluated: (shouldRevalidate) =>
|
|
1404
|
-
emitRevalidationDecision(
|
|
1405
|
-
orphan.shortCode,
|
|
1406
|
-
context.pathname,
|
|
1407
|
-
routeKey,
|
|
1408
|
-
shouldRevalidate,
|
|
1409
|
-
),
|
|
1410
|
-
});
|
|
1411
|
-
},
|
|
1412
|
-
async () => {
|
|
1413
|
-
// Below a unit a prefetch skipped: the orphan goes with it.
|
|
1414
|
-
deferred = deferUnit(context, orphan.shortCode, "below");
|
|
1415
|
-
if (deferred) return null;
|
|
1416
|
-
return resolveLayoutComponent(orphan, context);
|
|
1417
|
-
},
|
|
1418
|
-
() => null,
|
|
1419
|
-
);
|
|
1510
|
+
doneHandler();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1420
1513
|
|
|
1421
1514
|
segments.push({
|
|
1422
1515
|
id: orphan.shortCode,
|
|
@@ -1430,9 +1523,10 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1430
1523
|
loading: orphan.loading === false ? null : orphan.loading,
|
|
1431
1524
|
transition: applyViewTransitionDefault(
|
|
1432
1525
|
orphan.transition,
|
|
1433
|
-
deps.viewTransitionDefault,
|
|
1526
|
+
pass.deps.viewTransitionDefault,
|
|
1434
1527
|
),
|
|
1435
1528
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
1529
|
+
_handlerRan: handlerRan,
|
|
1436
1530
|
...deferred,
|
|
1437
1531
|
});
|
|
1438
1532
|
|
|
@@ -1442,48 +1536,35 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1442
1536
|
// `true` (orphan parallels always belong to the route — the #482 guard),
|
|
1443
1537
|
// where the main path would seed `belongsToRoute || isNewParent`.
|
|
1444
1538
|
// - loaderOrder "before": a slot's loaders are emitted before the slot
|
|
1445
|
-
// segment, matching the prior inlined order.
|
|
1539
|
+
// segment, matching the prior inlined order. FRESH emits them after it,
|
|
1540
|
+
// as on the main path.
|
|
1446
1541
|
// `entry.shortCode` inside the helper is `orphan.shortCode` (orphan is passed
|
|
1447
1542
|
// as `entry`), so the parallel ids + loader shortCodeOverride are unchanged.
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
{
|
|
1463
|
-
parentChainDefault: "force-render",
|
|
1464
|
-
loaderOrder: "before",
|
|
1465
|
-
boundaryEntry,
|
|
1466
|
-
},
|
|
1467
|
-
);
|
|
1468
|
-
segments.push(...parallelResult.segments);
|
|
1469
|
-
matchedIds.push(...parallelResult.matchedIds);
|
|
1543
|
+
if (declaresSlot(orphan.parallel)) {
|
|
1544
|
+
const parallelResult = await resolveParallelSegments(
|
|
1545
|
+
pass,
|
|
1546
|
+
orphan,
|
|
1547
|
+
belongsToRoute,
|
|
1548
|
+
{
|
|
1549
|
+
parentChainDefault: "force-render",
|
|
1550
|
+
loaderOrder: fresh ? "after" : "before",
|
|
1551
|
+
boundaryEntry,
|
|
1552
|
+
},
|
|
1553
|
+
);
|
|
1554
|
+
segments.push(...parallelResult.segments);
|
|
1555
|
+
matchedIds.push(...parallelResult.matchedIds);
|
|
1556
|
+
}
|
|
1470
1557
|
|
|
1471
|
-
//
|
|
1558
|
+
// Routeless entries nested in this one (a layout() after a bare cache()
|
|
1559
|
+
// marker, a wrapper inside a routeless wrapper) wrap the same content one
|
|
1560
|
+
// level deeper. Issue #918: they were never rendered. None is in the
|
|
1561
|
+
// matched chain: that would put this orphan in it, and
|
|
1562
|
+
// resolveSegment skips those.
|
|
1472
1563
|
for (const nested of orphan.layout) {
|
|
1473
|
-
const nestedResult = await
|
|
1564
|
+
const nestedResult = await resolveOrphanLayout(
|
|
1565
|
+
pass,
|
|
1474
1566
|
nested,
|
|
1475
|
-
params,
|
|
1476
|
-
context,
|
|
1477
|
-
clientSegmentIds,
|
|
1478
|
-
prevParams,
|
|
1479
|
-
request,
|
|
1480
|
-
prevUrl,
|
|
1481
|
-
nextUrl,
|
|
1482
|
-
routeKey,
|
|
1483
1567
|
belongsToRoute,
|
|
1484
|
-
deps,
|
|
1485
|
-
actionContext,
|
|
1486
|
-
stale,
|
|
1487
1568
|
undefined,
|
|
1488
1569
|
boundaryEntry,
|
|
1489
1570
|
);
|
|
@@ -1494,44 +1575,93 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1494
1575
|
return { segments, matchedIds };
|
|
1495
1576
|
}
|
|
1496
1577
|
|
|
1578
|
+
/** What resolveAllSegments needs beyond its pass. */
|
|
1579
|
+
export interface RevalidatingWalkOptions {
|
|
1580
|
+
/**
|
|
1581
|
+
* The intercept the request resolves: the route's entry (its handler and
|
|
1582
|
+
* loaders) is skipped and only its id is matched. None on a full match.
|
|
1583
|
+
*/
|
|
1584
|
+
readonly interceptResult?: InterceptResult | null;
|
|
1585
|
+
/** For the intercept skip's debug log. */
|
|
1586
|
+
readonly localRouteName?: string;
|
|
1587
|
+
/** Where an entry's error boundary reports (resolveWithErrorBoundary). */
|
|
1588
|
+
readonly pathname: string;
|
|
1589
|
+
/**
|
|
1590
|
+
* The matched route's full entry chain. Defaults to `entries`;
|
|
1591
|
+
* withCacheLookup passes it when it resolves only the entries above a
|
|
1592
|
+
* cache() boundary. An orphan list can hold an entry of the chain: a bare
|
|
1593
|
+
* cache() marker sits in its layout's layout[] (it wraps that layout's
|
|
1594
|
+
* routes) and is the parent of the routes after it. For those routes it
|
|
1595
|
+
* resolves once, as a chain entry, below the cache() header latch (issue
|
|
1596
|
+
* #918: resolved twice on a MISS, once and discarded on a HIT).
|
|
1597
|
+
*/
|
|
1598
|
+
readonly chain?: readonly EntryData[];
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/** An entry's error or not-found segment, as its whole result. */
|
|
1602
|
+
function boundaryResult(seg: ResolvedSegment): SegmentRevalidationResult {
|
|
1603
|
+
return { segments: [seg], matchedIds: [seg.id] };
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1497
1606
|
/**
|
|
1498
|
-
* Resolve all segments for a route
|
|
1607
|
+
* Resolve all segments for a route: a partial match (matchPartial), a full
|
|
1608
|
+
* match with a FRESH pass, or the FRESH pass of an intercept record's
|
|
1609
|
+
* background re-render (rerenderAndCacheRoute). A thrown action's pass is cut
|
|
1610
|
+
* at its error boundary (ActionContext.boundary).
|
|
1499
1611
|
*/
|
|
1500
|
-
export async function
|
|
1612
|
+
export async function resolveAllSegments<TEnv>(
|
|
1613
|
+
pass: ResolutionPass<TEnv>,
|
|
1501
1614
|
entries: EntryData[],
|
|
1502
|
-
|
|
1503
|
-
params: Record<string, string>,
|
|
1504
|
-
context: HandlerContext<any, TEnv>,
|
|
1505
|
-
clientSegmentSet: Set<string>,
|
|
1506
|
-
prevParams: Record<string, string>,
|
|
1507
|
-
request: Request,
|
|
1508
|
-
prevUrl: URL,
|
|
1509
|
-
nextUrl: URL,
|
|
1510
|
-
actionContext: ActionContext | undefined,
|
|
1511
|
-
interceptResult: { intercept: any; entry: EntryData } | null,
|
|
1512
|
-
localRouteName: string,
|
|
1513
|
-
pathname: string,
|
|
1514
|
-
deps: SegmentResolutionDeps<TEnv>,
|
|
1515
|
-
stale?: boolean,
|
|
1516
|
-
/** The matched route's full chain; see ResolveSegmentOptions.chain (fresh.ts). */
|
|
1517
|
-
chain: readonly EntryData[] = entries,
|
|
1615
|
+
walk: RevalidatingWalkOptions,
|
|
1518
1616
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
1617
|
+
const { context, params, routeKey, request } = pass;
|
|
1618
|
+
const { interceptResult, localRouteName, pathname } = walk;
|
|
1619
|
+
const chain = walk.chain ?? entries;
|
|
1519
1620
|
const allSegments: ResolvedSegment[] = [];
|
|
1520
1621
|
const matchedIds: string[] = [];
|
|
1521
1622
|
const seenSegIds = new Set<string>();
|
|
1522
1623
|
const seenMatchIds = new Set<string>();
|
|
1523
1624
|
|
|
1524
|
-
// ppr
|
|
1625
|
+
// ppr routes are document-scoped cached territory: the whole chain (root
|
|
1626
|
+
// layout down to the page) bakes into the shared shell, so the header-write
|
|
1627
|
+
// guard latches BEFORE any entry resolves (unlike the positional cache()
|
|
1628
|
+
// latch below). See assertCachedHeaderWriteAllowed (server/context.ts).
|
|
1525
1629
|
latchPprHeaderScopeForEntries(entries, routeKey);
|
|
1526
1630
|
|
|
1527
|
-
|
|
1631
|
+
// No router context at build time (prerender-match.ts): no telemetry sink.
|
|
1632
|
+
let telemetry;
|
|
1633
|
+
try {
|
|
1634
|
+
telemetry = getRouterContext()?.telemetry;
|
|
1635
|
+
} catch {}
|
|
1636
|
+
// FRESH: a full match reports an entry's error without isPartial, and a
|
|
1637
|
+
// build-time pass rethrows it (throwOnError). A re-render a navigation
|
|
1638
|
+
// started reports it with isPartial (partialRequest).
|
|
1639
|
+
const report = {
|
|
1640
|
+
request,
|
|
1641
|
+
url: context.url,
|
|
1642
|
+
routeKey,
|
|
1643
|
+
isPartial: pass.client !== FRESH || pass.partialRequest ? true : undefined,
|
|
1644
|
+
telemetry,
|
|
1645
|
+
};
|
|
1528
1646
|
const fillMatch = {
|
|
1529
1647
|
handlerContext: context,
|
|
1530
1648
|
isIntercept: !!interceptResult,
|
|
1531
|
-
clientSegmentSet,
|
|
1649
|
+
clientSegmentSet: pass.client,
|
|
1532
1650
|
};
|
|
1533
1651
|
|
|
1652
|
+
const boundary = pass.actionContext?.boundary;
|
|
1653
|
+
const boundaryId = boundary?.id;
|
|
1534
1654
|
for (const entry of entries) {
|
|
1655
|
+
if (entry.shortCode === boundaryId) {
|
|
1656
|
+
// The outlet's loader ids stay claimed. _handlerRan makes the browser
|
|
1657
|
+
// clear the outlet's handle values.
|
|
1658
|
+
allSegments.push({ ...boundary!, _handlerRan: true });
|
|
1659
|
+
matchedIds.push(boundaryId);
|
|
1660
|
+
entry.loader?.forEach((loaderEntry, i) =>
|
|
1661
|
+
matchedIds.push(`${entry.shortCode}D${i}.${loaderEntry.loader.$$id}`),
|
|
1662
|
+
);
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1535
1665
|
if (entry.type === "route" && interceptResult) {
|
|
1536
1666
|
debugLog(
|
|
1537
1667
|
"matchPartial.intercept",
|
|
@@ -1550,7 +1680,11 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1550
1680
|
if (interceptFillSkips(fillMatch, entry.shortCode)) continue;
|
|
1551
1681
|
|
|
1552
1682
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1553
|
-
//
|
|
1683
|
+
// Set ALS flag when entering a cache() boundary so that ctx.get()
|
|
1684
|
+
// can guard non-cacheable variable reads. Also latch the header-write
|
|
1685
|
+
// scope (response-level side effects — headers/cookies/status).
|
|
1686
|
+
// Persists for all descendant entries. A route entry carries `cache` when
|
|
1687
|
+
// cache() is among its own children (dsl-helpers.ts cache()).
|
|
1554
1688
|
if (entry.cache) {
|
|
1555
1689
|
const store = RangoContext.getStore();
|
|
1556
1690
|
if (store) store.insideCacheScope = true;
|
|
@@ -1560,32 +1694,19 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1560
1694
|
const resolved = await resolveWithErrorBoundary(
|
|
1561
1695
|
nonParallelEntry,
|
|
1562
1696
|
params,
|
|
1563
|
-
() =>
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
params,
|
|
1568
|
-
context,
|
|
1569
|
-
clientSegmentSet,
|
|
1570
|
-
prevParams,
|
|
1571
|
-
request,
|
|
1572
|
-
prevUrl,
|
|
1573
|
-
nextUrl,
|
|
1574
|
-
deps,
|
|
1575
|
-
actionContext,
|
|
1576
|
-
stale,
|
|
1577
|
-
chain,
|
|
1578
|
-
),
|
|
1579
|
-
(seg) => ({ segments: [seg], matchedIds: [seg.id] }),
|
|
1580
|
-
deps,
|
|
1581
|
-
{ request, url: context.url, routeKey, isPartial: true, telemetry },
|
|
1697
|
+
() => resolveSegment(pass, nonParallelEntry, chain),
|
|
1698
|
+
boundaryResult,
|
|
1699
|
+
pass.deps,
|
|
1700
|
+
report,
|
|
1582
1701
|
pathname,
|
|
1702
|
+
pass.throwOnError,
|
|
1583
1703
|
);
|
|
1584
1704
|
doneEntry();
|
|
1585
1705
|
|
|
1586
|
-
// Deduplicate segments and matchedIds by ID
|
|
1587
|
-
//
|
|
1588
|
-
//
|
|
1706
|
+
// Deduplicate segments and matchedIds by ID. include() scopes can produce
|
|
1707
|
+
// entries that resolve the same shared layout/loader segment. Duplicates
|
|
1708
|
+
// in the segment array propagate to the client's matched[] and change
|
|
1709
|
+
// the React tree depth.
|
|
1589
1710
|
for (const seg of resolved.segments) {
|
|
1590
1711
|
if (!seenSegIds.has(seg.id)) {
|
|
1591
1712
|
seenSegIds.add(seg.id);
|