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

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 (174) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +126 -38
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1171 -461
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +19 -16
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/cache-guide/SKILL.md +32 -0
  9. package/skills/caching/SKILL.md +45 -4
  10. package/skills/handler-use/SKILL.md +362 -0
  11. package/skills/hooks/SKILL.md +28 -20
  12. package/skills/intercept/SKILL.md +20 -0
  13. package/skills/layout/SKILL.md +22 -0
  14. package/skills/links/SKILL.md +91 -17
  15. package/skills/loader/SKILL.md +88 -45
  16. package/skills/middleware/SKILL.md +34 -3
  17. package/skills/migrate-nextjs/SKILL.md +560 -0
  18. package/skills/migrate-react-router/SKILL.md +765 -0
  19. package/skills/parallel/SKILL.md +185 -0
  20. package/skills/prerender/SKILL.md +110 -68
  21. package/skills/rango/SKILL.md +24 -22
  22. package/skills/response-routes/SKILL.md +8 -0
  23. package/skills/route/SKILL.md +55 -0
  24. package/skills/router-setup/SKILL.md +87 -2
  25. package/skills/streams-and-websockets/SKILL.md +283 -0
  26. package/skills/typesafety/SKILL.md +13 -1
  27. package/src/__internal.ts +1 -1
  28. package/src/browser/app-shell.ts +52 -0
  29. package/src/browser/app-version.ts +14 -0
  30. package/src/browser/event-controller.ts +5 -0
  31. package/src/browser/navigation-bridge.ts +90 -16
  32. package/src/browser/navigation-client.ts +167 -59
  33. package/src/browser/navigation-store.ts +68 -9
  34. package/src/browser/navigation-transaction.ts +11 -9
  35. package/src/browser/partial-update.ts +113 -17
  36. package/src/browser/prefetch/cache.ts +184 -16
  37. package/src/browser/prefetch/fetch.ts +180 -33
  38. package/src/browser/prefetch/policy.ts +6 -0
  39. package/src/browser/prefetch/queue.ts +123 -20
  40. package/src/browser/prefetch/resource-ready.ts +77 -0
  41. package/src/browser/rango-state.ts +53 -13
  42. package/src/browser/react/Link.tsx +81 -9
  43. package/src/browser/react/NavigationProvider.tsx +89 -14
  44. package/src/browser/react/context.ts +7 -2
  45. package/src/browser/react/use-handle.ts +9 -58
  46. package/src/browser/react/use-navigation.ts +22 -2
  47. package/src/browser/react/use-params.ts +11 -1
  48. package/src/browser/react/use-router.ts +29 -9
  49. package/src/browser/rsc-router.tsx +168 -65
  50. package/src/browser/scroll-restoration.ts +41 -42
  51. package/src/browser/segment-reconciler.ts +36 -9
  52. package/src/browser/server-action-bridge.ts +8 -6
  53. package/src/browser/types.ts +49 -5
  54. package/src/build/generate-manifest.ts +6 -6
  55. package/src/build/generate-route-types.ts +3 -0
  56. package/src/build/route-trie.ts +50 -24
  57. package/src/build/route-types/include-resolution.ts +8 -1
  58. package/src/build/route-types/router-processing.ts +223 -74
  59. package/src/build/route-types/scan-filter.ts +8 -1
  60. package/src/cache/cache-runtime.ts +15 -11
  61. package/src/cache/cache-scope.ts +48 -7
  62. package/src/cache/cf/cf-cache-store.ts +455 -15
  63. package/src/cache/cf/index.ts +5 -1
  64. package/src/cache/document-cache.ts +17 -7
  65. package/src/cache/index.ts +1 -0
  66. package/src/cache/taint.ts +55 -0
  67. package/src/client.tsx +84 -230
  68. package/src/context-var.ts +72 -2
  69. package/src/debug.ts +2 -2
  70. package/src/handle.ts +40 -0
  71. package/src/index.rsc.ts +6 -1
  72. package/src/index.ts +49 -6
  73. package/src/outlet-context.ts +1 -1
  74. package/src/prerender/store.ts +5 -4
  75. package/src/prerender.ts +138 -77
  76. package/src/response-utils.ts +28 -0
  77. package/src/reverse.ts +27 -2
  78. package/src/route-definition/dsl-helpers.ts +240 -40
  79. package/src/route-definition/helpers-types.ts +67 -19
  80. package/src/route-definition/index.ts +3 -0
  81. package/src/route-definition/redirect.ts +11 -3
  82. package/src/route-definition/resolve-handler-use.ts +155 -0
  83. package/src/route-map-builder.ts +7 -1
  84. package/src/route-types.ts +18 -0
  85. package/src/router/content-negotiation.ts +100 -1
  86. package/src/router/find-match.ts +4 -2
  87. package/src/router/handler-context.ts +101 -25
  88. package/src/router/intercept-resolution.ts +11 -4
  89. package/src/router/lazy-includes.ts +10 -7
  90. package/src/router/loader-resolution.ts +159 -21
  91. package/src/router/logging.ts +5 -2
  92. package/src/router/manifest.ts +31 -16
  93. package/src/router/match-api.ts +127 -192
  94. package/src/router/match-middleware/background-revalidation.ts +30 -2
  95. package/src/router/match-middleware/cache-lookup.ts +94 -17
  96. package/src/router/match-middleware/cache-store.ts +53 -10
  97. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  98. package/src/router/match-middleware/segment-resolution.ts +61 -5
  99. package/src/router/match-result.ts +104 -10
  100. package/src/router/metrics.ts +6 -1
  101. package/src/router/middleware-types.ts +8 -30
  102. package/src/router/middleware.ts +36 -10
  103. package/src/router/navigation-snapshot.ts +182 -0
  104. package/src/router/pattern-matching.ts +60 -9
  105. package/src/router/prerender-match.ts +110 -10
  106. package/src/router/preview-match.ts +30 -102
  107. package/src/router/request-classification.ts +310 -0
  108. package/src/router/route-snapshot.ts +245 -0
  109. package/src/router/router-context.ts +6 -1
  110. package/src/router/router-interfaces.ts +36 -4
  111. package/src/router/router-options.ts +37 -11
  112. package/src/router/segment-resolution/fresh.ts +198 -20
  113. package/src/router/segment-resolution/helpers.ts +29 -24
  114. package/src/router/segment-resolution/loader-cache.ts +1 -0
  115. package/src/router/segment-resolution/revalidation.ts +438 -300
  116. package/src/router/segment-wrappers.ts +2 -0
  117. package/src/router/trie-matching.ts +10 -4
  118. package/src/router/types.ts +1 -0
  119. package/src/router/url-params.ts +49 -0
  120. package/src/router.ts +60 -8
  121. package/src/rsc/handler.ts +478 -374
  122. package/src/rsc/helpers.ts +69 -41
  123. package/src/rsc/loader-fetch.ts +23 -3
  124. package/src/rsc/manifest-init.ts +5 -1
  125. package/src/rsc/progressive-enhancement.ts +16 -2
  126. package/src/rsc/response-route-handler.ts +14 -1
  127. package/src/rsc/rsc-rendering.ts +19 -1
  128. package/src/rsc/server-action.ts +10 -0
  129. package/src/rsc/ssr-setup.ts +2 -2
  130. package/src/rsc/types.ts +9 -1
  131. package/src/segment-content-promise.ts +67 -0
  132. package/src/segment-loader-promise.ts +122 -0
  133. package/src/segment-system.tsx +109 -23
  134. package/src/server/context.ts +166 -17
  135. package/src/server/handle-store.ts +19 -0
  136. package/src/server/loader-registry.ts +9 -8
  137. package/src/server/request-context.ts +194 -60
  138. package/src/ssr/index.tsx +4 -0
  139. package/src/static-handler.ts +18 -6
  140. package/src/types/cache-types.ts +4 -4
  141. package/src/types/handler-context.ts +137 -65
  142. package/src/types/loader-types.ts +41 -15
  143. package/src/types/request-scope.ts +126 -0
  144. package/src/types/route-entry.ts +19 -1
  145. package/src/types/segments.ts +2 -0
  146. package/src/urls/include-helper.ts +24 -14
  147. package/src/urls/path-helper-types.ts +39 -6
  148. package/src/urls/path-helper.ts +48 -13
  149. package/src/urls/pattern-types.ts +12 -0
  150. package/src/urls/response-types.ts +18 -16
  151. package/src/use-loader.tsx +77 -5
  152. package/src/vite/debug.ts +55 -0
  153. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  154. package/src/vite/discovery/discover-routers.ts +5 -1
  155. package/src/vite/discovery/prerender-collection.ts +128 -74
  156. package/src/vite/discovery/state.ts +13 -6
  157. package/src/vite/index.ts +4 -0
  158. package/src/vite/plugin-types.ts +51 -79
  159. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  160. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  161. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  162. package/src/vite/plugins/expose-action-id.ts +1 -3
  163. package/src/vite/plugins/expose-id-utils.ts +12 -0
  164. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  165. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  166. package/src/vite/plugins/performance-tracks.ts +86 -0
  167. package/src/vite/plugins/refresh-cmd.ts +88 -26
  168. package/src/vite/plugins/version-plugin.ts +13 -1
  169. package/src/vite/rango.ts +204 -217
  170. package/src/vite/router-discovery.ts +335 -64
  171. package/src/vite/utils/banner.ts +4 -4
  172. package/src/vite/utils/package-resolution.ts +41 -1
  173. package/src/vite/utils/prerender-utils.ts +37 -5
  174. package/src/vite/utils/shared-utils.ts +3 -2
@@ -22,10 +22,10 @@ import { collectRouteMiddleware } from "./middleware.js";
22
22
  import { traverseBack } from "./pattern-matching.js";
23
23
  import { DefaultErrorFallback } from "../default-error-boundary.js";
24
24
  import {
25
- EntryData,
26
- LoaderEntry,
25
+ type EntryData,
26
+ type LoaderEntry,
27
27
  getContext,
28
- InterceptSelectorContext,
28
+ type InterceptSelectorContext,
29
29
  } from "../server/context";
30
30
  import type { ErrorBoundaryHandler, ErrorInfo, MatchResult } from "../types";
31
31
  import type { ReactNode } from "react";
@@ -36,7 +36,14 @@ import {
36
36
  setRequestContextPrevRouteKey,
37
37
  } from "../server/request-context.js";
38
38
  import { isAutoGeneratedRouteName } from "../route-name.js";
39
+ import type { DefaultRouteName } from "../types/global-namespace.js";
39
40
  import { debugLog, debugWarn } from "./logging.js";
41
+ import {
42
+ resolveRoute,
43
+ ensureFullRouteSnapshot,
44
+ type RouteSnapshot,
45
+ } from "./route-snapshot.js";
46
+ import { resolveNavigation } from "./navigation-snapshot.js";
40
47
 
41
48
  /**
42
49
  * Create match context for full requests (document/SSR).
@@ -52,57 +59,36 @@ export async function createMatchContextForFull<TEnv>(
52
59
 
53
60
  const metricsStore = deps.getMetricsStore();
54
61
 
55
- const routeMatchStart = metricsStore ? performance.now() : 0;
56
- const matched = deps.findMatch(pathname, metricsStore);
57
- if (metricsStore) {
58
- metricsStore.metrics.push({
59
- label: "route-matching",
60
- duration: performance.now() - routeMatchStart,
61
- startTime: routeMatchStart - metricsStore.requestStart,
62
- });
63
- }
62
+ // Full renders always resolve fresh with isSSR: true because loadManifest
63
+ // keys its cache on isSSR and stamps Store.isSSR for downstream behavior.
64
+ const result = await resolveRoute<TEnv>(pathname, {
65
+ findMatch: (p) => deps.findMatch(p, metricsStore),
66
+ metricsStore,
67
+ isSSR: true,
68
+ });
64
69
 
65
- if (!matched) {
70
+ if (!result) {
66
71
  throw new RouteNotFoundError(`No route matched for ${pathname}`, {
67
72
  cause: { pathname, method: request.method },
68
73
  });
69
74
  }
70
75
 
71
- if (matched.redirectTo) {
76
+ if (result.type === "redirect") {
72
77
  return {
73
78
  type: "redirect",
74
- redirectUrl: matched.redirectTo + url.search,
79
+ redirectUrl: result.redirectTo + url.search,
75
80
  };
76
81
  }
77
82
 
78
- const manifestStart = metricsStore ? performance.now() : 0;
79
- const manifestEntry = await loadManifest(
80
- matched.entry,
81
- matched.routeKey,
82
- pathname,
83
- metricsStore,
84
- true,
85
- );
86
- if (metricsStore) {
87
- metricsStore.metrics.push({
88
- label: "manifest-loading",
89
- duration: performance.now() - manifestStart,
90
- startTime: manifestStart - metricsStore.requestStart,
91
- });
92
- }
83
+ const snapshot = result.snapshot;
93
84
 
94
- if (
95
- manifestEntry.type === "route" &&
96
- manifestEntry.prerenderDef?.options?.passthrough === true
97
- ) {
85
+ const { matched } = snapshot;
86
+
87
+ // Backward compat: downstream middleware reads matched.pt
88
+ if (snapshot.isPassthrough) {
98
89
  matched.pt = true;
99
90
  }
100
91
 
101
- const routeMiddleware = collectRouteMiddleware(
102
- traverseBack(manifestEntry),
103
- matched.params,
104
- );
105
-
106
92
  // Clean URL without internal _rsc* params for userland access
107
93
  const cleanUrl = stripInternalParams(url);
108
94
 
@@ -134,14 +120,6 @@ export async function createMatchContextForFull<TEnv>(
134
120
  Store.metrics = metricsStore;
135
121
  }
136
122
 
137
- const entries = [...traverseBack(manifestEntry)];
138
- let cacheScope: CacheScope | null = null;
139
- for (const entry of entries) {
140
- if (entry.cache) {
141
- cacheScope = createCacheScope(entry.cache, cacheScope);
142
- }
143
- }
144
-
145
123
  return {
146
124
  request,
147
125
  url: cleanUrl,
@@ -154,12 +132,10 @@ export async function createMatchContextForFull<TEnv>(
154
132
  prevParams: {},
155
133
  prevMatch: null,
156
134
  matched,
157
- manifestEntry,
158
- entries,
135
+ manifestEntry: snapshot.manifestEntry,
136
+ entries: snapshot.entries,
159
137
  routeKey: matched.routeKey,
160
- localRouteName: matched.routeKey.includes(".")
161
- ? matched.routeKey.split(".").pop()!
162
- : matched.routeKey,
138
+ localRouteName: snapshot.localRouteName,
163
139
  handlerContext,
164
140
  loaderPromises,
165
141
  routeMap: deps.getRouteMap(),
@@ -175,16 +151,16 @@ export async function createMatchContextForFull<TEnv>(
175
151
  segments: { path: [], ids: [] },
176
152
  toRouteName:
177
153
  matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
178
- ? matched.routeKey
154
+ ? (matched.routeKey as DefaultRouteName)
179
155
  : undefined,
180
156
  },
181
157
  isSameRouteNavigation: false,
182
158
  interceptResult: null,
183
- cacheScope,
159
+ cacheScope: snapshot.cacheScope,
184
160
  isIntercept: false,
185
161
  actionContext: undefined,
186
162
  isAction: false,
187
- routeMiddleware,
163
+ routeMiddleware: snapshot.routeMiddleware,
188
164
  isFullMatch: true,
189
165
  };
190
166
  }
@@ -204,103 +180,85 @@ export async function createMatchContextForPartial<TEnv>(
204
180
 
205
181
  const metricsStore = deps.getMetricsStore();
206
182
 
207
- const clientSegmentIds =
208
- url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
209
- const stale = url.searchParams.get("_rsc_stale") === "true";
210
- const previousUrl =
211
- request.headers.get("X-RSC-Router-Client-Path") ||
212
- request.headers.get("Referer");
213
- const interceptSourceUrl = request.headers.get(
214
- "X-RSC-Router-Intercept-Source",
215
- );
183
+ const isHmr = !!request.headers.get("X-RSC-HMR");
216
184
 
217
185
  // HMR: clear manifest cache so stale handler references are discarded
218
- if (request.headers.get("X-RSC-HMR")) {
186
+ if (isHmr) {
219
187
  clearManifestCache();
220
188
  }
221
189
 
222
- if (!previousUrl) {
223
- return null;
224
- }
225
-
226
- let prevUrl: URL;
227
- try {
228
- prevUrl = new URL(previousUrl, url.origin);
229
- } catch {
230
- return null;
231
- }
190
+ // Reuse the classified snapshot when available and not invalidated by HMR.
191
+ // classifyRequest already called resolveRoute(lite) with isSSR=false, which
192
+ // matches the partial path. On HMR, discard to pick up manifest changes.
193
+ const classifiedRoute = isHmr
194
+ ? undefined
195
+ : getRequestContext()?._classifiedRoute;
196
+
197
+ // Time route matching. On the reuse path, only nav findMatch calls are new
198
+ // (current-route findMatch and manifest-loading were already timed during
199
+ // classifyRequest via its metricsStore). On the fresh path, all findMatch
200
+ // calls are measured together.
201
+ const routeMatchStart = metricsStore ? performance.now() : 0;
232
202
 
233
- let interceptContextUrl: URL;
234
- try {
235
- interceptContextUrl = interceptSourceUrl
236
- ? new URL(interceptSourceUrl, url.origin)
237
- : prevUrl;
238
- } catch {
239
- interceptContextUrl = prevUrl;
240
- }
203
+ let snapshot: RouteSnapshot<TEnv>;
204
+ if (classifiedRoute && classifiedRoute.manifestEntry) {
205
+ snapshot = ensureFullRouteSnapshot(classifiedRoute);
206
+ } else {
207
+ const result = await resolveRoute<TEnv>(pathname, {
208
+ findMatch: (p) => deps.findMatch(p, metricsStore),
209
+ metricsStore,
210
+ isSSR: false,
211
+ skipRouteMatchMetric: true,
212
+ });
241
213
 
242
- const routeMatchStart = metricsStore ? performance.now() : 0;
243
- const prevMatch = deps.findMatch(prevUrl.pathname);
244
- const prevParams = prevMatch?.params || {};
245
- const interceptContextMatch = interceptSourceUrl
246
- ? deps.findMatch(interceptContextUrl.pathname)
247
- : prevMatch;
214
+ if (!result) {
215
+ throw new RouteNotFoundError(`No route matched for ${pathname}`, {
216
+ cause: { pathname, method: request.method },
217
+ });
218
+ }
248
219
 
249
- const matched = deps.findMatch(pathname, metricsStore);
220
+ if (result.type === "redirect") {
221
+ return null;
222
+ }
250
223
 
251
- if (metricsStore) {
252
- metricsStore.metrics.push({
253
- label: "route-matching",
254
- duration: performance.now() - routeMatchStart,
255
- startTime: routeMatchStart - metricsStore.requestStart,
256
- });
224
+ snapshot = result.snapshot;
257
225
  }
258
226
 
259
- if (!matched) {
260
- throw new RouteNotFoundError(`No route matched for ${pathname}`, {
261
- cause: { pathname, method: request.method, previousUrl },
262
- });
263
- }
227
+ const { matched } = snapshot;
264
228
 
265
- if (matched.redirectTo) {
266
- return null;
229
+ // Backward compat: downstream middleware reads matched.pt
230
+ if (snapshot.isPassthrough) {
231
+ matched.pt = true;
267
232
  }
268
233
 
269
- if (prevMatch && prevMatch.entry !== matched.entry && !matched.pr) {
270
- debugLog("matchPartial", "route group changed", {
271
- from: prevMatch.routeKey,
272
- to: matched.routeKey,
273
- });
234
+ // Navigation state (prev + intercept-source findMatch calls)
235
+ const nav = resolveNavigation(request, url, matched.routeKey, {
236
+ findMatch: deps.findMatch,
237
+ });
238
+ if (!nav) {
239
+ return null;
274
240
  }
275
241
 
276
- const manifestStart = metricsStore ? performance.now() : 0;
277
- const manifestEntry = await loadManifest(
278
- matched.entry,
279
- matched.routeKey,
280
- pathname,
281
- metricsStore,
282
- false,
283
- );
242
+ // Push route-matching metric. On the fresh path this covers all findMatch
243
+ // calls (current + prev + intercept-source). On the reuse path, current-route
244
+ // findMatch was already timed during classification, so this only covers
245
+ // the nav lookups (prev + intercept-source).
284
246
  if (metricsStore) {
247
+ const isReuse = !!classifiedRoute;
285
248
  metricsStore.metrics.push({
286
- label: "manifest-loading",
287
- duration: performance.now() - manifestStart,
288
- startTime: manifestStart - metricsStore.requestStart,
249
+ label: isReuse ? "route-matching:nav" : "route-matching",
250
+ duration: performance.now() - routeMatchStart,
251
+ startTime: routeMatchStart - metricsStore.requestStart,
289
252
  });
290
253
  }
291
254
 
292
- if (
293
- manifestEntry.type === "route" &&
294
- manifestEntry.prerenderDef?.options?.passthrough === true
295
- ) {
296
- matched.pt = true;
255
+ if (nav.prevMatch && nav.prevMatch.entry !== matched.entry && !matched.pr) {
256
+ debugLog("matchPartial", "route group changed", {
257
+ from: nav.prevMatch.routeKey,
258
+ to: matched.routeKey,
259
+ });
297
260
  }
298
261
 
299
- const routeMiddleware = collectRouteMiddleware(
300
- traverseBack(manifestEntry),
301
- matched.params,
302
- );
303
-
304
262
  // Clean URL without internal _rsc* params for userland access
305
263
  const cleanUrl = stripInternalParams(url);
306
264
 
@@ -317,9 +275,8 @@ export async function createMatchContextForPartial<TEnv>(
317
275
  matched.pt === true,
318
276
  );
319
277
 
320
- const clientSegmentSet = new Set(clientSegmentIds);
321
278
  debugLog("matchPartial", "client segments", {
322
- segments: Array.from(clientSegmentSet),
279
+ segments: Array.from(nav.clientSegmentSet),
323
280
  });
324
281
 
325
282
  const loaderPromises = new Map<string, Promise<any>>();
@@ -337,100 +294,78 @@ export async function createMatchContextForPartial<TEnv>(
337
294
  Store.metrics = metricsStore;
338
295
  }
339
296
 
340
- const isSameRouteNavigation = !!(
341
- interceptContextMatch && interceptContextMatch.routeKey === matched.routeKey
342
- );
343
-
344
- if (interceptSourceUrl) {
297
+ if (nav.hasInterceptSource) {
345
298
  debugLog("matchPartial.intercept", "intercept context detected", {
346
299
  currentUrl: pathname,
347
- interceptSource: interceptSourceUrl,
348
- contextRoute: interceptContextMatch?.routeKey,
300
+ interceptSource: nav.interceptContextUrl.href,
301
+ contextRoute: nav.interceptContextMatch?.routeKey,
349
302
  currentRoute: matched.routeKey,
350
- sameRouteNavigation: isSameRouteNavigation,
303
+ sameRouteNavigation: nav.isSameRouteNavigation,
351
304
  });
352
305
  }
353
306
 
354
- const localRouteName = matched.routeKey.includes(".")
355
- ? matched.routeKey.split(".").pop()!
356
- : matched.routeKey;
357
-
358
- const filteredSegmentIds = clientSegmentIds.filter((id) => {
359
- if (id.includes(".@")) return false;
360
- if (/D\d+\./.test(id)) return false;
361
- return true;
362
- });
363
- const effectiveFromUrl = interceptSourceUrl ? interceptContextUrl : prevUrl;
364
- const effectiveFromMatch = interceptSourceUrl
365
- ? interceptContextMatch
366
- : prevMatch;
367
-
368
307
  // Store previous route key on the request context for revalidation
369
308
  // fromRouteName. Uses effectiveFromMatch so intercept-source navigations
370
309
  // see the intercept origin route, not the plain previous URL route.
371
- setRequestContextPrevRouteKey(effectiveFromMatch?.routeKey);
310
+ setRequestContextPrevRouteKey(nav.effectiveFromMatch?.routeKey);
372
311
 
373
312
  const interceptSelectorContext: InterceptSelectorContext = {
374
- from: effectiveFromUrl,
313
+ from: nav.effectiveFromUrl,
375
314
  to: cleanUrl,
376
315
  params: matched.params,
377
316
  request,
378
317
  env,
379
318
  segments: {
380
- path: effectiveFromUrl.pathname.split("/").filter(Boolean),
381
- ids: filteredSegmentIds,
319
+ path: nav.effectiveFromUrl.pathname.split("/").filter(Boolean),
320
+ ids: nav.filteredSegmentIds,
382
321
  },
383
322
  fromRouteName:
384
- effectiveFromMatch?.routeKey &&
385
- !isAutoGeneratedRouteName(effectiveFromMatch.routeKey)
386
- ? effectiveFromMatch.routeKey
323
+ nav.effectiveFromMatch?.routeKey &&
324
+ !isAutoGeneratedRouteName(nav.effectiveFromMatch.routeKey)
325
+ ? (nav.effectiveFromMatch.routeKey as DefaultRouteName)
387
326
  : undefined,
388
327
  toRouteName:
389
328
  matched.routeKey && !isAutoGeneratedRouteName(matched.routeKey)
390
- ? matched.routeKey
329
+ ? (matched.routeKey as DefaultRouteName)
391
330
  : undefined,
392
331
  };
393
332
  const isAction = !!actionContext;
394
333
 
395
- const clientHasInterceptSegments = [...clientSegmentSet].some((id) =>
334
+ const clientHasInterceptSegments = [...nav.clientSegmentSet].some((id) =>
396
335
  id.includes(".@"),
397
336
  );
398
337
  const skipInterceptForAction = isAction && !clientHasInterceptSegments;
399
338
  const interceptResult =
400
- isSameRouteNavigation || skipInterceptForAction
339
+ nav.isSameRouteNavigation || skipInterceptForAction
401
340
  ? null
402
341
  : findInterceptForRoute(
403
342
  matched.routeKey,
404
- manifestEntry.parent,
343
+ snapshot.manifestEntry.parent,
405
344
  interceptSelectorContext,
406
345
  isAction,
407
346
  ) ||
408
- (localRouteName !== matched.routeKey
347
+ (snapshot.localRouteName !== matched.routeKey
409
348
  ? findInterceptForRoute(
410
- localRouteName,
411
- manifestEntry.parent,
349
+ snapshot.localRouteName,
350
+ snapshot.manifestEntry.parent,
412
351
  interceptSelectorContext,
413
352
  isAction,
414
353
  )
415
354
  : null);
416
355
 
356
+ // Make a mutable copy of clientSegmentSet so we can delete entries
357
+ // for same-route navigation forcing
358
+ const clientSegmentSet = new Set(nav.clientSegmentSet);
359
+
417
360
  if (
418
- isSameRouteNavigation &&
419
- manifestEntry.type === "route" &&
420
- interceptSourceUrl
361
+ nav.isSameRouteNavigation &&
362
+ snapshot.manifestEntry.type === "route" &&
363
+ nav.hasInterceptSource
421
364
  ) {
422
365
  debugLog("matchPartial.intercept", "forcing route segment render", {
423
- segmentId: manifestEntry.shortCode,
366
+ segmentId: snapshot.manifestEntry.shortCode,
424
367
  });
425
- clientSegmentSet.delete(manifestEntry.shortCode);
426
- }
427
-
428
- const entries = [...traverseBack(manifestEntry)];
429
- let cacheScope: CacheScope | null = null;
430
- for (const entry of entries) {
431
- if (entry.cache) {
432
- cacheScope = createCacheScope(entry.cache, cacheScope);
433
- }
368
+ clientSegmentSet.delete(snapshot.manifestEntry.shortCode);
434
369
  }
435
370
 
436
371
  const isIntercept = !!interceptResult;
@@ -440,31 +375,31 @@ export async function createMatchContextForPartial<TEnv>(
440
375
  url: cleanUrl,
441
376
  pathname,
442
377
  env,
443
- clientSegmentIds,
378
+ clientSegmentIds: nav.clientSegmentIds,
444
379
  clientSegmentSet,
445
- stale,
446
- prevUrl,
447
- prevParams,
448
- prevMatch,
380
+ stale: nav.stale,
381
+ prevUrl: nav.prevUrl,
382
+ prevParams: nav.prevParams,
383
+ prevMatch: nav.prevMatch,
449
384
  matched,
450
- manifestEntry,
451
- entries,
385
+ manifestEntry: snapshot.manifestEntry,
386
+ entries: snapshot.entries,
452
387
  routeKey: matched.routeKey,
453
- localRouteName,
388
+ localRouteName: snapshot.localRouteName,
454
389
  handlerContext,
455
390
  loaderPromises,
456
391
  routeMap: deps.getRouteMap(),
457
392
  metricsStore,
458
393
  Store,
459
- interceptContextMatch,
394
+ interceptContextMatch: nav.interceptContextMatch,
460
395
  interceptSelectorContext,
461
- isSameRouteNavigation,
396
+ isSameRouteNavigation: nav.isSameRouteNavigation,
462
397
  interceptResult,
463
- cacheScope,
398
+ cacheScope: snapshot.cacheScope,
464
399
  isIntercept,
465
400
  actionContext,
466
401
  isAction,
467
- routeMiddleware,
402
+ routeMiddleware: snapshot.routeMiddleware,
468
403
  isFullMatch: false,
469
404
  };
470
405
  }
@@ -103,7 +103,8 @@ import type { ResolvedSegment } from "../../types.js";
103
103
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
104
104
  import { getRouterContext } from "../router-context.js";
105
105
  import type { GeneratorMiddleware } from "./cache-lookup.js";
106
- import { debugLog, debugWarn } from "../logging.js";
106
+ import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
107
+ import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
107
108
 
108
109
  /**
109
110
  * Creates background revalidation middleware
@@ -143,8 +144,19 @@ export function withBackgroundRevalidation<TEnv>(
143
144
 
144
145
  const requestCtx = getRequestContext();
145
146
  const cacheScope = ctx.cacheScope;
147
+ const reqId = INTERNAL_RANGO_DEBUG
148
+ ? getOrCreateRequestId(ctx.request)
149
+ : undefined;
146
150
 
147
151
  requestCtx?.waitUntil(async () => {
152
+ // Prevent background metrics from polluting foreground timeline.
153
+ // The foreground uses its own metricsStore reference directly (via
154
+ // appendMetric), so nulling Store.metrics only affects track() calls
155
+ // inside this background Store.run() scope.
156
+ const savedMetrics = ctx.Store.metrics;
157
+ ctx.Store.metrics = undefined;
158
+
159
+ const start = performance.now();
148
160
  debugLog("backgroundRevalidation", "revalidating stale route", {
149
161
  pathname: ctx.pathname,
150
162
  fullMatch: ctx.isFullMatch,
@@ -174,7 +186,9 @@ export function withBackgroundRevalidation<TEnv>(
174
186
  setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
175
187
 
176
188
  // Resolve all segments fresh (without revalidation logic)
177
- // to ensure complete components for caching
189
+ // to ensure complete components for caching.
190
+ // Skip DSL loaders — they are never cached (cacheRoute filters them)
191
+ // and are always resolved fresh on each request.
178
192
  const freshSegments = await ctx.Store.run(() =>
179
193
  resolveAllSegments(
180
194
  ctx.entries,
@@ -182,6 +196,7 @@ export function withBackgroundRevalidation<TEnv>(
182
196
  ctx.matched.params,
183
197
  freshHandlerContext,
184
198
  freshLoaderPromises,
199
+ { skipLoaders: true },
185
200
  ),
186
201
  );
187
202
 
@@ -207,16 +222,29 @@ export function withBackgroundRevalidation<TEnv>(
207
222
  completeSegments,
208
223
  ctx.isIntercept,
209
224
  );
225
+ if (INTERNAL_RANGO_DEBUG) {
226
+ const dur = performance.now() - start;
227
+ console.log(
228
+ `[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
229
+ );
230
+ }
210
231
  debugLog("backgroundRevalidation", "revalidation complete", {
211
232
  pathname: ctx.pathname,
212
233
  });
213
234
  } catch (error) {
235
+ if (INTERNAL_RANGO_DEBUG) {
236
+ const dur = performance.now() - start;
237
+ console.log(
238
+ `[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
239
+ );
240
+ }
214
241
  debugWarn("backgroundRevalidation", "revalidation failed", {
215
242
  pathname: ctx.pathname,
216
243
  error: String(error),
217
244
  });
218
245
  } finally {
219
246
  requestCtx._handleStore = originalHandleStore;
247
+ ctx.Store.metrics = savedMetrics;
220
248
  }
221
249
  });
222
250
  };