@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847

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 (298) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -212
  4. package/dist/vite/index.js +3995 -2489
  5. package/package.json +57 -52
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -23
  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 +6 -4
  13. package/skills/hooks/SKILL.md +328 -70
  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 +62 -15
  18. package/skills/loader/SKILL.md +368 -42
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +14 -10
  21. package/skills/parallel/SKILL.md +137 -1
  22. package/skills/prerender/SKILL.md +366 -28
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +136 -83
  25. package/skills/route/SKILL.md +195 -21
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +240 -102
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +102 -4
  31. package/src/bin/rango.ts +312 -15
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +92 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +24 -4
  38. package/src/browser/logging.ts +11 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +266 -558
  41. package/src/browser/navigation-client.ts +132 -75
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +297 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +303 -309
  46. package/src/browser/prefetch/cache.ts +206 -0
  47. package/src/browser/prefetch/fetch.ts +144 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +48 -0
  50. package/src/browser/prefetch/queue.ts +128 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +190 -70
  53. package/src/browser/react/NavigationProvider.tsx +78 -11
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +6 -1
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +29 -70
  65. package/src/browser/react/use-link-status.ts +6 -5
  66. package/src/browser/react/use-navigation.ts +22 -63
  67. package/src/browser/react/use-params.ts +65 -0
  68. package/src/browser/react/use-pathname.ts +47 -0
  69. package/src/browser/react/use-router.ts +63 -0
  70. package/src/browser/react/use-search-params.ts +56 -0
  71. package/src/browser/react/use-segments.ts +80 -97
  72. package/src/browser/response-adapter.ts +73 -0
  73. package/src/browser/rsc-router.tsx +188 -57
  74. package/src/browser/scroll-restoration.ts +117 -44
  75. package/src/browser/segment-reconciler.ts +221 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +488 -606
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +116 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +63 -21
  82. package/src/build/generate-route-types.ts +36 -1038
  83. package/src/build/index.ts +2 -5
  84. package/src/build/route-trie.ts +38 -12
  85. package/src/build/route-types/ast-helpers.ts +25 -0
  86. package/src/build/route-types/ast-route-extraction.ts +98 -0
  87. package/src/build/route-types/codegen.ts +102 -0
  88. package/src/build/route-types/include-resolution.ts +411 -0
  89. package/src/build/route-types/param-extraction.ts +48 -0
  90. package/src/build/route-types/per-module-writer.ts +128 -0
  91. package/src/build/route-types/router-processing.ts +479 -0
  92. package/src/build/route-types/scan-filter.ts +78 -0
  93. package/src/build/runtime-discovery.ts +231 -0
  94. package/src/cache/background-task.ts +34 -0
  95. package/src/cache/cache-key-utils.ts +44 -0
  96. package/src/cache/cache-policy.ts +125 -0
  97. package/src/cache/cache-runtime.ts +342 -0
  98. package/src/cache/cache-scope.ts +122 -303
  99. package/src/cache/cf/cf-cache-store.ts +571 -17
  100. package/src/cache/cf/index.ts +13 -3
  101. package/src/cache/document-cache.ts +116 -77
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +1 -15
  105. package/src/cache/memory-segment-store.ts +191 -13
  106. package/src/cache/profile-registry.ts +73 -0
  107. package/src/cache/read-through-swr.ts +134 -0
  108. package/src/cache/segment-codec.ts +256 -0
  109. package/src/cache/taint.ts +98 -0
  110. package/src/cache/types.ts +72 -122
  111. package/src/client.rsc.tsx +3 -1
  112. package/src/client.tsx +84 -126
  113. package/src/component-utils.ts +4 -4
  114. package/src/components/DefaultDocument.tsx +5 -1
  115. package/src/context-var.ts +86 -0
  116. package/src/debug.ts +19 -9
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +12 -7
  119. package/src/handles/MetaTags.tsx +73 -20
  120. package/src/handles/breadcrumbs.ts +66 -0
  121. package/src/handles/index.ts +1 -0
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +21 -15
  124. package/src/host/errors.ts +8 -8
  125. package/src/host/index.ts +4 -7
  126. package/src/host/pattern-matcher.ts +27 -27
  127. package/src/host/router.ts +61 -39
  128. package/src/host/testing.ts +8 -8
  129. package/src/host/types.ts +15 -7
  130. package/src/host/utils.ts +1 -1
  131. package/src/href-client.ts +65 -45
  132. package/src/index.rsc.ts +104 -40
  133. package/src/index.ts +122 -67
  134. package/src/internal-debug.ts +9 -3
  135. package/src/loader.rsc.ts +18 -93
  136. package/src/loader.ts +26 -9
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +4 -2
  140. package/src/prerender/store.ts +121 -17
  141. package/src/prerender.ts +325 -20
  142. package/src/reverse.ts +144 -124
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +7 -4
  145. package/src/route-definition/dsl-helpers.ts +959 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1450
  151. package/src/route-map-builder.ts +87 -133
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +41 -6
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +160 -0
  158. package/src/router/handler-context.ts +324 -116
  159. package/src/router/intercept-resolution.ts +11 -4
  160. package/src/router/lazy-includes.ts +237 -0
  161. package/src/router/loader-resolution.ts +179 -133
  162. package/src/router/logging.ts +112 -6
  163. package/src/router/manifest.ts +58 -19
  164. package/src/router/match-api.ts +89 -88
  165. package/src/router/match-context.ts +4 -2
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +86 -89
  168. package/src/router/match-middleware/cache-lookup.ts +295 -49
  169. package/src/router/match-middleware/cache-store.ts +56 -13
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -22
  171. package/src/router/match-middleware/segment-resolution.ts +20 -9
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +44 -21
  174. package/src/router/metrics.ts +240 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +327 -369
  178. package/src/router/pattern-matching.ts +169 -31
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +105 -14
  182. package/src/router/router-context.ts +40 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +677 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +199 -0
  189. package/src/router/segment-resolution/revalidation.ts +1296 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1354
  192. package/src/router/segment-wrappers.ts +291 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +96 -29
  197. package/src/router/types.ts +15 -9
  198. package/src/router.ts +642 -2366
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +639 -1027
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +0 -20
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +237 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +38 -11
  215. package/src/search-params.ts +66 -54
  216. package/src/segment-system.tsx +165 -17
  217. package/src/server/context.ts +237 -54
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +11 -6
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +438 -71
  223. package/src/server.ts +26 -164
  224. package/src/ssr/index.tsx +101 -31
  225. package/src/static-handler.ts +22 -4
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +773 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +109 -0
  241. package/src/types/segments.ts +150 -0
  242. package/src/types.ts +1 -1795
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -1323
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +108 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -2259
  261. package/src/vite/plugin-types.ts +48 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
  266. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +266 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +445 -0
  280. package/src/vite/router-discovery.ts +777 -0
  281. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -43
  289. package/dist/vite/index.named-routes.gen.ts +0 -103
  290. package/src/browser/lru-cache.ts +0 -69
  291. package/src/browser/request-controller.ts +0 -164
  292. package/src/cache/memory-store.ts +0 -253
  293. package/src/href-context.ts +0 -33
  294. package/src/router.gen.ts +0 -6
  295. package/src/static-handler.gen.ts +0 -5
  296. package/src/urls.gen.ts +0 -8
  297. package/src/vite/expose-internal-ids.ts +0 -1167
  298. /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,33 +82,29 @@
90
82
  * ISOLATION FROM RESPONSE
91
83
  * =======================
92
84
  *
93
- * The background revalidation creates a fresh handleStore:
94
- *
95
- * requestCtx._handleStore = createHandleStore();
96
- *
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
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
101
90
  *
91
+ * This matches the proactive caching pattern in cache-store.ts.
102
92
  *
103
- * FULL VS PARTIAL REVALIDATION
104
- * ============================
105
93
  *
106
- * Full Match (document request):
107
- * - Simple resolveAllSegments()
108
- * - No need to compare with previous state
94
+ * FRESH RESOLUTION (NO REVALIDATION)
95
+ * ===================================
109
96
  *
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";
119
- import { debugLog, debugWarn } from "../logging.js";
106
+ import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
107
+ import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
120
108
 
121
109
  /**
122
110
  * Creates background revalidation middleware
@@ -128,10 +116,10 @@ import { debugLog, debugWarn } from "../logging.js";
128
116
  */
129
117
  export function withBackgroundRevalidation<TEnv>(
130
118
  ctx: MatchContext<TEnv>,
131
- state: MatchPipelineState
119
+ state: MatchPipelineState,
132
120
  ): GeneratorMiddleware<ResolvedSegment> {
133
121
  return async function* (
134
- source: AsyncGenerator<ResolvedSegment>
122
+ source: AsyncGenerator<ResolvedSegment>,
135
123
  ): AsyncGenerator<ResolvedSegment> {
136
124
  // Pass through all segments unchanged
137
125
  for await (const segment of source) {
@@ -148,95 +136,104 @@ export function withBackgroundRevalidation<TEnv>(
148
136
  const {
149
137
  getRequestContext,
150
138
  createHandleStore,
151
- resolveAllSegmentsWithRevalidation,
139
+ createHandlerContext,
140
+ setupLoaderAccess,
152
141
  resolveAllSegments,
153
142
  resolveInterceptEntry,
154
143
  } = getRouterContext<TEnv>();
155
144
 
156
145
  const requestCtx = getRequestContext();
157
146
  const cacheScope = ctx.cacheScope;
147
+ const reqId = INTERNAL_RANGO_DEBUG
148
+ ? getOrCreateRequestId(ctx.request)
149
+ : undefined;
158
150
 
159
151
  requestCtx?.waitUntil(async () => {
152
+ const start = performance.now();
160
153
  debugLog("backgroundRevalidation", "revalidating stale route", {
161
154
  pathname: ctx.pathname,
162
155
  fullMatch: ctx.isFullMatch,
163
156
  });
164
- try {
165
- // Create a fresh handleStore for background revalidation
166
- // to avoid polluting the current response's handle stream
167
- if (requestCtx) {
168
- requestCtx._handleStore = createHandleStore();
169
- }
170
157
 
171
- let freshSegments: ResolvedSegment[];
158
+ // Save and replace handleStore to avoid polluting the response stream.
159
+ // Restore in finally (same pattern as proactive caching in cache-store).
160
+ const originalHandleStore = requestCtx._handleStore;
161
+ requestCtx._handleStore = createHandleStore();
172
162
 
173
- if (ctx.isFullMatch) {
174
- // Full match (document request) - simple resolution
175
- freshSegments = await resolveAllSegments(
176
- ctx.entries,
177
- ctx.routeKey,
178
- ctx.matched.params,
179
- ctx.handlerContext,
180
- ctx.loaderPromises
181
- );
182
- } else {
183
- // Partial match (navigation) - resolution with revalidation
184
- const freshResult = await resolveAllSegmentsWithRevalidation(
163
+ try {
164
+ // Create fresh handler context and loader promises to avoid
165
+ // reusing memoized results from the foreground pass
166
+ const freshHandlerContext = createHandlerContext(
167
+ ctx.matched.params,
168
+ ctx.request,
169
+ ctx.url.searchParams,
170
+ ctx.pathname,
171
+ ctx.url,
172
+ ctx.env,
173
+ ctx.routeMap,
174
+ ctx.matched.routeKey,
175
+ ctx.matched.responseType,
176
+ ctx.matched.pt === true,
177
+ );
178
+ const freshLoaderPromises = new Map<string, Promise<any>>();
179
+ setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
180
+
181
+ // Resolve all segments fresh (without revalidation logic)
182
+ // to ensure complete components for caching
183
+ const freshSegments = await ctx.Store.run(() =>
184
+ resolveAllSegments(
185
185
  ctx.entries,
186
186
  ctx.routeKey,
187
187
  ctx.matched.params,
188
- ctx.handlerContext,
189
- ctx.clientSegmentSet,
190
- ctx.prevParams,
191
- ctx.request,
192
- ctx.prevUrl,
193
- ctx.url,
194
- ctx.loaderPromises,
195
- ctx.actionContext,
196
- ctx.interceptResult,
197
- ctx.localRouteName,
198
- ctx.pathname
199
- );
200
-
201
- freshSegments = freshResult.segments;
188
+ freshHandlerContext,
189
+ freshLoaderPromises,
190
+ ),
191
+ );
202
192
 
203
- // For intercept revalidation, also resolve fresh intercept segments
204
- if (ctx.interceptResult) {
205
- const freshInterceptSegments = await resolveInterceptEntry(
206
- ctx.interceptResult.intercept,
207
- ctx.interceptResult.entry,
193
+ // Also resolve intercept segments fresh if applicable
194
+ let freshInterceptSegments: ResolvedSegment[] = [];
195
+ if (ctx.interceptResult) {
196
+ freshInterceptSegments = await ctx.Store.run(() =>
197
+ resolveInterceptEntry(
198
+ ctx.interceptResult!.intercept,
199
+ ctx.interceptResult!.entry,
208
200
  ctx.matched.params,
209
- ctx.handlerContext,
201
+ freshHandlerContext,
210
202
  true,
211
- {
212
- clientSegmentIds: ctx.clientSegmentSet,
213
- prevParams: ctx.prevParams,
214
- request: ctx.request,
215
- prevUrl: ctx.prevUrl,
216
- nextUrl: ctx.url,
217
- routeKey: ctx.routeKey,
218
- actionContext: ctx.actionContext,
219
- stale: false,
220
- }
221
- );
222
- freshSegments = [...freshSegments, ...freshInterceptSegments];
223
- }
203
+ ),
204
+ );
224
205
  }
225
206
 
207
+ const completeSegments = [...freshSegments, ...freshInterceptSegments];
208
+ requestCtx._handleStore.seal();
226
209
  await cacheScope.cacheRoute(
227
210
  ctx.pathname,
228
211
  ctx.matched.params,
229
- freshSegments,
230
- ctx.isIntercept
212
+ completeSegments,
213
+ ctx.isIntercept,
231
214
  );
215
+ if (INTERNAL_RANGO_DEBUG) {
216
+ const dur = performance.now() - start;
217
+ console.log(
218
+ `[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
219
+ );
220
+ }
232
221
  debugLog("backgroundRevalidation", "revalidation complete", {
233
222
  pathname: ctx.pathname,
234
223
  });
235
224
  } catch (error) {
225
+ if (INTERNAL_RANGO_DEBUG) {
226
+ const dur = performance.now() - start;
227
+ console.log(
228
+ `[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
229
+ );
230
+ }
236
231
  debugWarn("backgroundRevalidation", "revalidation failed", {
237
232
  pathname: ctx.pathname,
238
233
  error: String(error),
239
234
  });
235
+ } finally {
236
+ requestCtx._handleStore = originalHandleStore;
240
237
  }
241
238
  });
242
239
  };