@rangojs/router 0.18.0 → 0.19.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 (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -5,6 +5,7 @@
5
5
  * (initial page load) requests.
6
6
  */
7
7
 
8
+ import { requestHeaders } from "../server/request-headers.js";
8
9
  import {
9
10
  getRequestContext,
10
11
  setRequestContextParams,
@@ -26,6 +27,7 @@ import { getSSRSetup, createSsrHtmlStage, isRscRequest } from "./ssr-setup.js";
26
27
  import type { RscPayload } from "./types.js";
27
28
  import type { SSRModule } from "./types.js";
28
29
  import type { RequestContext } from "../server/request-context.js";
30
+ import type { PprReplayBypassReason } from "./shell-capture-constants.js";
29
31
  import {
30
32
  createResponseWithMergedHeaders,
31
33
  createSimpleRedirectResponse,
@@ -46,7 +48,8 @@ import {
46
48
  type RoutinePlan,
47
49
  } from "./routine-plan.js";
48
50
  import type { HandlerContext } from "./handler-context.js";
49
- import { gateTransitions } from "./transition-gate.js";
51
+ import { attachTransitionWhen } from "./attach-transition-when.js";
52
+ import { publicRouteName } from "../route-name.js";
50
53
  import { buildFullPayload } from "./full-payload.js";
51
54
  import {
52
55
  capShellWindow,
@@ -117,6 +120,7 @@ import type {
117
120
  SegmentCacheStore,
118
121
  ShellCacheEntry,
119
122
  ShellDocumentRead,
123
+ ShellEntryHead,
120
124
  ShellReadStats,
121
125
  ShellSnapshotRecord,
122
126
  } from "../cache/types.js";
@@ -125,25 +129,6 @@ import {
125
129
  SEGMENT_FRAGMENT_RECOVERY_HEADER,
126
130
  } from "../segment-fragments.js";
127
131
 
128
- type PprReplayBypassReason =
129
- | "method"
130
- | "dynamic"
131
- | "nonce"
132
- | "store-unavailable"
133
- | "passive-read-unsupported"
134
- | "no-navigation-context"
135
- | "prerender-store"
136
- | "intercept"
137
- | "cache-disabled"
138
- | "read-error"
139
- | "no-entry"
140
- | "invalid-version"
141
- | "corrupt-entry"
142
- | "no-segment-snapshot"
143
- | "snapshot-miss"
144
- | "explicit-cache-hit"
145
- | "stale-build-entry";
146
-
147
132
  type PprReplayStatus =
148
133
  | { outcome: "HIT"; freshness: "fresh" | "stale" }
149
134
  | { outcome: "BYPASS"; reason: PprReplayBypassReason };
@@ -187,7 +172,6 @@ function createShellCaptureDescriptor(
187
172
  tags: pprConfig.tags,
188
173
  captureTimeout: pprConfig.captureTimeout,
189
174
  store,
190
- debug: INTERNAL_RANGO_DEBUG,
191
175
  maxSnapshotBytes: pprConfig.maxSnapshotBytes,
192
176
  debugSink: resolveShellCaptureDebugSink(ctx.router.debugShellCapture),
193
177
  navigationOnly,
@@ -200,8 +184,7 @@ function shouldHealReplayMiss(
200
184
  return (
201
185
  reason === undefined ||
202
186
  reason === "invalid-version" ||
203
- reason === "corrupt-entry" ||
204
- reason === "stale-build-entry"
187
+ reason === "corrupt-entry"
205
188
  );
206
189
  }
207
190
 
@@ -256,16 +239,11 @@ function replayableShellSnapshot(
256
239
  if (!entry.navigationOnly && !hasIntactShellPayload(entry)) {
257
240
  return { reason: "corrupt-entry" };
258
241
  }
259
- // Eligibility requires the CANONICAL doc segment record (`docKey`), not just
260
- // any segment record: captures under an explicit cache() scope also record
261
- // that tier's reads/writes under keys a partial lookup can never resolve,
262
- // and counting those declared entries "replayable" that then always missed
263
- // (a timing-dependent snapshot-miss/no-segment-snapshot flip-flop). Entries
264
- // captured before the field existed read as no-segment-snapshot and heal on
265
- // recapture (pre-release, same treatment as the buildVersion field).
266
- const snapshot = entry.snapshot;
267
- return snapshot && hasDocRecord(snapshot, entry.docKey)
268
- ? { snapshot }
242
+ // Eligibility requires the CANONICAL doc segment record (`docKey`): replay
243
+ // resolves that record and nothing else. An entry without it (a
244
+ // prerender-served capture, a tombstone) reads as no-segment-snapshot.
245
+ return hasDocRecord(entry.snapshot, entry.docKey)
246
+ ? { snapshot: entry.snapshot }
269
247
  : { reason: "no-segment-snapshot" };
270
248
  }
271
249
 
@@ -319,10 +297,11 @@ type PreparedRender =
319
297
  | {
320
298
  kind: "payload";
321
299
  payload: RscPayload;
322
- hasInterceptSlots: boolean;
300
+ /** The response depends on the source page: an intercept targets the route. */
301
+ sourceScoped: boolean;
323
302
  pprReplayStatus?: PprReplayStatus;
324
- partialCaptureNeeded: boolean;
325
- partialCaptureKey?: string;
303
+ /** The key a navigation-only heal capture stores under, when one is needed. */
304
+ healKey?: string;
326
305
  };
327
306
 
328
307
  async function handleRscRenderingInner<TEnv>(
@@ -434,16 +413,15 @@ function* requestRenderPlan<TEnv>(
434
413
  response.headers.set(SHELL_STATUS_HEADER, "MISS");
435
414
  }
436
415
 
437
- const captureKey = prepared.partialCaptureKey;
416
+ const healKey = prepared.healKey;
438
417
  if (
439
418
  isPartial &&
440
- prepared.partialCaptureNeeded &&
441
- captureKey !== undefined &&
419
+ healKey !== undefined &&
442
420
  !reqCtx._dynamic &&
443
421
  response.status === 200
444
422
  ) {
445
423
  yield* handoff("navigation-shell-capture", () =>
446
- scheduleNavigationShellCapture(input, captureKey),
424
+ scheduleNavigationShellCapture(input, healKey),
447
425
  );
448
426
  }
449
427
 
@@ -506,24 +484,21 @@ function* shellServePlan<TEnv>(
506
484
 
507
485
  // A per-request CSP nonce pins the route to axis 1: a shared shell would
508
486
  // freeze the capture request's nonce and CSP would reject it for every
509
- // other visitor. BOTH nonce sources must gate — the createRouter({ nonce })
510
- // provider (`nonce` param) and a middleware ctx.set(nonce, …) token write;
511
- // the provider-only check missed the latter (issue #656).
512
- const activeNonce = nonce ?? contextGet(reqCtx._variables, nonceToken);
487
+ // other visitor. Every step below, a HIT included, therefore runs without
488
+ // one.
513
489
  const store = reqCtx._cacheStore;
514
490
  const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
515
- let key = baseKey;
516
- if (activeNonce !== undefined) {
491
+ if (activeRequestNonce(nonce, reqCtx) !== undefined) {
517
492
  // Declared intent that cannot be honored deserves a diagnostic (unlike an
518
493
  // undeclared route, which is silent): a ppr route gated off by an active
519
494
  // per-request nonce warns once per key. Axis 1 after the warning.
520
- warnPprNonceActiveOnce(key);
495
+ warnPprNonceActiveOnce(baseKey);
521
496
  return { kind: "pass" };
522
497
  }
523
498
  if (!hasShellFamily(store)) {
524
499
  // Declared intent that cannot be honored deserves a diagnostic (unlike an
525
500
  // undeclared route, which is silent). Axis 1 after the warning.
526
- warnShellStoreMissingOnce(key);
501
+ warnShellStoreMissingOnce(baseKey);
527
502
  return { kind: "pass" };
528
503
  }
529
504
  // The route's own cache() opt-out — cache(false), or a condition() that
@@ -557,26 +532,18 @@ function* shellServePlan<TEnv>(
557
532
  // or the store's keyGenerator) partitions its shell the same way: each
558
533
  // partition captures and serves its own shell. A failed key resolution
559
534
  // serves no shell, never another partition's.
560
- const partitionedKey = partitionedShellKey(
535
+ const requestKey = resolveRequestShellKey(
561
536
  baseKey,
562
- route?.cacheScope,
537
+ route,
563
538
  store,
564
- reqCtx.url.pathname,
565
- route?.params,
539
+ reqCtx,
540
+ "ShellServe",
566
541
  );
567
- if (partitionedKey) {
568
- const resolved = yield* step("shell-partition", () =>
569
- partitionedKey.then(
570
- (value): string | undefined => value,
571
- (error: unknown) => {
572
- reportCacheError(error, "cache-read", "[ShellServe] shell key");
573
- return undefined;
574
- },
575
- ),
576
- );
577
- if (resolved === undefined) return { kind: "pass" };
578
- key = resolved;
579
- }
542
+ const key =
543
+ typeof requestKey === "string"
544
+ ? requestKey
545
+ : yield* step("shell-partition", () => requestKey);
546
+ if (key === undefined) return { kind: "pass" };
580
547
  reqCtx._shellKey = key;
581
548
  // The buffered capture and tail timings are keyed by the partitioned key.
582
549
  mirrorPprServerTimings(key, reqCtx);
@@ -612,6 +579,14 @@ function* shellServePlan<TEnv>(
612
579
  pprConfig,
613
580
  store,
614
581
  );
582
+ // MISS (no entry, invalid reactVersion, a tombstone, or store read
583
+ // failure): axis 1 + a background capture scheduled once the response is
584
+ // known servable.
585
+ const miss: ShellServeOutcome = {
586
+ kind: "miss",
587
+ descriptor: { ...descriptor, storedSeqAtRead },
588
+ ssrModule,
589
+ };
615
590
 
616
591
  // A HIT replays the handler layer from the entry's doc record (`docKey`),
617
592
  // or, for a Prerender route, from the prerender store. An entry that has
@@ -622,11 +597,7 @@ function* shellServePlan<TEnv>(
622
597
  (cached.entry.docKey !== undefined || route?.matched?.pr === true) &&
623
598
  isValidShellHit(cached.entry, ctx.version)
624
599
  ) {
625
- const document = openShellDocumentMetered(
626
- reqCtx,
627
- cached.entry,
628
- cached.read,
629
- );
600
+ const document = openShellDocumentMetered(reqCtx, cached);
630
601
  if (!document) {
631
602
  // Corrupt stored payload (undecodable prelude / unparseable
632
603
  // postponed): a store-layer fault worth a diagnostic, unlike the
@@ -671,7 +642,6 @@ function* shellServePlan<TEnv>(
671
642
  ssrModule,
672
643
  document,
673
644
  descriptor,
674
- activeNonce,
675
645
  ),
676
646
  );
677
647
  return { kind: "serve", response };
@@ -686,11 +656,7 @@ function* shellServePlan<TEnv>(
686
656
  cached?.entry.navigationOnly &&
687
657
  isValidShellHit(cached.entry, ctx.version)
688
658
  ) {
689
- return {
690
- kind: "miss",
691
- descriptor: { ...descriptor, storedSeqAtRead },
692
- ssrModule,
693
- };
659
+ return miss;
694
660
  }
695
661
 
696
662
  // Build-time shell read-through (producer B, #699): on a runtime
@@ -736,7 +702,7 @@ function* shellServePlan<TEnv>(
736
702
  // The document serve decodes the baked prelude once here; an undecodable
737
703
  // one is a MISS like a corrupt runtime entry.
738
704
  const buildDocument = buildHit
739
- ? openShellDocumentMetered(reqCtx, buildHit.entry)
705
+ ? openShellDocumentMetered(reqCtx, { entry: buildHit.entry })
740
706
  : null;
741
707
  if (buildHit && buildDocument) {
742
708
  // Past ppr.ttl: still serve the baked entry, recapture upgrades it.
@@ -764,19 +730,12 @@ function* shellServePlan<TEnv>(
764
730
  ssrModule,
765
731
  buildDocument,
766
732
  descriptor,
767
- activeNonce,
768
733
  ),
769
734
  );
770
735
  return { kind: "serve", response };
771
736
  }
772
737
 
773
- // MISS (no entry, invalid reactVersion, or store read failure): axis 1
774
- // + a background capture scheduled once the response is known servable.
775
- return {
776
- kind: "miss",
777
- descriptor: { ...descriptor, storedSeqAtRead },
778
- ssrModule,
779
- };
738
+ return miss;
780
739
  }
781
740
 
782
741
  /**
@@ -796,10 +755,7 @@ function* preparePayloadPlan<TEnv>(
796
755
  );
797
756
  const result = replay.result;
798
757
  const pprReplayStatus = replay.status;
799
- const partialCaptureNeeded =
800
- "captureNeeded" in replay && replay.captureNeeded === true;
801
- const partialCaptureKey =
802
- "captureKey" in replay ? replay.captureKey : undefined;
758
+ const healKey = "healKey" in replay ? replay.healKey : undefined;
803
759
 
804
760
  if (!result) {
805
761
  // Fall back to full render
@@ -828,10 +784,9 @@ function* preparePayloadPlan<TEnv>(
828
784
  return {
829
785
  kind: "payload",
830
786
  payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
831
- hasInterceptSlots: false,
787
+ sourceScoped: false,
832
788
  pprReplayStatus,
833
- partialCaptureNeeded,
834
- partialCaptureKey,
789
+ healKey,
835
790
  };
836
791
  }
837
792
 
@@ -846,15 +801,12 @@ function* preparePayloadPlan<TEnv>(
846
801
  // intercepted server-side (X-RSC-Reload) and never delivers a
847
802
  // different-router payload to the client.
848
803
  routerId: ctx.router.id,
849
- segments: gateTransitions(
850
- result.segments,
851
- reqCtx,
852
- ctx.router.onError,
853
- ),
804
+ segments: attachTransitionWhen(result.segments, reqCtx),
854
805
  matched: result.matched,
855
806
  diff: result.diff,
856
807
  resolvedIds: result.resolvedIds,
857
808
  params: result.params,
809
+ routeName: publicRouteName(result.routeName),
858
810
  isPartial: true,
859
811
  slots: result.slots,
860
812
  interceptTargets: result.interceptTargets,
@@ -867,10 +819,9 @@ function* preparePayloadPlan<TEnv>(
867
819
  stateCookieName: ctx.router.resolvedStateCookieName,
868
820
  },
869
821
  },
870
- hasInterceptSlots: !!result.slots,
822
+ sourceScoped: result.interceptTargeted === true,
871
823
  pprReplayStatus,
872
- partialCaptureNeeded,
873
- partialCaptureKey,
824
+ healKey,
874
825
  };
875
826
  }
876
827
 
@@ -889,14 +840,10 @@ function* preparePayloadPlan<TEnv>(
889
840
  };
890
841
  }
891
842
 
892
- // Caching is now handled in router.match() via cache provider in request context
893
- // match.segments already contains cached or fresh segments as appropriate
894
-
895
843
  return {
896
844
  kind: "payload",
897
845
  payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
898
- hasInterceptSlots: false,
899
- partialCaptureNeeded: false,
846
+ sourceScoped: false,
900
847
  };
901
848
  }
902
849
 
@@ -926,12 +873,18 @@ async function matchPartialAndRecordParams<TEnv>(
926
873
  return replay;
927
874
  }
928
875
 
929
- /** A document serve's store read: the entry, plus the prelude-first read when the store has one. */
930
- interface ShellStoreRead {
931
- entry: ShellCacheEntry;
932
- shouldRevalidate?: boolean;
933
- read?: ShellDocumentRead;
934
- }
876
+ /**
877
+ * A document serve's store read: a getShell entry, or a prelude-first read
878
+ * (readShellDocument) whose entry is the head and whose snapshot arrives on
879
+ * its own promise.
880
+ */
881
+ type ShellStoreRead =
882
+ | { entry: ShellCacheEntry; shouldRevalidate?: boolean; read?: undefined }
883
+ | {
884
+ entry: ShellEntryHead;
885
+ shouldRevalidate?: boolean;
886
+ read: ShellDocumentRead;
887
+ };
935
888
 
936
889
  /**
937
890
  * Shell store read that degrades to null on failure (axis 1 MISS, never a 500)
@@ -1030,13 +983,17 @@ function recordShellReadStats(
1030
983
  */
1031
984
  function openShellDocumentMetered(
1032
985
  reqCtx: RequestContext<any>,
1033
- entry: ShellCacheEntry,
1034
- read?: ShellDocumentRead,
986
+ stored: ShellStoreRead,
1035
987
  ): ShellDocument | null {
988
+ const read = stored.read;
989
+ const open = (): ShellDocument | null =>
990
+ stored.read
991
+ ? openShellDocument(stored.entry, stored.read)
992
+ : openShellDocument(stored.entry);
1036
993
  const store = reqCtx._metricsStore;
1037
- if (!store) return openShellDocument(entry, read);
994
+ if (!store) return open();
1038
995
  const start = performance.now();
1039
- const document = openShellDocument(entry, read);
996
+ const document = open();
1040
997
  appendMetric(
1041
998
  store,
1042
999
  "ppr:shell-open",
@@ -1048,22 +1005,82 @@ function openShellDocumentMetered(
1048
1005
  return document;
1049
1006
  }
1050
1007
 
1008
+ /**
1009
+ * The request's per-request CSP nonce. BOTH sources count: the
1010
+ * createRouter({ nonce }) provider (`nonce`) and a middleware
1011
+ * ctx.set(nonce, …) token write; the provider-only check missed the latter
1012
+ * (issue #656). Read at the commit point, after the whole middleware chain.
1013
+ */
1014
+ function activeRequestNonce(
1015
+ nonce: string | undefined,
1016
+ reqCtx: RequestContext<any>,
1017
+ ): string | undefined {
1018
+ return nonce ?? contextGet(reqCtx._variables, nonceToken);
1019
+ }
1020
+
1051
1021
  /**
1052
1022
  * The shell key a request reads and captures: `baseKey`, partitioned by the
1053
1023
  * route's request partition (cache-scope.ts resolveShellPartition; a
1054
- * partition equal to the default key leaves it unchanged). Undefined,
1055
- * synchronously and with no work, when nothing partitions the route; a
1056
- * rejection is a failed key resolution.
1024
+ * partition equal to the default key leaves it unchanged). `baseKey` itself,
1025
+ * synchronously and with no work, when nothing partitions the route; else a
1026
+ * promise that resolves undefined when the key failed to resolve (reported
1027
+ * as `[source] shell key`): such a request reads and captures no shell,
1028
+ * never another partition's.
1057
1029
  */
1058
- function partitionedShellKey(
1030
+ function resolveRequestShellKey(
1059
1031
  baseKey: string,
1060
- ...partition: Parameters<typeof resolveShellPartition>
1061
- ): Promise<string> | undefined {
1062
- return resolveShellPartition(...partition)?.then((resolved) =>
1063
- resolved === null ? baseKey : partitionShellKey(baseKey, resolved),
1032
+ route: RouteSnapshot<any> | null,
1033
+ store: SegmentCacheStore,
1034
+ reqCtx: RequestContext<any>,
1035
+ source: string,
1036
+ ): string | Promise<string | undefined> {
1037
+ const partition = resolveShellPartition(
1038
+ route?.cacheScope,
1039
+ store,
1040
+ reqCtx.url.pathname,
1041
+ route?.params,
1042
+ );
1043
+ if (!partition) return baseKey;
1044
+ return partition.then(
1045
+ (resolved) =>
1046
+ resolved === null ? baseKey : partitionShellKey(baseKey, resolved),
1047
+ (error: unknown) => {
1048
+ reportCacheError(error, "cache-read", `[${source}] shell key`);
1049
+ return undefined;
1050
+ },
1064
1051
  );
1065
1052
  }
1066
1053
 
1054
+ /**
1055
+ * Observe the explicit-scope composition in withCacheLookup: the route's own
1056
+ * cache() scope served the request (`explicit-cache-hit`) or refused it at
1057
+ * lookup time (`cache-disabled`). The two are exclusive per lookup, and
1058
+ * neither fires when the seeded doc record served.
1059
+ */
1060
+ function observeExplicitScope(): {
1061
+ onExplicitHit: () => void;
1062
+ onExplicitBypass: () => void;
1063
+ bypassed: () => boolean;
1064
+ status: (otherwise: PprReplayStatus) => PprReplayStatus;
1065
+ } {
1066
+ let observed: "hit" | "bypass" | undefined;
1067
+ return {
1068
+ onExplicitHit: () => {
1069
+ observed = "hit";
1070
+ },
1071
+ onExplicitBypass: () => {
1072
+ observed = "bypass";
1073
+ },
1074
+ bypassed: () => observed === "bypass",
1075
+ status: (otherwise) =>
1076
+ observed === "hit"
1077
+ ? { outcome: "BYPASS", reason: "explicit-cache-hit" }
1078
+ : observed === "bypass"
1079
+ ? { outcome: "BYPASS", reason: "cache-disabled" }
1080
+ : otherwise,
1081
+ };
1082
+ }
1083
+
1067
1084
  /**
1068
1085
  * Server-Timing mirror (issue #651): a capture or a HIT tail completes AFTER
1069
1086
  * its triggering response committed, so its numbers can only ride a LATER
@@ -1116,7 +1133,7 @@ function renderPreparedRscResponse<TEnv>(
1116
1133
  ): Promise<Response> {
1117
1134
  const { ctx, request, env, url, isPartial, nonce, reqCtx, renderSpan } =
1118
1135
  input;
1119
- const { payload, pprReplayStatus, hasInterceptSlots } = prepared;
1136
+ const { payload, pprReplayStatus, sourceScoped } = prepared;
1120
1137
  const metricsStore = reqCtx._metricsStore;
1121
1138
 
1122
1139
  const rscHeaders: Record<string, string> = {
@@ -1133,18 +1150,20 @@ function renderPreparedRscResponse<TEnv>(
1133
1150
  serializePprReplayStatus(pprReplayStatus);
1134
1151
  }
1135
1152
  // Tell the client's prefetch cache to scope this response to its source
1136
- // URL (instead of the default source-agnostic wildcard). Intercept
1137
- // responses depend on the source page matching an intercept rule, so
1138
- // they must not be reused for navigations from other sources.
1139
- if (hasInterceptSlots) {
1153
+ // URL (instead of the default source-agnostic wildcard). A route an
1154
+ // intercept targets renders the modal or the full page depending on the
1155
+ // source, so neither response may be reused from another source: a full
1156
+ // page prefetched where the intercept does not apply would otherwise
1157
+ // serve a later click where it does (#1007).
1158
+ if (sourceScoped) {
1140
1159
  rscHeaders["x-rsc-prefetch-scope"] = "source";
1141
1160
  }
1142
1161
  // Enable browser HTTP caching for prefetch responses only.
1143
1162
  // Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
1144
- // non-intercept context (intercept responses depend on source page),
1145
- // and a configured cache-control value (false disables caching).
1146
- const isPrefetch = request.headers.has("X-Rango-Prefetch");
1147
- if (isPrefetch && isPartial && !hasInterceptSlots) {
1163
+ // a source-agnostic response (see sourceScoped above), and a configured
1164
+ // cache-control value (false disables caching).
1165
+ const isPrefetch = requestHeaders(request).has("X-Rango-Prefetch");
1166
+ if (isPrefetch && isPartial && !sourceScoped) {
1148
1167
  const cc = ctx.router.prefetchCacheControl;
1149
1168
  if (cc) {
1150
1169
  rscHeaders["cache-control"] = cc;
@@ -1229,10 +1248,11 @@ function scheduleNavigationShellCapture<TEnv>(
1229
1248
  /**
1230
1249
  * Reuse a PPR capture's canonical segment record for a partial navigation.
1231
1250
  * The ordinary matchPartial pipeline remains authoritative: it projects the
1232
- * cached target tree against the client's segment ids, evaluates revalidation,
1233
- * and resolves every loader fresh. Only segment-family records are seeded;
1234
- * captured item/response/loader values belong to document parity and must not
1235
- * pin navigation data.
1251
+ * cached target tree against the client's segment ids and evaluates
1252
+ * revalidation. Only the segment family is seeded into the store overlay, so
1253
+ * item and response reads stay live; a record that served also arms the
1254
+ * bake-lane loader pins (onHit), which serve as on a document HIT, while live
1255
+ * loaders run fresh.
1236
1256
  */
1237
1257
  async function matchPartialWithPprReplay<TEnv>(
1238
1258
  ctx: HandlerContext<TEnv>,
@@ -1289,7 +1309,7 @@ async function matchPartialWithPprReplay<TEnv>(
1289
1309
  return { result, status: finalStatus };
1290
1310
  };
1291
1311
  const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry);
1292
- const activeNonce = nonce ?? contextGet(reqCtx._variables, nonceToken);
1312
+ const activeNonce = activeRequestNonce(nonce, reqCtx);
1293
1313
  const store = reqCtx._cacheStore;
1294
1314
 
1295
1315
  if (!pprConfig) return runMatch();
@@ -1325,8 +1345,8 @@ async function matchPartialWithPprReplay<TEnv>(
1325
1345
  // one-shot corruption recovery request still replay the same segment record,
1326
1346
  // but through the server decode path.
1327
1347
  armFragments =
1328
- request.headers.get(SEGMENT_FRAGMENT_RECOVERY_HEADER) !== "1" &&
1329
- request.headers.get(SEGMENT_FRAGMENT_CAPABILITY_HEADER) === "1";
1348
+ requestHeaders(request).get(SEGMENT_FRAGMENT_RECOVERY_HEADER) !== "1" &&
1349
+ requestHeaders(request).get(SEGMENT_FRAGMENT_CAPABILITY_HEADER) === "1";
1330
1350
  // The prerender probe and the cache-opt-out gate below both need the
1331
1351
  // classified snapshot; resolve (and persist) it once.
1332
1352
  const routeSnapshot = classifiedRouteSnapshot(reqCtx);
@@ -1377,22 +1397,15 @@ async function matchPartialWithPprReplay<TEnv>(
1377
1397
 
1378
1398
  // The visitor's own partition (resolveShellPartition): replay never
1379
1399
  // crosses into another partition's shell.
1380
- const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
1381
- let key = baseKey;
1382
- const partitionedKey = partitionedShellKey(
1383
- baseKey,
1384
- routeCacheScope,
1400
+ const key = await resolveRequestShellKey(
1401
+ buildShellKey(url, reqCtx._searchParamsFilter),
1402
+ routeSnapshot,
1385
1403
  store,
1386
- reqCtx.url.pathname,
1387
- routeSnapshot?.params,
1404
+ reqCtx,
1405
+ "NavigationPPR",
1388
1406
  );
1389
- if (partitionedKey) {
1390
- try {
1391
- key = await partitionedKey;
1392
- } catch (error) {
1393
- reportCacheError(error, "cache-read", "[NavigationPPR] shell key");
1394
- return runMatch({ outcome: "BYPASS", reason: "read-error" });
1395
- }
1407
+ if (key === undefined) {
1408
+ return runMatch({ outcome: "BYPASS", reason: "read-error" });
1396
1409
  }
1397
1410
  const navigationKey = navigationShellKey(key);
1398
1411
  let cached: Awaited<ReturnType<typeof store.getShell>> = null;
@@ -1443,35 +1456,6 @@ async function matchPartialWithPprReplay<TEnv>(
1443
1456
  }
1444
1457
  }
1445
1458
 
1446
- if (!snapshot) {
1447
- // Production build manifests are local module data. In dev, resolving a
1448
- // missing build shell would foreground-fetch /__rsc_shell and block an
1449
- // otherwise ordinary navigation on capture, so replay remains runtime-only.
1450
- const buildHit =
1451
- key !== baseKey
1452
- ? null
1453
- : await lookupBuildShell(
1454
- url,
1455
- ctx.version,
1456
- store,
1457
- undefined,
1458
- reqCtx._searchParamsFilter,
1459
- );
1460
- if (buildHit?.stale) {
1461
- bypassReason ??= "stale-build-entry";
1462
- } else if (buildHit) {
1463
- const buildDecision = replayableShellSnapshot(
1464
- buildHit.entry,
1465
- ctx.version,
1466
- );
1467
- if ("snapshot" in buildDecision) {
1468
- snapshot = buildDecision.snapshot;
1469
- } else {
1470
- bypassReason ??= buildDecision.reason;
1471
- }
1472
- }
1473
- }
1474
-
1475
1459
  if (!snapshot) {
1476
1460
  // Report-only marker for routes with an ENABLED route-derived scope: the
1477
1461
  // lookup's own outcome must stay reportable even without a replayable
@@ -1486,25 +1470,20 @@ async function matchPartialWithPprReplay<TEnv>(
1486
1470
  // implicit scope over the REAL doc: partition in
1487
1471
  // resolveShellImplicitCacheScope).
1488
1472
  if (routeCacheScope?.enabled) {
1489
- let explicitCacheHit = false;
1490
- let explicitCacheBypass = false;
1473
+ const explicit = observeExplicitScope();
1491
1474
  const previousImplicitCache = reqCtx._shellImplicitCache;
1492
1475
  reqCtx._shellImplicitCache = {
1493
- onExplicitHit: () => {
1494
- explicitCacheHit = true;
1495
- },
1496
- onExplicitBypass: () => {
1497
- explicitCacheBypass = true;
1498
- },
1476
+ onExplicitHit: explicit.onExplicitHit,
1477
+ onExplicitBypass: explicit.onExplicitBypass,
1499
1478
  };
1500
1479
  try {
1501
1480
  const result = await matchPartialForReplay();
1502
- const provisionalStatus: PprReplayStatus = explicitCacheBypass
1503
- ? { outcome: "BYPASS", reason: "cache-disabled" }
1504
- : explicitCacheHit
1505
- ? { outcome: "BYPASS", reason: "explicit-cache-hit" }
1506
- : { outcome: "BYPASS", reason: bypassReason ?? "no-entry" };
1507
- const status = finalizeReplayStatus(provisionalStatus);
1481
+ const status = finalizeReplayStatus(
1482
+ explicit.status({
1483
+ outcome: "BYPASS",
1484
+ reason: bypassReason ?? "no-entry",
1485
+ }),
1486
+ );
1508
1487
  return {
1509
1488
  result,
1510
1489
  status,
@@ -1518,13 +1497,14 @@ async function matchPartialWithPprReplay<TEnv>(
1518
1497
  // capture derives from THIS request's context, so its doc record
1519
1498
  // records. Excluding it left replay dead until the document
1520
1499
  // recaptured. The always-false route stays protected: its every
1521
- // lookup refuses, so explicitCacheBypass suppresses the heal.
1522
- captureNeeded:
1523
- !explicitCacheBypass &&
1500
+ // lookup refuses, so its bypass suppresses the heal.
1501
+ healKey:
1502
+ !explicit.bypassed() &&
1524
1503
  reqCtx._pprReplayPostMatchReason === undefined &&
1525
1504
  (shouldHealReplayMiss(bypassReason) ||
1526
- bypassReason === "no-segment-snapshot"),
1527
- captureKey: navigationKey,
1505
+ bypassReason === "no-segment-snapshot")
1506
+ ? navigationKey
1507
+ : undefined,
1528
1508
  };
1529
1509
  } finally {
1530
1510
  reqCtx._shellImplicitCache = previousImplicitCache;
@@ -1536,10 +1516,11 @@ async function matchPartialWithPprReplay<TEnv>(
1536
1516
  });
1537
1517
  return {
1538
1518
  ...match,
1539
- captureNeeded:
1519
+ healKey:
1540
1520
  reqCtx._pprReplayPostMatchReason === undefined &&
1541
- shouldHealReplayMiss(bypassReason),
1542
- captureKey: navigationKey,
1521
+ shouldHealReplayMiss(bypassReason)
1522
+ ? navigationKey
1523
+ : undefined,
1543
1524
  };
1544
1525
  }
1545
1526
 
@@ -1548,14 +1529,11 @@ async function matchPartialWithPprReplay<TEnv>(
1548
1529
  let loaderSeed: ReturnType<typeof buildShellLoaderSeed> | undefined;
1549
1530
  let segmentReplayHit = false;
1550
1531
  let segmentReplayCorrupt = false;
1551
- let explicitCacheHit = false;
1552
- let explicitCacheBypass = false;
1532
+ const explicit = observeExplicitScope();
1553
1533
  reqCtx._shellImplicitCache = {
1554
1534
  ttl: pprConfig.ttl,
1555
1535
  swr: pprConfig.swr,
1556
- store: new SeededShellStore(store, snapshot, {
1557
- segmentsOnly: true,
1558
- }),
1536
+ store: new SeededShellStore(store, snapshot),
1559
1537
  keyPrefix: "doc",
1560
1538
  // Arms the bake-lane loader pins a document HIT serves (loader-cache.ts
1561
1539
  // resolveLoaderData), decoded only now: the lookup awaits this before
@@ -1576,27 +1554,18 @@ async function matchPartialWithPprReplay<TEnv>(
1576
1554
  // Arms the explicit-scope composition in withCacheLookup: a route-derived
1577
1555
  // cache() scope stays authoritative, the seeded doc record supplies the
1578
1556
  // match only on its miss. An explicit-tier hit must NOT report a replay
1579
- // HIT — it served under the consumer's own semantics.
1580
- onExplicitHit: () => {
1581
- explicitCacheHit = true;
1582
- },
1583
- // Lookup-time condition() refusal (or a scope with no store): the gate
1584
- // deliberately does not pre-decide predicates, so the truthful
1585
- // cache-disabled report comes from the lookup that actually refused.
1586
- onExplicitBypass: () => {
1587
- explicitCacheBypass = true;
1588
- },
1557
+ // HIT — it served under the consumer's own semantics. A lookup-time
1558
+ // condition() refusal (or a scope with no store) reports cache-disabled:
1559
+ // the gate deliberately does not pre-decide predicates.
1560
+ onExplicitHit: explicit.onExplicitHit,
1561
+ onExplicitBypass: explicit.onExplicitBypass,
1589
1562
  };
1590
1563
 
1591
1564
  try {
1592
1565
  const result = await matchPartialForReplay();
1593
1566
  const provisionalStatus: PprReplayStatus = segmentReplayHit
1594
1567
  ? { outcome: "HIT", freshness }
1595
- : explicitCacheHit
1596
- ? { outcome: "BYPASS", reason: "explicit-cache-hit" }
1597
- : explicitCacheBypass
1598
- ? { outcome: "BYPASS", reason: "cache-disabled" }
1599
- : { outcome: "BYPASS", reason: "snapshot-miss" };
1568
+ : explicit.status({ outcome: "BYPASS", reason: "snapshot-miss" });
1600
1569
  // A prerender serve or intercept resolution leaves the seeded record
1601
1570
  // unconsulted (prerender short-circuits before the scope; intercepts
1602
1571
  // keep snapshot.cacheScope) — the "snapshot-miss" guess would blame the
@@ -1605,14 +1574,15 @@ async function matchPartialWithPprReplay<TEnv>(
1605
1574
  return {
1606
1575
  result,
1607
1576
  status,
1608
- captureNeeded:
1609
- segmentReplayCorrupt && reqCtx._pprReplayPostMatchReason === undefined,
1610
1577
  // A corrupt document snapshot must be overwritten at the document key;
1611
1578
  // writing only the secondary navigation key would leave the preferred
1612
1579
  // document entry shadowing the repair forever. The capture remains marked
1613
1580
  // navigationOnly, so a document request ignores it and recaptures a
1614
1581
  // document-safe shell under the same key.
1615
- captureKey: segmentReplayCorrupt ? snapshotStoreKey : undefined,
1582
+ healKey:
1583
+ segmentReplayCorrupt && reqCtx._pprReplayPostMatchReason === undefined
1584
+ ? snapshotStoreKey
1585
+ : undefined,
1616
1586
  };
1617
1587
  } finally {
1618
1588
  reqCtx._shellImplicitCache = previousImplicitCache;
@@ -1707,7 +1677,7 @@ async function isSettled(value: unknown): Promise<boolean> {
1707
1677
  */
1708
1678
  async function degradeUnreplayableShell(
1709
1679
  descriptor: ShellCaptureDescriptor,
1710
- entry: ShellCacheEntry,
1680
+ entry: ShellEntryHead,
1711
1681
  scheduleRecapture: () => void,
1712
1682
  ): Promise<void> {
1713
1683
  const store = descriptor.store;
@@ -1717,6 +1687,7 @@ async function degradeUnreplayableShell(
1717
1687
  {
1718
1688
  reactVersion: entry.reactVersion,
1719
1689
  buildVersion: entry.buildVersion,
1690
+ snapshot: [],
1720
1691
  navigationOnly: true,
1721
1692
  createdAt: Date.now(),
1722
1693
  },
@@ -1749,7 +1720,6 @@ function serveShellHit(
1749
1720
  ssrModule: SSRModule,
1750
1721
  document: ShellDocument,
1751
1722
  descriptor: ShellCaptureDescriptor,
1752
- nonce: string | undefined,
1753
1723
  ): Response {
1754
1724
  const { entry, prelude: preludeBytes } = document;
1755
1725
  const metricsStore = reqCtx._metricsStore;
@@ -1859,10 +1829,10 @@ function serveShellHit(
1859
1829
  ReadableStream<Uint8Array> | { redirect: string }
1860
1830
  > = (async () => {
1861
1831
  // Snapshot seeding (docs/design/ppr-shell-resume.md): the tail render must
1862
- // match the frozen prelude, so pinned cache reads replay their capture-time
1863
- // values via a SeededShellStore overlay while unpinned reads (the holes)
1864
- // stay live. The overlay lives on a DERIVED context so the shared reqCtx is
1865
- // untouched.
1832
+ // match the frozen prelude, so it replays the doc record and serves the
1833
+ // bake-lane loader pins, on a DERIVED context so the shared reqCtx is
1834
+ // untouched. Every cache read the tail makes, a hole's included, goes to
1835
+ // the request's own store.
1866
1836
  // A prelude-first read delivers the snapshot on its own promise: the
1867
1837
  // prelude is already committed, so only the tail waits for it.
1868
1838
  const inMemory = await isSettled(document.snapshot);
@@ -1886,9 +1856,6 @@ function serveShellHit(
1886
1856
  }
1887
1857
  const seededCtx = createTailContext();
1888
1858
  const records = snapshot ?? [];
1889
- if (reqCtx._cacheStore && records.length > 0) {
1890
- seededCtx._cacheStore = new SeededShellStore(reqCtx._cacheStore, records);
1891
- }
1892
1859
  // Loader-family records (bake-lane containers, loader-container-bake):
1893
1860
  // decode into a seed Map for the resolveLoaderData overlay, so the
1894
1861
  // payload's baked container bytes match the frozen prelude while the
@@ -1922,9 +1889,7 @@ function serveShellHit(
1922
1889
  seededCtx._shellImplicitCache = {
1923
1890
  ttl: descriptor.ttl,
1924
1891
  swr: descriptor.swr,
1925
- store: new SeededShellStore(reqCtx._cacheStore!, records, {
1926
- segmentsOnly: true,
1927
- }),
1892
+ store: new SeededShellStore(reqCtx._cacheStore!, records),
1928
1893
  keyPrefix: "doc",
1929
1894
  docTail: true,
1930
1895
  fixedDocKey: entry.docKey,
@@ -2041,7 +2006,6 @@ function serveShellHit(
2041
2006
  new TextEncoder().encode(
2042
2007
  inlineShellScript(
2043
2008
  `location.replace(${escapeJsonForScript(JSON.stringify(safeTarget))})`,
2044
- nonce,
2045
2009
  ),
2046
2010
  ),
2047
2011
  );
@@ -2067,9 +2031,7 @@ function serveShellHit(
2067
2031
  scheduleRecapture,
2068
2032
  );
2069
2033
  }
2070
- controller.enqueue(
2071
- new TextEncoder().encode(shellReloadScript(nonce)),
2072
- );
2034
+ controller.enqueue(new TextEncoder().encode(shellReloadScript()));
2073
2035
  if (tailTiming) {
2074
2036
  tailTiming.outcome = "error";
2075
2037
  tailTiming.completeMs = Math.round(performance.now() - tailT0);
@@ -2084,15 +2046,7 @@ function serveShellHit(
2084
2046
  // request until the entry ages out — recapture overwrites the entry.
2085
2047
  // Client disconnects land here too; the recapture is idempotent and
2086
2048
  // bounded by scheduleShellCapture's stampede guard + backoff.
2087
- scheduleShellCapture(
2088
- ctx,
2089
- request,
2090
- env,
2091
- url,
2092
- reqCtx,
2093
- ssrModule,
2094
- descriptor,
2095
- );
2049
+ scheduleRecapture();
2096
2050
  if (tailTiming) {
2097
2051
  tailTiming.outcome = "error";
2098
2052
  tailTiming.completeMs = Math.round(performance.now() - tailT0);