@rangojs/router 0.26.6 → 0.28.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 (234) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-coordinator.d.ts +0 -4
  4. package/dist/types/browser/action-fence.d.ts +11 -3
  5. package/dist/types/browser/event-controller.d.ts +20 -1
  6. package/dist/types/browser/history-state.d.ts +7 -9
  7. package/dist/types/browser/intercept-utils.d.ts +23 -0
  8. package/dist/types/browser/navigation-store.d.ts +8 -6
  9. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  10. package/dist/types/browser/rsc-router.d.ts +1 -1
  11. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  12. package/dist/types/browser/types.d.ts +2 -1
  13. package/dist/types/browser/wire-protocol.d.ts +25 -0
  14. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  15. package/dist/types/cache/cache-scope.d.ts +34 -22
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  18. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  19. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  20. package/dist/types/cache/segment-codec.d.ts +14 -0
  21. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  22. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +4 -6
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +4 -4
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +12 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
  38. package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
  39. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/revalidation.d.ts +6 -1
  44. package/dist/types/router/router-context.d.ts +9 -63
  45. package/dist/types/router/router-interfaces.d.ts +8 -14
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution.d.ts +1 -2
  52. package/dist/types/router/segment-wrappers.d.ts +11 -48
  53. package/dist/types/router/telemetry.d.ts +2 -0
  54. package/dist/types/router/types.d.ts +10 -0
  55. package/dist/types/rsc/full-payload.d.ts +2 -2
  56. package/dist/types/rsc/handler-context.d.ts +6 -6
  57. package/dist/types/rsc/json-route-result.d.ts +5 -9
  58. package/dist/types/rsc/request-stages.d.ts +203 -0
  59. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  60. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  61. package/dist/types/rsc/routine-plan.d.ts +1 -1
  62. package/dist/types/rsc/server-action.d.ts +2 -2
  63. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture.d.ts +132 -15
  65. package/dist/types/rsc/shell-serve.d.ts +37 -12
  66. package/dist/types/server/context.d.ts +9 -8
  67. package/dist/types/server/handle-store.d.ts +16 -0
  68. package/dist/types/server/render-pass.d.ts +72 -0
  69. package/dist/types/server/request-context.d.ts +24 -104
  70. package/dist/types/server/rewrite-trail.d.ts +2 -2
  71. package/dist/types/testing/dispatch.d.ts +35 -18
  72. package/dist/types/testing/serve-shell-request.d.ts +7 -0
  73. package/dist/types/types/cache-types.d.ts +6 -3
  74. package/dist/types/types/error-types.d.ts +9 -1
  75. package/dist/types/types/handler-context.d.ts +18 -7
  76. package/dist/types/types/segments.d.ts +1 -1
  77. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  78. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  79. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  80. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  81. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  82. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  83. package/dist/types/vite/discovery/state.d.ts +12 -0
  84. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  85. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  86. package/dist/types/vite/router-discovery.d.ts +8 -47
  87. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  88. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  89. package/dist/types/wire-params.d.ts +34 -0
  90. package/dist/vite/index.js +3809 -3710
  91. package/package.json +1 -1
  92. package/skills/cache-guide/SKILL.md +8 -5
  93. package/skills/caching/SKILL.md +37 -18
  94. package/skills/client-urls/SKILL.md +2 -1
  95. package/skills/document-cache/SKILL.md +18 -1
  96. package/skills/handler-use/SKILL.md +1 -1
  97. package/skills/intercept/SKILL.md +53 -13
  98. package/skills/layout/SKILL.md +11 -7
  99. package/skills/loader/SKILL.md +30 -13
  100. package/skills/middleware/SKILL.md +19 -1
  101. package/skills/migrate-nextjs/SKILL.md +5 -4
  102. package/skills/parallel/SKILL.md +5 -4
  103. package/skills/ppr/SKILL.md +19 -3
  104. package/skills/prerender/SKILL.md +39 -15
  105. package/skills/rango/SKILL.md +23 -11
  106. package/skills/server-actions/SKILL.md +51 -6
  107. package/skills/testing/predicates.md +1 -1
  108. package/skills/testing/response-routes.md +6 -4
  109. package/skills/testing/serve-shell-request.md +7 -7
  110. package/skills/use-cache/SKILL.md +46 -8
  111. package/skills/ux-architecture/SKILL.md +3 -2
  112. package/src/__internal.ts +7 -1
  113. package/src/browser/action-coordinator.ts +2 -13
  114. package/src/browser/action-fence.ts +11 -3
  115. package/src/browser/connection-warmup.ts +3 -1
  116. package/src/browser/event-controller.ts +42 -3
  117. package/src/browser/history-state.ts +15 -13
  118. package/src/browser/intercept-utils.ts +51 -0
  119. package/src/browser/navigation-bridge.ts +34 -30
  120. package/src/browser/navigation-client.ts +21 -33
  121. package/src/browser/navigation-store.ts +25 -27
  122. package/src/browser/navigation-transaction.ts +7 -11
  123. package/src/browser/partial-update.ts +8 -0
  124. package/src/browser/prefetch/cache.ts +2 -1
  125. package/src/browser/prefetch/fetch.ts +8 -16
  126. package/src/browser/prefetch/queue.ts +4 -4
  127. package/src/browser/react/NavigationProvider.tsx +2 -1
  128. package/src/browser/react/filter-segment-order.ts +2 -2
  129. package/src/browser/rsc-router.tsx +17 -2
  130. package/src/browser/segment-reconciler.ts +66 -53
  131. package/src/browser/server-action-bridge.ts +265 -127
  132. package/src/browser/types.ts +2 -1
  133. package/src/browser/wire-protocol.ts +41 -0
  134. package/src/cache/cache-key-utils.ts +11 -5
  135. package/src/cache/cache-runtime.ts +110 -50
  136. package/src/cache/cache-scope.ts +110 -64
  137. package/src/cache/cf/cf-cache-constants.ts +2 -2
  138. package/src/cache/cf/cf-cache-store.ts +486 -874
  139. package/src/cache/cf/cf-kv-utils.ts +1 -1
  140. package/src/cache/document-cache.ts +42 -15
  141. package/src/cache/handle-snapshot.ts +19 -7
  142. package/src/cache/segment-codec.ts +55 -0
  143. package/src/cache/shell-snapshot.ts +2 -2
  144. package/src/cache/tag-invalidation.ts +21 -2
  145. package/src/cache/taint.ts +5 -3
  146. package/src/cache/types.ts +7 -3
  147. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  148. package/src/handles/pushed-by.ts +1 -1
  149. package/src/prefetch-markers.ts +4 -7
  150. package/src/prerender/store.ts +76 -0
  151. package/src/prerender/warm-request.ts +8 -4
  152. package/src/prerender.ts +2 -2
  153. package/src/route-name.ts +8 -0
  154. package/src/router/error-handling.ts +4 -4
  155. package/src/router/instrument.ts +4 -3
  156. package/src/router/intercept-resolution.ts +77 -75
  157. package/src/router/loader-resolution.ts +5 -4
  158. package/src/router/match-api.ts +4 -5
  159. package/src/router/match-context.ts +33 -12
  160. package/src/router/match-handlers.ts +8 -20
  161. package/src/router/match-middleware/background-revalidation.ts +41 -53
  162. package/src/router/match-middleware/cache-lookup.ts +680 -720
  163. package/src/router/match-middleware/cache-store.ts +14 -12
  164. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  165. package/src/router/match-middleware/segment-resolution.ts +34 -61
  166. package/src/router/match-result.ts +1 -20
  167. package/src/router/middleware.ts +69 -11
  168. package/src/router/navigation-snapshot.ts +4 -3
  169. package/src/router/prerender-match.ts +100 -44
  170. package/src/router/preview-match.ts +6 -41
  171. package/src/router/request-classification.ts +13 -7
  172. package/src/router/revalidation.ts +33 -7
  173. package/src/router/route-snapshot.ts +2 -5
  174. package/src/router/router-context.ts +14 -117
  175. package/src/router/router-interfaces.ts +8 -14
  176. package/src/router/segment-resolution/loader-cache.ts +14 -9
  177. package/src/router/segment-resolution/loader-mask.ts +5 -5
  178. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  179. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  180. package/src/router/segment-resolution/revalidation.ts +897 -776
  181. package/src/router/segment-resolution.ts +4 -13
  182. package/src/router/segment-wrappers.ts +51 -269
  183. package/src/router/telemetry.ts +5 -0
  184. package/src/router/types.ts +10 -0
  185. package/src/router.ts +12 -13
  186. package/src/rsc/full-payload.ts +6 -5
  187. package/src/rsc/handler-context.ts +8 -14
  188. package/src/rsc/handler.ts +126 -507
  189. package/src/rsc/json-route-result.ts +5 -9
  190. package/src/rsc/loader-fetch.ts +6 -5
  191. package/src/rsc/progressive-enhancement.ts +5 -0
  192. package/src/rsc/request-stages.ts +757 -0
  193. package/src/rsc/response-cache-serve.ts +10 -14
  194. package/src/rsc/response-route-handler.ts +13 -5
  195. package/src/rsc/rewrite-passes.ts +2 -1
  196. package/src/rsc/routine-plan.ts +1 -1
  197. package/src/rsc/rsc-rendering.ts +172 -349
  198. package/src/rsc/server-action.ts +87 -113
  199. package/src/rsc/shell-capture-constants.ts +3 -1
  200. package/src/rsc/shell-capture.ts +212 -169
  201. package/src/rsc/shell-serve.ts +121 -18
  202. package/src/rsc/ssr-setup.ts +5 -4
  203. package/src/segment-system.tsx +3 -3
  204. package/src/server/context.ts +22 -25
  205. package/src/server/handle-store.ts +55 -0
  206. package/src/server/render-pass.ts +291 -0
  207. package/src/server/request-context.ts +26 -115
  208. package/src/server/rewrite-trail.ts +6 -4
  209. package/src/ssr/index.tsx +1 -1
  210. package/src/ssr/ssr-root.tsx +1 -0
  211. package/src/testing/dispatch.ts +223 -710
  212. package/src/testing/e2e/page-helpers.ts +2 -2
  213. package/src/testing/serve-shell-request.ts +37 -32
  214. package/src/types/cache-types.ts +6 -3
  215. package/src/types/error-types.ts +9 -1
  216. package/src/types/handler-context.ts +21 -7
  217. package/src/types/segments.ts +1 -1
  218. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  219. package/src/vite/discovery/build-env.ts +148 -0
  220. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  221. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  222. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  223. package/src/vite/discovery/discover-routers.ts +1 -1
  224. package/src/vite/discovery/discovery-errors.ts +1 -1
  225. package/src/vite/discovery/prerender-collection.ts +24 -39
  226. package/src/vite/discovery/state.ts +15 -0
  227. package/src/vite/plugins/version-plugin.ts +1 -1
  228. package/src/vite/plugins/virtual-entries.ts +1 -1
  229. package/src/vite/router-discovery.ts +29 -2428
  230. package/src/vite/temp-rsc-server.ts +223 -0
  231. package/src/vite/utils/prerender-utils.ts +47 -0
  232. package/src/wire-params.ts +35 -0
  233. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  234. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -4,9 +4,9 @@
4
4
  * Capture does NOT flow through the HTTP middleware pipeline. The integrated PPR
5
5
  * serve path (rsc-rendering.ts + shell-serve.ts) builds a ShellCaptureDescriptor
6
6
  * from the route's `ppr` path option after the served response is built and calls
7
- * scheduleShellCapture. The capture then runs as a background task that re-derives
7
+ * planShellCapture with it (ShellCaptureInput). The capture then runs as a background task that re-derives
8
8
  * the page via `ctx.router.match()` under its OWN derived request context — fresh
9
- * handle store, `_shellCaptureRun: true` so live-lane loaders mask (loader-mask.ts)
9
+ * handle store, `_shellCaptureState.run` so live-lane loaders mask (loader-mask.ts)
10
10
  * and postpone at their loading() or inline <Suspense> boundary. The match is
11
11
  * MIXED-CHAIN: cache()'d segments replay from ring 3, uncached segments execute
12
12
  * their handlers. The record-first step (settleCaptureRecord) waits for the
@@ -50,11 +50,10 @@ import type { TraceSpan } from "../router/tracing.js";
50
50
  import {
51
51
  runWithRequestContext,
52
52
  setRequestContextParams,
53
- wireRenderBarrier,
54
53
  UNTRACKED_BACKGROUND_TASK,
55
54
  type RequestContext,
56
- type RouteRecordWindow,
57
55
  } from "../server/request-context.js";
56
+ import { deriveRenderPass } from "../server/render-pass.js";
58
57
  import {
59
58
  DEFAULT_FUNCTION_TTL,
60
59
  resolveSwrWindow,
@@ -149,7 +148,7 @@ const SHELL_SNAPSHOT_WRITE_SETTLE_MS = 1000;
149
148
  * Upper bound on the pre-render WRITE BARRIER: before the capture's match/render,
150
149
  * settle the background tasks the FOREGROUND request already scheduled — its
151
150
  * deferred ring-3 cacheRoute and ring-1 setItem writes all go through
152
- * reqCtx.waitUntil, and every one of them is scheduled BEFORE scheduleShellCapture
151
+ * reqCtx.waitUntil, and every one of them is scheduled BEFORE planShellCapture
153
152
  * runs (the response, and its onResponse callbacks, are committed first). Draining
154
153
  * them turns the capture's cache reads from a RACE into an ORDERING EDGE: the
155
154
  * capture deterministically observes the foreground's cache generation, replays it
@@ -226,7 +225,7 @@ async function raceTaskHardCap<T>(task: Promise<T>, key: string): Promise<T> {
226
225
  * Module-level in-flight key map: the stampede guard for background captures, and
227
226
  * its single owner. One capture runs per key per isolate; concurrent MISS/stale
228
227
  * requests for the same key coalesce onto the first (the rest see the key present
229
- * in scheduleShellCapture and skip). Added when a capture is scheduled and cleared
228
+ * in planShellCapture and skip). Added when a capture is scheduled and cleared
230
229
  * in the task's settle paths via a token-guarded release, so a later request can
231
230
  * recapture when TTL rolls. Living here (not split across the middleware) keeps
232
231
  * the add/clear lifecycle in one layer.
@@ -234,7 +233,7 @@ async function raceTaskHardCap<T>(task: Promise<T>, key: string): Promise<T> {
234
233
  * The value is the owning task's token ({@link CaptureGuardToken}) rather than
235
234
  * a bare set entry: workerd can kill a capture's waitUntil context before its
236
235
  * finally runs (or the task can wedge past every deadline), stranding the key.
237
- * scheduleShellCapture treats an entry older than
236
+ * planShellCapture treats an entry older than
238
237
  * SHELL_CAPTURE_TASK_HARD_CAP_MS as abandoned and reclaims it; the token guard
239
238
  * keeps a late-settling stale task from releasing its replacement's entry.
240
239
  */
@@ -474,14 +473,14 @@ function refuseOnCaptureGuard(
474
473
  );
475
474
  return true;
476
475
  }
477
- const trip = derivedCtx._shellCaptureGuardTripped;
476
+ const trip = derivedCtx._shellCaptureState?.guardTripped;
478
477
  if (!trip) return false;
479
478
  // Name the recorded source instead of hardcoding a lane: a trip can come
480
479
  // from a bake-lane SEGMENT loader or from handler/render code, including a
481
480
  // loader a handler awaits (issue #672). The guard recorded the read and its
482
481
  // fix (server/context.ts guardIdentityRead): cookies()/headers(), a
483
482
  // { cache: false } variable, and the theme reads (#971).
484
- const loaderId = derivedCtx._shellCaptureGuardTrippedLoaderId;
483
+ const loaderId = derivedCtx._shellCaptureState?.guardTrippedLoaderId;
485
484
  const origin = loaderId
486
485
  ? `the loader "${loaderId}"`
487
486
  : "handler/render code (no loader body was executing)";
@@ -625,7 +624,7 @@ export interface ShellCaptureDebugEvent {
625
624
  * - skip-stored: a capture for the key stored after the scheduling request
626
625
  * read the store and missed (ShellCaptureDescriptor.storedSeqAtRead), so
627
626
  * the shell that request missed exists now
628
- * - skip-in-flight: scheduleShellCapture found a capture already running for
627
+ * - skip-in-flight: planShellCapture found a capture already running for
629
628
  * the key (stampede guard) and scheduled nothing
630
629
  * - skip-backoff: the key is inside its refused-capture backoff window and
631
630
  * the capture was not attempted
@@ -998,7 +997,7 @@ export function gateFlightForCapture(
998
997
  * The background shell-capture descriptor: everything the capture task needs to
999
998
  * store the shell. Built by the integrated PPR serve path (rsc-rendering.ts) from
1000
999
  * the route's `ppr` path option (`PartialPrerenderProps`) and the app-level cache
1001
- * store, and passed to scheduleShellCapture directly — it is NOT threaded through
1000
+ * store, and passed to planShellCapture in its input — it is NOT threaded through
1002
1001
  * the request context. `tags` carries the route's OPERATIONAL `ppr.tags`; the
1003
1002
  * capture UNIONS them with the shell's own auto-collected (non-loader) request
1004
1003
  * tags from its derived render (the collected set stays authoritative). That
@@ -1066,7 +1065,7 @@ export interface ShellCaptureDescriptor {
1066
1065
  /**
1067
1066
  * Set for a shell keyed by the visitor's URL (`rewrite(...).shell("visitor")`,
1068
1067
  * visitorShellKey): the capture renders the visitor's location
1069
- * (`RequestContext._shellCaptureForVisitor`) and stores it as
1068
+ * (`ShellCaptureState.forVisitor`) and stores it as
1070
1069
  * ShellCacheEntry.pathname.
1071
1070
  */
1072
1071
  pathname?: string;
@@ -1081,7 +1080,7 @@ export interface ShellCaptureDescriptor {
1081
1080
  /**
1082
1081
  * A `router.prerender()` warm's capture (rsc-rendering.ts shellServePlan):
1083
1082
  * one explicit call that must store, so the guards that shed a herd of
1084
- * visitors do not apply to it. scheduleShellCapture skips the in-flight
1083
+ * visitors do not apply to it. planShellCapture skips the in-flight
1085
1084
  * dedup and the refused-capture backoff, and attemptCapture waits longer on
1086
1085
  * the write barrier. Every correctness guard, the inert-store skip and the
1087
1086
  * queue limits still decide.
@@ -1108,6 +1107,25 @@ function noteCaptureStored(key: string): void {
1108
1107
  storedCaptureSeqs.set(key, ++storedCaptureSeq);
1109
1108
  }
1110
1109
 
1110
+ /** One shell capture: the request it re-renders and what it stores. */
1111
+ export interface ShellCaptureInput {
1112
+ ctx: HandlerContext<any>;
1113
+ request: Request;
1114
+ env: any;
1115
+ url: URL;
1116
+ reqCtx: RequestContext<any>;
1117
+ /**
1118
+ * The SSR module for the capture's own request and URL, called once the
1119
+ * capture leaves the queue. A document passes the module it already
1120
+ * resolved, returned as is, so the capture awaits only a pending one.
1121
+ */
1122
+ ssr: (
1123
+ request: Request,
1124
+ url: URL,
1125
+ ) => SSRModule | null | Promise<SSRModule | null>;
1126
+ descriptor: ShellCaptureDescriptor;
1127
+ }
1128
+
1111
1129
  /**
1112
1130
  * Schedule the background shell capture for a served document. Stampede-guarded:
1113
1131
  * one capture per key per isolate. Runs via runBackground (waitUntil on workerd,
@@ -1115,21 +1133,10 @@ function noteCaptureStored(key: string): void {
1115
1133
  * error is routed through reportCacheError — capture is best-effort; a failure just
1116
1134
  * means the next request recaptures.
1117
1135
  *
1118
- * Eligibility (nonce/partial/status/strategy) is decided by the caller. An SSR
1119
- * module loader may be passed for cold partial requests; it runs only after the
1120
- * capture enters the guarded background queue, never on response latency.
1136
+ * Eligibility (nonce/partial/status/strategy) is decided by the caller.
1121
1137
  */
1122
- export function scheduleShellCapture(
1123
- ctx: HandlerContext<any>,
1124
- request: Request,
1125
- env: any,
1126
- url: URL,
1127
- reqCtx: RequestContext<any>,
1128
- ssrModule:
1129
- | SSRModule
1130
- | ((request: Request, url: URL) => Promise<SSRModule | null>),
1131
- descriptor: ShellCaptureDescriptor,
1132
- ): void {
1138
+ export function planShellCapture(input: ShellCaptureInput): void {
1139
+ const { ctx, request, env, url, reqCtx, descriptor } = input;
1133
1140
  const key = descriptor.key;
1134
1141
  // A forced capture installs its own token below: a visitor's capture that
1135
1142
  // started before the warm may have read data from before the change the
@@ -1191,10 +1198,9 @@ export function scheduleShellCapture(
1191
1198
  const captureRequest = descriptor.navigationOnly
1192
1199
  ? createNavigationCaptureRequest(request, captureUrl)
1193
1200
  : request;
1201
+ const ssrModule = input.ssr(captureRequest, captureUrl);
1194
1202
  const resolvedSsrModule =
1195
- typeof ssrModule === "function"
1196
- ? await ssrModule(captureRequest, captureUrl)
1197
- : ssrModule;
1203
+ ssrModule instanceof Promise ? await ssrModule : ssrModule;
1198
1204
  if (
1199
1205
  !resolvedSsrModule ||
1200
1206
  !resolvedSsrModule.resumeShellHTML ||
@@ -1350,7 +1356,7 @@ export function scheduleShellCapture(
1350
1356
  /**
1351
1357
  * The ttl/swr a shell is stored with: the route's ppr policy, capped by the
1352
1358
  * route cache() records the capture replayed or wrote
1353
- * (RequestContext._routeRecordWindow). A HIT replays the capture's handler
1359
+ * (ShellCaptureState.routeRecordWindow). A HIT replays the capture's handler
1354
1360
  * output, while a client navigation reads the route's cache() tier first, so
1355
1361
  * a shell that outlived its record would show a document request other
1356
1362
  * handler output than a navigation of the same URL. Freshness and total
@@ -1518,9 +1524,9 @@ type CaptureAttemptStats = Pick<
1518
1524
  * several HTTP requests before a capture stuck. We retry ONLY on `no-shell`
1519
1525
  * that did not run out of ppr.captureTimeout (CaptureAttemptStats.noShellCause),
1520
1526
  * and on `expired`; a genuine render error is NOT retried — it propagates to
1521
- * scheduleShellCapture's reportCacheError. See docs/design/ppr-shell-resume.md.
1527
+ * planShellCapture's reportCacheError. See docs/design/ppr-shell-resume.md.
1522
1528
  *
1523
- * `request` and `url` are the capture's own identity (scheduleShellCapture
1529
+ * `request` and `url` are the capture's own identity (planShellCapture
1524
1530
  * strips a navigation-only capture's transport parameters). `retryDelayMs` is
1525
1531
  * a parameter (defaulting to the module const) so unit tests can drive the
1526
1532
  * retry without a real 400ms wall-clock wait.
@@ -1541,7 +1547,7 @@ async function runShellCapture(
1541
1547
  // attemptCapture/captureAndStoreShell collecting the observability fields
1542
1548
  // (barrier wait, write-settle wait, prelude/snapshot bytes), and the event
1543
1549
  // folds them with the outcome. A genuine render error skips the attempt
1544
- // event — scheduleShellCapture's catch publishes the terminal `error` event.
1550
+ // event — planShellCapture's catch publishes the terminal `error` event.
1545
1551
  const timedAttempt = async (
1546
1552
  attempt: number,
1547
1553
  ): Promise<{
@@ -1626,7 +1632,7 @@ async function runShellCapture(
1626
1632
  // lane rule: see resolveLoaderData, loader-cache.ts) is the likely cause —
1627
1633
  // warn once per key. Ordering matters: because the retry
1628
1634
  // absorbs cold-start, cold-start routes almost never reach this warning. The
1629
- // caller (scheduleShellCapture) reads this `no-shell` return to back the key off.
1635
+ // caller (planShellCapture) reads this `no-shell` return to back the key off.
1630
1636
  warnNullCaptureOnce(key, true, second.stats);
1631
1637
  return "no-shell";
1632
1638
  }
@@ -1673,7 +1679,8 @@ export type CaptureRecordOutcome =
1673
1679
  * a HIT restores, and its handle encode has its own 5s timeout that must
1674
1680
  * never be the one that fires); then the capture's onResponse callbacks fire
1675
1681
  * with a synthetic 200 (the implicit doc scope's cacheRoute, an explicit
1676
- * scope's own write), and the deferred writes settle.
1682
+ * scope's own write), and the deferred writes settle. A match holding an
1683
+ * error or notFound boundary throws before the callbacks fire.
1677
1684
  */
1678
1685
  export async function settleCaptureRecord(
1679
1686
  match: MatchResult,
@@ -1690,7 +1697,7 @@ export async function settleCaptureRecord(
1690
1697
  // (ctx.use(Handle).defer()), so a defer whose resolver depends on a masked
1691
1698
  // loader never settles and the wait below runs into the deadline instead.
1692
1699
  freshHandleStore.seal();
1693
- const loaderRecords = derivedCtx._shellCaptureLoaderRecords;
1700
+ const loaderRecords = derivedCtx._shellCaptureState?.loaderRecords;
1694
1701
  const settled = await raceDeadline(
1695
1702
  Promise.all([
1696
1703
  freshHandleStore
@@ -1741,6 +1748,32 @@ export async function settleCaptureRecord(
1741
1748
  return { kind: "refused", refusal: "loader" };
1742
1749
  }
1743
1750
 
1751
+ // A boundary the match rendered in place of a handler (route, layout or
1752
+ // slot) that threw, or threw notFound(), on this pass. A MISS whose own
1753
+ // render failed answered 500/404 and scheduled no capture, so on a first
1754
+ // capture the failure is this pass's own (a flaky upstream); a recapture
1755
+ // with no visitor render behind it (stale HIT, warm, stale build shell)
1756
+ // can only have failed. The pass's _setStatus is the foreground's and the
1757
+ // callbacks below get a synthetic 200, so nothing else refuses it. A stale
1758
+ // shell whose recapture fails keeps serving, as a failed cache() refresh's
1759
+ // record does: until ttl + swr ends (a build-time shell, until a recapture
1760
+ // stores or a tag evicts it). Checked after the settle so a guard trip in
1761
+ // a handler or a loader names the read, and before any write starts.
1762
+ // Thrown like captureAndStoreShell's render errors: no retry,
1763
+ // reportCacheError and backoff in scheduleShellCapture.
1764
+ const boundary = match.segments.find(
1765
+ (s) => s.type === "error" || s.type === "notFound",
1766
+ );
1767
+ if (boundary) {
1768
+ if (refuseOnCaptureGuard(capture.key, derivedCtx))
1769
+ return { kind: "refused", refusal: guardRefusal(derivedCtx) };
1770
+ throw new Error(
1771
+ `[rango] Shell capture for "${capture.key}" rendered the ${boundary.type} ` +
1772
+ `boundary for segment "${boundary.id}"; a failed render is not stored.`,
1773
+ { cause: boundary.error ?? boundary.notFoundInfo },
1774
+ );
1775
+ }
1776
+
1744
1777
  const callbacks = derivedCtx._onResponseCallbacks?.splice(0) ?? [];
1745
1778
  if (callbacks.length > 0) {
1746
1779
  const synthetic = new Response(null, { status: 200 });
@@ -1770,7 +1803,7 @@ export async function settleCaptureRecord(
1770
1803
  !Array.isArray(record.segments) ||
1771
1804
  record.segments.length === 0
1772
1805
  ) {
1773
- // Post-match serve-source truth (cache-lookup.ts tryPrerenderLookup).
1806
+ // Post-match serve-source truth (cache-lookup.ts decodeStoredEntry).
1774
1807
  if (derivedCtx._pprReplayPostMatchReason === "prerender-store") {
1775
1808
  return { kind: "prerender", match };
1776
1809
  }
@@ -1838,25 +1871,10 @@ function recordSegmentsHaveHandles(
1838
1871
  /**
1839
1872
  * One capture attempt in a DERIVED request context.
1840
1873
  *
1841
- * The derived context is `Object.create(reqCtx)` so it inherits the foreground's
1842
- * post-middleware state (variables, cache store, env/request/url, waitUntil) while
1843
- * overriding the render-scoped accumulators as own properties:
1844
- * - _handleStore: a fresh store. The foreground store is already drained to
1845
- * completion (its stream() flipped `completed` on settle) and would throw
1846
- * LateHandlePushError on any re-push. Every downstream reader resolves the
1847
- * store off the ambient context (setupLoaderAccess captures
1848
- * _getRequestContext()._handleStore; trackHandler reads it), so the fresh
1849
- * store on the derived context is what the capture match() writes handles to.
1850
- * - _requestTags: a fresh Set. The capture collects its OWN shell tags here —
1851
- * non-loader tags only, since loaders are masked — which is exactly the tag
1852
- * set a shell entry should be invalidatable by (loader tags belong to holes).
1853
- * - _shellCaptureRun: true — the switch loaders/cookies/headers guards read.
1854
- * - _dynamic / dynamic(): an own latch the capture reads.
1855
- * - _metricsStore: undefined so the capture never appends to the foreground's
1856
- * (already-finalized) metrics.
1857
- * - _renderBarrier family: an own barrier wired to the fresh handle store
1858
- * (wireRenderBarrier), plus _treeHasStreaming/deadlock-guard resets — the
1859
- * capture's rendered() lifecycle is its own, not the foreground's.
1874
+ * The derived context (deriveRenderPass, server/render-pass.ts) inherits the
1875
+ * foreground's post-middleware state (variables, cache store, env/request/url,
1876
+ * waitUntil) and owns the render-scoped accumulators, its render barrier
1877
+ * included: RENDER_PASS_FIELDS["shell-capture"] lists each and why.
1860
1878
  *
1861
1879
  * The capture is MIXED-CHAIN: its match() behaves like a normal render with
1862
1880
  * respect to the segment cache — cache()'d segments replay from ring 3, UNCACHED
@@ -1913,7 +1931,7 @@ async function attemptCapture(
1913
1931
  );
1914
1932
  const { derivedCtx, freshHandleStore } = derivation;
1915
1933
  if (descriptor.pathname !== undefined) {
1916
- derivedCtx._shellCaptureForVisitor = true;
1934
+ derivedCtx._shellCaptureState!.forVisitor = true;
1917
1935
  }
1918
1936
  // The capture generation starts before matching or any snapshot read
1919
1937
  // (ShellCacheEntry.createdAt): a tag invalidated after it wins.
@@ -2014,6 +2032,115 @@ async function attemptCapture(
2014
2032
  });
2015
2033
  }
2016
2034
 
2035
+ /**
2036
+ * @internal The freshness window of a route cache() record, as ms
2037
+ * timestamps: fresh until `freshUntil` (its `expiresAt`), stale until
2038
+ * `staleUntil` (plus its scope's swr). `ttl`, `swr` and `written` describe
2039
+ * the record that sets `staleUntil`: its cache() config, and whether this
2040
+ * capture wrote it (cacheRoute) rather than read it.
2041
+ */
2042
+ export interface RouteRecordWindow {
2043
+ freshUntil: number;
2044
+ staleUntil: number;
2045
+ ttl: number;
2046
+ swr: number;
2047
+ written: boolean;
2048
+ }
2049
+
2050
+ /**
2051
+ * @internal What a shell capture's render records about itself, on its
2052
+ * derived request context as `_shellCaptureState`.
2053
+ * RENDER_PASS_FIELDS["shell-capture"] (server/render-pass.ts) creates it with
2054
+ * every field present, so it keeps one shape while the capture writes it. A
2055
+ * background re-render the capture starts has none: it writes a route
2056
+ * record, under cache() rules (RENDER_PASS_FIELDS["background-revalidation"]).
2057
+ */
2058
+ export interface ShellCaptureState {
2059
+ /**
2060
+ * @internal PPR shell-capture ACTIVE marker. True ONLY inside the background
2061
+ * capture task's derived request context (built by shell-capture.ts). This is
2062
+ * the switch every capture-specific behavior reads: loader masking
2063
+ * (loader-mask.ts isShellCaptureActive / resolveOwnerLoaders) and the
2064
+ * identity-read capture guard (server/context.ts guardIdentityRead). The
2065
+ * foreground render never sets it, so the
2066
+ * served response is byte-identical to axis 1. The capture descriptor itself
2067
+ * (key/ttl/swr/tags/store) is NOT threaded through the request context — the
2068
+ * integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
2069
+ * locally and passes it to planShellCapture in its input.
2070
+ */
2071
+ readonly run: true;
2072
+
2073
+ /**
2074
+ * @internal Set beside `run` when the shell is keyed by the
2075
+ * visitor's URL (`rewrite(...).shell("visitor")`): the capture renders the
2076
+ * visitor's location and `ctx.rewrittenFrom`; the theme stays the default.
2077
+ */
2078
+ forVisitor: boolean | undefined;
2079
+
2080
+ /**
2081
+ * @internal Bake-lane loader containers collected DURING a shell capture:
2082
+ * segment-key -> the loader's (pre-wrap) result promise. Populated by
2083
+ * resolveLoaderData for bake-lane loaders (lane rule: see its JSDoc in
2084
+ * loader-cache.ts). Drained by captureAndStoreShell after the shell
2085
+ * quiesces: settled containers are promise-elided, Flight-serialized, and
2086
+ * pinned into the snapshot's loader family; a REJECTED container refuses
2087
+ * the capture (error UI must never bake into the shared shell).
2088
+ */
2089
+ loaderRecords: Map<string, Promise<unknown>>;
2090
+
2091
+ /**
2092
+ * @internal Set when a loader pushed a
2093
+ * value the record cannot keep (masked nested promises, a pushed promise
2094
+ * that settled to one, a live-lane loader's push, or a push outside a
2095
+ * loader body): the loader records then carry `runs: 1`, so a HIT still
2096
+ * runs their bodies for those pushes.
2097
+ */
2098
+ unrecordedLoaderPush: true | undefined;
2099
+
2100
+ /**
2101
+ * @internal The lane of each loader the capture's matched route registers
2102
+ * (loader-cache.ts routeLoaderLanes), set by createMatchContextForFull
2103
+ * before any loader runs. The capture's push funnel credits a push made
2104
+ * under a live-lane loader's body, at any depth, to that loader
2105
+ * (shell-capture.ts deriveShellCaptureContext).
2106
+ */
2107
+ loaderLanes: ReadonlyMap<string, "live" | "bake"> | undefined;
2108
+
2109
+ /**
2110
+ * @internal Set by the capture guard (server/context.ts
2111
+ * guardIdentityRead) when it throws DURING a capture render: the read
2112
+ * ("cookies()", "headers()", `ctx.get() for a non-cacheable variable`,
2113
+ * "ctx.theme") and the fix the
2114
+ * refusal warning gives for it. Load-bearing for the
2115
+ * bake lane: a guard throw inside an executing loader is swallowed by
2116
+ * wrapLoaderPromise into per-loader error UI, which would otherwise bake
2117
+ * silently into the shared shell — the capture checks this flag after the
2118
+ * render and refuses instead. Deterministic, so the capture does not retry.
2119
+ */
2120
+ guardTripped: { surface: string; fix: string } | undefined;
2121
+
2122
+ /**
2123
+ * @internal The loader $$id whose BODY was executing when the capture guard
2124
+ * tripped (read off the loader-body ALS scope at trip time), or undefined
2125
+ * when the read came from handler/render code. Only used to make the
2126
+ * once-per-key refusal warning name the real source — the old text
2127
+ * hardcoded "a bake-lane loader", which misattributed handler-land reads
2128
+ * and sent users debugging the wrong lane (issue #672, secondary).
2129
+ */
2130
+ guardTrippedLoaderId: string | undefined;
2131
+
2132
+ /**
2133
+ * @internal The freshness window of the route cache() records this
2134
+ * capture read or wrote (cache-scope.ts noteRouteRecordWindow), as ms
2135
+ * timestamps: `freshUntil` is the record's `expiresAt`, `staleUntil` adds
2136
+ * its scope's swr. The narrowest record wins. A shell capture caps its
2137
+ * entry's ttl and ttl+swr to it (shell-capture.ts capShellWindow), so a
2138
+ * shell is never fresher, or served longer, than the route cache() entry
2139
+ * it replayed or wrote.
2140
+ */
2141
+ routeRecordWindow: RouteRecordWindow | undefined;
2142
+ }
2143
+
2017
2144
  /**
2018
2145
  * The derived capture context and its fresh (mask-funneled) handle store,
2019
2146
  * shared by BOTH shell producers: the runtime background capture
@@ -2123,7 +2250,7 @@ export function deriveShellCaptureContext(
2123
2250
  // A replay of a live-lane loader's push (a "use cache" or loader cache()
2124
2251
  // hit a recapture makes inside a bake-lane body) is that hole's too.
2125
2252
  const liveLaneOwner = (): string | undefined => {
2126
- const lanes = derivedCtx._shellCaptureLoaderLanes;
2253
+ const lanes = captureState.loaderLanes;
2127
2254
  if (!lanes) return undefined;
2128
2255
  return (
2129
2256
  findEnclosingLoaderBody(
@@ -2190,7 +2317,7 @@ export function deriveShellCaptureContext(
2190
2317
  const judged = mask(settled);
2191
2318
  if (maskedNestedThenable) {
2192
2319
  freshHandleStore.unrecordPush(handleName, segmentId, slot);
2193
- derivedCtx._shellCaptureUnrecordedLoaderPush = true;
2320
+ captureState.unrecordedLoaderPush = true;
2194
2321
  }
2195
2322
  return judged;
2196
2323
  });
@@ -2220,7 +2347,7 @@ export function deriveShellCaptureContext(
2220
2347
  // A push the record cannot keep reaches a HIT only from a live run of
2221
2348
  // its loader, so the loader records ask for one (runs: 1).
2222
2349
  const loaderPush = owner === undefined;
2223
- if (loaderPush) derivedCtx._shellCaptureUnrecordedLoaderPush = true;
2350
+ if (loaderPush) captureState.unrecordedLoaderPush = true;
2224
2351
  rawCapturePush(handleName, segmentId, masked, loaderPush, owner);
2225
2352
  };
2226
2353
 
@@ -2229,111 +2356,25 @@ export function deriveShellCaptureContext(
2229
2356
  // request before the derivation, or the capture would start its own and
2230
2357
  // run key() under the capture guard.
2231
2358
  reqCtx._resolvedCacheKeys ??= new Map();
2232
- const derivedCtx: RequestContext = Object.assign(Object.create(reqCtx), {
2233
- _handleStore: freshHandleStore,
2234
- // The capture notes the route records it reads or writes itself; the
2235
- // foreground's (a partial request's `partial:` record, say) are not the
2236
- // ones this shell is captured from.
2237
- _routeRecordWindow: undefined,
2359
+ const recordingStore = reqCtx._cacheStore
2360
+ ? new RecordingShellStore(reqCtx._cacheStore)
2361
+ : undefined;
2362
+ // What the capture owns, and why: RENDER_PASS_FIELDS["shell-capture"].
2363
+ const derivedCtx: RequestContext = deriveRenderPass(reqCtx, "shell-capture", {
2364
+ handleStore: freshHandleStore,
2365
+ identity,
2366
+ recording: recordingStore && {
2367
+ store: recordingStore,
2368
+ implicitCache: {
2369
+ ttl: descriptor.ttl,
2370
+ swr: descriptor.swr,
2371
+ store: new SnapshotOnlySegmentStore(recordingStore),
2372
+ keyPrefix: "doc",
2373
+ },
2374
+ },
2238
2375
  });
2239
- if (identity) {
2240
- derivedCtx.request = identity.request;
2241
- derivedCtx.url = identity.url;
2242
- derivedCtx.originalUrl = new URL(identity.url);
2243
- derivedCtx.pathname = identity.url.pathname;
2244
- derivedCtx.searchParams = identity.url.searchParams;
2245
- }
2246
- // Own render barrier, closure-bound to the derived ctx and the fresh store
2247
- // (issue #684, plan 009). Without this every _renderBarrier* read fell
2248
- // through the prototype to the foreground's ALREADY-RESOLVED barrier: a
2249
- // bake-lane loader's `await ctx.rendered()` resolved instantly and
2250
- // ctx.use(handle) read the FOREGROUND handle snapshot — foreground
2251
- // per-request handle data could bake into the shared shell. wireRenderBarrier
2252
- // also resets _treeHasStreaming (recomputed for the capture's tree) and the
2253
- // deadlock-guard fields as own properties.
2254
- wireRenderBarrier(derivedCtx, freshHandleStore);
2255
- derivedCtx._requestTags = new Set<string>();
2256
- // Own explicit-store registry: cache-store resolutions during the capture
2257
- // (the implicit scope's SnapshotOnlySegmentStore, any per-capture explicit
2258
- // store instance) must NOT register into the handler-lifetime
2259
- // _explicitTaggedStores set — a capture-ephemeral store pinned there would
2260
- // trip the partial-tag-store warning on every later updateTag() and retain
2261
- // the whole capture snapshot in memory. Capture registrations die with this
2262
- // context; module-singleton stores stay registered by normal renders.
2263
- derivedCtx._explicitTaggedStores = new Set();
2264
- // Own list: the capture's render errors refuse the capture only, and the
2265
- // foreground's never reach it.
2266
- derivedCtx._renderErrors = [];
2267
- derivedCtx._recoveredHandlerErrors = undefined;
2268
- derivedCtx._shellCaptureRun = true;
2269
- // Own dynamic() latch. The inherited method writes the FOREGROUND context
2270
- // (its closure), which already served, and nothing read it for a runtime
2271
- // capture: handler code that opts out after an await (a handler promise the
2272
- // capture now waits for) must refuse THIS capture instead
2273
- // (settleCaptureRecord and captureAndStoreShell read _dynamic). The ppr
2274
- // header latch is left alone: a capture's response is never sent.
2275
- derivedCtx._dynamic = false;
2276
- derivedCtx.dynamic = (): void => {
2277
- derivedCtx._dynamic = true;
2278
- };
2279
- // Own-property reset: an inherited flag would make serializeSegments store
2280
- // a double-encoded fragment. No current arming site reaches a capture
2281
- // render (kept as defense-in-depth for a future one).
2282
- derivedCtx._shellFragmentPayload = false;
2283
- derivedCtx._metricsStore = undefined;
2284
- // Spans, like perf metrics above, are a FOREGROUND surface: the capture
2285
- // re-render must not emit a second rango.render/loader/ssr set after the
2286
- // foreground rango.request span ended (orphan spans in the trace).
2287
- // _tracing is otherwise inherited through Object.create(reqCtx).
2288
- derivedCtx._tracing = undefined;
2289
- // Bake-lane (`ssr: false`) loader containers, which execute during capture
2290
- // (docs/design/loader-container-bake.md). resolveLoaderData registers each
2291
- // container promise here; settleCaptureRecord waits for them, and the drain
2292
- // in captureAndStoreShell elides + pins them into the snapshot's loader
2293
- // family.
2294
- derivedCtx._shellCaptureLoaderRecords = new Map();
2295
- // Own onResponse list so the capture's match-middleware callbacks (the doc
2296
- // record registers here) are ISOLATED from the foreground's shared array AND
2297
- // can be fired by settleCaptureRecord. The segment write is gated behind
2298
- // onResponse, which the capture never triggers (it builds no Response):
2299
- // without the synthetic fire, no doc record is written.
2300
- derivedCtx._onResponseCallbacks = [];
2301
-
2302
- // Capture data snapshot: route the capture's cache-store calls through a
2303
- // recording wrapper on the DERIVED context's store (own property, so the
2304
- // shared reqCtx._cacheStore is untouched — the snapshot is per-capture). It
2305
- // records the doc segment record, which rides inside the ShellCacheEntry
2306
- // with the bake-lane loader pins; no cache read is recorded. See
2307
- // cache/shell-snapshot.ts and the design doc.
2308
- //
2309
- // Cache writes are deferred (waitUntil): the doc record's write would
2310
- // otherwise land after the match returns. Override the derived context's
2311
- // waitUntil to COLLECT those write promises (still forwarding to the parent
2312
- // so the write persists and the worker stays alive), then
2313
- // settleCaptureRecord and captureAndStoreShell await them before reading
2314
- // the record and draining.
2315
- if (reqCtx._cacheStore) {
2316
- const recordingStore = new RecordingShellStore(reqCtx._cacheStore);
2317
- derivedCtx._cacheStore = recordingStore;
2318
- derivedCtx.waitUntil = (fn: () => Promise<void>): void => {
2319
- const p = Promise.resolve().then(fn);
2320
- recordingStore.trackWrite(p);
2321
- reqCtx.waitUntil(() => p);
2322
- };
2323
- // The doc record (capture side): the implicit doc-cache scope makes the
2324
- // capture's match write ALL matched non-loader segments as one doc-keyed
2325
- // segment record — into the snapshot only (SnapshotOnlySegmentStore), so
2326
- // the record dies with the shell entry and the next capture's lookup
2327
- // still misses (handlers re-run on recapture). A route deriving its own
2328
- // cache() scope keeps it, and recordShellCaptureDocRecord (cache-store.ts)
2329
- // writes the same record for it.
2330
- derivedCtx._shellImplicitCache = {
2331
- ttl: descriptor.ttl,
2332
- swr: descriptor.swr,
2333
- store: new SnapshotOnlySegmentStore(recordingStore),
2334
- keyPrefix: "doc",
2335
- };
2336
- }
2376
+ // The row creates it; the push funnel above writes it.
2377
+ const captureState = derivedCtx._shellCaptureState!;
2337
2378
 
2338
2379
  return {
2339
2380
  derivedCtx,
@@ -2479,7 +2520,7 @@ async function captureAndStoreShell(
2479
2520
  // container still pending here either pinned the tree (the trivial-prelude
2480
2521
  // gate above already returned no-shell) or postponed under an ANCESTOR
2481
2522
  // boundary (it is a hole; omitting the record keeps it live).
2482
- const loaderRecords = reqCtx._shellCaptureLoaderRecords;
2523
+ const loaderRecords = reqCtx._shellCaptureState?.loaderRecords;
2483
2524
  // Set once a bake-lane loader settles with real (non-hole) material: its data
2484
2525
  // is frozen into the shell prelude regardless of whether snapshot
2485
2526
  // serialization succeeds. Drives the opt-in debug metadata below.
@@ -2492,7 +2533,9 @@ async function captureAndStoreShell(
2492
2533
  | typeof import("../cache/segment-codec.js").serializeResult
2493
2534
  | undefined;
2494
2535
  // Capture-wide, read once: every record of this snapshot agrees.
2495
- const runs: 0 | 1 = reqCtx._shellCaptureUnrecordedLoaderPush ? 1 : 0;
2536
+ const runs: 0 | 1 = reqCtx._shellCaptureState?.unrecordedLoaderPush
2537
+ ? 1
2538
+ : 0;
2496
2539
  for (const [segmentKey, containerPromise] of loaderRecords) {
2497
2540
  const elided = await elideLoaderContainer(containerPromise);
2498
2541
  if (elided.state === "rejected") {
@@ -2583,7 +2626,7 @@ async function captureAndStoreShell(
2583
2626
  // The drain's container encode adds its Flight errors here too (#927).
2584
2627
  // After the loader drain so a rejected bake-lane loader keeps its specific
2585
2628
  // refusal. Thrown like a fatal shell error: no retry, reportCacheError and
2586
- // backoff in scheduleShellCapture.
2629
+ // backoff in planShellCapture.
2587
2630
  const renderErrors = reqCtx._renderErrors;
2588
2631
  if (renderErrors && renderErrors.length > 0) throw renderErrors[0];
2589
2632
 
@@ -2744,7 +2787,7 @@ async function captureAndStoreShell(
2744
2787
  // Capped to the route record the capture used: the policy the
2745
2788
  // store would apply (resolveTtl/resolveSwrWindow, as putShell does),
2746
2789
  // made explicit.
2747
- const record = reqCtx._routeRecordWindow;
2790
+ const record = reqCtx._shellCaptureState?.routeRecordWindow;
2748
2791
  let window: { ttl?: number; swr?: number } = {
2749
2792
  ttl: capture.ttl,
2750
2793
  swr: capture.swr,