@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
@@ -120,12 +120,8 @@ export async function resolveLoaders<TEnv>(
120
120
  // loading-disabled entries consult it), so normal requests are unchanged.
121
121
  const emitStreaming = !loadingDisabled || isShellCaptureActive();
122
122
 
123
- // PPR lane decision for this entry's loaders (loader-container-bake): an
124
- // entry WITHOUT renderable loading() puts its loaders on the BAKE lane —
125
- // executed at capture (container bakes, nested pending promises hole at the
126
- // consumer's Suspense) and overlay-pinned from the shell snapshot on a HIT.
127
- // Renderable loading() keeps the LIVE lane (masked at capture, always
128
- // fresh). Computed per entry; resolveLoaderData applies the policy.
123
+ // PPR bake/seed key input only; lane rule: see resolveLoaderData
124
+ // (loader-cache.ts). Unflagged loaders mask at capture regardless of the key.
129
125
  const bakeLane = !entryLoadingMasksLoaders(entry.loading);
130
126
 
131
127
  // Error context for wrapLoaderPromise: without it, a throwing DSL loader never
@@ -146,9 +142,9 @@ export async function resolveLoaders<TEnv>(
146
142
  // barrier), and the loader body can call rendered() before the await below
147
143
  // is reached.
148
144
  //
149
- // Shell-capture renders await too: flagged loaders BAKE at capture
150
- // (loader-mask masks only LIVE-lane loaders). Capture must await them so
151
- // bake-lane handle pushes land in the prelude.
145
+ // Shell-capture renders await too: flagged loaders execute at capture
146
+ // (lane rule: see resolveLoaderData, loader-cache.ts), so their handle
147
+ // pushes must land in the prelude.
152
148
  const awaitedIndices = registerAwaitBeforeFlushIds(loaderEntries);
153
149
 
154
150
  // Streaming loaders: promises kick off now, settle during RSC serialization.
@@ -276,6 +272,16 @@ export interface ResolveSegmentOptions {
276
272
  * so error boundaries keep catching at request time.
277
273
  */
278
274
  throwOnError?: boolean;
275
+ /**
276
+ * The matched route's full entry chain. resolveAllSegments defaults it to
277
+ * its `entries`; withCacheLookup passes it when it resolves only the entries
278
+ * above a cache() boundary. An orphan list can hold an entry of the chain: a
279
+ * bare cache() marker sits in its layout's layout[] (it wraps that layout's
280
+ * routes) and is the parent of the routes after it. For those routes it
281
+ * resolves once, as a chain entry, below the cache() header latch (issue
282
+ * #918: resolved twice on a MISS, once and discarded on a HIT).
283
+ */
284
+ chain?: readonly EntryData[];
279
285
  }
280
286
 
281
287
  /**
@@ -349,6 +355,7 @@ export async function resolveSegment<TEnv>(
349
355
  }
350
356
 
351
357
  for (const orphan of entry.layout) {
358
+ if (options?.chain?.includes(orphan)) continue;
352
359
  const orphanSegments = await resolveOrphanLayout(
353
360
  orphan,
354
361
  params,
@@ -578,6 +585,26 @@ export async function resolveOrphanLayout<TEnv>(
578
585
  resolvedParallelEntries.add(parallelEntry.id);
579
586
  }
580
587
 
588
+ // Routeless entries nested in this one (a layout() after a bare cache()
589
+ // marker, a wrapper inside a routeless wrapper) wrap the same content one
590
+ // level deeper. Issue #918: they were never rendered. None is in the
591
+ // matched chain: that would put this orphan in it, and resolveSegment
592
+ // skips those.
593
+ for (const nested of orphan.layout) {
594
+ segments.push(
595
+ ...(await resolveOrphanLayout(
596
+ nested,
597
+ params,
598
+ context,
599
+ loaderPromises,
600
+ belongsToRoute,
601
+ deps,
602
+ options,
603
+ routeKey,
604
+ )),
605
+ );
606
+ }
607
+
581
608
  return segments;
582
609
  }
583
610
 
@@ -736,6 +763,10 @@ export async function resolveAllSegments<TEnv>(
736
763
  ): Promise<ResolvedSegment[]> {
737
764
  const allSegments: ResolvedSegment[] = [];
738
765
  const seenIds = new Set<string>();
766
+ const segmentOptions: ResolveSegmentOptions = {
767
+ ...options,
768
+ chain: options?.chain ?? entries,
769
+ };
739
770
 
740
771
  // ppr routes are document-scoped cached territory: the whole chain (root
741
772
  // layout down to the page) bakes into the shared shell, so the header-write
@@ -760,8 +791,9 @@ export async function resolveAllSegments<TEnv>(
760
791
  // Set ALS flag when entering a cache() boundary so that ctx.get()
761
792
  // can guard non-cacheable variable reads. Also latch the header-write
762
793
  // scope (response-level side effects — headers/cookies/status).
763
- // Persists for all descendant entries.
764
- if (entry.type === "cache") {
794
+ // Persists for all descendant entries. A route entry carries `cache` when
795
+ // cache() is among its own children (dsl-helpers.ts cache()).
796
+ if (entry.cache) {
765
797
  const store = RangoContext.getStore();
766
798
  if (store) store.insideCacheScope = true;
767
799
  latchCachedHeaderScope("cache", routeKey);
@@ -779,7 +811,7 @@ export async function resolveAllSegments<TEnv>(
779
811
  loaderPromises,
780
812
  deps,
781
813
  false,
782
- options,
814
+ segmentOptions,
783
815
  ),
784
816
  (seg) => [seg],
785
817
  deps,
@@ -16,10 +16,28 @@
16
16
  * On hit: returns cached data directly, skips loader execution.
17
17
  * On stale hit (SWR): returns stale data, schedules background revalidation.
18
18
  * On miss: executes loader, schedules non-blocking cache write.
19
+ *
20
+ * Handle pushes (`ctx.use(Handle)(...)` in the loader body) are a side effect
21
+ * the HIT must reproduce: the MISS records the pushes of the body and of the
22
+ * loaders it awaits via ctx.use into the entry's `handles` blob (the
23
+ * "use cache" capture/encode), and every HIT — stale included — replays
24
+ * them, each loader's pushes at most once per request (a dependency another
25
+ * reader already ran keeps its live pushes). See replayLoaderHandles.
19
26
  */
20
27
 
21
- import type { LoaderEntry } from "../../server/context.js";
28
+ import {
29
+ getCurrentLoaderBodyId,
30
+ isInsideLoaderBody,
31
+ type LoaderEntry,
32
+ } from "../../server/context.js";
22
33
  import type { HandlerContext, InternalHandlerContext } from "../../types.js";
34
+ import type { HandleStore } from "../../server/handle-store.js";
35
+ import type { CacheItemResult } from "../../cache/types.js";
36
+ import {
37
+ startHandleCapture,
38
+ type HandleCapture,
39
+ } from "../../cache/handle-capture.js";
40
+ import { encodeHandles, decodeHandles } from "../../cache/handle-snapshot.js";
23
41
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
24
42
  import {
25
43
  getRequestContext,
@@ -121,6 +139,73 @@ function getLoaderStore(
121
139
  return resolveCacheStore(cacheConfig.options.store);
122
140
  }
123
141
 
142
+ /**
143
+ * Replay a loader-cache entry's recorded handle pushes on a HIT.
144
+ *
145
+ * Pushed to the CURRENT owning segment (the recorded ids are the MISS
146
+ * request's), in recorded order.
147
+ *
148
+ * One delivery per loader per request: each recorded group is one loader
149
+ * body's pushes (recordOwnerKey), and `claim` (setupLoaderAccess
150
+ * _claimLoaderPushes) skips a group whose loader already ran in this request
151
+ * — a sibling loader or the handler read the dependency, and its live pushes
152
+ * stand — or that another replay delivered. A claimed loader that runs later
153
+ * (loaders stay live) replaces its replayed values with its live pushes
154
+ * (HandleStore.pushReplayed).
155
+ *
156
+ * Deliberately NOT inside the loader's body scope: a stale hit's background
157
+ * revalidation of the same loader can be running with a diverting capture
158
+ * keyed on that body scope (executeLoaderData), which would swallow the
159
+ * replay; and the store reads the body scope to tell a live push from a
160
+ * replayed one.
161
+ * Outside it, a PPR shell capture classifies replayed pushes as
162
+ * loader-scoped and unbaked (shell-capture.ts), so a shell HIT gets them
163
+ * from the loader's own re-run, not twice.
164
+ */
165
+ async function replayLoaderHandles(
166
+ encoded: string,
167
+ handleStore: HandleStore,
168
+ segmentId: string,
169
+ claim: ((loaderId: string) => boolean) | undefined,
170
+ ): Promise<void> {
171
+ const recorded = await decodeHandles(encoded);
172
+ if (!recorded) return;
173
+ const delivers = new Map<string, boolean>();
174
+ for (const key in recorded) {
175
+ const owner = key.slice(key.indexOf(":") + 1);
176
+ let deliver = delivers.get(owner);
177
+ if (deliver === undefined) {
178
+ deliver = claim ? claim(owner) : true;
179
+ delivers.set(owner, deliver);
180
+ }
181
+ if (!deliver) continue;
182
+ for (const [handleName, values] of Object.entries(recorded[key])) {
183
+ for (const value of values) {
184
+ handleStore.pushReplayed(handleName, segmentId, value, owner);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Record key for the MISS capture: run-length groups by the innermost loader
192
+ * body, `${seq}:${loaderId}` — the cached loader or a dependency it awaits
193
+ * via ctx.use. Groups keep push order across bodies; the owner lets the
194
+ * replay deliver each loader's pushes at most once per request.
195
+ */
196
+ function recordOwnerKey(cachedLoaderId: string): () => string {
197
+ let seq = 0;
198
+ let owner: string | undefined;
199
+ return () => {
200
+ const current = getCurrentLoaderBodyId() ?? cachedLoaderId;
201
+ if (current !== owner) {
202
+ owner = current;
203
+ seq++;
204
+ }
205
+ return `${seq}:${current}`;
206
+ };
207
+ }
208
+
124
209
  /**
125
210
  * Resolve loader data with optional caching.
126
211
  *
@@ -131,6 +216,35 @@ function getLoaderStore(
131
216
  * (observePhase; see instrument.ts). A cache HIT returns without calling ctx.use,
132
217
  * so it emits no loader phase (the loader did not execute; the hit is only a
133
218
  * LoaderCache debug log).
219
+ *
220
+ * PPR loader lane rule — the canonical source statement; other sites point
221
+ * here (consumer docs: skills/ppr/SKILL.md "The loader lane rule";
222
+ * docs/design/loader-container-bake.md). This is the single funnel every
223
+ * loader segment path routes through (fresh resolveLoaders, cache-hit
224
+ * resolveLoadersOnly, revalidation resolveLoadersOnlyWithRevalidation, and
225
+ * intercept loaders in intercept-resolution.ts, which pass no bake key and
226
+ * never run under a shell capture), so the lane is decided here, per LOADER,
227
+ * never by the entry's loading():
228
+ *
229
+ * - BAKE lane: `loader(Def, { ssr: false })` (awaitBeforeFlush) with a
230
+ * `bakeSegmentKey` from the caller. The loader EXECUTES at capture (the
231
+ * flag's "data in the HTML before first flush" maps to the frozen prelude)
232
+ * and its settled non-promise data bakes into the shell. Promises nested in
233
+ * plain objects/arrays are masked (mask-nested.ts) and stay live holes at
234
+ * the consumer's own Suspense — per-request material must be promise-shaped
235
+ * (the #692 cross-session scar). The masked container registers on
236
+ * `_shellCaptureLoaderRecords`: it holds the capture gate (bounded by
237
+ * `ppr.captureTimeout`) and pins into the snapshot's loader family, and on a
238
+ * HIT the recorded container is overlaid onto the fresh run (below) so the
239
+ * payload matches the frozen prelude byte-for-byte. cookies()/headers() in
240
+ * its body (nested promise bodies included) trip the capture guard.
241
+ * - LIVE lane: every other loader, whatever its entry's loading(). Never
242
+ * executes at capture: the slot gets a never-resolving promise
243
+ * (loader-mask.ts) and postpones at the reader's boundary — loading() or an
244
+ * inline Suspense — then streams fresh per request. A masked read with NO
245
+ * boundary above it root-postpones and the <body> sanity gate refuses the
246
+ * shell (eternal-MISS warning). A route whose loaders are ALL `ssr: false`
247
+ * needs no loading(): nothing masks.
134
248
  */
135
249
  export function resolveLoaderData<TEnv>(
136
250
  loaderEntry: LoaderEntry,
@@ -141,42 +255,8 @@ export function resolveLoaderData<TEnv>(
141
255
  // One ALS read serves the capture check, the record registration, and the
142
256
  // seed lookup — this runs for every loader on every request.
143
257
  const reqCtx = _getRequestContext();
144
- // PPR shell capture policy — gated here, the single funnel every loader
145
- // segment path routes through (fresh resolveLoaders, cache-hit
146
- // resolveLoadersOnly, revalidation resolveLoadersOnlyWithRevalidation).
147
- //
148
- // Two lanes (docs/design/loader-container-bake.md):
149
- // - LIVE lane (entry has renderable loading(); no `bakeSegmentKey`): never
150
- // execute during capture. The slot gets a never-resolving promise so the
151
- // LoaderBoundary postpones (a hole). See loader-mask.ts.
152
- // - BAKE lane (no renderable loading(); callers pass `bakeSegmentKey`):
153
- // execute during capture exactly like axis 1 — the settled container bakes
154
- // into the prelude, nested pending promises postpone at the consumer's own
155
- // Suspense. The container promise is registered on the derived context so
156
- // captureAndStoreShell pins it into the snapshot's loader family; on a
157
- // shell HIT the recorded container is overlaid onto the fresh run so the
158
- // payload matches the frozen prelude byte-for-byte.
159
258
  if (isShellCaptureActive(reqCtx)) {
160
- // Capture lane, per LOADER (not per entry):
161
- //
162
- // - `ssr: false` (awaitBeforeFlush) — the BAKE lane. The flag's
163
- // document promise is "this loader's data is in the HTML before first
164
- // flush"; under ppr the pre-flush HTML IS the frozen prelude, so the
165
- // loader executes at capture and its SETTLED return bakes into the
166
- // shell. Nested-promise SHAPE stays the liveness declaration: nested
167
- // thenables are masked so their subtrees postpone as holes (per-request
168
- // material must be promise-shaped — the #692 cross-session scar). The
169
- // masked container registers on _shellCaptureLoaderRecords: it holds
170
- // the capture gate (bounded by ppr.captureTimeout) and pins into the
171
- // shell snapshot, so a HIT tail replays the baked value and the
172
- // hydration payload matches the frozen prelude byte-for-byte.
173
- // - Every other loader — LIVE unconditionally: masked with a
174
- // never-resolving promise, postponed at its boundary (loading() or an
175
- // inline Suspense), streamed fresh per request. A masked reader with NO
176
- // boundary above it root-postpones and the <body> sanity gate refuses
177
- // the shell (eternal-MISS warning) — the degrade for boundary-less ppr
178
- // routes. A route whose loaders are ALL flagged therefore needs no
179
- // loading() at all: nothing masks, the shell captures complete.
259
+ // Lane rule: see this function's JSDoc. Flagged = bake, else masked.
180
260
  if (loaderEntry.awaitBeforeFlush && bakeSegmentKey) {
181
261
  const containerPromise = executeLoaderData(loaderEntry, ctx, pathname);
182
262
  // Pre-attach a no-op catch: a bake-lane rejection during capture must
@@ -317,6 +397,16 @@ function executeLoaderData<TEnv>(
317
397
  const tags = resolveTags(loaderEntry);
318
398
  recordRequestTags(tags);
319
399
 
400
+ // Handle pushes: the store and the owning segment are read synchronously
401
+ // at kickoff, as createLoaderExecutor does for the body's own pushes.
402
+ const handleStore = _getRequestContext()?._handleStore;
403
+ const owningSegmentId = internal._currentSegmentId;
404
+ // Record only pushes from THIS loader's body and the loaders it awaits via
405
+ // ctx.use: the handler and sibling loaders push into the same store
406
+ // concurrently. A dependency can still run on a HIT for another reader
407
+ // (memoized per request); the replay's claim keeps its pushes to one copy.
408
+ const isOwnBodyPush = () => isInsideLoaderBody(loaderId);
409
+
320
410
  const dataPromise = (async () => {
321
411
  const codec = await getCodec();
322
412
  const key = await resolveLoaderKey(
@@ -335,11 +425,63 @@ function executeLoaderData<TEnv>(
335
425
  // The wrap is applied via wrapBackground (background path only); the
336
426
  // foreground miss runs execute() directly since its context is present.
337
427
  const requestCtxForExecute = getRequestContext();
338
- return readThroughItem({
428
+
429
+ // One readThroughItem call runs at most one execution — the foreground
430
+ // MISS or the background stale revalidation (flagged by wrapBackground)
431
+ // — so a single capture slot serves the setItem that follows it.
432
+ let capture: HandleCapture | undefined;
433
+ let revalidating = false;
434
+ let hitHandles: string | undefined;
435
+ const onCachedRead = (label: string, cached: CacheItemResult) => {
436
+ hitHandles = cached.handles;
437
+ debugLoaderCacheLog(`[LoaderCache] ${label}: ${key}`);
438
+ };
439
+ const execute = async (): Promise<any> => {
440
+ // A stale-hit revalidation runs on its own executor: it diverts its
441
+ // pushes (below), so sharing the page's memoized run of a dependency
442
+ // would take that dependency's pushes off the page.
443
+ const run = (revalidating && internal._runLoaderIsolated) || runMiss;
444
+ if (!handleStore) return run(loaderEntry.loader);
445
+ // A stale-hit revalidation diverts the body's pushes: the foreground
446
+ // already replayed the stale entry's, so the fresh ones belong only to
447
+ // the refreshed entry (not a duplicate in the live response).
448
+ const c = startHandleCapture(handleStore, {
449
+ accept: isOwnBodyPush,
450
+ divert: revalidating,
451
+ key: recordOwnerKey(loaderId),
452
+ });
453
+ capture = c.capture;
454
+ try {
455
+ return await run(loaderEntry.loader);
456
+ } finally {
457
+ c.stop();
458
+ }
459
+ };
460
+
461
+ // Flight encodes a rejected promise in the value or a handle push as an
462
+ // error row and completes normally. setItem runs after serialize and
463
+ // throws instead of storing it, into readThroughItem's cache-write /
464
+ // stale-revalidation report; a stale entry keeps serving.
465
+ const flightErrors: unknown[] = [];
466
+ const onFlightError = (error: unknown): void => {
467
+ flightErrors.push(error);
468
+ };
469
+
470
+ const data = await readThroughItem({
339
471
  getItem: (k) => store.getItem!(k),
340
- setItem: (k, v, o) => store.setItem!(k, v, o),
472
+ // Handles ride the entry like "use cache" (encodeHandles: Flight, pending
473
+ // pushes awaited up to its timeout, the whole blob dropped on a timeout
474
+ // or a thrown encode). Encoded here, inside the deferred write, so a
475
+ // MISS response never waits on it.
476
+ setItem: async (k, v, o) => {
477
+ const handles = capture
478
+ ? await encodeHandles(capture.data, onFlightError)
479
+ : "";
480
+ if (flightErrors.length > 0) throw flightErrors[0];
481
+ await store.setItem!(k, v, handles ? { ...o, handles } : o);
482
+ },
341
483
  key,
342
- execute: () => runMiss(loaderEntry.loader),
484
+ execute,
343
485
  // The rango.background span (kind=loader-revalidation) wraps the WHOLE
344
486
  // stale revalidation — the re-execution AND the serialize/setItem write
345
487
  // (read-through-swr routes the full task through wrapBackground) — so
@@ -347,21 +489,39 @@ function executeLoaderData<TEnv>(
347
489
  // store write all nest under one explanatory parent instead of dangling
348
490
  // under the ended foreground phases. Inside runWithRequestContext so
349
491
  // observePhase can read tracing on workerd (ALS detaches in waitUntil).
350
- wrapBackground: (run) =>
351
- runWithRequestContext(requestCtxForExecute, () =>
492
+ wrapBackground: (run) => {
493
+ revalidating = true;
494
+ return runWithRequestContext(requestCtxForExecute, () =>
352
495
  observePhase(PHASES.background("loader-revalidation"), run),
353
- ),
354
- serialize: (d) => codec.serializeResult(d),
496
+ );
497
+ },
498
+ serialize: (d) => codec.serializeResult(d, onFlightError),
355
499
  deserialize: (v) => codec.deserializeResult(v),
356
500
  storeOptions: { ttl, swr, tags },
357
- onHit: () => debugLoaderCacheLog(`[LoaderCache] HIT: ${key}`),
358
- onStale: () => debugLoaderCacheLog(`[LoaderCache] STALE: ${key}`),
501
+ onHit: (cached) => onCachedRead("HIT", cached),
502
+ onStale: (cached) => onCachedRead("STALE", cached),
359
503
  onMiss: () => debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`),
360
504
  onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
361
505
  host: requestCtxForExecute,
362
506
  });
507
+
508
+ // An entry written before handles were recorded has none: no replay.
509
+ if (hitHandles && handleStore && owningSegmentId) {
510
+ await replayLoaderHandles(
511
+ hitHandles,
512
+ handleStore,
513
+ owningSegmentId,
514
+ internal._claimLoaderPushes,
515
+ );
516
+ }
517
+ return data;
363
518
  })();
364
519
 
520
+ // Keep the store open for the replay: a HIT runs no loader body, so nothing
521
+ // else holds the auxiliary lane while getItem/decode are in flight, and a
522
+ // push after full drain throws LateHandlePushError.
523
+ handleStore?.trackAuxiliary(dataPromise);
524
+
365
525
  overrides.set(loaderId, dataPromise);
366
526
 
367
527
  return dataPromise;
@@ -2,13 +2,10 @@
2
2
  * PPR shell-capture loader masking.
3
3
  *
4
4
  * During a shell CAPTURE re-render (Axis 2, see docs/design/ppr-shell-resume.md)
5
- * route loaders are the "live lane": they must NOT execute — no side effects, no
6
- * cost, no cache round-trips. Instead every loader segment's value slot receives
7
- * a never-resolving promise, so the loader-consuming Suspense subtree stays
8
- * pending and React's static `prerender` marks it as a postponed hole. The frozen
9
- * shell (prelude) captures only the fallback; the resumed serve pass runs the
10
- * loaders fresh through the unchanged execution path and streams their output
11
- * into the holes.
5
+ * live-lane loaders (lane rule: see resolveLoaderData, loader-cache.ts) must
6
+ * NOT execute; their value slot gets a never-resolving promise so the
7
+ * consuming Suspense subtree postpones as a hole, and the serve pass runs them
8
+ * fresh into it.
12
9
  *
13
10
  * Capture mode is signalled by `requestCtx._shellCaptureRun`, set to true ONLY on
14
11
  * the derived request context of the background capture task (shell-capture.ts) —
@@ -36,24 +33,17 @@ export function isShellCaptureActive(
36
33
  return reqCtx?._shellCaptureRun === true;
37
34
  }
38
35
 
39
- // createMaskedLoaderPromise moved to the leaf module mask-nested.ts (shared
40
- // with the handle-push funnel in request-context, which cannot import THIS
41
- // module without a cycle). Re-exported to keep the mask API in one place.
36
+ // createMaskedLoaderPromise lives in the leaf module mask-nested.ts, beside
37
+ // maskNestedContainerThenables (also used by the capture handle-store push
38
+ // wrap in rsc/shell-capture.ts). Re-exported to keep the mask API in one place.
42
39
  export { createMaskedLoaderPromise } from "./mask-nested.js";
43
40
 
44
41
  /**
45
- * Entry-level lane input for an entry's loaders under PPR (the loading()
46
- * value; docs/design/loader-container-bake.md). The CAPTURE decision itself
47
- * is per LOADER in loader-cache.ts: an `ssr: false`
48
- * (awaitBeforeFlush) loader BAKES at capture regardless of this value — the
49
- * flag's document promise ("data in the HTML before first flush") maps to
50
- * the frozen prelude — while every other loader is LIVE (masked at capture,
51
- * fresh on every serve), postponing at loading() or an inline Suspense.
52
- * This helper still gates which entries pass a bake segment key for the
53
- * legacy loading()-less shape and the HIT-tail seed overlay.
54
- *
55
- * Mirrors segment-system's isRenderableLoading so the mask decision and the
56
- * tree's boundary placement can never disagree.
42
+ * True when the entry's loading() is renderable (mirrors segment-system's
43
+ * isRenderableLoading). Not the lane decision (lane rule: see
44
+ * resolveLoaderData, loader-cache.ts): callers only use it to decide whether
45
+ * an unflagged loader's segment key rides as the bake/seed key, which is inert
46
+ * at capture.
57
47
  */
58
48
  export function entryLoadingMasksLoaders(loading: unknown): boolean {
59
49
  return loading !== undefined && loading !== null && loading !== false;
@@ -2,9 +2,9 @@
2
2
  * Loader-family capture snapshot: elide + overlay for bake-lane loader
3
3
  * containers (docs/design/loader-container-bake.md).
4
4
  *
5
- * A loader on an entry with NO renderable loading() executes during shell
6
- * capture; its settled container bakes into the prelude while every promise
7
- * still pending at the quiet window postpones as a hole. To keep a HIT's fresh
5
+ * A bake-lane loader (lane rule: see resolveLoaderData, loader-cache.ts)
6
+ * executes during shell capture; its settled container bakes into the prelude
7
+ * while nested promises postpone as holes. To keep a HIT's fresh
8
8
  * payload byte-identical to that frozen prelude, the capture pins the container
9
9
  * in the shell snapshot:
10
10
  *
@@ -31,9 +31,9 @@
31
31
  import { isThenable } from "../../handles/is-thenable.js";
32
32
 
33
33
  // Capture-side nested-thenable masking lives in the LEAF module mask-nested.ts
34
- // (request-context also needs it for handle pushes and cannot import through
35
- // loader-mask without a cycle). Re-exported here so loader-cache and the unit
36
- // tests keep one import site for the snapshot family.
34
+ // (shared with the capture handle-store push wrap in rsc/shell-capture.ts).
35
+ // Re-exported here so loader-cache and the unit tests keep one import site for
36
+ // the snapshot family.
37
37
  export { maskNestedContainerThenables } from "./mask-nested.js";
38
38
 
39
39
  /**
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Capture-side nested-thenable masking — the mechanism behind "nested-promise
3
3
  * shape is the liveness declaration" for BOTH bake-lane loader containers
4
- * (loader-cache.ts) and pushed handle containers (request-context.ts
5
- * createUseFunction).
4
+ * (loader-cache.ts) and pushed handle containers (the capture handle store's
5
+ * push wrap in rsc/shell-capture.ts).
6
6
  *
7
- * Deliberately a LEAF module: request-context needs the mask for handle
8
- * pushes, and loader-mask (the other natural home) imports request-context —
9
- * importing from there would cycle. This module imports only is-thenable.
7
+ * Deliberately a LEAF module: loader-mask (the other natural home) imports
8
+ * request-context, so any funnel importing the mask from there inherits that
9
+ * graph. This module imports only is-thenable.
10
10
  */
11
11
 
12
12
  import { isThenable } from "../../handles/is-thenable.js";
@@ -33,8 +33,8 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
33
33
  * Deep-copy a container with every NESTED thenable replaced by a masked
34
34
  * (never-resolving) promise. Applied during shell capture to (a) bake-lane
35
35
  * loader containers (loader-cache.ts) and (b) pushed handle containers
36
- * (createUseFunction) — the two rango-owned funnels where consumers declare
37
- * per-request data by promise SHAPE.
36
+ * (shell-capture.ts capture store push wrap) — the two rango-owned funnels
37
+ * where consumers declare per-request data by promise SHAPE.
38
38
  *
39
39
  * Why: a nested promise that happened to SETTLE before the capture's quiet
40
40
  * window closed used to bake its value into the SHARED shell (and, for
@@ -215,11 +215,11 @@ export async function resolveLoadersWithRevalidation<TEnv>(
215
215
  // onError/loader.error. isPartial flags the reporting phase accordingly.
216
216
  const errorContext = { ...buildLoaderErrorContext(ctx), isPartial: true };
217
217
 
218
- // PPR lane decision, same rule as the fresh funnel (fresh.ts): an entry
219
- // without renderable loading() puts its loaders on the BAKE lane. The key
220
- // must thread through EVERY resolveLoaderData funnel — omitting it here made
221
- // a capture-active context whole-container-mask every loader on this path
222
- // (a never-settling promise for a loader that should have executed).
218
+ // PPR bake/seed key, same as fresh.ts (lane rule: see resolveLoaderData,
219
+ // loader-cache.ts). The key must thread through EVERY resolveLoaderData
220
+ // funnel — omitting it here made a capture-active context
221
+ // whole-container-mask every loader on this path (a never-settling promise
222
+ // for a loader that should have executed).
223
223
  // Outside capture the key only activates the _shellLoaderSeed overlay,
224
224
  // which document-only serveShellHit seeds — inert for partial requests.
225
225
  const bakeLane = !entryLoadingMasksLoaders(entry.loading);
@@ -934,6 +934,8 @@ export async function resolveSegmentWithRevalidation<TEnv>(
934
934
  deps: SegmentResolutionDeps<TEnv>,
935
935
  actionContext?: ActionContext,
936
936
  stale?: boolean,
937
+ /** The matched route's full chain; see ResolveSegmentOptions.chain (fresh.ts). */
938
+ chain?: readonly EntryData[],
937
939
  ): Promise<SegmentRevalidationResult> {
938
940
  const segments: ResolvedSegment[] = [];
939
941
  const matchedIds: string[] = [];
@@ -1066,6 +1068,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
1066
1068
  matchedIds.push(...parallelResult.matchedIds);
1067
1069
 
1068
1070
  for (const orphan of entry.layout) {
1071
+ if (chain?.includes(orphan)) continue;
1069
1072
  const orphanResult = await resolveOrphanLayoutWithRevalidation(
1070
1073
  orphan,
1071
1074
  params,
@@ -1274,6 +1277,27 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1274
1277
  segments.push(...parallelResult.segments);
1275
1278
  matchedIds.push(...parallelResult.matchedIds);
1276
1279
 
1280
+ // Nested routeless entries; see resolveOrphanLayout (fresh.ts).
1281
+ for (const nested of orphan.layout) {
1282
+ const nestedResult = await resolveOrphanLayoutWithRevalidation(
1283
+ nested,
1284
+ params,
1285
+ context,
1286
+ clientSegmentIds,
1287
+ prevParams,
1288
+ request,
1289
+ prevUrl,
1290
+ nextUrl,
1291
+ routeKey,
1292
+ belongsToRoute,
1293
+ deps,
1294
+ actionContext,
1295
+ stale,
1296
+ );
1297
+ segments.push(...nestedResult.segments);
1298
+ matchedIds.push(...nestedResult.matchedIds);
1299
+ }
1300
+
1277
1301
  return { segments, matchedIds };
1278
1302
  }
1279
1303
 
@@ -1296,6 +1320,8 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1296
1320
  pathname: string,
1297
1321
  deps: SegmentResolutionDeps<TEnv>,
1298
1322
  stale?: boolean,
1323
+ /** The matched route's full chain; see ResolveSegmentOptions.chain (fresh.ts). */
1324
+ chain: readonly EntryData[] = entries,
1299
1325
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1300
1326
  const allSegments: ResolvedSegment[] = [];
1301
1327
  const matchedIds: string[] = [];
@@ -1325,7 +1351,8 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1325
1351
  }
1326
1352
 
1327
1353
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1328
- if (entry.type === "cache") {
1354
+ // Same latch as resolveAllSegments (fresh.ts), route-level cache() included.
1355
+ if (entry.cache) {
1329
1356
  const store = RangoContext.getStore();
1330
1357
  if (store) store.insideCacheScope = true;
1331
1358
  latchCachedHeaderScope("cache", routeKey);
@@ -1348,6 +1375,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1348
1375
  deps,
1349
1376
  actionContext,
1350
1377
  stale,
1378
+ chain,
1351
1379
  ),
1352
1380
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1353
1381
  deps,
@@ -80,6 +80,8 @@ export interface SegmentWrappers<TEnv = any> {
80
80
  interceptResult: { intercept: InterceptEntry; entry: EntryData } | null,
81
81
  localRouteName: string,
82
82
  pathname: string,
83
+ stale?: boolean,
84
+ chain?: readonly EntryData[],
83
85
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
84
86
  findInterceptForRoute: (
85
87
  targetRouteKey: string,
@@ -205,6 +207,7 @@ export function createSegmentWrappers<TEnv = any>(
205
207
  localRouteName: string,
206
208
  pathname: string,
207
209
  stale?: boolean,
210
+ chain?: readonly EntryData[],
208
211
  ): ReturnType<typeof _resolveAllSegmentsWithRevalidation> {
209
212
  return _resolveAllSegmentsWithRevalidation(
210
213
  entries,
@@ -222,6 +225,7 @@ export function createSegmentWrappers<TEnv = any>(
222
225
  pathname,
223
226
  segmentDeps,
224
227
  stale,
228
+ chain,
225
229
  );
226
230
  }
227
231
 
@@ -155,9 +155,12 @@ function createFlightStage<TEnv>(
155
155
  if (input.ctx.devDiscoveryEpoch !== undefined && input.payload.metadata) {
156
156
  input.payload.metadata.devDiscoveryEpoch = input.ctx.devDiscoveryEpoch;
157
157
  }
158
+ // Read once here: onError can fire outside the ALS frame.
159
+ const renderErrors = _getRequestContext()?._renderErrors;
158
160
  const stream = input.ctx.renderToReadableStream<RscPayload>(input.payload, {
159
161
  temporaryReferences: input.temporaryReferences,
160
162
  onError: (error: unknown) => {
163
+ renderErrors?.push(error);
161
164
  input.ctx.callOnError(error, "rendering", {
162
165
  request: input.request,
163
166
  url: input.url,