@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
@@ -159,6 +159,82 @@ export function createPrerenderStore(): PrerenderStore | null {
159
159
  };
160
160
  }
161
161
 
162
+ /**
163
+ * The segments of a prerendered URL's intercept variant (`/i`): the main
164
+ * variant's without the intercepted route's own (`belongsToRoute`: its
165
+ * handler, layouts and slots), then the intercept's. A render under an
166
+ * intercept skips the route's entry (resolveAllSegments), so the variant
167
+ * holds what a cache() intercept record holds. The producer
168
+ * (matchForPrerender) encodes `interceptHandles` for exactly these segments.
169
+ */
170
+ export function interceptVariantSegments(
171
+ segments: SerializedSegmentData[],
172
+ interceptSegments: SerializedSegmentData[],
173
+ ): SerializedSegmentData[] {
174
+ return [
175
+ ...segments.filter((s) => s.metadata.belongsToRoute !== true),
176
+ ...interceptSegments,
177
+ ];
178
+ }
179
+
180
+ /**
181
+ * The intercept variant as served under the intercept the match selected
182
+ * (findInterceptForRoute): its main segments and that intercept's slot,
183
+ * `slotId` in `intercept:<routeName>`. The build bakes every intercept that
184
+ * can target the route, since it cannot evaluate when(). Null when the
185
+ * variant does not hold that slot: the request renders live. The handles
186
+ * string is kept whole; decodeStoredEntry (cache-lookup.ts) replays only the
187
+ * served segments' pushes.
188
+ */
189
+ export function selectInterceptVariant(
190
+ entry: PrerenderEntry,
191
+ slotId: string,
192
+ routeName: string,
193
+ ): PrerenderEntry | null {
194
+ const namespace = `intercept:${routeName}`;
195
+ let found = false;
196
+ const segments = entry.segments.filter(({ metadata }) => {
197
+ if (!metadata.namespace?.startsWith("intercept:")) return true;
198
+ if (metadata.id !== slotId || metadata.namespace !== namespace) {
199
+ return false;
200
+ }
201
+ found = true;
202
+ return true;
203
+ });
204
+ return found ? { segments, handles: entry.handles } : null;
205
+ }
206
+
207
+ /**
208
+ * The manifest entries ([key, PrerenderEntry JSON]) one prerendered URL
209
+ * bakes, keyed as the production store's get() reads them. The intercept
210
+ * variant (`/i`) holds interceptVariantSegments with the handle string the
211
+ * producer encoded for it. Written by the build
212
+ * (vite/discovery/prerender-collection.ts) and serveShellRequest.
213
+ */
214
+ export function prerenderManifestEntries(
215
+ result: PrerenderEntry & {
216
+ routeName: string;
217
+ interceptSegments?: SerializedSegmentData[];
218
+ interceptHandles?: string;
219
+ },
220
+ paramHash: string,
221
+ ): Array<[key: string, value: string]> {
222
+ const { segments, handles, interceptSegments } = result;
223
+ const key = `${result.routeName}/${paramHash}`;
224
+ const entries: Array<[string, string]> = [
225
+ [key, JSON.stringify({ segments, handles })],
226
+ ];
227
+ if (interceptSegments?.length) {
228
+ const intercept = interceptVariantSegments(segments, interceptSegments);
229
+ const interceptHandles = result.interceptHandles ?? "";
230
+ entries.push([
231
+ `${key}/i`,
232
+ JSON.stringify({ segments: intercept, handles: interceptHandles }),
233
+ ]);
234
+ }
235
+ return entries;
236
+ }
237
+
162
238
  /**
163
239
  * Create a static segment store.
164
240
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
@@ -8,7 +8,10 @@
8
8
  */
9
9
 
10
10
  import type { HandlerCacheConfig } from "../rsc/types.js";
11
- import type { ShellCaptureDebugEvent } from "../rsc/shell-capture.js";
11
+ import type {
12
+ ShellCaptureDebugEvent,
13
+ ShellCaptureState,
14
+ } from "../rsc/shell-capture.js";
12
15
  import type { PrerenderWarmCaches } from "./on-demand.js";
13
16
 
14
17
  /** What one warm request did: written by the layers, read by the trigger. */
@@ -36,7 +39,7 @@ export interface PrerenderWarmRecord extends PrerenderWarmCaches {
36
39
  /** What the helpers read; a derived capture context inherits the record. */
37
40
  export interface WarmContext {
38
41
  _prerenderWarm?: PrerenderWarmRecord;
39
- _shellCaptureRun?: boolean;
42
+ _shellCaptureState?: Pick<ShellCaptureState, "run">;
40
43
  }
41
44
 
42
45
  /**
@@ -79,14 +82,15 @@ export function readWarmMark(
79
82
  * "use cache" in-flight dedup lives outside the store, so a wrapper would
80
83
  * miss both.
81
84
  *
82
- * False in the warm's own shell capture (`_shellCaptureRun`): it runs after
85
+ * False in the warm's own shell capture (`_shellCaptureState`): it runs after
83
86
  * the warm's writes settled and must replay them, or it would render every
84
87
  * handler a second time and could bake another generation than the one just
85
88
  * written.
86
89
  */
87
90
  export function isWarmReplace(ctx: WarmContext | null | undefined): boolean {
88
91
  return (
89
- ctx?._prerenderWarm?.mode === "replace" && ctx._shellCaptureRun !== true
92
+ ctx?._prerenderWarm?.mode === "replace" &&
93
+ ctx._shellCaptureState?.run !== true
90
94
  );
91
95
  }
92
96
 
package/src/prerender.ts CHANGED
@@ -490,8 +490,8 @@ export function isPrerenderPassthrough(
490
490
  * A build handler signals passthrough by returning `ctx.passthrough()` (the
491
491
  * PRERENDER_PASSTHROUGH sentinel), which lands on the segment's `component`.
492
492
  * But when the route declares `loading()`, the handler result is deferred
493
- * upstream (segment-resolution/fresh.ts), so `component` is a thenable resolving
494
- * to the sentinel rather than the sentinel itself — a synchronous
493
+ * upstream (resolveEntryHandler), so `component` is a thenable
494
+ * resolving to the sentinel rather than the sentinel itself — a synchronous
495
495
  * `isPrerenderPassthrough(component)` on the Promise returns false and the build
496
496
  * bakes a corrupt artifact instead of deferring. Resolve thenables first.
497
497
  *
package/src/route-name.ts CHANGED
@@ -63,3 +63,11 @@ export function validateUserRouteName(name: string): void {
63
63
  }
64
64
  }
65
65
  }
66
+
67
+ /**
68
+ * The route key's last name segment ("item" for "shop.item"). An intercept
69
+ * that targets it is selected when none targets the full key (match-api.ts).
70
+ */
71
+ export function localRouteName(routeKey: string): string {
72
+ return routeKey.slice(routeKey.lastIndexOf(".") + 1);
73
+ }
@@ -8,6 +8,10 @@
8
8
  import { createElement, type ReactNode } from "react";
9
9
  import type { EntryData } from "../server/context.js";
10
10
  import { runIdentityExempt } from "../cache/cache-exec-scope.js";
11
+ import {
12
+ _getRequestContext,
13
+ isBackgroundWork,
14
+ } from "../server/request-context.js";
11
15
  import type {
12
16
  ResolvedSegment,
13
17
  ErrorInfo,
@@ -39,7 +43,7 @@ export interface InvokeOnErrorContext<TEnv = any> {
39
43
  isPartial?: boolean;
40
44
  handledByBoundary?: boolean;
41
45
  metadata?: Record<string, unknown>;
42
- /** Request start time from performance.now() for duration calculation */
46
+ /** performance.now() at request entry; the request context's by default. */
43
47
  requestStartTime?: number;
44
48
  }
45
49
 
@@ -50,6 +54,12 @@ export interface InvokeOnErrorContext<TEnv = any> {
50
54
  * This is a shared utility used by both the router and RSC handler to ensure
51
55
  * consistent error callback behavior across the codebase.
52
56
  *
57
+ * `background` is set inside runAsBackgroundWork (server/request-context.ts),
58
+ * and `duration` and `env` come from the request context on the ALS
59
+ * (`_handlerStart`, `env`) when the call site leaves them out, so no call
60
+ * site has to know. A Prerender pass's ctx.env throws without buildEnv, so
61
+ * the walk never reads it eagerly.
62
+ *
53
63
  * @param onError - The onError callback to invoke (may be undefined)
54
64
  * @param error - The error that occurred
55
65
  * @param phase - The phase where the error occurred
@@ -66,9 +76,10 @@ export function invokeOnError<TEnv = any>(
66
76
  if (!onError) return;
67
77
 
68
78
  const errorObj = error instanceof Error ? error : new Error(String(error));
69
- const duration = context.requestStartTime
70
- ? performance.now() - context.requestStartTime
71
- : undefined;
79
+ const reqCtx = _getRequestContext();
80
+ const background = isBackgroundWork() || undefined;
81
+ const start = context.requestStartTime ?? reqCtx?._handlerStart;
82
+ const duration = start !== undefined ? performance.now() - start : undefined;
72
83
 
73
84
  const errorContext: OnErrorContext<TEnv> = {
74
85
  error: errorObj,
@@ -84,10 +95,11 @@ export function invokeOnError<TEnv = any>(
84
95
  loaderName: context.loaderName,
85
96
  middlewareId: context.middlewareId,
86
97
  actionId: context.actionId,
87
- env: context.env,
98
+ env: context.env ?? (reqCtx?.env as TEnv | undefined),
88
99
  duration,
89
100
  isPartial: context.isPartial,
90
- handledByBoundary: context.handledByBoundary,
101
+ background,
102
+ handledByBoundary: background ? false : context.handledByBoundary,
91
103
  stack: errorObj.stack,
92
104
  metadata: context.metadata,
93
105
  };
@@ -111,10 +123,10 @@ export function invokeOnError<TEnv = any>(
111
123
  /**
112
124
  * First non-undefined `pick(orphan)` over the routeless entries in
113
125
  * `entry.layout` and, recursively, in theirs: depth-first in render order
114
- * (resolveOrphanLayout in segment-resolution/fresh.ts). Since #922 a routeless
115
- * entry nested in another renders (a layout() after a bare cache() marker, a
116
- * layout() in a routeless wrapper); a one-level scan missed the boundaries and
117
- * intercepts it declares (#926).
126
+ * (resolveOrphanLayout in segment-resolution/revalidation.ts).
127
+ * Since #922 a routeless entry nested in another renders (a layout() after a
128
+ * bare cache() marker, a layout() in a routeless wrapper); a one-level scan
129
+ * missed the boundaries and intercepts it declares (#926).
118
130
  *
119
131
  * `skip` is the entry an upward walk came from. Only a bare cache() marker is
120
132
  * both in its layout's layout[] and in the chain, and its subtree was already
@@ -428,9 +428,10 @@ function observeInSegmentScope<C, R>(
428
428
  * loop (request.start/end/error, cache.decision, ...).
429
429
  * - segment-resolution/helpers.ts emits via a caller-provided report.telemetry
430
430
  * sink rather than the ALS router context.
431
- * - rsc/handler.ts handleTimeoutResponse (request.timeout), the origin guard
432
- * (request.origin-rejected), and handleStore.onError (late-handle
433
- * handler.error) emit via router.telemetry directly — they run outside the
431
+ * - rsc/request-stages.ts timeoutResponse (request.timeout) and
432
+ * originRejectedResponse (request.origin-rejected), and rsc/handler.ts
433
+ * handleStore.onError (late-handle handler.error) emit via
434
+ * router.telemetry directly — they run outside the
434
435
  * RouterContext ALS (only match()/matchPartial() enter it), so a
435
436
  * getRouterContext() read there throws and the event would vanish.
436
437
  */
@@ -11,11 +11,7 @@ import type {
11
11
  InterceptEntry,
12
12
  InterceptSelectorContext,
13
13
  } from "../server/context";
14
- import type {
15
- HandlerContext,
16
- InternalHandlerContext,
17
- ResolvedSegment,
18
- } from "../types";
14
+ import type { InternalHandlerContext, ResolvedSegment } from "../types";
19
15
  import {
20
16
  handleHandlerResult,
21
17
  warnOnStreamedResponse,
@@ -28,18 +24,15 @@ import {
28
24
  deliverStartedLoaders,
29
25
  resolveOwnerLoaders,
30
26
  } from "./segment-resolution/revalidation.js";
31
- import {
32
- fillSends,
33
- NOTHING_HELD,
34
- } from "./segment-resolution/prefetch-deferral.js";
35
- import type { SegmentResolutionDeps } from "./types.js";
27
+ import { fillSends } from "./segment-resolution/prefetch-deferral.js";
28
+ import type { ResolutionPass } from "./segment-resolution/resolution-pass.js";
36
29
  import { getRouterContext } from "./router-context.js";
37
- import type { RevalidationContext } from "./router-context.js";
38
30
  import { runInSegmentTagScope } from "../cache/cache-tag.js";
39
31
  import {
40
32
  RangoContext,
41
33
  latchPprHeaderScopeForEntries,
42
34
  } from "../server/context.js";
35
+ import { _getRequestContext } from "../server/request-context.js";
43
36
 
44
37
  /**
45
38
  * Header-guard latch for the intercept funnels (issue #713). ppr and
@@ -171,31 +164,40 @@ export function findInterceptForRoute(
171
164
  /**
172
165
  * An intercept handler's throw or notFound() renders the declaring entry's
173
166
  * boundary as the slot component (catchSegmentError, as for route handlers on
174
- * partial navigations: 500/404 status, onError "handler"). Responses and every
175
- * error on the background re-render (`background`) rethrow, so a failed
176
- * re-render aborts its cache write instead of storing the fallback.
167
+ * partial navigations: 500/404 status, onError "handler" naming the slot).
168
+ * Responses rethrow. The background re-render (`background`) reports the
169
+ * error the same way and then rethrows it, so a failed re-render aborts its
170
+ * cache write instead of storing the fallback.
177
171
  */
178
172
  function renderInterceptHandlerError<TEnv>(
173
+ pass: ResolutionPass<TEnv>,
179
174
  error: unknown,
180
175
  parentEntry: EntryData,
181
- params: Record<string, string>,
182
- context: HandlerContext<any, TEnv>,
183
- deps: SegmentResolutionDeps<TEnv>,
176
+ slotSegmentId: string,
184
177
  background: boolean | undefined,
185
178
  ): ReactNode {
186
- if (error instanceof Response || background) throw error;
187
- return catchSegmentError(
179
+ if (error instanceof Response) throw error;
180
+ // A background re-render's waitUntil task may run outside the router
181
+ // context, as the segment walk's report allows.
182
+ let telemetry;
183
+ try {
184
+ telemetry = getRouterContext()?.telemetry;
185
+ } catch {}
186
+ const { component } = catchSegmentError(
188
187
  error,
189
188
  parentEntry,
190
- params,
191
- deps,
189
+ pass.params,
190
+ pass.deps,
192
191
  {
193
- ...buildLoaderErrorContext(context),
192
+ ...buildLoaderErrorContext(pass.context),
194
193
  isPartial: true,
195
- telemetry: getRouterContext()?.telemetry,
194
+ telemetry,
196
195
  },
197
- context.pathname,
198
- ).component;
196
+ pass.context.pathname,
197
+ { id: slotSegmentId, type: "parallel" },
198
+ );
199
+ if (background) throw error;
200
+ return component;
199
201
  }
200
202
 
201
203
  /** The segment id of `intercept`'s slot on `parentEntry`. */
@@ -206,6 +208,33 @@ export function interceptSlotId(
206
208
  return `${parentEntry.shortCode}.${intercept.slotName}`;
207
209
  }
208
210
 
211
+ /**
212
+ * The intercept a match selected (findInterceptForRoute), as its cache()
213
+ * record names it (CacheScope): the slot id, then `~` and its position among
214
+ * the intercepts its entry declares for the same slot and target route. Two
215
+ * such intercepts share the slot id, and the position tells them apart; one
216
+ * for another slot or route does not renumber it. `~` is unreserved in a
217
+ * URL, so a store that maps the key onto one unencoded keeps the whole key.
218
+ * Undefined when the match selected none.
219
+ */
220
+ export function interceptRecordKey(
221
+ selected: { intercept: InterceptEntry; entry: EntryData } | null,
222
+ ): string | undefined {
223
+ if (!selected) return undefined;
224
+ const { intercept, entry } = selected;
225
+ let position = 0;
226
+ for (const other of entry.intercept) {
227
+ if (other === intercept) break;
228
+ if (
229
+ other.slotName === intercept.slotName &&
230
+ other.routeName === intercept.routeName
231
+ ) {
232
+ position++;
233
+ }
234
+ }
235
+ return `${interceptSlotId(entry, intercept)}~${position}`;
236
+ }
237
+
209
238
  /**
210
239
  * Every segment id an intercept's slot owns, in order: the slot, then one
211
240
  * loader segment per loader in declaration order, run or not. A request that
@@ -228,14 +257,13 @@ export function interceptSegmentIds(
228
257
  * revalidated on its own, kept in a fill, deferred by a prefetch when flagged.
229
258
  */
230
259
  function resolveInterceptLoaders<TEnv>(
260
+ pass: ResolutionPass<TEnv>,
231
261
  interceptEntry: InterceptEntry,
232
262
  parentEntry: EntryData,
233
- context: HandlerContext<any, TEnv>,
234
263
  belongsToRoute: boolean,
235
- deps: SegmentResolutionDeps<TEnv>,
236
- rc: RevalidationContext,
237
264
  ): Promise<{ segments: ResolvedSegment[] }> {
238
265
  return resolveOwnerLoaders(
266
+ pass,
239
267
  {
240
268
  loaders: interceptEntry.loader,
241
269
  namespace: `intercept:${interceptEntry.routeName}`,
@@ -244,17 +272,7 @@ function resolveInterceptLoaders<TEnv>(
244
272
  bakeLane: false,
245
273
  traceSource: "intercept-loader",
246
274
  },
247
- context,
248
275
  belongsToRoute,
249
- rc.clientSegmentIds,
250
- rc.prevParams,
251
- rc.request,
252
- rc.prevUrl,
253
- rc.nextUrl,
254
- rc.routeKey,
255
- deps,
256
- rc.actionContext,
257
- rc.stale,
258
276
  );
259
277
  }
260
278
 
@@ -271,45 +289,37 @@ async function settleLoaderSegments(
271
289
  }
272
290
 
273
291
  /**
274
- * Resolve an intercept entry: its slot segment, then its loader segments.
292
+ * Resolve an intercept entry: its slot segment, then its loader segments. The
293
+ * background re-render resolves with a pass that holds nothing
294
+ * (heldNothingPassInput, resolution-pass.ts): every segment renders fresh, and
295
+ * its loaders are skipped (skipLoaders), each registered for the handler's
296
+ * read.
275
297
  */
276
298
  export async function resolveInterceptEntry<TEnv>(
299
+ pass: ResolutionPass<TEnv>,
277
300
  interceptEntry: InterceptEntry,
278
301
  parentEntry: EntryData,
279
- params: Record<string, string>,
280
- context: HandlerContext<any, TEnv>,
281
302
  belongsToRoute: boolean,
282
- deps: SegmentResolutionDeps<TEnv>,
283
- // The background re-render passes none: nothing held, rendered fresh.
284
- revalidationContext: RevalidationContext = {
285
- clientSegmentIds: NOTHING_HELD,
286
- prevParams: {},
287
- request: context.request,
288
- prevUrl: context.url,
289
- nextUrl: context.url,
290
- routeKey: interceptEntry.routeName,
291
- },
292
303
  options?: {
293
304
  /**
294
305
  * Set ONLY by the post-response background re-render (stale background
295
306
  * revalidation), whose sole purpose is to re-render the segment tree to
296
- * populate the cache: a handler error rejects instead of rendering the
297
- * error boundary, so a failure is never cached. Never set on the
298
- * foreground path.
307
+ * populate the cache: a handler error is reported, then rejects instead
308
+ * of rendering the error boundary, so a failure is never cached. Never
309
+ * set on the foreground path.
299
310
  */
300
311
  background?: boolean;
301
312
  },
302
313
  ): Promise<ResolvedSegment[]> {
303
314
  latchPprHeaderScopeForInterceptTarget(interceptEntry);
304
315
 
316
+ const { context, params } = pass;
305
317
  const slotSegmentId = interceptSlotId(parentEntry, interceptEntry);
306
318
  const { segments: loaderSegments } = await resolveInterceptLoaders(
319
+ pass,
307
320
  interceptEntry,
308
321
  parentEntry,
309
- context,
310
322
  belongsToRoute,
311
- deps,
312
- revalidationContext,
313
323
  );
314
324
  // The slot's tag scope (#957, cache-tag.ts runInSegmentTagScope) and the
315
325
  // owner of its handler's handle pushes (a handler's ctx.use(Handle) reads
@@ -337,10 +347,7 @@ export async function resolveInterceptEntry<TEnv>(
337
347
  // A fill keeps a slot the client holds: neither its handler nor its layout
338
348
  // runs, only the loaders the client lacks (match-result.ts leaves the null
339
349
  // slot out of the response).
340
- if (
341
- fillSends(context, slotSegmentId, revalidationContext.clientSegmentIds) ===
342
- false
343
- ) {
350
+ if (fillSends(context, slotSegmentId, pass.client) === false) {
344
351
  return [slotSegment(null, undefined), ...loaderSegments];
345
352
  }
346
353
 
@@ -355,11 +362,10 @@ export async function resolveInterceptEntry<TEnv>(
355
362
  : handler;
356
363
  } catch (error) {
357
364
  handlerResult = renderInterceptHandlerError(
365
+ pass,
358
366
  error,
359
367
  parentEntry,
360
- params,
361
- context,
362
- deps,
368
+ slotSegmentId,
363
369
  options?.background,
364
370
  );
365
371
  }
@@ -392,13 +398,14 @@ export async function resolveInterceptEntry<TEnv>(
392
398
  // recovered into the declared boundary, except on the background
393
399
  // re-render, which must reject so a failure is never cached.
394
400
  const streamedComponent = (promise: Promise<unknown>): ReactNode =>
395
- trackStreamedHandler(deps, promise, {
401
+ trackStreamedHandler(pass.deps, promise, {
396
402
  boundaryEntry: parentEntry,
397
403
  segmentId: slotSegmentId,
398
404
  segmentType: "parallel",
399
405
  context,
400
406
  routeKey: interceptEntry.routeName,
401
407
  params,
408
+ isPartial: true,
402
409
  recover: !options?.background,
403
410
  });
404
411
 
@@ -423,11 +430,10 @@ export async function resolveInterceptEntry<TEnv>(
423
430
  component = await handlerResult;
424
431
  } catch (error) {
425
432
  component = renderInterceptHandlerError(
433
+ pass,
426
434
  error,
427
435
  parentEntry,
428
- params,
429
- context,
430
- deps,
436
+ slotSegmentId,
431
437
  options?.background,
432
438
  );
433
439
  }
@@ -445,23 +451,24 @@ export async function resolveInterceptEntry<TEnv>(
445
451
  * keeps its cached component and layout, its loaders are never cached.
446
452
  */
447
453
  export async function resolveInterceptLoadersOnly<TEnv>(
454
+ pass: ResolutionPass<TEnv>,
448
455
  interceptEntry: InterceptEntry,
449
456
  parentEntry: EntryData,
450
- context: HandlerContext<any, TEnv>,
451
457
  belongsToRoute: boolean,
452
- deps: SegmentResolutionDeps<TEnv>,
453
- revalidationContext: RevalidationContext,
454
458
  ): Promise<ResolvedSegment[]> {
455
459
  latchPprHeaderScopeForInterceptTarget(interceptEntry);
460
+ const handleStore = _getRequestContext()?._handleStore;
456
461
  const { segments } = await resolveInterceptLoaders(
462
+ pass,
457
463
  interceptEntry,
458
464
  parentEntry,
459
- context,
460
465
  belongsToRoute,
461
- deps,
462
- revalidationContext,
466
+ );
467
+ // Where resolveInterceptEntry runs the handler.
468
+ handleStore?.releaseRecordedValues(
469
+ interceptSlotId(parentEntry, interceptEntry),
463
470
  );
464
471
  if (!interceptEntry.loading) await settleLoaderSegments(segments);
465
- deliverStartedLoaders(context, segments);
472
+ deliverStartedLoaders(pass.context, segments);
466
473
  return segments;
467
474
  }
@@ -341,7 +341,8 @@ export function wrapLoaderWithErrorHandling<T>(
341
341
  // Render fallback on server. The user ErrorBoundaryHandler may throw
342
342
  // synchronously; if it does we must NOT let that rejection escape — the
343
343
  // wrapped LoaderDataResult promise is contracted to never reject (see
344
- // segment-resolution/fresh.ts `await Promise.all(...wrapped)`), and a
344
+ // resolveOwnerLoaders `await Promise.all(...)` in
345
+ // segment-resolution/revalidation.ts), and a
345
346
  // rejection here would collapse the whole entry and discard healthy
346
347
  // sibling loader data. On a fallback-render throw, fall back to the
347
348
  // no-boundary result (fallback: null) so the client throws the ORIGINAL
@@ -585,7 +586,7 @@ function createLoaderExecutor<TEnv>(
585
586
  const variables = internal._variables;
586
587
 
587
588
  // Capture whether this loader is being started from a DSL loader scope
588
- // (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
589
+ // (runInsideLoaderScope in resolveOwnerLoaders). Handler-invoked loaders are NOT
589
590
  // inside loader scope. This determines whether rendered() is allowed.
590
591
  const isDslLoader = isInsideLoaderScope();
591
592
  // The body resolves ctx.reverse(".name") in its DSL declaration's module
@@ -596,7 +597,7 @@ function createLoaderExecutor<TEnv>(
596
597
 
597
598
  // Owning segment for ctx.handle() attribution, captured SYNCHRONOUSLY at
598
599
  // kickoff: segment resolution sets _currentSegmentId while resolving the
599
- // segment that declares this loader (fresh.ts), and the value moves on as
600
+ // segment that declares this loader (resolveOwnerLoaders), and the value moves on as
600
601
  // resolution continues — by the time the async body pushes, it would be
601
602
  // stale. Attributing to the owning route/layout segment gives handle
602
603
  // pushes the same granularity handler pushes have (segmentOrder-correct
@@ -844,7 +845,7 @@ function createLoaderExecutor<TEnv>(
844
845
  // Run the loader body inside loader scope so request-scoped reads
845
846
  // (cookies()/headers() and non-cacheable ctx.get) are exempt from the
846
847
  // cache-purity guards: loaders always run fresh, so their reads never leak
847
- // into a cached segment. DSL loaders are already wrapped by fresh.ts; this
848
+ // into a cached segment. DSL loaders are already wrapped by resolveOwnerLoaders; this
848
849
  // also covers handler-invoked loaders (ctx.use(Loader) from a handler),
849
850
  // which otherwise execute in the caller's cache scope and would wrongly
850
851
  // throw. rendered() gating uses the captured isDslLoader (above), so this
@@ -184,8 +184,9 @@ export async function createMatchContextForFull<TEnv>(
184
184
  // A shell capture credits a push under a live-lane loader's body to that
185
185
  // loader (shell-capture.ts deriveShellCaptureContext). The lanes are set
186
186
  // before any loader runs: a bake-lane loader can start a later one.
187
- if (reqCtx?._shellCaptureRun) {
188
- reqCtx._shellCaptureLoaderLanes = routeLoaderLanes(snapshot.entries);
187
+ const captureState = reqCtx?._shellCaptureState;
188
+ if (captureState?.run) {
189
+ captureState.loaderLanes = routeLoaderLanes(snapshot.entries);
189
190
  }
190
191
  // A document never defers; the plan feeds the ctx.rendered() guards.
191
192
  (handlerContext as InternalHandlerContext<any, TEnv>)._prefetchDeferral =
@@ -208,7 +209,6 @@ export async function createMatchContextForFull<TEnv>(
208
209
  routeKey: matched.routeKey,
209
210
  localRouteName: snapshot.localRouteName,
210
211
  handlerContext,
211
- loaderPromises,
212
212
  routeMap: deps.getRouteMap(),
213
213
  metricsStore,
214
214
  Store,
@@ -492,7 +492,7 @@ export async function createMatchContextForPartial<TEnv>(
492
492
  prefetch: kind === "prefetch",
493
493
  method: request.method,
494
494
  isAction,
495
- isShellCapture: reqCtx?._shellCaptureRun === true,
495
+ isShellCapture: reqCtx?._shellCaptureState?.run === true,
496
496
  }),
497
497
  matched,
498
498
  cacheScope,
@@ -514,6 +514,7 @@ export async function createMatchContextForPartial<TEnv>(
514
514
  clientSegmentIds: nav.clientSegmentIds,
515
515
  clientSegmentSet,
516
516
  stale: nav.stale,
517
+ heldBoundary: nav.heldBoundary,
517
518
  prevUrl: nav.prevUrl,
518
519
  prevParams: nav.prevParams,
519
520
  prevMatch: nav.prevMatch,
@@ -523,7 +524,6 @@ export async function createMatchContextForPartial<TEnv>(
523
524
  routeKey: matched.routeKey,
524
525
  localRouteName: snapshot.localRouteName,
525
526
  handlerContext,
526
- loaderPromises,
527
527
  routeMap: deps.getRouteMap(),
528
528
  metricsStore,
529
529
  Store,