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