@rangojs/router 0.20.0 → 0.22.0

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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -61,6 +61,8 @@ export interface GeneratedManifest {
61
61
  prerenderRoutes?: string[];
62
62
  /** Route names wrapped with Passthrough() (live handler for runtime fallback) */
63
63
  passthroughRoutes?: string[];
64
+ /** Route names opted into on-demand prerender (Prerender(..., { onDemand })) */
65
+ onDemandRoutes?: string[];
64
66
  /** Route name → response type for non-RSC routes */
65
67
  responseTypeRoutes?: Record<string, string>;
66
68
  /** Route name -> search schema descriptor for typed URL helpers */
@@ -172,6 +174,7 @@ async function buildPrefixTreeNode(
172
174
  prerenderRoutes?: string[],
173
175
  prerenderDefs?: Record<string, any>,
174
176
  passthroughRoutes?: string[],
177
+ onDemandRoutes?: string[],
175
178
  responseTypeRoutes?: Record<string, string>,
176
179
  routeSearchSchemas?: Record<string, Record<string, string>>,
177
180
  routerId?: string,
@@ -264,6 +267,13 @@ async function buildPrefixTreeNode(
264
267
  passthroughRoutes.push(name);
265
268
  }
266
269
  }
270
+ if (
271
+ onDemandRoutes &&
272
+ entry.type === "route" &&
273
+ entry.isOnDemand === true
274
+ ) {
275
+ onDemandRoutes.push(name);
276
+ }
267
277
  }
268
278
  }
269
279
 
@@ -289,6 +299,7 @@ async function buildPrefixTreeNode(
289
299
  prerenderRoutes,
290
300
  prerenderDefs,
291
301
  passthroughRoutes,
302
+ onDemandRoutes,
292
303
  responseTypeRoutes,
293
304
  routeSearchSchemas,
294
305
  routerId,
@@ -455,6 +466,7 @@ export async function generateManifestFull<TEnv>(
455
466
  const prerenderRoutes: string[] = [];
456
467
  const prerenderDefs: Record<string, any> = {};
457
468
  const passthroughRoutes: string[] = [];
469
+ const onDemandRoutes: string[] = [];
458
470
  const responseTypeRoutes: Record<string, string> = {};
459
471
  for (const [name, entry] of manifest) {
460
472
  if (entry.type === "route" && entry.isPrerender) {
@@ -465,6 +477,9 @@ export async function generateManifestFull<TEnv>(
465
477
  if (entry.isPassthrough === true) {
466
478
  passthroughRoutes.push(name);
467
479
  }
480
+ if (entry.isOnDemand === true) {
481
+ onDemandRoutes.push(name);
482
+ }
468
483
  }
469
484
  if (entry.type === "route" && entry.responseType) {
470
485
  responseTypeRoutes[name] = entry.responseType;
@@ -485,6 +500,7 @@ export async function generateManifestFull<TEnv>(
485
500
  prerenderRoutes,
486
501
  prerenderDefs,
487
502
  passthroughRoutes,
503
+ onDemandRoutes,
488
504
  responseTypeRoutes,
489
505
  routeSearchSchemas,
490
506
  options?.routerId,
@@ -502,6 +518,7 @@ export async function generateManifestFull<TEnv>(
502
518
  prerenderRoutes: prerenderRoutes.length > 0 ? prerenderRoutes : undefined,
503
519
  passthroughRoutes:
504
520
  passthroughRoutes.length > 0 ? passthroughRoutes : undefined,
521
+ onDemandRoutes: onDemandRoutes.length > 0 ? onDemandRoutes : undefined,
505
522
  responseTypeRoutes:
506
523
  Object.keys(responseTypeRoutes).length > 0
507
524
  ? responseTypeRoutes
@@ -136,6 +136,11 @@ export function mergeFullManifests(
136
136
  manifest.passthroughRoutes,
137
137
  overwrittenRoutes,
138
138
  ),
139
+ onDemandRoutes: mergeRouteList(
140
+ merged.onDemandRoutes,
141
+ manifest.onDemandRoutes,
142
+ overwrittenRoutes,
143
+ ),
139
144
  responseTypeRoutes: mergeRouteRecord(
140
145
  merged.responseTypeRoutes,
141
146
  manifest.responseTypeRoutes,
@@ -50,5 +50,6 @@ export function buildPerRouterTrie(manifest: FullManifest): TrieNode | null {
50
50
  ? new Set(manifest.passthroughRoutes)
51
51
  : undefined,
52
52
  manifest.responseTypeRoutes,
53
+ manifest.onDemandRoutes ? new Set(manifest.onDemandRoutes) : undefined,
53
54
  );
54
55
  }
@@ -7,6 +7,8 @@
7
7
  * skipping.
8
8
  */
9
9
 
10
+ import type { ExecutionContext } from "../types/request-scope.js";
11
+
10
12
  interface WaitUntilHost {
11
13
  waitUntil?: (fn: () => Promise<void>) => void;
12
14
  }
@@ -85,3 +87,62 @@ export async function settleGrowing(
85
87
  }
86
88
  return true;
87
89
  }
90
+
91
+ /** Macrotask turns `awaitLaterMillisecond` waits at most. */
92
+ export const LATER_MILLISECOND_MAX_TURNS = 10;
93
+
94
+ /**
95
+ * Yield until `Date.now()` passes `since`, so a store's whole-millisecond tag
96
+ * gate (putShell vs an invalidation made just before) sees a later
97
+ * millisecond. Bounded: on Cloudflare the clock stands still between I/O
98
+ * events, so an unbounded wait could hang. At the bound the caller proceeds
99
+ * and a store may refuse the write, which its result reports.
100
+ */
101
+ export async function awaitLaterMillisecond(
102
+ since: number,
103
+ maxTurns: number = LATER_MILLISECOND_MAX_TURNS,
104
+ ): Promise<void> {
105
+ for (let turn = 0; turn < maxTurns && Date.now() === since; turn++) {
106
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
107
+ }
108
+ }
109
+
110
+ /** An ExecutionContext that keeps every promise handed to `waitUntil`. */
111
+ export interface CollectingExecutionContext extends ExecutionContext {
112
+ readonly tasks: Promise<unknown>[];
113
+ }
114
+
115
+ /**
116
+ * Wrap the caller's ExecutionContext (or none) so a caller can wait for the
117
+ * request's background work: the deferred cache writes, the shell capture, a
118
+ * store's own writes. Every other member reads through to `inner`, bound to
119
+ * it (workerd's methods reject a foreign `this`).
120
+ */
121
+ export function createCollectingExecutionContext(
122
+ inner?: ExecutionContext,
123
+ ): CollectingExecutionContext {
124
+ const tasks: Promise<unknown>[] = [];
125
+ const own: CollectingExecutionContext = {
126
+ tasks,
127
+ waitUntil(promise) {
128
+ const task = Promise.resolve(promise);
129
+ // The list only waits; the host (or the log below) owns the failure.
130
+ tasks.push(task.catch(() => {}));
131
+ if (inner) inner.waitUntil(task);
132
+ else {
133
+ task.catch((error) =>
134
+ console.error("[waitUntil] Background task failed:", error),
135
+ );
136
+ }
137
+ },
138
+ passThroughOnException() {},
139
+ };
140
+ if (!inner) return own;
141
+ return new Proxy(own, {
142
+ get(target, prop) {
143
+ if (prop in target) return target[prop as keyof typeof target];
144
+ const value = (inner as unknown as Record<PropertyKey, unknown>)[prop];
145
+ return typeof value === "function" ? value.bind(inner) : value;
146
+ },
147
+ });
148
+ }
@@ -22,7 +22,7 @@ import type { SearchParamsFilter } from "./search-params-filter.js";
22
22
  */
23
23
  const RESERVED_SEARCH_PARAMS = new Set(["__no_cache", "__rsc", "__html"]);
24
24
 
25
- function isReservedSearchParam(key: string): boolean {
25
+ export function isReservedSearchParam(key: string): boolean {
26
26
  return key.startsWith("_rsc") || RESERVED_SEARCH_PARAMS.has(key);
27
27
  }
28
28
 
@@ -65,20 +65,37 @@ export function sortedRouteParams(
65
65
  }
66
66
 
67
67
  /**
68
- * Host-namespaced cache key base: `${host}${pathname}[:params][?search]`.
68
+ * THE ROUTER RULE (#1065). A key that names one router's output for a host
69
+ * and path starts with the router part, `{encodeURIComponent(routerId)}@`. A
70
+ * key that names a function and its arguments, or a tag, does not.
69
71
  *
70
- * The ONE composition of the host-namespacing rule, shared by the segment tier
71
- * (cache-scope.ts) and the document tier (document-cache.ts) so the rule cannot
72
- * drift between them. Host prefixing matters because VercelCacheStore /
73
- * MemorySegmentCacheStore key by the raw string (only CFCacheStore adds host
74
- * internally) -- on a single function serving multiple domains an
75
- * un-namespaced key bleeds tenant A's cached response to tenant B.
72
+ * - With the part: PPR shells and the build-shell manifest (buildShellKey,
73
+ * buildShellManifestKey); route cache() records, the document cache and
74
+ * response routes (cacheKeyBase); a loader's own cache() entry; a
75
+ * `"use cache"` entry of a call that takes a ctx (a ctx is one router's:
76
+ * its variables, env and reverse() map).
77
+ * - Without: a `"use cache"` entry of plain arguments or a bare Request
78
+ * (requestArgKey); tag markers; a key the app builds itself
79
+ * (`cache({ key })`, a `keyGenerator`).
76
80
  *
77
- * Output is BYTE-STABLE by contract: changing the composition silently
78
- * invalidates every persisted cache entry on upgrade. Callers append their own
79
- * tier-specific suffixes (`:rsc`/`:html`, segment hash) after this base.
81
+ * Why: a host and path do not name a router. A `hostOverride` cookie picks
82
+ * the router and forwards the request unmodified, and a `router.prerender()`
83
+ * warm requests whatever origin it resolved, so two routers on one store
84
+ * built one key and read each other's entry.
85
+ *
86
+ * The id (router.ts) has to be the same in every isolate and in build-time
87
+ * discovery: an explicit `id` and the injected `$$id` are, the `router_{n}`
88
+ * fallback is for a single router only (discover-routers.ts warns about
89
+ * more). Encoded, the id holds no `@` or `/`, and a host holds no `@`, so a
90
+ * key has a router part exactly when its first `@` comes before its first
91
+ * `/`: no id can be chosen to spell another router's key.
80
92
  */
81
- export function cacheKeyBase(
93
+ export function routerKeyPrefix(routerId: string): string {
94
+ return `${encodeURIComponent(routerId)}@`;
95
+ }
96
+
97
+ /** `{host}{pathname}[:params][?search]`: what follows the router part. */
98
+ function hostPathKey(
82
99
  host: string,
83
100
  pathname: string,
84
101
  searchParams?: URLSearchParams,
@@ -96,6 +113,68 @@ export function cacheKeyBase(
96
113
  return key;
97
114
  }
98
115
 
116
+ /**
117
+ * Key base of one router's output (the router rule, routerKeyPrefix):
118
+ * `${routerId}@${host}${pathname}[:params][?search]`.
119
+ *
120
+ * The ONE composition of the namespacing rule, shared by the segment tier
121
+ * (cache-scope.ts), the document tier (document-cache.ts), the response tier
122
+ * (rsc/response-cache-serve.ts) and the loader cache so the rule cannot drift
123
+ * between them. Host prefixing matters because VercelCacheStore /
124
+ * MemorySegmentCacheStore key by the raw string (only CFCacheStore adds host
125
+ * internally) -- on a single function serving multiple domains an
126
+ * un-namespaced key bleeds tenant A's cached response to tenant B.
127
+ *
128
+ * Output is BYTE-STABLE by contract: changing the composition silently
129
+ * invalidates every persisted cache entry on upgrade. Callers append their own
130
+ * tier-specific suffixes (`:rsc`/`:html`, segment hash) after this base.
131
+ */
132
+ export function cacheKeyBase(
133
+ routerId: string,
134
+ host: string,
135
+ pathname: string,
136
+ searchParams?: URLSearchParams,
137
+ params?: Record<string, string>,
138
+ filter?: SearchParamsFilter,
139
+ ): string {
140
+ return `${routerKeyPrefix(routerId)}${hostPathKey(host, pathname, searchParams, params, filter)}`;
141
+ }
142
+
143
+ /**
144
+ * cacheKeyBase for the request context `ctx`. rsc/handler.ts sets `_routerId`
145
+ * before the request scope opens and derived contexts inherit it, so a served
146
+ * request always has one. Only a hand-built context has none (a unit test,
147
+ * the runLoader / renderHandler primitives): its key has no router part,
148
+ * which no router's key equals (routerKeyPrefix).
149
+ */
150
+ export function requestKeyBase(
151
+ ctx: { readonly _routerId?: string } | null | undefined,
152
+ host: string,
153
+ pathname: string,
154
+ searchParams?: URLSearchParams,
155
+ params?: Record<string, string>,
156
+ filter?: SearchParamsFilter,
157
+ ): string {
158
+ const routerId = ctx?._routerId;
159
+ return routerId === undefined
160
+ ? hostPathKey(host, pathname, searchParams, params, filter)
161
+ : cacheKeyBase(routerId, host, pathname, searchParams, params, filter);
162
+ }
163
+
164
+ /**
165
+ * The `"use cache"` key part of a bare Request argument: its URL, with no
166
+ * router part (the router rule, routerKeyPrefix).
167
+ */
168
+ export function requestArgKey(url: URL, filter?: SearchParamsFilter): string {
169
+ return hostPathKey(
170
+ url.host,
171
+ url.pathname,
172
+ url.searchParams,
173
+ undefined,
174
+ filter,
175
+ );
176
+ }
177
+
99
178
  /**
100
179
  * The prefix of a `key()` result's part in a record key (#975), and of a
101
180
  * loader's own `key()` result after its `loader:<id>:` namespace (#1009,
@@ -13,6 +13,15 @@ import type { RequestContext } from "../server/request-context.js";
13
13
  import { normalizeTags } from "./cache-tag.js";
14
14
  import { reportCacheError } from "./cache-error.js";
15
15
 
16
+ /**
17
+ * How long a follower trusts an in-flight leader before evicting it and
18
+ * running fresh. High enough that a slow-but-healthy upstream never triggers
19
+ * duplicate work (a legitimate cached call taking >15s is already pathological);
20
+ * low enough to bound the blast radius of a wedged leader to seconds, not the
21
+ * isolate lifetime. Aligned with SHELL_CAPTURE_MAX_WAIT_MS.
22
+ */
23
+ export const IN_FLIGHT_LEADER_MAX_WAIT_MS = 15_000;
24
+
16
25
  /**
17
26
  * Default TTL for route-level cache() DSL and loader cache.
18
27
  * Applied when neither the cache options nor the store defaults specify a TTL.
@@ -184,8 +193,8 @@ export function resolveTagsOption<TEnv>(
184
193
  * and on CFCacheStore would also cost a wasted KV marker read per request.
185
194
  * Returns undefined when nothing usable remains, keeping the entry header-free.
186
195
  */
187
- function normalizeTagList(tags: string[]): string[] | undefined {
188
- const out = normalizeTags(tags);
196
+ export function normalizeTagList(tags: string[]): string[] | undefined {
197
+ const out = [...new Set(normalizeTags(tags))];
189
198
  return out.length > 0 ? out : undefined;
190
199
  }
191
200
 
@@ -42,7 +42,11 @@ import {
42
42
  } from "./handle-snapshot.js";
43
43
  import { startHandleCapture, useCacheRecordKey } from "./handle-capture.js";
44
44
  import { isHandle } from "../handle.js";
45
- import { cacheKeyBase, sortedSearchString } from "./cache-key-utils.js";
45
+ import {
46
+ requestArgKey,
47
+ requestKeyBase,
48
+ sortedSearchString,
49
+ } from "./cache-key-utils.js";
46
50
  import { encodeKV } from "../encode-kv.js";
47
51
  import { runBackground } from "./background-task.js";
48
52
  import { observePhase, PHASES } from "../router/instrument.js";
@@ -60,6 +64,8 @@ import {
60
64
  type CacheExecScope,
61
65
  } from "./cache-exec-scope.js";
62
66
  import { reportCacheError } from "./cache-error.js";
67
+ import { IN_FLIGHT_LEADER_MAX_WAIT_MS } from "./cache-policy.js";
68
+ import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
63
69
  import {
64
70
  executionStart,
65
71
  invalidatedSince,
@@ -314,15 +320,6 @@ interface InFlightExecution {
314
320
 
315
321
  const inFlightExecutions = new Map<string, InFlightExecution>();
316
322
 
317
- /**
318
- * How long a follower trusts an in-flight leader before evicting it and
319
- * running fresh. High enough that a slow-but-healthy upstream never triggers
320
- * duplicate work (a legitimate cached call taking >15s is already pathological);
321
- * low enough to bound the blast radius of a wedged leader to seconds, not the
322
- * isolate lifetime. Aligned with SHELL_CAPTURE_MAX_WAIT_MS.
323
- */
324
- const IN_FLIGHT_LEADER_MAX_WAIT_MS = 15_000;
325
-
326
323
  /** Distinguishes a leader timeout from a leader rejection in raceLeader. */
327
324
  const LEADER_TIMED_OUT = Symbol("leader-timed-out");
328
325
 
@@ -499,9 +496,11 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
499
496
  }
500
497
  if (ctx.params && typeof ctx.params === "object") {
501
498
  // Include host to prevent cross-host cache collisions (same
502
- // pattern as route-level cache-scope.ts key generation).
499
+ // pattern as route-level cache-scope.ts key generation), led by the
500
+ // router part: a ctx is one router's (cache-key-utils.ts, the
501
+ // router rule).
503
502
  if (ctx.url?.host) {
504
- keyArgs.push(ctx.url.host);
503
+ keyArgs.push(requestKeyBase(requestCtx, ctx.url.host, ""));
505
504
  }
506
505
  // Include route name to prevent collisions when the same cached
507
506
  // function is reused across routes with identical pathname/params
@@ -529,15 +528,8 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
529
528
  } else if (arg instanceof Request) {
530
529
  // A raw Request (ctx.request) is request-scoped like ctx: fold in its
531
530
  // URL with the same host-namespacing and search normalization.
532
- const url = new URL(arg.url);
533
531
  keyArgs.push(
534
- cacheKeyBase(
535
- url.host,
536
- url.pathname,
537
- url.searchParams,
538
- undefined,
539
- requestCtx?._searchParamsFilter,
540
- ),
532
+ requestArgKey(new URL(arg.url), requestCtx?._searchParamsFilter),
541
533
  );
542
534
  } else if (arg != null && arg === requestCtx?.env) {
543
535
  // The request's env is left out: constant per deployment, and its
@@ -617,8 +609,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
617
609
  return result;
618
610
  }
619
611
 
620
- // Cache lookup
621
- const cached = await store.getItem(cacheKey);
612
+ // Cache lookup. A router.prerender() warm reads a miss and leads its own
613
+ // execution below: a leader that started before the warm was called may
614
+ // be reading data from before the change the warm publishes, and a
615
+ // follower writes nothing of its own.
616
+ const warmReplace = isWarmReplace(requestCtx);
617
+ const cached = warmReplace ? null : await store.getItem(cacheKey);
622
618
 
623
619
  // Serve a cached entry on the hit path: deserialize the stored value,
624
620
  // replay handle data (gated on tainted args), and surface the entry's tags
@@ -767,7 +763,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
767
763
  ttl: profile.ttl,
768
764
  swr: profile.swr,
769
765
  tags: freshTags.length > 0 ? freshTags : undefined,
766
+ startedAt: start.at,
770
767
  });
768
+ noteWarmWrite(requestCtx, "item");
771
769
  }
772
770
  },
773
771
  ),
@@ -806,7 +804,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
806
804
  // against its OWN handle store (gated on ITS hasTaintedArgs), and records
807
805
  // tags into its OWN request — no deserialized result is shared across
808
806
  // requests. The store write stays exactly once (the leader's).
809
- let existing = inFlightExecutions.get(cacheKey);
807
+ let existing = warmReplace ? undefined : inFlightExecutions.get(cacheKey);
810
808
  while (existing) {
811
809
  const remainingMs =
812
810
  IN_FLIGHT_LEADER_MAX_WAIT_MS - (Date.now() - existing.registeredAt);
@@ -1022,7 +1020,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
1022
1020
  ttl: profile.ttl,
1023
1021
  swr: profile.swr,
1024
1022
  tags: tags.length > 0 ? tags : undefined,
1023
+ startedAt: start.at,
1025
1024
  });
1025
+ noteWarmWrite(requestCtx, "item");
1026
1026
  } catch (writeError) {
1027
1027
  requestCtx?._reportBackgroundError?.(writeError, "cache-write");
1028
1028
  }
@@ -38,12 +38,11 @@ import {
38
38
  import {
39
39
  captureHandles,
40
40
  captureHandleOwners,
41
- restoreHandles,
41
+ restoreRecordHandles,
42
42
  type OwnedPushDelivery,
43
43
  encodeHandles,
44
- decodeHandles,
45
44
  } from "./handle-snapshot.js";
46
- import { cacheKeyBase, composeCacheKeys } from "./cache-key-utils.js";
45
+ import { composeCacheKeys, requestKeyBase } from "./cache-key-utils.js";
47
46
  import {
48
47
  DEFAULT_ROUTE_TTL,
49
48
  isFiniteNonNegativeSeconds,
@@ -53,6 +52,7 @@ import {
53
52
  } from "./cache-policy.js";
54
53
  import type { RequestContext } from "../server/request-context.js";
55
54
  import { runIdentityExempt } from "./cache-exec-scope.js";
55
+ import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
56
56
 
57
57
  /**
58
58
  * Narrow the request's route-record window (RequestContext._routeRecordWindow)
@@ -138,7 +138,7 @@ function getDefaultRouteCacheKey(
138
138
  ? "intercept"
139
139
  : (prefixOverride ?? (isPartial ? "partial" : "doc"));
140
140
 
141
- return `${prefix}:${cacheKeyBase(host, pathname, searchParams, params, ctx?._searchParamsFilter)}`;
141
+ return `${prefix}:${requestKeyBase(ctx, host, pathname, searchParams, params, ctx?._searchParamsFilter)}`;
142
142
  }
143
143
 
144
144
  // ============================================================================
@@ -735,15 +735,14 @@ export class CacheScope {
735
735
  * @param pathname - URL pathname for cache key generation
736
736
  * @param params - Route params for cache key generation
737
737
  * @param isIntercept - Whether this is an intercept navigation (uses different cache key)
738
- * @param ownedPushes - How restoreHandles delivers the record's
739
- * loader-owned values (withCacheLookup: the handler context's
740
- * _claimLoaderPushes, and on a document HIT tail the owners it restores)
738
+ * @param ownedPushes - What the record is to each loader's handle pushes
739
+ * (withCacheLookup: loader-cache.ts loaderPins), called on a hit
741
740
  */
742
741
  async lookupRoute(
743
742
  pathname: string,
744
743
  params: Record<string, string>,
745
744
  isIntercept?: boolean,
746
- ownedPushes?: OwnedPushDelivery,
745
+ ownedPushes?: () => OwnedPushDelivery,
747
746
  ): Promise<{
748
747
  segments: ResolvedSegment[];
749
748
  shouldRevalidate: boolean;
@@ -779,7 +778,7 @@ export class CacheScope {
779
778
  pathname: string,
780
779
  params: Record<string, string>,
781
780
  isIntercept?: boolean,
782
- ownedPushes?: OwnedPushDelivery,
781
+ ownedPushes?: () => OwnedPushDelivery,
783
782
  ): Promise<CacheRouteLookupOutcome> {
784
783
  if (!this.enabled) return { status: "bypass" };
785
784
  if (!this.conditionAllows("read")) return { status: "bypass" };
@@ -787,6 +786,12 @@ export class CacheScope {
787
786
  const store = this.getStore();
788
787
  if (!store) return { status: "bypass" };
789
788
 
789
+ // A router.prerender() warm renders as on a cold cache: the opt-outs
790
+ // above still answer `bypass`, and the miss lets the write path replace
791
+ // the record under the key it resolves (cacheRoute).
792
+ const ambientContext = _getRequestContext();
793
+ if (isWarmReplace(ambientContext)) return { status: "miss" };
794
+
790
795
  // Resolve cache key INSIDE the try so a throwing consumer key() (or a
791
796
  // store.keyGenerator) degrades to a cache miss (return null -> render
792
797
  // uncached) instead of crashing the foreground render. resolveCacheKey
@@ -827,7 +832,6 @@ export class CacheScope {
827
832
  // seeded record, which a document tail turns into the degrade
828
833
  // (ShellRecordUnavailableError).
829
834
  let segments: ResolvedSegment[];
830
- const ambientContext = _getRequestContext();
831
835
  try {
832
836
  const codec = await import("./segment-codec.js");
833
837
  segments = ambientContext?._shellFragmentPayload
@@ -870,27 +874,24 @@ export class CacheScope {
870
874
  cached.tags,
871
875
  );
872
876
 
873
- // Replay handle data. An empty string means the route pushed no handles —
874
- // skip the decode entirely (the common case). Otherwise decode the
875
- // Flight-encoded blob; a decode failure skips handle restore but keeps the
876
- // valid cached segments.
877
+ // Before the handle replay: the implicit doc scope's marker arms the
878
+ // loader pins of a navigation replay here (matchPartialWithPprReplay),
879
+ // and `ownedPushes` reads them to decide which loader-owned values
880
+ // stand. The segments decoded, so the record serves from here on.
881
+ await this.onHit?.();
882
+
877
883
  const handleStore = _getRequestContext()?._handleStore;
878
- if (handleStore && cached.handles) {
879
- const handlesRecord = await decodeHandles(cached.handles);
880
- if (handlesRecord) {
881
- if (this.boundary !== undefined) {
882
- const kept = new Set(segments.map((s) => s.id));
883
- for (const id of Object.keys(handlesRecord)) {
884
- if (!kept.has(id)) delete handlesRecord[id];
885
- }
886
- }
887
- restoreHandles(
888
- handlesRecord,
889
- handleStore,
890
- cached.handleOwners,
891
- ownedPushes,
892
- );
893
- }
884
+ // An empty handles string means the route pushed none (the common
885
+ // case); a decode failure skips the restore but keeps the segments.
886
+ if (handleStore) {
887
+ await restoreRecordHandles(
888
+ handleStore,
889
+ cached,
890
+ ownedPushes?.(),
891
+ this.boundary !== undefined
892
+ ? new Set(segments.map((s) => s.id))
893
+ : undefined,
894
+ );
894
895
  }
895
896
 
896
897
  if (INTERNAL_RANGO_DEBUG) {
@@ -905,7 +906,6 @@ export class CacheScope {
905
906
  if (ambientContext)
906
907
  this.noteRecordWindow(ambientContext, cached.expiresAt, false);
907
908
 
908
- await this.onHit?.();
909
909
  return { status: "hit", result: { segments, shouldRevalidate } };
910
910
  } catch (error) {
911
911
  // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
@@ -1105,15 +1105,25 @@ export class CacheScope {
1105
1105
  // the tree) or a rejected handle value as an error row and completes
1106
1106
  // normally; stored, every HIT would render that error until expiry.
1107
1107
  if (flightErrors.length > 0) throw flightErrors[0];
1108
+ // A streamed handler recovered into its boundary fallback resolves, so
1109
+ // Flight reports nothing; refuse like a Flight error row.
1110
+ const recovered = requestCtx._recoveredHandlerErrors;
1111
+ if (recovered?.size) {
1112
+ const failed = nonLoaderSegments.find((s) => recovered.has(s.id));
1113
+ if (failed) throw recovered.get(failed.id);
1114
+ }
1108
1115
 
1116
+ const recordTags = collectRecordTags(requestCtx, tags, [
1117
+ ...nonLoaderSegments.map((s) => s.id),
1118
+ handlesOwnerId,
1119
+ ]);
1109
1120
  const data: CachedEntryData = {
1110
1121
  segments: serializedSegments,
1111
1122
  handles: encodedHandles,
1112
1123
  expiresAt: Date.now() + ttl * 1000,
1113
- tags: collectRecordTags(requestCtx, tags, [
1114
- ...nonLoaderSegments.map((s) => s.id),
1115
- handlesOwnerId,
1116
- ]),
1124
+ tags: recordTags,
1125
+ // The stamp is the render's start, not the write's (#1068).
1126
+ ...(start && recordTags?.length ? { taggedAt: start.at } : {}),
1117
1127
  };
1118
1128
  if (handleOwners && encodedHandles) data.handleOwners = handleOwners;
1119
1129
 
@@ -1127,6 +1137,8 @@ export class CacheScope {
1127
1137
  }
1128
1138
 
1129
1139
  await store.set(key, data, ttl, swr);
1140
+ // The implicit doc scope's record lives in the shell entry only.
1141
+ if (!this.isShellImplicitDocScope) noteWarmWrite(requestCtx, "record");
1130
1142
 
1131
1143
  if (INTERNAL_RANGO_DEBUG) {
1132
1144
  const segmentTypes = nonLoaderSegments.map((s) =>
@@ -1175,7 +1187,7 @@ function collectRecordTags(
1175
1187
  * enclosing scopes on another store (#974) and of a scope without its own
1176
1188
  * `key()` (#970)); with none, the app store's keyGenerator result
1177
1189
  * given the document default key. Undefined (synchronously, with no work)
1178
- * when neither applies: the shell key stays host + path + filtered search.
1190
+ * when neither applies: the shell key stays unpartitioned (buildShellKey).
1179
1191
  * Resolves to null when the partition is the default key itself (a
1180
1192
  * keyGenerator that returns it unchanged): that partitions nothing, so the
1181
1193
  * shell stays unpartitioned and keeps its build shell. The shell's key, its