@rangojs/router 0.17.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -21,12 +21,36 @@
21
21
  * primitive for each tag. A single configured store (the common case) owns its
22
22
  * own tag index and distributed invalidation - there is no separate
23
23
  * tag-invalidation store.
24
+ *
25
+ * Both call invalidateTags() synchronously, inside the request (#973). A store
26
+ * masks the tags for the rest of the request before its first await (the
27
+ * built-in stores write their per-request marker state there), so the request
28
+ * that ran either verb reads its own writes; revalidateTag() leaves only the
29
+ * durable remainder (KV marker put, tag purge, expireTag) to waitUntil.
30
+ * Invariant: the mask only turns the invalidating request's hits into misses.
31
+ * If the durable write then fails, that request paid extra misses, never a
32
+ * stale read, and other requests read the durable state. Both also record
33
+ * the tags in the isolate's invalidation order (markInvalidated), so a cache
34
+ * execution that started before one of its tags was invalidated, in any
35
+ * request, is neither joined nor written (#973, #977): see
36
+ * predatesInvalidation, and docs/design/caching.md "Read-your-own-writes".
24
37
  */
25
38
 
26
39
  import { _getRequestContext } from "../server/request-context.js";
27
40
  import { reportingAsync } from "./cache-error.js";
28
41
  import { normalizeTags } from "./cache-tag.js";
29
42
  import type { SegmentCacheStore } from "./types.js";
43
+ import {
44
+ invalidatedSince,
45
+ markInvalidated,
46
+ type ExecutionStart,
47
+ } from "./invalidation-order.js";
48
+
49
+ export {
50
+ executionStart,
51
+ invalidatedSince,
52
+ type ExecutionStart,
53
+ } from "./invalidation-order.js";
30
54
 
31
55
  /**
32
56
  * Collect every store that may hold entries tagged for this request's handler:
@@ -104,6 +128,67 @@ function warnPartialTagStore(fn: string, incapable: number): void {
104
128
  );
105
129
  }
106
130
 
131
+ /**
132
+ * Set the fresh-reads cookie on this request's response for as long as the
133
+ * invalidated stores' isolate memos can stay stale (isolate-tag-memo.ts), so
134
+ * the same user's next requests read past them on every isolate.
135
+ */
136
+ function markFreshReads(stores: SegmentCacheStore[]): void {
137
+ const ctx = _getRequestContext();
138
+ if (!ctx?._setFreshReadsCookie) return;
139
+ let windowMs = 0;
140
+ for (const store of stores) {
141
+ windowMs = Math.max(windowMs, store.freshReadsWindowMs ?? 0);
142
+ }
143
+ if (windowMs > 0) ctx._setFreshReadsCookie(windowMs);
144
+ }
145
+
146
+ /**
147
+ * The write gate of every cache execution (#977): whether one of `tags` was
148
+ * invalidated after the execution that produced the value started.
149
+ *
150
+ * Every built-in store stamps an entry when it is written (CFCacheStore's
151
+ * `taggedAt`, VercelCacheStore's `ta`) or checks nothing on write
152
+ * (MemorySegmentCacheStore), so a value read before an invalidation and
153
+ * written after it would be served as newer than the invalidation until it
154
+ * expires. Two answers, either one skips the write:
155
+ *
156
+ * - this isolate's order (invalidatedSince), which sees every request's
157
+ * updateTag()/revalidateTag() here, the calling one included;
158
+ * - the store's markers (isTagsInvalidatedSince), for another isolate's
159
+ * invalidation, as far as the store can tell: KV markers on CFCacheStore
160
+ * (KV-less, only this request's own), `tm` markers on VercelCacheStore,
161
+ * the process markers on MemorySegmentCacheStore. Asked about the
162
+ * milliseconds after the start's: this isolate's own invalidation that
163
+ * preceded the start usually shares its millisecond (Workers freeze
164
+ * Date.now() between I/O), and `>=` would skip every write started right
165
+ * after it. Another isolate's invalidation in the start's own millisecond
166
+ * is not caught. Asked fail-closed: a marker the store cannot read
167
+ * counts as an invalidation.
168
+ *
169
+ * The order is read again once the store answers: its read can be in flight
170
+ * while this isolate invalidates (an action awaits a "use cache" miss, whose
171
+ * write waits on the read, then runs updateTag()), and it then answers with
172
+ * the markers from before. markInvalidated is synchronous, so this second
173
+ * look sees every invalidation made before the answer.
174
+ *
175
+ * Invariant: a skipped write only costs a later miss, never a stale read.
176
+ * The caller still returns the value it computed.
177
+ */
178
+ export async function predatesInvalidation(
179
+ store: SegmentCacheStore,
180
+ tags: readonly string[] | undefined,
181
+ start: ExecutionStart,
182
+ ): Promise<boolean> {
183
+ if (!tags || tags.length === 0) return false;
184
+ if (invalidatedSince(tags, start.seq)) return true;
185
+ if (!store.isTagsInvalidatedSince) return false;
186
+ const marked = await store.isTagsInvalidatedSince([...tags], start.at + 1, {
187
+ failClosed: true,
188
+ });
189
+ return marked || invalidatedSince(tags, start.seq);
190
+ }
191
+
107
192
  async function invalidateAcross(
108
193
  stores: SegmentCacheStore[],
109
194
  tags: string[],
@@ -163,6 +248,8 @@ export async function updateTag(...tags: string[]): Promise<void> {
163
248
  }
164
249
  if (incapable > 0) warnPartialTagStore("updateTag", incapable);
165
250
 
251
+ markFreshReads(capable);
252
+ markInvalidated(valid);
166
253
  await invalidateAcross(capable, valid);
167
254
  }
168
255
 
@@ -173,10 +260,12 @@ export async function updateTag(...tags: string[]): Promise<void> {
173
260
  * This is NOT stale-while-revalidate: like updateTag() it hard-purges, so the
174
261
  * next read after the invalidation lands is a miss that re-renders fresh. The
175
262
  * only difference from updateTag() is awaitability - revalidateTag() defers the
176
- * purge off the response path and is not awaited.
263
+ * durable write off the response path and is not awaited.
177
264
  *
178
- * Use in Route Handlers / webhooks. For read-your-own-writes inside a Server
179
- * Action, use updateTag() instead so the action's own response is fresh.
265
+ * Read-your-own-writes (#973): each built-in store masks the tags for the rest
266
+ * of this request before revalidateTag() returns, so a Server Action's own
267
+ * re-render after it is fresh, as after `await updateTag()`. Other requests see
268
+ * the invalidation once the durable write lands.
180
269
  *
181
270
  * Fire-and-forget: because this returns void and runs in the background, a
182
271
  * failed durable marker write (e.g. a transient KV outage) is NOT surfaced to
@@ -209,23 +298,25 @@ export function revalidateTag(...tags: string[]): void {
209
298
  }
210
299
  if (incapable > 0) warnPartialTagStore("revalidateTag", incapable);
211
300
 
301
+ markFreshReads(capable);
212
302
  const ctx = _getRequestContext();
303
+ // A build-time render drops background work (RequestContext.waitUntil), so
304
+ // it never ran this invalidation; keep it that way.
305
+ if (ctx?.build) return;
306
+ markInvalidated(valid);
307
+ // Started here, not in the waitUntil task, so each store masks the tags
308
+ // before this returns (#973, header above).
309
+ //
213
310
  // reportingAsync never rejects: it catches a failed durable write and routes
214
311
  // it through reportCacheError (loud log + onError). This is the only place a
215
312
  // revalidateTag failure can be observed, since it is not awaitable. Pass ctx
216
- // explicitly - the run executes in a detached waitUntil where the ALS context
217
- // is gone, so onError fires only if we hand it the captured context.
218
- const run = () =>
219
- reportingAsync(
220
- () => invalidateAcross(capable, valid),
221
- "cache-invalidate",
222
- "[revalidateTag] background invalidation",
223
- ctx,
224
- );
225
- if (ctx?.waitUntil) {
226
- ctx.waitUntil(run);
227
- } else {
228
- // No request context (e.g. called outside ALS): best-effort background run.
229
- void run();
230
- }
313
+ // explicitly: the write can settle after the response, outside the
314
+ // request's ALS scope, and onError fires only with the captured context.
315
+ const pending = reportingAsync(
316
+ () => invalidateAcross(capable, valid),
317
+ "cache-invalidate",
318
+ "[revalidateTag] background invalidation",
319
+ ctx,
320
+ );
321
+ ctx?.waitUntil(() => pending);
231
322
  }
@@ -75,6 +75,12 @@ export interface SegmentCacheStore<TEnv = unknown> {
75
75
  * 2. Store-level `keyGenerator` (modifies default key)
76
76
  * 3. Default key generation (prefix:pathname:params)
77
77
  *
78
+ * Return `defaultKey` unchanged to leave a request unpartitioned. An empty
79
+ * string is not a key: where the result partitions a nested cache() on
80
+ * another store, or a ppr shell, it fails key resolution, so the request
81
+ * renders uncached (as when the keyGenerator throws), and the router warns
82
+ * once naming the store.
83
+ *
78
84
  * @example Using headers for cache segmentation
79
85
  * ```typescript
80
86
  * keyGenerator: (ctx, defaultKey) => {
@@ -112,7 +118,11 @@ export interface SegmentCacheStore<TEnv = unknown> {
112
118
  get(key: string): Promise<CacheGetResult | null | CacheReadError>;
113
119
 
114
120
  /**
115
- * Store entry data with TTL
121
+ * Store entry data with TTL. Resolve once a later `get(key)` from the same
122
+ * location observes the entry: the route cache() write runs in a background
123
+ * task, and a PPR shell capture awaits that task before it reads the entry
124
+ * back (#957). A store that resolves earlier makes the capture re-render
125
+ * the page instead of replaying the entry.
116
126
  * @param key - Cache key
117
127
  * @param data - Serialized entry data
118
128
  * @param ttl - Time-to-live in seconds
@@ -183,8 +193,14 @@ export interface SegmentCacheStore<TEnv = unknown> {
183
193
  * upgrade — the reactVersion field on the entry gates that at read time).
184
194
  * @param key - Cache key
185
195
  * @param entry - The shell prelude/postponed/version/createdAt bundle
186
- * @param ttlSeconds - Time-to-live in seconds
187
- * @param swrSeconds - Optional stale-while-revalidate window in seconds
196
+ * @param ttlSeconds - Time-to-live in seconds: the route's `ppr.ttl`
197
+ * (undefined: the store's default), capped to the route cache() entry
198
+ * the capture replayed or wrote. A capped window arrives in whole
199
+ * seconds, rounded up, with ttl + swr at least 1; its ttl can be 0 (a
200
+ * shell stale from the start, captured from a record inside its swr
201
+ * window).
202
+ * @param swrSeconds - Optional stale-while-revalidate window in seconds,
203
+ * under the same cap
188
204
  * @param tags - Optional cache tags for invalidation (participates in
189
205
  * invalidateTags via the same tag machinery as the item family)
190
206
  * @returns `invalidated` when a generation marker rejected the write,
@@ -203,6 +219,47 @@ export interface SegmentCacheStore<TEnv = unknown> {
203
219
  tags?: string[],
204
220
  ): Promise<"stored" | "invalidated" | "uncacheable" | void>;
205
221
 
222
+ /**
223
+ * @internal The document serve path's shell read, for built-in stores. The
224
+ * serve path prefers it over getShell: it resolves with the entry and the
225
+ * raw (decoded) prelude, the snapshot on its own promise, and the read's
226
+ * `debugPerformance` stats. CFCacheStore's layout keeps the snapshot behind
227
+ * the prelude, so its HIT can commit before the snapshot is read;
228
+ * VercelCacheStore reads the whole entry and decodes the prelude once per
229
+ * memoized shell (issue #941, docs/design/shell-entry-layout.md). Not part
230
+ * of the custom-store contract.
231
+ */
232
+ readShellDocument?(
233
+ key: string,
234
+ options?: ShellDocumentReadOptions,
235
+ ): Promise<ShellDocumentRead | null>;
236
+
237
+ /**
238
+ * @internal How long (ms) this store's isolate memos (shell, tag markers)
239
+ * can serve a read that predates a tag invalidation made elsewhere. After
240
+ * updateTag()/revalidateTag(), the response's fresh-reads cookie lasts the
241
+ * longest of these, and the same user's requests skip the memos meanwhile.
242
+ * Absent or 0: nothing to cover.
243
+ */
244
+ readonly freshReadsWindowMs?: number;
245
+
246
+ /**
247
+ * @internal Drop this isolate's memoized copy of a shell (shell-memo.ts) so
248
+ * the next read goes to the store. A HIT whose doc record failed to decode
249
+ * calls it after replacing the entry with a tombstone: the memoized copy
250
+ * holds the same record. Built-in stores only; not part of the custom-store
251
+ * contract.
252
+ */
253
+ dropShellMemo?(key: string): void;
254
+
255
+ /**
256
+ * @internal The largest shell entry (prelude, postponed state, and snapshot
257
+ * bytes) this store can hold in one value. The capture refuses a bigger
258
+ * entry instead of letting the write fail inside waitUntil. Absent: the
259
+ * capture applies DEFAULT_SHELL_ENTRY_MAX_BYTES (Cloudflare KV's 25 MiB).
260
+ */
261
+ readonly maxShellEntryBytes?: number;
262
+
206
263
  /**
207
264
  * Declares the shell family present-but-inert: getShell/putShell exist but
208
265
  * no-op (a custom store whose backing tier is conditionally unavailable).
@@ -252,6 +309,20 @@ export interface SegmentCacheStore<TEnv = unknown> {
252
309
  * APIs delegate to. Receives ALL of one invalidation call's tags at once so
253
310
  * stores can batch their work (e.g. a single CDN purge request rather than
254
311
  * one per tag). Stores that do not support tags simply omit this method.
312
+ *
313
+ * Both verbs call it synchronously, inside the request that invalidates
314
+ * (#973); revalidateTag() then hands the returned promise to waitUntil
315
+ * without awaiting it. Read-your-own-writes for that request comes from
316
+ * what the store does before its first await: record the tags as
317
+ * invalidated in state its reads consult for the rest of the request (the
318
+ * built-in stores key a map by the request's root context and the store,
319
+ * so contexts derived from the request share it, and compare each hit's
320
+ * tags and write time against it), then start the
321
+ * durable write. Such a mask must only ever turn hits into misses, and only
322
+ * in that request: a durable write that later fails then costs extra
323
+ * misses, never a stale read. A store that records nothing before its
324
+ * first await still works; the request that ran revalidateTag() can read
325
+ * entries the invalidation covers until the durable write lands.
255
326
  * @param tags - The cache tags to invalidate
256
327
  */
257
328
  invalidateTags?(tags: string[]): Promise<void>;
@@ -262,12 +333,27 @@ export interface SegmentCacheStore<TEnv = unknown> {
262
333
  * freshness). Consulted by build-shell read-through and runtime shell stores:
263
334
  * "was it evicted" is answered by tag markers against the entry's createdAt,
264
335
  * including when invalidation races a capture that has not been written yet.
336
+ * Also the store's half of every cache write's gate (#977,
337
+ * tag-invalidation.ts predatesInvalidation): a "use cache", loader cache(),
338
+ * route cache() or document-cache write whose execution started before an
339
+ * invalidation another isolate made is skipped when this answers true, so
340
+ * it must reflect invalidations made after this request started reading
341
+ * (not a value memoized earlier in the request).
265
342
  * Optional: without it, TAGGED build entries are not served (untagged ones
266
- * are unaffected — they are evictable only by deploy/buildVersion anyway).
343
+ * are unaffected — they are evictable only by deploy/buildVersion anyway),
344
+ * and writes are gated by this isolate's invalidations only.
267
345
  * Fail open to `false` on marker-read errors: a transient store fault must
268
346
  * degrade to "still valid", the same posture as the envelope tag checks.
269
- */
270
- isTagsInvalidatedSince?(tags: string[], sinceMs: number): Promise<boolean>;
347
+ * The write gate passes `{ failClosed: true }` instead: a marker it cannot
348
+ * read (an error, a timed-out read) answers `true`, since a skipped write
349
+ * only costs a later miss. A store that ignores the option still works;
350
+ * its fault then lets that write through.
351
+ */
352
+ isTagsInvalidatedSince?(
353
+ tags: string[],
354
+ sinceMs: number,
355
+ options?: { failClosed?: boolean },
356
+ ): Promise<boolean>;
271
357
  }
272
358
 
273
359
  /**
@@ -334,49 +420,51 @@ export interface ShellCacheEntry {
334
420
  * on dev RSC-module edits). The second read-time gate: a persistent shared
335
421
  * store (KV/runtime-cache) survives deploys, and an app-code change that
336
422
  * keeps the same React version would otherwise leave a stale-build
337
- * prelude+postponed live under the same key. Optional only for entries
338
- * stored before the field existed — those are treated as a miss and the
339
- * recapture re-stamps them (pre-release, no compat shim).
423
+ * prelude+postponed live under the same key. A custom store returns it as
424
+ * putShell received it.
340
425
  */
341
- buildVersion?: string;
426
+ buildVersion: string;
342
427
  /**
343
- * The initialTheme the CAPTURE render was built with (the derived context's
344
- * reqCtx.theme). The resume tail must render ThemeProvider with the SAME
428
+ * The initialTheme the CAPTURE render was built with: the no-cookie default
429
+ * (payloadInitialTheme, rsc/full-payload.ts), never the capturing visitor's
430
+ * theme (#971). The resume tail must render ThemeProvider with the SAME
345
431
  * initialTheme the frozen prelude was rendered with: React resume requires the
346
432
  * tree above the holes to match the prerendered tree, and initialTheme is
347
433
  * per-request METADATA, not part of the cached segments — a visitor whose
348
- * theme differs from the capturer's would otherwise produce a divergent resume
434
+ * theme differs from the capture's would otherwise produce a divergent resume
349
435
  * tree (broken stitching/hydration). The visitor's real theme is applied
350
436
  * pre-paint by the FOUC script and re-synced from the cookie post-mount by
351
437
  * ThemeProvider.
352
438
  */
353
439
  initialTheme?: string;
354
440
  /**
355
- * The CAPTURE DATA SNAPSHOT: every cache-store read-hit and write the capture
356
- * render performed, in stored/serialized form. Replaying these on a HIT (via
357
- * the SeededShellStore overlay, for the tail render only) reproduces the
358
- * shell's cached content byte-identically, so the freshly rendered hydration
359
- * payload matches the frozen prelude even after the underlying cache entries
360
- * have drifted (expired, been recomputed, or been tag-invalidated).
361
- *
362
- * Optional: an entry captured before this field existed simply has no
363
- * snapshot and keeps the pre-snapshot behavior (the tail reads live, so any
364
- * shell-baked cached value that drifted mismatches the prelude). Recapture
365
- * heals it. See docs/design/ppr-shell-resume.md ("the capture data snapshot").
366
- */
367
- snapshot?: ShellSnapshotRecord[];
368
- /**
369
- * The key of the CANONICAL document segment record inside `snapshot` — the
370
- * one navigation replay can actually consume (resolved under the implicit
371
- * doc namespace at capture; see CacheScope.cacheRoute). Replay eligibility
372
- * requires this exact record: the snapshot also carries incidentally
373
- * recorded explicit-tier records (RecordingShellStore passthroughs) whose
374
- * keys a partial lookup can never resolve, and counting those declared
375
- * entries "replayable" that always missed (`snapshot-miss` flip-flop).
376
- * Absent on entries captured before the field existed OR when the capture
377
- * recorded no doc record (cache(false)/condition-false routes, prerender
378
- * short-circuit) — both read as `no-segment-snapshot`; recapture heals the
379
- * former.
441
+ * The CAPTURE DATA SNAPSHOT, in stored/serialized form: the doc segment
442
+ * record every HIT replays the handler layer from (`docKey`) and the
443
+ * bake-lane loader pins (pruned to what a HIT reads, issue #941). Replaying
444
+ * them on a HIT keeps the freshly rendered hydration payload equal to the
445
+ * frozen prelude after the underlying cache entries have drifted; every
446
+ * other read on a HIT, a hole's included, reads the store. Empty when the capture
447
+ * recorded nothing (a prerender-served capture without bake-lane loaders,
448
+ * a tombstone). A custom store returns it as putShell received it. See
449
+ * docs/design/ppr-shell-resume.md ("the capture data snapshot").
450
+ */
451
+ snapshot: ShellSnapshotRecord[];
452
+ /**
453
+ * Records the capture dropped from `snapshot` because no reader of this
454
+ * entry consumes them, by family (`loader:1`). Diagnostic only: the
455
+ * HIT tail timing reports it next to the kept records. See
456
+ * docs/design/shell-entry-layout.md ("Record only what a HIT reads").
457
+ */
458
+ prunedRecords?: string;
459
+ /**
460
+ * The key of the document segment record inside `snapshot`: the handler
461
+ * layer every HIT replays (a document HIT tail looks the record up by this
462
+ * key, so a store keyGenerator or a build-time capture host cannot send it
463
+ * elsewhere) and the record navigation replay consumes (resolved under the
464
+ * implicit doc namespace at capture; see CacheScope.cacheRoute). A capture
465
+ * that ran handlers always stores it; only a prerender-served entry (the
466
+ * prerender store supplies the handler layer) has none. A document entry
467
+ * without it is served as a MISS.
380
468
  */
381
469
  docKey?: string;
382
470
  /**
@@ -385,30 +473,17 @@ export interface ShellCacheEntry {
385
473
  * the partial request's headers and middleware state are not document state.
386
474
  */
387
475
  navigationOnly?: true;
388
- /**
389
- * True when the capture's HANDLER layer declared per-request liveness: a
390
- * handle pushed OUTSIDE a DSL loader scope carried a nested thenable (the
391
- * capture mask turns it into a never-filling hole), such a push was still
392
- * pending when the entry was written, or a handler-invoked loader
393
- * (ctx.use(loader) from a handler body — the consumption lane, #672)
394
- * executed during the capture. The serve tail then must NOT take the
395
- * handler-free fast path (the implicit doc-cache hit): only a handler
396
- * re-run can mint that hole's live promise or refresh that consumed value.
397
- * DSL-loader pushes never set this — loaders re-run fresh on every HIT, so
398
- * their holes always fill.
399
- */
400
- handlerLiveHoles?: boolean;
401
- /**
402
- * Legacy/fallback marker for a transition({ when }) predicate that was not
403
- * evaluated by the PPR pre-handler gate. Such an entry stays conservatively
404
- * ineligible for handler-free replay. New PPR matches evaluate known segment
405
- * predicates before the pipeline and do not set this marker.
406
- */
407
- transitionWhen?: true;
408
476
  /** Capture-generation start time; tag invalidations at or after it win. */
409
477
  createdAt: number;
410
478
  }
411
479
 
480
+ /**
481
+ * @internal A shell entry without its prelude and snapshot: what a
482
+ * prelude-first read (SegmentCacheStore.readShellDocument) resolves with
483
+ * before the snapshot bytes arrive.
484
+ */
485
+ export type ShellEntryHead = Omit<ShellCacheEntry, "prelude" | "snapshot">;
486
+
412
487
  /**
413
488
  * A shell entry whose document half is present — what the document HIT path
414
489
  * (serveShellHit / lookupBuildShell) consumes. hasIntactShellPayload
@@ -421,15 +496,122 @@ export type DocumentShellCacheEntry = ShellCacheEntry & {
421
496
  postponed: string | null;
422
497
  };
423
498
 
499
+ /** @internal Options of {@link SegmentCacheStore.readShellDocument}. */
500
+ export interface ShellDocumentReadOptions {
501
+ /**
502
+ * Tag names the route declares for its shell (`ppr.tags`), known before the
503
+ * entry is read: their marker reads can start alongside the entry read.
504
+ */
505
+ tagHints?: readonly string[];
506
+ }
507
+
508
+ /**
509
+ * @internal Why a prelude-first read's snapshot is missing although the entry
510
+ * stored one: `unavailable` (the read timed out or failed; the entry may be
511
+ * sound) or `corrupt` (the bytes arrived and do not parse; the store evicted
512
+ * the entry).
513
+ */
514
+ export type ShellSnapshotFailure = "unavailable" | "corrupt";
515
+
516
+ /**
517
+ * @internal Result of {@link SegmentCacheStore.readShellDocument}.
518
+ */
519
+ export interface ShellDocumentRead {
520
+ /** The entry without its prelude and snapshot (delivered separately). */
521
+ entry: ShellEntryHead;
522
+ /** Raw prelude bytes (empty for a navigationOnly entry). */
523
+ prelude: Uint8Array;
524
+ shouldRevalidate?: boolean;
525
+ /**
526
+ * The capture snapshot, read after the prelude. Resolves undefined when the
527
+ * entry has none or it could not be read (`snapshotFailure` says which); a
528
+ * document HIT then cannot replay its doc record and degrades
529
+ * (rsc-rendering.ts serveShellHit).
530
+ * Never rejects.
531
+ */
532
+ snapshot: Promise<ShellSnapshotRecord[] | undefined>;
533
+ /**
534
+ * Why `snapshot` resolved undefined although the entry stored one, or
535
+ * undefined when it did not fail. A document HIT that cannot replay its
536
+ * doc record replaces the entry only when it is broken, not when its read
537
+ * was merely slow (rsc-rendering.ts serveShellHit). Never rejects.
538
+ */
539
+ snapshotFailure?: Promise<ShellSnapshotFailure | undefined>;
540
+ /**
541
+ * Where the read's time and bytes went, for the `debugPerformance` metrics
542
+ * (rsc-rendering.ts) and the store's own debug trace. Present only when one
543
+ * of them is on for the request.
544
+ */
545
+ stats?: ShellReadStats;
546
+ }
547
+
548
+ /**
549
+ * @internal Sub-steps of a prelude-first shell read. Times are ms. workerd
550
+ * advances its clock only on I/O, so a CPU-only field
551
+ * (`snapshot.parseMs`) reads 0 on a deployed worker: the byte counts are the
552
+ * cost signal there.
553
+ */
554
+ export interface ShellReadStats {
555
+ /**
556
+ * The tier that answered: `memo` (the store's shell memo), `l1`/`kv`
557
+ * (CFCacheStore), or `store` (a single-tier store, VercelCacheStore).
558
+ */
559
+ tier: "memo" | "l1" | "kv" | "store";
560
+ /** The per-isolate memo's outcome for this read and its size after it. */
561
+ memo?: { hit: boolean; bytes: number };
562
+ /**
563
+ * A KV read after an L1 attempt: how long the L1 attempt took and why it
564
+ * missed. The KV fields below then restart from the KV read.
565
+ */
566
+ l1MissMs?: number;
567
+ l1MissReason?: string;
568
+ /** Cache API match (L1) or KV get (KV) until the body was available. */
569
+ matchMs?: number;
570
+ /** Frame head read (I/O) and parse. */
571
+ headMs?: number;
572
+ /** Prelude bytes read. */
573
+ preludeMs?: number;
574
+ /** Tag-marker read, start to resolve (runs alongside the prelude read). */
575
+ markerMs?: number;
576
+ /** How long the read waited for the marker after the prelude was read. */
577
+ markerWaitMs?: number;
578
+ /**
579
+ * The marker check was awaited after the entry read (VercelCacheStore), not
580
+ * alongside the prelude read; hinted reads still start with the entry read.
581
+ */
582
+ markerSerial?: true;
583
+ /**
584
+ * How the isolate marker memo answered the shell's tags (the most
585
+ * store-bound outcome across them): `fresh`, `stale` (served, refreshing in
586
+ * the background), `read` (from the store), `bypass` (fresh-reads cookie).
587
+ */
588
+ markerMemo?: "fresh" | "stale" | "read" | "bypass";
589
+ /** Tags whose marker reads were started before the entry's head (hints). */
590
+ markerHinted?: readonly string[];
591
+ /** When the hinted marker reads started (performance.now()). */
592
+ markerHintStartedAt?: number;
593
+ /** How many of the shell's tags were hinted. */
594
+ markerHintHits?: number;
595
+ /** How long the hinted marker reads ran before the entry named its tags. */
596
+ markerLeadMs?: number;
597
+ /** The request carried the fresh-reads cookie: no isolate memo was used. */
598
+ freshReads?: true;
599
+ headBytes?: number;
600
+ preludeBytes?: number;
601
+ /** Number of the shell's tags the marker read covered. */
602
+ tags?: number;
603
+ /** Filled after the commit, once the snapshot has been read. */
604
+ snapshot?: { readMs: number; parseMs: number; bytes: number };
605
+ }
606
+
424
607
  /**
425
- * The families a shell snapshot pins. The item/segment/response families are
426
- * cache-store reads/writes (recorded by RecordingShellStore); the loader family
427
- * pins the settled CONTAINER of a bake-lane loader (lane rule: see
428
- * resolveLoaderData, loader-cache.ts). Excludes the shell family itself
429
- * (getShell/putShell) — the snapshot rides INSIDE a shell entry, so recording
430
- * it would be self-referential.
608
+ * The families a shell snapshot pins: the doc segment record (recorded by
609
+ * RecordingShellStore) and the settled CONTAINER of each bake-lane loader
610
+ * (lane rule: see resolveLoaderData, loader-cache.ts). No cache read is
611
+ * pinned: a HIT's holes, and a bake-lane loader body that runs on the HIT,
612
+ * read the store.
431
613
  */
432
- export type ShellSnapshotFamily = "item" | "segment" | "response" | "loader";
614
+ export type ShellSnapshotFamily = "segment" | "loader";
433
615
 
434
616
  /**
435
617
  * The stored form of a loader-family snapshot value: the bake-lane loader's
@@ -446,49 +628,35 @@ export interface ShellSnapshotLoaderValue {
446
628
  * container carries hole markers, so a HIT must gate the overlay on the
447
629
  * fresh run (only the loader body can mint the live nested promises);
448
630
  * 0 = fully pinned, so a HIT resolves the payload promise immediately from
449
- * the pin while the fresh run proceeds ungated (side effects and cache
450
- * read-through writes preserved; its values were discarded either way —
451
- * recorded paths win wholesale). Absent on pre-bit snapshots, which keep
452
- * the gated path.
631
+ * the pin and does not run the loader body (unless `runs`).
453
632
  */
454
- holes?: 0 | 1;
455
- }
456
-
457
- /** A serialized cached Response for the response family of a shell snapshot. */
458
- export interface ShellSnapshotResponseValue {
459
- status: number;
460
- /** Client-facing header pairs (per-client signal headers excluded at record). */
461
- headers: [string, string][];
462
- /** base64-encoded response body (binary-safe, JSON-serializable). */
463
- body: string;
464
- }
465
-
466
- /** The stored form of an item-family (use cache / loader cache) snapshot value. */
467
- export interface ShellSnapshotItemValue {
468
- /** RSC-serialized return value. */
469
- value: string;
470
- /** RSC-encoded handle data, if any. */
471
- handles?: string;
472
- /** The entry's cache tags. */
473
- tags?: string[];
633
+ holes: 0 | 1;
634
+ /**
635
+ * 1 when the capture saw a loader push it could not record (a deferred
636
+ * push, one with masked nested promises, or one made outside any loader
637
+ * body): a HIT then still runs the loader body in the background
638
+ * (pin-first) so those pushes reach the page. 0: a hole-free record is
639
+ * served from the pin alone and its body does not run on a HIT.
640
+ *
641
+ * Both bits are always written. A record stored before they existed (v0.17)
642
+ * reads each missing bit as 1 (buildShellLoaderSeed): its snapshot lacks
643
+ * the loader-owned pushes, so the body supplies them.
644
+ */
645
+ runs: 0 | 1;
474
646
  }
475
647
 
476
648
  /**
477
- * One recorded cache-store read-hit or write from the capture render. `value`
478
- * carries the entry in its stored/serialized shape so it round-trips through a
479
- * JSON-serializing store (KV, CF, Vercel) with the rest of the ShellCacheEntry:
480
- * - `item` -> {@link ShellSnapshotItemValue}
649
+ * One snapshot record: the doc segment record or a bake-lane loader pin.
650
+ * `value` carries it in its stored/serialized shape so it round-trips
651
+ * through a JSON-serializing store (KV, CF, Vercel) with the rest of the
652
+ * ShellCacheEntry:
481
653
  * - `segment` -> {@link CachedEntryData} (already JSON-able)
482
- * - `response`-> {@link ShellSnapshotResponseValue}
654
+ * - `loader` -> {@link ShellSnapshotLoaderValue}
483
655
  */
484
656
  export interface ShellSnapshotRecord {
485
657
  family: ShellSnapshotFamily;
486
658
  key: string;
487
- value:
488
- | ShellSnapshotItemValue
489
- | CachedEntryData
490
- | ShellSnapshotResponseValue
491
- | ShellSnapshotLoaderValue;
659
+ value: CachedEntryData | ShellSnapshotLoaderValue;
492
660
  }
493
661
 
494
662
  /**
@@ -591,8 +759,12 @@ export type SegmentHandleData = Record<string, unknown[]>;
591
759
  /**
592
760
  * segmentId -> handleName -> the owning loader id of each recorded value, by
593
761
  * index (null: not owned). Only a PPR shell capture writes owners: the
594
- * settled pushes of an `ssr: false` loader's own body, which the record keeps
595
- * because the prelude rendered them. restoreHandles replays an owned value
596
- * through HandleStore.pushReplayed, so that loader's live re-run replaces it.
762
+ * settled pushes of the loader bodies it ran (an `ssr: false` loader's own,
763
+ * the loaders it awaits, and its own cache() replays), which the record keeps
764
+ * because the prelude rendered them. A document HIT restores a bake-lane
765
+ * owner's value through HandleStore.pushRestored, and it stands against a run
766
+ * of its loader there; a live-lane owner's (a hole), and every owner's on
767
+ * any other replay of the record (a client navigation), go through
768
+ * pushReplayed, so a run of the loader replaces it with its live push.
597
769
  */
598
770
  export type HandleOwners = Record<string, Record<string, (string | null)[]>>;