@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc

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 (285) hide show
  1. package/README.md +196 -43
  2. package/dist/bin/rango.js +277 -99
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2779 -1064
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +57 -11
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +155 -6
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +273 -53
  22. package/skills/middleware/SKILL.md +49 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +197 -6
  28. package/skills/prerender/SKILL.md +123 -100
  29. package/skills/rango/SKILL.md +242 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +88 -4
  33. package/skills/router-setup/SKILL.md +90 -5
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +716 -0
  37. package/skills/typesafety/SKILL.md +329 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +1 -1
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/navigation-bridge.ts +102 -16
  50. package/src/browser/navigation-client.ts +164 -59
  51. package/src/browser/navigation-store.ts +75 -17
  52. package/src/browser/navigation-transaction.ts +21 -37
  53. package/src/browser/partial-update.ts +139 -38
  54. package/src/browser/prefetch/cache.ts +175 -15
  55. package/src/browser/prefetch/fetch.ts +180 -33
  56. package/src/browser/prefetch/queue.ts +123 -20
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +53 -13
  59. package/src/browser/react/Link.tsx +81 -9
  60. package/src/browser/react/NavigationProvider.tsx +110 -33
  61. package/src/browser/react/context.ts +7 -2
  62. package/src/browser/react/filter-segment-order.ts +51 -7
  63. package/src/browser/react/index.ts +3 -0
  64. package/src/browser/react/location-state-shared.ts +175 -4
  65. package/src/browser/react/location-state.ts +39 -13
  66. package/src/browser/react/use-handle.ts +23 -64
  67. package/src/browser/react/use-navigation.ts +22 -2
  68. package/src/browser/react/use-params.ts +20 -8
  69. package/src/browser/react/use-reverse.ts +106 -0
  70. package/src/browser/react/use-router.ts +43 -10
  71. package/src/browser/react/use-segments.ts +11 -8
  72. package/src/browser/response-adapter.ts +25 -0
  73. package/src/browser/rsc-router.tsx +191 -74
  74. package/src/browser/scroll-restoration.ts +41 -14
  75. package/src/browser/segment-reconciler.ts +36 -9
  76. package/src/browser/segment-structure-assert.ts +2 -2
  77. package/src/browser/server-action-bridge.ts +31 -36
  78. package/src/browser/types.ts +57 -5
  79. package/src/build/collect-fallback-refs.ts +107 -0
  80. package/src/build/generate-manifest.ts +65 -40
  81. package/src/build/generate-route-types.ts +5 -0
  82. package/src/build/index.ts +2 -0
  83. package/src/build/route-trie.ts +52 -25
  84. package/src/build/route-types/codegen.ts +4 -4
  85. package/src/build/route-types/include-resolution.ts +9 -2
  86. package/src/build/route-types/per-module-writer.ts +7 -4
  87. package/src/build/route-types/router-processing.ts +278 -88
  88. package/src/build/route-types/scan-filter.ts +9 -2
  89. package/src/build/route-types/source-scan.ts +118 -0
  90. package/src/build/runtime-discovery.ts +9 -20
  91. package/src/cache/cache-runtime.ts +15 -11
  92. package/src/cache/cache-scope.ts +76 -49
  93. package/src/cache/cf/cf-cache-store.ts +501 -18
  94. package/src/cache/cf/index.ts +5 -1
  95. package/src/cache/document-cache.ts +17 -7
  96. package/src/cache/index.ts +1 -0
  97. package/src/cache/taint.ts +55 -0
  98. package/src/client.rsc.tsx +3 -0
  99. package/src/client.tsx +94 -238
  100. package/src/context-var.ts +72 -2
  101. package/src/debug.ts +2 -2
  102. package/src/decode-loader-results.ts +36 -0
  103. package/src/errors.ts +30 -1
  104. package/src/handle.ts +65 -12
  105. package/src/host/index.ts +2 -2
  106. package/src/host/router.ts +129 -57
  107. package/src/host/types.ts +31 -2
  108. package/src/host/utils.ts +1 -1
  109. package/src/href-client.ts +140 -20
  110. package/src/index.rsc.ts +12 -5
  111. package/src/index.ts +61 -11
  112. package/src/loader-store.ts +500 -0
  113. package/src/loader.rsc.ts +2 -5
  114. package/src/loader.ts +3 -10
  115. package/src/missing-id-error.ts +68 -0
  116. package/src/outlet-context.ts +1 -1
  117. package/src/prerender/store.ts +5 -4
  118. package/src/prerender.ts +141 -80
  119. package/src/response-utils.ts +37 -0
  120. package/src/reverse.ts +65 -15
  121. package/src/route-content-wrapper.tsx +6 -28
  122. package/src/route-definition/dsl-helpers.ts +435 -260
  123. package/src/route-definition/helper-factories.ts +29 -139
  124. package/src/route-definition/helpers-types.ts +110 -34
  125. package/src/route-definition/index.ts +3 -0
  126. package/src/route-definition/redirect.ts +11 -3
  127. package/src/route-definition/resolve-handler-use.ts +155 -0
  128. package/src/route-definition/use-item-types.ts +32 -0
  129. package/src/route-map-builder.ts +7 -1
  130. package/src/route-types.ts +37 -41
  131. package/src/router/basename.ts +14 -0
  132. package/src/router/content-negotiation.ts +113 -1
  133. package/src/router/error-handling.ts +1 -1
  134. package/src/router/find-match.ts +4 -2
  135. package/src/router/handler-context.ts +77 -38
  136. package/src/router/intercept-resolution.ts +15 -22
  137. package/src/router/lazy-includes.ts +12 -9
  138. package/src/router/loader-resolution.ts +174 -22
  139. package/src/router/logging.ts +5 -2
  140. package/src/router/manifest.ts +31 -16
  141. package/src/router/match-api.ts +128 -192
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/background-revalidation.ts +30 -2
  144. package/src/router/match-middleware/cache-lookup.ts +136 -106
  145. package/src/router/match-middleware/cache-store.ts +54 -10
  146. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  147. package/src/router/match-middleware/segment-resolution.ts +61 -5
  148. package/src/router/match-result.ts +125 -10
  149. package/src/router/metrics.ts +7 -2
  150. package/src/router/middleware-types.ts +21 -34
  151. package/src/router/middleware.ts +103 -90
  152. package/src/router/navigation-snapshot.ts +182 -0
  153. package/src/router/pattern-matching.ts +101 -17
  154. package/src/router/prerender-match.ts +110 -10
  155. package/src/router/preview-match.ts +32 -102
  156. package/src/router/request-classification.ts +286 -0
  157. package/src/router/revalidation.ts +58 -2
  158. package/src/router/route-snapshot.ts +245 -0
  159. package/src/router/router-context.ts +6 -1
  160. package/src/router/router-interfaces.ts +77 -28
  161. package/src/router/router-options.ts +76 -11
  162. package/src/router/router-registry.ts +2 -5
  163. package/src/router/segment-resolution/fresh.ts +223 -24
  164. package/src/router/segment-resolution/helpers.ts +29 -24
  165. package/src/router/segment-resolution/loader-cache.ts +1 -0
  166. package/src/router/segment-resolution/revalidation.ts +466 -285
  167. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  168. package/src/router/segment-wrappers.ts +2 -0
  169. package/src/router/substitute-pattern-params.ts +56 -0
  170. package/src/router/telemetry.ts +99 -0
  171. package/src/router/trie-matching.ts +18 -13
  172. package/src/router/types.ts +9 -0
  173. package/src/router/url-params.ts +49 -0
  174. package/src/router.ts +91 -23
  175. package/src/rsc/handler-context.ts +2 -2
  176. package/src/rsc/handler.ts +440 -381
  177. package/src/rsc/helpers.ts +91 -43
  178. package/src/rsc/index.ts +1 -1
  179. package/src/rsc/loader-fetch.ts +23 -3
  180. package/src/rsc/manifest-init.ts +5 -1
  181. package/src/rsc/origin-guard.ts +28 -10
  182. package/src/rsc/progressive-enhancement.ts +18 -2
  183. package/src/rsc/response-route-handler.ts +46 -53
  184. package/src/rsc/rsc-rendering.ts +41 -48
  185. package/src/rsc/runtime-warnings.ts +9 -10
  186. package/src/rsc/server-action.ts +25 -37
  187. package/src/rsc/ssr-setup.ts +18 -2
  188. package/src/rsc/types.ts +17 -3
  189. package/src/search-params.ts +4 -4
  190. package/src/segment-content-promise.ts +67 -0
  191. package/src/segment-loader-promise.ts +122 -0
  192. package/src/segment-system.tsx +219 -67
  193. package/src/serialize.ts +243 -0
  194. package/src/server/context.ts +277 -61
  195. package/src/server/cookie-store.ts +28 -4
  196. package/src/server/handle-store.ts +19 -0
  197. package/src/server/loader-registry.ts +9 -8
  198. package/src/server/request-context.ts +204 -60
  199. package/src/ssr/index.tsx +9 -1
  200. package/src/static-handler.ts +19 -7
  201. package/src/testing/cache-status.ts +166 -0
  202. package/src/testing/collect-handle.ts +63 -0
  203. package/src/testing/dispatch.ts +440 -0
  204. package/src/testing/dom.entry.ts +22 -0
  205. package/src/testing/e2e/fixture.ts +154 -0
  206. package/src/testing/e2e/index.ts +149 -0
  207. package/src/testing/e2e/matchers.ts +51 -0
  208. package/src/testing/e2e/page-helpers.ts +272 -0
  209. package/src/testing/e2e/parity.ts +306 -0
  210. package/src/testing/e2e/server.ts +183 -0
  211. package/src/testing/flight-matchers.ts +104 -0
  212. package/src/testing/flight-runtime.d.ts +21 -0
  213. package/src/testing/flight.entry.ts +22 -0
  214. package/src/testing/flight.ts +182 -0
  215. package/src/testing/generated-routes.ts +223 -0
  216. package/src/testing/index.ts +106 -0
  217. package/src/testing/internal/context.ts +255 -0
  218. package/src/testing/render-route.tsx +565 -0
  219. package/src/testing/run-loader.ts +296 -0
  220. package/src/testing/run-middleware.ts +179 -0
  221. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  222. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  223. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  224. package/src/testing/vitest-stubs/version.ts +5 -0
  225. package/src/testing/vitest.ts +183 -0
  226. package/src/types/cache-types.ts +4 -4
  227. package/src/types/global-namespace.ts +39 -26
  228. package/src/types/handler-context.ts +194 -72
  229. package/src/types/index.ts +1 -0
  230. package/src/types/loader-types.ts +41 -15
  231. package/src/types/request-scope.ts +126 -0
  232. package/src/types/route-entry.ts +19 -1
  233. package/src/types/segments.ts +37 -1
  234. package/src/urls/include-helper.ts +34 -67
  235. package/src/urls/index.ts +0 -3
  236. package/src/urls/path-helper-types.ts +50 -9
  237. package/src/urls/path-helper.ts +63 -63
  238. package/src/urls/pattern-types.ts +48 -19
  239. package/src/urls/response-types.ts +25 -22
  240. package/src/urls/type-extraction.ts +26 -116
  241. package/src/urls/urls-function.ts +1 -5
  242. package/src/use-loader.tsx +487 -44
  243. package/src/vite/debug.ts +185 -0
  244. package/src/vite/discovery/bundle-postprocess.ts +34 -37
  245. package/src/vite/discovery/discover-routers.ts +105 -51
  246. package/src/vite/discovery/discovery-errors.ts +194 -0
  247. package/src/vite/discovery/gate-state.ts +171 -0
  248. package/src/vite/discovery/prerender-collection.ts +188 -93
  249. package/src/vite/discovery/route-types-writer.ts +40 -84
  250. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  251. package/src/vite/discovery/state.ts +46 -6
  252. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  253. package/src/vite/index.ts +6 -0
  254. package/src/vite/plugin-types.ts +111 -72
  255. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  256. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  257. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  258. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  259. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  260. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  261. package/src/vite/plugins/expose-action-id.ts +55 -33
  262. package/src/vite/plugins/expose-id-utils.ts +24 -8
  263. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  264. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  265. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  266. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  267. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  268. package/src/vite/plugins/performance-tracks.ts +92 -0
  269. package/src/vite/plugins/refresh-cmd.ts +88 -26
  270. package/src/vite/plugins/use-cache-transform.ts +65 -50
  271. package/src/vite/plugins/version-injector.ts +39 -23
  272. package/src/vite/plugins/version-plugin.ts +72 -3
  273. package/src/vite/plugins/virtual-entries.ts +2 -2
  274. package/src/vite/rango.ts +265 -226
  275. package/src/vite/router-discovery.ts +920 -137
  276. package/src/vite/utils/ast-handler-extract.ts +15 -15
  277. package/src/vite/utils/banner.ts +4 -4
  278. package/src/vite/utils/bundle-analysis.ts +4 -2
  279. package/src/vite/utils/client-chunks.ts +190 -0
  280. package/src/vite/utils/forward-user-plugins.ts +193 -0
  281. package/src/vite/utils/manifest-utils.ts +21 -5
  282. package/src/vite/utils/package-resolution.ts +41 -1
  283. package/src/vite/utils/prerender-utils.ts +38 -5
  284. package/src/vite/utils/shared-utils.ts +109 -27
  285. package/src/browser/action-response-classifier.ts +0 -99
@@ -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
  };
@@ -70,9 +70,11 @@
70
70
  * - No segments yielded from this middleware
71
71
  *
72
72
  * Loaders:
73
- * - NEVER cached by design
73
+ * - NEVER cached in the segment cache
74
74
  * - Always resolved fresh on every request
75
75
  * - Ensures data freshness even with cached UI components
76
+ * - Segment cache staleness does NOT propagate to loader revalidation;
77
+ * loaders use their own revalidation rules (actionId, user-defined)
76
78
  *
77
79
  *
78
80
  * REVALIDATION RULES
@@ -94,6 +96,7 @@ import type { MatchContext, MatchPipelineState } from "../match-context.js";
94
96
  import { getRouterContext } from "../router-context.js";
95
97
  import { resolveSink, safeEmit } from "../telemetry.js";
96
98
  import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
99
+ import { treeHasStreaming } from "./segment-resolution.js";
97
100
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
98
101
  import type { HandleStore } from "../../server/handle-store.js";
99
102
  import {
@@ -191,6 +194,16 @@ async function* yieldFromStore<TEnv>(
191
194
  state.cachedSegments = segments;
192
195
  state.cachedMatchedIds = segments.map((s) => s.id);
193
196
 
197
+ // Set streaming flag (once) and resolve render barrier.
198
+ const reqCtx = handleStoreRef ? undefined : _lazyGetRequestContext?.();
199
+ const barrierReqCtx = reqCtx ?? _getRequestContext();
200
+ if (barrierReqCtx) {
201
+ if (barrierReqCtx._treeHasStreaming === undefined) {
202
+ barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
203
+ }
204
+ barrierReqCtx._resolveRenderBarrier(segments);
205
+ }
206
+
194
207
  // For partial navigation, nullify components the client already has
195
208
  // so parent layouts stay live (client keeps its existing versions).
196
209
  // When params changed (e.g., different guide slug), the segments have
@@ -210,6 +223,9 @@ async function* yieldFromStore<TEnv>(
210
223
  }
211
224
 
212
225
  // Resolve loaders fresh (loaders are never pre-rendered/cached)
226
+ const ms = ctx.metricsStore;
227
+ const loaderStart = performance.now();
228
+
213
229
  if (ctx.isFullMatch) {
214
230
  if (resolveLoadersOnly) {
215
231
  const loaderSegments = await ctx.Store.run(() =>
@@ -235,6 +251,7 @@ async function* yieldFromStore<TEnv>(
235
251
  ctx.url,
236
252
  ctx.routeKey,
237
253
  ctx.actionContext,
254
+ ctx.stale || undefined,
238
255
  ),
239
256
  );
240
257
  state.matchedIds = [
@@ -249,16 +266,54 @@ async function* yieldFromStore<TEnv>(
249
266
  }
250
267
  }
251
268
 
252
- const ms = ctx.metricsStore;
253
269
  if (ms) {
270
+ const loaderEnd = performance.now();
271
+ ms.metrics.push({
272
+ label: "pipeline:loader-resolve",
273
+ duration: loaderEnd - loaderStart,
274
+ startTime: loaderStart - ms.requestStart,
275
+ depth: 1,
276
+ });
254
277
  ms.metrics.push({
255
- label: "pipeline:cache-lookup",
256
- duration: performance.now() - pipelineStart,
278
+ label: "pipeline:cache-hit",
279
+ duration: loaderEnd - pipelineStart,
257
280
  startTime: pipelineStart - ms.requestStart,
258
281
  });
259
282
  }
260
283
  }
261
284
 
285
+ /**
286
+ * Look up a prerendered (build-time cached) entry for the current route and, on
287
+ * a hit, yield its segments. Returns true when an entry was served (the caller
288
+ * should stop the pipeline) and false on a miss. Intercept navigations consult
289
+ * only the intercept-specific entry (`paramHash + "/i"`); a miss there falls
290
+ * through to the normal pipeline so intercept-resolution can run. Callers must
291
+ * guard on `prerenderStoreInstance` after `ensurePrerenderDeps()`.
292
+ */
293
+ async function* tryPrerenderLookup<TEnv>(
294
+ ctx: MatchContext<TEnv>,
295
+ state: MatchPipelineState,
296
+ pipelineStart: number,
297
+ handleStoreRef?: HandleStore,
298
+ ): AsyncGenerator<ResolvedSegment, boolean> {
299
+ const paramHash = _hashParams!(ctx.matched.params);
300
+ const isPassthroughPrerenderRoute = ctx.entries.some(
301
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
302
+ );
303
+ const lookupHash = ctx.isIntercept ? paramHash + "/i" : paramHash;
304
+ const entry = await prerenderStoreInstance!.get(
305
+ ctx.matched.routeKey,
306
+ lookupHash,
307
+ {
308
+ pathname: ctx.pathname,
309
+ isPassthroughRoute: isPassthroughPrerenderRoute,
310
+ },
311
+ );
312
+ if (!entry) return false;
313
+ yield* yieldFromStore(entry, ctx, state, pipelineStart, handleStoreRef);
314
+ return true;
315
+ }
316
+
262
317
  /**
263
318
  * Async generator middleware type
264
319
  */
@@ -305,59 +360,19 @@ export function withCacheLookup<TEnv>(
305
360
 
306
361
  // Prerender lookup: check build-time cached data before runtime cache.
307
362
  // Prerender data is available regardless of runtime cache configuration.
308
- if (!ctx.isAction && ctx.matched.pr) {
363
+ // Skip for HMR requests — the dev prerender endpoint reads from a stale
364
+ // RouterRegistry snapshot; rendering fresh ensures edits are visible.
365
+ const isHmr = !!ctx.request.headers.get("X-RSC-HMR");
366
+ if (!ctx.isAction && !isHmr && ctx.matched.pr) {
309
367
  await ensurePrerenderDeps();
310
368
  if (prerenderStoreInstance) {
311
- const paramHash = _hashParams!(ctx.matched.params);
312
- const isPassthroughPrerenderRoute = ctx.entries.some(
313
- (entry) =>
314
- entry.type === "route" &&
315
- entry.prerenderDef?.options?.passthrough === true,
369
+ const served = yield* tryPrerenderLookup(
370
+ ctx,
371
+ state,
372
+ pipelineStart,
373
+ handleStoreRef,
316
374
  );
317
-
318
- if (ctx.isIntercept) {
319
- // Intercept navigation: try intercept-specific prerender entry
320
- const entry = await prerenderStoreInstance.get(
321
- ctx.matched.routeKey,
322
- paramHash + "/i",
323
- {
324
- pathname: ctx.pathname,
325
- isPassthroughRoute: isPassthroughPrerenderRoute,
326
- },
327
- );
328
- if (entry) {
329
- yield* yieldFromStore(
330
- entry,
331
- ctx,
332
- state,
333
- pipelineStart,
334
- handleStoreRef,
335
- );
336
- return;
337
- }
338
- // No intercept prerender -- fall through to normal pipeline
339
- // (skip non-intercept prerender to let intercept-resolution run)
340
- } else {
341
- // Normal navigation: existing behavior
342
- const entry = await prerenderStoreInstance.get(
343
- ctx.matched.routeKey,
344
- paramHash,
345
- {
346
- pathname: ctx.pathname,
347
- isPassthroughRoute: isPassthroughPrerenderRoute,
348
- },
349
- );
350
- if (entry) {
351
- yield* yieldFromStore(
352
- entry,
353
- ctx,
354
- state,
355
- pipelineStart,
356
- handleStoreRef,
357
- );
358
- return;
359
- }
360
- }
375
+ if (served) return;
361
376
  }
362
377
  }
363
378
 
@@ -380,53 +395,13 @@ export function withCacheLookup<TEnv>(
380
395
  if (hasStatic) {
381
396
  await ensurePrerenderDeps();
382
397
  if (prerenderStoreInstance) {
383
- const paramHash = _hashParams!(ctx.matched.params);
384
- const isPassthroughPrerenderRoute = ctx.entries.some(
385
- (entry) =>
386
- entry.type === "route" &&
387
- entry.prerenderDef?.options?.passthrough === true,
398
+ const served = yield* tryPrerenderLookup(
399
+ ctx,
400
+ state,
401
+ pipelineStart,
402
+ handleStoreRef,
388
403
  );
389
-
390
- if (ctx.isIntercept) {
391
- const entry = await prerenderStoreInstance.get(
392
- ctx.matched.routeKey,
393
- paramHash + "/i",
394
- {
395
- pathname: ctx.pathname,
396
- isPassthroughRoute: isPassthroughPrerenderRoute,
397
- },
398
- );
399
- if (entry) {
400
- yield* yieldFromStore(
401
- entry,
402
- ctx,
403
- state,
404
- pipelineStart,
405
- handleStoreRef,
406
- );
407
- return;
408
- }
409
- // No intercept prerender -- fall through to normal pipeline
410
- } else {
411
- const entry = await prerenderStoreInstance.get(
412
- ctx.matched.routeKey,
413
- paramHash,
414
- {
415
- pathname: ctx.pathname,
416
- isPassthroughRoute: isPassthroughPrerenderRoute,
417
- },
418
- );
419
- if (entry) {
420
- yield* yieldFromStore(
421
- entry,
422
- ctx,
423
- state,
424
- pipelineStart,
425
- handleStoreRef,
426
- );
427
- return;
428
- }
429
- }
404
+ if (served) return;
430
405
  }
431
406
  }
432
407
  }
@@ -437,7 +412,7 @@ export function withCacheLookup<TEnv>(
437
412
  yield* source;
438
413
  if (ms) {
439
414
  ms.metrics.push({
440
- label: "pipeline:cache-lookup",
415
+ label: "pipeline:cache-miss",
441
416
  duration: performance.now() - pipelineStart,
442
417
  startTime: pipelineStart - ms.requestStart,
443
418
  });
@@ -457,7 +432,7 @@ export function withCacheLookup<TEnv>(
457
432
  yield* source;
458
433
  if (ms) {
459
434
  ms.metrics.push({
460
- label: "pipeline:cache-lookup",
435
+ label: "pipeline:cache-miss",
461
436
  duration: performance.now() - pipelineStart,
462
437
  startTime: pipelineStart - ms.requestStart,
463
438
  });
@@ -509,7 +484,41 @@ export function withCacheLookup<TEnv>(
509
484
 
510
485
  // Look up revalidation rules for this segment
511
486
  const entryInfo = entryRevalidateMap?.get(segment.id);
487
+
488
+ // Even without explicit revalidation rules, route segments and their
489
+ // children must re-render when params or search params change — the
490
+ // handler reads ctx.params/ctx.searchParams so different values produce
491
+ // different content. Matches evaluateRevalidation's default logic.
492
+ const searchChanged = ctx.prevUrl.search !== ctx.url.search;
493
+ const routeParamsChanged = !paramsEqual(
494
+ ctx.matched.params,
495
+ ctx.prevParams,
496
+ );
497
+ const shouldDefaultRevalidate =
498
+ (searchChanged || routeParamsChanged) &&
499
+ (segment.type === "route" ||
500
+ (segment.belongsToRoute &&
501
+ (segment.type === "layout" || segment.type === "parallel")));
502
+
512
503
  if (!entryInfo || entryInfo.revalidate.length === 0) {
504
+ if (shouldDefaultRevalidate) {
505
+ // Params or search params changed — must re-render even without custom rules
506
+ if (isTraceActive()) {
507
+ pushRevalidationTraceEntry({
508
+ segmentId: segment.id,
509
+ segmentType: segment.type,
510
+ belongsToRoute: segment.belongsToRoute ?? false,
511
+ source: "cache-hit",
512
+ defaultShouldRevalidate: true,
513
+ finalShouldRevalidate: true,
514
+ reason: routeParamsChanged
515
+ ? "cached-params-changed"
516
+ : "cached-search-changed",
517
+ });
518
+ }
519
+ yield segment;
520
+ continue;
521
+ }
513
522
  // No revalidation rules, use default behavior (skip if client has)
514
523
  if (isTraceActive()) {
515
524
  pushRevalidationTraceEntry({
@@ -543,7 +552,7 @@ export function withCacheLookup<TEnv>(
543
552
  routeKey: ctx.routeKey,
544
553
  context: ctx.handlerContext,
545
554
  actionContext: ctx.actionContext,
546
- stale: cacheResult.shouldRevalidate || undefined,
555
+ stale: cacheResult.shouldRevalidate || ctx.stale || undefined,
547
556
  traceSource: "cache-hit",
548
557
  });
549
558
 
@@ -570,9 +579,19 @@ export function withCacheLookup<TEnv>(
570
579
  yield segment;
571
580
  }
572
581
 
582
+ // Set streaming flag (once) and resolve render barrier.
583
+ const barrierReqCtx = _getRequestContext();
584
+ if (barrierReqCtx) {
585
+ if (barrierReqCtx._treeHasStreaming === undefined) {
586
+ barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
587
+ }
588
+ barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
589
+ }
590
+
573
591
  // Resolve loaders fresh (loaders are NOT cached by default)
574
592
  // This ensures fresh data even on cache hit
575
593
  const Store = ctx.Store;
594
+ const loaderStart = performance.now();
576
595
 
577
596
  if (ctx.isFullMatch) {
578
597
  // Full match (document request) - simple loader resolution without revalidation
@@ -605,7 +624,11 @@ export function withCacheLookup<TEnv>(
605
624
  ctx.url,
606
625
  ctx.routeKey,
607
626
  ctx.actionContext,
608
- cacheResult.shouldRevalidate || undefined,
627
+ // Loaders are never cached in the segment cache, so segment
628
+ // staleness (cacheResult.shouldRevalidate) must not propagate.
629
+ // But browser-sent staleness (ctx.stale) — indicating an action
630
+ // happened in this or another tab — must still reach loaders.
631
+ ctx.stale || undefined,
609
632
  ),
610
633
  );
611
634
 
@@ -624,9 +647,16 @@ export function withCacheLookup<TEnv>(
624
647
  }
625
648
  }
626
649
  if (ms) {
650
+ const loaderEnd = performance.now();
651
+ ms.metrics.push({
652
+ label: "pipeline:loader-resolve",
653
+ duration: loaderEnd - loaderStart,
654
+ startTime: loaderStart - ms.requestStart,
655
+ depth: 1,
656
+ });
627
657
  ms.metrics.push({
628
- label: "pipeline:cache-lookup",
629
- duration: performance.now() - pipelineStart,
658
+ label: "pipeline:cache-hit",
659
+ duration: loaderEnd - pipelineStart,
630
660
  startTime: pipelineStart - ms.requestStart,
631
661
  });
632
662
  }
@@ -104,7 +104,8 @@ import type { ResolvedSegment } from "../../types.js";
104
104
  import { getRequestContext } from "../../server/request-context.js";
105
105
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
106
106
  import { getRouterContext } from "../router-context.js";
107
- import { debugLog, debugWarn } from "../logging.js";
107
+ import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
108
+ import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
108
109
  import type { GeneratorMiddleware } from "./cache-lookup.js";
109
110
 
110
111
  /**
@@ -120,7 +121,6 @@ export function withCacheStore<TEnv>(
120
121
  return async function* (
121
122
  source: AsyncGenerator<ResolvedSegment>,
122
123
  ): AsyncGenerator<ResolvedSegment> {
123
- const pipelineStart = performance.now();
124
124
  const ms = ctx.metricsStore;
125
125
 
126
126
  // Collect all segments while passing them through
@@ -130,6 +130,9 @@ export function withCacheStore<TEnv>(
130
130
  yield segment;
131
131
  }
132
132
 
133
+ // Measure own work only (after source iteration completes)
134
+ const ownStart = performance.now();
135
+
133
136
  // Skip caching if:
134
137
  // 1. Cache miss but cache scope is disabled
135
138
  // 2. This is an action (actions don't cache)
@@ -144,8 +147,8 @@ export function withCacheStore<TEnv>(
144
147
  if (ms) {
145
148
  ms.metrics.push({
146
149
  label: "pipeline:cache-store",
147
- duration: performance.now() - pipelineStart,
148
- startTime: pipelineStart - ms.requestStart,
150
+ duration: performance.now() - ownStart,
151
+ startTime: ownStart - ms.requestStart,
149
152
  });
150
153
  }
151
154
  return;
@@ -162,16 +165,24 @@ export function withCacheStore<TEnv>(
162
165
  // Combine main segments with intercept segments
163
166
  const allSegmentsToCache = [...allSegments, ...state.interceptSegments];
164
167
 
165
- // Check if any non-loader segments have null components
166
- // This happens when client already had those segments (partial navigation)
168
+ // Check if any non-loader segments have null components from revalidation
169
+ // skip (client already had them). Segments where the handler intentionally
170
+ // returned null are not revalidation skips — re-rendering them will still
171
+ // produce null, so proactive caching would be wasted work.
167
172
  const hasNullComponents = allSegmentsToCache.some(
168
- (s) => s.component === null && s.type !== "loader",
173
+ (s) =>
174
+ s.component === null &&
175
+ s.type !== "loader" &&
176
+ ctx.clientSegmentSet.has(s.id),
169
177
  );
170
178
 
171
179
  const requestCtx = getRequestContext();
172
180
  if (!requestCtx) return;
173
181
 
174
182
  const cacheScope = ctx.cacheScope;
183
+ const reqId = INTERNAL_RANGO_DEBUG
184
+ ? getOrCreateRequestId(ctx.request)
185
+ : undefined;
175
186
 
176
187
  // Register onResponse callback to skip caching for non-200 responses
177
188
  // Note: error/notFound status codes are set elsewhere (not caching-specific)
@@ -189,6 +200,11 @@ export function withCacheStore<TEnv>(
189
200
  // Proactive caching: render all segments fresh in background
190
201
  // This ensures cache has complete components for future requests
191
202
  requestCtx.waitUntil(async () => {
203
+ // Prevent background metrics from polluting foreground timeline.
204
+ const savedMetrics = ctx.Store.metrics;
205
+ ctx.Store.metrics = undefined;
206
+
207
+ const start = performance.now();
192
208
  debugLog("cacheStore", "proactive caching started", {
193
209
  pathname: ctx.pathname,
194
210
  });
@@ -218,7 +234,9 @@ export function withCacheStore<TEnv>(
218
234
  // Use normal loader access so handle data is captured
219
235
  setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
220
236
 
221
- // Re-resolve ALL segments without revalidation
237
+ // Re-resolve ALL segments without revalidation.
238
+ // Skip DSL loaders — they are never cached (cacheRoute filters them)
239
+ // and are always resolved fresh on each request.
222
240
  const Store = ctx.Store;
223
241
  const freshSegments = await Store.run(() =>
224
242
  resolveAllSegments(
@@ -227,6 +245,7 @@ export function withCacheStore<TEnv>(
227
245
  ctx.matched.params,
228
246
  proactiveHandlerContext,
229
247
  proactiveLoaderPromises,
248
+ { skipLoaders: true },
230
249
  ),
231
250
  );
232
251
 
@@ -256,28 +275,53 @@ export function withCacheStore<TEnv>(
256
275
  completeSegments,
257
276
  ctx.isIntercept,
258
277
  );
278
+ if (INTERNAL_RANGO_DEBUG) {
279
+ const dur = performance.now() - start;
280
+ console.log(
281
+ `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
282
+ );
283
+ }
259
284
  debugLog("cacheStore", "proactive caching complete", {
260
285
  pathname: ctx.pathname,
261
286
  });
262
287
  } catch (error) {
288
+ if (INTERNAL_RANGO_DEBUG) {
289
+ const dur = performance.now() - start;
290
+ console.log(
291
+ `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
292
+ );
293
+ }
263
294
  debugWarn("cacheStore", "proactive caching failed", {
264
295
  pathname: ctx.pathname,
265
296
  error: String(error),
266
297
  });
267
298
  } finally {
268
299
  requestCtx._handleStore = originalHandleStore;
300
+ ctx.Store.metrics = savedMetrics;
269
301
  }
270
302
  });
271
303
  } else {
272
304
  // All segments have components - cache directly
273
305
  // Schedule caching in waitUntil since cacheRoute is now async (key resolution)
306
+ if (INTERNAL_RANGO_DEBUG) {
307
+ console.log(
308
+ `[RSC CacheStore][req:${reqId}] Direct cache path: scheduling cacheRoute for ${ctx.pathname} (${allSegmentsToCache.length} segments, hasNullComponents=${hasNullComponents})`,
309
+ );
310
+ }
274
311
  requestCtx.waitUntil(async () => {
312
+ const start = performance.now();
275
313
  await cacheScope.cacheRoute(
276
314
  ctx.pathname,
277
315
  ctx.matched.params,
278
316
  allSegmentsToCache,
279
317
  ctx.isIntercept,
280
318
  );
319
+ if (INTERNAL_RANGO_DEBUG) {
320
+ const dur = performance.now() - start;
321
+ console.log(
322
+ `[RSC Background][req:${reqId}] Cache store ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${allSegmentsToCache.length}`,
323
+ );
324
+ }
281
325
  });
282
326
  }
283
327
 
@@ -287,8 +331,8 @@ export function withCacheStore<TEnv>(
287
331
  if (ms) {
288
332
  ms.metrics.push({
289
333
  label: "pipeline:cache-store",
290
- duration: performance.now() - pipelineStart,
291
- startTime: pipelineStart - ms.requestStart,
334
+ duration: performance.now() - ownStart,
335
+ startTime: ownStart - ms.requestStart,
292
336
  });
293
337
  }
294
338
  };
@@ -123,7 +123,6 @@ export function withInterceptResolution<TEnv>(
123
123
  return async function* (
124
124
  source: AsyncGenerator<ResolvedSegment>,
125
125
  ): AsyncGenerator<ResolvedSegment> {
126
- const pipelineStart = performance.now();
127
126
  const ms = ctx.metricsStore;
128
127
 
129
128
  // First, yield all segments from the source (main segment resolution or cache)
@@ -133,13 +132,16 @@ export function withInterceptResolution<TEnv>(
133
132
  yield segment;
134
133
  }
135
134
 
135
+ // Measure own work only (after source iteration completes)
136
+ const ownStart = performance.now();
137
+
136
138
  // Skip intercept resolution for full match (document requests don't have intercepts)
137
139
  if (ctx.isFullMatch) {
138
140
  if (ms) {
139
141
  ms.metrics.push({
140
142
  label: "pipeline:intercept",
141
- duration: performance.now() - pipelineStart,
142
- startTime: pipelineStart - ms.requestStart,
143
+ duration: performance.now() - ownStart,
144
+ startTime: ownStart - ms.requestStart,
143
145
  });
144
146
  }
145
147
  return;
@@ -163,8 +165,8 @@ export function withInterceptResolution<TEnv>(
163
165
  if (ms) {
164
166
  ms.metrics.push({
165
167
  label: "pipeline:intercept",
166
- duration: performance.now() - pipelineStart,
167
- startTime: pipelineStart - ms.requestStart,
168
+ duration: performance.now() - ownStart,
169
+ startTime: ownStart - ms.requestStart,
168
170
  });
169
171
  }
170
172
  return;
@@ -216,8 +218,8 @@ export function withInterceptResolution<TEnv>(
216
218
  if (ms) {
217
219
  ms.metrics.push({
218
220
  label: "pipeline:intercept",
219
- duration: performance.now() - pipelineStart,
220
- startTime: pipelineStart - ms.requestStart,
221
+ duration: performance.now() - ownStart,
222
+ startTime: ownStart - ms.requestStart,
221
223
  });
222
224
  }
223
225
  };