@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -7,11 +7,18 @@
7
7
  */
8
8
  import type { ResolvedSegment } from "../types.js";
9
9
  import type { HandleStore } from "../server/handle-store.js";
10
- import type { SegmentHandleData } from "./types.js";
10
+ import type { HandleOwners, SegmentHandleData } from "./types.js";
11
11
  type HandleRecord = Record<string, SegmentHandleData>;
12
- export declare function encodeHandles(handles: HandleRecord): Promise<string>;
12
+ /**
13
+ * `onError` reaches the Flight encode (see serializeResult). A cache writer
14
+ * passes the collector it gives the value's encode, so a handle value that
15
+ * fails to encode (a rejected pushed promise) refuses the whole entry like a
16
+ * failed value does; prerender (prerender-match.ts) passes one too and
17
+ * hands the error to its prerender.onError policy.
18
+ */
19
+ export declare function encodeHandles(handles: HandleRecord, onError?: (error: unknown) => void): Promise<string>;
13
20
  export declare function decodeHandles(encoded: string): Promise<HandleRecord | null>;
14
- export declare function encodeHandleValue(value: unknown): Promise<string>;
21
+ export declare function encodeHandleValue(value: unknown, onError?: (error: unknown) => void): Promise<string>;
15
22
  /**
16
23
  * Decode a Flight-encoded handle-data string. Returns null on any decode
17
24
  * failure so the caller can skip handle restore without discarding valid
@@ -22,18 +29,52 @@ export declare function decodeHandleValue<T>(encoded: string): Promise<T | null>
22
29
  * Capture handle data for a set of segments from the handle store.
23
30
  * Used when caching segments to preserve their handle data.
24
31
  *
25
- * `exclude` (shell captures: RequestContext._shellCaptureLoaderHandleValues)
26
- * drops DSL-loader-scoped push values from the CACHE WRITE only: loaders
27
- * re-run fresh on every HIT, so replaying their captured values would
28
- * duplicate the fresh push — and their masked nested promises would stall the
29
- * Flight handle encode to its timeout. Threaded as an explicit argument so
30
- * every other getDataForSegment consumer (the render-barrier snapshot,
31
- * prerender) provably sees every push.
32
+ * DSL-loader pushes stay out of the CACHE WRITE: a HIT runs loaders exactly
33
+ * as an uncached render of the same request would (cache-lookup.ts
34
+ * resolveFreshLoadersAndYield), so replaying a recorded copy would duplicate
35
+ * the live push. Handler pushes, including
36
+ * handler-invoked ctx.use(Loader) bodies (skipped with their handler on a
37
+ * HIT), are kept. The store tags loader pushes by array position at push
38
+ * time, so primitive values are covered. A PPR shell capture untags the
39
+ * settled bake-lane pushes its record keeps (shell-capture.ts) and gives
40
+ * them an owner (captureHandleOwners). Only this call site passes
41
+ * excludeLoaderPushes; every other getDataForSegment consumer (the
42
+ * render-barrier snapshot, prerender) sees every push.
32
43
  */
33
- export declare function captureHandles(segments: ResolvedSegment[], handleStore: HandleStore, exclude?: WeakSet<object>): Record<string, SegmentHandleData>;
44
+ export declare function captureHandles(segments: ResolvedSegment[], handleStore: HandleStore): Record<string, SegmentHandleData>;
45
+ /** The owners of captureHandles' values; undefined when none is owned. */
46
+ export declare function captureHandleOwners(segments: ResolvedSegment[], handleStore: HandleStore): HandleOwners | undefined;
34
47
  /**
35
48
  * Restore handle data from a cached snapshot into the handle store.
36
- * Used when serving cached segments to replay their handle data.
49
+ * Used when serving cached segments to replay their handle data: a route
50
+ * cache() record owns its segments' arrays, so it REPLACES them.
51
+ *
52
+ * A segment with `owners` is replaced with empty arrays and re-pushed in
53
+ * recorded order, an owned value through pushReplayed: if the owning loader
54
+ * runs on the HIT (a live loader, or a promise-carrying `ssr: false` one;
55
+ * a promise-free one is served from the shell and does not), its live pushes
56
+ * replace the recorded ones in place instead of appending a second copy. `claim`
57
+ * (setupLoaderAccess _claimLoaderPushes) is asked once per owner, so the
58
+ * loader's own cache() HIT does not replay its pushes a second time
59
+ * (loader-cache.ts replayLoaderHandles); an owner it refuses is skipped. A
60
+ * record without owners restores as a plain replay.
61
+ */
62
+ export declare function restoreHandles(handles: Record<string, SegmentHandleData>, handleStore: HandleStore, owners?: HandleOwners, claim?: (loaderId: string) => boolean): void;
63
+ /**
64
+ * Append recorded handle pushes to the store, in recorded order, via push()
65
+ * so active captures and loader-scope tagging see them like live pushes. For
66
+ * a cached unit that shares its segments with live pushes ("use cache");
67
+ * restoreHandles would wipe those. `segmentId` redirects every value to one
68
+ * segment (the caller's).
69
+ *
70
+ * Owner-keyed groups (useCacheRecordKey, `${seq}:${loaderId}`) are a loader's
71
+ * pushes, which reach the page once per request: `claim` (setupLoaderAccess
72
+ * _claimLoaderPushes) skips a loader that already ran or was replayed in this
73
+ * request, and a claimed loader that runs later replaces its replayed values
74
+ * (HandleStore.pushReplayed). `${seq}:` groups are the function's own pushes.
75
+ * A key without ":" is a segment id (records written before owner keys) and
76
+ * replays unchanged. A loader's own cache() replays through loader-cache.ts
77
+ * replayLoaderHandles.
37
78
  */
38
- export declare function restoreHandles(handles: Record<string, SegmentHandleData>, handleStore: HandleStore): void;
79
+ export declare function appendHandles(handles: Record<string, SegmentHandleData>, handleStore: HandleStore, segmentId?: string, claim?: (loaderId: string) => boolean): void;
39
80
  export {};
@@ -1,6 +1,7 @@
1
1
  export type { SegmentCacheStore, CachedEntryData, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry, SerializedSegmentData, SegmentHandleData, CacheReadError, } from "./types.js";
2
2
  export { CACHE_READ_ERROR } from "./types.js";
3
3
  export { MemorySegmentCacheStore } from "./memory-segment-store.js";
4
+ export type { StoreMemoOptions } from "./shell-memo.js";
4
5
  export { CFCacheStore, type CFCacheStoreOptions, type CFCacheDebug, type CFCacheReadDebugEvent, type KVNamespace, CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER, CACHE_REVALIDATING_AT_HEADER, EDGE_LOOKUP_TIMEOUT_MS, EDGE_READ_TIMEOUT_MS, KV_READ_TIMEOUT_MS, createCloudflareZonePurge, type CloudflareZonePurgeOptions, } from "./cf/index.js";
5
6
  export { VercelCacheStore, type VercelCacheStoreOptions, type VercelRuntimeCache, type VercelCacheDebug, type VercelCacheReadDebugEvent, type VercelCacheReadOutcome, VERCEL_MAX_ITEM_BYTES, VERCEL_MAX_TAGS_PER_ITEM, VERCEL_MAX_TAG_BYTES, } from "./vercel/index.js";
6
7
  export { TRACKING_SEARCH_PARAMS, type CacheSearchParams, } from "./search-params-filter.js";
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Per-isolate tag-marker memos shared by the built-in stores (issue #941;
3
+ * docs/design/shell-entry-layout.md "The tag-marker memo").
4
+ *
5
+ * A tagged shell HIT waits for its tag-invalidation markers before the first
6
+ * byte. Two things here shorten that wait:
7
+ *
8
+ * - {@link TagNameHints}: which tag NAMES a shell key carried last time, so a
9
+ * HIT can start the marker reads alongside the entry read instead of after
10
+ * the entry's head names its tags. Never marker values.
11
+ * - {@link TagMarkerMemo}: the marker VALUES (a tag's latest invalidatedAt,
12
+ * or null), served stale-while-revalidate. A value younger than
13
+ * `markerFreshMs` is used as is; one younger than `markerMaxStaleMs` is used
14
+ * and refreshed from the store in the background; an older one blocks on
15
+ * the store read as before.
16
+ *
17
+ * Simple reads get SWR; a mutation gets correct reads. The request that runs
18
+ * updateTag()/revalidateTag() reads its own writes through the per-request
19
+ * memo; the isolate that runs it writes the new value through into this memo;
20
+ * and its response sets the fresh-reads cookie (request-context.ts), so the
21
+ * same user's next requests on OTHER isolates skip this memo and the shell
22
+ * memo for as long as either can be stale. Other users can see a marker up to
23
+ * `markerMaxStaleMs` old, on top of the platform's own consistency
24
+ * (Cloudflare KV is eventually consistent across locations, up to about 60 s;
25
+ * Vercel's `tm` markers are regional).
26
+ *
27
+ * Only PPR shell reads use the value memo. Cached data families (`cache()`,
28
+ * `"use cache"`, responses) and the shell write gate
29
+ * (`isTagsInvalidatedSince`) keep reading their markers, in every request,
30
+ * whatever its shell read found: a shell read's memoized values never reach
31
+ * the data families' per-request memo (cf-tag-marker-memo.ts
32
+ * ShellMarkerReads).
33
+ */
34
+ import type { RequestContext } from "../server/request-context.js";
35
+ import type { ShellReadStats } from "./types.js";
36
+ /** Default freshness of a memoized marker on CFCacheStore (ms). */
37
+ export declare const DEFAULT_CF_MARKER_FRESH_MS = 1000;
38
+ /** Default freshness of a memoized marker on VercelCacheStore (ms). */
39
+ export declare const DEFAULT_VERCEL_MARKER_FRESH_MS = 300;
40
+ /** Default oldest memoized marker served while it refreshes (ms), CF. */
41
+ export declare const DEFAULT_CF_MARKER_MAX_STALE_MS = 10000;
42
+ /**
43
+ * The same cap on VercelCacheStore: the runtime cache expires tags globally
44
+ * within about 300 ms, so a much longer stale window would widen that for
45
+ * other users more than KV's own up-to-60 s cross-location lag does on CF.
46
+ */
47
+ export declare const DEFAULT_VERCEL_MARKER_MAX_STALE_MS = 2000;
48
+ /**
49
+ * Most shell keys a hint map holds (least recently used evicted).
50
+ * @internal Exported for tests that fill the map to its cap.
51
+ */
52
+ export declare const TAG_HINTS_MAX_ENTRIES = 2048;
53
+ /** How a marker read was answered, for the debugPerformance marker row. */
54
+ export type MarkerMemoOutcome = "fresh" | "stale" | "read" | "bypass";
55
+ /** A memoized marker a read may use: its value, and whether it is stale. */
56
+ export interface MarkerMemoHit {
57
+ value: number | null;
58
+ stale: boolean;
59
+ }
60
+ /**
61
+ * A store read of a marker for {@link TagMarkerMemo.readThrough}: the value,
62
+ * and whether it may be memoized (a timed-out read that failed open may not).
63
+ */
64
+ export interface MarkerFetch {
65
+ value: number | null;
66
+ memoize: boolean;
67
+ }
68
+ /** Options of {@link TagMarkerMemo.readThrough}. */
69
+ export interface MarkerReadThroughOptions {
70
+ /** Fresh window (ms); 0 or less reads the store and memoizes nothing. */
71
+ freshMs: number;
72
+ /** The oldest value served while it refreshes (ms). */
73
+ maxStaleMs: number;
74
+ /** The request carries the fresh-reads cookie: read the store. */
75
+ bypass: boolean;
76
+ /** Keeps a background refresh alive after the response (waitUntil). */
77
+ keepAlive?: (pending: Promise<void>) => void;
78
+ /** How the read was answered, for the debugPerformance marker row. */
79
+ onOutcome?: (outcome: MarkerMemoOutcome) => void;
80
+ }
81
+ export declare class TagMarkerMemo {
82
+ private readonly maxEntries;
83
+ private readonly slots;
84
+ private readonly refreshing;
85
+ constructor(maxEntries?: number);
86
+ /**
87
+ * The memoized marker for `key` if it is younger than `maxStaleMs` (stale
88
+ * when older than `freshMs`), or undefined when the caller must read the
89
+ * store. `freshMs <= 0` disables the memo.
90
+ */
91
+ lookup(key: string, freshMs: number, maxStaleMs: number, now?: number): MarkerMemoHit | undefined;
92
+ /**
93
+ * Record a marker the store returned to a read that started at
94
+ * `readStartedAt`. The age counts from the read's start, not its end: a
95
+ * marker written while the read was in flight may be missing from it, so
96
+ * the value is only known to be current as of the start. Markers only move
97
+ * forward, so a value older than one already held (a write-through that
98
+ * landed during the read) is kept at the newer value, with the later stamp.
99
+ */
100
+ store(key: string, value: number | null, readStartedAt?: number): void;
101
+ /**
102
+ * Refresh a stale marker in the background: one read per key at a time,
103
+ * handed to `keepAlive` (the store's waitUntil) so the runtime lets it
104
+ * finish after the response; without it the read runs detached. A read
105
+ * that fails, or may not be memoized, stores nothing.
106
+ */
107
+ refresh(key: string, read: () => Promise<MarkerFetch>, keepAlive?: (pending: Promise<void>) => void): void;
108
+ /**
109
+ * A marker through the memo, stale-while-revalidate: a value within
110
+ * `freshMs` is used as is; one within `maxStaleMs` is used while one
111
+ * background read refreshes it; otherwise, and for a request carrying the
112
+ * fresh-reads cookie, the store is read (`fetch(false)`) and the result
113
+ * memoized. `fetch(true)` is the background refresh, outside any request.
114
+ */
115
+ readThrough(key: string, fetch: (background: boolean) => Promise<MarkerFetch>, options: MarkerReadThroughOptions): Promise<number | null>;
116
+ clear(): void;
117
+ }
118
+ /**
119
+ * The tag names a shell key's entry carried when it was last read or written
120
+ * in this isolate, so the next HIT can start their marker reads before the
121
+ * entry names them. A wrong or missing hint costs a wasted read or nothing;
122
+ * the freshness check always uses the entry's own tags.
123
+ */
124
+ export declare class TagNameHints {
125
+ private readonly maxEntries;
126
+ private readonly keys;
127
+ constructor(maxEntries?: number);
128
+ get(key: string): readonly string[] | undefined;
129
+ remember(key: string, tags: readonly string[] | undefined): void;
130
+ clear(): void;
131
+ }
132
+ /** The union of a key's remembered tag names and the route's static tags. */
133
+ export declare function hintedTags(remembered: readonly string[] | undefined, routeTags: readonly string[] | undefined): string[];
134
+ /**
135
+ * The marker row's memo outcome for a read's tags: the most store-bound one
136
+ * (a bypass or store read outranks a stale value, which outranks a fresh one).
137
+ */
138
+ export declare function summarizeMarkerOutcomes(tags: readonly string[] | undefined, outcomes: ReadonlyMap<string, MarkerMemoOutcome>): MarkerMemoOutcome | undefined;
139
+ /**
140
+ * Fill a shell read's marker-row stats (debugPerformance) for the entry's
141
+ * own tags: the memo outcome, and how many of them the read had hinted.
142
+ */
143
+ export declare function recordMarkerRow(stats: ShellReadStats, tags: readonly string[] | undefined, outcomes: ReadonlyMap<string, MarkerMemoOutcome> | undefined): void;
144
+ /**
145
+ * True when this request carries the fresh-reads cookie: a request of the same
146
+ * user after an updateTag()/revalidateTag(), which must not be served from an
147
+ * isolate memo (shell or marker) that could predate the mutation.
148
+ */
149
+ export declare function freshReadsRequired(ctx: Pick<RequestContext, "_freshReads"> | undefined): boolean;
@@ -23,7 +23,7 @@ export declare function stringToStream(str: string): ReadableStream<Uint8Array>;
23
23
  * For contexts where null is a valid result (loader caching, "use cache"),
24
24
  * use serializeResult() instead which preserves null through RSC Flight.
25
25
  */
26
- export declare function rscSerialize(value: unknown): Promise<string | undefined>;
26
+ export declare function rscSerialize(value: unknown, onError?: (error: unknown) => void): Promise<string | undefined>;
27
27
  /**
28
28
  * RSC-deserialize a value from a stored string.
29
29
  */
@@ -36,7 +36,7 @@ export declare function rscDeserialize<T>(encoded: string | undefined): Promise<
36
36
  *
37
37
  * Returns null only on serialization failure.
38
38
  */
39
- export declare function serializeResult(value: unknown): Promise<string | null>;
39
+ export declare function serializeResult(value: unknown, onError?: (error: unknown) => void): Promise<string | null>;
40
40
  /**
41
41
  * RSC-deserialize a cached result string.
42
42
  * Counterpart to serializeResult() — always receives a non-empty string.
@@ -52,8 +52,15 @@ export declare const deserializeComponent: (encoded: string) => Promise<unknown>
52
52
  * Serialize segments for storage.
53
53
  * Each segment's component, layout, loading, and loaderData are RSC-serialized.
54
54
  * Metadata is preserved as-is.
55
+ *
56
+ * `onError` goes to every Flight encode. A component that throws while
57
+ * encoding (e.g. an async server component in the tree) does not reject the
58
+ * encode: Flight reports it through onError and writes an error row
59
+ * (`1:E{...}`) that throws when the decoded tree renders. cacheRoute and the
60
+ * prerender producers (prerender-match.ts) pass it to refuse such an entry;
61
+ * without it the encode is unchanged.
55
62
  */
56
- export declare function serializeSegments(segments: ResolvedSegment[]): Promise<SerializedSegmentData[]>;
63
+ export declare function serializeSegments(segments: ResolvedSegment[], onError?: (error: unknown) => void): Promise<SerializedSegmentData[]>;
57
64
  /**
58
65
  * Build ResolvedSegments that carry the STORED fragment strings verbatim
59
66
  * instead of decoding them (PPR fast-path payload splice, issue #700; see
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Per-isolate memo of fresh PPR shell reads (issue #941, decision 1;
3
+ * docs/design/shell-entry-layout.md).
4
+ *
5
+ * A warm isolate serving the same shell many times a second re-reads, and
6
+ * re-parses, the same stored entry on every HIT. The store keeps the last
7
+ * fresh read per key for a short window (`memo.shellMs`) and serves the next
8
+ * HIT from memory. What the memo does NOT skip is the part that decides
9
+ * whether the entry may be served: the store still runs its tag-marker check
10
+ * on every read, so with KV bound `updateTag()` / `revalidateTag()` stay
11
+ * immediate across isolates. The isolate that runs the invalidation drops its
12
+ * own copies and keeps reads that were in flight from memoizing them again
13
+ * (RecentTagInvalidations). What another isolate can serve for up to one
14
+ * window: a newer capture of the same key; a KV-less purge-mode purge; on
15
+ * Vercel, an invalidation from another region (tag markers are regional) and
16
+ * a platform `expireTag` issued outside rango (it writes no tag marker).
17
+ *
18
+ * Store instances are usually created per request, so the memo must outlive
19
+ * them: CFCacheStore keeps one module-level memo per isolate, VercelCacheStore
20
+ * one per runtime-cache handle (the handle carries the namespace). One byte
21
+ * budget (`memo.shellMaxBytes`) caps a memo; storing sweeps entries past their
22
+ * window, then evicts the least recently used. Measured on a 3.2 MB shell
23
+ * (Node 22 heap): CFCacheStore holds 1.08x what it counts (prelude plus
24
+ * snapshot JSON); VercelCacheStore holds 0.93x (envelope JSON plus the
25
+ * decoded prelude).
26
+ */
27
+ /**
28
+ * Per-isolate memos a built-in store keeps (`CFCacheStore`: one per isolate;
29
+ * `VercelCacheStore`: one per `cache` handle). A window of 0 turns that memo
30
+ * off.
31
+ */
32
+ export interface StoreMemoOptions {
33
+ /**
34
+ * PPR shell memo window (ms): after a shell read, the next HITs of that key
35
+ * within the window are served from memory, with no store read and no
36
+ * parse. Only a fresh shell whose snapshot read completed is kept. The
37
+ * tag-marker check still runs on every HIT, and the isolate that runs
38
+ * `updateTag()` / `revalidateTag()` drops its own copies, so neither that
39
+ * request nor a later one on the same isolate gets the invalidated shell.
40
+ * What ANOTHER isolate can serve for up to one window:
41
+ * - a newer capture of the same key;
42
+ * - `CFCacheStore` without KV, in purge mode: a purged shell. The purge
43
+ * removes the stored entry, not other isolates' memos;
44
+ * - `VercelCacheStore`: an invalidation from another region (the tag
45
+ * markers are a regional `cache.set`; only `expireTag` is global), and a
46
+ * platform `expireTag` issued outside rango.
47
+ *
48
+ * The mutating user is not served these: the response of a request that
49
+ * ran `updateTag()` / `revalidateTag()` sets the fresh-reads cookie, and
50
+ * requests carrying it skip the memos. Set `{ shellMs: 0, markerFreshMs: 0 }`
51
+ * where every user's next request must see an invalidation (with KV,
52
+ * `CFCacheStore`'s marker memo alone can serve an invalidated shell for up
53
+ * to `markerMaxStaleMs`). Default 2000.
54
+ */
55
+ shellMs?: number;
56
+ /**
57
+ * Total bytes (prelude plus snapshot) the shell memo may hold; the least
58
+ * recently used shells are evicted first, and a shell larger than the whole
59
+ * budget is not kept. Default 16 MiB. 0 turns the memo off.
60
+ */
61
+ shellMaxBytes?: number;
62
+ /**
63
+ * Tag-marker memo for PPR shell reads: a tag's invalidation marker read
64
+ * within this many ms is used without a store read. Default 1000 on
65
+ * `CFCacheStore` (with KV; without KV there are no markers), 300 on
66
+ * `VercelCacheStore`. 0 turns the marker memo off. Cached data families
67
+ * (`cache()`, `"use cache"`, responses) keep reading their markers.
68
+ */
69
+ markerFreshMs?: number;
70
+ /**
71
+ * The oldest memoized marker a shell read still uses while a background
72
+ * read refreshes it; an older one blocks on the store read. Default 10000
73
+ * on `CFCacheStore`, 2000 on `VercelCacheStore`. The isolate that runs
74
+ * `updateTag()` / `revalidateTag()` writes the new marker into its memo,
75
+ * and the response sets the fresh-reads cookie, so the same user's next
76
+ * requests skip both memos on any isolate.
77
+ */
78
+ markerMaxStaleMs?: number;
79
+ }
80
+ /** Default memo window (ms). */
81
+ export declare const DEFAULT_SHELL_MEMO_MS = 2000;
82
+ /** Default total memo size per isolate (bytes). */
83
+ export declare const DEFAULT_SHELL_MEMO_MAX_BYTES: number;
84
+ /** A store's memo options, validated. */
85
+ export interface ResolvedShellMemoOptions {
86
+ shellMs: number;
87
+ shellMaxBytes: number;
88
+ markerFreshMs: number;
89
+ markerMaxStaleMs: number;
90
+ }
91
+ /**
92
+ * Validate a store's `memo` option; the marker memo's defaults are per store
93
+ * (isolate-tag-memo.ts). A non-finite value (`NaN` from `Number(env.UNSET)`,
94
+ * `Infinity`, null) falls back to the default, like the CFCacheStore read
95
+ * budgets; a finite 0 or negative passes through and turns that memo off.
96
+ */
97
+ export declare function resolveShellMemoOptions(memo: StoreMemoOptions | undefined, markerDefaults: {
98
+ markerFreshMs: number;
99
+ markerMaxStaleMs: number;
100
+ }): ResolvedShellMemoOptions;
101
+ /**
102
+ * Headroom on the fresh-reads cookie's lifetime: revalidateTag() writes its
103
+ * markers in the background, so they can land after the response that sets
104
+ * the cookie, and the memos' windows count from then.
105
+ */
106
+ export declare const FRESH_READS_MARGIN_MS = 1000;
107
+ /**
108
+ * How long the fresh-reads cookie keeps a mutating user off a store's memos:
109
+ * the longest either memo can still hold a value from before the mutation
110
+ * (the shell window; the marker memo's max-stale cap when the store reads
111
+ * markers through it, counted from the start of the read that filled it),
112
+ * plus FRESH_READS_MARGIN_MS. 0 when both memos are off: no cookie.
113
+ */
114
+ export declare function freshReadsWindowMs(memo: ResolvedShellMemoOptions, markerMemo: boolean): number;
115
+ /**
116
+ * Whether a shell is neither stale nor expired: the only kind a memo keeps or
117
+ * serves (a stale read stays with the store, which schedules the recapture).
118
+ */
119
+ export declare function isShellFresh(staleAt: number, expiresAt: number, now?: number): boolean;
120
+ /** Whether a shell's tags include any of `tags`. */
121
+ export declare function shellHasAnyTag(shellTags: readonly string[] | undefined, tags: readonly string[]): boolean;
122
+ export declare class ShellMemo<T> {
123
+ private readonly slots;
124
+ private totalBytes;
125
+ /**
126
+ * The value stored under `key` less than `windowMs` ago, or undefined.
127
+ * A hit becomes the most recently used entry.
128
+ */
129
+ get(key: string, windowMs: number, now?: number): T | undefined;
130
+ /**
131
+ * Store `value` (`bytes` counted against `maxBytes`). Entries past their own
132
+ * window are dropped first, so a key that is never read again does not hold
133
+ * memory until the cap pushes it out; then the least recently used entries
134
+ * are evicted until the value fits. A value larger than the whole budget, or
135
+ * a window of 0, is not stored.
136
+ */
137
+ set(key: string, value: T, bytes: number, windowMs: number, maxBytes: number, now?: number): void;
138
+ delete(key: string): void;
139
+ /** Drop every entry `predicate` selects. */
140
+ deleteWhere(predicate: (value: T) => boolean): void;
141
+ /** Bytes currently held. */
142
+ get size(): number;
143
+ clear(): void;
144
+ }
145
+ /**
146
+ * Tags this isolate invalidated recently, and when. `invalidateTags()` drops
147
+ * the isolate's memoized shells for its tags at once, but a read that was in
148
+ * flight (its store read done, its snapshot still arriving) can memoize one
149
+ * of them again while the markers or the purge are still being written. With
150
+ * KV that copy is harmless (every memo hit reads the markers); in KV-less
151
+ * purge mode the memo-hit check sees only the request's own marker memo, so
152
+ * nothing would reject it for a whole window, and the mutating user's next
153
+ * request on this isolate got the invalidated shell.
154
+ *
155
+ * A record lives from the invalidation's start until `holdMs` after it
156
+ * settles, which covers a read in flight when it settled (its snapshot read
157
+ * is bounded by the store's read budget, 170 ms by default on CFCacheStore).
158
+ * While it lives, a shell carrying the tag and tagged at or before the
159
+ * invalidation is neither memoized nor served from the memo. It only adds
160
+ * rejections: a shell tagged after the invalidation is not affected, and no
161
+ * marker read is skipped. Records are per tag across namespaces, like the
162
+ * memo drop in `invalidateTags()`: another namespace's shell with the same
163
+ * tag skips the memo for the record's life.
164
+ */
165
+ export declare class RecentTagInvalidations {
166
+ private readonly records;
167
+ /** An invalidation of `tags` at `at` started. */
168
+ begin(tags: readonly string[], at: number, now?: number): void;
169
+ /** The invalidation begun for `tags` settled: hold its records `holdMs`. */
170
+ settle(tags: readonly string[], holdMs: number, now?: number): void;
171
+ /**
172
+ * Whether a shell carrying `tags`, tagged at `taggedAt`, predates a live
173
+ * record of one of them (a tagged shell with no tag time is treated as
174
+ * predating it).
175
+ */
176
+ covers(tags: readonly string[] | undefined, taggedAt: number | undefined, now?: number): boolean;
177
+ clear(): void;
178
+ }
@@ -1,26 +1,27 @@
1
1
  /**
2
2
  * Capture data snapshot: recording + seeding stores for PPR shell parity.
3
3
  *
4
- * The scar tissue this fixes: a PPR HIT serves frozen prelude bytes, then a
5
- * FULL FRESH Flight render for hydration. Any shell-baked (non-hole) content
6
- * that drifts between capture time and hit time — a cache() segment with a
7
- * shorter ttl than the shell, a tag-invalidated item — makes the fresh payload
8
- * disagree with the prelude, so React throws a hydration text mismatch and
9
- * regenerates the tree client-side (wiping the FOUC theme class, flashing
10
- * content). See docs/design/ppr-shell-resume.md.
4
+ * A PPR HIT serves frozen prelude bytes, then a Flight payload for hydration
5
+ * that must agree with them byte for byte. The handler layer's part of that
6
+ * payload is the capture's own doc segment record: the capture writes it
7
+ * first, renders its prelude from it, and every HIT replays it (no handler
8
+ * runs on a HIT). What still executes on a HIT is the loader layer: live
9
+ * loaders and promise-carrying bake-lane (`ssr: false`) loaders run, and the
10
+ * values the bake-lane ones read must match what the capture baked (a
11
+ * promise-free bake-lane loader is served from its pin and does not run). See docs/design/ppr-shell-resume.md.
11
12
  *
12
- * The fix (Next.js resume-data-cache analog, adapted to Rango's cache rings):
13
- * the CAPTURE render records every cache-store read-hit and write it performed
14
- * (the {@link RecordingShellStore}); the record rides inside the ShellCacheEntry
15
- * as its `snapshot`; on a HIT the tail render reads through a
16
- * {@link SeededShellStore} overlay that serves those recorded values AS FRESH,
17
- * so the shell region reproduces byte-identically while everything NOT recorded
18
- * (the holes — masked loaders were never executed at capture, so their reads
19
- * were never recorded) stays live.
13
+ * The mechanism (Next.js resume-data-cache analog, adapted to Rango's cache
14
+ * rings): the CAPTURE render records every cache-store read-hit and write it
15
+ * performed (the {@link RecordingShellStore}); the records ride inside the
16
+ * ShellCacheEntry as its `snapshot`; on a HIT the tail render reads through a
17
+ * {@link SeededShellStore} overlay that serves those recorded values AS FRESH.
18
+ * Everything NOT recorded (the holes — masked loaders were never executed at
19
+ * capture, so their reads were never recorded) stays live.
20
20
  *
21
- * The invariant, verbatim: the snapshot is exactly the set of cache-store reads
22
- * the capture render performed; replaying them on a HIT reproduces the shell
23
- * content byte-identically; everything not recorded stays live.
21
+ * The invariant, verbatim: the snapshot is the doc record plus the cache-store
22
+ * reads the capture's loaders performed; replaying them on a HIT reproduces
23
+ * the shell content byte-identically; everything not recorded stays live.
24
+ * {@link pruneShellSnapshot} drops the rest.
24
25
  */
25
26
  import type { SegmentCacheStore, CacheGetResult, CacheItemResult, CacheItemOptions, CachedEntryData, ShellCacheEntry, ShellSnapshotRecord, CacheReadError } from "./types.js";
26
27
  /**
@@ -39,16 +40,26 @@ import type { SegmentCacheStore, CacheGetResult, CacheItemResult, CacheItemOptio
39
40
  * — may land after the shell has quiesced. The capture collects those write
40
41
  * promises via {@link trackWrite} and awaits them ({@link settleWrites}) before
41
42
  * draining, so a MISS-at-capture value is still pinned.
43
+ *
44
+ * Loader attribution: every item/response access made inside a loader scope
45
+ * (hit, miss or write) marks its key in {@link loaderKeys}. A loader re-runs
46
+ * on every HIT, so those records stay pinned when the snapshot is pruned. A
47
+ * miss counts too: a loader that joins a handler's in-flight "use cache" call
48
+ * reads the store first (cache-runtime.ts) and finds nothing, and the record
49
+ * then comes from the handler's write.
42
50
  */
43
51
  export declare class RecordingShellStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
44
52
  private readonly inner;
45
53
  private readonly records;
46
54
  private readonly writes;
55
+ /** Record keys ({@link recordKey}) a loader read or wrote during the capture. */
56
+ readonly loaderKeys: Set<string>;
47
57
  constructor(inner: SegmentCacheStore<TEnv>);
48
58
  get defaults(): SegmentCacheStore<TEnv>["defaults"];
49
59
  get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"];
50
60
  get supportsPassiveShellReads(): true | undefined;
51
61
  private record;
62
+ private noteLoaderAccess;
52
63
  /** Track a deferred cache-write promise so the capture can await it pre-drain. */
53
64
  trackWrite(p: Promise<unknown>): void;
54
65
  /**
@@ -67,12 +78,15 @@ export declare class RecordingShellStore<TEnv = unknown> implements SegmentCache
67
78
  * cacheRoute path schedules its actual store.set in a second waitUntil while the
68
79
  * first is running), so each awaited batch may enqueue more. Loop until the
69
80
  * queue empties or the deadline passes. Bounded: a pathologically slow write
70
- * must never stall the capture task, so a key that does not settle in time is
71
- * left unpinned (it drifts, the pre-snapshot behavior) rather than hanging.
81
+ * must never stall the capture task. Resolves true when every tracked write
82
+ * settled, false at the deadline (settleCaptureRecord then fails the capture
83
+ * when the doc record is among the writes still pending).
72
84
  */
73
- settleWrites(timeoutMs: number): Promise<void>;
85
+ settleWrites(timeoutMs: number): Promise<boolean>;
74
86
  /** The recorded snapshot (last-write-wins per family+key), or undefined if empty. */
75
87
  drainSnapshot(): ShellSnapshotRecord[] | undefined;
88
+ /** One recorded value, by family and key. */
89
+ getRecord(family: ShellSnapshotRecord["family"], key: string): ShellSnapshotRecord["value"] | undefined;
76
90
  get(key: string): Promise<CacheGetResult | null | CacheReadError>;
77
91
  set(key: string, data: CachedEntryData, ttl: number, swr?: number): Promise<void>;
78
92
  delete(key: string): Promise<boolean>;
@@ -95,6 +109,64 @@ export declare class RecordingShellStore<TEnv = unknown> implements SegmentCache
95
109
  }
96
110
  /** True iff `store` is a RecordingShellStore (duck-typed across module copies). */
97
111
  export declare function getRecordingStore<TEnv>(store: SegmentCacheStore<TEnv> | undefined): RecordingShellStore<TEnv> | undefined;
112
+ /**
113
+ * True when `snapshot` carries the canonical doc segment record `docKey`
114
+ * names, with at least one segment: the record partial replay and a
115
+ * fast-path HIT tail consume.
116
+ */
117
+ export declare function hasDocRecord(snapshot: readonly ShellSnapshotRecord[] | undefined, docKey: string | undefined): boolean;
118
+ /**
119
+ * Who reads a stored snapshot besides the doc record
120
+ * (docs/design/shell-entry-layout.md §2):
121
+ * - "segments": nobody. A navigation-only entry is read only by partial
122
+ * replay, which seeds the doc record alone (SeededShellStore
123
+ * `segmentsOnly`, implicit doc scope only).
124
+ * - "loaders": the loaders a document HIT re-runs. Every HIT tail replays
125
+ * the handler layer from the doc record and never runs a handler, so the
126
+ * records only handler code read are dead weight.
127
+ */
128
+ export type ShellSnapshotReaders = "segments" | "loaders";
129
+ /**
130
+ * Split a capture's snapshot into what its readers consume and the rest: the
131
+ * doc record (`docKey`) always; with "loaders", the loader-family records and
132
+ * the item/response records a loader touched during the capture
133
+ * (RecordingShellStore.loaderKeys). Every other segment record goes: an
134
+ * explicit cache() tier's reads and writes are keyed where no HIT tail and no
135
+ * partial replay looks (both consult only the doc record). The doc record
136
+ * stays a copy inside the entry: a reference to a separately stored record
137
+ * could be evicted on its own, and the HIT would have nothing to replay.
138
+ */
139
+ export declare function pruneShellSnapshot(snapshot: readonly ShellSnapshotRecord[], readers: ShellSnapshotReaders, loaderKeys: ReadonlySet<string>, docKey: string): {
140
+ kept: ShellSnapshotRecord[];
141
+ pruned: ShellSnapshotRecord[];
142
+ };
143
+ /**
144
+ * The shell entry head's fields estimateShellEntryBytes does not measure
145
+ * (versions, timestamps, theme, lengths, flags; about 200 bytes in a
146
+ * CFCacheStore frame) and the frame's 12-byte prefix, with room to spare.
147
+ */
148
+ export declare const SHELL_ENTRY_HEAD_ALLOWANCE_BYTES: number;
149
+ /**
150
+ * Bytes (UTF-8) a stored shell entry takes, as the built-in stores write it:
151
+ * the prelude, the snapshot's JSON, and a JSON head that carries the
152
+ * postponed state as an escaped string next to the tags and doc key
153
+ * (CFCacheStore's frame head, VercelCacheStore's envelope), plus
154
+ * SHELL_ENTRY_HEAD_ALLOWANCE_BYTES for the head's fixed fields and the
155
+ * frame prefix. Measuring the raw postponed bytes undercounted: every quote
156
+ * in it is escaped in the head (a /ppr-large/holes entry measured 1,098
157
+ * bytes under its KV value). The capture's whole-entry guard compares this
158
+ * with the store's value limit.
159
+ */
160
+ export declare function estimateShellEntryBytes(parts: {
161
+ preludeBytes: number;
162
+ postponed: string | null | undefined;
163
+ snapshot: readonly ShellSnapshotRecord[] | undefined;
164
+ tags: readonly string[] | undefined;
165
+ docKey: string | undefined;
166
+ prunedRecords: string | undefined;
167
+ }): number;
168
+ /** Snapshot records by family, e.g. `segment:1/item:5` (no commas: it rides a Server-Timing desc). */
169
+ export declare function countSnapshotFamilies(snapshot: readonly ShellSnapshotRecord[]): string;
98
170
  /**
99
171
  * The store the shell fast path's IMPLICIT doc-cache scope resolves during a
100
172
  * capture: reads pass through the recording store (a real-store hit is
@@ -137,6 +209,12 @@ export interface ShellLoaderSeedEntry {
137
209
  * from the pin (loader-cache.ts pin-first path).
138
210
  */
139
211
  holes: boolean;
212
+ /**
213
+ * The record needs its loader body to run on a HIT for pushes the capture
214
+ * could not record (ShellSnapshotLoaderValue.runs). Only read for a
215
+ * hole-free record: a hole-carrying one always runs.
216
+ */
217
+ runs: boolean;
140
218
  }
141
219
  export declare function buildShellLoaderSeed(snapshot: ShellSnapshotRecord[]): Promise<Map<string, ShellLoaderSeedEntry> | undefined>;
142
220
  /**
@@ -184,3 +262,14 @@ export declare class SeededShellStore<TEnv = unknown> implements SegmentCacheSto
184
262
  putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | "uncacheable" | void>;
185
263
  invalidateTags(tags: string[]): Promise<void>;
186
264
  }
265
+ /**
266
+ * Thrown by a document HIT tail's match when its doc record cannot supply the
267
+ * handler layer (it failed to decode, or the entry lost it). The tail must not
268
+ * run handlers after the prelude committed, so withCacheLookup throws this
269
+ * instead of resolving segments, and serveShellHit degrades the response:
270
+ * the entry is replaced with a tombstone, the memo is dropped, a recapture is
271
+ * scheduled, and the browser reloads into a MISS.
272
+ */
273
+ export declare class ShellRecordUnavailableError extends Error {
274
+ constructor(docKey: string | undefined);
275
+ }