@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -242,25 +242,3 @@ export function createPipelineState(): MatchPipelineState {
242
242
  slots: {},
243
243
  };
244
244
  }
245
-
246
- /**
247
- * Input parameters for createMatchContext
248
- */
249
- export interface CreateMatchContextInput<TEnv = any> {
250
- request: Request;
251
- env: TEnv;
252
- actionContext?: ActionContext;
253
- }
254
-
255
- /**
256
- * Result from createMatchContext - either a context or null (fall back to full match)
257
- */
258
- export type CreateMatchContextResult<TEnv = any> =
259
- | { type: "context"; ctx: MatchContext<TEnv> }
260
- | { type: "fallback"; reason: string }
261
- | { type: "error"; error: Error };
262
-
263
- // Note: createMatchContext() will be implemented in Step J10 when we wire everything together.
264
- // It requires access to RouterContext (findMatch, loadManifest, etc.) which are closure
265
- // functions from createRouter(). The implementation will live in router.ts initially
266
- // and call getRouterContext() to access these dependencies.
@@ -89,20 +89,6 @@ export interface MatchHandlers<TEnv = any> {
89
89
  negotiated?: boolean;
90
90
  manifestEntry?: EntryData;
91
91
  } | null>;
92
- createMatchContextForFull: (
93
- request: Request,
94
- env: TEnv,
95
- ) => Promise<MatchContext<TEnv> | { type: "redirect"; redirectUrl: string }>;
96
- createMatchContextForPartial: (
97
- request: Request,
98
- env: TEnv,
99
- actionContext?: {
100
- actionId?: string;
101
- actionUrl?: URL;
102
- actionResult?: any;
103
- formData?: FormData;
104
- },
105
- ) => Promise<MatchContext<TEnv> | null>;
106
92
  }
107
93
 
108
94
  /**
@@ -123,9 +109,6 @@ export function createMatchHandlers<TEnv = any>(
123
109
  const hasTelemetry = !!deps.telemetry;
124
110
  const telemetry = resolveSink(deps.telemetry);
125
111
  const cacheSignalEnabled = !!deps.cacheSignalEnabled;
126
- // Compute the coarse cache signal when EITHER telemetry needs it (for the
127
- // cache.decision event) OR the debug header gate is on. When neither is set,
128
- // this is never called — zero extra work on the hot path.
129
112
  const buildSignal = (
130
113
  routeKey: string,
131
114
  state: {
@@ -134,8 +117,6 @@ export function createMatchHandlers<TEnv = any>(
134
117
  shouldRevalidate?: boolean;
135
118
  },
136
119
  ): CacheSegmentSignal[] => buildCacheSignalSegments(routeKey, state);
137
- // Stash the signal on the request context for the response path to emit as
138
- // the X-Rango-Cache header. Only when the debug gate is on.
139
120
  const recordSignalIfEnabled = (segments: CacheSegmentSignal[]): void => {
140
121
  if (!cacheSignalEnabled) return;
141
122
  const reqCtx = _getRequestContext();
@@ -173,15 +154,6 @@ export function createMatchHandlers<TEnv = any>(
173
154
  );
174
155
  }
175
156
 
176
- /**
177
- * Match request and return segments (document/SSR requests)
178
- *
179
- * Uses generator middleware pipeline for clean separation of concerns:
180
- * - cache-lookup: Check cache first
181
- * - segment-resolution: Resolve segments on cache miss
182
- * - cache-store: Store results in cache
183
- * - background-revalidation: SWR revalidation
184
- */
185
157
  async function match(request: Request, env: TEnv): Promise<MatchResult> {
186
158
  const requestId = hasTelemetry ? getRequestId(request) : undefined;
187
159
  return runWithRouterLogContext({ request, transaction: "match" }, () => {
@@ -205,7 +177,6 @@ export function createMatchHandlers<TEnv = any>(
205
177
 
206
178
  const result = await createMatchContextForFull(request, env);
207
179
 
208
- // Handle redirect case
209
180
  if ("type" in result && result.type === "redirect") {
210
181
  if (hasTelemetry) {
211
182
  safeEmit(telemetry, {
@@ -284,7 +255,6 @@ export function createMatchHandlers<TEnv = any>(
284
255
  });
285
256
  }
286
257
  if (error instanceof Response) throw error;
287
- // Report unhandled errors during full match pipeline
288
258
  callOnError(error, "routing", {
289
259
  request,
290
260
  url: ctx.url,
@@ -319,16 +289,6 @@ export function createMatchHandlers<TEnv = any>(
319
289
  );
320
290
  }
321
291
 
322
- /**
323
- * Match partial request with revalidation
324
- *
325
- * Uses generator middleware pipeline for clean separation of concerns:
326
- * - cache-lookup: Check cache first
327
- * - segment-resolution: Resolve segments on cache miss
328
- * - intercept-resolution: Handle intercept routes
329
- * - cache-store: Store results in cache
330
- * - background-revalidation: SWR revalidation
331
- */
332
292
  async function matchPartial(
333
293
  request: Request,
334
294
  context: TEnv,
@@ -448,7 +408,6 @@ export function createMatchHandlers<TEnv = any>(
448
408
  });
449
409
  }
450
410
  if (error instanceof Response) throw error;
451
- // Report unhandled errors during partial match pipeline
452
411
  callOnError(error, actionContext ? "action" : "revalidation", {
453
412
  request,
454
413
  url: ctx.url,
@@ -477,7 +436,5 @@ export function createMatchHandlers<TEnv = any>(
477
436
  matchPartial: matchPartial,
478
437
  matchError: matchError,
479
438
  previewMatch: previewMatch,
480
- createMatchContextForFull: createMatchContextForFull,
481
- createMatchContextForPartial: createMatchContextForPartial,
482
439
  };
483
440
  }
@@ -168,8 +168,6 @@ export function withBackgroundRevalidation<TEnv>(
168
168
  requestCtx._handleStore = createHandleStore();
169
169
 
170
170
  try {
171
- // Create fresh handler context and loader promises to avoid
172
- // reusing memoized results from the foreground pass
173
171
  const freshHandlerContext = createHandlerContext(
174
172
  ctx.matched.params,
175
173
  ctx.request,
@@ -185,10 +183,6 @@ export function withBackgroundRevalidation<TEnv>(
185
183
  const freshLoaderPromises = new Map<string, Promise<any>>();
186
184
  setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
187
185
 
188
- // Resolve all segments fresh (without revalidation logic)
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.
192
186
  const freshSegments = await ctx.Store.run(() =>
193
187
  resolveAllSegments(
194
188
  ctx.entries,
@@ -200,7 +194,6 @@ export function withBackgroundRevalidation<TEnv>(
200
194
  ),
201
195
  );
202
196
 
203
- // Also resolve intercept segments fresh if applicable
204
197
  let freshInterceptSegments: ResolvedSegment[] = [];
205
198
  if (ctx.interceptResult) {
206
199
  freshInterceptSegments = await ctx.Store.run(() =>
@@ -93,7 +93,7 @@
93
93
  */
94
94
  import type { ResolvedSegment } from "../../types.js";
95
95
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
96
- import { getRouterContext } from "../router-context.js";
96
+ import { getRouterContext, type RouterContext } from "../router-context.js";
97
97
  import { resolveSink, safeEmit } from "../telemetry.js";
98
98
  import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
99
99
  import { treeHasStreaming } from "./segment-resolution.js";
@@ -103,6 +103,7 @@ import {
103
103
  getRequestContext,
104
104
  _getRequestContext,
105
105
  } from "../../server/request-context.js";
106
+ import { paramsEqual } from "../params-util.js";
106
107
 
107
108
  // Lazily initialized prerender store singleton and dynamically imported deps.
108
109
  // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
@@ -124,22 +125,6 @@ let _lazyGetRequestContext:
124
125
  | typeof import("../../server/request-context.js").getRequestContext
125
126
  | undefined;
126
127
 
127
- function paramsEqual(
128
- a: Record<string, string>,
129
- b: Record<string, string>,
130
- ): boolean {
131
- if (a === b) return true;
132
-
133
- const keysA = Object.keys(a);
134
- if (keysA.length !== Object.keys(b).length) return false;
135
-
136
- for (const key of keysA) {
137
- if (a[key] !== b[key]) return false;
138
- }
139
-
140
- return true;
141
- }
142
-
143
128
  async function ensurePrerenderDeps() {
144
129
  if (!_deserializeSegments) {
145
130
  const [codec, snapshot, paramHash, reqCtx, store] = await Promise.all([
@@ -165,6 +150,81 @@ async function ensurePrerenderDeps() {
165
150
  * Deserializes segments, replays handle data, yields segments with partial
166
151
  * navigation nullification, and resolves fresh loaders.
167
152
  */
153
+ // Resolve loaders fresh on a cache hit (loaders are NEVER cached) and yield
154
+ // the loader segments, updating state.matchedIds and pushing the loader-resolve
155
+ // + cache-hit metrics. Shared verbatim by the prerender-store path
156
+ // (yieldFromStore) and the runtime cache-hit path (withCacheLookup). The router
157
+ // resolve fns are passed in (captured early by the callers) rather than re-read
158
+ // from getRouterContext() here, because that is ALS-backed and the callers run
159
+ // awaits before reaching this point (workerd can disrupt ALS mid-pipeline).
160
+ async function* resolveFreshLoadersAndYield<TEnv>(
161
+ ctx: MatchContext<TEnv>,
162
+ state: MatchPipelineState,
163
+ pipelineStart: number,
164
+ ms: MatchContext<TEnv>["metricsStore"],
165
+ resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
166
+ resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
167
+ ): AsyncGenerator<ResolvedSegment> {
168
+ const loaderStart = performance.now();
169
+
170
+ if (ctx.isFullMatch) {
171
+ if (resolveLoadersOnly) {
172
+ const loaderSegments = await ctx.Store.run(() =>
173
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext),
174
+ );
175
+ state.matchedIds = state.cachedMatchedIds!;
176
+ for (const segment of loaderSegments) {
177
+ yield segment;
178
+ }
179
+ } else {
180
+ state.matchedIds = state.cachedMatchedIds!;
181
+ }
182
+ } else {
183
+ if (resolveLoadersOnlyWithRevalidation) {
184
+ const loaderResult = await ctx.Store.run(() =>
185
+ resolveLoadersOnlyWithRevalidation(
186
+ ctx.entries,
187
+ ctx.handlerContext,
188
+ ctx.clientSegmentSet,
189
+ ctx.prevParams,
190
+ ctx.request,
191
+ ctx.prevUrl,
192
+ ctx.url,
193
+ ctx.routeKey,
194
+ ctx.actionContext,
195
+ // Loaders are never cached in the segment cache, so segment staleness
196
+ // must not propagate; browser-sent staleness (ctx.stale) still must.
197
+ ctx.stale || undefined,
198
+ ),
199
+ );
200
+ state.matchedIds = [
201
+ ...state.cachedMatchedIds!,
202
+ ...loaderResult.matchedIds,
203
+ ];
204
+ for (const segment of loaderResult.segments) {
205
+ yield segment;
206
+ }
207
+ } else {
208
+ state.matchedIds = state.cachedMatchedIds!;
209
+ }
210
+ }
211
+
212
+ if (ms) {
213
+ const loaderEnd = performance.now();
214
+ ms.metrics.push({
215
+ label: "pipeline:loader-resolve",
216
+ duration: loaderEnd - loaderStart,
217
+ startTime: loaderStart - ms.requestStart,
218
+ depth: 1,
219
+ });
220
+ ms.metrics.push({
221
+ label: "pipeline:cache-hit",
222
+ duration: loaderEnd - pipelineStart,
223
+ startTime: pipelineStart - ms.requestStart,
224
+ });
225
+ }
226
+ }
227
+
168
228
  async function* yieldFromStore<TEnv>(
169
229
  entry: PrerenderEntry,
170
230
  ctx: MatchContext<TEnv>,
@@ -231,64 +291,15 @@ async function* yieldFromStore<TEnv>(
231
291
  yield segment;
232
292
  }
233
293
 
234
- // Resolve loaders fresh (loaders are never pre-rendered/cached)
235
- const ms = ctx.metricsStore;
236
- const loaderStart = performance.now();
237
-
238
- if (ctx.isFullMatch) {
239
- if (resolveLoadersOnly) {
240
- const loaderSegments = await ctx.Store.run(() =>
241
- resolveLoadersOnly(ctx.entries, ctx.handlerContext),
242
- );
243
- state.matchedIds = state.cachedMatchedIds!;
244
- for (const segment of loaderSegments) {
245
- yield segment;
246
- }
247
- } else {
248
- state.matchedIds = state.cachedMatchedIds!;
249
- }
250
- } else {
251
- if (resolveLoadersOnlyWithRevalidation) {
252
- const loaderResult = await ctx.Store.run(() =>
253
- resolveLoadersOnlyWithRevalidation(
254
- ctx.entries,
255
- ctx.handlerContext,
256
- ctx.clientSegmentSet,
257
- ctx.prevParams,
258
- ctx.request,
259
- ctx.prevUrl,
260
- ctx.url,
261
- ctx.routeKey,
262
- ctx.actionContext,
263
- ctx.stale || undefined,
264
- ),
265
- );
266
- state.matchedIds = [
267
- ...state.cachedMatchedIds!,
268
- ...loaderResult.matchedIds,
269
- ];
270
- for (const segment of loaderResult.segments) {
271
- yield segment;
272
- }
273
- } else {
274
- state.matchedIds = state.cachedMatchedIds!;
275
- }
276
- }
277
-
278
- if (ms) {
279
- const loaderEnd = performance.now();
280
- ms.metrics.push({
281
- label: "pipeline:loader-resolve",
282
- duration: loaderEnd - loaderStart,
283
- startTime: loaderStart - ms.requestStart,
284
- depth: 1,
285
- });
286
- ms.metrics.push({
287
- label: "pipeline:cache-hit",
288
- duration: loaderEnd - pipelineStart,
289
- startTime: pipelineStart - ms.requestStart,
290
- });
291
- }
294
+ // Resolve loaders fresh (loaders are never pre-rendered/cached).
295
+ yield* resolveFreshLoadersAndYield(
296
+ ctx,
297
+ state,
298
+ pipelineStart,
299
+ ctx.metricsStore,
300
+ resolveLoadersOnly,
301
+ resolveLoadersOnlyWithRevalidation,
302
+ );
292
303
  }
293
304
 
294
305
  /**
@@ -367,10 +378,6 @@ export function withCacheLookup<TEnv>(
367
378
  resolveLoadersOnly,
368
379
  } = getRouterContext<TEnv>();
369
380
 
370
- // Prerender lookup: check build-time cached data before runtime cache.
371
- // Prerender data is available regardless of runtime cache configuration.
372
- // Skip for HMR requests — the dev prerender endpoint reads from a stale
373
- // RouterRegistry snapshot; rendering fresh ensures edits are visible.
374
381
  const isHmr = !!ctx.request.headers.get("X-RSC-HMR");
375
382
  if (!ctx.isAction && !isHmr && ctx.matched.pr) {
376
383
  await ensurePrerenderDeps();
@@ -385,14 +392,6 @@ export function withCacheLookup<TEnv>(
385
392
  }
386
393
  }
387
394
 
388
- // Dev-mode static handler interception for non-Node.js runtimes.
389
- // __PRERENDER_DEV_URL is set by the Vite plugin when the RSC environment
390
- // lacks a Node.js module runner (e.g. workerd, Deno workers). In those
391
- // runtimes, handlers that depend on Node APIs like node:fs can't run
392
- // in-process. We redirect them to the /__rsc_prerender endpoint which
393
- // resolves segments in a Node.js temp server, same as prerender routes.
394
- // In Node.js dev mode this variable is undefined -- handlers run
395
- // in-process where Node APIs work, so no interception is needed.
396
395
  if (!ctx.isAction && !ctx.matched.pr && globalThis.__PRERENDER_DEV_URL) {
397
396
  const hasStatic = ctx.entries.some(
398
397
  (e) =>
@@ -415,9 +414,7 @@ export function withCacheLookup<TEnv>(
415
414
  }
416
415
  }
417
416
 
418
- // Skip cache during actions
419
417
  if (ctx.isAction || !ctx.cacheScope?.enabled) {
420
- // Cache miss - pass through to segment resolution
421
418
  yield* source;
422
419
  if (ms) {
423
420
  ms.metrics.push({
@@ -429,7 +426,6 @@ export function withCacheLookup<TEnv>(
429
426
  return;
430
427
  }
431
428
 
432
- // Lookup cache
433
429
  const cacheResult = await ctx.cacheScope.lookupRoute(
434
430
  ctx.pathname,
435
431
  ctx.matched.params,
@@ -437,7 +433,6 @@ export function withCacheLookup<TEnv>(
437
433
  );
438
434
 
439
435
  if (!cacheResult) {
440
- // Cache miss - pass through to segment resolution
441
436
  yield* source;
442
437
  if (ms) {
443
438
  ms.metrics.push({
@@ -449,16 +444,12 @@ export function withCacheLookup<TEnv>(
449
444
  return;
450
445
  }
451
446
 
452
- // Cache HIT
453
447
  state.cacheHit = true;
454
448
  state.cacheSource = "runtime";
455
449
  state.shouldRevalidate = cacheResult.shouldRevalidate;
456
450
  state.cachedSegments = cacheResult.segments;
457
451
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
458
452
 
459
- // Apply revalidation to cached segments.
460
- // For full matches or empty client segment sets, this map is unnecessary:
461
- // we never run segment-level revalidation and can stream segments directly.
462
453
  const canCheckSegmentRevalidation =
463
454
  !ctx.isFullMatch &&
464
455
  ctx.clientSegmentSet.size > 0 &&
@@ -468,7 +459,6 @@ export function withCacheLookup<TEnv>(
468
459
  : undefined;
469
460
 
470
461
  for (const segment of cacheResult.segments) {
471
- // Skip segments client doesn't have - they need their component
472
462
  if (!ctx.clientSegmentSet.has(segment.id)) {
473
463
  if (isTraceActive()) {
474
464
  pushRevalidationTraceEntry({
@@ -485,19 +475,13 @@ export function withCacheLookup<TEnv>(
485
475
  continue;
486
476
  }
487
477
 
488
- // Skip intercept segments - they're handled separately
489
478
  if (segment.namespace?.startsWith("intercept:")) {
490
479
  yield segment;
491
480
  continue;
492
481
  }
493
482
 
494
- // Look up revalidation rules for this segment
495
483
  const entryInfo = entryRevalidateMap?.get(segment.id);
496
484
 
497
- // Even without explicit revalidation rules, route segments and their
498
- // children must re-render when params or search params change — the
499
- // handler reads ctx.params/ctx.searchParams so different values produce
500
- // different content. Matches evaluateRevalidation's default logic.
501
485
  const searchChanged = ctx.prevUrl.search !== ctx.url.search;
502
486
  const routeParamsChanged = !paramsEqual(
503
487
  ctx.matched.params,
@@ -511,7 +495,6 @@ export function withCacheLookup<TEnv>(
511
495
 
512
496
  if (!entryInfo || entryInfo.revalidate.length === 0) {
513
497
  if (shouldDefaultRevalidate) {
514
- // Params or search params changed — must re-render even without custom rules
515
498
  if (isTraceActive()) {
516
499
  pushRevalidationTraceEntry({
517
500
  segmentId: segment.id,
@@ -528,7 +511,6 @@ export function withCacheLookup<TEnv>(
528
511
  yield segment;
529
512
  continue;
530
513
  }
531
- // No revalidation rules, use default behavior (skip if client has)
532
514
  if (isTraceActive()) {
533
515
  pushRevalidationTraceEntry({
534
516
  segmentId: segment.id,
@@ -546,7 +528,6 @@ export function withCacheLookup<TEnv>(
546
528
  continue;
547
529
  }
548
530
 
549
- // Evaluate revalidation rules
550
531
  const shouldRevalidate = await evaluateRevalidation({
551
532
  segment,
552
533
  prevParams: ctx.prevParams,
@@ -580,7 +561,6 @@ export function withCacheLookup<TEnv>(
580
561
  }
581
562
 
582
563
  if (!shouldRevalidate) {
583
- // Client has it, no revalidation needed
584
564
  segment.component = null;
585
565
  segment.loading = undefined;
586
566
  }
@@ -588,7 +568,6 @@ export function withCacheLookup<TEnv>(
588
568
  yield segment;
589
569
  }
590
570
 
591
- // Set streaming flag (once) and resolve render barrier.
592
571
  const barrierReqCtx = _getRequestContext();
593
572
  if (barrierReqCtx) {
594
573
  if (barrierReqCtx._treeHasStreaming === undefined) {
@@ -597,77 +576,15 @@ export function withCacheLookup<TEnv>(
597
576
  barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
598
577
  }
599
578
 
600
- // Resolve loaders fresh (loaders are NOT cached by default)
601
- // This ensures fresh data even on cache hit
602
- const Store = ctx.Store;
603
- const loaderStart = performance.now();
604
-
605
- if (ctx.isFullMatch) {
606
- // Full match (document request) - simple loader resolution without revalidation
607
- if (resolveLoadersOnly) {
608
- const loaderSegments = await Store.run(() =>
609
- resolveLoadersOnly(ctx.entries, ctx.handlerContext),
610
- );
611
-
612
- // Update state - full match doesn't track matchedIds separately
613
- state.matchedIds = state.cachedMatchedIds!;
614
-
615
- // Yield fresh loader segments
616
- for (const segment of loaderSegments) {
617
- yield segment;
618
- }
619
- } else {
620
- state.matchedIds = state.cachedMatchedIds!;
621
- }
622
- } else {
623
- // Partial match (navigation) - loader resolution with revalidation
624
- if (resolveLoadersOnlyWithRevalidation) {
625
- const loaderResult = await Store.run(() =>
626
- resolveLoadersOnlyWithRevalidation(
627
- ctx.entries,
628
- ctx.handlerContext,
629
- ctx.clientSegmentSet,
630
- ctx.prevParams,
631
- ctx.request,
632
- ctx.prevUrl,
633
- ctx.url,
634
- ctx.routeKey,
635
- ctx.actionContext,
636
- // Loaders are never cached in the segment cache, so segment
637
- // staleness (cacheResult.shouldRevalidate) must not propagate.
638
- // But browser-sent staleness (ctx.stale) — indicating an action
639
- // happened in this or another tab — must still reach loaders.
640
- ctx.stale || undefined,
641
- ),
642
- );
643
-
644
- // Update state with fresh loader matchedIds
645
- state.matchedIds = [
646
- ...state.cachedMatchedIds!,
647
- ...loaderResult.matchedIds,
648
- ];
649
-
650
- // Yield fresh loader segments
651
- for (const segment of loaderResult.segments) {
652
- yield segment;
653
- }
654
- } else {
655
- state.matchedIds = state.cachedMatchedIds!;
656
- }
657
- }
658
- if (ms) {
659
- const loaderEnd = performance.now();
660
- ms.metrics.push({
661
- label: "pipeline:loader-resolve",
662
- duration: loaderEnd - loaderStart,
663
- startTime: loaderStart - ms.requestStart,
664
- depth: 1,
665
- });
666
- ms.metrics.push({
667
- label: "pipeline:cache-hit",
668
- duration: loaderEnd - pipelineStart,
669
- startTime: pipelineStart - ms.requestStart,
670
- });
671
- }
579
+ // Resolve loaders fresh (loaders are never cached). Shared with the
580
+ // prerender-store path via resolveFreshLoadersAndYield.
581
+ yield* resolveFreshLoadersAndYield(
582
+ ctx,
583
+ state,
584
+ pipelineStart,
585
+ ms,
586
+ resolveLoadersOnly,
587
+ resolveLoadersOnlyWithRevalidation,
588
+ );
672
589
  };
673
590
  }
@@ -123,21 +123,14 @@ export function withCacheStore<TEnv>(
123
123
  ): AsyncGenerator<ResolvedSegment> {
124
124
  const ms = ctx.metricsStore;
125
125
 
126
- // Collect all segments while passing them through
127
126
  const allSegments: ResolvedSegment[] = [];
128
127
  for await (const segment of source) {
129
128
  allSegments.push(segment);
130
129
  yield segment;
131
130
  }
132
131
 
133
- // Measure own work only (after source iteration completes)
134
132
  const ownStart = performance.now();
135
133
 
136
- // Skip caching if:
137
- // 1. Cache miss but cache scope is disabled
138
- // 2. This is an action (actions don't cache)
139
- // 3. Cache was already hit (no need to re-cache)
140
- // 4. Non-GET request (only cache GET requests)
141
134
  if (
142
135
  !ctx.cacheScope?.enabled ||
143
136
  ctx.isAction ||
@@ -162,13 +155,8 @@ export function withCacheStore<TEnv>(
162
155
  createHandleStore,
163
156
  } = getRouterContext<TEnv>();
164
157
 
165
- // Combine main segments with intercept segments
166
158
  const allSegmentsToCache = [...allSegments, ...state.interceptSegments];
167
159
 
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.
172
160
  const hasNullComponents = allSegmentsToCache.some(
173
161
  (s) =>
174
162
  s.component === null &&
@@ -184,10 +172,7 @@ export function withCacheStore<TEnv>(
184
172
  ? getOrCreateRequestId(ctx.request)
185
173
  : undefined;
186
174
 
187
- // Register onResponse callback to skip caching for non-200 responses
188
- // Note: error/notFound status codes are set elsewhere (not caching-specific)
189
175
  requestCtx.onResponse((response) => {
190
- // Only cache successful responses
191
176
  if (response.status !== 200) {
192
177
  debugLog("cacheStore", "skipping cache for non-200 response", {
193
178
  status: response.status,
@@ -197,10 +182,7 @@ export function withCacheStore<TEnv>(
197
182
  }
198
183
 
199
184
  if (hasNullComponents) {
200
- // Proactive caching: render all segments fresh in background
201
- // This ensures cache has complete components for future requests
202
185
  requestCtx.waitUntil(async () => {
203
- // Prevent background metrics from polluting foreground timeline.
204
186
  const savedMetrics = ctx.Store.metrics;
205
187
  ctx.Store.metrics = undefined;
206
188
 
@@ -208,15 +190,9 @@ export function withCacheStore<TEnv>(
208
190
  debugLog("cacheStore", "proactive caching started", {
209
191
  pathname: ctx.pathname,
210
192
  });
211
- // Swap to a fresh HandleStore so handle.push() calls from
212
- // proactive resolution are captured (not silenced). The original
213
- // store's stream is already sent by waitUntil time.
214
- // cacheRoute reads from requestCtx._handleStore, so this ensures
215
- // complete handle data (e.g. breadcrumbs) is cached.
216
193
  const originalHandleStore = requestCtx._handleStore;
217
194
  requestCtx._handleStore = createHandleStore();
218
195
  try {
219
- // Create fresh context for proactive caching
220
196
  const proactiveHandlerContext = createHandlerContext(
221
197
  ctx.matched.params,
222
198
  ctx.request,
@@ -231,12 +207,8 @@ export function withCacheStore<TEnv>(
231
207
  );
232
208
  const proactiveLoaderPromises = new Map<string, Promise<any>>();
233
209
 
234
- // Use normal loader access so handle data is captured
235
210
  setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
236
211
 
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.
240
212
  const Store = ctx.Store;
241
213
  const freshSegments = await Store.run(() =>
242
214
  resolveAllSegments(
@@ -249,7 +221,6 @@ export function withCacheStore<TEnv>(
249
221
  ),
250
222
  );
251
223
 
252
- // Also resolve intercept segments fresh if applicable
253
224
  let freshInterceptSegments: ResolvedSegment[] = [];
254
225
  if (ctx.interceptResult) {
255
226
  freshInterceptSegments = await Store.run(() =>
@@ -301,8 +272,6 @@ export function withCacheStore<TEnv>(
301
272
  }
302
273
  });
303
274
  } else {
304
- // All segments have components - cache directly
305
- // Schedule caching in waitUntil since cacheRoute is now async (key resolution)
306
275
  if (INTERNAL_RANGO_DEBUG) {
307
276
  console.log(
308
277
  `[RSC CacheStore][req:${reqId}] Direct cache path: scheduling cacheRoute for ${ctx.pathname} (${allSegmentsToCache.length} segments, hasNullComponents=${hasNullComponents})`,