@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
@@ -0,0 +1,757 @@
1
+ /**
2
+ * Request stages
3
+ *
4
+ * The stages a request runs through before and after rendering: classify,
5
+ * guard, the global middleware passes, the response route, and the
6
+ * response's finalization. The RSC handler (rsc/handler.ts createRSCHandler)
7
+ * runs them all around its Flight executor. The dispatch testing primitive
8
+ * (testing/dispatch.ts) runs classify, the terminal plans, the passes, the
9
+ * response route and the finalization, and throws where a plan would render
10
+ * a component. Sharing them keeps dispatch's responses identical to
11
+ * production's; a hand-kept mirror drifted once (#1128).
12
+ *
13
+ * No import of @vitejs/plugin-rsc on this graph: dispatch loads it in a
14
+ * plain Vitest project, where plugin-rsc's entries cannot load (they import
15
+ * Vite virtuals). testing/__tests__/testing-module-graph.test.ts pins it.
16
+ */
17
+
18
+ import { requestHeaders } from "../server/request-headers.js";
19
+ import { isRouteNotFoundError } from "../errors.js";
20
+ import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
21
+ import type { MiddlewareEntry } from "../router/middleware-types.js";
22
+ import {
23
+ _getRequestContext,
24
+ getRequestContext,
25
+ setRequestContextParams,
26
+ type RequestContext,
27
+ } from "../server/request-context.js";
28
+ import {
29
+ REWRITE_HEADER,
30
+ runRewritePasses,
31
+ type RewritePassesOptions,
32
+ type RewritePassesResult,
33
+ } from "./rewrite-passes.js";
34
+ import { rewriteTrail } from "../server/rewrite-trail.js";
35
+ import {
36
+ createReloadResponse,
37
+ finalizeResponse,
38
+ interceptRedirectForPartial,
39
+ } from "./helpers.js";
40
+ import { guardOutgoingRedirect } from "./redirect-guard.js";
41
+ import {
42
+ isWebSocketUpgradeResponse,
43
+ appendVaryAccept,
44
+ trySetHeader,
45
+ } from "../response-utils.js";
46
+ import {
47
+ handleResponseRoute,
48
+ type ResponseRouteMatch,
49
+ } from "./response-route-handler.js";
50
+ import type { ErrorPhase } from "../types.js";
51
+ import type { OnErrorCallback } from "../types/error-types.js";
52
+ import {
53
+ invokeOnError,
54
+ type InvokeOnErrorContext,
55
+ } from "../router/error-handling.js";
56
+ import { createReverseFunction } from "../router/handler-context.js";
57
+ import type { OriginCheckPhase } from "./origin-guard.js";
58
+ import { applyStreamIdleTimeout } from "./stream-idle.js";
59
+ import {
60
+ withTimeout,
61
+ isTimeoutEnabled,
62
+ RouterTimeoutError,
63
+ createDefaultTimeoutResponse,
64
+ type OnTimeoutCallback,
65
+ type RenderTimeoutContext,
66
+ type ResolvedTimeouts,
67
+ type TimeoutPhase,
68
+ } from "../router/timeout.js";
69
+ import {
70
+ safeEmit,
71
+ resolveSink,
72
+ getRequestId,
73
+ setRequestId,
74
+ type TelemetrySink,
75
+ } from "../router/telemetry.js";
76
+ import {
77
+ classifyRequest,
78
+ type ClassifyRequestDeps,
79
+ type ExecutableRequestPlan,
80
+ type RequestPlan,
81
+ type ResponseRoutePlan,
82
+ } from "../router/request-classification.js";
83
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
84
+ import type { CacheErrorCategory } from "../cache/cache-error.js";
85
+ import { WIRE } from "../wire-params.js";
86
+
87
+ /**
88
+ * The router members the stages read. The handler's `RangoInternal` carries
89
+ * them; dispatch reads them off the router a test passes.
90
+ */
91
+ export interface StageRouter<TEnv = unknown> {
92
+ readonly basename: string | undefined;
93
+ readonly middleware: MiddlewareEntry<TEnv>[];
94
+ readonly onError?: OnErrorCallback<TEnv>;
95
+ readonly telemetry?: TelemetrySink;
96
+ readonly timeouts?: ResolvedTimeouts;
97
+ readonly onTimeout?: OnTimeoutCallback<TEnv>;
98
+ }
99
+
100
+ /** Report an error through the router's onError. */
101
+ export type ReportError<TEnv = unknown> = (
102
+ error: unknown,
103
+ phase: ErrorPhase,
104
+ context: InvokeOnErrorContext<TEnv>,
105
+ ) => void;
106
+
107
+ /**
108
+ * What the stages call back into. The handler's context bundle
109
+ * (rsc/handler-context.ts HandlerContext) extends it.
110
+ */
111
+ export interface RequestStageContext<TEnv = unknown> {
112
+ router: StageRouter<TEnv>;
113
+ callOnError: ReportError<TEnv>;
114
+ getRequiredRouteMap: () => Record<string, string>;
115
+ /**
116
+ * The redirect a partial or action request receives: a Flight payload in
117
+ * the handler (fetch() would follow a raw 3xx to HTML). dispatch has no
118
+ * Flight runtime and answers 204 + X-RSC-Redirect.
119
+ */
120
+ createRedirectFlightResponse: (
121
+ redirectUrl: string,
122
+ locationState?: Record<string, unknown>,
123
+ external?: boolean,
124
+ ) => Response;
125
+ /**
126
+ * Reports a response-route handler's own throw; absent, it goes to
127
+ * callOnError with phase "handler". dispatch passes a no-op: it does not
128
+ * report that throw (its documented boundary, testing/dispatch.ts).
129
+ */
130
+ reportResponseRouteError?: (
131
+ error: unknown,
132
+ context: InvokeOnErrorContext<TEnv>,
133
+ ) => void;
134
+ }
135
+
136
+ /** What classify and the terminal plans read beyond the stage context. */
137
+ export interface ClassifyContext<TEnv = unknown> {
138
+ router: {
139
+ readonly id: string;
140
+ readonly findMatch: ClassifyRequestDeps<TEnv>["findMatch"];
141
+ };
142
+ /** The router's DOCUMENT version: the `_rsc_v` reload check. */
143
+ version: string;
144
+ }
145
+
146
+ /**
147
+ * Per-request error reporter that deduplicates via the ALS request context.
148
+ *
149
+ * Uses the same _reportedErrors WeakSet as the router layer so errors
150
+ * that propagate across layers are only reported once per request.
151
+ */
152
+ export function createErrorReporter<TEnv>(router: {
153
+ readonly onError?: OnErrorCallback<TEnv>;
154
+ }): ReportError<TEnv> {
155
+ return (error, phase, context) => {
156
+ // Guard: abort signal handlers fire asynchronously outside the ALS
157
+ // request scope, so the context may be gone. Skip dedup in that
158
+ // case — the error is from a cancelled stream, not a real failure.
159
+ const reqCtx = _getRequestContext();
160
+ if (error != null && typeof error === "object" && reqCtx) {
161
+ if (reqCtx._reportedErrors.has(error)) return;
162
+ reqCtx._reportedErrors.add(error);
163
+ }
164
+ invokeOnError(router.onError, error, phase, context, "RSC");
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Wire background error reporting so "use cache" and other subsystems can
170
+ * surface non-fatal errors through the router's onError callback.
171
+ */
172
+ export function reportBackgroundErrors<TEnv>(
173
+ reqCtx: RequestContext<TEnv>,
174
+ request: Request,
175
+ url: URL,
176
+ callOnError: ReportError<TEnv>,
177
+ ): void {
178
+ reqCtx._reportBackgroundError = (
179
+ error: unknown,
180
+ category: CacheErrorCategory,
181
+ ) => {
182
+ callOnError(error, "cache", {
183
+ request,
184
+ url,
185
+ metadata: { category },
186
+ });
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Classify a request into its plan. classifyRequest throws
192
+ * RouteNotFoundError for an unknown route; that falls through to a
193
+ * full-render plan with no matched route, so the render path renders the
194
+ * 404 page (match()/matchPartial() re-throw and the render's catch renders
195
+ * it).
196
+ */
197
+ export async function classify<TEnv>(
198
+ request: Request,
199
+ url: URL,
200
+ ctx: ClassifyContext<TEnv>,
201
+ ): Promise<RequestPlan<TEnv>> {
202
+ try {
203
+ return await classifyRequest<TEnv>(request, url, {
204
+ findMatch: ctx.router.findMatch,
205
+ routerVersion: ctx.version,
206
+ routerId: ctx.router.id,
207
+ });
208
+ } catch (error) {
209
+ if (!isRouteNotFoundError(error)) throw error;
210
+ return {
211
+ mode: "full-render",
212
+ route: {
213
+ matched: null as any,
214
+ manifestEntry: null as any,
215
+ entries: [],
216
+ routeKey: "",
217
+ localRouteName: "",
218
+ params: {},
219
+ routeMiddleware: [],
220
+ cacheScope: null,
221
+ isPassthrough: false,
222
+ },
223
+ negotiated: false,
224
+ unmatched: true,
225
+ };
226
+ }
227
+ }
228
+
229
+ /** The plan classify answers an unknown route with (the render path's 404). */
230
+ export function isUnmatchedPlan(plan: RequestPlan<any>): boolean {
231
+ return plan.mode === "full-render" && plan.unmatched === true;
232
+ }
233
+
234
+ /**
235
+ * Record a plan's route on the request context: its params and name, and the
236
+ * classified snapshot match()/matchPartial() reuse instead of resolving the
237
+ * route again. Runs inside the global chain, so global middleware sees the
238
+ * route after next(). A redirect has none.
239
+ */
240
+ export function recordPlanRoute<TEnv>(plan: ExecutableRequestPlan<TEnv>): void {
241
+ if (plan.mode === "redirect") return;
242
+ setRequestContextParams(plan.route.params, plan.route.routeKey);
243
+ getRequestContext()._classifiedRoute = plan.route;
244
+ }
245
+
246
+ /**
247
+ * The checks between classification and execution that need no await.
248
+ * Returns the response of a terminal plan (a partial request's redirect, a
249
+ * version or app-switch reload), null to go on. Throws for a rewritten
250
+ * action whose target is not a page route.
251
+ *
252
+ * Synchronous, and the origin guard is a separate step: the caller awaits
253
+ * checkRequestOrigin only for the modes ORIGIN_CHECK_PHASE_BY_MODE gates, so
254
+ * a full, partial or response plan reaches execution without an added
255
+ * microtask (rsc/__tests__/request-microtasks.rsc-test.tsx).
256
+ */
257
+ export function terminalPlanResponse<TEnv>(
258
+ plan: RequestPlan<TEnv>,
259
+ url: URL,
260
+ ctx: RequestStageContext<TEnv> & ClassifyContext<TEnv>,
261
+ ): Response | null {
262
+ // A rewritten pass of an action must reach the branch that runs it: the
263
+ // one that ran it re-renders from its outcome, and a JS action a global
264
+ // rewrite moved before it ran runs only on the action branch. Any other
265
+ // plan would serve the POST as a fresh request or drop the action; a
266
+ // reload (version mismatch, app switch) does neither.
267
+ const passCtx = getRequestContext();
268
+ const actionRan = passCtx._actionRan;
269
+ const actionMode = actionRan
270
+ ? actionRan.kind === "action"
271
+ ? "action"
272
+ : "pe-render"
273
+ : url.searchParams.has(WIRE.action) && rewriteTrail(passCtx)
274
+ ? "action"
275
+ : undefined;
276
+ if (
277
+ actionMode &&
278
+ plan.mode !== actionMode &&
279
+ plan.mode !== "version-mismatch" &&
280
+ plan.mode !== "app-switch"
281
+ ) {
282
+ throw new Error(
283
+ `A rewrite of a server action or form post must target a page ` +
284
+ `route; ${url.pathname} ${plan.mode === "response" ? "is a response route" : plan.mode === "redirect" ? "redirects" : "is not one"}.`,
285
+ );
286
+ }
287
+
288
+ if (plan.mode === "redirect") {
289
+ const redirected = partialRedirectResponse(plan.redirectUrl, url, ctx);
290
+ if (redirected) return redirected;
291
+ }
292
+
293
+ // A reload points at the visitor's URL, never a rewrite target's.
294
+ if (plan.mode === "version-mismatch") {
295
+ console.log(
296
+ `[RSC] Version mismatch: client=${url.searchParams.get(WIRE.version)}, server=${ctx.version}. Forcing reload.`,
297
+ );
298
+ return createReloadResponse(getRequestContext(), plan.reloadUrl);
299
+ }
300
+
301
+ if (plan.mode === "app-switch") {
302
+ // Cross-app SPA navigation crossed a host-router app boundary. Force a
303
+ // real document navigation so the target app's document is re-established
304
+ // (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
305
+ return createReloadResponse(getRequestContext(), plan.reloadUrl);
306
+ }
307
+
308
+ return null;
309
+ }
310
+
311
+ /**
312
+ * The origin guard's rejection: reports it through onError and telemetry,
313
+ * and returns the response checkRequestOrigin rejected the request with. The
314
+ * caller awaits checkRequestOrigin itself, for the modes
315
+ * ORIGIN_CHECK_PHASE_BY_MODE gates (action, loader, PE form), so the guard
316
+ * adds no microtask over that await.
317
+ */
318
+ export function originRejectedResponse<TEnv>(
319
+ rejection: Response,
320
+ phase: OriginCheckPhase,
321
+ request: Request,
322
+ env: TEnv,
323
+ url: URL,
324
+ ctx: RequestStageContext<TEnv>,
325
+ ): Response {
326
+ const origin = requestHeaders(request).get("origin");
327
+ const host = requestHeaders(request).get("host");
328
+ const originError = new Error(
329
+ `Origin check rejected: ${origin ?? "none"} vs ${host ?? "none"}`,
330
+ );
331
+ originError.name = "OriginCheckError";
332
+
333
+ ctx.callOnError(originError, "origin", {
334
+ request,
335
+ url,
336
+ env,
337
+ handledByBoundary: false,
338
+ metadata: { phase, origin, host },
339
+ });
340
+
341
+ if (ctx.router.telemetry) {
342
+ safeEmit(resolveSink(ctx.router.telemetry), {
343
+ type: "request.origin-rejected",
344
+ timestamp: performance.now(),
345
+ requestId: getRequestId(request),
346
+ method: request.method,
347
+ pathname: url.pathname,
348
+ phase,
349
+ origin,
350
+ host,
351
+ });
352
+ }
353
+
354
+ return rejection;
355
+ }
356
+
357
+ /**
358
+ * A findMatch() redirect (trailing slash) on a partial request: redirects are
359
+ * handled by the pipeline (match/matchPartial), but a partial request
360
+ * short-circuits with a Flight redirect, since fetch() would follow a raw
361
+ * 3xx. Null on a full request, which the render path redirects (match()
362
+ * returns { redirect: url }).
363
+ */
364
+ function partialRedirectResponse<TEnv>(
365
+ redirectUrl: string,
366
+ url: URL,
367
+ ctx: RequestStageContext<TEnv>,
368
+ ): Response | null {
369
+ return url.searchParams.has(WIRE.partial)
370
+ ? ctx.createRedirectFlightResponse(redirectUrl)
371
+ : null;
372
+ }
373
+
374
+ export interface RequestPassesOptions<TEnv> {
375
+ ctx: RequestStageContext<TEnv>;
376
+ reqCtx: RequestContext<TEnv>;
377
+ /** The visitor's request and raw URL: the first pass. */
378
+ request: Request;
379
+ url: URL;
380
+ env: TEnv;
381
+ variables: Record<string, any>;
382
+ /** The visitor's global middleware, when the caller matched it already. */
383
+ firstPassMiddleware?: Array<{
384
+ entry: MiddlewareEntry<TEnv>;
385
+ params: Record<string, string>;
386
+ }>;
387
+ /** Runs before a pass's global chain; dispatch matches the pass there. */
388
+ beforePass?: (request: Request, url: URL, index: number) => Promise<void>;
389
+ /** The core handler a pass's global chain wraps. */
390
+ core: (request: Request, url: URL) => Promise<Response>;
391
+ onHop?: RewritePassesOptions["onHop"];
392
+ refuse?: RewritePassesOptions["refuse"];
393
+ }
394
+
395
+ export interface RequestPassesResult extends RewritePassesResult {
396
+ /** Whether the last pass ran a global middleware chain. */
397
+ hasMiddleware: boolean;
398
+ }
399
+
400
+ /**
401
+ * One pass per URL: the global middleware chain matched on the pass's path,
402
+ * wrapping the core handler. A `throw rewrite()` from global or route
403
+ * middleware unwinds the pass, and the next one runs the same request as
404
+ * the target from the top, as a direct visit would (rsc/rewrite-passes.ts).
405
+ */
406
+ export async function runRequestPasses<TEnv>(
407
+ opts: RequestPassesOptions<TEnv>,
408
+ ): Promise<RequestPassesResult> {
409
+ const { ctx, env, variables } = opts;
410
+ let passIndex = 0;
411
+ let hasMiddleware = false;
412
+ const runChain = (
413
+ passRequest: Request,
414
+ passUrl: URL,
415
+ index: number,
416
+ ): Promise<Response> => {
417
+ const passMiddleware =
418
+ index === 0 && opts.firstPassMiddleware
419
+ ? opts.firstPassMiddleware
420
+ : matchMiddleware(passUrl.pathname, ctx.router.middleware);
421
+ hasMiddleware = passMiddleware.length > 0;
422
+ const coreHandler = (): Promise<Response> =>
423
+ opts.core(passRequest, passUrl);
424
+ return hasMiddleware
425
+ ? executeMiddleware(
426
+ passMiddleware,
427
+ passRequest,
428
+ env,
429
+ variables,
430
+ coreHandler,
431
+ createReverseFunction(ctx.getRequiredRouteMap()),
432
+ )
433
+ : coreHandler();
434
+ };
435
+ const { response, rewrittenTo } = await runRewritePasses({
436
+ reqCtx: opts.reqCtx,
437
+ request: opts.request,
438
+ url: opts.url,
439
+ routeMap: ctx.getRequiredRouteMap,
440
+ basename: ctx.router.basename,
441
+ runPass: (passRequest, passUrl) => {
442
+ const index = passIndex++;
443
+ return opts.beforePass
444
+ ? opts
445
+ .beforePass(passRequest, passUrl, index)
446
+ .then(() => runChain(passRequest, passUrl, index))
447
+ : runChain(passRequest, passUrl, index);
448
+ },
449
+ onHop: (pass) => {
450
+ // A pass's request is a new Request: keep the visitor's telemetry id.
451
+ if (ctx.router.telemetry) {
452
+ setRequestId(pass.request, getRequestId(opts.request));
453
+ }
454
+ opts.onHop?.(pass);
455
+ },
456
+ report: (error, at) => {
457
+ ctx.callOnError(error, "routing", {
458
+ request: at.request,
459
+ url: at.url,
460
+ env,
461
+ handledByBoundary: false,
462
+ });
463
+ },
464
+ refuse: opts.refuse,
465
+ });
466
+ return { response, rewrittenTo, hasMiddleware };
467
+ }
468
+
469
+ /**
470
+ * The flat match handleResponseRoute takes, with params at the top level,
471
+ * built from a response-route plan.
472
+ */
473
+ export function responseRouteMatch(
474
+ plan: ResponseRoutePlan<any>,
475
+ ): ResponseRouteMatch {
476
+ return {
477
+ responseType: plan.responseType,
478
+ handler: plan.handler,
479
+ params: plan.route.params,
480
+ negotiated: plan.negotiated,
481
+ manifestEntry: plan.manifestEntry,
482
+ routeMiddleware: plan.routeMiddleware,
483
+ };
484
+ }
485
+
486
+ export interface ServeResponseRouteOptions<TEnv> {
487
+ match: ResponseRouteMatch;
488
+ /** The matched route's key, for the timeout report. */
489
+ routeKey: string | undefined;
490
+ /** The pass's request, raw URL and env. */
491
+ request: Request;
492
+ url: URL;
493
+ env: TEnv;
494
+ variables: Record<string, any>;
495
+ /**
496
+ * Receives the handler's work when the deadline wins. The handler leaves
497
+ * it running; dispatch cancels its body.
498
+ */
499
+ abandon?: (work: Promise<Response>) => void;
500
+ }
501
+
502
+ /**
503
+ * A response route: its handler (rsc/response-route-handler.ts) under the
504
+ * renderStartMs deadline, which starts here, inside the global chain, so
505
+ * global middleware time never counts toward it.
506
+ */
507
+ export async function serveResponseRoute<TEnv>(
508
+ ctx: RequestStageContext<TEnv>,
509
+ opts: ServeResponseRouteOptions<TEnv>,
510
+ ): Promise<Response> {
511
+ const { match, request, env, url } = opts;
512
+ const work = handleResponseRoute(
513
+ ctx,
514
+ match,
515
+ request,
516
+ env,
517
+ url,
518
+ opts.variables,
519
+ );
520
+ const responseOutcome = await withTimeout(
521
+ work,
522
+ ctx.router.timeouts?.renderStartMs,
523
+ "render-start",
524
+ );
525
+ if (responseOutcome.timedOut) {
526
+ opts.abandon?.(work);
527
+ return timeoutResponse(
528
+ ctx,
529
+ request,
530
+ env,
531
+ url,
532
+ "render-start",
533
+ responseOutcome.durationMs,
534
+ opts.routeKey,
535
+ );
536
+ }
537
+ const response = responseOutcome.result;
538
+ if (match.negotiated && !isWebSocketUpgradeResponse(response)) {
539
+ // handleResponseRoute (callHandlerWithVary) already appends Vary: Accept
540
+ // for negotiated responses; dedup so we don't emit Vary: Accept, Accept.
541
+ appendVaryAccept(response);
542
+ }
543
+ return response;
544
+ }
545
+
546
+ /**
547
+ * Handle a timeout by reporting the error, emitting telemetry,
548
+ * and returning either the custom onTimeout response or a default 504.
549
+ */
550
+ export async function timeoutResponse<TEnv>(
551
+ ctx: RequestStageContext<TEnv>,
552
+ request: Request,
553
+ env: TEnv,
554
+ url: URL,
555
+ phase: TimeoutPhase,
556
+ durationMs: number,
557
+ routeKey?: string,
558
+ actionId?: string,
559
+ ): Promise<Response> {
560
+ const router = ctx.router;
561
+ const timeoutError = new RouterTimeoutError(phase, durationMs);
562
+ const requestContext = _getRequestContext<TEnv>();
563
+ const cursor = requestContext?._renderForeground;
564
+ const render: RenderTimeoutContext | undefined =
565
+ phase === "render-start" && cursor
566
+ ? {
567
+ mode: cursor.mode,
568
+ phase: cursor.phase,
569
+ state: cursor.state,
570
+ completed: cursor.completed,
571
+ total: cursor.total,
572
+ ...(cursor.phaseStartedAt !== undefined && {
573
+ phaseDurationMs: performance.now() - cursor.phaseStartedAt,
574
+ }),
575
+ }
576
+ : undefined;
577
+ const trace =
578
+ phase === "render-start" ? requestContext?._activeRoutine : undefined;
579
+ const activeEntries = trace?.active() ?? [];
580
+ const activeStep = activeEntries.at(-1);
581
+ const activeAt = performance.now();
582
+ const routineSnapshot =
583
+ trace && activeStep
584
+ ? {
585
+ name: trace.name,
586
+ path: activeEntries.map((entry) => entry.name),
587
+ durationMs: activeAt - activeStep.startedAt,
588
+ }
589
+ : undefined;
590
+
591
+ if (INTERNAL_RANGO_DEBUG && trace && activeStep) {
592
+ console.log(
593
+ `[routine] TIMEOUT ${request.method} ${url.pathname} (${trace.name})\n${trace.formatActive(activeEntries, activeAt)}`,
594
+ );
595
+ }
596
+
597
+ // Each surface gets its OWN shallow render snapshot. A consumer that mutates
598
+ // its copy must not corrupt what the other two observe. The internal-debug
599
+ // routine snapshot is bounded metadata sent only to onError.
600
+ ctx.callOnError(timeoutError, phase === "action" ? "action" : "handler", {
601
+ request,
602
+ url,
603
+ env,
604
+ routeKey,
605
+ actionId,
606
+ handledByBoundary: false,
607
+ metadata: {
608
+ timeout: true,
609
+ phase,
610
+ durationMs,
611
+ ...(render && { render: { ...render } }),
612
+ ...(routineSnapshot && { routine: routineSnapshot }),
613
+ },
614
+ });
615
+
616
+ if (router.telemetry) {
617
+ safeEmit(resolveSink(router.telemetry), {
618
+ type: "request.timeout",
619
+ timestamp: performance.now(),
620
+ requestId: getRequestId(request),
621
+ phase,
622
+ pathname: url.pathname,
623
+ routeKey,
624
+ actionId,
625
+ durationMs,
626
+ customHandler: !!router.onTimeout,
627
+ ...(render && { render: { ...render } }),
628
+ });
629
+ }
630
+
631
+ if (router.onTimeout) {
632
+ try {
633
+ return await router.onTimeout({
634
+ phase,
635
+ request,
636
+ url,
637
+ env,
638
+ routeKey,
639
+ actionId,
640
+ durationMs,
641
+ ...(render && { render: { ...render } }),
642
+ });
643
+ } catch (e) {
644
+ if (process.env.NODE_ENV !== "production") {
645
+ console.error("[RSC] onTimeout callback error:", e);
646
+ }
647
+ return createDefaultTimeoutResponse(phase);
648
+ }
649
+ }
650
+
651
+ return createDefaultTimeoutResponse(phase);
652
+ }
653
+
654
+ export interface FinalizeRequestOptions<TEnv> {
655
+ ctx: RequestStageContext<TEnv>;
656
+ reqCtx: RequestContext<TEnv>;
657
+ /** The visitor's request, raw URL and env. */
658
+ request: Request;
659
+ url: URL;
660
+ env: TEnv;
661
+ /** runRequestPasses' result. */
662
+ hasMiddleware: boolean;
663
+ rewrittenTo?: string;
664
+ /** Runs on the response before the open-redirect guard. */
665
+ beforeGuard?: (response: Response) => void;
666
+ }
667
+
668
+ /**
669
+ * The response the global chain returned, made ready to leave: a partial or
670
+ * action request's redirect converted, onResponse callbacks drained, the
671
+ * rewrite header, the open-redirect guard and the stream-idle watchdog.
672
+ */
673
+ export function finalizeRequestResponse<TEnv>(
674
+ downstream: Response,
675
+ opts: FinalizeRequestOptions<TEnv>,
676
+ ): Response {
677
+ const { ctx, reqCtx, request, url, env } = opts;
678
+ const router = ctx.router;
679
+ let response: Response;
680
+ if (opts.hasMiddleware) {
681
+ if (
682
+ url.searchParams.has(WIRE.partial) ||
683
+ url.searchParams.has(WIRE.action)
684
+ ) {
685
+ const intercepted = interceptRedirectForPartial(
686
+ downstream,
687
+ ctx.createRedirectFlightResponse,
688
+ { requestOrigin: url.origin, basename: router.basename },
689
+ );
690
+ response = intercepted ?? finalizeResponse(downstream);
691
+ } else {
692
+ response = finalizeResponse(downstream);
693
+ }
694
+ } else {
695
+ response = downstream;
696
+ }
697
+
698
+ if (opts.rewrittenTo !== undefined) {
699
+ trySetHeader(response, REWRITE_HEADER, opts.rewrittenTo);
700
+ }
701
+ opts.beforeGuard?.(response);
702
+
703
+ // Single open-redirect chokepoint: every response (PE, full-page,
704
+ // middleware short-circuit, response-route) funnels through here, so
705
+ // guarding browser-followed (3xx) redirects once covers them all and any
706
+ // future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
707
+ // through untouched (validated client-side instead).
708
+ const guarded = guardOutgoingRedirect(response, url.origin, router.basename);
709
+
710
+ // Stream-idle watchdog (opt-in via timeouts.streamIdleMs): bounds
711
+ // end-to-end idle flow on the streamed body — see rsc/stream-idle.ts
712
+ // for the semantics. Applied at this finalization chokepoint so every
713
+ // streaming exit is covered; websocket upgrades must never be
714
+ // reconstructed and bodiless responses have nothing to bound. The
715
+ // trip fires POST-handoff (the request ALS may be gone), so it
716
+ // reports via the eagerly captured surfaces — callOnError +
717
+ // router.telemetry directly — mirroring handleStore.onError.
718
+ // onTimeout does NOT apply: the response already left the handler,
719
+ // so no replacement Response can be served mid-stream.
720
+ const streamIdleMs = router.timeouts?.streamIdleMs;
721
+ // Websocket check FIRST: a workerd upgrade response must never have
722
+ // its body getter poked.
723
+ if (
724
+ !isTimeoutEnabled(streamIdleMs) ||
725
+ isWebSocketUpgradeResponse(guarded) ||
726
+ !guarded.body
727
+ ) {
728
+ return guarded;
729
+ }
730
+ const routeKey = reqCtx._routeName;
731
+ return applyStreamIdleTimeout(guarded, streamIdleMs!, (tripInfo) => {
732
+ ctx.callOnError(tripInfo.error, "handler", {
733
+ request,
734
+ url,
735
+ env,
736
+ routeKey,
737
+ handledByBoundary: false,
738
+ metadata: {
739
+ timeout: true,
740
+ phase: "stream-idle",
741
+ durationMs: tripInfo.totalMs,
742
+ },
743
+ });
744
+ if (router.telemetry) {
745
+ safeEmit(resolveSink(router.telemetry), {
746
+ type: "request.timeout",
747
+ timestamp: performance.now(),
748
+ requestId: getRequestId(request),
749
+ phase: "stream-idle",
750
+ pathname: url.pathname,
751
+ routeKey,
752
+ durationMs: tripInfo.totalMs,
753
+ customHandler: false,
754
+ });
755
+ }
756
+ });
757
+ }