@rangojs/router 0.26.6 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) 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-coordinator.d.ts +0 -4
  4. package/dist/types/browser/action-fence.d.ts +11 -3
  5. package/dist/types/browser/event-controller.d.ts +20 -1
  6. package/dist/types/browser/history-state.d.ts +7 -9
  7. package/dist/types/browser/intercept-utils.d.ts +23 -0
  8. package/dist/types/browser/navigation-store.d.ts +8 -6
  9. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  10. package/dist/types/browser/rsc-router.d.ts +1 -1
  11. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  12. package/dist/types/browser/types.d.ts +2 -1
  13. package/dist/types/browser/wire-protocol.d.ts +25 -0
  14. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  15. package/dist/types/cache/cache-scope.d.ts +34 -22
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  18. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  19. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  20. package/dist/types/cache/segment-codec.d.ts +14 -0
  21. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  22. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  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 +4 -6
  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 +4 -4
  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 +12 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
  38. package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
  39. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  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/revalidation.d.ts +6 -1
  44. package/dist/types/router/router-context.d.ts +9 -63
  45. package/dist/types/router/router-interfaces.d.ts +8 -14
  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 +93 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution.d.ts +1 -2
  52. package/dist/types/router/segment-wrappers.d.ts +11 -48
  53. package/dist/types/router/telemetry.d.ts +2 -0
  54. package/dist/types/router/types.d.ts +10 -0
  55. package/dist/types/rsc/full-payload.d.ts +2 -2
  56. package/dist/types/rsc/handler-context.d.ts +6 -6
  57. package/dist/types/rsc/json-route-result.d.ts +5 -9
  58. package/dist/types/rsc/request-stages.d.ts +203 -0
  59. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  60. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  61. package/dist/types/rsc/routine-plan.d.ts +1 -1
  62. package/dist/types/rsc/server-action.d.ts +2 -2
  63. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture.d.ts +132 -15
  65. package/dist/types/rsc/shell-serve.d.ts +37 -12
  66. package/dist/types/server/context.d.ts +9 -8
  67. package/dist/types/server/handle-store.d.ts +16 -0
  68. package/dist/types/server/render-pass.d.ts +72 -0
  69. package/dist/types/server/request-context.d.ts +24 -104
  70. package/dist/types/server/rewrite-trail.d.ts +2 -2
  71. package/dist/types/testing/dispatch.d.ts +35 -18
  72. package/dist/types/testing/serve-shell-request.d.ts +7 -0
  73. package/dist/types/types/cache-types.d.ts +6 -3
  74. package/dist/types/types/error-types.d.ts +9 -1
  75. package/dist/types/types/handler-context.d.ts +18 -7
  76. package/dist/types/types/segments.d.ts +1 -1
  77. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  78. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  79. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  80. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  81. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  82. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  83. package/dist/types/vite/discovery/state.d.ts +12 -0
  84. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  85. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  86. package/dist/types/vite/router-discovery.d.ts +8 -47
  87. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  88. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  89. package/dist/types/wire-params.d.ts +34 -0
  90. package/dist/vite/index.js +3809 -3710
  91. package/package.json +1 -1
  92. package/skills/cache-guide/SKILL.md +8 -5
  93. package/skills/caching/SKILL.md +37 -18
  94. package/skills/client-urls/SKILL.md +2 -1
  95. package/skills/document-cache/SKILL.md +18 -1
  96. package/skills/handler-use/SKILL.md +1 -1
  97. package/skills/intercept/SKILL.md +53 -13
  98. package/skills/layout/SKILL.md +11 -7
  99. package/skills/loader/SKILL.md +30 -13
  100. package/skills/middleware/SKILL.md +19 -1
  101. package/skills/migrate-nextjs/SKILL.md +5 -4
  102. package/skills/parallel/SKILL.md +5 -4
  103. package/skills/ppr/SKILL.md +19 -3
  104. package/skills/prerender/SKILL.md +39 -15
  105. package/skills/rango/SKILL.md +23 -11
  106. package/skills/server-actions/SKILL.md +51 -6
  107. package/skills/testing/predicates.md +1 -1
  108. package/skills/testing/response-routes.md +6 -4
  109. package/skills/testing/serve-shell-request.md +7 -7
  110. package/skills/use-cache/SKILL.md +46 -8
  111. package/skills/ux-architecture/SKILL.md +3 -2
  112. package/src/__internal.ts +7 -1
  113. package/src/browser/action-coordinator.ts +2 -13
  114. package/src/browser/action-fence.ts +11 -3
  115. package/src/browser/connection-warmup.ts +3 -1
  116. package/src/browser/event-controller.ts +42 -3
  117. package/src/browser/history-state.ts +15 -13
  118. package/src/browser/intercept-utils.ts +51 -0
  119. package/src/browser/navigation-bridge.ts +34 -30
  120. package/src/browser/navigation-client.ts +21 -33
  121. package/src/browser/navigation-store.ts +25 -27
  122. package/src/browser/navigation-transaction.ts +7 -11
  123. package/src/browser/partial-update.ts +8 -0
  124. package/src/browser/prefetch/cache.ts +2 -1
  125. package/src/browser/prefetch/fetch.ts +8 -16
  126. package/src/browser/prefetch/queue.ts +4 -4
  127. package/src/browser/react/NavigationProvider.tsx +2 -1
  128. package/src/browser/react/filter-segment-order.ts +2 -2
  129. package/src/browser/rsc-router.tsx +17 -2
  130. package/src/browser/segment-reconciler.ts +66 -53
  131. package/src/browser/server-action-bridge.ts +265 -127
  132. package/src/browser/types.ts +2 -1
  133. package/src/browser/wire-protocol.ts +41 -0
  134. package/src/cache/cache-key-utils.ts +11 -5
  135. package/src/cache/cache-runtime.ts +110 -50
  136. package/src/cache/cache-scope.ts +110 -64
  137. package/src/cache/cf/cf-cache-constants.ts +2 -2
  138. package/src/cache/cf/cf-cache-store.ts +486 -874
  139. package/src/cache/cf/cf-kv-utils.ts +1 -1
  140. package/src/cache/document-cache.ts +42 -15
  141. package/src/cache/handle-snapshot.ts +19 -7
  142. package/src/cache/segment-codec.ts +55 -0
  143. package/src/cache/shell-snapshot.ts +2 -2
  144. package/src/cache/tag-invalidation.ts +21 -2
  145. package/src/cache/taint.ts +5 -3
  146. package/src/cache/types.ts +7 -3
  147. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  148. package/src/handles/pushed-by.ts +1 -1
  149. package/src/prefetch-markers.ts +4 -7
  150. package/src/prerender/store.ts +76 -0
  151. package/src/prerender/warm-request.ts +8 -4
  152. package/src/prerender.ts +2 -2
  153. package/src/route-name.ts +8 -0
  154. package/src/router/error-handling.ts +4 -4
  155. package/src/router/instrument.ts +4 -3
  156. package/src/router/intercept-resolution.ts +77 -75
  157. package/src/router/loader-resolution.ts +5 -4
  158. package/src/router/match-api.ts +4 -5
  159. package/src/router/match-context.ts +33 -12
  160. package/src/router/match-handlers.ts +8 -20
  161. package/src/router/match-middleware/background-revalidation.ts +41 -53
  162. package/src/router/match-middleware/cache-lookup.ts +680 -720
  163. package/src/router/match-middleware/cache-store.ts +14 -12
  164. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  165. package/src/router/match-middleware/segment-resolution.ts +34 -61
  166. package/src/router/match-result.ts +1 -20
  167. package/src/router/middleware.ts +69 -11
  168. package/src/router/navigation-snapshot.ts +4 -3
  169. package/src/router/prerender-match.ts +100 -44
  170. package/src/router/preview-match.ts +6 -41
  171. package/src/router/request-classification.ts +13 -7
  172. package/src/router/revalidation.ts +33 -7
  173. package/src/router/route-snapshot.ts +2 -5
  174. package/src/router/router-context.ts +14 -117
  175. package/src/router/router-interfaces.ts +8 -14
  176. package/src/router/segment-resolution/loader-cache.ts +14 -9
  177. package/src/router/segment-resolution/loader-mask.ts +5 -5
  178. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  179. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  180. package/src/router/segment-resolution/revalidation.ts +897 -776
  181. package/src/router/segment-resolution.ts +4 -13
  182. package/src/router/segment-wrappers.ts +51 -269
  183. package/src/router/telemetry.ts +5 -0
  184. package/src/router/types.ts +10 -0
  185. package/src/router.ts +12 -13
  186. package/src/rsc/full-payload.ts +6 -5
  187. package/src/rsc/handler-context.ts +8 -14
  188. package/src/rsc/handler.ts +126 -507
  189. package/src/rsc/json-route-result.ts +5 -9
  190. package/src/rsc/loader-fetch.ts +6 -5
  191. package/src/rsc/progressive-enhancement.ts +5 -0
  192. package/src/rsc/request-stages.ts +757 -0
  193. package/src/rsc/response-cache-serve.ts +10 -14
  194. package/src/rsc/response-route-handler.ts +13 -5
  195. package/src/rsc/rewrite-passes.ts +2 -1
  196. package/src/rsc/routine-plan.ts +1 -1
  197. package/src/rsc/rsc-rendering.ts +172 -349
  198. package/src/rsc/server-action.ts +87 -113
  199. package/src/rsc/shell-capture-constants.ts +3 -1
  200. package/src/rsc/shell-capture.ts +212 -169
  201. package/src/rsc/shell-serve.ts +121 -18
  202. package/src/rsc/ssr-setup.ts +5 -4
  203. package/src/segment-system.tsx +3 -3
  204. package/src/server/context.ts +22 -25
  205. package/src/server/handle-store.ts +55 -0
  206. package/src/server/render-pass.ts +291 -0
  207. package/src/server/request-context.ts +26 -115
  208. package/src/server/rewrite-trail.ts +6 -4
  209. package/src/ssr/index.tsx +1 -1
  210. package/src/ssr/ssr-root.tsx +1 -0
  211. package/src/testing/dispatch.ts +223 -710
  212. package/src/testing/e2e/page-helpers.ts +2 -2
  213. package/src/testing/serve-shell-request.ts +37 -32
  214. package/src/types/cache-types.ts +6 -3
  215. package/src/types/error-types.ts +9 -1
  216. package/src/types/handler-context.ts +21 -7
  217. package/src/types/segments.ts +1 -1
  218. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  219. package/src/vite/discovery/build-env.ts +148 -0
  220. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  221. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  222. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  223. package/src/vite/discovery/discover-routers.ts +1 -1
  224. package/src/vite/discovery/discovery-errors.ts +1 -1
  225. package/src/vite/discovery/prerender-collection.ts +24 -39
  226. package/src/vite/discovery/state.ts +15 -0
  227. package/src/vite/plugins/version-plugin.ts +1 -1
  228. package/src/vite/plugins/virtual-entries.ts +1 -1
  229. package/src/vite/router-discovery.ts +29 -2428
  230. package/src/vite/temp-rsc-server.ts +223 -0
  231. package/src/vite/utils/prerender-utils.ts +47 -0
  232. package/src/wire-params.ts +35 -0
  233. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  234. 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
+ }
@@ -111,10 +111,10 @@ export function invokeOnError<TEnv = any>(
111
111
  /**
112
112
  * First non-undefined `pick(orphan)` over the routeless entries in
113
113
  * `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).
114
+ * (resolveOrphanLayout in segment-resolution/revalidation.ts).
115
+ * Since #922 a routeless entry nested in another renders (a layout() after a
116
+ * bare cache() marker, a layout() in a routeless wrapper); a one-level scan
117
+ * missed the boundaries and intercepts it declares (#926).
118
118
  *
119
119
  * `skip` is the entry an upward walk came from. Only a bare cache() marker is
120
120
  * 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,38 @@ 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"). Responses rethrow.
168
+ * The background re-render (`background`) reports the error the same way and
169
+ * then rethrows it, so a failed re-render aborts its cache write instead of
170
+ * 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>,
184
176
  background: boolean | undefined,
185
177
  ): ReactNode {
186
- if (error instanceof Response || background) throw error;
187
- return catchSegmentError(
178
+ if (error instanceof Response) throw error;
179
+ // A background re-render's waitUntil task may run outside the router
180
+ // context, as the segment walk's report allows.
181
+ let telemetry;
182
+ try {
183
+ telemetry = getRouterContext()?.telemetry;
184
+ } catch {}
185
+ const { component } = catchSegmentError(
188
186
  error,
189
187
  parentEntry,
190
- params,
191
- deps,
188
+ pass.params,
189
+ pass.deps,
192
190
  {
193
- ...buildLoaderErrorContext(context),
191
+ ...buildLoaderErrorContext(pass.context),
194
192
  isPartial: true,
195
- telemetry: getRouterContext()?.telemetry,
193
+ telemetry,
196
194
  },
197
- context.pathname,
198
- ).component;
195
+ pass.context.pathname,
196
+ );
197
+ if (background) throw error;
198
+ return component;
199
199
  }
200
200
 
201
201
  /** The segment id of `intercept`'s slot on `parentEntry`. */
@@ -206,6 +206,33 @@ export function interceptSlotId(
206
206
  return `${parentEntry.shortCode}.${intercept.slotName}`;
207
207
  }
208
208
 
209
+ /**
210
+ * The intercept a match selected (findInterceptForRoute), as its cache()
211
+ * record names it (CacheScope): the slot id, then `~` and its position among
212
+ * the intercepts its entry declares for the same slot and target route. Two
213
+ * such intercepts share the slot id, and the position tells them apart; one
214
+ * for another slot or route does not renumber it. `~` is unreserved in a
215
+ * URL, so a store that maps the key onto one unencoded keeps the whole key.
216
+ * Undefined when the match selected none.
217
+ */
218
+ export function interceptRecordKey(
219
+ selected: { intercept: InterceptEntry; entry: EntryData } | null,
220
+ ): string | undefined {
221
+ if (!selected) return undefined;
222
+ const { intercept, entry } = selected;
223
+ let position = 0;
224
+ for (const other of entry.intercept) {
225
+ if (other === intercept) break;
226
+ if (
227
+ other.slotName === intercept.slotName &&
228
+ other.routeName === intercept.routeName
229
+ ) {
230
+ position++;
231
+ }
232
+ }
233
+ return `${interceptSlotId(entry, intercept)}~${position}`;
234
+ }
235
+
209
236
  /**
210
237
  * Every segment id an intercept's slot owns, in order: the slot, then one
211
238
  * loader segment per loader in declaration order, run or not. A request that
@@ -228,14 +255,13 @@ export function interceptSegmentIds(
228
255
  * revalidated on its own, kept in a fill, deferred by a prefetch when flagged.
229
256
  */
230
257
  function resolveInterceptLoaders<TEnv>(
258
+ pass: ResolutionPass<TEnv>,
231
259
  interceptEntry: InterceptEntry,
232
260
  parentEntry: EntryData,
233
- context: HandlerContext<any, TEnv>,
234
261
  belongsToRoute: boolean,
235
- deps: SegmentResolutionDeps<TEnv>,
236
- rc: RevalidationContext,
237
262
  ): Promise<{ segments: ResolvedSegment[] }> {
238
263
  return resolveOwnerLoaders(
264
+ pass,
239
265
  {
240
266
  loaders: interceptEntry.loader,
241
267
  namespace: `intercept:${interceptEntry.routeName}`,
@@ -244,17 +270,7 @@ function resolveInterceptLoaders<TEnv>(
244
270
  bakeLane: false,
245
271
  traceSource: "intercept-loader",
246
272
  },
247
- context,
248
273
  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
274
  );
259
275
  }
260
276
 
@@ -271,45 +287,37 @@ async function settleLoaderSegments(
271
287
  }
272
288
 
273
289
  /**
274
- * Resolve an intercept entry: its slot segment, then its loader segments.
290
+ * Resolve an intercept entry: its slot segment, then its loader segments. The
291
+ * background re-render resolves with a pass that holds nothing
292
+ * (heldNothingPassInput, resolution-pass.ts): every segment renders fresh, and
293
+ * its loaders are skipped (skipLoaders), each registered for the handler's
294
+ * read.
275
295
  */
276
296
  export async function resolveInterceptEntry<TEnv>(
297
+ pass: ResolutionPass<TEnv>,
277
298
  interceptEntry: InterceptEntry,
278
299
  parentEntry: EntryData,
279
- params: Record<string, string>,
280
- context: HandlerContext<any, TEnv>,
281
300
  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
301
  options?: {
293
302
  /**
294
303
  * Set ONLY by the post-response background re-render (stale background
295
304
  * 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.
305
+ * populate the cache: a handler error is reported, then rejects instead
306
+ * of rendering the error boundary, so a failure is never cached. Never
307
+ * set on the foreground path.
299
308
  */
300
309
  background?: boolean;
301
310
  },
302
311
  ): Promise<ResolvedSegment[]> {
303
312
  latchPprHeaderScopeForInterceptTarget(interceptEntry);
304
313
 
314
+ const { context, params } = pass;
305
315
  const slotSegmentId = interceptSlotId(parentEntry, interceptEntry);
306
316
  const { segments: loaderSegments } = await resolveInterceptLoaders(
317
+ pass,
307
318
  interceptEntry,
308
319
  parentEntry,
309
- context,
310
320
  belongsToRoute,
311
- deps,
312
- revalidationContext,
313
321
  );
314
322
  // The slot's tag scope (#957, cache-tag.ts runInSegmentTagScope) and the
315
323
  // owner of its handler's handle pushes (a handler's ctx.use(Handle) reads
@@ -337,10 +345,7 @@ export async function resolveInterceptEntry<TEnv>(
337
345
  // A fill keeps a slot the client holds: neither its handler nor its layout
338
346
  // runs, only the loaders the client lacks (match-result.ts leaves the null
339
347
  // slot out of the response).
340
- if (
341
- fillSends(context, slotSegmentId, revalidationContext.clientSegmentIds) ===
342
- false
343
- ) {
348
+ if (fillSends(context, slotSegmentId, pass.client) === false) {
344
349
  return [slotSegment(null, undefined), ...loaderSegments];
345
350
  }
346
351
 
@@ -355,11 +360,9 @@ export async function resolveInterceptEntry<TEnv>(
355
360
  : handler;
356
361
  } catch (error) {
357
362
  handlerResult = renderInterceptHandlerError(
363
+ pass,
358
364
  error,
359
365
  parentEntry,
360
- params,
361
- context,
362
- deps,
363
366
  options?.background,
364
367
  );
365
368
  }
@@ -392,7 +395,7 @@ export async function resolveInterceptEntry<TEnv>(
392
395
  // recovered into the declared boundary, except on the background
393
396
  // re-render, which must reject so a failure is never cached.
394
397
  const streamedComponent = (promise: Promise<unknown>): ReactNode =>
395
- trackStreamedHandler(deps, promise, {
398
+ trackStreamedHandler(pass.deps, promise, {
396
399
  boundaryEntry: parentEntry,
397
400
  segmentId: slotSegmentId,
398
401
  segmentType: "parallel",
@@ -423,11 +426,9 @@ export async function resolveInterceptEntry<TEnv>(
423
426
  component = await handlerResult;
424
427
  } catch (error) {
425
428
  component = renderInterceptHandlerError(
429
+ pass,
426
430
  error,
427
431
  parentEntry,
428
- params,
429
- context,
430
- deps,
431
432
  options?.background,
432
433
  );
433
434
  }
@@ -445,23 +446,24 @@ export async function resolveInterceptEntry<TEnv>(
445
446
  * keeps its cached component and layout, its loaders are never cached.
446
447
  */
447
448
  export async function resolveInterceptLoadersOnly<TEnv>(
449
+ pass: ResolutionPass<TEnv>,
448
450
  interceptEntry: InterceptEntry,
449
451
  parentEntry: EntryData,
450
- context: HandlerContext<any, TEnv>,
451
452
  belongsToRoute: boolean,
452
- deps: SegmentResolutionDeps<TEnv>,
453
- revalidationContext: RevalidationContext,
454
453
  ): Promise<ResolvedSegment[]> {
455
454
  latchPprHeaderScopeForInterceptTarget(interceptEntry);
455
+ const handleStore = _getRequestContext()?._handleStore;
456
456
  const { segments } = await resolveInterceptLoaders(
457
+ pass,
457
458
  interceptEntry,
458
459
  parentEntry,
459
- context,
460
460
  belongsToRoute,
461
- deps,
462
- revalidationContext,
461
+ );
462
+ // Where resolveInterceptEntry runs the handler.
463
+ handleStore?.releaseRecordedValues(
464
+ interceptSlotId(parentEntry, interceptEntry),
463
465
  );
464
466
  if (!interceptEntry.loading) await settleLoaderSegments(segments);
465
- deliverStartedLoaders(context, segments);
467
+ deliverStartedLoaders(pass.context, segments);
466
468
  return segments;
467
469
  }
@@ -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,
@@ -523,7 +523,6 @@ export async function createMatchContextForPartial<TEnv>(
523
523
  routeKey: matched.routeKey,
524
524
  localRouteName: snapshot.localRouteName,
525
525
  handlerContext,
526
- loaderPromises,
527
526
  routeMap: deps.getRouteMap(),
528
527
  metricsStore,
529
528
  Store,
@@ -63,7 +63,6 @@
63
63
  *
64
64
  * Handler Context:
65
65
  * - handlerContext: Context passed to loaders
66
- * - loaderPromises: Memoized loader promises
67
66
  *
68
67
  * Intercepts:
69
68
  * - interceptResult: Detected intercept (if soft navigation)
@@ -118,16 +117,8 @@ import type {
118
117
  import type { HandlerContext, ResolvedSegment } from "../types.js";
119
118
  import type { RouteMatchResult } from "./pattern-matching.js";
120
119
  import type { InterceptResult } from "./router-context.js";
121
-
122
- /**
123
- * Action context passed to matchPartial
124
- */
125
- export interface ActionContext {
126
- actionId?: string;
127
- actionUrl?: URL;
128
- actionResult?: any;
129
- formData?: FormData;
130
- }
120
+ import type { ResolutionPassInput } from "./segment-resolution/resolution-pass.js";
121
+ import type { ActionContext } from "./types.js";
131
122
 
132
123
  /**
133
124
  * Match context containing all state for the match pipeline
@@ -160,7 +151,6 @@ export interface MatchContext<TEnv = any> {
160
151
 
161
152
  // Handler context (for loaders)
162
153
  handlerContext: HandlerContext<any, TEnv>;
163
- loaderPromises: Map<string, Promise<any>>;
164
154
 
165
155
  // Route map for server-side ctx.reverse() resolution
166
156
  routeMap: Record<string, string>;
@@ -201,6 +191,37 @@ export interface MatchContext<TEnv = any> {
201
191
  isFullMatch: boolean;
202
192
  }
203
193
 
194
+ /**
195
+ * An action, or a stale partial (`_rsc_stale`: Back/Forward onto an entry
196
+ * marked stale, `refresh()`, the cross-tab refresh, the action follow-up, a
197
+ * zero-segment refetch), renders live: no route-cache or prerender-store read
198
+ * a live handler can replace, and no store. An action's render is never
199
+ * stored, so a record can be older than the copy the client holds after it.
200
+ */
201
+ export function rendersLive(
202
+ ctx: Pick<MatchContext, "isAction" | "stale">,
203
+ ): boolean {
204
+ return ctx.isAction || ctx.stale;
205
+ }
206
+
207
+ /** This match as the input of a revalidating resolution pass (resolution-pass.ts). */
208
+ export function resolutionPassInput<TEnv>(
209
+ ctx: MatchContext<TEnv>,
210
+ ): ResolutionPassInput<TEnv> {
211
+ return {
212
+ context: ctx.handlerContext,
213
+ client: ctx.clientSegmentSet,
214
+ params: ctx.matched.params,
215
+ prevParams: ctx.prevParams,
216
+ request: ctx.request,
217
+ prevUrl: ctx.prevUrl,
218
+ nextUrl: ctx.url,
219
+ routeKey: ctx.routeKey,
220
+ actionContext: ctx.actionContext,
221
+ stale: ctx.stale,
222
+ };
223
+ }
224
+
204
225
  /**
205
226
  * Mutable state that flows through the pipeline
206
227
  */