@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -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,9 +92,14 @@
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
- import { getRequestContext } from "../../server/request-context.js";
99
+ import {
100
+ getRequestContext,
101
+ _getRequestContext,
102
+ } from "../../server/request-context.js";
98
103
 
99
104
  // Lazily initialized prerender store singleton and dynamically imported deps.
100
105
  // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
@@ -109,7 +114,7 @@ let _restoreHandles:
109
114
  let _hashParams:
110
115
  | typeof import("../../prerender/param-hash.js").hashParams
111
116
  | undefined;
112
- let _getRequestContext:
117
+ let _lazyGetRequestContext:
113
118
  | typeof import("../../server/request-context.js").getRequestContext
114
119
  | undefined;
115
120
 
@@ -141,7 +146,7 @@ async function ensurePrerenderDeps() {
141
146
  _deserializeSegments = codec.deserializeSegments;
142
147
  _restoreHandles = snapshot.restoreHandles;
143
148
  _hashParams = paramHash.hashParams;
144
- _getRequestContext = reqCtx.getRequestContext;
149
+ _lazyGetRequestContext = reqCtx.getRequestContext;
145
150
  if (prerenderStoreInstance === undefined) {
146
151
  prerenderStoreInstance = store.createPrerenderStore();
147
152
  }
@@ -167,7 +172,7 @@ async function* yieldFromStore<TEnv>(
167
172
  !_deserializeSegments ||
168
173
  !_restoreHandles ||
169
174
  !_hashParams ||
170
- !_getRequestContext
175
+ !_lazyGetRequestContext
171
176
  ) {
172
177
  throw new Error("yieldFromStore called before ensurePrerenderDeps");
173
178
  }
@@ -176,12 +181,13 @@ async function* yieldFromStore<TEnv>(
176
181
 
177
182
  // Replay handle data (same as runtime cache hit path).
178
183
  // Prefer the eagerly-captured handleStoreRef to avoid ALS disruption in workerd.
179
- const handleStore = handleStoreRef ?? _getRequestContext()?._handleStore;
184
+ const handleStore = handleStoreRef ?? _lazyGetRequestContext()?._handleStore;
180
185
  if (handleStore) {
181
186
  _restoreHandles(entry.handles, handleStore);
182
187
  }
183
188
 
184
189
  state.cacheHit = true;
190
+ state.cacheSource = "prerender";
185
191
  state.cachedSegments = segments;
186
192
  state.cachedMatchedIds = segments.map((s) => s.id);
187
193
 
@@ -288,7 +294,7 @@ export function withCacheLookup<TEnv>(
288
294
  // can disrupt AsyncLocalStorage, causing getRequestContext() to return
289
295
  // undefined afterward. Capturing the reference early ensures handle replay
290
296
  // and handler handle-push work regardless of ALS state.
291
- const handleStoreRef = getRequestContext()?._handleStore;
297
+ const handleStoreRef = _getRequestContext()?._handleStore;
292
298
 
293
299
  const {
294
300
  evaluateRevalidation,
@@ -303,13 +309,21 @@ export function withCacheLookup<TEnv>(
303
309
  await ensurePrerenderDeps();
304
310
  if (prerenderStoreInstance) {
305
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
+ );
306
317
 
307
318
  if (ctx.isIntercept) {
308
319
  // Intercept navigation: try intercept-specific prerender entry
309
320
  const entry = await prerenderStoreInstance.get(
310
321
  ctx.matched.routeKey,
311
322
  paramHash + "/i",
312
- { pathname: ctx.pathname },
323
+ {
324
+ pathname: ctx.pathname,
325
+ isPassthroughRoute: isPassthroughPrerenderRoute,
326
+ },
313
327
  );
314
328
  if (entry) {
315
329
  yield* yieldFromStore(
@@ -328,7 +342,10 @@ export function withCacheLookup<TEnv>(
328
342
  const entry = await prerenderStoreInstance.get(
329
343
  ctx.matched.routeKey,
330
344
  paramHash,
331
- { pathname: ctx.pathname },
345
+ {
346
+ pathname: ctx.pathname,
347
+ isPassthroughRoute: isPassthroughPrerenderRoute,
348
+ },
332
349
  );
333
350
  if (entry) {
334
351
  yield* yieldFromStore(
@@ -364,12 +381,20 @@ export function withCacheLookup<TEnv>(
364
381
  await ensurePrerenderDeps();
365
382
  if (prerenderStoreInstance) {
366
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
+ );
367
389
 
368
390
  if (ctx.isIntercept) {
369
391
  const entry = await prerenderStoreInstance.get(
370
392
  ctx.matched.routeKey,
371
393
  paramHash + "/i",
372
- { pathname: ctx.pathname },
394
+ {
395
+ pathname: ctx.pathname,
396
+ isPassthroughRoute: isPassthroughPrerenderRoute,
397
+ },
373
398
  );
374
399
  if (entry) {
375
400
  yield* yieldFromStore(
@@ -386,7 +411,10 @@ export function withCacheLookup<TEnv>(
386
411
  const entry = await prerenderStoreInstance.get(
387
412
  ctx.matched.routeKey,
388
413
  paramHash,
389
- { pathname: ctx.pathname },
414
+ {
415
+ pathname: ctx.pathname,
416
+ isPassthroughRoute: isPassthroughPrerenderRoute,
417
+ },
390
418
  );
391
419
  if (entry) {
392
420
  yield* yieldFromStore(
@@ -439,6 +467,7 @@ export function withCacheLookup<TEnv>(
439
467
 
440
468
  // Cache HIT
441
469
  state.cacheHit = true;
470
+ state.cacheSource = "runtime";
442
471
  state.shouldRevalidate = cacheResult.shouldRevalidate;
443
472
  state.cachedSegments = cacheResult.segments;
444
473
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
@@ -457,6 +486,17 @@ export function withCacheLookup<TEnv>(
457
486
  for (const segment of cacheResult.segments) {
458
487
  // Skip segments client doesn't have - they need their component
459
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
+ }
460
500
  yield segment;
461
501
  continue;
462
502
  }
@@ -471,6 +511,17 @@ export function withCacheLookup<TEnv>(
471
511
  const entryInfo = entryRevalidateMap?.get(segment.id);
472
512
  if (!entryInfo || entryInfo.revalidate.length === 0) {
473
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
+ }
474
525
  segment.component = null;
475
526
  segment.loading = undefined;
476
527
  yield segment;
@@ -492,8 +543,24 @@ export function withCacheLookup<TEnv>(
492
543
  routeKey: ctx.routeKey,
493
544
  context: ctx.handlerContext,
494
545
  actionContext: ctx.actionContext,
546
+ stale: cacheResult.shouldRevalidate || undefined,
547
+ traceSource: "cache-hit",
495
548
  });
496
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
+
497
564
  if (!shouldRevalidate) {
498
565
  // Client has it, no revalidation needed
499
566
  segment.component = null;
@@ -538,6 +605,7 @@ export function withCacheLookup<TEnv>(
538
605
  ctx.url,
539
606
  ctx.routeKey,
540
607
  ctx.actionContext,
608
+ cacheResult.shouldRevalidate || undefined,
541
609
  ),
542
610
  );
543
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,
@@ -86,19 +86,14 @@
86
86
  * -> output: cached segments + fresh loader data
87
87
  *
88
88
  *
89
- * TWO PIPELINE VARIANTS
90
- * =====================
91
- *
92
- * 1. createMatchPipeline (Full Match)
93
- * - Used for document requests (initial page load)
94
- * - No revalidation logic (no previous state to compare)
95
- * - Simpler segment resolution
96
- *
97
- * 2. createMatchPartialPipeline (Partial Match)
98
- * - Used for client-side navigation
99
- * - Includes revalidation for SWR
100
- * - Compares with previous params/URL
101
- * - Supports intercepts (soft navigation modals)
89
+ * PIPELINE VARIANT
90
+ * ================
91
+ *
92
+ * createMatchPartialPipeline handles both full (document) and partial
93
+ * (navigation) requests. The middleware steps adapt based on ctx.isFullMatch:
94
+ * - cache-lookup/store work for both
95
+ * - background-revalidation is a no-op for full matches (no stale state)
96
+ * - intercept-resolution is a no-op for full matches (no previous navigation)
102
97
  */
103
98
  import type { ResolvedSegment } from "../types.js";
104
99
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
@@ -182,33 +177,3 @@ export function createMatchPartialPipeline<TEnv>(
182
177
  // Start with empty source - cache lookup or segment resolution will produce segments
183
178
  return pipeline(empty());
184
179
  }
185
-
186
- /**
187
- * Create the full match pipeline (simpler, no revalidation)
188
- *
189
- * Used for document requests (initial page load) where we don't need
190
- * revalidation logic since there's no previous state to compare against.
191
- */
192
- export function createMatchPipeline<TEnv>(
193
- ctx: MatchContext<TEnv>,
194
- state: MatchPipelineState,
195
- ): AsyncGenerator<ResolvedSegment> {
196
- // For full match, we only need:
197
- // 1. Cache lookup
198
- // 2. Segment resolution (without revalidation)
199
- // 3. Intercept resolution
200
- // 4. Cache store
201
-
202
- // Note: Full match uses different resolution logic (resolveAllSegments instead of
203
- // resolveAllSegmentsWithRevalidation). This will be handled by the segment resolution
204
- // middleware checking ctx.isFullMatch or similar flag.
205
-
206
- const pipeline = compose<ResolvedSegment>(
207
- withCacheStore(ctx, state),
208
- withInterceptResolution(ctx, state),
209
- withSegmentResolution(ctx, state),
210
- withCacheLookup(ctx, state),
211
- );
212
-
213
- return pipeline(empty());
214
- }
@@ -108,7 +108,6 @@
108
108
  */
109
109
  import type { MatchResult, ResolvedSegment } from "../types.js";
110
110
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
111
- import { generateServerTiming, logMetrics } from "./metrics.js";
112
111
  import { debugLog } from "./logging.js";
113
112
 
114
113
  /**
@@ -186,20 +185,12 @@ export function buildMatchResult<TEnv>(
186
185
  segmentIds: segmentsToRender.map((s) => s.id),
187
186
  });
188
187
 
189
- // Output metrics if enabled
190
- let serverTiming: string | undefined;
191
- if (ctx.metricsStore) {
192
- logMetrics(ctx.request.method, ctx.pathname, ctx.metricsStore);
193
- serverTiming = generateServerTiming(ctx.metricsStore);
194
- }
195
-
196
188
  return {
197
189
  segments: segmentsToRender,
198
190
  matched: allIds,
199
191
  diff: segmentsToRender.map((s) => s.id),
200
192
  params: ctx.matched.params,
201
193
  routeName: ctx.routeKey,
202
- serverTiming,
203
194
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
204
195
  routeMiddleware:
205
196
  ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,