@rangojs/router 0.27.0 → 0.29.0

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