@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
@@ -612,6 +612,10 @@ included. A handler's `ctx.use(Loader)` read never caches by itself (see
612
612
  body, gets the binding's value: one value per loader per request, a HIT
613
613
  included. A reader that runs before the binding starts (a layout's loader
614
614
  reading a route's cached loader) gets the value of its own run, as before.
615
+ This is by design: that loader runs live while the binding serves its entry,
616
+ so its data and its handle push can come from different runs on that request.
617
+ To keep them together, declare the cached loader first or read it from the
618
+ handler.
615
619
 
616
620
  The loader's data is cached independently from the route's segment cache,
617
621
  using the same `SegmentCacheStore` (app-level or per-loader override).
@@ -648,11 +652,21 @@ rejects) skips the whole entry, as above. Entries written
648
652
  before handles were recorded replay none until they are rewritten (expiry or
649
653
  SWR revalidation).
650
654
 
655
+ On a `ppr` route the shell keeps a copy of an `ssr: false` loader's settled
656
+ pushes too. The two never disagree on the page: where a replay of the shell
657
+ serves the loader's data from the shell, the shell's copy is the one shown
658
+ and the entry only adds the deferred push the shell could not keep; where it
659
+ does not (the loader runs or reads its entry), the entry's pushes take the
660
+ shell copy's place, none if the entry recorded none. See `/ppr` → "On a shell
661
+ HIT".
662
+
651
663
  ### Cache Key
652
664
 
653
- The default cache key is `loader:{loaderId}:{host}{pathname}:{sortedParams}`
654
- (the host keeps multi-tenant hosts from sharing entries). This can be customized
655
- at two levels:
665
+ The default cache key is
666
+ `loader:{loaderId}:{routerId}@{host}{pathname}:{sortedParams}`: the host keeps
667
+ multi-tenant hosts from sharing entries, and the router id keeps two routers
668
+ that mount the loader on one store apart (see `/host-router`, "Shared cache
669
+ store"). This can be customized at two levels:
656
670
 
657
671
  ```typescript
658
672
  import { cookies } from "@rangojs/router";
@@ -676,7 +690,7 @@ Resolution priority (same order as route-level `cache()`):
676
690
  `loader:{loaderId}:key:` plus the result's URI encoding
677
691
  2. `store.keyGenerator(ctx, defaultKey)` — store-level modification, stored
678
692
  as returned
679
- 3. Default key — `loader:{loaderId}:{host}{pathname}:{sortedParams}`
693
+ 3. Default key — `loader:{loaderId}:{routerId}@{host}{pathname}:{sortedParams}`
680
694
 
681
695
  Because the `key()` result is namespaced by the loader id, no value it
682
696
  returns, request input included, can name another loader's entry or a
@@ -993,7 +1007,13 @@ on document loads (`metadata.handlesLate`) or progressively on navigations.
993
1007
  A push before your slow fetch usually beats the barrier; a push derived from
994
1008
  the fetched data usually does not. When it MUST be in the document, use
995
1009
  `ssr: false` below. A loader with its own `cache()` keeps its pushes on a hit —
996
- they are replayed from the entry (see "Opting a Loader into Caching").
1010
+ they are replayed from the entry (see "Opting a Loader into Caching"). On a
1011
+ `ppr` route the race does not apply to a document served from its shell:
1012
+ there every push a loader makes on that request applies post-hydration (see
1013
+ `/ppr`, "Handles on a shell HIT"). A `useHandle` reader inside this loader's
1014
+ `loading()` or `<Suspense>` hydrates with the data its HTML was rendered
1015
+ from and shows a later push right after, so a push after an `await` causes
1016
+ no hydration mismatch there.
997
1017
 
998
1018
  Under `cache()`, loader pushes are not stored with the cached segments: the
999
1019
  loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
@@ -426,6 +426,8 @@ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
426
426
 
427
427
  Read the flash on the target page with `useLocationState(FlashMessage)`. The `{ flash: true }` option makes it auto-clear after first render. See `/hooks`.
428
428
 
429
+ A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again. The client follows at most 20 redirects in one navigation; a loop (two pages redirecting to each other) stops there with the error boundary and a `[rango] Server redirect loop` console error.
430
+
429
431
  ## Authentication Middleware
430
432
 
431
433
  ```typescript
@@ -416,13 +416,82 @@ default) or serves an open-ended param space. See `/prerender` for full API.
416
416
 
417
417
  Next.js route segment config maps onto Rango's explicit primitives:
418
418
 
419
- | Next.js segment config | Rango |
420
- | --------------------------------------------------- | ------------------------------------------------------------ |
421
- | `dynamic = "force-static"` + `generateStaticParams` | `Static()` / `Prerender()` (see `/prerender`) |
422
- | `revalidate = 60` (ISR) | `cache({ ttl: 60, swr: ... })` on the route (see `/caching`) |
423
- | `dynamic = "force-dynamic"` | the default — routes are dynamic unless you cache them |
424
- | `dynamicParams = true` | `Passthrough()` (above) |
425
- | `experimental_ppr = true` | the `ppr` path option (below, and `/ppr`) |
419
+ | Next.js segment config | Rango |
420
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
421
+ | `dynamic = "force-static"` + `generateStaticParams` | `Static()` / `Prerender()` (see `/prerender`) |
422
+ | `revalidate = 60` (ISR) | `cache({ ttl: 60, swr: ... })` on the route (see `/caching`), or on-demand prerender with `onDemand: { ttl: 60 }` (below) |
423
+ | `dynamic = "force-dynamic"` | the default — routes are dynamic unless you cache them |
424
+ | `dynamicParams = true` | `Passthrough()` (above) |
425
+ | `experimental_ppr = true` | the `ppr` path option (below, and `/ppr`) |
426
+
427
+ ### On-demand ISR → `Prerender(..., { onDemand })` + `router.prerender()`
428
+
429
+ A Next.js page with `generateStaticParams` refreshed by `revalidatePath()` or
430
+ `revalidateTag()` from a CMS webhook maps to an on-demand prerender route. The
431
+ route opts in, a store holds refreshed payloads, and the webhook re-renders the
432
+ page instead of invalidating it:
433
+
434
+ ```typescript
435
+ // Next.js:
436
+ export const revalidate = 3600;
437
+ export async function generateStaticParams() {
438
+ return (await getFeatured()).map((p) => ({ id: p.id }));
439
+ }
440
+ // app/api/revalidate/route.ts
441
+ revalidatePath(`/products/${id}`);
442
+ revalidateTag(`product:${id}`);
443
+
444
+ // Rango:
445
+ export const ProductPage = Prerender<{ id: string }>(
446
+ async () => (await getFeatured()).map((p) => ({ id: p.id })),
447
+ async (ctx) => <Product data={await getProduct(ctx.params.id)} />,
448
+ { onDemand: { ttl: 3600, tags: ({ params }) => [`product:${params.id}`] } },
449
+ );
450
+
451
+ // router: createRouter({ prerender: { store, ttl, onRevalidate? } })
452
+ // webhook (route handler, cron or queue):
453
+ const prerender = router.prerender({ env, ctx });
454
+ await prerender(`/products/${id}`); // revalidatePath
455
+ await prerender.markStale([`product:${id}`]); // revalidateTag
456
+ ```
457
+
458
+ | Next.js | Rango |
459
+ | ------------------------------------- | --------------------------------------------------------------------------------------------------- |
460
+ | `revalidatePath(path)` on an ISR page | `await router.prerender({ env, ctx })(path)`: renders now, serves the new payload next request |
461
+ | `revalidateTag(tag)` on ISR pages | `router.prerender({ env, ctx }).markStale([tag])`: marks only; a stale hit schedules `onRevalidate` |
462
+ | `revalidate = N` (time-based ISR) | `onDemand: { ttl: N }` + `onRevalidate`; `ttl` is soft, entries never expire |
463
+ | `dynamicParams = false` | plain on-demand `Prerender`: an unknown param 404s until something refreshes it |
464
+ | `dynamicParams = true` | wrap it in `Passthrough()`: unknown params render live until refreshed |
465
+
466
+ A page whose item was deleted. In the Pages Router, `getStaticProps` returns
467
+ `{ notFound: true }` on a revalidation: "With `notFound: true`, the page will
468
+ return a `404` even if there was a successfully generated page before. This is
469
+ meant to support use cases like user-generated content getting removed by its
470
+ author. Note, `notFound` follows the same `revalidate` behavior described
471
+ here." In Rango a `notFound()` anywhere in the on-demand render (the route
472
+ handler, a layout or slot handler, any server component) does the same on the
473
+ next `prerender(path)`: the result is `{ ok: true, status: "removed" }`, the
474
+ page answers 404 instead of the page or its build-time entry, and the removal
475
+ is rechecked per the route's `ttl`, as Next's is per `revalidate`. When the
476
+ data source may be failing, throw anything else: that keeps the page.
477
+ `prerender.remove(path)` removes without rendering, for a webhook that already
478
+ knows the item is gone, and is permanent until a `prerender(path)` renders the
479
+ page again. See `/prerender` → "Remove a page".
480
+
481
+ Three differences to plan for:
482
+
483
+ - Prerender tags are their own namespace. `updateTag()` / `revalidateTag()`
484
+ from `@rangojs/router` purge the runtime cache and never reach on-demand
485
+ entries; `markStale()` never reaches the runtime cache.
486
+ - A refresh is requestless: `cookies()` / `headers()` in the page make it
487
+ return `skipped-personalized`, and `ctx.env` is the live env you pass.
488
+ - `markStale()` does not re-render: without `onRevalidate` or a cron calling
489
+ `prerender(target, { onlyIfStale: true })` (the runner from
490
+ `router.prerender({ env, ctx })`), a marked page keeps
491
+ serving its old payload.
492
+
493
+ See `/prerender` → "On-demand refresh (ISR)" for the store options, the serve
494
+ table and the rules.
426
495
 
427
496
  ### Partial prerendering → the `ppr` path option
428
497
 
@@ -552,7 +621,9 @@ cookie (`rango-state-fresh`); other users, and other locations on
552
621
  propagates (`/caching` → "The fresh-reads cookie").
553
622
  Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`, `VercelCacheStore`)
554
623
  index by tag. Next's
555
- `revalidatePath` has no path-based equivalent — tag the relevant entries instead.
624
+ `revalidatePath` has no path-based equivalent for runtime-cached data — tag the
625
+ relevant entries instead. For a prerendered ISR page it is
626
+ `router.prerender({ env, ctx })(path)` (see "On-demand ISR" above).
556
627
 
557
628
  **2. Partial-render selection (which segments re-run after an action).** This is
558
629
  NOT cache invalidation — it is `revalidate()`, controlling which segments
@@ -594,7 +665,9 @@ When migrating:
594
665
 
595
666
  - `revalidateTag(tag)` → `await updateTag(tag)` (in a server action) or
596
667
  `revalidateTag(tag)` (in a route handler / webhook). Effectively 1:1.
597
- - `revalidatePath(path)` → no path-based equivalent; tag the entries on that
668
+ - `revalidatePath(path)` on an ISR page → `router.prerender({ env, ctx })(path)`
669
+ (on-demand prerender, §3); on runtime-cached data, no path-based
670
+ equivalent; tag the entries on that
598
671
  route (`cache({ tags })` / `cacheTag(...)`) and invalidate by tag.
599
672
  - To also force specific segments to re-render after the action (independent of
600
673
  cache busting), attach a `revalidate()` rule at those segment boundaries.
@@ -53,7 +53,8 @@ implements the shell family (`getShell`/`putShell`): `MemorySegmentCacheStore`
53
53
  `VercelCacheStore` (Vercel Runtime Cache). A ppr route on a store without the
54
54
  family renders normally (no shell) with a once-per-key warning. A custom store
55
55
  returns each `ShellCacheEntry` field as `putShell` received it, including
56
- `buildVersion` and `snapshot` (both required; the snapshot can be empty).
56
+ `buildVersion` (the router's document version) and `snapshot` (both required;
57
+ the snapshot can be empty).
57
58
 
58
59
  ```typescript
59
60
  import { createRouter, urls } from "@rangojs/router";
@@ -169,7 +170,7 @@ The partition follows nested scopes (issue #970): a `ppr` route under a
169
170
  inner boundary has none, and by both composed (outermost first, joined by
170
171
  `|`) when it has its own. Each `key()` result is namespaced as `key:` plus
171
172
  its URI encoding (issue #975), exactly as in the record key, so the shell of
172
- `key: () => "tier:gold"` is `<host><path>:shell|key%3Atier%253Agold`. The
173
+ `key: () => "tier:gold"` is `<router>@<host><path>:shell|key%3Atier%253Agold`. The
173
174
  route's record adds the inner boundary's own default key when it has no
174
175
  `key`; the shell partition does not, since the shell key already carries the
175
176
  URL (a store `keyGenerator`'s result is kept when it adds anything, an
@@ -219,10 +220,10 @@ does not render from: otherwise the first variant captured is served for every
219
220
  value of it.
220
221
 
221
222
  In tests, `serveShellRequest` reports the key the serve path resolved
222
- (`result.key`, partition included); `shellCacheKey(url, searchParams,
223
+ (`result.key`, partition included); `shellCacheKey(router, url, searchParams,
223
224
  partition)` builds the same key from the `key()` result, namespacing it as
224
225
  production does. Under nested keyed scopes, pass the `key()` results as an
225
- array, outermost first (`shellCacheKey(url, undefined, ["tier:gold",
226
+ array, outermost first (`shellCacheKey(router, url, undefined, ["tier:gold",
226
227
  "v:a"])`); for a store `keyGenerator` partition pass
227
228
  `{ keys, generated }` (`/testing`, `cache-prerender.md`).
228
229
 
@@ -250,7 +251,9 @@ under `cache()` and under the shell alike.
250
251
 
251
252
  Invalidation crosses layers: `updateTag()`/`revalidateTag()` reach segment,
252
253
  shell, loader, and item entries in the same store, and shell entries
253
- additionally self-invalidate on `React.version` change.
254
+ additionally self-invalidate on `React.version` change and when the router's
255
+ document version changes (server code, SSR output, client asset names, `base`
256
+ or Prerender payloads changed), not on every deploy.
254
257
 
255
258
  ## The serve pipeline: commit after ALL middleware
256
259
 
@@ -261,7 +264,7 @@ On a document GET to a ppr route the router runs:
261
264
  DSL `middleware()`; both are guards, and the COMMIT POINT is after all of
262
265
  them: any rejection/redirect/401 wins before a single shell byte, on MISS
263
266
  and on a warmed HIT alike;
264
- 3. **shell lookup** — `getShell(key)` on the app store (key =
267
+ 3. **shell lookup** — `getShell(key)` on the app store (key = router id +
265
268
  host+pathname+sorted search). `CFCacheStore` stores the entry prelude-first
266
269
  and the serve path reads only its head and prelude here, with the tag-marker
267
270
  read running alongside; the capture snapshot is read off the commit path.
@@ -383,10 +386,12 @@ encountered by the shell capture.
383
386
  Two more decisions are made before any shell-store read, so probes and
384
387
  prerendered routes never spend passive `getShell` I/O:
385
388
 
386
- - A partial request carrying neither `X-RSC-Router-Client-Path` nor `Referer`
387
- (a curl probe, a synthetic monitor) can never produce a partial match; it
388
- reports `BYPASS; reason=no-navigation-context`. Alert on replay hit-rate
389
- with this in mind — such probes are not cache misses.
389
+ - A partial request carrying neither `X-RSC-Router-Client-Path` nor a
390
+ same-origin `Referer` (a curl probe, a synthetic monitor) can never produce
391
+ a partial match; it reports `BYPASS; reason=no-navigation-context`. A
392
+ `Referer` on another origin counts as absent: only its pathname would be
393
+ matched, as if it were a route of this app. Alert on replay hit-rate with
394
+ this in mind — such probes are not cache misses.
390
395
  - A `Prerender()` route's partial is served from the build-time prerender
391
396
  store inside matching (a better-than-HIT outcome); it reports
392
397
  `BYPASS; reason=prerender-store`. Its captures never record the page's
@@ -436,7 +441,7 @@ The bounded bypass tokens, grouped by when they are decided:
436
441
  | Token | Decided | Meaning |
437
442
  | ----------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
438
443
  | `method`, `dynamic`, `nonce`, `store-unavailable`, `passive-read-unsupported` | pre-read | request/route/store ineligible for replay |
439
- | `no-navigation-context` | pre-read | no `X-RSC-Router-Client-Path`/`Referer`; a partial match is impossible |
444
+ | `no-navigation-context` | pre-read | no `X-RSC-Router-Client-Path` and no same-origin `Referer`; a partial match is impossible |
440
445
  | `prerender-store` | pre-read or match | `Prerender()` route served by its baked artifact (pre-read probe of the normal variant; reclassified post-match when the store actually served, either variant) |
441
446
  | `intercept` | match | the navigation resolved to an intercept — replay is never armed for intercepts (they keep their normal cache path); no heal capture |
442
447
  | `cache-disabled` | pre-read or match | `cache(false)` (pre-read, static) or `condition()` false (decided at the lookup); consumer opt-out is absolute |
@@ -556,6 +561,19 @@ curl -s -D - -o /dev/null https://app.example.com/products/1 | grep -i x-rango-s
556
561
  writes, and marker rejection, with `cf-ray`/colo and read timings. The flag is baked at
557
562
  build time; setting only a Worker runtime variable is too late.
558
563
 
564
+ ### Capturing a shell before the first visitor
565
+
566
+ A shell is captured by the first document MISS for its key, so the first
567
+ visitor after a deploy or an `updateTag()` renders without one. To capture it
568
+ ahead of them, warm the URL: `router.prerender({ env, ctx })(url)` renders it
569
+ through the handler, schedules the capture and waits for it, so the next
570
+ document request is a HIT. A warm replaces a stored shell in place (the old
571
+ one serves until the new one is written) and reports the capture's outcome
572
+ (`result.caches.shell`, with `refusal` when the capture refused). It is not
573
+ subject to the per-key in-flight dedup or the refused-capture backoff, which
574
+ exist to stop a herd of visitors repeating work; every correctness guard on
575
+ this page still decides. See `/prerender` → "Warm any route before traffic".
576
+
559
577
  ### Unit / integration testing (public primitives)
560
578
 
561
579
  Import from `@rangojs/router/testing` (Vitest) or `@rangojs/router/testing/e2e`
@@ -566,17 +584,18 @@ Import from `@rangojs/router/testing` (Vitest) or `@rangojs/router/testing/e2e`
566
584
  | `assertShellStatus(res, "HIT" \| "MISS")` | Document Response from a real RSC serve / e2e `page.request` |
567
585
  | `assertPprReplayStatus(res, expected)` | Partial response fresh/stale replay or bounded bypass |
568
586
  | `parsePprReplayStatus(res)` | Read structured replay/bypass status or null |
569
- | `shellCacheKey(url)` | Production store key for `store.getShell` / custom stores |
587
+ | `shellCacheKey(router, url)` | Production store key for `store.getShell` / custom stores |
570
588
  | `MemorySegmentCacheStore` + `getShell`/`putShell` | Custom store contract / tag eviction (no faked HIT) |
571
589
  | `serveShellRequest(router, url, opts)` | A real MISS → capture → HIT (react-server Vitest) |
572
590
 
573
591
  ```ts
574
592
  import { MemorySegmentCacheStore } from "@rangojs/router/cache";
575
593
  import { assertShellStatus, shellCacheKey } from "@rangojs/router/testing";
594
+ import { router } from "./router";
576
595
 
577
596
  // Unit: store + key (after a real putShell / capture flush in e2e)
578
597
  const store = new MemorySegmentCacheStore();
579
- const key = shellCacheKey("http://localhost/products/1");
598
+ const key = shellCacheKey(router, "http://localhost/products/1");
580
599
  // ... after production putShell ...
581
600
  expect(await store.getShell(key)).not.toBeNull();
582
601
 
@@ -783,6 +802,9 @@ identifiers — served to anonymous visitors.)
783
802
  - `ctx.use(H)(promise)` from a handler — a TOP-LEVEL pushed promise is awaited
784
803
  at capture and BAKED into the shell (bounded by `ppr.captureTimeout`, 15s by
785
804
  default).
805
+ - A promise pushed from a LOADER is never in the shell and the capture does not
806
+ wait for it: however slow it is (or if it never settles), the shell is still
807
+ stored, and the HIT's own loader run delivers the value after hydration.
786
808
  - `ctx.use(H)({ x: promise })` from a HANDLER — also baked. The capture waits
787
809
  for every promise nested in a handler's push, at any depth of plain objects,
788
810
  arrays and JSX props, before it records the handles every HIT replays. Keep
@@ -896,31 +918,109 @@ value once it refreshes, while the baked container keeps the capture's. If a
896
918
  hole needs a stable value, give it its own cache (`"use cache"` with a
897
919
  profile, or a loader `cache()`); the shell never provides one.
898
920
 
899
- **Handle pushes from a bake-lane loader appear once.** The capture records
900
- the settled, thenable-free handle pushes of every loader body it runs (the
921
+ **Handle pushes from a bake-lane loader appear once, and they match its
922
+ data.** A loader's title, meta or breadcrumb describes the data next to it,
923
+ so both come from the same run on every way a shell can be replayed: a
924
+ document HIT, a client navigation, a prefetch. The capture records the
925
+ settled, thenable-free handle pushes of every loader body it runs (the
901
926
  prelude rendered them): the `ssr: false` loader's own, those of the loaders
902
- it awaits with `ctx.use()`, and those its own `cache()` entry replays. Every
903
- HIT restores them with the handler layer, and they stand: a bake-lane loader
904
- that does run on the HIT (a hole-carrying one) reads the store, so its settled
905
- pushes, and those a `"use cache"` hit inside its body replays, are dropped
906
- instead of replacing the restored ones. Any handle, deduping or not, shows
907
- each value once, and it is the value the prelude rendered. A loader the route
908
- also registers on the live lane is a hole: its live run replaces its restored
909
- pushes, and those of the loaders it awaits, so they show its live value, even
910
- when a bake-lane loader running on the HIT awaits it. A run that throws or
911
- makes no push shows none of the capture's once its run ends; a hole slower
912
- than the handler barrier shows the shell's copy in the first snapshot until
913
- then. A hole with its own loader `cache()` that hits shows the pushes that
914
- entry recorded, none if it recorded none, matching its data. A dependency the route does not
915
- register, which an `ssr: false` loader awaited at capture outside any live
916
- loader, is on neither lane: its settled pushes stay as the prelude rendered
917
- them, even where a live loader also awaits it, while its data is fresh. To
918
- keep its pushes live, declare it as its own `loader()` on the route. A push the capture cannot record (a
919
- deferred push, or one holding a promise) marks every loader record of the
920
- page to run: those bodies then still run on each HIT, in the background, and
921
- that push reaches the page (from the loader's own `cache()` entry when it
922
- hits) with the run's value; a push that lands after the document's handle snapshot
923
- reaches the client after hydration.
927
+ it awaits with `ctx.use()`, and those its own `cache()` entry replays. What
928
+ a replay does with a loader's recorded pushes follows what it does with that
929
+ loader's data:
930
+
931
+ | The replay serves the loader's data from | Its settled pushes are |
932
+ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
933
+ | the shell (its pinned container) | the shell's: restored with the handler layer, and they stand. A loader that does run on the replay (a hole-carrying one) reads the store, and the settled pushes that run makes are dropped |
934
+ | a run on this request (a live-lane loader, or one the shell has no pin for) | that run's: they replace the shell's copies in place; a run that throws or makes no push shows none |
935
+ | the loader's own `cache()` entry | that entry's: the pushes it recorded, none if it recorded none |
936
+
937
+ So any handle, deduping or not, shows each value once. A pinned loader's
938
+ pushes are the values the prelude rendered, also for the pushes a `"use
939
+ cache"` hit inside its body replays. A loader the route also registers on
940
+ the live lane is a hole: its live run replaces its restored pushes, and those
941
+ of the loaders it awaits, so they show its live value, even when a bake-lane
942
+ loader running on the replay awaits it. On a document HIT the page hydrates
943
+ with the shell's copy and the hole's value takes its place after hydration
944
+ ("Handles on a shell HIT" below). An
945
+ `ssr: false` loader is served from the shell only while the entry holds its
946
+ pin: a snapshot captured by a navigation alone carries none, and a shell over
947
+ `ppr.maxSnapshotBytes` drops them. There the loader runs fresh, and its
948
+ pushes are that run's, like its data.
949
+
950
+ A `Prerender` + `ppr` page follows the same table. Its handler layer comes
951
+ from the prerendered payload, which never holds a loader push (loaders do not
952
+ run at build), so the shell entry records the loader pushes its HTML rendered
953
+ on its own, and a HIT restores them as above (#1057).
954
+
955
+ A dependency the route does not register, which an `ssr: false` loader
956
+ awaited at capture outside any live loader, is on neither lane: its settled
957
+ pushes stay as the prelude rendered them, even where a live loader also
958
+ awaits it, while its data is fresh. (They follow the run instead once the
959
+ entry has lost a pin, because the loader that awaits it then runs.) To keep
960
+ its pushes live, declare it as its own `loader()` on the route. A push the
961
+ capture cannot record (a deferred push, or one holding a promise) marks every
962
+ loader record of the page to run: those bodies then still run on each replay,
963
+ in the background, and that push reaches the page (from the loader's own
964
+ `cache()` entry when it hits) with the run's value, on a document HIT and on
965
+ a client navigation alike. On a document HIT it arrives after hydration.
966
+
967
+ **Handles on a shell HIT.** A promise pushed from a loader is live and is
968
+ never in a shell. To have a value in the shell's HTML, push it settled
969
+ (await it) from the `ssr: false` loader, or push it from the handler.
970
+
971
+ Three things follow, in this order of scope:
972
+
973
+ 1. **What is in a shell's HTML** changes only for promise pushes of
974
+ `ssr: false` loaders and of the loaders they await: `push(promise)`, or a
975
+ pushed value that holds a promise. Those run at capture, and the shell
976
+ renders no element for such a push. A loader without `ssr: false` never
977
+ ran at capture, so nothing of it was ever in the shell.
978
+ 2. **On a shell HIT every loader push made during the request arrives after
979
+ hydration**, a live-lane loader's included. The document hydrates with
980
+ exactly the handle data the shell's HTML was rendered from (the handlers'
981
+ pushes and the settled pushes of the loaders that ran at capture), and
982
+ the request's own pushes follow as a normal `useHandle` update.
983
+ 3. **`useHandle` on every document, `ppr` or not:** a reader that React is
984
+ hydrating reads the handle data its HTML was rendered from, wherever its
985
+ boundary hydrates, and takes the newer data right after. So a reader
986
+ inside the `loading()` or `<Suspense>` of a loader that pushes after an
987
+ `await` hydrates clean.
988
+
989
+ | On the HIT's request | In the HIT's HTML | The client |
990
+ | --------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------- |
991
+ | a handler's push, a pinned `ssr: false` loader's settled push | yes (the shell has it) | hydrates with it |
992
+ | a deferred push (`push(promise)`), or a push holding a promise, by a loader | no | gets it right after hydration |
993
+ | a live-lane loader's push | no | gets it right after hydration |
994
+ | the run of an `ssr: false` loader the shell has no pin for | the capture's push | hydrates with the capture's, then shows the run's |
995
+
996
+ A value a loader pushes as a promise is therefore in the HTML of a document
997
+ MISS (the render awaits it) and of a route without `ppr`, and not in the
998
+ HTML of a shell HIT. When a crawler or the first paint needs it, change one
999
+ line:
1000
+
1001
+ ```ts
1002
+ ctx.use(Notes)(db.stockNote(id)); // a promise: live, after hydration on a HIT
1003
+ ctx.use(Notes)(await db.stockNote(id)); // settled: baked into the shell
1004
+ ```
1005
+
1006
+ The settled push bakes with the shell and is invalidated with it (its TTL,
1007
+ its tags). A promise is not baked, whenever it settles: it is the shape that
1008
+ says "per request", the same as a promise in the loader's data.
1009
+
1010
+ A pinned loader's pushes are exactly the ones its capture made. If its body
1011
+ pushes a settled value only on some runs (behind a condition), a replay that
1012
+ runs it shows the capture's answer, a push or none, never a later run's next
1013
+ to the pinned data. A deferred push is the exception by design: it is
1014
+ delivered on every replay.
1015
+
1016
+ One arrangement mixes sources by design. A loader that reads a
1017
+ `cache()`-bound loader with `ctx.use()` before that binding starts (a loader
1018
+ declared ahead of it, a parent layout) runs it live while the binding serves
1019
+ its entry, so the cached loader's data and its handle push can come from
1020
+ different runs on that request. To keep them together, declare the cached
1021
+ loader first, or read it from the handler: a handler's `ctx.use()` is where a
1022
+ loader read is baked. An entry such a request writes holds no pushes, and a
1023
+ later HIT of it then shows none for that loader.
924
1024
 
925
1025
  Four hard edges (each e2e/unit-pinned):
926
1026
 
@@ -1187,8 +1287,9 @@ eligible captured segment snapshot, per-request CSP nonce, `streamMode:
1187
1287
  request the route's own `cache()` scope refuses (`cache(false)`, or a
1188
1288
  `condition()` returning false for it), and any store without the shell family.
1189
1289
  A stored shell is invalidated when
1190
- `React.version` changes (postponed state is build-coupled), so deploys
1191
- self-heal via recapture.
1290
+ `React.version` changes (postponed state is build-coupled) or when the router's
1291
+ document version changes, so a deploy that changes the router self-heals via
1292
+ recapture, and a deploy that does not keeps its shells.
1192
1293
 
1193
1294
  The per-request CSP nonce guarantee covers BOTH ways a nonce arrives — the
1194
1295
  `createRouter({ nonce })` provider AND a direct `ctx.set(nonce, value)` token
@@ -1234,13 +1335,13 @@ path(
1234
1335
  );
1235
1336
  ```
1236
1337
 
1237
- | Field | Default | Notes |
1238
- | ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1239
- | `ttl` | `300` | shell freshness window in seconds (`ppr: true` uses the default) |
1240
- | `swr` | — | stale window: serve the stale shell + background recapture |
1241
- | `tags` | — | operational tags UNIONED with the tags the capture collects — see "Invalidation" below |
1242
- | `maxSnapshotBytes` | 8 MiB | cap on the loader pins in the entry's snapshot (the bake-lane loader records). The recorded handler layer is exempt. Over the cap the pins are dropped and the shell is stored with its handler layer (warned once per key); bake-lane loaders then read the live store, and drift is repaired client-side |
1243
- | `captureTimeout` | 15000ms | ONE capture deadline: the handler layer settling (promises it passes or pushes, async server components, loaders it awaits), bake-lane loaders, and the prerender; a capture that misses it stores nothing rather than a partial shell |
1338
+ | Field | Default | Notes |
1339
+ | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1340
+ | `ttl` | `300` | shell freshness window in seconds (`ppr: true` uses the default) |
1341
+ | `swr` | — | stale window: serve the stale shell + background recapture |
1342
+ | `tags` | — | operational tags UNIONED with the tags the capture collects — see "Invalidation" below |
1343
+ | `maxSnapshotBytes` | 8 MiB | cap on the loader pins in the entry's snapshot (the bake-lane loader records). The recorded handler layer is exempt. Over the cap the pins are dropped and the shell is stored with its handler layer (warned once per key); bake-lane loaders then run on every replay and read the live store, their handle pushes come from that run like their data, and drift from the prelude is repaired client-side |
1344
+ | `captureTimeout` | 15000ms | ONE capture deadline: the handler layer settling (promises it passes or pushes, async server components, loaders it awaits), bake-lane loaders, and the prerender; a capture that misses it stores nothing rather than a partial shell |
1244
1345
 
1245
1346
  A shell never outlives the route `cache()` entry it was captured from: under
1246
1347
  a route `cache()`, the shell is fresh no longer than that entry is (at most
@@ -1268,8 +1369,9 @@ shrink what the shell bakes or move large regions under a live loader's
1268
1369
  boundary.
1269
1370
 
1270
1371
  The shell store is always the app-level `createRouter({ cache })` store; the
1271
- default key is `${host}${pathname}${sortedSearch}:shell` (host-scoped so
1272
- multi-tenant shells never collide).
1372
+ default key is `${routerId}@${host}${pathname}${sortedSearch}:shell`: host-scoped
1373
+ so multi-tenant shells never collide, and router-scoped so two routers on one
1374
+ store keep their own (see `/host-router`, "Shared cache store").
1273
1375
 
1274
1376
  These options control the in-function shell entry only. They do not emit HTTP
1275
1377
  `Cache-Control`. Adding `s-maxage` separately allows a platform CDN to cache the