@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
@@ -1,5 +1,4 @@
1
1
  /// <reference types="@vitejs/plugin-rsc/types" />
2
- /// <reference path="../vite/plugins/version.d.ts" />
3
2
  /**
4
3
  * RSC Request Handler
5
4
  *
@@ -16,6 +15,7 @@ import {
16
15
  getRequestContext,
17
16
  _getRequestContext,
18
17
  createRequestContext,
18
+ captureRequestEntryContext,
19
19
  } from "../server/request-context.js";
20
20
  import * as rscDeps from "@vitejs/plugin-rsc/rsc/server";
21
21
  import type {
@@ -47,7 +47,10 @@ import {
47
47
  type ResponseRouteMatch,
48
48
  } from "./response-route-handler.js";
49
49
  import { nonce as nonceToken, resolveProviderNonce } from "./nonce.js";
50
- import { VERSION } from "@rangojs/router:version";
50
+ import {
51
+ resolvePrerenderVersion,
52
+ resolveRouterVersions,
53
+ } from "../server/build-version-table.js";
51
54
  import type { ErrorPhase } from "../types.js";
52
55
  import type { RouterRequestInput } from "../router/router-interfaces.js";
53
56
  import {
@@ -87,6 +90,8 @@ import {
87
90
  } from "./origin-guard.js";
88
91
  import { handleRscRendering } from "./rsc-rendering.js";
89
92
  import { withoutShellMissMarker } from "./shell-serve.js";
93
+ import { readWarmMark } from "../prerender/warm-request.js";
94
+ import { defaultSSRModuleLoaderForTests } from "./ssr-module-loader.js";
90
95
  import {
91
96
  withTimeout,
92
97
  isTimeoutEnabled,
@@ -169,7 +174,18 @@ export function createRSCHandler<
169
174
  TEnv = unknown,
170
175
  TRoutes extends Record<string, string> = Record<string, string>,
171
176
  >(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
172
- const { router, version = VERSION, nonce: nonceProvider } = options;
177
+ const { router, nonce: nonceProvider } = options;
178
+ // A consumer-set version (this option, else createRouter({ version })) is
179
+ // used for both; otherwise the router's own pair from the build.
180
+ const versions = resolveRouterVersions(
181
+ router.id,
182
+ options.version ?? router.version,
183
+ );
184
+ // Everything the handler itself versions is tied to the HTML a tab holds:
185
+ // payload metadata (what the browser echoes as `_rsc_v`), the reload check,
186
+ // shell stamping and gating, the rango state value. The data version is only
187
+ // a cache-key prefix, read by the stores from the request context.
188
+ const version = versions.document;
173
189
 
174
190
  // Handler-owned registry of explicit per-scope stores from cache({ store }).
175
191
  // Lives in the closure so it is scoped per handler (multi-router deployments
@@ -195,7 +211,13 @@ export function createRSCHandler<
195
211
  // In dev mode Vite may hot-reload the module, so skip memoization.
196
212
  const rawLoadSSRModule: LoadSSRModule =
197
213
  options.loadSSRModule ??
198
- (() => import.meta.viteRsc.loadModule("ssr", "index"));
214
+ (() =>
215
+ // The test seam (ssr-module-loader.ts). Gated on the build mode, not
216
+ // NODE_ENV: the rsc Vitest project forces NODE_ENV=production, its mode
217
+ // is "test", and a production build folds the whole branch away.
218
+ ((import.meta as any).env?.MODE !== "production" &&
219
+ defaultSSRModuleLoaderForTests()?.()) ||
220
+ import.meta.viteRsc.loadModule("ssr", "index"));
199
221
  let _ssrModulePromise: Promise<SSRModule> | undefined;
200
222
  const loadSSRModule: LoadSSRModule =
201
223
  process.env.NODE_ENV === "production"
@@ -442,12 +464,17 @@ export function createRSCHandler<
442
464
  input: RouterRequestInput<TEnv> = {},
443
465
  ): Promise<Response> {
444
466
  const handlerStart = performance.now();
467
+ // Before any router scope: see captureRequestEntryContext.
468
+ const runAtRequestEntry = captureRequestEntryContext();
445
469
  // A degraded PPR HIT's reload carries the forced-MISS marker: keep only
446
470
  // the flag (set on the request context below) and drop the marker before
447
471
  // anything reads the request, so middleware, handlers, loaders, cache
448
472
  // keys and the SSR search seed see the URL the visitor asked for.
449
473
  const unmarkedRequest = withoutShellMissMarker(incomingRequest);
450
474
  const request = unmarkedRequest ?? incomingRequest;
475
+ // A router.prerender() warm: the mark is the incoming Request object
476
+ // itself (prerender/warm-request.ts), read before anything rebuilds it.
477
+ const warm = readWarmMark(incomingRequest);
451
478
  // Create the metrics store at handler start so handler:total has startTime=0
452
479
  // and all metrics are relative to the request entry point.
453
480
  const earlyMetricsStore = router.debugPerformance
@@ -499,10 +526,12 @@ export function createRSCHandler<
499
526
  let searchParamsFilter: SearchParamsFilter | undefined;
500
527
  const cacheOption = options.cache ?? router.cache;
501
528
  if (cacheOption && !url.searchParams.has("__no_cache")) {
529
+ // A warm writes to the store the trigger checked the scope of.
502
530
  const cacheConfig =
503
- typeof cacheOption === "function"
531
+ warm?.cacheConfig ??
532
+ (typeof cacheOption === "function"
504
533
  ? cacheOption(env, executionCtx)
505
- : cacheOption;
534
+ : cacheOption);
506
535
 
507
536
  if (cacheConfig.enabled !== false) {
508
537
  cacheStore = cacheConfig.store;
@@ -512,6 +541,28 @@ export function createRSCHandler<
512
541
  }
513
542
  }
514
543
 
544
+ // Resolve the writable prerender store (durable overlay) for the serve path.
545
+ // Same factory-or-object shape as cache, resolved per request from env/ctx
546
+ // (never memoized). The version resolves per request, as router.prerender()
547
+ // resolves it per refresh, so both sides key off the same table state.
548
+ let resolvedPrerender:
549
+ | import("../prerender/on-demand.js").ResolvedPrerender<any>
550
+ | undefined;
551
+ const prerenderOption = router._prerenderConfig;
552
+ if (prerenderOption) {
553
+ const prerenderConfig =
554
+ typeof prerenderOption === "function"
555
+ ? prerenderOption(env, executionCtx)
556
+ : prerenderOption;
557
+ if (prerenderConfig?.store) {
558
+ resolvedPrerender = {
559
+ config: prerenderConfig,
560
+ routerId: router.id,
561
+ version: resolvePrerenderVersion(router.id, router.version),
562
+ };
563
+ }
564
+ }
565
+
515
566
  // Route manifest is populated at startup via the virtual module
516
567
  // (virtual:rsc-router/routes-manifest). In build/production, it's inlined
517
568
  // into the bundle. In dev mode (Node), the discovery plugin populates it
@@ -566,15 +617,21 @@ export function createRSCHandler<
566
617
  url,
567
618
  variables,
568
619
  cacheStore,
620
+ prerender: resolvedPrerender,
569
621
  searchParamsFilter,
570
622
  explicitTaggedStores,
571
623
  cacheProfiles: router.cacheProfiles,
572
624
  executionContext: executionCtx,
573
625
  themeConfig: router.themeConfig,
574
626
  stateCookieName: router.resolvedStateCookieName,
575
- version,
627
+ versions,
576
628
  });
577
629
  if (unmarkedRequest) requestContext._shellForcedMiss = true;
630
+ requestContext._runAtRequestEntry = runAtRequestEntry;
631
+ if (warm) {
632
+ requestContext._prerenderWarm = warm;
633
+ requestContext._renderErrors = warm.renderErrors;
634
+ }
578
635
  // Gate on the SAME enabled-semantics withTimeout uses (isTimeoutEnabled):
579
636
  // a `renderStartMs: 0` / negative opt-out disables the timeout, so the
580
637
  // driver's cursor bookkeeping (which only the timeout reads) must be off too.
@@ -438,6 +438,11 @@ function renderPeResponse<TEnv>(
438
438
  strictMode: ctx.router.strictMode,
439
439
  initialTheme: payloadInitialTheme(getRequestContext()),
440
440
  },
441
+ // The same decoded value the SSR render receives, never a second decode:
442
+ // hydrateRoot needs it on the client or useActionState resets. A promise,
443
+ // so a state Flight cannot serialize (a class instance) rejects only this
444
+ // slot instead of failing the payload root and the whole document.
445
+ ...(reactFormState ? { formState: Promise.resolve(reactFormState) } : {}),
441
446
  };
442
447
 
443
448
  const trace = getRequestContext()._activeRoutine;
@@ -470,7 +475,6 @@ function renderPeResponse<TEnv>(
470
475
  {
471
476
  // metricsStore=undefined is safe: the handler already stashed the early
472
477
  // SSR setup promise, so this reuses it instead of starting setup again.
473
- // reactFormState travels through the SSR option, not RscPayload.
474
478
  html: createSsrHtmlStage({
475
479
  ctx,
476
480
  request,
@@ -33,9 +33,11 @@ import {
33
33
  finalizeResponse,
34
34
  } from "./helpers.js";
35
35
  import { reportCacheError } from "../cache/cache-error.js";
36
- import { cacheKeyBase } from "../cache/cache-key-utils.js";
36
+ import { requestKeyBase } from "../cache/cache-key-utils.js";
37
+ import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
37
38
  import {
38
39
  executionStart,
40
+ markResponseStart,
39
41
  predatesInvalidation,
40
42
  type ExecutionStart,
41
43
  } from "../cache/tag-invalidation.js";
@@ -104,7 +106,7 @@ export async function serveResponseRouteWithCache(
104
106
  // Default key: response:{type}: + host-namespaced base (sorted search, reserved
105
107
  // _rsc*/__* params excluded). Same composition as document/segment tiers so
106
108
  // the host-namespacing and search-normalization rules cannot drift.
107
- let cacheKey = `response:${responseType}:${cacheKeyBase(url.host, url.pathname, url.searchParams, undefined, reqCtx._searchParamsFilter)}`;
109
+ let cacheKey = `response:${responseType}:${requestKeyBase(reqCtx, url.host, url.pathname, url.searchParams, undefined, reqCtx._searchParamsFilter)}`;
108
110
 
109
111
  // Keyed like the segment record (CacheScope.resolveKeyFrom): with key()
110
112
  // results on the cache() chain (a full override, namespaced, #975) or an
@@ -180,15 +182,19 @@ export async function serveResponseRouteWithCache(
180
182
  }
181
183
  await store2.putResponse!(
182
184
  cacheKey,
183
- fresh,
185
+ markResponseStart(fresh, start),
184
186
  cacheScope!.ttl,
185
187
  cacheScope!.swr,
186
188
  responseTags,
187
189
  );
190
+ noteWarmWrite(reqCtx, "response");
188
191
  };
189
192
 
190
193
  try {
191
- const cached = await store.getResponse(cacheKey);
194
+ // A router.prerender() warm takes the miss branch and replaces the entry.
195
+ const cached = isWarmReplace(reqCtx)
196
+ ? null
197
+ : await store.getResponse(cacheKey);
192
198
  if (cached && canServeCached(cached.response)) {
193
199
  if (!cached.shouldRevalidate) {
194
200
  return applyPreHandlerCallbacks(cached.response);
@@ -18,6 +18,7 @@ import {
18
18
  buildShellLoaderSeed,
19
19
  countSnapshotFamilies,
20
20
  hasDocRecord,
21
+ shellPrerenderHandles,
21
22
  } from "../cache/shell-snapshot.js";
22
23
  import { appendMetric } from "../router/metrics.js";
23
24
  import type { MetricsStore } from "../server/context.js";
@@ -93,6 +94,10 @@ import {
93
94
  type DevShellLookup,
94
95
  } from "./shell-build-manifest.js";
95
96
  import { contextGet } from "../context-var.js";
97
+ import {
98
+ isWarmReplace,
99
+ noteWarmShellEvent,
100
+ } from "../prerender/warm-request.js";
96
101
  import {
97
102
  resolveSwrWindow,
98
103
  staleShellRecaptureDue,
@@ -465,6 +470,13 @@ function* shellServePlan<TEnv>(
465
470
  ): RoutinePlan<ShellServeOutcome> {
466
471
  const { ctx, request, env, url, isPartial, handleStore, nonce, reqCtx } =
467
472
  input;
473
+ const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
474
+
475
+ // A router.prerender() warm (prerender/warm-request.ts) reports the shell of
476
+ // a ppr route: "not-eligible" stands for every gate below that passes, until
477
+ // a HIT or a capture event overwrites it.
478
+ const warm = reqCtx._prerenderWarm;
479
+ if (warm && resolvePprConfig(manifestEntry)) warm.shell ??= "not-eligible";
468
480
 
469
481
  if (
470
482
  isPartial ||
@@ -474,9 +486,11 @@ function* shellServePlan<TEnv>(
474
486
  ) {
475
487
  return { kind: "pass" };
476
488
  }
477
- const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
478
489
  const pprConfig = resolvePprConfig(manifestEntry);
479
490
  if (!pprConfig) return { kind: "pass" };
491
+ // In replace mode the request is a MISS without reading a shell, and its
492
+ // capture is forced (ShellCaptureDescriptor.force).
493
+ const warmReplace = isWarmReplace(reqCtx);
480
494
  // A degraded HIT's reload (shellReloadScript) renders like a cache miss:
481
495
  // no shell read, no capture, so it can never degrade again. The handler
482
496
  // stripped the marker from the request and left this flag.
@@ -487,7 +501,7 @@ function* shellServePlan<TEnv>(
487
501
  // other visitor. Every step below, a HIT included, therefore runs without
488
502
  // one.
489
503
  const store = reqCtx._cacheStore;
490
- const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
504
+ const baseKey = buildShellKey(ctx.router.id, url, reqCtx._searchParamsFilter);
491
505
  if (activeRequestNonce(nonce, reqCtx) !== undefined) {
492
506
  // Declared intent that cannot be honored deserves a diagnostic (unlike an
493
507
  // undeclared route, which is silent): a ppr route gated off by an active
@@ -551,12 +565,16 @@ function* shellServePlan<TEnv>(
551
565
  // A MISS schedules its capture after rendering: taken before the read, this
552
566
  // lets that capture skip itself when another request's capture stored the
553
567
  // shell in between (ShellCaptureDescriptor.storedSeqAtRead).
554
- const storedSeqAtRead = lastStoredCaptureSeq(key);
568
+ // A warm's capture is its own: one another request stored meanwhile does
569
+ // not cancel it, so it takes no sequence.
570
+ const storedSeqAtRead = warmReplace ? undefined : lastStoredCaptureSeq(key);
555
571
  // SSR setup starts before route handling, so read the shell before joining it
556
572
  // to overlap the remaining setup work with cache I/O.
557
- const cached = yield* step("shell-read", () =>
558
- readShellEntry(store, key, reqCtx, pprConfig.tags),
559
- );
573
+ const cached = warmReplace
574
+ ? null
575
+ : yield* step("shell-read", () =>
576
+ readShellEntry(store, key, reqCtx, pprConfig.tags),
577
+ );
560
578
 
561
579
  // allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
562
580
  // streaming, so bots/SEO crawlers get one complete axis-1 document. These
@@ -579,6 +597,15 @@ function* shellServePlan<TEnv>(
579
597
  pprConfig,
580
598
  store,
581
599
  );
600
+ if (warm) {
601
+ // The warm learns its capture's outcome from the capture's own events,
602
+ // ahead of the router's debugShellCapture sink.
603
+ const routerSink = descriptor.debugSink;
604
+ descriptor.debugSink = (event) => {
605
+ noteWarmShellEvent(warm, event);
606
+ routerSink?.(event);
607
+ };
608
+ }
582
609
  // MISS (no entry, invalid reactVersion, a tombstone, or store read
583
610
  // failure): axis 1 + a background capture scheduled once the response is
584
611
  // known servable.
@@ -587,6 +614,10 @@ function* shellServePlan<TEnv>(
587
614
  descriptor: { ...descriptor, storedSeqAtRead },
588
615
  ssrModule,
589
616
  };
617
+ // Neither the stored shell nor the build shell is read: a warm renders.
618
+ if (warmReplace) {
619
+ return { ...miss, descriptor: { ...descriptor, force: true } };
620
+ }
590
621
 
591
622
  // A HIT replays the handler layer from the entry's doc record (`docKey`),
592
623
  // or, for a Prerender route, from the prerender store. An entry that has
@@ -612,6 +643,9 @@ function* shellServePlan<TEnv>(
612
643
  "[ShellServe] getShell",
613
644
  );
614
645
  } else {
646
+ // An onlyIfStale warm found a servable shell; a recapture scheduled
647
+ // below overwrites this through the capture's events.
648
+ if (warm) warm.shell = "fresh";
615
649
  // Stale (SWR) hit: serve the stale shell now, recapture in the
616
650
  // background (stampede-guarded + backoff inside scheduleShellCapture),
617
651
  // at most once per SHELL_MIN_RECAPTURE_INTERVAL_MS of the shell's age.
@@ -680,17 +714,23 @@ function* shellServePlan<TEnv>(
680
714
  // route warned. A search-bearing URL never reads one: nothing to probe.
681
715
  const routeName = reqCtx._classifiedRoute?.routeKey;
682
716
  if (
683
- !partitionBuildShellCheckDone(url.pathname, routeName) &&
717
+ !partitionBuildShellCheckDone(ctx.router.id, url.pathname, routeName) &&
684
718
  shellSearchSeed(url, reqCtx._searchParamsFilter) === ""
685
719
  ) {
686
720
  const found = yield* step("build-shell-check", () =>
687
- hasBuildShell(url.pathname, devShellLookup),
721
+ hasBuildShell(ctx.router.id, url.pathname, devShellLookup),
722
+ );
723
+ notePartitionBuildShellCheck(
724
+ ctx.router.id,
725
+ url.pathname,
726
+ routeName,
727
+ found,
688
728
  );
689
- notePartitionBuildShellCheck(url.pathname, routeName, found);
690
729
  }
691
730
  } else {
692
731
  buildHit = yield* step("build-shell-lookup", () =>
693
732
  lookupBuildShell(
733
+ ctx.router.id,
694
734
  url,
695
735
  ctx.version,
696
736
  store,
@@ -705,6 +745,7 @@ function* shellServePlan<TEnv>(
705
745
  ? openShellDocumentMetered(reqCtx, { entry: buildHit.entry })
706
746
  : null;
707
747
  if (buildHit && buildDocument) {
748
+ if (warm) warm.shell = "fresh";
708
749
  // Past ppr.ttl: still serve the baked entry, recapture upgrades it.
709
750
  if (buildHit.stale) {
710
751
  yield* handoff("shell-recapture", () =>
@@ -1336,7 +1377,7 @@ async function matchPartialWithPprReplay<TEnv>(
1336
1377
  // null and the seeded record could not have been consulted, which used to
1337
1378
  // read as a misleading `snapshot-miss` AFTER two wasted getShell reads.
1338
1379
  // Same predicate as resolveNavigation, decided before any store I/O.
1339
- if (!getNavigationContextHeader(request)) {
1380
+ if (!getNavigationContextHeader(request, url.origin)) {
1340
1381
  return runMatch({ outcome: "BYPASS", reason: "no-navigation-context" });
1341
1382
  }
1342
1383
  // Fragment passthrough (#700), extended to partial replay. Navigation
@@ -1398,7 +1439,7 @@ async function matchPartialWithPprReplay<TEnv>(
1398
1439
  // The visitor's own partition (resolveShellPartition): replay never
1399
1440
  // crosses into another partition's shell.
1400
1441
  const key = await resolveRequestShellKey(
1401
- buildShellKey(url, reqCtx._searchParamsFilter),
1442
+ buildShellKey(ctx.router.id, url, reqCtx._searchParamsFilter),
1402
1443
  routeSnapshot,
1403
1444
  store,
1404
1445
  reqCtx,
@@ -1745,6 +1786,17 @@ function serveShellHit(
1745
1786
  // close over that same context or a streaming tail inherits the base
1746
1787
  // context's premature non-streaming handle snapshot.
1747
1788
  wireRenderBarrier(tailCtx, handleStore);
1789
+ // The tail's render barrier resolves once its record is replayed and
1790
+ // before a loader runs (cache-lookup.ts withCacheLookup, yieldFromStore).
1791
+ // The store then holds what the prelude was rendered from, so that is
1792
+ // the snapshot the document hydrates with; what this request's loaders
1793
+ // do to it afterwards reaches the client after hydration
1794
+ // (HandleStore.freezeDocumentSnapshot, issue #1035).
1795
+ const resolveRenderBarrier = tailCtx._resolveRenderBarrier;
1796
+ tailCtx._resolveRenderBarrier = (segments) => {
1797
+ handleStore.freezeDocumentSnapshot();
1798
+ resolveRenderBarrier(segments);
1799
+ };
1748
1800
  return tailCtx;
1749
1801
  };
1750
1802
 
@@ -1885,7 +1937,7 @@ function serveShellHit(
1885
1937
  // fresh; a promise-free bake-lane loader is served from its pin
1886
1938
  // (_shellLoaderSeed, loader-cache.ts). A Prerender route's tail takes
1887
1939
  // the handler layer from the prerender store before this scope is
1888
- // consulted.
1940
+ // consulted, and the loader pushes from `prerenderHandles`.
1889
1941
  seededCtx._shellImplicitCache = {
1890
1942
  ttl: descriptor.ttl,
1891
1943
  swr: descriptor.swr,
@@ -1893,6 +1945,7 @@ function serveShellHit(
1893
1945
  keyPrefix: "doc",
1894
1946
  docTail: true,
1895
1947
  fixedDocKey: entry.docKey,
1948
+ prerenderHandles: shellPrerenderHandles(records),
1896
1949
  };
1897
1950
  // Fragment splice (issue #700): store hits in THIS tail emit their stored
1898
1951
  // segment fragments verbatim (expanded by segment-fragments.ts). The flag
@@ -11,8 +11,9 @@
11
11
  * served through the same serveShellHit as a captured one.
12
12
  *
13
13
  * Lifecycle:
14
- * - No expiry until the next deploy — the buildVersion gate retires entries
15
- * the moment a new build ships (a new manifest replaces them anyway).
14
+ * - No expiry until the next deploy: every build ships its own manifest, and
15
+ * its entries carry that build's document version for the router, which is
16
+ * what the buildVersion gate compares.
16
17
  * - `ppr.ttl` drives STALENESS ONLY: past createdAt + ttl the entry still
17
18
  * serves, but a runtime recapture is scheduled — SWR is the UPGRADE path
18
19
  * from build entry to fresher runtime entry, not the bootstrap path. The
@@ -54,7 +55,7 @@ export interface BuildShellEntry {
54
55
  }
55
56
 
56
57
  interface ShellManifestModule {
57
- /** Manifest key (pathname — see shell-manifest-key.ts) -> asset specifier. */
58
+ /** Manifest key (router id + pathname, shell-manifest-key.ts) -> asset specifier. */
58
59
  default: Record<string, string>;
59
60
  loadShellAsset: (spec: string) => Promise<{ default: BuildShellEntry }>;
60
61
  }
@@ -90,9 +91,8 @@ function loadManifest(): Promise<ShellManifestModule | null> {
90
91
  * Only the per-request gates (tag markers, staleness) stay outside the memo;
91
92
  * the document HIT decodes the prelude once per serve (openShellDocument),
92
93
  * partial replay never does. `null` memoizes a failed verdict —
93
- * deterministically invalid. Spec-only keying is sound because buildVersion is
94
- * process-constant on the manifest path (folded into the shipped worker; dev
95
- * never loads a manifest).
94
+ * deterministically invalid. Keyed by the asking router's document version as
95
+ * well as the spec: the verdict is that version's.
96
96
  */
97
97
  const validatedSpecs = new Map<string, ValidatedBuildShellEntry | null>();
98
98
 
@@ -110,7 +110,8 @@ async function validatedManifestRecord(
110
110
  spec: string,
111
111
  buildVersion: string,
112
112
  ): Promise<ValidatedBuildShellEntry | undefined> {
113
- let verdict = validatedSpecs.get(spec);
113
+ const memoKey = `${buildVersion}\u0000${spec}`;
114
+ let verdict = validatedSpecs.get(memoKey);
114
115
  if (verdict === undefined) {
115
116
  const record = (await mod.loadShellAsset(spec)).default;
116
117
  verdict =
@@ -118,7 +119,7 @@ async function validatedManifestRecord(
118
119
  hasIntactShellPayload(record.entry)
119
120
  ? (record as ValidatedBuildShellEntry)
120
121
  : null;
121
- validatedSpecs.set(spec, verdict);
122
+ validatedSpecs.set(memoKey, verdict);
122
123
  }
123
124
  return verdict ?? undefined;
124
125
  }
@@ -267,9 +268,11 @@ async function fetchDevShellEntry(
267
268
  * request is search-less, like the entries lookupBuildShell serves): a
268
269
  * manifest entry for the path in production, a Prerender route in dev. No
269
270
  * asset load and no dev capture: it only decides whether a request that
270
- * cannot read the build shell deserves a warning.
271
+ * cannot read the build shell deserves a warning. The manifest entry is the
272
+ * one staged for the router `routerId` (shell-manifest-key.ts).
271
273
  */
272
274
  export async function hasBuildShell(
275
+ routerId: string,
273
276
  pathname: string,
274
277
  dev?: DevShellLookup,
275
278
  ): Promise<boolean> {
@@ -277,11 +280,12 @@ export async function hasBuildShell(
277
280
  return dev?.isPrerenderRoute === true;
278
281
  }
279
282
  const mod = await loadManifest();
280
- return mod?.default[buildShellManifestKey(pathname)] !== undefined;
283
+ return mod?.default[buildShellManifestKey(routerId, pathname)] !== undefined;
281
284
  }
282
285
 
283
286
  /**
284
287
  * Look up the baked shell entry for a request, applying every serve gate:
288
+ * the serving router's own entry only (`routerId`, shell-manifest-key.ts),
285
289
  * search-less requests only (the build captured the bare pathname; a
286
290
  * search-bearing URL has its own shell identity owned by runtime capture),
287
291
  * version validity, payload integrity, and tag-invalidation markers. Returns
@@ -294,6 +298,7 @@ export async function hasBuildShell(
294
298
  * traffic the shell was prerendered for.
295
299
  */
296
300
  export async function lookupBuildShell(
301
+ routerId: string,
297
302
  url: URL,
298
303
  buildVersion: string,
299
304
  store: SegmentCacheStore,
@@ -311,7 +316,7 @@ export async function lookupBuildShell(
311
316
  if (hasManifest) {
312
317
  const mod = await loadManifest();
313
318
  if (!mod) return null;
314
- const spec = mod.default[buildShellManifestKey(url.pathname)];
319
+ const spec = mod.default[buildShellManifestKey(routerId, url.pathname)];
315
320
  if (!spec) return null;
316
321
  record = await validatedManifestRecord(mod, spec, buildVersion);
317
322
  } else if (dev) {
@@ -338,7 +343,7 @@ export async function lookupBuildShell(
338
343
  // diagnostic; the route keeps runtime-capture semantics.
339
344
  warnOnce(
340
345
  "build-shell-tag-check",
341
- buildShellManifestKey(url.pathname),
346
+ buildShellManifestKey(routerId, url.pathname),
342
347
  () =>
343
348
  `[rango] Build-time shell for "${url.pathname}" carries cache tags, but ` +
344
349
  "the app cache store cannot answer tag-invalidation history durably " +
@@ -6,7 +6,10 @@
6
6
  * pulling the capture orchestration module or shell-serve.ts (React).
7
7
  */
8
8
 
9
- import { sortedSearchString } from "../cache/cache-key-utils.js";
9
+ import {
10
+ routerKeyPrefix,
11
+ sortedSearchString,
12
+ } from "../cache/cache-key-utils.js";
10
13
  import type { SearchParamsFilter } from "../cache/search-params-filter.js";
11
14
 
12
15
  /**
@@ -85,13 +88,14 @@ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
85
88
  }
86
89
 
87
90
  /**
88
- * Shell cache key: host + pathname + sorted search + a `:shell` namespace suffix
89
- * (so it can never collide with a document-cache key; the store further isolates
90
- * the shell family internally).
91
+ * Shell cache key: router id + host + pathname + sorted search + a `:shell`
92
+ * namespace suffix (so it can never collide with a document-cache key; the
93
+ * store further isolates the shell family internally).
91
94
  *
92
95
  * The key includes the request HOST: in a multi-tenant host-router deployment
93
96
  * (one worker, one shared KV/runtime-cache store) a host-less key would serve
94
- * tenant A's captured shell to tenant B's users.
97
+ * tenant A's captured shell to tenant B's users. It starts with the serving
98
+ * ROUTER for the same reason (cache-key-utils.ts, the router rule).
95
99
  *
96
100
  * `filter` is the request's compiled `cache.searchParams` config
97
101
  * (ctx._searchParamsFilter): excluded params collapse onto one shell slot.
@@ -99,8 +103,12 @@ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
99
103
  * and lookup makes every shell request a permanent miss. The testing helper
100
104
  * `shellCacheKey` (testing/shell-status.ts) builds its key with it too.
101
105
  */
102
- export function buildShellKey(url: URL, filter?: SearchParamsFilter): string {
103
- return `${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
106
+ export function buildShellKey(
107
+ routerId: string,
108
+ url: URL,
109
+ filter?: SearchParamsFilter,
110
+ ): string {
111
+ return `${routerKeyPrefix(routerId)}${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
104
112
  }
105
113
 
106
114
  /**
@@ -173,6 +181,30 @@ export const POST_QUIESCE_TASK_HOPS = 16;
173
181
  */
174
182
  export const SHELL_CAPTURE_TASK_HARD_CAP_MS = 25_000;
175
183
 
184
+ /**
185
+ * Why a capture attempt refused to store its shell
186
+ * (ShellCaptureDebugEvent.refusal, `caches.refusal` on a `router.prerender()`
187
+ * result): `identity` (a request-scoped read: cookies(), headers(), the
188
+ * theme, a `{ cache: false }` variable), `dynamic` (`ctx.dynamic()`),
189
+ * `loader` (a `{ ssr: false }` loader rejected, or settled with
190
+ * redirect()/notFound()), `no-record` (no doc record: the route's cache()
191
+ * refused the write, or the handler output failed), `handles` (a pushed
192
+ * handle value did not finish encoding), `size` (over the store's value
193
+ * limit), `record-expired` (the route cache() record the capture wrote ran
194
+ * out first), `invalidated` (a tag of the shell was invalidated after the
195
+ * capture started), `uncacheable` (the store cannot hold the entry).
196
+ */
197
+ export type ShellCaptureRefusal =
198
+ | "identity"
199
+ | "dynamic"
200
+ | "loader"
201
+ | "no-record"
202
+ | "handles"
203
+ | "size"
204
+ | "record-expired"
205
+ | "invalidated"
206
+ | "uncacheable";
207
+
176
208
  /**
177
209
  * Shared PPR loader-lane hint, appended to every capture warning or error
178
210
  * whose fix depends on the lane (shell-capture.ts warnings, the cookie-store.ts