@rangojs/router 0.15.1 → 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 (249) 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 +52 -0
  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 +36 -10
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +3 -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/thenable-status.d.ts +9 -0
  67. package/dist/types/types/cache-types.d.ts +14 -10
  68. package/dist/types/types/handler-context.d.ts +14 -0
  69. package/dist/types/types/loader-types.d.ts +8 -6
  70. package/dist/types/urls/response-types.d.ts +3 -12
  71. package/dist/types/use-loader.d.ts +12 -3
  72. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  73. package/dist/types/vite/rango.d.ts +1 -1
  74. package/dist/vite/index.js +884 -768
  75. package/package.json +4 -4
  76. package/skills/api-client/SKILL.md +22 -10
  77. package/skills/breadcrumbs/SKILL.md +65 -22
  78. package/skills/bundle-analysis/SKILL.md +22 -22
  79. package/skills/cache-guide/SKILL.md +207 -97
  80. package/skills/caching/SKILL.md +295 -93
  81. package/skills/catalog.json +16 -16
  82. package/skills/client-urls/SKILL.md +53 -31
  83. package/skills/cloudflare/SKILL.md +72 -20
  84. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  85. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  86. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  87. package/skills/comparison/SKILL.md +5 -0
  88. package/skills/comparison/references/framework-comparison.md +39 -33
  89. package/skills/composability/SKILL.md +38 -29
  90. package/skills/css/SKILL.md +10 -13
  91. package/skills/debug-manifest/SKILL.md +74 -31
  92. package/skills/defer-hydration/SKILL.md +2 -1
  93. package/skills/deployment-caching/SKILL.md +6 -1
  94. package/skills/document-cache/SKILL.md +92 -28
  95. package/skills/fonts/SKILL.md +16 -8
  96. package/skills/handler-use/SKILL.md +53 -17
  97. package/skills/hooks/SKILL.md +28 -20
  98. package/skills/hooks/data.md +43 -1
  99. package/skills/hooks/handle-and-actions.md +31 -16
  100. package/skills/hooks/navigation.md +39 -10
  101. package/skills/hooks/outlets.md +13 -6
  102. package/skills/hooks/state.md +37 -15
  103. package/skills/hooks/urls.md +7 -5
  104. package/skills/host-router/SKILL.md +64 -12
  105. package/skills/i18n/SKILL.md +60 -34
  106. package/skills/intercept/SKILL.md +161 -26
  107. package/skills/layout/SKILL.md +106 -50
  108. package/skills/links/SKILL.md +51 -19
  109. package/skills/loader/SKILL.md +242 -106
  110. package/skills/middleware/SKILL.md +119 -23
  111. package/skills/migrate-nextjs/SKILL.md +100 -45
  112. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  113. package/skills/migrate-react-router/SKILL.md +49 -38
  114. package/skills/migrate-react-router/component-migration.md +25 -6
  115. package/skills/migrate-react-router/data-and-actions.md +18 -5
  116. package/skills/migrate-react-router/route-mapping.md +12 -6
  117. package/skills/mime-routes/SKILL.md +12 -7
  118. package/skills/observability/SKILL.md +28 -14
  119. package/skills/parallel/SKILL.md +75 -39
  120. package/skills/ppr/SKILL.md +345 -150
  121. package/skills/prerender/SKILL.md +189 -100
  122. package/skills/rango/SKILL.md +65 -34
  123. package/skills/react-compiler/SKILL.md +6 -5
  124. package/skills/response-routes/SKILL.md +133 -25
  125. package/skills/route/SKILL.md +165 -60
  126. package/skills/router-setup/SKILL.md +418 -240
  127. package/skills/scripts/SKILL.md +31 -20
  128. package/skills/server-actions/SKILL.md +220 -59
  129. package/skills/shell-manifest/SKILL.md +12 -7
  130. package/skills/streams-and-websockets/SKILL.md +32 -14
  131. package/skills/tailwind/SKILL.md +37 -33
  132. package/skills/testing/SKILL.md +68 -32
  133. package/skills/testing/cache-prerender.md +55 -29
  134. package/skills/testing/client-components.md +59 -6
  135. package/skills/testing/e2e-parity.md +12 -2
  136. package/skills/testing/flight.md +19 -11
  137. package/skills/testing/handles.md +8 -5
  138. package/skills/testing/loader.md +8 -5
  139. package/skills/testing/middleware.md +8 -3
  140. package/skills/testing/predicates.md +120 -0
  141. package/skills/testing/render-handler.md +70 -22
  142. package/skills/testing/response-routes.md +11 -4
  143. package/skills/testing/reverse-and-types.md +20 -14
  144. package/skills/testing/server-actions.md +12 -3
  145. package/skills/testing/server-tree.md +9 -0
  146. package/skills/testing/setup.md +9 -5
  147. package/skills/theme/SKILL.md +106 -29
  148. package/skills/typesafety/SKILL.md +20 -9
  149. package/skills/typesafety/env-and-bindings.md +126 -47
  150. package/skills/typesafety/generated-files-and-cli.md +26 -14
  151. package/skills/typesafety/params-and-search.md +44 -19
  152. package/skills/typesafety/route-types.md +50 -14
  153. package/skills/use-cache/SKILL.md +200 -82
  154. package/skills/vercel/SKILL.md +21 -11
  155. package/skills/view-transitions/SKILL.md +41 -20
  156. package/src/__augment-tests__/augmented.check.ts +26 -1
  157. package/src/browser/index.ts +2 -2
  158. package/src/browser/navigation-bridge.ts +3 -4
  159. package/src/browser/navigation-transaction.ts +22 -2
  160. package/src/browser/partial-update.ts +75 -80
  161. package/src/browser/react/ScrollRestoration.tsx +6 -6
  162. package/src/browser/rsc-router.tsx +1 -1
  163. package/src/browser/scroll-restoration.ts +53 -6
  164. package/src/cache/cache-exec-scope.ts +37 -4
  165. package/src/cache/cache-key-utils.ts +1 -6
  166. package/src/cache/cache-runtime.ts +230 -61
  167. package/src/cache/cache-scope.ts +76 -12
  168. package/src/cache/document-cache.ts +15 -0
  169. package/src/cache/handle-capture.ts +90 -10
  170. package/src/cache/handle-snapshot.ts +132 -28
  171. package/src/cache/segment-codec.ts +26 -7
  172. package/src/cache/tag-invalidation.ts +4 -3
  173. package/src/cache/taint.ts +1 -1
  174. package/src/cache/types.ts +16 -3
  175. package/src/client.rsc.tsx +10 -9
  176. package/src/client.tsx +5 -4
  177. package/src/components/DefaultDocument.tsx +7 -2
  178. package/src/context-var.ts +6 -5
  179. package/src/escape-script.ts +1 -1
  180. package/src/handles/MetaTags.tsx +13 -11
  181. package/src/handles/Scripts.tsx +11 -10
  182. package/src/handles/meta.ts +2 -2
  183. package/src/handles/script.ts +17 -16
  184. package/src/href-client.ts +1 -1
  185. package/src/html.ts +12 -0
  186. package/src/index.rsc.ts +5 -0
  187. package/src/index.ts +1 -0
  188. package/src/loader-store.ts +106 -0
  189. package/src/prerender/build-shell-capture.ts +8 -11
  190. package/src/reverse.ts +44 -4
  191. package/src/route-definition/dsl-helpers.ts +134 -42
  192. package/src/route-definition/helpers-types.ts +36 -10
  193. package/src/route-definition/resolve-handler-use.ts +121 -6
  194. package/src/route-types.ts +2 -3
  195. package/src/router/debug-manifest.ts +10 -2
  196. package/src/router/error-handling.ts +51 -15
  197. package/src/router/handler-context.ts +34 -25
  198. package/src/router/intercept-resolution.ts +125 -39
  199. package/src/router/loader-resolution.ts +56 -11
  200. package/src/router/match-api.ts +12 -17
  201. package/src/router/match-middleware/background-revalidation.ts +161 -96
  202. package/src/router/match-middleware/cache-lookup.ts +93 -10
  203. package/src/router/match-middleware/cache-store.ts +17 -90
  204. package/src/router/middleware-types.ts +3 -5
  205. package/src/router/middleware.ts +52 -9
  206. package/src/router/prerender-match.ts +53 -19
  207. package/src/router/route-snapshot.ts +11 -1
  208. package/src/router/router-context.ts +2 -0
  209. package/src/router/router-interfaces.ts +25 -2
  210. package/src/router/router-options.ts +8 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -12
  212. package/src/router/segment-resolution/loader-cache.ts +205 -45
  213. package/src/router/segment-resolution/loader-mask.ts +12 -22
  214. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  215. package/src/router/segment-resolution/mask-nested.ts +7 -7
  216. package/src/router/segment-resolution/revalidation.ts +34 -6
  217. package/src/router/segment-wrappers.ts +4 -0
  218. package/src/rsc/render-pipeline.ts +3 -0
  219. package/src/rsc/response-route-handler.ts +11 -3
  220. package/src/rsc/rsc-rendering.ts +8 -45
  221. package/src/rsc/shell-capture-constants.ts +13 -0
  222. package/src/rsc/shell-capture.ts +111 -90
  223. package/src/rsc/ssr-setup.ts +14 -8
  224. package/src/rsc/types.ts +11 -0
  225. package/src/server/context.ts +88 -11
  226. package/src/server/cookie-store.ts +5 -4
  227. package/src/server/handle-store.ts +232 -7
  228. package/src/server/request-context.ts +35 -30
  229. package/src/ssr/index.tsx +40 -7
  230. package/src/ssr-suspension-warning.ts +2 -13
  231. package/src/static-handler.ts +4 -1
  232. package/src/testing/flight-runtime.d.ts +21 -0
  233. package/src/testing/generated-routes.ts +2 -2
  234. package/src/testing/internal/context.ts +1 -1
  235. package/src/testing/internal/flight-client-globals.ts +7 -5
  236. package/src/testing/render-route.tsx +85 -24
  237. package/src/testing/run-loader.ts +5 -1
  238. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  239. package/src/testing/vitest.ts +62 -1
  240. package/src/theme/ThemeProvider.tsx +14 -12
  241. package/src/theme/ThemeScript.tsx +3 -3
  242. package/src/thenable-status.ts +16 -0
  243. package/src/types/cache-types.ts +14 -10
  244. package/src/types/handler-context.ts +14 -0
  245. package/src/types/loader-types.ts +8 -6
  246. package/src/urls/response-types.ts +3 -23
  247. package/src/use-loader.tsx +24 -4
  248. package/src/vite/plugins/use-cache-transform.ts +118 -74
  249. package/src/vite/rango.ts +7 -3
@@ -7,7 +7,9 @@ import { RouteNotFoundError } from "../errors";
7
7
  import {
8
8
  createErrorInfo,
9
9
  createErrorSegment,
10
+ findInOrphans,
10
11
  findNearestErrorBoundary as findErrorBoundary,
12
+ withErrorBoundary,
11
13
  } from "./error-handling.js";
12
14
  import {
13
15
  createHandlerContext,
@@ -481,24 +483,21 @@ export async function matchError<TEnv>(
481
483
  segmentType,
482
484
  );
483
485
 
486
+ // Same search order as findNearestErrorBoundary, so the fallback above and
487
+ // the entry that positions it agree.
484
488
  let entryWithBoundary: EntryData | null = null;
485
489
  let current: EntryData | null = manifestEntry;
490
+ let prev: EntryData | null = null;
486
491
  while (current) {
487
492
  if (current.errorBoundary && current.errorBoundary.length > 0) {
488
493
  entryWithBoundary = current;
489
494
  break;
490
495
  }
491
496
 
492
- if (current.layout && current.layout.length > 0) {
493
- for (const orphan of current.layout) {
494
- if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
495
- entryWithBoundary = orphan;
496
- break;
497
- }
498
- }
499
- if (entryWithBoundary) break;
500
- }
497
+ entryWithBoundary = findInOrphans(current, withErrorBoundary, prev) ?? null;
498
+ if (entryWithBoundary) break;
501
499
 
500
+ prev = current;
502
501
  current = current.parent;
503
502
  }
504
503
 
@@ -507,23 +506,19 @@ export async function matchError<TEnv>(
507
506
 
508
507
  if (entryWithBoundary) {
509
508
  boundaryEntry = entryWithBoundary;
509
+ // The chain entry holding the boundary, itself or in its orphan tree: the
510
+ // error segment takes the place of its child in the chain.
511
+ const boundaryOwner = current;
510
512
 
511
513
  outletEntry = manifestEntry;
512
514
  current = manifestEntry;
513
515
 
514
516
  while (current) {
515
- if (current.parent === boundaryEntry) {
517
+ if (current.parent === boundaryOwner) {
516
518
  outletEntry = current;
517
519
  break;
518
520
  }
519
521
 
520
- if (current.parent && current.parent.layout) {
521
- if (current.parent.layout.includes(boundaryEntry)) {
522
- outletEntry = current;
523
- break;
524
- }
525
- }
526
-
527
522
  current = current.parent;
528
523
  }
529
524
  } else {
@@ -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,