@rangojs/router 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/event-controller.d.ts +2 -1
  4. package/dist/types/browser/history-state.d.ts +3 -9
  5. package/dist/types/browser/navigation-store.d.ts +8 -6
  6. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  7. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  8. package/dist/types/browser/types.d.ts +2 -1
  9. package/dist/types/browser/wire-protocol.d.ts +25 -0
  10. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  11. package/dist/types/cache/cache-scope.d.ts +34 -22
  12. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  13. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  14. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  15. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  16. package/dist/types/cache/segment-codec.d.ts +14 -0
  17. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  18. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  19. package/dist/types/cache/taint.d.ts +5 -3
  20. package/dist/types/cache/types.d.ts +7 -3
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  22. package/dist/types/prefetch-markers.d.ts +5 -14
  23. package/dist/types/prerender/store.d.ts +31 -0
  24. package/dist/types/prerender/warm-request.d.ts +3 -3
  25. package/dist/types/prerender.d.ts +2 -2
  26. package/dist/types/route-name.d.ts +5 -0
  27. package/dist/types/router/error-handling.d.ts +4 -4
  28. package/dist/types/router/instrument.d.ts +4 -3
  29. package/dist/types/router/intercept-resolution.d.ts +25 -9
  30. package/dist/types/router/match-context.d.ts +4 -11
  31. package/dist/types/router/match-handlers.d.ts +2 -6
  32. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  33. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  34. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  35. package/dist/types/router/prerender-match.d.ts +21 -6
  36. package/dist/types/router/preview-match.d.ts +2 -7
  37. package/dist/types/router/request-classification.d.ts +5 -0
  38. package/dist/types/router/router-context.d.ts +7 -46
  39. package/dist/types/router/router-interfaces.d.ts +8 -14
  40. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  41. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  42. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  43. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  44. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  45. package/dist/types/router/segment-resolution.d.ts +1 -2
  46. package/dist/types/router/segment-wrappers.d.ts +11 -48
  47. package/dist/types/router/telemetry.d.ts +2 -0
  48. package/dist/types/router/types.d.ts +10 -0
  49. package/dist/types/rsc/full-payload.d.ts +2 -2
  50. package/dist/types/rsc/handler-context.d.ts +6 -6
  51. package/dist/types/rsc/json-route-result.d.ts +5 -9
  52. package/dist/types/rsc/request-stages.d.ts +203 -0
  53. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  54. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  55. package/dist/types/rsc/routine-plan.d.ts +1 -1
  56. package/dist/types/rsc/server-action.d.ts +2 -2
  57. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  58. package/dist/types/rsc/shell-capture.d.ts +132 -15
  59. package/dist/types/rsc/shell-serve.d.ts +37 -12
  60. package/dist/types/server/context.d.ts +9 -8
  61. package/dist/types/server/handle-store.d.ts +16 -0
  62. package/dist/types/server/render-pass.d.ts +72 -0
  63. package/dist/types/server/request-context.d.ts +24 -104
  64. package/dist/types/server/rewrite-trail.d.ts +2 -2
  65. package/dist/types/testing/dispatch.d.ts +35 -18
  66. package/dist/types/types/cache-types.d.ts +6 -3
  67. package/dist/types/types/error-types.d.ts +9 -1
  68. package/dist/types/types/handler-context.d.ts +9 -1
  69. package/dist/types/types/segments.d.ts +1 -1
  70. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  71. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  72. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  73. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  74. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  75. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  76. package/dist/types/vite/discovery/state.d.ts +12 -0
  77. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  78. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  79. package/dist/types/vite/router-discovery.d.ts +8 -47
  80. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  81. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  82. package/dist/types/wire-params.d.ts +34 -0
  83. package/dist/vite/index.js +3809 -3710
  84. package/package.json +1 -1
  85. package/skills/cache-guide/SKILL.md +8 -5
  86. package/skills/caching/SKILL.md +37 -18
  87. package/skills/document-cache/SKILL.md +13 -1
  88. package/skills/handler-use/SKILL.md +1 -1
  89. package/skills/intercept/SKILL.md +17 -7
  90. package/skills/middleware/SKILL.md +18 -0
  91. package/skills/ppr/SKILL.md +18 -3
  92. package/skills/prerender/SKILL.md +39 -15
  93. package/skills/rango/SKILL.md +5 -1
  94. package/skills/server-actions/SKILL.md +8 -1
  95. package/skills/testing/response-routes.md +6 -4
  96. package/skills/use-cache/SKILL.md +46 -8
  97. package/src/__internal.ts +7 -1
  98. package/src/browser/connection-warmup.ts +3 -1
  99. package/src/browser/event-controller.ts +5 -3
  100. package/src/browser/history-state.ts +6 -12
  101. package/src/browser/navigation-bridge.ts +14 -13
  102. package/src/browser/navigation-client.ts +18 -25
  103. package/src/browser/navigation-store.ts +25 -27
  104. package/src/browser/navigation-transaction.ts +4 -1
  105. package/src/browser/prefetch/cache.ts +2 -1
  106. package/src/browser/prefetch/fetch.ts +8 -16
  107. package/src/browser/prefetch/queue.ts +4 -4
  108. package/src/browser/react/NavigationProvider.tsx +2 -1
  109. package/src/browser/react/filter-segment-order.ts +2 -2
  110. package/src/browser/rsc-router.tsx +1 -0
  111. package/src/browser/segment-reconciler.ts +66 -53
  112. package/src/browser/server-action-bridge.ts +50 -57
  113. package/src/browser/types.ts +2 -1
  114. package/src/browser/wire-protocol.ts +41 -0
  115. package/src/cache/cache-key-utils.ts +11 -5
  116. package/src/cache/cache-runtime.ts +110 -50
  117. package/src/cache/cache-scope.ts +110 -64
  118. package/src/cache/cf/cf-cache-constants.ts +2 -2
  119. package/src/cache/cf/cf-cache-store.ts +486 -874
  120. package/src/cache/cf/cf-kv-utils.ts +1 -1
  121. package/src/cache/document-cache.ts +34 -13
  122. package/src/cache/handle-snapshot.ts +19 -7
  123. package/src/cache/segment-codec.ts +55 -0
  124. package/src/cache/shell-snapshot.ts +2 -2
  125. package/src/cache/tag-invalidation.ts +21 -2
  126. package/src/cache/taint.ts +5 -3
  127. package/src/cache/types.ts +7 -3
  128. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  129. package/src/handles/pushed-by.ts +1 -1
  130. package/src/prefetch-markers.ts +5 -16
  131. package/src/prerender/store.ts +76 -0
  132. package/src/prerender/warm-request.ts +8 -4
  133. package/src/prerender.ts +2 -2
  134. package/src/route-name.ts +8 -0
  135. package/src/router/error-handling.ts +4 -4
  136. package/src/router/instrument.ts +4 -3
  137. package/src/router/intercept-resolution.ts +77 -75
  138. package/src/router/loader-resolution.ts +5 -4
  139. package/src/router/match-api.ts +4 -5
  140. package/src/router/match-context.ts +20 -12
  141. package/src/router/match-handlers.ts +8 -20
  142. package/src/router/match-middleware/background-revalidation.ts +41 -53
  143. package/src/router/match-middleware/cache-lookup.ts +675 -726
  144. package/src/router/match-middleware/cache-store.ts +4 -8
  145. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  146. package/src/router/match-middleware/segment-resolution.ts +34 -61
  147. package/src/router/middleware.ts +69 -11
  148. package/src/router/navigation-snapshot.ts +4 -4
  149. package/src/router/prerender-match.ts +100 -44
  150. package/src/router/preview-match.ts +6 -41
  151. package/src/router/request-classification.ts +13 -7
  152. package/src/router/route-snapshot.ts +2 -5
  153. package/src/router/router-context.ts +10 -95
  154. package/src/router/router-interfaces.ts +8 -14
  155. package/src/router/segment-resolution/loader-cache.ts +14 -9
  156. package/src/router/segment-resolution/loader-mask.ts +5 -5
  157. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  158. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  159. package/src/router/segment-resolution/revalidation.ts +897 -776
  160. package/src/router/segment-resolution.ts +4 -13
  161. package/src/router/segment-wrappers.ts +51 -269
  162. package/src/router/telemetry.ts +5 -0
  163. package/src/router/types.ts +10 -0
  164. package/src/router.ts +12 -13
  165. package/src/rsc/full-payload.ts +6 -5
  166. package/src/rsc/handler-context.ts +8 -14
  167. package/src/rsc/handler.ts +126 -507
  168. package/src/rsc/json-route-result.ts +5 -9
  169. package/src/rsc/loader-fetch.ts +6 -5
  170. package/src/rsc/progressive-enhancement.ts +5 -0
  171. package/src/rsc/request-stages.ts +757 -0
  172. package/src/rsc/response-cache-serve.ts +10 -14
  173. package/src/rsc/response-route-handler.ts +13 -5
  174. package/src/rsc/rewrite-passes.ts +2 -1
  175. package/src/rsc/routine-plan.ts +1 -1
  176. package/src/rsc/rsc-rendering.ts +164 -349
  177. package/src/rsc/server-action.ts +87 -113
  178. package/src/rsc/shell-capture-constants.ts +1 -1
  179. package/src/rsc/shell-capture.ts +212 -169
  180. package/src/rsc/shell-serve.ts +121 -18
  181. package/src/rsc/ssr-setup.ts +5 -4
  182. package/src/segment-system.tsx +3 -3
  183. package/src/server/context.ts +22 -25
  184. package/src/server/handle-store.ts +55 -0
  185. package/src/server/render-pass.ts +291 -0
  186. package/src/server/request-context.ts +26 -115
  187. package/src/server/rewrite-trail.ts +6 -4
  188. package/src/ssr/index.tsx +1 -1
  189. package/src/testing/dispatch.ts +223 -710
  190. package/src/testing/e2e/page-helpers.ts +2 -2
  191. package/src/testing/serve-shell-request.ts +23 -36
  192. package/src/types/cache-types.ts +6 -3
  193. package/src/types/error-types.ts +9 -1
  194. package/src/types/handler-context.ts +12 -1
  195. package/src/types/segments.ts +1 -1
  196. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  197. package/src/vite/discovery/build-env.ts +148 -0
  198. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  199. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  200. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  201. package/src/vite/discovery/discover-routers.ts +1 -1
  202. package/src/vite/discovery/discovery-errors.ts +1 -1
  203. package/src/vite/discovery/prerender-collection.ts +24 -39
  204. package/src/vite/discovery/state.ts +15 -0
  205. package/src/vite/plugins/version-plugin.ts +1 -1
  206. package/src/vite/plugins/virtual-entries.ts +1 -1
  207. package/src/vite/router-discovery.ts +29 -2428
  208. package/src/vite/temp-rsc-server.ts +223 -0
  209. package/src/vite/utils/prerender-utils.ts +47 -0
  210. package/src/wire-params.ts +35 -0
  211. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  212. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -114,6 +114,7 @@ import { createShellImplicitDocScope } from "../../cache/cache-scope.js";
114
114
  import { recordSegmentTags } from "../../cache/cache-tag.js";
115
115
  import { executionStart } from "../../cache/tag-invalidation.js";
116
116
  import { getRouterContext } from "../router-context.js";
117
+ import { interceptRecordKey } from "../intercept-resolution.js";
117
118
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
118
119
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
119
120
  import type { GeneratorMiddleware } from "./cache-lookup.js";
@@ -284,7 +285,7 @@ export function withCacheStore<TEnv>(
284
285
  ctx.pathname,
285
286
  ctx.matched.params,
286
287
  allSegmentsToCache,
287
- ctx.isIntercept,
288
+ interceptRecordKey(ctx.interceptResult),
288
289
  // A capture's doc record lives only in the shell entry, which
289
290
  // putShell gates by the capture start; skipping it here would
290
291
  // read as a render that produced no record.
@@ -354,7 +355,7 @@ function recordShellCaptureDocRecord<TEnv>(
354
355
  segments: ResolvedSegment[],
355
356
  ): void {
356
357
  const requestCtx = getRequestContext();
357
- if (!requestCtx?._shellCaptureRun) return;
358
+ if (!requestCtx?._shellCaptureState?.run) return;
358
359
  const marker = requestCtx._shellImplicitCache;
359
360
  if (!marker?.store) return;
360
361
  const scope = ctx.cacheScope;
@@ -381,12 +382,7 @@ function recordShellCaptureDocRecord<TEnv>(
381
382
  // pushes settled, so the record's handle encode never races them.
382
383
  requestCtx.onResponse((response) => {
383
384
  requestCtx.waitUntil(() =>
384
- docScope.cacheRoute(
385
- ctx.pathname,
386
- ctx.matched.params,
387
- segments,
388
- ctx.isIntercept,
389
- ),
385
+ docScope.cacheRoute(ctx.pathname, ctx.matched.params, segments),
390
386
  );
391
387
  return response;
392
388
  });
@@ -103,6 +103,7 @@
103
103
  */
104
104
  import type { ResolvedSegment } from "../../types.js";
105
105
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
106
+ import { resolutionPassInput } from "../match-context.js";
106
107
  import { getRouterContext } from "../router-context.js";
107
108
  import type { GeneratorMiddleware } from "./cache-lookup.js";
108
109
  import { debugLog } from "../logging.js";
@@ -175,21 +176,10 @@ export function withInterceptResolution<TEnv>(
175
176
  const Store = ctx.Store;
176
177
  const interceptSegments = await Store.run(() =>
177
178
  resolveInterceptEntry(
179
+ resolutionPassInput(ctx),
178
180
  ctx.interceptResult!.intercept,
179
181
  ctx.interceptResult!.entry,
180
- ctx.matched.params,
181
- ctx.handlerContext,
182
182
  true, // belongsToRoute
183
- {
184
- clientSegmentIds: ctx.clientSegmentSet,
185
- prevParams: ctx.prevParams,
186
- request: ctx.request,
187
- prevUrl: ctx.prevUrl,
188
- nextUrl: ctx.url,
189
- routeKey: ctx.routeKey,
190
- actionContext: ctx.actionContext,
191
- stale: ctx.stale,
192
- },
193
183
  ),
194
184
  );
195
185
 
@@ -242,20 +232,10 @@ async function handleCacheHitIntercept<TEnv>(
242
232
  const loaderSegments = resolveInterceptLoadersOnly
243
233
  ? await ctx.Store.run(() =>
244
234
  resolveInterceptLoadersOnly(
235
+ resolutionPassInput(ctx),
245
236
  ctx.interceptResult!.intercept,
246
237
  ctx.interceptResult!.entry,
247
- ctx.handlerContext,
248
238
  true, // belongsToRoute
249
- {
250
- clientSegmentIds: ctx.clientSegmentSet,
251
- prevParams: ctx.prevParams,
252
- request: ctx.request,
253
- prevUrl: ctx.prevUrl,
254
- nextUrl: ctx.url,
255
- routeKey: ctx.routeKey,
256
- actionContext: ctx.actionContext,
257
- stale: ctx.stale,
258
- },
259
239
  ),
260
240
  )
261
241
  : [];
@@ -29,15 +29,16 @@
29
29
  * yes no
30
30
  * | |
31
31
  * v v
32
- * resolveAll resolveAllWithRevalidation
33
- * Segments Segments
34
- * | |
32
+ * FRESH pass partial pass
35
33
  * | | (compares with prev state)
36
34
  * | | (handles null components)
37
35
  * | |
38
36
  * +-----------+
39
37
  * |
40
38
  * v
39
+ * resolveAllSegments()
40
+ * |
41
+ * v
41
42
  * +---------------------------+
42
43
  * | Update state: |
43
44
  * | - state.segments |
@@ -54,13 +55,14 @@
54
55
  * RESOLUTION MODES
55
56
  * ================
56
57
  *
58
+ * Both use resolveAllSegments().
59
+ *
57
60
  * Full Match (document request):
58
- * - Uses resolveAllSegments()
59
- * - No revalidation logic (nothing to compare against)
61
+ * - A FRESH pass (freshPassInput): no revalidation logic (nothing to
62
+ * compare against)
60
63
  * - Simple resolution of all route entries
61
64
  *
62
65
  * Partial Match (navigation):
63
- * - Uses resolveAllSegmentsWithRevalidation()
64
66
  * - Compares current vs previous params/URL
65
67
  * - Sets component = null for segments client already has
66
68
  * - Respects custom revalidation rules
@@ -90,6 +92,8 @@ import type { ResolvedSegment } from "../../types.js";
90
92
  import type { EntryData } from "../../server/context.js";
91
93
  import { _getRequestContext } from "../../server/request-context.js";
92
94
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
95
+ import { resolutionPassInput } from "../match-context.js";
96
+ import { freshPassInput } from "../segment-resolution/resolution-pass.js";
93
97
  import { getRouterContext } from "../router-context.js";
94
98
  import type { GeneratorMiddleware } from "./cache-lookup.js";
95
99
 
@@ -155,64 +159,33 @@ export function withSegmentResolution<TEnv>(
155
159
  reqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
156
160
  }
157
161
 
158
- const { resolveAllSegmentsWithRevalidation, resolveAllSegments } =
159
- getRouterContext<TEnv>();
160
-
161
- const Store = ctx.Store;
162
-
163
- if (ctx.isFullMatch) {
164
- const segments = await Store.run(() =>
165
- resolveAllSegments(
166
- ctx.entries,
167
- ctx.routeKey,
168
- ctx.matched.params,
169
- ctx.handlerContext,
170
- ctx.loaderPromises,
171
- ),
172
- );
173
-
174
- state.segments = segments;
175
- state.matchedIds = segments.map((s: { id: string }) => s.id);
162
+ const { resolveAllSegments } = getRouterContext<TEnv>();
176
163
 
177
- if (reqCtx) {
178
- reqCtx._resolveRenderBarrier(segments);
179
- }
180
- for (const segment of segments) {
181
- yield segment;
182
- }
183
- } else {
184
- // Partial match (navigation) - resolution with revalidation logic
185
- const result = await Store.run(() =>
186
- resolveAllSegmentsWithRevalidation(
187
- ctx.entries,
188
- ctx.routeKey,
189
- ctx.matched.params,
190
- ctx.handlerContext,
191
- ctx.clientSegmentSet,
192
- ctx.prevParams,
193
- ctx.request,
194
- ctx.prevUrl,
195
- ctx.url,
196
- ctx.actionContext,
197
- ctx.interceptResult,
198
- ctx.localRouteName,
199
- ctx.pathname,
200
- ctx.stale,
201
- ),
202
- );
164
+ // A full match (interceptResult null) is a FRESH pass.
165
+ const result = await ctx.Store.run(() =>
166
+ resolveAllSegments(
167
+ ctx.isFullMatch
168
+ ? freshPassInput(ctx.handlerContext, ctx.routeKey, ctx.matched.params)
169
+ : resolutionPassInput(ctx),
170
+ ctx.entries,
171
+ {
172
+ interceptResult: ctx.interceptResult,
173
+ localRouteName: ctx.localRouteName,
174
+ pathname: ctx.pathname,
175
+ },
176
+ ),
177
+ );
203
178
 
204
- // Update state with resolved segments
205
- state.segments = result.segments;
206
- state.matchedIds = result.matchedIds;
207
-
208
- if (reqCtx) {
209
- reqCtx._resolveRenderBarrier(result.segments);
210
- }
179
+ state.segments = result.segments;
180
+ state.matchedIds = ctx.isFullMatch
181
+ ? result.segments.map((s: { id: string }) => s.id)
182
+ : result.matchedIds;
211
183
 
212
- // Yield all resolved segments
213
- for (const segment of result.segments) {
214
- yield segment;
215
- }
184
+ if (reqCtx) {
185
+ reqCtx._resolveRenderBarrier(result.segments);
186
+ }
187
+ for (const segment of result.segments) {
188
+ yield segment;
216
189
  }
217
190
 
218
191
  if (ms) {
@@ -542,6 +542,51 @@ function mergeResponse(
542
542
  return merged;
543
543
  }
544
544
 
545
+ /**
546
+ * The response holder past one middleware. Attached, it reads and writes its
547
+ * middleware's holder, so a chain shares one response. A middleware that
548
+ * settles without that run's response while it runs (it returns or throws
549
+ * its own Response, throws, or rewrites: a timeout fallback, a stale
550
+ * document cache entry's background re-run, cache/document-cache.ts), or
551
+ * that calls next() after it returned, detaches that run: the rest of the
552
+ * chain builds its own response, and the middleware ahead keep what the
553
+ * middleware settled with. With one holder, a
554
+ * forgiving middleware ahead of the cache returned the re-run's response,
555
+ * whose body the cache had drained into the store.
556
+ */
557
+ class DownstreamHolder implements ResponseHolder {
558
+ private own: Response | null = null;
559
+ private attached = true;
560
+
561
+ constructor(private readonly parent: ResponseHolder) {}
562
+
563
+ get response(): Response | null {
564
+ return this.attached ? this.parent.response : this.own;
565
+ }
566
+
567
+ set response(response: Response | null) {
568
+ if (this.attached) this.parent.response = response;
569
+ else this.own = response;
570
+ }
571
+
572
+ /** From `from`: what the run has seen so far, or a late run's stub. */
573
+ detach(from: Response | null = this.parent.response): void {
574
+ this.own = from;
575
+ this.attached = false;
576
+ }
577
+ }
578
+
579
+ /**
580
+ * A middleware settled without the response of the run its next() started
581
+ * (its own Response, a throw, a rewrite error) while that run still goes.
582
+ */
583
+ function detachIfRunning(
584
+ holder: DownstreamHolder | undefined,
585
+ nextResolvedAt: number | undefined,
586
+ ): void {
587
+ if (holder && nextResolvedAt === undefined) holder.detach();
588
+ }
589
+
545
590
  /**
546
591
  * Execute middleware chain
547
592
  *
@@ -575,23 +620,23 @@ export async function executeMiddleware<TEnv>(
575
620
  const stubResponse = new Response(null, { status: 200 });
576
621
  const responseHolder: ResponseHolder = { response: stubResponse };
577
622
 
578
- const next = async (): Promise<Response> => {
623
+ const next = async (holder: ResponseHolder): Promise<Response> => {
579
624
  if (index >= middlewares.length) {
580
625
  // End of chain - call actual RSC handler
581
626
  const response = await finalHandler();
582
627
 
583
628
  if (isWebSocketUpgradeResponse(response)) {
584
- responseHolder.response = response;
629
+ holder.response = response;
585
630
  return response;
586
631
  }
587
632
 
588
633
  // Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
589
634
  // and reqCtx stub headers are merged in.
590
- responseHolder.response = mergeResponse(response, stubResponse.headers, {
635
+ holder.response = mergeResponse(response, stubResponse.headers, {
591
636
  stubOverridesNonCookie: true,
592
637
  });
593
638
 
594
- return responseHolder.response;
639
+ return holder.response;
595
640
  }
596
641
 
597
642
  const middlewareOrdinal = index;
@@ -601,7 +646,7 @@ export async function executeMiddleware<TEnv>(
601
646
  env,
602
647
  params,
603
648
  variables,
604
- responseHolder,
649
+ holder,
605
650
  reverse,
606
651
  );
607
652
  const metricStart = performance.now();
@@ -631,6 +676,8 @@ export async function executeMiddleware<TEnv>(
631
676
  // throws after its next(), the render's own error), so a rewrite thrown
632
677
  // after it would re-dispatch onto the used handle store.
633
678
  let nextRejectedWithRewrite = false;
679
+ let downstreamHolder: DownstreamHolder | undefined;
680
+ let settled = false;
634
681
  const wrappedNext = (): Promise<Response> => {
635
682
  if (nextPromise) {
636
683
  throw new Error(
@@ -638,7 +685,9 @@ export async function executeMiddleware<TEnv>(
638
685
  );
639
686
  }
640
687
  finishMiddleware();
641
- const downstream = next();
688
+ downstreamHolder = new DownstreamHolder(holder);
689
+ if (settled) downstreamHolder.detach(stubResponse);
690
+ const downstream = next(downstreamHolder);
642
691
  nextPromise = downstream.then(
643
692
  (res) => {
644
693
  nextResolvedAt = performance.now();
@@ -687,10 +736,14 @@ export async function executeMiddleware<TEnv>(
687
736
  throw error;
688
737
  }
689
738
  });
739
+ } catch (error) {
740
+ detachIfRunning(downstreamHolder, nextResolvedAt);
741
+ throw error;
690
742
  } finally {
691
743
  // Settle the middleware own-time metric once on both the success and
692
744
  // error paths (idempotent guard in finishMiddleware).
693
745
  finishMiddleware();
746
+ settled = true;
694
747
  }
695
748
  // A rewrite after this middleware's next() started the downstream chain
696
749
  // (resolved, still running, or failed) would leave that render running
@@ -698,10 +751,14 @@ export async function executeMiddleware<TEnv>(
698
751
  // already pushed, and drop the response it built. Only a rewrite after
699
752
  // next() rejected with a Rewrite (nothing rendered) unwinds.
700
753
  if (rewrite && nextPromise && !nextRejectedWithRewrite) {
754
+ detachIfRunning(downstreamHolder, nextResolvedAt);
701
755
  throw rewriteAfterNextError(entry.handler, rewrite);
702
756
  }
703
757
  if (rewrite) throw rewrite;
704
- if (isRewrite(result)) throw returnedRewriteError(entry.handler);
758
+ if (isRewrite(result)) {
759
+ detachIfRunning(downstreamHolder, nextResolvedAt);
760
+ throw returnedRewriteError(entry.handler);
761
+ }
705
762
 
706
763
  // Record post-next() processing time when middleware did work after
707
764
  // the downstream chain resolved (e.g. adding headers, logging).
@@ -723,8 +780,9 @@ export async function executeMiddleware<TEnv>(
723
780
  // RequestContext stub headers (from cookies().set()) into the
724
781
  // returned Response so they are not lost.
725
782
  if (result instanceof Response) {
783
+ detachIfRunning(downstreamHolder, nextResolvedAt);
726
784
  if (isWebSocketUpgradeResponse(result)) {
727
- responseHolder.response = result;
785
+ holder.response = result;
728
786
  return result;
729
787
  }
730
788
  // Explicit short-circuit: the returned Response's own headers win
@@ -732,7 +790,7 @@ export async function executeMiddleware<TEnv>(
732
790
  const merged = mergeResponse(result, stubResponse.headers, {
733
791
  stubOverridesNonCookie: false,
734
792
  });
735
- responseHolder.response = merged;
793
+ holder.response = merged;
736
794
  return merged;
737
795
  }
738
796
 
@@ -749,7 +807,7 @@ export async function executeMiddleware<TEnv>(
749
807
  // If middleware called next(), await it and return the response
750
808
  if (nextPromise) {
751
809
  await nextPromise;
752
- return responseHolder.response!;
810
+ return holder.response!;
753
811
  }
754
812
 
755
813
  // Middleware didn't call next() and didn't return a Response - that's an error
@@ -763,7 +821,7 @@ export async function executeMiddleware<TEnv>(
763
821
  );
764
822
  };
765
823
 
766
- await next();
824
+ await next(responseHolder);
767
825
 
768
826
  // Use the final response from responseHolder (may have been modified by middleware)
769
827
  const finalResponse = responseHolder.response;
@@ -1,7 +1,7 @@
1
1
  import { isLoaderSegmentId } from "../browser/react/filter-segment-order.js";
2
2
  import { requestHeaders } from "../server/request-headers.js";
3
- import { STALE_PARAM } from "../prefetch-markers.js";
4
3
  import type { RouteMatchResult } from "./pattern-matching.js";
4
+ import { WIRE } from "../wire-params.js";
5
5
 
6
6
  export interface NavigationSnapshot {
7
7
  prevUrl: URL;
@@ -106,7 +106,7 @@ export function getInterceptSourceHeader(request: Request): string | null {
106
106
  * left, never what renders, so a forged value runs or skips no middleware.
107
107
  */
108
108
  function getShownRouteUrl(url: URL): URL | null {
109
- const shown = url.searchParams.get("_rsc_rt");
109
+ const shown = url.searchParams.get(WIRE.rewriteTarget);
110
110
  if (!shown) return null;
111
111
  try {
112
112
  const parsed = new URL(shown, url.origin);
@@ -134,8 +134,8 @@ export async function resolveNavigation(
134
134
  deps: ResolveNavigationDeps,
135
135
  ): Promise<NavigationSnapshot | null> {
136
136
  const clientSegmentIds =
137
- url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
138
- const stale = url.searchParams.get(STALE_PARAM) === "true";
137
+ url.searchParams.get(WIRE.segments)?.split(",").filter(Boolean) || [];
138
+ const stale = url.searchParams.get(WIRE.stale) === "true";
139
139
  const previousUrl = getNavigationContextHeader(request, url.origin);
140
140
  const interceptSourceUrl = getInterceptSourceHeader(request);
141
141
  const isHmr = !!requestHeaders(request).get("X-RSC-HMR");