@rangojs/router 0.27.0 → 0.28.0

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