@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
@@ -2,36 +2,30 @@ import type { ResolvedSegment } from "./types.js";
2
2
  import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
3
3
 
4
4
  /**
5
- * Cache of aggregate Promise.all results keyed on the first loader's
6
- * `loaderData` reference. Each entry holds the source refs it was built from
7
- * plus the resulting Promise/array; lookup scans entries for the matching
8
- * source array (typically a single entry, since distinct loader groups rarely
9
- * share a first source). Object first-refs live in a WeakMap (auto-GC);
10
- * primitive first-refs (strings/numbers/booleans/null) live in a Map so
11
- * loaders that resolve to primitive data are memoized too. The per-key array
12
- * is capped (MAX_ENTRIES_PER_KEY, oldest evicted) so a long session under a
13
- * stable first-ref does not grow it without bound.
5
+ * Browser: a boundary is handed the settled value. React treats a promise as
6
+ * fulfilled only after it has read it, and a boundary on screen handed a
7
+ * promise it has not read, in a render that cannot wait, shows its fallback.
8
+ * So a cache entry records its array once the aggregate fulfils and later
9
+ * calls return that same array; until then (and when it rejects) they return
10
+ * the promise. A boundary with no loaders gets one shared empty array.
14
11
  *
15
- * Keying externally means reconciliation's fresh segment objects no longer
16
- * drop memoization — the cache survives as long as the underlying loader
17
- * segments do, and GC collects entries when those loaders are released
18
- * (object keys only).
12
+ * Server: a fresh promise per call, so Suspense emits the fallback in the
13
+ * streamed HTML. A shared resolved promise would carry React's `.status`
14
+ * across requests and skip it.
19
15
  *
20
- * Browser-only. On the server each SSR render needs a fresh Promise so
21
- * Suspense can actually suspend and emit the loading fallback HTML before
22
- * content streams. A shared already-resolved promise has `.status` attached
23
- * by React on first `use()`; subsequent observations return synchronously
24
- * and skip the fallback. The zero-loader case is especially prone because
25
- * every empty-loader site would otherwise share one promise across requests.
16
+ * Entries are keyed on the first loader's `loaderData` reference (object refs
17
+ * in a WeakMap, primitives in a Map), so reconciliation's fresh segment
18
+ * objects keep hitting them. Each key's entry list is capped (see
19
+ * MAX_ENTRIES_PER_KEY).
26
20
  */
27
21
  const IS_BROWSER = typeof window !== "undefined";
28
22
 
29
23
  interface LoaderCacheEntry {
30
24
  sources: any[];
31
- // buildLoaderPromise always returns a Promise, so the cached value is never a
32
- // bare array. The public getMemoizedLoaderPromise return type stays broader
33
- // (Promise<any[]> | any[]) to mirror its siblings.
25
+ // Resolves to the same array it records in `value`; a rejection passes
26
+ // through untouched.
34
27
  promise: Promise<any[]>;
28
+ value?: any[];
35
29
  }
36
30
 
37
31
  // Cap the per-key entries array. A stable first-ref (e.g. a layout loader whose
@@ -51,12 +45,8 @@ const primitiveLoaderCache = IS_BROWSER
51
45
  ? new Map<unknown, LoaderCacheEntry[]>()
52
46
  : null;
53
47
 
54
- // In the browser, a single shared empty aggregate is safe (and desirable) —
55
- // reusing the same resolved promise keeps React's `use()` in a known-fulfilled
56
- // state across renders. On the server it would leak `.status = "fulfilled"`
57
- // across requests and skip the Suspense fallback, so we rebuild on each call.
58
- const SHARED_EMPTY_LOADER_PROMISE: Promise<any[]> | null = IS_BROWSER
59
- ? Promise.resolve([])
48
+ const SHARED_EMPTY_LOADERS: any[] | null = IS_BROWSER
49
+ ? (Object.freeze([]) as unknown as any[])
60
50
  : null;
61
51
 
62
52
  function hasSameReferences(a: any[], b: any[]): boolean {
@@ -117,12 +107,8 @@ function isObjectLike(value: unknown): value is object {
117
107
  }
118
108
 
119
109
  /**
120
- * Memoize an aggregate Promise.all for a set of loader segments. Reusing the
121
- * same aggregate across renders — invalidated only when any underlying
122
- * loader.loaderData ref changes — keeps React's `use()` in "known fulfilled"
123
- * state and prevents a fresh Promise.all from suspending (and briefly
124
- * committing the Suspense fallback) on every partial update that doesn't
125
- * actually change loader data.
110
+ * The aggregate for a set of loader segments, memoized on their `loaderData`
111
+ * refs: the settled array once it has fulfilled, the promise before that.
126
112
  *
127
113
  * @internal
128
114
  */
@@ -130,7 +116,7 @@ export function getMemoizedLoaderPromise(
130
116
  loaders: ResolvedSegment[],
131
117
  ): Promise<any[]> | any[] {
132
118
  if (loaders.length === 0) {
133
- return SHARED_EMPTY_LOADER_PROMISE ?? buildLoaderPromise(loaders);
119
+ return SHARED_EMPTY_LOADERS ?? buildLoaderPromise(loaders);
134
120
  }
135
121
  if (!objectLoaderCache || !primitiveLoaderCache) {
136
122
  return buildLoaderPromise(loaders);
@@ -145,13 +131,17 @@ export function getMemoizedLoaderPromise(
145
131
  if (entries) {
146
132
  for (const entry of entries) {
147
133
  if (hasSameReferences(entry.sources, sources)) {
148
- return entry.promise;
134
+ return entry.value ?? entry.promise;
149
135
  }
150
136
  }
151
137
  }
152
138
 
153
- const promise = buildLoaderPromise(loaders);
154
- const newEntry: LoaderCacheEntry = { sources, promise };
139
+ const newEntry = { sources } as LoaderCacheEntry;
140
+ const promise = buildLoaderPromise(loaders).then((values) => {
141
+ newEntry.value = values;
142
+ return values;
143
+ });
144
+ newEntry.promise = promise;
155
145
  if (entries) {
156
146
  // Bound the array: drop the oldest entry before appending when at the cap.
157
147
  if (entries.length >= MAX_ENTRIES_PER_KEY) {
@@ -5,7 +5,7 @@ import {
5
5
  type ComponentType,
6
6
  isValidElement,
7
7
  } from "react";
8
- import { OutletProvider } from "./outlet-provider.js";
8
+ import { AuditedOutletProvider, OutletProvider } from "./outlet-provider.js";
9
9
  import { withOptimisticCommitNone } from "./browser/optimistic-commit.js";
10
10
  import { MountContextProvider } from "./browser/react/mount-context.js";
11
11
  import type { ResolvedSegment, RootLayoutProps } from "./types.js";
@@ -13,17 +13,25 @@ import { decodeLoaderResults } from "./decode-loader-results.js";
13
13
  import { LoaderRedirect } from "./loader-redirect.js";
14
14
  import { invariant } from "./errors.js";
15
15
  import {
16
+ AuditedLoaderBoundary,
17
+ AuditedRouteContent,
16
18
  RouteContentWrapper,
17
19
  LoaderBoundary,
18
20
  StreamedLoaderErrorBoundary,
19
21
  } from "./route-content-wrapper.js";
20
22
  import { RootErrorBoundary } from "./root-error-boundary.js";
21
23
  import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
22
- import { getMemoizedContentPromise } from "./segment-content-promise.js";
24
+ import { INTERNAL_RANGO_SUSPENSE_AUDIT } from "./internal-suspense-audit.js";
25
+ import { getBoundaryContent } from "./segment-boundary-content.js";
23
26
  import {
24
27
  buildLoaderPromise,
25
28
  getMemoizedLoaderPromise,
26
29
  } from "./segment-loader-promise.js";
30
+ import {
31
+ auditSegmentElement,
32
+ finishTreeAudit,
33
+ startTreeAudit,
34
+ } from "./suspense-audit.js";
27
35
 
28
36
  /**
29
37
  * Debug log for the segment tree build, gated on the baked flag. Runs on BOTH
@@ -212,13 +220,25 @@ function wrapDefaultOutletContent(
212
220
  } as any);
213
221
  }
214
222
 
215
- if (content.type === OutletProvider && props.segment?.type === "layout") {
223
+ if (
224
+ content.type ===
225
+ (process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
226
+ ? AuditedOutletProvider
227
+ : OutletProvider) &&
228
+ props.segment?.type === "layout"
229
+ ) {
216
230
  return React.cloneElement(content, {
217
231
  content: wrapDefaultOutletContent(props.content, transition),
218
232
  } as any);
219
233
  }
220
234
 
221
- if (content.type === LoaderBoundary && props.segment?.type === "layout") {
235
+ if (
236
+ content.type ===
237
+ (process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
238
+ ? AuditedLoaderBoundary
239
+ : LoaderBoundary) &&
240
+ props.segment?.type === "layout"
241
+ ) {
222
242
  return React.cloneElement(content, {
223
243
  outletContent: wrapDefaultOutletContent(props.outletContent, transition),
224
244
  } as any);
@@ -376,6 +396,10 @@ export async function renderSegments(
376
396
  }
377
397
  // Separate segments by type, passing intercept segments for explicit injection
378
398
  const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
399
+ // Dev only: the wrapper chain per segment, compared with the previous
400
+ // tree's (suspense-audit.ts, I5).
401
+ const treeAudit =
402
+ process.env.NODE_ENV !== "production" ? startTreeAudit() : undefined;
379
403
 
380
404
  // A route is "in a transition scope" when its own segment OR any layout in
381
405
  // its matched chain declares transition(). Both transition() forms land here:
@@ -473,34 +497,33 @@ export async function renderSegments(
473
497
 
474
498
  let nodeContent: ReactNode = null;
475
499
  if (!clientGroup && isRenderableLoading(loading)) {
476
- // forceAwait (popstate, stale-revalidation, fully-prefetched nav) renders a
477
- // loading() route with the route content ALREADY resolved, so its
478
- // RouteContentWrapper Suspender does not suspend for a microtask and flash
479
- // the loading() fallback on a NORMAL (non-transition) commit. The router
480
- // data is known-ready on these paths, so awaiting the content here is free.
481
- // The wrapper tree is unchanged (RouteContentWrapper is still created with
482
- // the same key/fallback) — only the `content` prop is a resolved node
483
- // instead of a pending promise, which Suspender renders synchronously. This
484
- // mirrors the forceAwait loaderData unwrap above; a CLIENT component that
485
- // suspends on mount inside the content still reveals a fallback (it is not
486
- // pre-resolved).
487
- const contentPromise = getMemoizedContentPromise(resolvedComponent);
488
- let loadingContent: Promise<ReactNode> | ReactNode = contentPromise;
500
+ // The wrapper tree is the same on every path; only `content` differs. A
501
+ // settled component is handed over as the node (getBoundaryContent), a
502
+ // pending one as its promise, so only pending work shows the loading()
503
+ // fallback. forceAwait also awaits a pending component here. A CLIENT
504
+ // component that suspends on mount inside the content still reveals a
505
+ // fallback.
506
+ let loadingContent: ReactNode = getBoundaryContent(resolvedComponent);
489
507
  if (forceAwait) {
490
508
  const contentAwaitStart = segDebug ? performance.now() : 0;
491
- loadingContent = await contentPromise;
509
+ loadingContent = await loadingContent;
492
510
  if (segDebug) {
493
511
  segDebugLog(`segment ${id}: content awaited (forceAwait)`, {
494
512
  ms: Math.round(performance.now() - contentAwaitStart),
495
513
  });
496
514
  }
497
515
  }
498
- nodeContent = createElement(RouteContentWrapper, {
499
- key: `suspense-loading-${id}`,
500
- content: loadingContent,
501
- fallback: loading,
502
- segmentId: id,
503
- });
516
+ nodeContent = createElement(
517
+ process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
518
+ ? AuditedRouteContent
519
+ : RouteContentWrapper,
520
+ {
521
+ key: `suspense-loading-${id}`,
522
+ content: loadingContent,
523
+ fallback: loading,
524
+ segmentId: id,
525
+ },
526
+ );
504
527
  } else {
505
528
  // [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. A segment in the no-loading()
506
529
  // branch whose component decodes as a Promise/lazy gets registered into
@@ -593,7 +616,10 @@ export async function renderSegments(
593
616
  let boundaryAwaitedLoaderIds: string[] | undefined;
594
617
  if (forceAwait || isAction) {
595
618
  const awaitStart = segDebug ? performance.now() : 0;
596
- boundaryLoaderData = await loaderDataPromise;
619
+ boundaryLoaderData =
620
+ loaderDataPromise instanceof Promise
621
+ ? await loaderDataPromise
622
+ : loaderDataPromise;
597
623
  if (segDebug) {
598
624
  segDebugLog(`segment ${id}: loaders awaited (forceAwait/action)`, {
599
625
  loaderIds,
@@ -621,28 +647,38 @@ export async function renderSegments(
621
647
  );
622
648
  }
623
649
  }
624
- content = createElement(LoaderBoundary, {
625
- key: `loader-boundary-${key}`,
626
- loaderDataPromise: boundaryLoaderData,
627
- loaderIds,
628
- loaderStreams: boundaryLoaderStreams,
629
- awaitedLoaderIds: boundaryAwaitedLoaderIds,
630
- fallback: loading,
631
- outletKey: key,
632
- outletContent,
633
- segment: node.segment,
634
- parallel: node.parallel,
635
- children: nodeContent,
636
- });
650
+ content = createElement(
651
+ process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
652
+ ? AuditedLoaderBoundary
653
+ : LoaderBoundary,
654
+ {
655
+ key: `loader-boundary-${key}`,
656
+ loaderDataPromise: boundaryLoaderData,
657
+ loaderIds,
658
+ loaderStreams: boundaryLoaderStreams,
659
+ awaitedLoaderIds: boundaryAwaitedLoaderIds,
660
+ fallback: loading,
661
+ outletKey: key,
662
+ outletContent,
663
+ segment: node.segment,
664
+ parallel: node.parallel,
665
+ children: nodeContent,
666
+ },
667
+ );
637
668
  } else if (loaderEntries.length === 0) {
638
- content = createElement(OutletProvider, {
639
- key,
640
- content: outletContent,
641
- segment: node.segment,
642
- parallel: node.parallel,
643
- pending: outletPending,
644
- children: nodeContent,
645
- });
669
+ content = createElement(
670
+ process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
671
+ ? AuditedOutletProvider
672
+ : OutletProvider,
673
+ {
674
+ key,
675
+ content: outletContent,
676
+ segment: node.segment,
677
+ parallel: node.parallel,
678
+ pending: outletPending,
679
+ children: nodeContent,
680
+ },
681
+ );
646
682
  } else {
647
683
  const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
648
684
  const parallelOwnedLoaders = loaderEntries.filter(
@@ -776,17 +812,23 @@ export async function renderSegments(
776
812
  }
777
813
  }
778
814
 
779
- content = createElement(OutletProvider, {
780
- key,
781
- content: outletContent,
782
- segment: node.segment,
783
- parallel: node.parallel,
784
- loaderData: Object.keys(loaderData).length > 0 ? loaderData : undefined,
785
- loaderStreams,
786
- awaitedLoaderIds,
787
- pending: outletPending,
788
- children: errorFallback ?? nodeContent,
789
- });
815
+ content = createElement(
816
+ process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
817
+ ? AuditedOutletProvider
818
+ : OutletProvider,
819
+ {
820
+ key,
821
+ content: outletContent,
822
+ segment: node.segment,
823
+ parallel: node.parallel,
824
+ loaderData:
825
+ Object.keys(loaderData).length > 0 ? loaderData : undefined,
826
+ loaderStreams,
827
+ awaitedLoaderIds,
828
+ pending: outletPending,
829
+ children: errorFallback ?? nodeContent,
830
+ },
831
+ );
790
832
  }
791
833
 
792
834
  // Wrap with MountContextProvider for include() scoped components.
@@ -800,6 +842,22 @@ export async function renderSegments(
800
842
  });
801
843
  }
802
844
 
845
+ if (process.env.NODE_ENV !== "production") {
846
+ auditSegmentElement(
847
+ treeAudit,
848
+ id,
849
+ node.segment.type,
850
+ key,
851
+ content,
852
+ node.segment.type === "layout" &&
853
+ !!ReactViewTransition &&
854
+ !!transition &&
855
+ transition.viewTransition !== false,
856
+ node.parallel.map((p) => p.id),
857
+ [node.segment, ...node.parallel, ...node.loaders],
858
+ );
859
+ }
860
+
803
861
  if (segDebug) {
804
862
  segDebugLog(`segment ${id} built`, {
805
863
  type: node.segment.type,
@@ -849,6 +907,8 @@ export async function renderSegments(
849
907
  });
850
908
  }
851
909
 
910
+ if (process.env.NODE_ENV !== "production") finishTreeAudit(treeAudit, result);
911
+
852
912
  return result;
853
913
  }
854
914
 
@@ -0,0 +1,130 @@
1
+ /// <reference path="../vite/plugins/version.d.ts" />
2
+ /**
3
+ * Runtime side of the per-router cache versions: binds the
4
+ * `@rangojs/router:version` virtual module and answers "which versions does
5
+ * this router, or this request, use".
6
+ *
7
+ * Design: docs/design/per-app-cache-version.md.
8
+ */
9
+ import { ROUTER_VERSIONS, VERSION } from "@rangojs/router:version";
10
+ import {
11
+ resolveVersionsFrom,
12
+ uniformVersions,
13
+ type RouterVersions,
14
+ type RouterVersionsTable,
15
+ } from "../router-versions.js";
16
+ import { _getRequestContext } from "./request-context.js";
17
+
18
+ /**
19
+ * The build's table. Module state only so the testing primitive
20
+ * (testing/build-versions.ts) can install one where the build's is absent;
21
+ * production never reassigns it.
22
+ */
23
+ let table: RouterVersionsTable | undefined = ROUTER_VERSIONS;
24
+
25
+ /**
26
+ * The versions a router serves with, in order:
27
+ *
28
+ * 1. `override`, a consumer-set `version` (createRouter or createRSCHandler):
29
+ * that exact value for both.
30
+ * 2. The router's entry in the build table, else the whole-build entry.
31
+ * 3. `VERSION` for both: the dev stamp, bumped on every RSC module edit.
32
+ */
33
+ export function resolveRouterVersions(
34
+ routerId: string | undefined,
35
+ override?: string,
36
+ ): RouterVersions {
37
+ return (
38
+ resolveVersionsFrom(table, routerId, override) ?? uniformVersions(VERSION)
39
+ );
40
+ }
41
+
42
+ /**
43
+ * The version an on-demand prerender overlay key carries: the router's data
44
+ * version, because the overlay stores segment data, as the segment cache does
45
+ * (a client-only deploy keeps both). The write side (`router.prerender`,
46
+ * router.ts) and the read side (rsc/handler.ts) both call this with
47
+ * `createRouter({ version })`, the only override the trigger can see, so they
48
+ * cannot disagree; a `createRSCHandler({ version })` does not move it.
49
+ */
50
+ export function resolvePrerenderVersion(
51
+ routerId: string | undefined,
52
+ routerVersion: string | undefined,
53
+ ): string {
54
+ return resolveRouterVersions(routerId, routerVersion).data;
55
+ }
56
+
57
+ /** The table gives some router a pair of its own. */
58
+ function hasRouterVersions(): boolean {
59
+ if (!table) return false;
60
+ const pairs = new Set(
61
+ Object.values(table).map(([data, document]) => `${data}\0${document}`),
62
+ );
63
+ return pairs.size > 1;
64
+ }
65
+
66
+ let warnedOutsideRequest = false;
67
+
68
+ /**
69
+ * The versions of the router serving the current request, for a cache store
70
+ * building a key. A store operation with no request context (a detached task
71
+ * that lost the ALS) gets the whole-build versions: it misses the request's
72
+ * entries instead of reading another version's. When routers have versions of
73
+ * their own that is a write no router reads back, so it is logged, once per
74
+ * process: `ctx.waitUntil()` re-enters the request context for every task it
75
+ * runs (server/request-context.ts), and this line in a production log means
76
+ * some other path lost it.
77
+ *
78
+ * Public from `@rangojs/router/cache`, for a custom persistent store to key
79
+ * the way the built-in ones do. Prefix cached RSC data (segment entries, items) with
80
+ * `data` and stored HTML or responses (shells, document responses) with
81
+ * `document`, and leave tag-invalidation records unversioned; an empty string
82
+ * means "no version" (tests). Call it per operation, inside the request, not
83
+ * in the store's constructor: the cache factory runs before the request
84
+ * context exists.
85
+ */
86
+ export function getCacheVersions(): RouterVersions {
87
+ const versions = _getRequestContext()?._versions;
88
+ if (versions) return versions;
89
+ if (!warnedOutsideRequest && hasRouterVersions()) {
90
+ warnedOutsideRequest = true;
91
+ console.warn(
92
+ "[rango] A cache key was built outside a request, so it carries the whole-build cache " +
93
+ "version instead of a router's. This build has per-router versions: an entry written " +
94
+ "under that key is not read by a router with its own version, and an entry that router " +
95
+ "wrote is not found from here. A store operation ran in a task that lost the request's " +
96
+ "async context. Logged once per process.",
97
+ );
98
+ }
99
+ return resolveRouterVersions(undefined);
100
+ }
101
+
102
+ /** Which of the serving router's versions a store key carries; `null`: none. */
103
+ export type KeyVersion = keyof RouterVersions | null;
104
+
105
+ /**
106
+ * The `v/{version}/` prefix of a persistent store key, or "" for an
107
+ * unversioned one. A store-level `version` is used for every versioned kind;
108
+ * otherwise the version is the serving router's, read per operation because
109
+ * the cache factory builds the store before the request context exists.
110
+ */
111
+ export function versionKeyPrefix(
112
+ storeVersion: string | undefined,
113
+ kind: KeyVersion,
114
+ ): string {
115
+ if (kind === null) return "";
116
+ const version = storeVersion ?? getCacheVersions()[kind];
117
+ return version ? `v/${version}/` : "";
118
+ }
119
+
120
+ /**
121
+ * @internal Install a table, or restore the build's with `undefined`. A
122
+ * request handler resolves its router's versions once, when it is created, so
123
+ * a test that swaps the table (a "deploy") needs new handlers.
124
+ */
125
+ export function installRouterVersionsTable(
126
+ next: RouterVersionsTable | undefined,
127
+ ): void {
128
+ table = next ?? ROUTER_VERSIONS;
129
+ warnedOutsideRequest = false;
130
+ }
@@ -15,6 +15,10 @@ import { invariant, DslContextError } from "../errors";
15
15
  import type { DefaultRouteName } from "../types/global-namespace.js";
16
16
  import type { ContextVar } from "../context-var.js";
17
17
  import { PPR_LANE_HINT } from "../rsc/shell-capture-constants.js";
18
+ import {
19
+ noteWarmIdentityRead,
20
+ type WarmContext,
21
+ } from "../prerender/warm-request.js";
18
22
  import {
19
23
  endIdentityExempt,
20
24
  isInsideCacheExecScope,
@@ -261,8 +265,13 @@ export type EntryData =
261
265
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
262
266
  prerenderDef?: {
263
267
  getParams?: (ctx: any) => Promise<any[]> | any[];
264
- options?: { concurrency?: number };
268
+ options?: {
269
+ concurrency?: number;
270
+ onDemand?: import("../prerender/on-demand.js").OnDemandOption;
271
+ };
265
272
  };
273
+ /** Set when the route opted into on-demand prerender (Prerender(..., { onDemand })) */
274
+ isOnDemand?: true;
266
275
  /** Set when route is wrapped with Passthrough() — has a separate live handler */
267
276
  isPassthrough?: true;
268
277
  /** Live handler for runtime fallback (only set on Passthrough routes) */
@@ -428,6 +437,7 @@ export const getContext = (): {
428
437
  getStore: () => HelperContext;
429
438
  getParent: () => EntryData | null;
430
439
  getOrCreateStore: (forRoute?: string) => HelperContext;
440
+ runIsolated: <T>(forRoute: string, callback: () => T) => T;
431
441
  getNextIndex: (
432
442
  type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
433
443
  ) => string;
@@ -447,26 +457,29 @@ export const getContext = (): {
447
457
  ) => T;
448
458
  } => {
449
459
  const context = RangoContext;
460
+ const createStore = (forRoute?: string): HelperContext => ({
461
+ manifest: new Map<string, EntryData>(),
462
+ namespace: "",
463
+ parent: null,
464
+ forRoute,
465
+ counters: {},
466
+ patterns: new Map<string, string>(),
467
+ patternsByPrefix: new Map<string, Map<string, string>>(),
468
+ trailingSlash: new Map<string, "never" | "always" | "ignore">(),
469
+ searchSchemas: new Map<string, Record<string, string>>(),
470
+ });
450
471
 
451
472
  return {
452
473
  context,
453
474
  getOrCreateStore: (forRoute?: string): HelperContext => {
454
475
  let store = RangoContext.getStore();
455
476
  if (!store) {
456
- store = {
457
- manifest: new Map<string, EntryData>(),
458
- namespace: "",
459
- parent: null,
460
- forRoute,
461
- counters: {},
462
- patterns: new Map<string, string>(),
463
- patternsByPrefix: new Map<string, Map<string, string>>(),
464
- trailingSlash: new Map<string, "never" | "always" | "ignore">(),
465
- searchSchemas: new Map<string, Record<string, string>>(),
466
- } satisfies HelperContext;
477
+ store = createStore(forRoute);
467
478
  }
468
479
  return store;
469
480
  },
481
+ runIsolated: <T>(forRoute: string, callback: () => T): T =>
482
+ context.run(createStore(forRoute), callback),
470
483
  getStore: (): HelperContext => {
471
484
  const store = context.getStore();
472
485
  if (!store) {
@@ -959,19 +972,20 @@ export interface IdentityReadWording {
959
972
  * nothing and are not captured reads: they take refuseInCacheScope alone.
960
973
  */
961
974
  export function guardIdentityRead(
962
- ctx: unknown,
975
+ ctx: WarmContext | null | undefined,
963
976
  surface: string,
964
977
  wording: IdentityReadWording,
965
978
  ): void {
966
979
  if (isInsideIdentityExempt()) return;
967
980
  const { verb, fix } = wording;
968
981
  if (tripShellCaptureGuard(ctx, surface, fix.warning)) {
982
+ noteWarmIdentityRead(ctx, surface);
969
983
  throw new Error(
970
984
  `${surface} cannot be ${verb} while capturing a shared shell ` +
971
985
  `(ppr shell capture). ${fix.capture}`,
972
986
  );
973
987
  }
974
- refuseInCacheScope(surface, wording);
988
+ refuseInCacheScope(surface, wording, ctx);
975
989
  if (wording.record !== false) recordLoaderIdentityRead(surface, verb);
976
990
  }
977
991
 
@@ -982,13 +996,16 @@ export function guardIdentityRead(
982
996
  export function refuseInCacheScope(
983
997
  surface: string,
984
998
  { verb, fix }: IdentityReadWording,
999
+ ctx?: WarmContext | null,
985
1000
  ): void {
986
1001
  if (isInsideCacheExecScope()) {
1002
+ noteWarmIdentityRead(ctx, surface);
987
1003
  throw new Error(
988
1004
  `${surface} cannot be ${verb} inside a "use cache" function. ${fix.useCache}`,
989
1005
  );
990
1006
  }
991
1007
  if (isInsideCacheScope()) {
1008
+ noteWarmIdentityRead(ctx, surface);
992
1009
  throw new Error(
993
1010
  `${surface} cannot be ${verb} inside a cache() boundary. ${fix.cacheScope}`,
994
1011
  );
@@ -1074,7 +1091,7 @@ const NON_CACHEABLE_READ: IdentityReadWording = {
1074
1091
  */
1075
1092
  export function assertNonCacheableReadAllowed(
1076
1093
  keyOrVar: string | ContextVar<unknown>,
1077
- requestCtx?: unknown,
1094
+ requestCtx?: WarmContext | null,
1078
1095
  ): void {
1079
1096
  const name = typeof keyOrVar === "string" ? ` "${keyOrVar}"` : "";
1080
1097
  guardIdentityRead(
@@ -1276,7 +1293,10 @@ export function isInsideAnyLoaderScope(): boolean {
1276
1293
 
1277
1294
  /**
1278
1295
  * The one ppr opt-in predicate: a page route entry that DECLARED `ppr`
1279
- * (`false` and undefined mean plain axis 1). Shared by the serve path
1296
+ * (`false`, undefined, and on-demand routes mean plain axis 1). A writable
1297
+ * prerender refresh cannot atomically replace the separate document shell, so
1298
+ * combining those lanes would pair a fresh tail with a stale prelude. Shared by
1299
+ * the serve path
1280
1300
  * (rsc/shell-serve.ts resolvePprConfig) and the header-write latch below so
1281
1301
  * the two layers can never drift on what counts as a ppr route.
1282
1302
  */
@@ -1285,7 +1305,10 @@ export function isPprEntry(entry: EntryData): entry is EntryData & {
1285
1305
  ppr: true | import("../urls/pattern-types.js").PartialPrerenderProps;
1286
1306
  } {
1287
1307
  return (
1288
- entry.type === "route" && entry.ppr !== undefined && entry.ppr !== false
1308
+ entry.type === "route" &&
1309
+ entry.isOnDemand !== true &&
1310
+ entry.ppr !== undefined &&
1311
+ entry.ppr !== false
1289
1312
  );
1290
1313
  }
1291
1314