@rangojs/router 0.16.0 → 0.17.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 (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -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,10 @@ 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";
107
110
  import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
108
111
  import { paramsEqual } from "../params-util.js";
109
112
 
@@ -160,6 +163,7 @@ async function ensurePrerenderDeps() {
160
163
  // awaits before reaching this point (workerd can disrupt ALS mid-pipeline).
161
164
  async function* resolveFreshLoadersAndYield<TEnv>(
162
165
  ctx: MatchContext<TEnv>,
166
+ entries: EntryData[],
163
167
  state: MatchPipelineState,
164
168
  pipelineStart: number,
165
169
  ms: MatchContext<TEnv>["metricsStore"],
@@ -171,7 +175,7 @@ async function* resolveFreshLoadersAndYield<TEnv>(
171
175
  if (ctx.isFullMatch) {
172
176
  if (resolveLoadersOnly) {
173
177
  const loaderSegments = await ctx.Store.run(() =>
174
- resolveLoadersOnly(ctx.entries, ctx.handlerContext),
178
+ resolveLoadersOnly(entries, ctx.handlerContext),
175
179
  );
176
180
  state.matchedIds = state.cachedMatchedIds!;
177
181
  for (const segment of loaderSegments) {
@@ -184,7 +188,7 @@ async function* resolveFreshLoadersAndYield<TEnv>(
184
188
  if (resolveLoadersOnlyWithRevalidation) {
185
189
  const loaderResult = await ctx.Store.run(() =>
186
190
  resolveLoadersOnlyWithRevalidation(
187
- ctx.entries,
191
+ entries,
188
192
  ctx.handlerContext,
189
193
  ctx.clientSegmentSet,
190
194
  ctx.prevParams,
@@ -301,6 +305,7 @@ async function* yieldFromStore<TEnv>(
301
305
  // Resolve loaders fresh (loaders are never pre-rendered/cached).
302
306
  yield* resolveFreshLoadersAndYield(
303
307
  ctx,
308
+ ctx.entries,
304
309
  state,
305
310
  pipelineStart,
306
311
  ctx.metricsStore,
@@ -448,6 +453,8 @@ export function withCacheLookup<TEnv>(
448
453
  buildEntryRevalidateMap,
449
454
  resolveLoadersOnlyWithRevalidation,
450
455
  resolveLoadersOnly,
456
+ resolveAllSegments,
457
+ resolveAllSegmentsWithRevalidation,
451
458
  } = getRouterContext<TEnv>();
452
459
 
453
460
  if (prerenderStoreShortCircuits(ctx.matched.pr, ctx.request)) {
@@ -513,13 +520,20 @@ export function withCacheLookup<TEnv>(
513
520
  return;
514
521
  }
515
522
 
523
+ // A record's loader-owned handle values claim their loader, so its own
524
+ // cache() HIT does not replay them again (restoreHandles).
525
+ const claimLoaderPushes = (ctx.handlerContext as InternalHandlerContext)
526
+ ._claimLoaderPushes;
516
527
  const explicitLookup = await ctx.cacheScope.lookupRouteDetailed(
517
528
  ctx.pathname,
518
529
  ctx.matched.params,
519
530
  ctx.isIntercept,
531
+ claimLoaderPushes,
520
532
  );
521
533
  let cacheResult =
522
534
  explicitLookup.status === "hit" ? explicitLookup.result : null;
535
+ // The scope whose record answered decides which entries it covers.
536
+ let hitScope: CacheScope = ctx.cacheScope;
523
537
 
524
538
  // PPR navigation replay composed with a route-derived cache() scope. The
525
539
  // explicit tier stays authoritative: its hit serves under its own
@@ -553,9 +567,13 @@ export function withCacheLookup<TEnv>(
553
567
  // no-eligible-snapshot path purely for truthful status) inert: a
554
568
  // store-less marker minting a doc scope here would resolve the APP
555
569
  // 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);
570
+ hitScope = createShellImplicitDocScope(replayMarker);
571
+ cacheResult = await hitScope.lookupRoute(
572
+ ctx.pathname,
573
+ ctx.matched.params,
574
+ ctx.isIntercept,
575
+ claimLoaderPushes,
576
+ );
559
577
  } else if (explicitLookup.status === "bypass") {
560
578
  // condition() refused at lookup time (the gate only pre-decides the
561
579
  // static cache(false) case) — report cache-disabled truthfully.
@@ -578,11 +596,68 @@ export function withCacheLookup<TEnv>(
578
596
  return;
579
597
  }
580
598
 
599
+ // Entries above the cache() boundary are not in the record: resolve them
600
+ // fresh, exactly as an uncached render of this request would.
601
+ const boundaryIndex =
602
+ hitScope.boundary === undefined
603
+ ? 0
604
+ : Math.max(
605
+ 0,
606
+ ctx.entries.findIndex((e) => e.shortCode === hitScope.boundary),
607
+ );
608
+ let liveSegments: ResolvedSegment[] = [];
609
+ let liveMatchedIds: string[] = [];
610
+ if (boundaryIndex > 0) {
611
+ const liveEntries = ctx.entries.slice(0, boundaryIndex);
612
+ const live: { segments: ResolvedSegment[]; matchedIds: string[] } =
613
+ await ctx.Store.run(async () => {
614
+ if (!ctx.isFullMatch) {
615
+ return resolveAllSegmentsWithRevalidation(
616
+ liveEntries,
617
+ ctx.routeKey,
618
+ ctx.matched.params,
619
+ ctx.handlerContext,
620
+ ctx.clientSegmentSet,
621
+ ctx.prevParams,
622
+ ctx.request,
623
+ ctx.prevUrl,
624
+ ctx.url,
625
+ ctx.actionContext,
626
+ ctx.interceptResult,
627
+ ctx.localRouteName,
628
+ ctx.pathname,
629
+ ctx.stale,
630
+ ctx.entries,
631
+ );
632
+ }
633
+ // The full chain keeps a bare cache() marker, which also sits in
634
+ // its layout's orphan list, out of the live pass: the record
635
+ // supplies it and its subtree (issue #918).
636
+ const segments = await resolveAllSegments(
637
+ liveEntries,
638
+ ctx.routeKey,
639
+ ctx.matched.params,
640
+ ctx.handlerContext,
641
+ ctx.loaderPromises,
642
+ { chain: ctx.entries },
643
+ );
644
+ return { segments, matchedIds: segments.map((s) => s.id) };
645
+ });
646
+ // The record supplies every covered segment.
647
+ liveSegments = live.segments.filter(
648
+ (s) => !hitScope.covers(s.id, s.namespace),
649
+ );
650
+ liveMatchedIds = live.matchedIds.filter((id) => !hitScope.covers(id));
651
+ }
652
+
581
653
  state.cacheHit = true;
582
654
  state.cacheSource = "runtime";
583
655
  state.shouldRevalidate = cacheResult.shouldRevalidate;
584
656
  state.cachedSegments = cacheResult.segments;
585
- state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
657
+ state.cachedMatchedIds = [
658
+ ...liveMatchedIds,
659
+ ...cacheResult.segments.map((s) => s.id),
660
+ ];
586
661
  const pprTransitionDecisions = pipelineReqCtx?._pprTransitionDecisions;
587
662
 
588
663
  const canCheckSegmentRevalidation =
@@ -593,6 +668,8 @@ export function withCacheLookup<TEnv>(
593
668
  ? buildEntryRevalidateMap(ctx.entries)
594
669
  : undefined;
595
670
 
671
+ yield* liveSegments;
672
+
596
673
  for (const segment of cacheResult.segments) {
597
674
  if (!ctx.clientSegmentSet.has(segment.id)) {
598
675
  if (isTraceActive()) {
@@ -708,13 +785,19 @@ export function withCacheLookup<TEnv>(
708
785
  if (barrierReqCtx._treeHasStreaming === undefined) {
709
786
  barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
710
787
  }
711
- barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
788
+ barrierReqCtx._resolveRenderBarrier(
789
+ liveSegments.length > 0
790
+ ? [...liveSegments, ...cacheResult.segments]
791
+ : cacheResult.segments,
792
+ );
712
793
  }
713
794
 
714
795
  // Resolve loaders fresh (loaders are never cached). Shared with the
715
- // prerender-store path via resolveFreshLoadersAndYield.
796
+ // prerender-store path via resolveFreshLoadersAndYield. The live pass
797
+ // already resolved the loaders of the entries above the boundary.
716
798
  yield* resolveFreshLoadersAndYield(
717
799
  ctx,
800
+ ctx.entries.slice(boundaryIndex),
718
801
  state,
719
802
  pipelineStart,
720
803
  ms,
@@ -103,7 +103,7 @@
103
103
  import type { ResolvedSegment } from "../../types.js";
104
104
  import {
105
105
  getRequestContext,
106
- runWithRequestContext,
106
+ type RequestContext,
107
107
  } from "../../server/request-context.js";
108
108
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
109
109
  import { createShellImplicitDocScope } from "../../cache/cache-scope.js";
@@ -111,6 +111,7 @@ import { getRouterContext } from "../router-context.js";
111
111
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
112
112
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
113
113
  import type { GeneratorMiddleware } from "./cache-lookup.js";
114
+ import { rerenderAndCacheRoute } from "./background-revalidation.js";
114
115
 
115
116
  /**
116
117
  * Creates cache store middleware
@@ -159,13 +160,9 @@ export function withCacheStore<TEnv>(
159
160
  return;
160
161
  }
161
162
 
162
- const {
163
- createHandlerContext,
164
- setupLoaderAccess,
165
- resolveAllSegments,
166
- resolveInterceptEntry,
167
- createHandleStore,
168
- } = getRouterContext<TEnv>();
163
+ // Read synchronously in the pipeline: the proactive write runs in
164
+ // waitUntil, where the router-context ALS may be gone.
165
+ const routerCtx = getRouterContext<TEnv>();
169
166
 
170
167
  // On a fresh intercept miss the intercept slot segments flow through
171
168
  // `source` into allSegments AND are also recorded on state.interceptSegments.
@@ -178,18 +175,21 @@ export function withCacheStore<TEnv>(
178
175
  ...state.interceptSegments.filter((s) => !seenSegmentIds.has(s.id)),
179
176
  ];
180
177
 
178
+ const cacheScope = ctx.cacheScope;
179
+
180
+ // Only the segments the entry stores count: a null layout above the
181
+ // boundary (the client kept it) is never written, so it needs no re-render.
181
182
  const hasNullComponents = allSegmentsToCache.some(
182
183
  (s) =>
183
184
  s.component === null &&
184
185
  s.type !== "loader" &&
185
- ctx.clientSegmentSet.has(s.id),
186
+ ctx.clientSegmentSet.has(s.id) &&
187
+ cacheScope.covers(s.id, s.namespace),
186
188
  );
187
189
 
188
190
  const requestCtx = getRequestContext();
189
191
  if (!requestCtx) return;
190
192
 
191
- const cacheScope = ctx.cacheScope;
192
-
193
193
  // Record the route's segment-DSL cache tags into the request tag union NOW,
194
194
  // synchronously in the pipeline. The actual store write (cacheRoute) runs in
195
195
  // requestCtx.waitUntil() below — and the proactive path re-resolves the whole
@@ -216,91 +216,21 @@ export function withCacheStore<TEnv>(
216
216
 
217
217
  if (hasNullComponents) {
218
218
  requestCtx.waitUntil(async () => {
219
- const savedMetrics = ctx.Store.metrics;
220
- ctx.Store.metrics = undefined;
221
-
222
219
  const start = performance.now();
223
220
  debugLog("cacheStore", "proactive caching started", {
224
221
  pathname: ctx.pathname,
225
222
  });
226
- const originalHandleStore = requestCtx._handleStore;
227
- requestCtx._handleStore = createHandleStore();
228
223
  try {
229
- const proactiveHandlerContext = createHandlerContext(
230
- ctx.matched.params,
231
- ctx.request,
232
- ctx.url.searchParams,
233
- ctx.pathname,
234
- ctx.url,
235
- ctx.env,
236
- ctx.routeMap,
237
- ctx.matched.routeKey,
238
- ctx.matched.responseType,
239
- ctx.matched.pt === true,
240
- );
241
- const proactiveLoaderPromises = new Map<string, Promise<any>>();
242
-
243
- setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
244
-
245
- const Store = ctx.Store;
246
- // Re-establish the request-context ALS around the re-render. Store
247
- // is a different ALS (DSL build context); on workerd a waitUntil
248
- // task runs detached from the request's I/O context, so a handler/
249
- // component that reads the ambient getRequestContext() during this
250
- // background re-render would otherwise throw "called outside of a
251
- // request context".
252
- const freshSegments = await runWithRequestContext(requestCtx, () =>
253
- Store.run(() =>
254
- resolveAllSegments(
255
- ctx.entries,
256
- ctx.routeKey,
257
- ctx.matched.params,
258
- proactiveHandlerContext,
259
- proactiveLoaderPromises,
260
- { skipLoaders: true },
261
- ),
262
- ),
263
- );
264
-
265
- let freshInterceptSegments: ResolvedSegment[] = [];
266
- if (ctx.interceptResult) {
267
- freshInterceptSegments = await runWithRequestContext(
268
- requestCtx,
269
- () =>
270
- Store.run(() =>
271
- resolveInterceptEntry(
272
- ctx.interceptResult!.intercept,
273
- ctx.interceptResult!.entry,
274
- ctx.matched.params,
275
- proactiveHandlerContext,
276
- true, // belongsToRoute
277
- // No revalidationContext = render fresh
278
- undefined,
279
- // Skip intercept middleware: the foreground already ran it
280
- // before the response was sent. Re-running here (post-
281
- // response, background) would fire side effects twice and a
282
- // short-circuit Response would silently abort this write.
283
- { skipMiddleware: true },
284
- ),
285
- ),
286
- );
287
- }
288
-
289
- const completeSegments = [
290
- ...freshSegments,
291
- ...freshInterceptSegments,
292
- ];
293
- requestCtx._handleStore.seal();
294
- await cacheScope.cacheRoute(
295
- ctx.pathname,
296
- ctx.matched.params,
297
- completeSegments,
298
- ctx.isIntercept,
224
+ const count = await rerenderAndCacheRoute<TEnv>(
225
+ ctx,
226
+ requestCtx as RequestContext<TEnv>,
227
+ cacheScope,
228
+ routerCtx,
299
229
  );
300
230
  if (INTERNAL_RANGO_DEBUG) {
301
231
  const dur = performance.now() - start;
302
232
  console.log(
303
- `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
233
+ `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${count}`,
304
234
  );
305
235
  }
306
236
  debugLog("cacheStore", "proactive caching complete", {
@@ -317,9 +247,6 @@ export function withCacheStore<TEnv>(
317
247
  pathname: ctx.pathname,
318
248
  error: String(error),
319
249
  });
320
- } finally {
321
- requestCtx._handleStore = originalHandleStore;
322
- ctx.Store.metrics = savedMetrics;
323
250
  }
324
251
  });
325
252
  } else {