@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847

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 (298) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -212
  4. package/dist/vite/index.js +3995 -2489
  5. package/package.json +57 -52
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +6 -4
  13. package/skills/hooks/SKILL.md +328 -70
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +62 -15
  18. package/skills/loader/SKILL.md +368 -42
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +14 -10
  21. package/skills/parallel/SKILL.md +137 -1
  22. package/skills/prerender/SKILL.md +366 -28
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +136 -83
  25. package/skills/route/SKILL.md +195 -21
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +240 -102
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +102 -4
  31. package/src/bin/rango.ts +312 -15
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +92 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +24 -4
  38. package/src/browser/logging.ts +11 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +266 -558
  41. package/src/browser/navigation-client.ts +132 -75
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +297 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +303 -309
  46. package/src/browser/prefetch/cache.ts +206 -0
  47. package/src/browser/prefetch/fetch.ts +144 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +48 -0
  50. package/src/browser/prefetch/queue.ts +128 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +190 -70
  53. package/src/browser/react/NavigationProvider.tsx +78 -11
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +6 -1
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +29 -70
  65. package/src/browser/react/use-link-status.ts +6 -5
  66. package/src/browser/react/use-navigation.ts +22 -63
  67. package/src/browser/react/use-params.ts +65 -0
  68. package/src/browser/react/use-pathname.ts +47 -0
  69. package/src/browser/react/use-router.ts +63 -0
  70. package/src/browser/react/use-search-params.ts +56 -0
  71. package/src/browser/react/use-segments.ts +80 -97
  72. package/src/browser/response-adapter.ts +73 -0
  73. package/src/browser/rsc-router.tsx +188 -57
  74. package/src/browser/scroll-restoration.ts +117 -44
  75. package/src/browser/segment-reconciler.ts +221 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +488 -606
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +116 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +63 -21
  82. package/src/build/generate-route-types.ts +36 -1038
  83. package/src/build/index.ts +2 -5
  84. package/src/build/route-trie.ts +38 -12
  85. package/src/build/route-types/ast-helpers.ts +25 -0
  86. package/src/build/route-types/ast-route-extraction.ts +98 -0
  87. package/src/build/route-types/codegen.ts +102 -0
  88. package/src/build/route-types/include-resolution.ts +411 -0
  89. package/src/build/route-types/param-extraction.ts +48 -0
  90. package/src/build/route-types/per-module-writer.ts +128 -0
  91. package/src/build/route-types/router-processing.ts +479 -0
  92. package/src/build/route-types/scan-filter.ts +78 -0
  93. package/src/build/runtime-discovery.ts +231 -0
  94. package/src/cache/background-task.ts +34 -0
  95. package/src/cache/cache-key-utils.ts +44 -0
  96. package/src/cache/cache-policy.ts +125 -0
  97. package/src/cache/cache-runtime.ts +342 -0
  98. package/src/cache/cache-scope.ts +122 -303
  99. package/src/cache/cf/cf-cache-store.ts +571 -17
  100. package/src/cache/cf/index.ts +13 -3
  101. package/src/cache/document-cache.ts +116 -77
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +1 -15
  105. package/src/cache/memory-segment-store.ts +191 -13
  106. package/src/cache/profile-registry.ts +73 -0
  107. package/src/cache/read-through-swr.ts +134 -0
  108. package/src/cache/segment-codec.ts +256 -0
  109. package/src/cache/taint.ts +98 -0
  110. package/src/cache/types.ts +72 -122
  111. package/src/client.rsc.tsx +3 -1
  112. package/src/client.tsx +84 -126
  113. package/src/component-utils.ts +4 -4
  114. package/src/components/DefaultDocument.tsx +5 -1
  115. package/src/context-var.ts +86 -0
  116. package/src/debug.ts +19 -9
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +12 -7
  119. package/src/handles/MetaTags.tsx +73 -20
  120. package/src/handles/breadcrumbs.ts +66 -0
  121. package/src/handles/index.ts +1 -0
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +21 -15
  124. package/src/host/errors.ts +8 -8
  125. package/src/host/index.ts +4 -7
  126. package/src/host/pattern-matcher.ts +27 -27
  127. package/src/host/router.ts +61 -39
  128. package/src/host/testing.ts +8 -8
  129. package/src/host/types.ts +15 -7
  130. package/src/host/utils.ts +1 -1
  131. package/src/href-client.ts +65 -45
  132. package/src/index.rsc.ts +104 -40
  133. package/src/index.ts +122 -67
  134. package/src/internal-debug.ts +9 -3
  135. package/src/loader.rsc.ts +18 -93
  136. package/src/loader.ts +26 -9
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +4 -2
  140. package/src/prerender/store.ts +121 -17
  141. package/src/prerender.ts +325 -20
  142. package/src/reverse.ts +144 -124
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +7 -4
  145. package/src/route-definition/dsl-helpers.ts +959 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1450
  151. package/src/route-map-builder.ts +87 -133
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +41 -6
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +160 -0
  158. package/src/router/handler-context.ts +324 -116
  159. package/src/router/intercept-resolution.ts +11 -4
  160. package/src/router/lazy-includes.ts +237 -0
  161. package/src/router/loader-resolution.ts +179 -133
  162. package/src/router/logging.ts +112 -6
  163. package/src/router/manifest.ts +58 -19
  164. package/src/router/match-api.ts +89 -88
  165. package/src/router/match-context.ts +4 -2
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +86 -89
  168. package/src/router/match-middleware/cache-lookup.ts +295 -49
  169. package/src/router/match-middleware/cache-store.ts +56 -13
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -22
  171. package/src/router/match-middleware/segment-resolution.ts +20 -9
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +44 -21
  174. package/src/router/metrics.ts +240 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +327 -369
  178. package/src/router/pattern-matching.ts +169 -31
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +105 -14
  182. package/src/router/router-context.ts +40 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +677 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +199 -0
  189. package/src/router/segment-resolution/revalidation.ts +1296 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1354
  192. package/src/router/segment-wrappers.ts +291 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +96 -29
  197. package/src/router/types.ts +15 -9
  198. package/src/router.ts +642 -2366
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +639 -1027
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +0 -20
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +237 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +38 -11
  215. package/src/search-params.ts +66 -54
  216. package/src/segment-system.tsx +165 -17
  217. package/src/server/context.ts +237 -54
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +11 -6
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +438 -71
  223. package/src/server.ts +26 -164
  224. package/src/ssr/index.tsx +101 -31
  225. package/src/static-handler.ts +22 -4
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +773 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +109 -0
  241. package/src/types/segments.ts +150 -0
  242. package/src/types.ts +1 -1795
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -1323
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +108 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -2259
  261. package/src/vite/plugin-types.ts +48 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
  266. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +266 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +445 -0
  280. package/src/vite/router-discovery.ts +777 -0
  281. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -43
  289. package/dist/vite/index.named-routes.gen.ts +0 -103
  290. package/src/browser/lru-cache.ts +0 -69
  291. package/src/browser/request-controller.ts +0 -164
  292. package/src/cache/memory-store.ts +0 -253
  293. package/src/href-context.ts +0 -33
  294. package/src/router.gen.ts +0 -6
  295. package/src/static-handler.gen.ts +0 -5
  296. package/src/urls.gen.ts +0 -8
  297. package/src/vite/expose-internal-ids.ts +0 -1167
  298. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -70,9 +70,11 @@
70
70
  * - No segments yielded from this middleware
71
71
  *
72
72
  * Loaders:
73
- * - NEVER cached by design
73
+ * - NEVER cached in the segment cache
74
74
  * - Always resolved fresh on every request
75
75
  * - Ensures data freshness even with cached UI components
76
+ * - Segment cache staleness does NOT propagate to loader revalidation;
77
+ * loaders use their own revalidation rules (actionId, user-defined)
76
78
  *
77
79
  *
78
80
  * REVALIDATION RULES
@@ -92,27 +94,61 @@
92
94
  import type { ResolvedSegment } from "../../types.js";
93
95
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
94
96
  import { getRouterContext } from "../router-context.js";
97
+ import { resolveSink, safeEmit } from "../telemetry.js";
98
+ import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
95
99
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
100
+ import type { HandleStore } from "../../server/handle-store.js";
101
+ import {
102
+ getRequestContext,
103
+ _getRequestContext,
104
+ } from "../../server/request-context.js";
96
105
 
97
106
  // Lazily initialized prerender store singleton and dynamically imported deps.
98
107
  // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
99
108
  // top-level, which breaks vitest (only URLs with file:, data:, node: schemes).
100
109
  let prerenderStoreInstance: PrerenderStore | null | undefined;
101
- let _deserializeSegments: typeof import("../../cache/cache-scope.js").deserializeSegments | undefined;
102
- let _hashParams: typeof import("../../prerender/param-hash.js").hashParams | undefined;
103
- let _getRequestContext: typeof import("../../server/request-context.js").getRequestContext | undefined;
110
+ let _deserializeSegments:
111
+ | typeof import("../../cache/segment-codec.js").deserializeSegments
112
+ | undefined;
113
+ let _restoreHandles:
114
+ | typeof import("../../cache/handle-snapshot.js").restoreHandles
115
+ | undefined;
116
+ let _hashParams:
117
+ | typeof import("../../prerender/param-hash.js").hashParams
118
+ | undefined;
119
+ let _lazyGetRequestContext:
120
+ | typeof import("../../server/request-context.js").getRequestContext
121
+ | undefined;
122
+
123
+ function paramsEqual(
124
+ a: Record<string, string>,
125
+ b: Record<string, string>,
126
+ ): boolean {
127
+ if (a === b) return true;
128
+
129
+ const keysA = Object.keys(a);
130
+ if (keysA.length !== Object.keys(b).length) return false;
131
+
132
+ for (const key of keysA) {
133
+ if (a[key] !== b[key]) return false;
134
+ }
135
+
136
+ return true;
137
+ }
104
138
 
105
139
  async function ensurePrerenderDeps() {
106
140
  if (!_deserializeSegments) {
107
- const [cache, paramHash, reqCtx, store] = await Promise.all([
108
- import("../../cache/cache-scope.js"),
141
+ const [codec, snapshot, paramHash, reqCtx, store] = await Promise.all([
142
+ import("../../cache/segment-codec.js"),
143
+ import("../../cache/handle-snapshot.js"),
109
144
  import("../../prerender/param-hash.js"),
110
145
  import("../../server/request-context.js"),
111
146
  import("../../prerender/store.js"),
112
147
  ]);
113
- _deserializeSegments = cache.deserializeSegments;
148
+ _deserializeSegments = codec.deserializeSegments;
149
+ _restoreHandles = snapshot.restoreHandles;
114
150
  _hashParams = paramHash.hashParams;
115
- _getRequestContext = reqCtx.getRequestContext;
151
+ _lazyGetRequestContext = reqCtx.getRequestContext;
116
152
  if (prerenderStoreInstance === undefined) {
117
153
  prerenderStoreInstance = store.createPrerenderStore();
118
154
  }
@@ -129,29 +165,31 @@ async function* yieldFromStore<TEnv>(
129
165
  ctx: MatchContext<TEnv>,
130
166
  state: MatchPipelineState,
131
167
  pipelineStart: number,
168
+ handleStoreRef?: HandleStore,
132
169
  ): AsyncGenerator<ResolvedSegment> {
133
- const {
134
- resolveLoadersOnlyWithRevalidation,
135
- resolveLoadersOnly,
136
- } = getRouterContext<TEnv>();
137
-
138
- if (!_deserializeSegments || !_hashParams || !_getRequestContext) {
170
+ const { resolveLoadersOnlyWithRevalidation, resolveLoadersOnly } =
171
+ getRouterContext<TEnv>();
172
+
173
+ if (
174
+ !_deserializeSegments ||
175
+ !_restoreHandles ||
176
+ !_hashParams ||
177
+ !_lazyGetRequestContext
178
+ ) {
139
179
  throw new Error("yieldFromStore called before ensurePrerenderDeps");
140
180
  }
141
181
 
142
182
  const segments = await _deserializeSegments(entry.segments);
143
183
 
144
- // Replay handle data (same as runtime cache hit path)
145
- const handleStore = _getRequestContext()?._handleStore;
184
+ // Replay handle data (same as runtime cache hit path).
185
+ // Prefer the eagerly-captured handleStoreRef to avoid ALS disruption in workerd.
186
+ const handleStore = handleStoreRef ?? _lazyGetRequestContext()?._handleStore;
146
187
  if (handleStore) {
147
- for (const [segId, segHandles] of Object.entries(entry.handles)) {
148
- if (Object.keys(segHandles).length > 0) {
149
- handleStore.replaySegmentData(segId, segHandles);
150
- }
151
- }
188
+ _restoreHandles(entry.handles, handleStore);
152
189
  }
153
190
 
154
191
  state.cacheHit = true;
192
+ state.cacheSource = "prerender";
155
193
  state.cachedSegments = segments;
156
194
  state.cachedMatchedIds = segments.map((s) => s.id);
157
195
 
@@ -159,10 +197,14 @@ async function* yieldFromStore<TEnv>(
159
197
  // so parent layouts stay live (client keeps its existing versions).
160
198
  // When params changed (e.g., different guide slug), the segments have
161
199
  // different content, so we must NOT nullify.
162
- const paramsChanged = !ctx.isFullMatch &&
163
- JSON.stringify(ctx.matched.params) !== JSON.stringify(ctx.prevParams);
200
+ const paramsChanged =
201
+ !ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
164
202
  for (const segment of segments) {
165
- if (!ctx.isFullMatch && !paramsChanged && ctx.clientSegmentSet.has(segment.id)) {
203
+ if (
204
+ !ctx.isFullMatch &&
205
+ !paramsChanged &&
206
+ ctx.clientSegmentSet.has(segment.id)
207
+ ) {
166
208
  segment.component = null;
167
209
  segment.loading = undefined;
168
210
  }
@@ -170,6 +212,9 @@ async function* yieldFromStore<TEnv>(
170
212
  }
171
213
 
172
214
  // Resolve loaders fresh (loaders are never pre-rendered/cached)
215
+ const ms = ctx.metricsStore;
216
+ const loaderStart = performance.now();
217
+
173
218
  if (ctx.isFullMatch) {
174
219
  if (resolveLoadersOnly) {
175
220
  const loaderSegments = await ctx.Store.run(() =>
@@ -209,9 +254,19 @@ async function* yieldFromStore<TEnv>(
209
254
  }
210
255
  }
211
256
 
212
- const ms = ctx.metricsStore;
213
257
  if (ms) {
214
- ms.metrics.push({ label: "pipeline:cache-lookup", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
258
+ const loaderEnd = performance.now();
259
+ ms.metrics.push({
260
+ label: "pipeline:loader-resolve",
261
+ duration: loaderEnd - loaderStart,
262
+ startTime: loaderStart - ms.requestStart,
263
+ depth: 1,
264
+ });
265
+ ms.metrics.push({
266
+ label: "pipeline:cache-hit",
267
+ duration: loaderEnd - pipelineStart,
268
+ startTime: pipelineStart - ms.requestStart,
269
+ });
215
270
  }
216
271
  }
217
272
 
@@ -219,7 +274,7 @@ async function* yieldFromStore<TEnv>(
219
274
  * Async generator middleware type
220
275
  */
221
276
  export type GeneratorMiddleware<T> = (
222
- source: AsyncGenerator<T>
277
+ source: AsyncGenerator<T>,
223
278
  ) => AsyncGenerator<T>;
224
279
 
225
280
  /**
@@ -237,14 +292,21 @@ export type GeneratorMiddleware<T> = (
237
292
  */
238
293
  export function withCacheLookup<TEnv>(
239
294
  ctx: MatchContext<TEnv>,
240
- state: MatchPipelineState
295
+ state: MatchPipelineState,
241
296
  ): GeneratorMiddleware<ResolvedSegment> {
242
297
  return async function* (
243
- source: AsyncGenerator<ResolvedSegment>
298
+ source: AsyncGenerator<ResolvedSegment>,
244
299
  ): AsyncGenerator<ResolvedSegment> {
245
300
  const pipelineStart = performance.now();
246
301
  const ms = ctx.metricsStore;
247
302
 
303
+ // Eagerly capture the HandleStore before any async operations.
304
+ // In workerd/Cloudflare, dynamic imports and fetch() inside the pipeline
305
+ // can disrupt AsyncLocalStorage, causing getRequestContext() to return
306
+ // undefined afterward. Capturing the reference early ensures handle replay
307
+ // and handler handle-push work regardless of ALS state.
308
+ const handleStoreRef = _getRequestContext()?._handleStore;
309
+
248
310
  const {
249
311
  evaluateRevalidation,
250
312
  buildEntryRevalidateMap,
@@ -258,12 +320,54 @@ export function withCacheLookup<TEnv>(
258
320
  await ensurePrerenderDeps();
259
321
  if (prerenderStoreInstance) {
260
322
  const paramHash = _hashParams!(ctx.matched.params);
261
- const entry = await prerenderStoreInstance.get(
262
- ctx.matched.routeKey, paramHash, { pathname: ctx.pathname }
323
+ const isPassthroughPrerenderRoute = ctx.entries.some(
324
+ (entry) =>
325
+ entry.type === "route" &&
326
+ entry.prerenderDef?.options?.passthrough === true,
263
327
  );
264
- if (entry) {
265
- yield* yieldFromStore(entry, ctx, state, pipelineStart);
266
- return;
328
+
329
+ if (ctx.isIntercept) {
330
+ // Intercept navigation: try intercept-specific prerender entry
331
+ const entry = await prerenderStoreInstance.get(
332
+ ctx.matched.routeKey,
333
+ paramHash + "/i",
334
+ {
335
+ pathname: ctx.pathname,
336
+ isPassthroughRoute: isPassthroughPrerenderRoute,
337
+ },
338
+ );
339
+ if (entry) {
340
+ yield* yieldFromStore(
341
+ entry,
342
+ ctx,
343
+ state,
344
+ pipelineStart,
345
+ handleStoreRef,
346
+ );
347
+ return;
348
+ }
349
+ // No intercept prerender -- fall through to normal pipeline
350
+ // (skip non-intercept prerender to let intercept-resolution run)
351
+ } else {
352
+ // Normal navigation: existing behavior
353
+ const entry = await prerenderStoreInstance.get(
354
+ ctx.matched.routeKey,
355
+ paramHash,
356
+ {
357
+ pathname: ctx.pathname,
358
+ isPassthroughRoute: isPassthroughPrerenderRoute,
359
+ },
360
+ );
361
+ if (entry) {
362
+ yield* yieldFromStore(
363
+ entry,
364
+ ctx,
365
+ state,
366
+ pipelineStart,
367
+ handleStoreRef,
368
+ );
369
+ return;
370
+ }
267
371
  }
268
372
  }
269
373
  }
@@ -278,18 +382,61 @@ export function withCacheLookup<TEnv>(
278
382
  // in-process where Node APIs work, so no interception is needed.
279
383
  if (!ctx.isAction && !ctx.matched.pr && globalThis.__PRERENDER_DEV_URL) {
280
384
  const hasStatic = ctx.entries.some(
281
- (e) => (e.type === "layout" || e.type === "route" || e.type === "parallel") && e.isStaticPrerender
385
+ (e) =>
386
+ (e.type === "layout" ||
387
+ e.type === "route" ||
388
+ e.type === "parallel") &&
389
+ e.isStaticPrerender,
282
390
  );
283
391
  if (hasStatic) {
284
392
  await ensurePrerenderDeps();
285
393
  if (prerenderStoreInstance) {
286
394
  const paramHash = _hashParams!(ctx.matched.params);
287
- const entry = await prerenderStoreInstance.get(
288
- ctx.matched.routeKey, paramHash, { pathname: ctx.pathname }
395
+ const isPassthroughPrerenderRoute = ctx.entries.some(
396
+ (entry) =>
397
+ entry.type === "route" &&
398
+ entry.prerenderDef?.options?.passthrough === true,
289
399
  );
290
- if (entry) {
291
- yield* yieldFromStore(entry, ctx, state, pipelineStart);
292
- return;
400
+
401
+ if (ctx.isIntercept) {
402
+ const entry = await prerenderStoreInstance.get(
403
+ ctx.matched.routeKey,
404
+ paramHash + "/i",
405
+ {
406
+ pathname: ctx.pathname,
407
+ isPassthroughRoute: isPassthroughPrerenderRoute,
408
+ },
409
+ );
410
+ if (entry) {
411
+ yield* yieldFromStore(
412
+ entry,
413
+ ctx,
414
+ state,
415
+ pipelineStart,
416
+ handleStoreRef,
417
+ );
418
+ return;
419
+ }
420
+ // No intercept prerender -- fall through to normal pipeline
421
+ } else {
422
+ const entry = await prerenderStoreInstance.get(
423
+ ctx.matched.routeKey,
424
+ paramHash,
425
+ {
426
+ pathname: ctx.pathname,
427
+ isPassthroughRoute: isPassthroughPrerenderRoute,
428
+ },
429
+ );
430
+ if (entry) {
431
+ yield* yieldFromStore(
432
+ entry,
433
+ ctx,
434
+ state,
435
+ pipelineStart,
436
+ handleStoreRef,
437
+ );
438
+ return;
439
+ }
293
440
  }
294
441
  }
295
442
  }
@@ -300,7 +447,11 @@ export function withCacheLookup<TEnv>(
300
447
  // Cache miss - pass through to segment resolution
301
448
  yield* source;
302
449
  if (ms) {
303
- ms.metrics.push({ label: "pipeline:cache-lookup", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
450
+ ms.metrics.push({
451
+ label: "pipeline:cache-miss",
452
+ duration: performance.now() - pipelineStart,
453
+ startTime: pipelineStart - ms.requestStart,
454
+ });
304
455
  }
305
456
  return;
306
457
  }
@@ -309,30 +460,54 @@ export function withCacheLookup<TEnv>(
309
460
  const cacheResult = await ctx.cacheScope.lookupRoute(
310
461
  ctx.pathname,
311
462
  ctx.matched.params,
312
- ctx.isIntercept
463
+ ctx.isIntercept,
313
464
  );
314
465
 
315
466
  if (!cacheResult) {
316
467
  // Cache miss - pass through to segment resolution
317
468
  yield* source;
318
469
  if (ms) {
319
- ms.metrics.push({ label: "pipeline:cache-lookup", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
470
+ ms.metrics.push({
471
+ label: "pipeline:cache-miss",
472
+ duration: performance.now() - pipelineStart,
473
+ startTime: pipelineStart - ms.requestStart,
474
+ });
320
475
  }
321
476
  return;
322
477
  }
323
478
 
324
479
  // Cache HIT
325
480
  state.cacheHit = true;
481
+ state.cacheSource = "runtime";
326
482
  state.shouldRevalidate = cacheResult.shouldRevalidate;
327
483
  state.cachedSegments = cacheResult.segments;
328
484
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
329
485
 
330
- // Apply revalidation to cached segments
331
- const entryRevalidateMap = buildEntryRevalidateMap?.(ctx.entries);
486
+ // Apply revalidation to cached segments.
487
+ // For full matches or empty client segment sets, this map is unnecessary:
488
+ // we never run segment-level revalidation and can stream segments directly.
489
+ const canCheckSegmentRevalidation =
490
+ !ctx.isFullMatch &&
491
+ ctx.clientSegmentSet.size > 0 &&
492
+ !!buildEntryRevalidateMap;
493
+ const entryRevalidateMap = canCheckSegmentRevalidation
494
+ ? buildEntryRevalidateMap(ctx.entries)
495
+ : undefined;
332
496
 
333
497
  for (const segment of cacheResult.segments) {
334
498
  // Skip segments client doesn't have - they need their component
335
499
  if (!ctx.clientSegmentSet.has(segment.id)) {
500
+ if (isTraceActive()) {
501
+ pushRevalidationTraceEntry({
502
+ segmentId: segment.id,
503
+ segmentType: segment.type,
504
+ belongsToRoute: segment.belongsToRoute ?? false,
505
+ source: "cache-hit",
506
+ defaultShouldRevalidate: true,
507
+ finalShouldRevalidate: true,
508
+ reason: "new-segment",
509
+ });
510
+ }
336
511
  yield segment;
337
512
  continue;
338
513
  }
@@ -345,8 +520,46 @@ export function withCacheLookup<TEnv>(
345
520
 
346
521
  // Look up revalidation rules for this segment
347
522
  const entryInfo = entryRevalidateMap?.get(segment.id);
523
+
524
+ // Even without explicit revalidation rules, route segments and their
525
+ // children must re-render when search params change — the handler reads
526
+ // ctx.searchParams so different ?page= values produce different content.
527
+ const searchChanged = ctx.prevUrl.search !== ctx.url.search;
528
+ const shouldDefaultRevalidate =
529
+ searchChanged &&
530
+ (segment.type === "route" ||
531
+ (segment.belongsToRoute &&
532
+ (segment.type === "layout" || segment.type === "parallel")));
533
+
348
534
  if (!entryInfo || entryInfo.revalidate.length === 0) {
535
+ if (shouldDefaultRevalidate) {
536
+ // Search params changed — must re-render even without custom rules
537
+ if (isTraceActive()) {
538
+ pushRevalidationTraceEntry({
539
+ segmentId: segment.id,
540
+ segmentType: segment.type,
541
+ belongsToRoute: segment.belongsToRoute ?? false,
542
+ source: "cache-hit",
543
+ defaultShouldRevalidate: true,
544
+ finalShouldRevalidate: true,
545
+ reason: "cached-search-changed",
546
+ });
547
+ }
548
+ yield segment;
549
+ continue;
550
+ }
349
551
  // No revalidation rules, use default behavior (skip if client has)
552
+ if (isTraceActive()) {
553
+ pushRevalidationTraceEntry({
554
+ segmentId: segment.id,
555
+ segmentType: segment.type,
556
+ belongsToRoute: segment.belongsToRoute ?? false,
557
+ source: "cache-hit",
558
+ defaultShouldRevalidate: false,
559
+ finalShouldRevalidate: false,
560
+ reason: "cached-no-rules",
561
+ });
562
+ }
350
563
  segment.component = null;
351
564
  segment.loading = undefined;
352
565
  yield segment;
@@ -368,8 +581,24 @@ export function withCacheLookup<TEnv>(
368
581
  routeKey: ctx.routeKey,
369
582
  context: ctx.handlerContext,
370
583
  actionContext: ctx.actionContext,
584
+ stale: cacheResult.shouldRevalidate || undefined,
585
+ traceSource: "cache-hit",
371
586
  });
372
587
 
588
+ const routerCtx = getRouterContext<TEnv>();
589
+ if (routerCtx.telemetry) {
590
+ const tSink = resolveSink(routerCtx.telemetry);
591
+ safeEmit(tSink, {
592
+ type: "revalidation.decision",
593
+ timestamp: performance.now(),
594
+ requestId: routerCtx.requestId,
595
+ segmentId: segment.id,
596
+ pathname: ctx.pathname,
597
+ routeKey: ctx.routeKey,
598
+ shouldRevalidate,
599
+ });
600
+ }
601
+
373
602
  if (!shouldRevalidate) {
374
603
  // Client has it, no revalidation needed
375
604
  segment.component = null;
@@ -382,12 +611,13 @@ export function withCacheLookup<TEnv>(
382
611
  // Resolve loaders fresh (loaders are NOT cached by default)
383
612
  // This ensures fresh data even on cache hit
384
613
  const Store = ctx.Store;
614
+ const loaderStart = performance.now();
385
615
 
386
616
  if (ctx.isFullMatch) {
387
617
  // Full match (document request) - simple loader resolution without revalidation
388
618
  if (resolveLoadersOnly) {
389
619
  const loaderSegments = await Store.run(() =>
390
- resolveLoadersOnly(ctx.entries, ctx.handlerContext)
620
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext),
391
621
  );
392
622
 
393
623
  // Update state - full match doesn't track matchedIds separately
@@ -413,8 +643,13 @@ export function withCacheLookup<TEnv>(
413
643
  ctx.prevUrl,
414
644
  ctx.url,
415
645
  ctx.routeKey,
416
- ctx.actionContext
417
- )
646
+ ctx.actionContext,
647
+ // Loaders are never cached in the segment cache, so segment
648
+ // staleness (cacheResult.shouldRevalidate) must not propagate.
649
+ // But browser-sent staleness (ctx.stale) — indicating an action
650
+ // happened in this or another tab — must still reach loaders.
651
+ ctx.stale || undefined,
652
+ ),
418
653
  );
419
654
 
420
655
  // Update state with fresh loader matchedIds
@@ -432,7 +667,18 @@ export function withCacheLookup<TEnv>(
432
667
  }
433
668
  }
434
669
  if (ms) {
435
- ms.metrics.push({ label: "pipeline:cache-lookup", duration: performance.now() - pipelineStart, startTime: pipelineStart - ms.requestStart });
670
+ const loaderEnd = performance.now();
671
+ ms.metrics.push({
672
+ label: "pipeline:loader-resolve",
673
+ duration: loaderEnd - loaderStart,
674
+ startTime: loaderStart - ms.requestStart,
675
+ depth: 1,
676
+ });
677
+ ms.metrics.push({
678
+ label: "pipeline:cache-hit",
679
+ duration: loaderEnd - pipelineStart,
680
+ startTime: pipelineStart - ms.requestStart,
681
+ });
436
682
  }
437
683
  };
438
684
  }