@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -867
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1133
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,440 @@
1
+ import type { ReactNode } from "react";
2
+ import { sanitizeError } from "../errors";
3
+ import type { ErrorInfo, ErrorPhase, MatchResult } from "../types";
4
+ import type {
5
+ EntryData,
6
+ InterceptEntry,
7
+ InterceptSelectorContext,
8
+ } from "../server/context";
9
+ import type { MatchApiDeps } from "./types.js";
10
+ import type { RouterContext } from "./router-context.js";
11
+ import { runWithRouterContext } from "./router-context.js";
12
+ import {
13
+ type ActionContext,
14
+ type MatchContext,
15
+ createPipelineState,
16
+ } from "./match-context.js";
17
+ import { createMatchPartialPipeline } from "./match-pipelines.js";
18
+ import { collectMatchResult } from "./match-result.js";
19
+ import {
20
+ createMatchContextForFull as _createMatchContextForFull,
21
+ createMatchContextForPartial as _createMatchContextForPartial,
22
+ matchError as _matchError,
23
+ } from "./match-api.js";
24
+ import { previewMatch as _previewMatch } from "./preview-match.js";
25
+ import {
26
+ runWithRouterLogContext,
27
+ withRouterLogScope,
28
+ isRouterDebugEnabled,
29
+ startRevalidationTrace,
30
+ flushRevalidationTrace,
31
+ } from "./logging.js";
32
+ import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
33
+ import type { MiddlewareFn } from "./middleware.js";
34
+ import {
35
+ type TelemetrySink,
36
+ safeEmit,
37
+ resolveSink,
38
+ getRequestId,
39
+ } from "./telemetry.js";
40
+
41
+ export interface MatchHandlerDeps<TEnv = any> {
42
+ buildRouterContext: () => RouterContext<TEnv>;
43
+ callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
44
+ matchApiDeps: MatchApiDeps<TEnv>;
45
+ defaultErrorBoundary: ReactNode | ErrorBoundaryHandler | undefined;
46
+ findMatch: (pathname: string, ms?: any) => any;
47
+ findInterceptForRoute: (
48
+ routeKey: string,
49
+ parentEntry: EntryData | null,
50
+ selectorContext: InterceptSelectorContext | null,
51
+ isAction: boolean,
52
+ ) => { intercept: InterceptEntry; entry: EntryData } | null;
53
+ telemetry?: TelemetrySink;
54
+ }
55
+
56
+ export interface MatchHandlers<TEnv = any> {
57
+ match: (request: Request, env: TEnv) => Promise<MatchResult>;
58
+ matchPartial: (
59
+ request: Request,
60
+ context: TEnv,
61
+ actionContext?: ActionContext,
62
+ ) => Promise<MatchResult | null>;
63
+ matchError: (
64
+ request: Request,
65
+ _context: TEnv,
66
+ error: unknown,
67
+ segmentType?: ErrorInfo["segmentType"],
68
+ ) => Promise<MatchResult | null>;
69
+ previewMatch: (
70
+ request: Request,
71
+ _context: TEnv,
72
+ ) => Promise<{
73
+ routeMiddleware?: Array<{
74
+ handler: MiddlewareFn;
75
+ params: Record<string, string>;
76
+ }>;
77
+ responseType?: string;
78
+ handler?: Function;
79
+ params?: Record<string, string>;
80
+ negotiated?: boolean;
81
+ manifestEntry?: EntryData;
82
+ } | null>;
83
+ createMatchContextForFull: (
84
+ request: Request,
85
+ env: TEnv,
86
+ ) => Promise<MatchContext<TEnv> | { type: "redirect"; redirectUrl: string }>;
87
+ createMatchContextForPartial: (
88
+ request: Request,
89
+ env: TEnv,
90
+ actionContext?: {
91
+ actionId?: string;
92
+ actionUrl?: URL;
93
+ actionResult?: any;
94
+ formData?: FormData;
95
+ },
96
+ ) => Promise<MatchContext<TEnv> | null>;
97
+ }
98
+
99
+ /**
100
+ * Create match handler functions bound to router closure state.
101
+ * These are the main request-handling entry points for SSR, navigation,
102
+ * error recovery, and preview matching.
103
+ */
104
+ export function createMatchHandlers<TEnv = any>(
105
+ deps: MatchHandlerDeps<TEnv>,
106
+ ): MatchHandlers<TEnv> {
107
+ const {
108
+ buildRouterContext,
109
+ callOnError,
110
+ matchApiDeps,
111
+ defaultErrorBoundary,
112
+ findInterceptForRoute,
113
+ } = deps;
114
+ const hasTelemetry = !!deps.telemetry;
115
+ const telemetry = resolveSink(deps.telemetry);
116
+
117
+ async function createMatchContextForFull(
118
+ request: Request,
119
+ env: TEnv,
120
+ ): Promise<MatchContext<TEnv> | { type: "redirect"; redirectUrl: string }> {
121
+ return _createMatchContextForFull(
122
+ request,
123
+ env,
124
+ matchApiDeps,
125
+ findInterceptForRoute,
126
+ );
127
+ }
128
+
129
+ async function createMatchContextForPartial(
130
+ request: Request,
131
+ env: TEnv,
132
+ actionContext?: {
133
+ actionId?: string;
134
+ actionUrl?: URL;
135
+ actionResult?: any;
136
+ formData?: FormData;
137
+ },
138
+ ): Promise<MatchContext<TEnv> | null> {
139
+ return _createMatchContextForPartial(
140
+ request,
141
+ env,
142
+ matchApiDeps,
143
+ findInterceptForRoute,
144
+ actionContext,
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Match request and return segments (document/SSR requests)
150
+ *
151
+ * Uses generator middleware pipeline for clean separation of concerns:
152
+ * - cache-lookup: Check cache first
153
+ * - segment-resolution: Resolve segments on cache miss
154
+ * - cache-store: Store results in cache
155
+ * - background-revalidation: SWR revalidation
156
+ */
157
+ async function match(request: Request, env: TEnv): Promise<MatchResult> {
158
+ const requestId = hasTelemetry ? getRequestId(request) : undefined;
159
+ return runWithRouterLogContext({ request, transaction: "match" }, () => {
160
+ const routerCtx = buildRouterContext();
161
+ routerCtx.requestId = requestId;
162
+ return runWithRouterContext(routerCtx, async () =>
163
+ withRouterLogScope("match", async () => {
164
+ const matchStart = performance.now();
165
+ const pathname = new URL(request.url).pathname;
166
+ if (hasTelemetry) {
167
+ safeEmit(telemetry, {
168
+ type: "request.start",
169
+ timestamp: matchStart,
170
+ requestId,
171
+ method: request.method,
172
+ pathname,
173
+ transaction: "match",
174
+ isPartial: false,
175
+ });
176
+ }
177
+
178
+ const result = await createMatchContextForFull(request, env);
179
+
180
+ // Handle redirect case
181
+ if ("type" in result && result.type === "redirect") {
182
+ if (hasTelemetry) {
183
+ safeEmit(telemetry, {
184
+ type: "request.end",
185
+ timestamp: performance.now(),
186
+ requestId,
187
+ method: request.method,
188
+ pathname,
189
+ transaction: "match",
190
+ durationMs: performance.now() - matchStart,
191
+ segmentCount: 0,
192
+ cacheHit: false,
193
+ });
194
+ }
195
+ return {
196
+ segments: [],
197
+ matched: [],
198
+ diff: [],
199
+ params: {},
200
+ redirect: result.redirectUrl,
201
+ };
202
+ }
203
+
204
+ const ctx = result as MatchContext<TEnv>;
205
+
206
+ try {
207
+ const state = createPipelineState();
208
+ const pipeline = createMatchPartialPipeline(ctx, state);
209
+ const matchResult = await collectMatchResult(pipeline, ctx, state);
210
+ if (hasTelemetry) {
211
+ safeEmit(telemetry, {
212
+ type: "cache.decision",
213
+ timestamp: performance.now(),
214
+ requestId,
215
+ pathname,
216
+ routeKey: ctx.routeKey,
217
+ hit: state.cacheHit,
218
+ shouldRevalidate: !!state.shouldRevalidate,
219
+ source: state.cacheSource,
220
+ });
221
+ safeEmit(telemetry, {
222
+ type: "request.end",
223
+ timestamp: performance.now(),
224
+ requestId,
225
+ method: request.method,
226
+ pathname,
227
+ transaction: "match",
228
+ durationMs: performance.now() - matchStart,
229
+ segmentCount: matchResult.segments.length,
230
+ cacheHit: state.cacheHit,
231
+ });
232
+ }
233
+ return matchResult;
234
+ } catch (error) {
235
+ if (hasTelemetry) {
236
+ const errorObj =
237
+ error instanceof Error ? error : new Error(String(error));
238
+ safeEmit(telemetry, {
239
+ type: "request.error",
240
+ timestamp: performance.now(),
241
+ requestId,
242
+ method: request.method,
243
+ pathname,
244
+ transaction: "match",
245
+ error: errorObj,
246
+ phase: error instanceof Response ? "redirect" : "routing",
247
+ durationMs: performance.now() - matchStart,
248
+ });
249
+ }
250
+ if (error instanceof Response) throw error;
251
+ // Report unhandled errors during full match pipeline
252
+ callOnError(error, "routing", {
253
+ request,
254
+ url: ctx.url,
255
+ env,
256
+ isPartial: false,
257
+ handledByBoundary: false,
258
+ });
259
+ throw sanitizeError(error);
260
+ }
261
+ }),
262
+ );
263
+ });
264
+ }
265
+
266
+ async function matchError(
267
+ request: Request,
268
+ _context: TEnv,
269
+ error: unknown,
270
+ segmentType: ErrorInfo["segmentType"] = "route",
271
+ ): Promise<MatchResult | null> {
272
+ return runWithRouterLogContext({ request, transaction: "matchError" }, () =>
273
+ withRouterLogScope("matchError", () =>
274
+ _matchError(
275
+ request,
276
+ _context,
277
+ error,
278
+ matchApiDeps,
279
+ defaultErrorBoundary,
280
+ segmentType,
281
+ ),
282
+ ),
283
+ );
284
+ }
285
+
286
+ /**
287
+ * Match partial request with revalidation
288
+ *
289
+ * Uses generator middleware pipeline for clean separation of concerns:
290
+ * - cache-lookup: Check cache first
291
+ * - segment-resolution: Resolve segments on cache miss
292
+ * - intercept-resolution: Handle intercept routes
293
+ * - cache-store: Store results in cache
294
+ * - background-revalidation: SWR revalidation
295
+ */
296
+ async function matchPartial(
297
+ request: Request,
298
+ context: TEnv,
299
+ actionContext?: ActionContext,
300
+ ): Promise<MatchResult | null> {
301
+ const partialRequestId = hasTelemetry ? getRequestId(request) : undefined;
302
+ return runWithRouterLogContext(
303
+ { request, transaction: "matchPartial" },
304
+ () => {
305
+ const routerCtx = buildRouterContext();
306
+ routerCtx.requestId = partialRequestId;
307
+ return runWithRouterContext(routerCtx, async () =>
308
+ withRouterLogScope("matchPartial", async () => {
309
+ const matchStart = performance.now();
310
+ const pathname = new URL(request.url).pathname;
311
+ if (hasTelemetry) {
312
+ safeEmit(telemetry, {
313
+ type: "request.start",
314
+ timestamp: matchStart,
315
+ requestId: partialRequestId,
316
+ method: request.method,
317
+ pathname,
318
+ transaction: "matchPartial",
319
+ isPartial: true,
320
+ });
321
+ }
322
+
323
+ const ctx = await createMatchContextForPartial(
324
+ request,
325
+ context,
326
+ actionContext,
327
+ );
328
+ if (!ctx) {
329
+ if (hasTelemetry) {
330
+ safeEmit(telemetry, {
331
+ type: "request.end",
332
+ timestamp: performance.now(),
333
+ requestId: partialRequestId,
334
+ method: request.method,
335
+ pathname,
336
+ transaction: "matchPartial",
337
+ durationMs: performance.now() - matchStart,
338
+ segmentCount: 0,
339
+ cacheHit: false,
340
+ });
341
+ }
342
+ return null;
343
+ }
344
+
345
+ if (isRouterDebugEnabled()) {
346
+ startRevalidationTrace({
347
+ method: request.method,
348
+ prevUrl: ctx.prevUrl.href,
349
+ nextUrl: ctx.url.href,
350
+ routeKey: ctx.routeKey,
351
+ isAction: !!actionContext,
352
+ stale: ctx.stale || undefined,
353
+ });
354
+ }
355
+
356
+ try {
357
+ const state = createPipelineState();
358
+ const pipeline = createMatchPartialPipeline(ctx, state);
359
+ const matchResult = await collectMatchResult(
360
+ pipeline,
361
+ ctx,
362
+ state,
363
+ );
364
+ flushRevalidationTrace();
365
+ if (hasTelemetry) {
366
+ safeEmit(telemetry, {
367
+ type: "cache.decision",
368
+ timestamp: performance.now(),
369
+ requestId: partialRequestId,
370
+ pathname,
371
+ routeKey: ctx.routeKey,
372
+ hit: state.cacheHit,
373
+ shouldRevalidate: !!state.shouldRevalidate,
374
+ source: state.cacheSource,
375
+ });
376
+ safeEmit(telemetry, {
377
+ type: "request.end",
378
+ timestamp: performance.now(),
379
+ requestId: partialRequestId,
380
+ method: request.method,
381
+ pathname,
382
+ transaction: "matchPartial",
383
+ durationMs: performance.now() - matchStart,
384
+ segmentCount: matchResult.segments.length,
385
+ cacheHit: state.cacheHit,
386
+ });
387
+ }
388
+ return matchResult;
389
+ } catch (error) {
390
+ flushRevalidationTrace();
391
+ if (hasTelemetry) {
392
+ const errorObj =
393
+ error instanceof Error ? error : new Error(String(error));
394
+ const phase = actionContext ? "action" : "revalidation";
395
+ safeEmit(telemetry, {
396
+ type: "request.error",
397
+ timestamp: performance.now(),
398
+ requestId: partialRequestId,
399
+ method: request.method,
400
+ pathname,
401
+ transaction: "matchPartial",
402
+ error: errorObj,
403
+ phase: error instanceof Response ? "redirect" : phase,
404
+ durationMs: performance.now() - matchStart,
405
+ });
406
+ }
407
+ if (error instanceof Response) throw error;
408
+ // Report unhandled errors during partial match pipeline
409
+ callOnError(error, actionContext ? "action" : "revalidation", {
410
+ request,
411
+ url: ctx.url,
412
+ env: context,
413
+ actionId: actionContext?.actionId,
414
+ isPartial: true,
415
+ handledByBoundary: false,
416
+ });
417
+ throw sanitizeError(error);
418
+ }
419
+ }),
420
+ );
421
+ },
422
+ );
423
+ }
424
+
425
+ async function previewMatch(
426
+ request: Request,
427
+ _context: TEnv,
428
+ ): ReturnType<typeof _previewMatch> {
429
+ return _previewMatch(request, _context, { findMatch: deps.findMatch });
430
+ }
431
+
432
+ return {
433
+ match: match,
434
+ matchPartial: matchPartial,
435
+ matchError: matchError,
436
+ previewMatch: previewMatch,
437
+ createMatchContextForFull: createMatchContextForFull,
438
+ createMatchContextForPartial: createMatchContextForPartial,
439
+ };
440
+ }
@@ -30,23 +30,15 @@
30
30
  * |
31
31
  * v (async, doesn't block response)
32
32
  * +---------------------------+
33
- * | Create fresh handleStore | Isolate from response stream
33
+ * | Create fresh context | Fresh handleStore, handlerContext,
34
+ * | (full isolation) | and loaderPromises map
34
35
  * +---------------------------+
35
36
  * |
36
37
  * v
37
- * +---------------------+
38
- * | isFullMatch? |
39
- * +---------------------+
40
- * |
41
- * +-----+-----+
42
- * | |
43
- * yes no
44
- * | |
45
- * v v
46
- * resolveAll resolveWithRevalidation
47
- * Segments + resolveIntercepts
48
- * | |
49
- * +-----------+
38
+ * +---------------------------+
39
+ * | resolveAllSegments() | Fresh resolution (no revalidation)
40
+ * | + resolveIntercepts() | Ensures complete components
41
+ * +---------------------------+
50
42
  * |
51
43
  * v
52
44
  * +---------------------------+
@@ -90,32 +82,28 @@
90
82
  * ISOLATION FROM RESPONSE
91
83
  * =======================
92
84
  *
93
- * The background revalidation creates a fresh handleStore:
94
- *
95
- * requestCtx._handleStore = createHandleStore();
85
+ * Background revalidation creates fully isolated context:
86
+ * - Fresh handleStore (prevents polluting the response stream)
87
+ * - Fresh handlerContext + loaderPromises (prevents reusing memoized
88
+ * loader results from the foreground pass)
89
+ * - handleStore is saved/restored in try/finally
96
90
  *
97
- * This prevents background handle.push() calls from:
98
- * - Polluting the current response stream
99
- * - Causing duplicate data in the client
100
- * - Creating race conditions
91
+ * This matches the proactive caching pattern in cache-store.ts.
101
92
  *
102
93
  *
103
- * FULL VS PARTIAL REVALIDATION
104
- * ============================
94
+ * FRESH RESOLUTION (NO REVALIDATION)
95
+ * ===================================
105
96
  *
106
- * Full Match (document request):
107
- * - Simple resolveAllSegments()
108
- * - No need to compare with previous state
109
- *
110
- * Partial Match (navigation):
111
- * - resolveAllSegmentsWithRevalidation()
112
- * - Also resolves intercept segments if applicable
113
- * - More complex but handles all scenarios
97
+ * Both full and partial requests use resolveAllSegments() (without
98
+ * revalidation logic) to ensure all segments have complete components.
99
+ * Using revalidation-aware resolution would produce null components
100
+ * for skipped segments, which would corrupt the cache entry.
114
101
  */
115
102
  import type { ResolvedSegment } from "../../types.js";
116
103
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
117
104
  import { getRouterContext } from "../router-context.js";
118
105
  import type { GeneratorMiddleware } from "./cache-lookup.js";
106
+ import { debugLog, debugWarn } from "../logging.js";
119
107
 
120
108
  /**
121
109
  * Creates background revalidation middleware
@@ -127,10 +115,10 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
127
115
  */
128
116
  export function withBackgroundRevalidation<TEnv>(
129
117
  ctx: MatchContext<TEnv>,
130
- state: MatchPipelineState
118
+ state: MatchPipelineState,
131
119
  ): GeneratorMiddleware<ResolvedSegment> {
132
120
  return async function* (
133
- source: AsyncGenerator<ResolvedSegment>
121
+ source: AsyncGenerator<ResolvedSegment>,
134
122
  ): AsyncGenerator<ResolvedSegment> {
135
123
  // Pass through all segments unchanged
136
124
  for await (const segment of source) {
@@ -147,7 +135,8 @@ export function withBackgroundRevalidation<TEnv>(
147
135
  const {
148
136
  getRequestContext,
149
137
  createHandleStore,
150
- resolveAllSegmentsWithRevalidation,
138
+ createHandlerContext,
139
+ setupLoaderAccess,
151
140
  resolveAllSegments,
152
141
  resolveInterceptEntry,
153
142
  } = getRouterContext<TEnv>();
@@ -155,81 +144,79 @@ export function withBackgroundRevalidation<TEnv>(
155
144
  const requestCtx = getRequestContext();
156
145
  const cacheScope = ctx.cacheScope;
157
146
 
158
- const logPrefix = ctx.isFullMatch ? "[Router.match]" : "[Router.matchPartial]";
159
-
160
147
  requestCtx?.waitUntil(async () => {
161
- console.log(`${logPrefix} Revalidating stale route: ${ctx.pathname}`);
162
- try {
163
- // Create a fresh handleStore for background revalidation
164
- // to avoid polluting the current response's handle stream
165
- if (requestCtx) {
166
- requestCtx._handleStore = createHandleStore();
167
- }
148
+ debugLog("backgroundRevalidation", "revalidating stale route", {
149
+ pathname: ctx.pathname,
150
+ fullMatch: ctx.isFullMatch,
151
+ });
168
152
 
169
- let freshSegments: ResolvedSegment[];
153
+ // Save and replace handleStore to avoid polluting the response stream.
154
+ // Restore in finally (same pattern as proactive caching in cache-store).
155
+ const originalHandleStore = requestCtx._handleStore;
156
+ requestCtx._handleStore = createHandleStore();
170
157
 
171
- if (ctx.isFullMatch) {
172
- // Full match (document request) - simple resolution
173
- freshSegments = await resolveAllSegments(
174
- ctx.entries,
175
- ctx.routeKey,
176
- ctx.matched.params,
177
- ctx.handlerContext,
178
- ctx.loaderPromises
179
- );
180
- } else {
181
- // Partial match (navigation) - resolution with revalidation
182
- const freshResult = await resolveAllSegmentsWithRevalidation(
158
+ try {
159
+ // Create fresh handler context and loader promises to avoid
160
+ // reusing memoized results from the foreground pass
161
+ const freshHandlerContext = createHandlerContext(
162
+ ctx.matched.params,
163
+ ctx.request,
164
+ ctx.url.searchParams,
165
+ ctx.pathname,
166
+ ctx.url,
167
+ ctx.env,
168
+ ctx.routeMap,
169
+ ctx.matched.routeKey,
170
+ ctx.matched.responseType,
171
+ ctx.matched.pt === true,
172
+ );
173
+ const freshLoaderPromises = new Map<string, Promise<any>>();
174
+ setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
175
+
176
+ // Resolve all segments fresh (without revalidation logic)
177
+ // to ensure complete components for caching
178
+ const freshSegments = await ctx.Store.run(() =>
179
+ resolveAllSegments(
183
180
  ctx.entries,
184
181
  ctx.routeKey,
185
182
  ctx.matched.params,
186
- ctx.handlerContext,
187
- ctx.clientSegmentSet,
188
- ctx.prevParams,
189
- ctx.request,
190
- ctx.prevUrl,
191
- ctx.url,
192
- ctx.loaderPromises,
193
- ctx.actionContext,
194
- ctx.interceptResult,
195
- ctx.localRouteName,
196
- ctx.pathname
197
- );
198
-
199
- freshSegments = freshResult.segments;
183
+ freshHandlerContext,
184
+ freshLoaderPromises,
185
+ ),
186
+ );
200
187
 
201
- // For intercept revalidation, also resolve fresh intercept segments
202
- if (ctx.interceptResult) {
203
- const freshInterceptSegments = await resolveInterceptEntry(
204
- ctx.interceptResult.intercept,
205
- ctx.interceptResult.entry,
188
+ // Also resolve intercept segments fresh if applicable
189
+ let freshInterceptSegments: ResolvedSegment[] = [];
190
+ if (ctx.interceptResult) {
191
+ freshInterceptSegments = await ctx.Store.run(() =>
192
+ resolveInterceptEntry(
193
+ ctx.interceptResult!.intercept,
194
+ ctx.interceptResult!.entry,
206
195
  ctx.matched.params,
207
- ctx.handlerContext,
196
+ freshHandlerContext,
208
197
  true,
209
- {
210
- clientSegmentIds: ctx.clientSegmentSet,
211
- prevParams: ctx.prevParams,
212
- request: ctx.request,
213
- prevUrl: ctx.prevUrl,
214
- nextUrl: ctx.url,
215
- routeKey: ctx.routeKey,
216
- actionContext: ctx.actionContext,
217
- stale: false,
218
- }
219
- );
220
- freshSegments = [...freshSegments, ...freshInterceptSegments];
221
- }
198
+ ),
199
+ );
222
200
  }
223
201
 
202
+ const completeSegments = [...freshSegments, ...freshInterceptSegments];
203
+ requestCtx._handleStore.seal();
224
204
  await cacheScope.cacheRoute(
225
205
  ctx.pathname,
226
206
  ctx.matched.params,
227
- freshSegments,
228
- ctx.isIntercept
207
+ completeSegments,
208
+ ctx.isIntercept,
229
209
  );
230
- console.log(`${logPrefix} Revalidation complete: ${ctx.pathname}`);
210
+ debugLog("backgroundRevalidation", "revalidation complete", {
211
+ pathname: ctx.pathname,
212
+ });
231
213
  } catch (error) {
232
- console.error(`${logPrefix} Revalidation failed:`, error);
214
+ debugWarn("backgroundRevalidation", "revalidation failed", {
215
+ pathname: ctx.pathname,
216
+ error: String(error),
217
+ });
218
+ } finally {
219
+ requestCtx._handleStore = originalHandleStore;
233
220
  }
234
221
  });
235
222
  };