@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
@@ -22,9 +22,21 @@ import {
22
22
  matchError as _matchError,
23
23
  } from "./match-api.js";
24
24
  import { previewMatch as _previewMatch } from "./preview-match.js";
25
- import { runWithRouterLogContext, withRouterLogScope } from "./logging.js";
25
+ import {
26
+ runWithRouterLogContext,
27
+ withRouterLogScope,
28
+ isRouterDebugEnabled,
29
+ startRevalidationTrace,
30
+ flushRevalidationTrace,
31
+ } from "./logging.js";
26
32
  import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
27
33
  import type { MiddlewareFn } from "./middleware.js";
34
+ import {
35
+ type TelemetrySink,
36
+ safeEmit,
37
+ resolveSink,
38
+ getRequestId,
39
+ } from "./telemetry.js";
28
40
 
29
41
  export interface MatchHandlerDeps<TEnv = any> {
30
42
  buildRouterContext: () => RouterContext<TEnv>;
@@ -38,6 +50,7 @@ export interface MatchHandlerDeps<TEnv = any> {
38
50
  selectorContext: InterceptSelectorContext | null,
39
51
  isAction: boolean,
40
52
  ) => { intercept: InterceptEntry; entry: EntryData } | null;
53
+ telemetry?: TelemetrySink;
41
54
  }
42
55
 
43
56
  export interface MatchHandlers<TEnv = any> {
@@ -98,6 +111,8 @@ export function createMatchHandlers<TEnv = any>(
98
111
  defaultErrorBoundary,
99
112
  findInterceptForRoute,
100
113
  } = deps;
114
+ const hasTelemetry = !!deps.telemetry;
115
+ const telemetry = resolveSink(deps.telemetry);
101
116
 
102
117
  async function createMatchContextForFull(
103
118
  request: Request,
@@ -140,13 +155,43 @@ export function createMatchHandlers<TEnv = any>(
140
155
  * - background-revalidation: SWR revalidation
141
156
  */
142
157
  async function match(request: Request, env: TEnv): Promise<MatchResult> {
143
- return runWithRouterLogContext({ request, transaction: "match" }, () =>
144
- runWithRouterContext(buildRouterContext(), async () =>
158
+ const requestId = hasTelemetry ? getRequestId(request) : undefined;
159
+ return runWithRouterLogContext({ request, transaction: "match" }, () => {
160
+ const routerCtx = buildRouterContext();
161
+ routerCtx.requestId = requestId;
162
+ return runWithRouterContext(routerCtx, async () =>
145
163
  withRouterLogScope("match", async () => {
164
+ const matchStart = performance.now();
165
+ const pathname = new URL(request.url).pathname;
166
+ if (hasTelemetry) {
167
+ safeEmit(telemetry, {
168
+ type: "request.start",
169
+ timestamp: matchStart,
170
+ requestId,
171
+ method: request.method,
172
+ pathname,
173
+ transaction: "match",
174
+ isPartial: false,
175
+ });
176
+ }
177
+
146
178
  const result = await createMatchContextForFull(request, env);
147
179
 
148
180
  // Handle redirect case
149
181
  if ("type" in result && result.type === "redirect") {
182
+ if (hasTelemetry) {
183
+ safeEmit(telemetry, {
184
+ type: "request.end",
185
+ timestamp: performance.now(),
186
+ requestId,
187
+ method: request.method,
188
+ pathname,
189
+ transaction: "match",
190
+ durationMs: performance.now() - matchStart,
191
+ segmentCount: 0,
192
+ cacheHit: false,
193
+ });
194
+ }
150
195
  return {
151
196
  segments: [],
152
197
  matched: [],
@@ -161,8 +206,47 @@ export function createMatchHandlers<TEnv = any>(
161
206
  try {
162
207
  const state = createPipelineState();
163
208
  const pipeline = createMatchPartialPipeline(ctx, state);
164
- return await collectMatchResult(pipeline, ctx, state);
209
+ const matchResult = await collectMatchResult(pipeline, ctx, state);
210
+ if (hasTelemetry) {
211
+ safeEmit(telemetry, {
212
+ type: "cache.decision",
213
+ timestamp: performance.now(),
214
+ requestId,
215
+ pathname,
216
+ routeKey: ctx.routeKey,
217
+ hit: state.cacheHit,
218
+ shouldRevalidate: !!state.shouldRevalidate,
219
+ source: state.cacheSource,
220
+ });
221
+ safeEmit(telemetry, {
222
+ type: "request.end",
223
+ timestamp: performance.now(),
224
+ requestId,
225
+ method: request.method,
226
+ pathname,
227
+ transaction: "match",
228
+ durationMs: performance.now() - matchStart,
229
+ segmentCount: matchResult.segments.length,
230
+ cacheHit: state.cacheHit,
231
+ });
232
+ }
233
+ return matchResult;
165
234
  } catch (error) {
235
+ if (hasTelemetry) {
236
+ const errorObj =
237
+ error instanceof Error ? error : new Error(String(error));
238
+ safeEmit(telemetry, {
239
+ type: "request.error",
240
+ timestamp: performance.now(),
241
+ requestId,
242
+ method: request.method,
243
+ pathname,
244
+ transaction: "match",
245
+ error: errorObj,
246
+ phase: error instanceof Response ? "redirect" : "routing",
247
+ durationMs: performance.now() - matchStart,
248
+ });
249
+ }
166
250
  if (error instanceof Response) throw error;
167
251
  // Report unhandled errors during full match pipeline
168
252
  callOnError(error, "routing", {
@@ -175,8 +259,8 @@ export function createMatchHandlers<TEnv = any>(
175
259
  throw sanitizeError(error);
176
260
  }
177
261
  }),
178
- ),
179
- );
262
+ );
263
+ });
180
264
  }
181
265
 
182
266
  async function matchError(
@@ -214,23 +298,112 @@ export function createMatchHandlers<TEnv = any>(
214
298
  context: TEnv,
215
299
  actionContext?: ActionContext,
216
300
  ): Promise<MatchResult | null> {
301
+ const partialRequestId = hasTelemetry ? getRequestId(request) : undefined;
217
302
  return runWithRouterLogContext(
218
303
  { request, transaction: "matchPartial" },
219
- () =>
220
- runWithRouterContext(buildRouterContext(), async () =>
304
+ () => {
305
+ const routerCtx = buildRouterContext();
306
+ routerCtx.requestId = partialRequestId;
307
+ return runWithRouterContext(routerCtx, async () =>
221
308
  withRouterLogScope("matchPartial", async () => {
309
+ const matchStart = performance.now();
310
+ const pathname = new URL(request.url).pathname;
311
+ if (hasTelemetry) {
312
+ safeEmit(telemetry, {
313
+ type: "request.start",
314
+ timestamp: matchStart,
315
+ requestId: partialRequestId,
316
+ method: request.method,
317
+ pathname,
318
+ transaction: "matchPartial",
319
+ isPartial: true,
320
+ });
321
+ }
322
+
222
323
  const ctx = await createMatchContextForPartial(
223
324
  request,
224
325
  context,
225
326
  actionContext,
226
327
  );
227
- if (!ctx) return null;
328
+ if (!ctx) {
329
+ if (hasTelemetry) {
330
+ safeEmit(telemetry, {
331
+ type: "request.end",
332
+ timestamp: performance.now(),
333
+ requestId: partialRequestId,
334
+ method: request.method,
335
+ pathname,
336
+ transaction: "matchPartial",
337
+ durationMs: performance.now() - matchStart,
338
+ segmentCount: 0,
339
+ cacheHit: false,
340
+ });
341
+ }
342
+ return null;
343
+ }
344
+
345
+ if (isRouterDebugEnabled()) {
346
+ startRevalidationTrace({
347
+ method: request.method,
348
+ prevUrl: ctx.prevUrl.href,
349
+ nextUrl: ctx.url.href,
350
+ routeKey: ctx.routeKey,
351
+ isAction: !!actionContext,
352
+ stale: ctx.stale || undefined,
353
+ });
354
+ }
228
355
 
229
356
  try {
230
357
  const state = createPipelineState();
231
358
  const pipeline = createMatchPartialPipeline(ctx, state);
232
- return await collectMatchResult(pipeline, ctx, state);
359
+ const matchResult = await collectMatchResult(
360
+ pipeline,
361
+ ctx,
362
+ state,
363
+ );
364
+ flushRevalidationTrace();
365
+ if (hasTelemetry) {
366
+ safeEmit(telemetry, {
367
+ type: "cache.decision",
368
+ timestamp: performance.now(),
369
+ requestId: partialRequestId,
370
+ pathname,
371
+ routeKey: ctx.routeKey,
372
+ hit: state.cacheHit,
373
+ shouldRevalidate: !!state.shouldRevalidate,
374
+ source: state.cacheSource,
375
+ });
376
+ safeEmit(telemetry, {
377
+ type: "request.end",
378
+ timestamp: performance.now(),
379
+ requestId: partialRequestId,
380
+ method: request.method,
381
+ pathname,
382
+ transaction: "matchPartial",
383
+ durationMs: performance.now() - matchStart,
384
+ segmentCount: matchResult.segments.length,
385
+ cacheHit: state.cacheHit,
386
+ });
387
+ }
388
+ return matchResult;
233
389
  } catch (error) {
390
+ flushRevalidationTrace();
391
+ if (hasTelemetry) {
392
+ const errorObj =
393
+ error instanceof Error ? error : new Error(String(error));
394
+ const phase = actionContext ? "action" : "revalidation";
395
+ safeEmit(telemetry, {
396
+ type: "request.error",
397
+ timestamp: performance.now(),
398
+ requestId: partialRequestId,
399
+ method: request.method,
400
+ pathname,
401
+ transaction: "matchPartial",
402
+ error: errorObj,
403
+ phase: error instanceof Response ? "redirect" : phase,
404
+ durationMs: performance.now() - matchStart,
405
+ });
406
+ }
234
407
  if (error instanceof Response) throw error;
235
408
  // Report unhandled errors during partial match pipeline
236
409
  callOnError(error, actionContext ? "action" : "revalidation", {
@@ -244,7 +417,8 @@ export function createMatchHandlers<TEnv = any>(
244
417
  throw sanitizeError(error);
245
418
  }
246
419
  }),
247
- ),
420
+ );
421
+ },
248
422
  );
249
423
  }
250
424
 
@@ -30,23 +30,15 @@
30
30
  * |
31
31
  * v (async, doesn't block response)
32
32
  * +---------------------------+
33
- * | Create fresh handleStore | Isolate from response stream
33
+ * | Create fresh context | Fresh handleStore, handlerContext,
34
+ * | (full isolation) | and loaderPromises map
34
35
  * +---------------------------+
35
36
  * |
36
37
  * v
37
- * +---------------------+
38
- * | isFullMatch? |
39
- * +---------------------+
40
- * |
41
- * +-----+-----+
42
- * | |
43
- * yes no
44
- * | |
45
- * v v
46
- * resolveAll resolveWithRevalidation
47
- * Segments + resolveIntercepts
48
- * | |
49
- * +-----------+
38
+ * +---------------------------+
39
+ * | resolveAllSegments() | Fresh resolution (no revalidation)
40
+ * | + resolveIntercepts() | Ensures complete components
41
+ * +---------------------------+
50
42
  * |
51
43
  * v
52
44
  * +---------------------------+
@@ -90,27 +82,22 @@
90
82
  * ISOLATION FROM RESPONSE
91
83
  * =======================
92
84
  *
93
- * The background revalidation creates a fresh handleStore:
94
- *
95
- * requestCtx._handleStore = createHandleStore();
85
+ * Background revalidation creates fully isolated context:
86
+ * - Fresh handleStore (prevents polluting the response stream)
87
+ * - Fresh handlerContext + loaderPromises (prevents reusing memoized
88
+ * loader results from the foreground pass)
89
+ * - handleStore is saved/restored in try/finally
96
90
  *
97
- * This prevents background handle.push() calls from:
98
- * - Polluting the current response stream
99
- * - Causing duplicate data in the client
100
- * - Creating race conditions
91
+ * This matches the proactive caching pattern in cache-store.ts.
101
92
  *
102
93
  *
103
- * FULL VS PARTIAL REVALIDATION
104
- * ============================
94
+ * FRESH RESOLUTION (NO REVALIDATION)
95
+ * ===================================
105
96
  *
106
- * Full Match (document request):
107
- * - Simple resolveAllSegments()
108
- * - No need to compare with previous state
109
- *
110
- * Partial Match (navigation):
111
- * - resolveAllSegmentsWithRevalidation()
112
- * - Also resolves intercept segments if applicable
113
- * - More complex but handles all scenarios
97
+ * Both full and partial requests use resolveAllSegments() (without
98
+ * revalidation logic) to ensure all segments have complete components.
99
+ * Using revalidation-aware resolution would produce null components
100
+ * for skipped segments, which would corrupt the cache entry.
114
101
  */
115
102
  import type { ResolvedSegment } from "../../types.js";
116
103
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
@@ -148,7 +135,8 @@ export function withBackgroundRevalidation<TEnv>(
148
135
  const {
149
136
  getRequestContext,
150
137
  createHandleStore,
151
- resolveAllSegmentsWithRevalidation,
138
+ createHandlerContext,
139
+ setupLoaderAccess,
152
140
  resolveAllSegments,
153
141
  resolveInterceptEntry,
154
142
  } = getRouterContext<TEnv>();
@@ -161,72 +149,62 @@ export function withBackgroundRevalidation<TEnv>(
161
149
  pathname: ctx.pathname,
162
150
  fullMatch: ctx.isFullMatch,
163
151
  });
164
- try {
165
- // Create a fresh handleStore for background revalidation
166
- // to avoid polluting the current response's handle stream
167
- if (requestCtx) {
168
- requestCtx._handleStore = createHandleStore();
169
- }
170
152
 
171
- let freshSegments: ResolvedSegment[];
153
+ // Save and replace handleStore to avoid polluting the response stream.
154
+ // Restore in finally (same pattern as proactive caching in cache-store).
155
+ const originalHandleStore = requestCtx._handleStore;
156
+ requestCtx._handleStore = createHandleStore();
172
157
 
173
- if (ctx.isFullMatch) {
174
- // Full match (document request) - simple resolution
175
- freshSegments = await resolveAllSegments(
176
- ctx.entries,
177
- ctx.routeKey,
178
- ctx.matched.params,
179
- ctx.handlerContext,
180
- ctx.loaderPromises,
181
- );
182
- } else {
183
- // Partial match (navigation) - resolution with revalidation
184
- const freshResult = await resolveAllSegmentsWithRevalidation(
158
+ try {
159
+ // Create fresh handler context and loader promises to avoid
160
+ // reusing memoized results from the foreground pass
161
+ const freshHandlerContext = createHandlerContext(
162
+ ctx.matched.params,
163
+ ctx.request,
164
+ ctx.url.searchParams,
165
+ ctx.pathname,
166
+ ctx.url,
167
+ ctx.env,
168
+ ctx.routeMap,
169
+ ctx.matched.routeKey,
170
+ ctx.matched.responseType,
171
+ ctx.matched.pt === true,
172
+ );
173
+ const freshLoaderPromises = new Map<string, Promise<any>>();
174
+ setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
175
+
176
+ // Resolve all segments fresh (without revalidation logic)
177
+ // to ensure complete components for caching
178
+ const freshSegments = await ctx.Store.run(() =>
179
+ resolveAllSegments(
185
180
  ctx.entries,
186
181
  ctx.routeKey,
187
182
  ctx.matched.params,
188
- ctx.handlerContext,
189
- ctx.clientSegmentSet,
190
- ctx.prevParams,
191
- ctx.request,
192
- ctx.prevUrl,
193
- ctx.url,
194
- ctx.loaderPromises,
195
- ctx.actionContext,
196
- ctx.interceptResult,
197
- ctx.localRouteName,
198
- ctx.pathname,
199
- );
200
-
201
- freshSegments = freshResult.segments;
183
+ freshHandlerContext,
184
+ freshLoaderPromises,
185
+ ),
186
+ );
202
187
 
203
- // For intercept revalidation, also resolve fresh intercept segments
204
- if (ctx.interceptResult) {
205
- const freshInterceptSegments = await resolveInterceptEntry(
206
- ctx.interceptResult.intercept,
207
- ctx.interceptResult.entry,
188
+ // Also resolve intercept segments fresh if applicable
189
+ let freshInterceptSegments: ResolvedSegment[] = [];
190
+ if (ctx.interceptResult) {
191
+ freshInterceptSegments = await ctx.Store.run(() =>
192
+ resolveInterceptEntry(
193
+ ctx.interceptResult!.intercept,
194
+ ctx.interceptResult!.entry,
208
195
  ctx.matched.params,
209
- ctx.handlerContext,
196
+ freshHandlerContext,
210
197
  true,
211
- {
212
- clientSegmentIds: ctx.clientSegmentSet,
213
- prevParams: ctx.prevParams,
214
- request: ctx.request,
215
- prevUrl: ctx.prevUrl,
216
- nextUrl: ctx.url,
217
- routeKey: ctx.routeKey,
218
- actionContext: ctx.actionContext,
219
- stale: false,
220
- },
221
- );
222
- freshSegments = [...freshSegments, ...freshInterceptSegments];
223
- }
198
+ ),
199
+ );
224
200
  }
225
201
 
202
+ const completeSegments = [...freshSegments, ...freshInterceptSegments];
203
+ requestCtx._handleStore.seal();
226
204
  await cacheScope.cacheRoute(
227
205
  ctx.pathname,
228
206
  ctx.matched.params,
229
- freshSegments,
207
+ completeSegments,
230
208
  ctx.isIntercept,
231
209
  );
232
210
  debugLog("backgroundRevalidation", "revalidation complete", {
@@ -237,6 +215,8 @@ export function withBackgroundRevalidation<TEnv>(
237
215
  pathname: ctx.pathname,
238
216
  error: String(error),
239
217
  });
218
+ } finally {
219
+ requestCtx._handleStore = originalHandleStore;
240
220
  }
241
221
  });
242
222
  };
@@ -92,6 +92,8 @@
92
92
  import type { ResolvedSegment } from "../../types.js";
93
93
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
94
94
  import { getRouterContext } from "../router-context.js";
95
+ import { resolveSink, safeEmit } from "../telemetry.js";
96
+ import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
95
97
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
96
98
  import type { HandleStore } from "../../server/handle-store.js";
97
99
  import {
@@ -185,6 +187,7 @@ async function* yieldFromStore<TEnv>(
185
187
  }
186
188
 
187
189
  state.cacheHit = true;
190
+ state.cacheSource = "prerender";
188
191
  state.cachedSegments = segments;
189
192
  state.cachedMatchedIds = segments.map((s) => s.id);
190
193
 
@@ -306,13 +309,21 @@ export function withCacheLookup<TEnv>(
306
309
  await ensurePrerenderDeps();
307
310
  if (prerenderStoreInstance) {
308
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,
316
+ );
309
317
 
310
318
  if (ctx.isIntercept) {
311
319
  // Intercept navigation: try intercept-specific prerender entry
312
320
  const entry = await prerenderStoreInstance.get(
313
321
  ctx.matched.routeKey,
314
322
  paramHash + "/i",
315
- { pathname: ctx.pathname },
323
+ {
324
+ pathname: ctx.pathname,
325
+ isPassthroughRoute: isPassthroughPrerenderRoute,
326
+ },
316
327
  );
317
328
  if (entry) {
318
329
  yield* yieldFromStore(
@@ -331,7 +342,10 @@ export function withCacheLookup<TEnv>(
331
342
  const entry = await prerenderStoreInstance.get(
332
343
  ctx.matched.routeKey,
333
344
  paramHash,
334
- { pathname: ctx.pathname },
345
+ {
346
+ pathname: ctx.pathname,
347
+ isPassthroughRoute: isPassthroughPrerenderRoute,
348
+ },
335
349
  );
336
350
  if (entry) {
337
351
  yield* yieldFromStore(
@@ -367,12 +381,20 @@ export function withCacheLookup<TEnv>(
367
381
  await ensurePrerenderDeps();
368
382
  if (prerenderStoreInstance) {
369
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,
388
+ );
370
389
 
371
390
  if (ctx.isIntercept) {
372
391
  const entry = await prerenderStoreInstance.get(
373
392
  ctx.matched.routeKey,
374
393
  paramHash + "/i",
375
- { pathname: ctx.pathname },
394
+ {
395
+ pathname: ctx.pathname,
396
+ isPassthroughRoute: isPassthroughPrerenderRoute,
397
+ },
376
398
  );
377
399
  if (entry) {
378
400
  yield* yieldFromStore(
@@ -389,7 +411,10 @@ export function withCacheLookup<TEnv>(
389
411
  const entry = await prerenderStoreInstance.get(
390
412
  ctx.matched.routeKey,
391
413
  paramHash,
392
- { pathname: ctx.pathname },
414
+ {
415
+ pathname: ctx.pathname,
416
+ isPassthroughRoute: isPassthroughPrerenderRoute,
417
+ },
393
418
  );
394
419
  if (entry) {
395
420
  yield* yieldFromStore(
@@ -442,6 +467,7 @@ export function withCacheLookup<TEnv>(
442
467
 
443
468
  // Cache HIT
444
469
  state.cacheHit = true;
470
+ state.cacheSource = "runtime";
445
471
  state.shouldRevalidate = cacheResult.shouldRevalidate;
446
472
  state.cachedSegments = cacheResult.segments;
447
473
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
@@ -460,6 +486,17 @@ export function withCacheLookup<TEnv>(
460
486
  for (const segment of cacheResult.segments) {
461
487
  // Skip segments client doesn't have - they need their component
462
488
  if (!ctx.clientSegmentSet.has(segment.id)) {
489
+ if (isTraceActive()) {
490
+ pushRevalidationTraceEntry({
491
+ segmentId: segment.id,
492
+ segmentType: segment.type,
493
+ belongsToRoute: segment.belongsToRoute ?? false,
494
+ source: "cache-hit",
495
+ defaultShouldRevalidate: true,
496
+ finalShouldRevalidate: true,
497
+ reason: "new-segment",
498
+ });
499
+ }
463
500
  yield segment;
464
501
  continue;
465
502
  }
@@ -474,6 +511,17 @@ export function withCacheLookup<TEnv>(
474
511
  const entryInfo = entryRevalidateMap?.get(segment.id);
475
512
  if (!entryInfo || entryInfo.revalidate.length === 0) {
476
513
  // No revalidation rules, use default behavior (skip if client has)
514
+ if (isTraceActive()) {
515
+ pushRevalidationTraceEntry({
516
+ segmentId: segment.id,
517
+ segmentType: segment.type,
518
+ belongsToRoute: segment.belongsToRoute ?? false,
519
+ source: "cache-hit",
520
+ defaultShouldRevalidate: false,
521
+ finalShouldRevalidate: false,
522
+ reason: "cached-no-rules",
523
+ });
524
+ }
477
525
  segment.component = null;
478
526
  segment.loading = undefined;
479
527
  yield segment;
@@ -495,8 +543,24 @@ export function withCacheLookup<TEnv>(
495
543
  routeKey: ctx.routeKey,
496
544
  context: ctx.handlerContext,
497
545
  actionContext: ctx.actionContext,
546
+ stale: cacheResult.shouldRevalidate || undefined,
547
+ traceSource: "cache-hit",
498
548
  });
499
549
 
550
+ const routerCtx = getRouterContext<TEnv>();
551
+ if (routerCtx.telemetry) {
552
+ const tSink = resolveSink(routerCtx.telemetry);
553
+ safeEmit(tSink, {
554
+ type: "revalidation.decision",
555
+ timestamp: performance.now(),
556
+ requestId: routerCtx.requestId,
557
+ segmentId: segment.id,
558
+ pathname: ctx.pathname,
559
+ routeKey: ctx.routeKey,
560
+ shouldRevalidate,
561
+ });
562
+ }
563
+
500
564
  if (!shouldRevalidate) {
501
565
  // Client has it, no revalidation needed
502
566
  segment.component = null;
@@ -541,6 +605,7 @@ export function withCacheLookup<TEnv>(
541
605
  ctx.url,
542
606
  ctx.routeKey,
543
607
  ctx.actionContext,
608
+ cacheResult.shouldRevalidate || undefined,
544
609
  ),
545
610
  );
546
611
 
@@ -211,6 +211,7 @@ export function withCacheStore<TEnv>(
211
211
  ctx.routeMap,
212
212
  ctx.matched.routeKey,
213
213
  ctx.matched.responseType,
214
+ ctx.matched.pt === true,
214
215
  );
215
216
  const proactiveLoaderPromises = new Map<string, Promise<any>>();
216
217
 
@@ -248,6 +249,7 @@ export function withCacheStore<TEnv>(
248
249
  ...freshSegments,
249
250
  ...freshInterceptSegments,
250
251
  ];
252
+ requestCtx._handleStore.seal();
251
253
  await cacheScope.cacheRoute(
252
254
  ctx.pathname,
253
255
  ctx.matched.params,