@rangojs/router 0.27.0 → 0.29.0

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