@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -82,13 +82,13 @@
82
82
  * ISOLATION FROM RESPONSE
83
83
  * =======================
84
84
  *
85
- * Background revalidation creates fully isolated context:
86
- * - Fresh handleStore (prevents polluting the response stream)
85
+ * Background revalidation re-renders through rerenderAndCacheRoute (shared
86
+ * with proactive caching in cache-store.ts):
87
+ * - Derived request context with its own handleStore, transition({ when })
88
+ * predicates and no perf metrics (the shared fields are never touched;
89
+ * the foreground is still producing the page)
87
90
  * - Fresh handlerContext + loaderPromises (prevents reusing memoized
88
91
  * loader results from the foreground pass)
89
- * - handleStore is saved/restored in try/finally
90
- *
91
- * This matches the proactive caching pattern in cache-store.ts.
92
92
  *
93
93
  *
94
94
  * FRESH RESOLUTION (NO REVALIDATION)
@@ -101,15 +101,163 @@
101
101
  */
102
102
  import type { ResolvedSegment } from "../../types.js";
103
103
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
104
- import { getRouterContext } from "../router-context.js";
104
+ import { getRouterContext, type RouterContext } from "../router-context.js";
105
+ import type { CacheScope } from "../../cache/cache-scope.js";
105
106
  import type { GeneratorMiddleware } from "./cache-lookup.js";
106
107
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
107
108
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
109
+ import { getContext } from "../../server/context.js";
108
110
  import {
111
+ createRequestContext,
109
112
  runWithRequestContext,
110
113
  type RequestContext,
111
114
  } from "../../server/request-context.js";
112
115
 
116
+ /**
117
+ * Re-render a cached route in the background and write it with
118
+ * cacheScope.cacheRoute: the stale-hit refresh below and proactive caching
119
+ * (cache-store.ts). Returns the number of segments written.
120
+ *
121
+ * A render whose status is not 200 (an error or notFound boundary resolved)
122
+ * is not written and returns 0, as a non-200 MISS is not cached
123
+ * (cache-store.ts): the entry it would replace keeps serving until a later
124
+ * refresh succeeds or it expires. Nothing is held in process; a store's own
125
+ * revalidation marker (CFCacheStore's REVALIDATING) re-arms after
126
+ * MAX_REVALIDATION_INTERVAL, as after a refresh that throws.
127
+ *
128
+ * Runs on a DERIVED request context that owns the per-render state the
129
+ * foreground reads while it is still producing the page (a stale HIT re-runs
130
+ * its loaders; a proactive render starts once the Response exists, while its
131
+ * body streams):
132
+ * - _handleStore: read late (loader pushes and aux-lane tracking in
133
+ * loader-resolution.ts, nested cache restores and captures in
134
+ * cache-scope.ts), so a swap sent live pushes into this render.
135
+ * setupLoaderAccess binds the store at setup, so it runs inside the
136
+ * derived context too.
137
+ * - _transitionWhen: fresh resolution appends transition({ when })
138
+ * predicates; the foreground's gateTransitions reads the array after the
139
+ * match, and a stale HIT (which replays the stored transition) must not
140
+ * evaluate this render's predicates.
141
+ * - _metricsStore: undefined, and the DSL store below has `metrics` unset
142
+ * (track() reads that one), so nothing lands on the foreground's perf
143
+ * timeline. ctx.Store.run closes over ctx.Store, so the derived store
144
+ * needs its own run.
145
+ * - response writes (headers, cookies, status, onResponse callbacks) go to
146
+ * a throwaway context (whose status gates the write, above) and are
147
+ * dropped: a layout above the cache() boundary is outside the header
148
+ * guard and an error boundary sets a status, so they would otherwise
149
+ * reach the live response.
150
+ * runWithRequestContext also re-establishes the request ALS, which a waitUntil
151
+ * task on workerd loses; the DSL store is a different ALS (build context).
152
+ */
153
+ export async function rerenderAndCacheRoute<TEnv>(
154
+ ctx: MatchContext<TEnv>,
155
+ requestCtx: RequestContext<TEnv>,
156
+ cacheScope: CacheScope,
157
+ routerCtx: RouterContext<TEnv>,
158
+ ): Promise<number> {
159
+ const handleStore = routerCtx.createHandleStore();
160
+ // Response writes land in a throwaway context nothing merges or drains. Its
161
+ // mutators are closures over its own stub, and they keep the same guards.
162
+ const sink = createRequestContext({
163
+ env: ctx.env,
164
+ request: ctx.request,
165
+ url: ctx.url,
166
+ variables: {},
167
+ themeConfig: requestCtx._themeConfig,
168
+ });
169
+ const renderCtx: RequestContext<TEnv> = Object.assign(
170
+ Object.create(requestCtx, {
171
+ res: Object.getOwnPropertyDescriptor(sink, "res")!,
172
+ }),
173
+ {
174
+ _handleStore: handleStore,
175
+ _transitionWhen: [],
176
+ _metricsStore: undefined,
177
+ _onResponseCallbacks: [],
178
+ header: sink.header,
179
+ setCookie: sink.setCookie,
180
+ deleteCookie: sink.deleteCookie,
181
+ setStatus: sink.setStatus,
182
+ _setStatus: sink._setStatus,
183
+ setTheme: sink.setTheme,
184
+ _rotateStateCookie: sink._rotateStateCookie,
185
+ _setKeepCacheDirective: sink._setKeepCacheDirective,
186
+ },
187
+ );
188
+ const store = Object.assign(Object.create(ctx.Store), { metrics: undefined });
189
+ const runInStore = <T>(fn: () => T): T =>
190
+ getContext().runWithStore(
191
+ store,
192
+ store.namespace || "#router",
193
+ store.parent,
194
+ fn,
195
+ );
196
+ return runWithRequestContext(renderCtx, async () => {
197
+ const handlerContext = routerCtx.createHandlerContext(
198
+ ctx.matched.params,
199
+ ctx.request,
200
+ ctx.url.searchParams,
201
+ ctx.pathname,
202
+ ctx.url,
203
+ ctx.env,
204
+ ctx.routeMap,
205
+ ctx.matched.routeKey,
206
+ ctx.matched.responseType,
207
+ ctx.matched.pt === true,
208
+ );
209
+ const loaderPromises = new Map<string, Promise<any>>();
210
+ routerCtx.setupLoaderAccess(handlerContext, loaderPromises);
211
+
212
+ const segments = await runInStore(() =>
213
+ routerCtx.resolveAllSegments(
214
+ ctx.entries,
215
+ ctx.routeKey,
216
+ ctx.matched.params,
217
+ handlerContext,
218
+ loaderPromises,
219
+ { skipLoaders: true },
220
+ ),
221
+ );
222
+ if (ctx.interceptResult) {
223
+ segments.push(
224
+ ...(await runInStore(() =>
225
+ routerCtx.resolveInterceptEntry(
226
+ ctx.interceptResult!.intercept,
227
+ ctx.interceptResult!.entry,
228
+ ctx.matched.params,
229
+ handlerContext,
230
+ true, // belongsToRoute
231
+ undefined, // no revalidationContext: render fresh
232
+ // Skip intercept middleware: the foreground already ran it.
233
+ // Re-running it here would double its side effects, and a
234
+ // short-circuit Response would abort the write.
235
+ { skipMiddleware: true },
236
+ ),
237
+ )),
238
+ );
239
+ }
240
+
241
+ handleStore.seal();
242
+ // Boundaries set 500/404 through _setStatus (catchSegmentError), which
243
+ // lands on the sink.
244
+ if (sink.res.status !== 200) {
245
+ debugLog("backgroundRevalidation", "skipping cache for non-200 render", {
246
+ status: sink.res.status,
247
+ pathname: ctx.pathname,
248
+ });
249
+ return 0;
250
+ }
251
+ await cacheScope.cacheRoute(
252
+ ctx.pathname,
253
+ ctx.matched.params,
254
+ segments,
255
+ ctx.isIntercept,
256
+ );
257
+ return segments.length;
258
+ });
259
+ }
260
+
113
261
  /**
114
262
  * Creates background revalidation middleware
115
263
  *
@@ -137,111 +285,31 @@ export function withBackgroundRevalidation<TEnv>(
137
285
  return;
138
286
  }
139
287
 
140
- const {
141
- getRequestContext,
142
- createHandleStore,
143
- createHandlerContext,
144
- setupLoaderAccess,
145
- resolveAllSegments,
146
- resolveInterceptEntry,
147
- } = getRouterContext<TEnv>();
148
-
149
- const requestCtx = getRequestContext();
288
+ const routerCtx = getRouterContext<TEnv>();
289
+ const requestCtx = routerCtx.getRequestContext();
150
290
  const cacheScope = ctx.cacheScope;
151
291
  const reqId = INTERNAL_RANGO_DEBUG
152
292
  ? getOrCreateRequestId(ctx.request)
153
293
  : undefined;
154
294
 
155
295
  requestCtx?.waitUntil(async () => {
156
- // Prevent background metrics from polluting foreground timeline.
157
- // The foreground uses its own metricsStore reference directly (via
158
- // appendMetric), so nulling Store.metrics only affects track() calls
159
- // inside this background Store.run() scope.
160
- const savedMetrics = ctx.Store.metrics;
161
- ctx.Store.metrics = undefined;
162
-
163
296
  const start = performance.now();
164
297
  debugLog("backgroundRevalidation", "revalidating stale route", {
165
298
  pathname: ctx.pathname,
166
299
  fullMatch: ctx.isFullMatch,
167
300
  });
168
301
 
169
- // Save and replace handleStore to avoid polluting the response stream.
170
- // Restore in finally (same pattern as proactive caching in cache-store).
171
- const originalHandleStore = requestCtx._handleStore;
172
- requestCtx._handleStore = createHandleStore();
173
-
174
302
  try {
175
- const freshHandlerContext = createHandlerContext(
176
- ctx.matched.params,
177
- ctx.request,
178
- ctx.url.searchParams,
179
- ctx.pathname,
180
- ctx.url,
181
- ctx.env,
182
- ctx.routeMap,
183
- ctx.matched.routeKey,
184
- ctx.matched.responseType,
185
- ctx.matched.pt === true,
186
- );
187
- const freshLoaderPromises = new Map<string, Promise<any>>();
188
- setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
189
-
190
- // Re-establish the request-context ALS around the re-render. ctx.Store
191
- // is a different ALS (DSL build context); on workerd a waitUntil task
192
- // runs detached from the request's I/O context, so a handler/component
193
- // that reads the ambient getRequestContext() during this background
194
- // re-render would otherwise throw "called outside of a request context".
195
- const freshSegments = await runWithRequestContext(
303
+ const count = await rerenderAndCacheRoute(
304
+ ctx,
196
305
  requestCtx as RequestContext<TEnv>,
197
- () =>
198
- ctx.Store.run(() =>
199
- resolveAllSegments(
200
- ctx.entries,
201
- ctx.routeKey,
202
- ctx.matched.params,
203
- freshHandlerContext,
204
- freshLoaderPromises,
205
- { skipLoaders: true },
206
- ),
207
- ),
208
- );
209
-
210
- let freshInterceptSegments: ResolvedSegment[] = [];
211
- if (ctx.interceptResult) {
212
- freshInterceptSegments = await runWithRequestContext(
213
- requestCtx as RequestContext<TEnv>,
214
- () =>
215
- ctx.Store.run(() =>
216
- resolveInterceptEntry(
217
- ctx.interceptResult!.intercept,
218
- ctx.interceptResult!.entry,
219
- ctx.matched.params,
220
- freshHandlerContext,
221
- true,
222
- undefined,
223
- // Skip intercept middleware: this is a post-response background
224
- // re-render to refresh a stale cached route. The foreground
225
- // already ran the middleware; re-running it would double its
226
- // side effects and a short-circuit Response would abort the write.
227
- { skipMiddleware: true },
228
- ),
229
- ),
230
- );
231
- }
232
-
233
- const completeSegments = [...freshSegments, ...freshInterceptSegments];
234
- requestCtx._handleStore.seal();
235
- await cacheScope.cacheRoute(
236
- ctx.pathname,
237
- ctx.matched.params,
238
- completeSegments,
239
- ctx.isIntercept,
306
+ cacheScope,
307
+ routerCtx,
240
308
  );
241
309
  if (INTERNAL_RANGO_DEBUG) {
242
310
  const dur = performance.now() - start;
243
311
  console.log(
244
- `[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
312
+ `[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${count}`,
245
313
  );
246
314
  }
247
315
  debugLog("backgroundRevalidation", "revalidation complete", {
@@ -258,9 +326,6 @@ export function withBackgroundRevalidation<TEnv>(
258
326
  pathname: ctx.pathname,
259
327
  error: String(error),
260
328
  });
261
- } finally {
262
- requestCtx._handleStore = originalHandleStore;
263
- ctx.Store.metrics = savedMetrics;
264
329
  }
265
330
  });
266
331
  };
@@ -91,7 +91,7 @@
91
91
  * - Request object
92
92
  * - Action context (if POST)
93
93
  */
94
- import type { ResolvedSegment } from "../../types.js";
94
+ import type { InternalHandlerContext, ResolvedSegment } from "../../types.js";
95
95
  import type { EntryData } from "../../server/context.js";
96
96
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
97
97
  import { getRouterContext, type RouterContext } from "../router-context.js";
@@ -103,7 +103,11 @@ import {
103
103
  _getRequestContext,
104
104
  type RequestContext,
105
105
  } from "../../server/request-context.js";
106
- import { createShellImplicitDocScope } from "../../cache/cache-scope.js";
106
+ import {
107
+ createShellImplicitDocScope,
108
+ type CacheScope,
109
+ } from "../../cache/cache-scope.js";
110
+ import { ShellRecordUnavailableError } from "../../cache/shell-snapshot.js";
107
111
  import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
108
112
  import { paramsEqual } from "../params-util.js";
109
113
 
@@ -160,6 +164,7 @@ async function ensurePrerenderDeps() {
160
164
  // awaits before reaching this point (workerd can disrupt ALS mid-pipeline).
161
165
  async function* resolveFreshLoadersAndYield<TEnv>(
162
166
  ctx: MatchContext<TEnv>,
167
+ entries: EntryData[],
163
168
  state: MatchPipelineState,
164
169
  pipelineStart: number,
165
170
  ms: MatchContext<TEnv>["metricsStore"],
@@ -171,7 +176,7 @@ async function* resolveFreshLoadersAndYield<TEnv>(
171
176
  if (ctx.isFullMatch) {
172
177
  if (resolveLoadersOnly) {
173
178
  const loaderSegments = await ctx.Store.run(() =>
174
- resolveLoadersOnly(ctx.entries, ctx.handlerContext),
179
+ resolveLoadersOnly(entries, ctx.handlerContext),
175
180
  );
176
181
  state.matchedIds = state.cachedMatchedIds!;
177
182
  for (const segment of loaderSegments) {
@@ -184,7 +189,7 @@ async function* resolveFreshLoadersAndYield<TEnv>(
184
189
  if (resolveLoadersOnlyWithRevalidation) {
185
190
  const loaderResult = await ctx.Store.run(() =>
186
191
  resolveLoadersOnlyWithRevalidation(
187
- ctx.entries,
192
+ entries,
188
193
  ctx.handlerContext,
189
194
  ctx.clientSegmentSet,
190
195
  ctx.prevParams,
@@ -301,6 +306,7 @@ async function* yieldFromStore<TEnv>(
301
306
  // Resolve loaders fresh (loaders are never pre-rendered/cached).
302
307
  yield* resolveFreshLoadersAndYield(
303
308
  ctx,
309
+ ctx.entries,
304
310
  state,
305
311
  pipelineStart,
306
312
  ctx.metricsStore,
@@ -448,6 +454,8 @@ export function withCacheLookup<TEnv>(
448
454
  buildEntryRevalidateMap,
449
455
  resolveLoadersOnlyWithRevalidation,
450
456
  resolveLoadersOnly,
457
+ resolveAllSegments,
458
+ resolveAllSegmentsWithRevalidation,
451
459
  } = getRouterContext<TEnv>();
452
460
 
453
461
  if (prerenderStoreShortCircuits(ctx.matched.pr, ctx.request)) {
@@ -501,6 +509,15 @@ export function withCacheLookup<TEnv>(
501
509
  }
502
510
  }
503
511
 
512
+ // A document HIT tail replays the handler layer through the implicit doc
513
+ // scope and never runs a handler: without that scope (a context that lost
514
+ // the marker before the scope resolved), fail like a lookup miss instead
515
+ // of falling through to the handlers below.
516
+ const tailMarker = pipelineReqCtx?._shellImplicitCache;
517
+ if (tailMarker?.docTail && !ctx.cacheScope?.isShellImplicitDocScope) {
518
+ throw new ShellRecordUnavailableError(tailMarker.fixedDocKey);
519
+ }
520
+
504
521
  if (ctx.isAction || !ctx.cacheScope?.enabled) {
505
522
  yield* source;
506
523
  if (ms) {
@@ -513,13 +530,20 @@ export function withCacheLookup<TEnv>(
513
530
  return;
514
531
  }
515
532
 
533
+ // A record's loader-owned handle values claim their loader, so its own
534
+ // cache() HIT does not replay them again (restoreHandles).
535
+ const claimLoaderPushes = (ctx.handlerContext as InternalHandlerContext)
536
+ ._claimLoaderPushes;
516
537
  const explicitLookup = await ctx.cacheScope.lookupRouteDetailed(
517
538
  ctx.pathname,
518
539
  ctx.matched.params,
519
540
  ctx.isIntercept,
541
+ claimLoaderPushes,
520
542
  );
521
543
  let cacheResult =
522
544
  explicitLookup.status === "hit" ? explicitLookup.result : null;
545
+ // The scope whose record answered decides which entries it covers.
546
+ let hitScope: CacheScope = ctx.cacheScope;
523
547
 
524
548
  // PPR navigation replay composed with a route-derived cache() scope. The
525
549
  // explicit tier stays authoritative: its hit serves under its own
@@ -553,9 +577,13 @@ export function withCacheLookup<TEnv>(
553
577
  // no-eligible-snapshot path purely for truthful status) inert: a
554
578
  // store-less marker minting a doc scope here would resolve the APP
555
579
  // store and read the REAL doc: partition — a cross-partition serve.
556
- cacheResult = await createShellImplicitDocScope(
557
- replayMarker,
558
- ).lookupRoute(ctx.pathname, ctx.matched.params, ctx.isIntercept);
580
+ hitScope = createShellImplicitDocScope(replayMarker);
581
+ cacheResult = await hitScope.lookupRoute(
582
+ ctx.pathname,
583
+ ctx.matched.params,
584
+ ctx.isIntercept,
585
+ claimLoaderPushes,
586
+ );
559
587
  } else if (explicitLookup.status === "bypass") {
560
588
  // condition() refused at lookup time (the gate only pre-decides the
561
589
  // static cache(false) case) — report cache-disabled truthfully.
@@ -567,6 +595,13 @@ export function withCacheLookup<TEnv>(
567
595
  }
568
596
 
569
597
  if (!cacheResult) {
598
+ // A document shell HIT tail replays the handler layer from the entry's
599
+ // doc record and must never run a handler behind the committed prelude:
600
+ // a record that did not hit (it failed to decode, or the entry lost it)
601
+ // ends the tail here; serveShellHit degrades the response.
602
+ if (replayMarker?.docTail) {
603
+ throw new ShellRecordUnavailableError(replayMarker.fixedDocKey);
604
+ }
570
605
  yield* source;
571
606
  if (ms) {
572
607
  ms.metrics.push({
@@ -578,11 +613,68 @@ export function withCacheLookup<TEnv>(
578
613
  return;
579
614
  }
580
615
 
616
+ // Entries above the cache() boundary are not in the record: resolve them
617
+ // fresh, exactly as an uncached render of this request would.
618
+ const boundaryIndex =
619
+ hitScope.boundary === undefined
620
+ ? 0
621
+ : Math.max(
622
+ 0,
623
+ ctx.entries.findIndex((e) => e.shortCode === hitScope.boundary),
624
+ );
625
+ let liveSegments: ResolvedSegment[] = [];
626
+ let liveMatchedIds: string[] = [];
627
+ if (boundaryIndex > 0) {
628
+ const liveEntries = ctx.entries.slice(0, boundaryIndex);
629
+ const live: { segments: ResolvedSegment[]; matchedIds: string[] } =
630
+ await ctx.Store.run(async () => {
631
+ if (!ctx.isFullMatch) {
632
+ return resolveAllSegmentsWithRevalidation(
633
+ liveEntries,
634
+ ctx.routeKey,
635
+ ctx.matched.params,
636
+ ctx.handlerContext,
637
+ ctx.clientSegmentSet,
638
+ ctx.prevParams,
639
+ ctx.request,
640
+ ctx.prevUrl,
641
+ ctx.url,
642
+ ctx.actionContext,
643
+ ctx.interceptResult,
644
+ ctx.localRouteName,
645
+ ctx.pathname,
646
+ ctx.stale,
647
+ ctx.entries,
648
+ );
649
+ }
650
+ // The full chain keeps a bare cache() marker, which also sits in
651
+ // its layout's orphan list, out of the live pass: the record
652
+ // supplies it and its subtree (issue #918).
653
+ const segments = await resolveAllSegments(
654
+ liveEntries,
655
+ ctx.routeKey,
656
+ ctx.matched.params,
657
+ ctx.handlerContext,
658
+ ctx.loaderPromises,
659
+ { chain: ctx.entries },
660
+ );
661
+ return { segments, matchedIds: segments.map((s) => s.id) };
662
+ });
663
+ // The record supplies every covered segment.
664
+ liveSegments = live.segments.filter(
665
+ (s) => !hitScope.covers(s.id, s.namespace),
666
+ );
667
+ liveMatchedIds = live.matchedIds.filter((id) => !hitScope.covers(id));
668
+ }
669
+
581
670
  state.cacheHit = true;
582
671
  state.cacheSource = "runtime";
583
672
  state.shouldRevalidate = cacheResult.shouldRevalidate;
584
673
  state.cachedSegments = cacheResult.segments;
585
- state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
674
+ state.cachedMatchedIds = [
675
+ ...liveMatchedIds,
676
+ ...cacheResult.segments.map((s) => s.id),
677
+ ];
586
678
  const pprTransitionDecisions = pipelineReqCtx?._pprTransitionDecisions;
587
679
 
588
680
  const canCheckSegmentRevalidation =
@@ -593,6 +685,8 @@ export function withCacheLookup<TEnv>(
593
685
  ? buildEntryRevalidateMap(ctx.entries)
594
686
  : undefined;
595
687
 
688
+ yield* liveSegments;
689
+
596
690
  for (const segment of cacheResult.segments) {
597
691
  if (!ctx.clientSegmentSet.has(segment.id)) {
598
692
  if (isTraceActive()) {
@@ -708,13 +802,19 @@ export function withCacheLookup<TEnv>(
708
802
  if (barrierReqCtx._treeHasStreaming === undefined) {
709
803
  barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
710
804
  }
711
- barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
805
+ barrierReqCtx._resolveRenderBarrier(
806
+ liveSegments.length > 0
807
+ ? [...liveSegments, ...cacheResult.segments]
808
+ : cacheResult.segments,
809
+ );
712
810
  }
713
811
 
714
812
  // Resolve loaders fresh (loaders are never cached). Shared with the
715
- // prerender-store path via resolveFreshLoadersAndYield.
813
+ // prerender-store path via resolveFreshLoadersAndYield. The live pass
814
+ // already resolved the loaders of the entries above the boundary.
716
815
  yield* resolveFreshLoadersAndYield(
717
816
  ctx,
817
+ ctx.entries.slice(boundaryIndex),
718
818
  state,
719
819
  pipelineStart,
720
820
  ms,