@rangojs/router 0.20.0 → 0.22.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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { replayLoaderPush } from "./handle-capture.js";
10
10
  import type { ResolvedSegment } from "../types.js";
11
- import type { HandleStore } from "../server/handle-store.js";
11
+ import type { HandleStore, RecordAuthority } from "../server/handle-store.js";
12
12
  import type { HandleOwners, SegmentHandleData } from "./types.js";
13
13
  // segment-codec eagerly pulls @vitejs/plugin-rsc (a virtual: module unresolvable
14
14
  // in plain node/vitest). It is imported LAZILY inside the two async encode/decode
@@ -134,53 +134,51 @@ export function captureHandleOwners(
134
134
  return owners;
135
135
  }
136
136
 
137
- /** How restoreHandles delivers a record's loader-owned values. */
138
- export interface OwnedPushDelivery {
139
- /**
140
- * Asked once per owner delivered through pushReplayed
141
- * (setupLoaderAccess _claimLoaderPushes); an owner it refuses is skipped.
142
- */
143
- claim?: (loaderId: string) => boolean;
144
- /**
145
- * A PPR shell's record (capture, document HIT tail, navigation replay): the
146
- * loaders the route runs on the live lane (liveLaneLoaderIds), marked on
147
- * the store (HandleStore.markLiveLane). A hole's values are replayed
148
- * unclaimed, so its own cache() HIT redelivers its entry's pushes in their
149
- * place (HandleStore.redeliverReplays), and its run ending without a push
150
- * drops them (HandleStore.settleLoaderRun).
151
- */
152
- liveLane?: ReadonlySet<string>;
153
- /**
154
- * A document HIT tail: every owner outside `liveLane` goes through
155
- * pushRestored and stands, unclaimed.
156
- */
157
- restore?: boolean;
137
+ /**
138
+ * captureHandles and captureHandleOwners cut down to the handle arrays that
139
+ * hold an owned value, each whole (its unowned values too, in place), so
140
+ * restoreHandles rebuilds exactly those arrays over a store that already
141
+ * holds the rest. A prerender-served shell capture stores this: the
142
+ * prerender store restores the handler pushes on a HIT, and the loader
143
+ * pushes have no other record (shell-capture.ts captureAndStoreShell).
144
+ */
145
+ export function captureOwnedHandles(handleStore: HandleStore): {
146
+ handles: HandleRecord;
147
+ owners?: HandleOwners;
148
+ } {
149
+ const handles: HandleRecord = {};
150
+ let owners: HandleOwners | undefined;
151
+ for (const id of handleStore.getOwnedSegmentIds()) {
152
+ const segOwners = handleStore.getRecordOwners(id);
153
+ if (!segOwners) continue;
154
+ const kept = handleStore.getDataForSegment(id, true);
155
+ handles[id] = {};
156
+ for (const name in segOwners) handles[id][name] = kept[name] ?? [];
157
+ (owners ??= {})[id] = segOwners;
158
+ }
159
+ return { handles, owners };
158
160
  }
159
161
 
162
+ /**
163
+ * What the record being restored is to each loader's handle pushes on this
164
+ * request (HandleStore RecordAuthority). Built by loaderPins (loader-cache.ts)
165
+ * over the pins resolveLoaderData serves a loader's value from, so a value
166
+ * and its record copies get one answer.
167
+ */
168
+ export type OwnedPushDelivery = (loaderId: string) => RecordAuthority;
169
+
160
170
  /**
161
171
  * Restore handle data from a cached snapshot into the handle store.
162
172
  * Used when serving cached segments to replay their handle data: a route
163
173
  * cache() record owns its segments' arrays, so it REPLACES them.
164
174
  *
165
175
  * A segment with `owners` is replaced with empty arrays and re-pushed in
166
- * recorded order, an owned value through pushReplayed: if the owning loader
167
- * runs (a client navigation replaying a ppr route's cache() record runs its
168
- * `ssr: false` loaders), its live pushes replace the recorded ones in place
169
- * instead of appending a second copy. `owned.claim` is asked once per
170
- * owner, so the loader's own cache() HIT does not replay its pushes a second
171
- * time (loader-cache.ts replayLoaderHandles). A record without owners
172
- * restores as a plain replay.
173
- *
174
- * `owned.restore`: a document HIT tail restoring the shell's doc record
175
- * (withCacheLookup). A bake-lane owner's values are the prelude's, so they go
176
- * through pushRestored and stand: a run of that loader on the HIT (a
177
- * promise-carrying `ssr: false` loader, one whose record asks for a run)
178
- * reads the store, and its settled pushes are dropped, not swapped in.
179
- * Nothing is claimed, so the loader's own cache() HIT still replays the
180
- * pushes the record could not keep (the thenable ones pushRestored lets
181
- * through). A loader the route also runs on the live lane is a hole: its
182
- * values keep pushReplayed, unclaimed, and its live run replaces them
183
- * (#936), even inside a restored loader's body (`owned.liveLane`).
176
+ * recorded order. A loader-owned value stands (pushRestored) where `owned`
177
+ * says the record supplies that loader's value too ("pin", "copies"), and is
178
+ * a placeholder (pushPlaceholder) everywhere else: without `owned`, every
179
+ * owner. Neither claims its loader, so the loader's own cache() HIT still
180
+ * delivers (the thenable pushes a restored copy lets through, or its entry's
181
+ * pushes in a placeholder's place).
184
182
  */
185
183
  export function restoreHandles(
186
184
  handles: Record<string, SegmentHandleData>,
@@ -188,8 +186,6 @@ export function restoreHandles(
188
186
  owners?: HandleOwners,
189
187
  owned?: OwnedPushDelivery,
190
188
  ): void {
191
- let delivers: Map<string, boolean> | undefined;
192
- if (owned?.liveLane) handleStore.markLiveLane(owned.liveLane);
193
189
  for (const [segId, segHandles] of Object.entries(handles)) {
194
190
  if (Object.keys(segHandles).length === 0) continue;
195
191
  const segOwners = owners?.[segId];
@@ -208,29 +204,42 @@ export function restoreHandles(
208
204
  handleStore.push(handleName, segId, values[i]);
209
205
  continue;
210
206
  }
211
- const hole = owned?.liveLane?.has(owner) === true;
212
- if (owned?.restore && !hole) {
207
+ const authority = owned?.(owner);
208
+ if (authority === "pin" || authority === "copies") {
213
209
  handleStore.pushRestored(handleName, segId, values[i], owner);
214
- continue;
215
- }
216
- if (hole) {
217
- handleStore.pushReplayed(handleName, segId, values[i], owner);
218
- continue;
219
- }
220
- delivers ??= new Map();
221
- let deliver = delivers.get(owner);
222
- if (deliver === undefined) {
223
- deliver = owned?.claim ? owned.claim(owner) : true;
224
- delivers.set(owner, deliver);
225
- }
226
- if (deliver) {
227
- handleStore.pushReplayed(handleName, segId, values[i], owner);
210
+ } else {
211
+ handleStore.pushPlaceholder(handleName, segId, values[i], owner);
228
212
  }
229
213
  }
230
214
  }
231
215
  }
232
216
  }
233
217
 
218
+ /**
219
+ * Serve a doc record's handles: set its authority over the loaders' pushes
220
+ * (with or without a handles blob: a pinned loader whose capture pushed
221
+ * nothing has no copy, and its run must still add no settled push), then
222
+ * decode and restoreHandles. `keepIds` limits the restore to the segments a
223
+ * boundary-scoped scope serves. A decode failure skips the restore.
224
+ */
225
+ export async function restoreRecordHandles(
226
+ handleStore: HandleStore,
227
+ record: { handles?: string; handleOwners?: HandleOwners },
228
+ owned: OwnedPushDelivery | undefined,
229
+ keepIds?: ReadonlySet<string>,
230
+ ): Promise<void> {
231
+ if (owned) handleStore.setRecordAuthority(owned);
232
+ if (!record.handles) return;
233
+ const decoded = await decodeHandles(record.handles);
234
+ if (!decoded) return;
235
+ if (keepIds) {
236
+ for (const id of Object.keys(decoded)) {
237
+ if (!keepIds.has(id)) delete decoded[id];
238
+ }
239
+ }
240
+ restoreHandles(decoded, handleStore, record.handleOwners, owned);
241
+ }
242
+
234
243
  /**
235
244
  * Append recorded handle pushes to the store, in recorded order, via push()
236
245
  * so active captures and loader-scope tagging see them like live pushes. For
@@ -238,43 +247,66 @@ export function restoreHandles(
238
247
  * restoreHandles would wipe those. `segmentId` redirects every value to one
239
248
  * segment (the caller's).
240
249
  *
241
- * Owner-keyed groups (useCacheRecordKey, `${seq}:${loaderId}`) are a loader's
242
- * pushes, which reach the page once per request: `claim` (setupLoaderAccess
243
- * _claimLoaderPushes) skips a loader that already ran or was replayed in this
244
- * request, and a claimed loader that runs later replaces its replayed values
245
- * (HandleStore.pushReplayed). `${seq}:` groups are the function's own pushes.
246
- * A key without ":" is a segment id (records written before owner keys) and
247
- * replays unchanged. A loader's own cache() replays through loader-cache.ts
248
- * replayLoaderHandles.
250
+ * The replay of every cached unit that records by owner: a "use cache"
251
+ * function (useCacheRecordKey) and a loader's own cache() (loader-cache.ts
252
+ * recordOwnerKey, with `unitLoader` the cached loader).
253
+ *
254
+ * A `${seq}:${loaderId}` group is that loader's pushes, which reach the page
255
+ * once per request: `claim` (setupLoaderAccess _claimLoaderPushes) is asked
256
+ * once per loader and skips one that already ran or was replayed in this
257
+ * request; a claimed loader that runs later replaces its replayed values
258
+ * (HandleStore.pushReplayed). A group without an owner (`${seq}:`, or a key
259
+ * without ":" from a record written before owner keys, which is a segment
260
+ * id) is the unit's own: `unitLoader`'s, or no loader's for a function,
261
+ * whose pushes are plain.
262
+ *
263
+ * The claimed loaders' placeholders give way to this delivery
264
+ * (HandleStore.replacePlaceholders), `unitLoader`'s even when the entry
265
+ * recorded no push for it: the entry stands for that loader's run.
266
+ *
267
+ * No `claim` means the caller's pushes are diverted off the page (a stale
268
+ * refresh: cache-runtime.ts refreshView, setupLoaderAccess
269
+ * _runLoaderIsolated): every group is delivered, into the diverting capture,
270
+ * and the page's placeholders stay, or the page would lose the push.
249
271
  */
250
272
  export function appendHandles(
251
273
  handles: Record<string, SegmentHandleData>,
252
274
  handleStore: HandleStore,
253
- segmentId?: string,
254
- claim?: (loaderId: string) => boolean,
275
+ segmentId: string | undefined,
276
+ claim: ((loaderId: string) => boolean) | undefined,
277
+ unitLoader?: string,
255
278
  ): void {
256
- let claims: Map<string, boolean> | undefined;
257
- for (const [key, segHandles] of Object.entries(handles)) {
279
+ const ownerOf = (key: string): string | undefined => {
258
280
  const colon = key.indexOf(":");
259
- const owner = colon < 0 ? "" : key.slice(colon + 1);
260
- if (owner) {
261
- claims ??= new Map();
262
- let deliver = claims.get(owner);
263
- if (deliver === undefined) {
264
- deliver = claim ? claim(owner) : true;
265
- claims.set(owner, deliver);
266
- }
267
- if (!deliver) continue;
281
+ return (colon < 0 ? "" : key.slice(colon + 1)) || unitLoader;
282
+ };
283
+ const claims = new Map<string, boolean>();
284
+ const ask = (owner: string | undefined): void => {
285
+ if (owner !== undefined && !claims.has(owner)) {
286
+ claims.set(owner, claim ? claim(owner) : true);
268
287
  }
269
- const target = segmentId ?? key;
270
- for (const [handleName, values] of Object.entries(segHandles)) {
271
- for (const value of values) {
272
- if (owner) {
273
- replayLoaderPush(handleStore, handleName, target, value, owner);
274
- } else {
275
- handleStore.push(handleName, target, value);
288
+ };
289
+ ask(unitLoader);
290
+ for (const key in handles) ask(ownerOf(key));
291
+ const deliver = (): void => {
292
+ for (const [key, segHandles] of Object.entries(handles)) {
293
+ const owner = ownerOf(key);
294
+ if (owner !== undefined && !claims.get(owner)) continue;
295
+ const target = segmentId ?? key;
296
+ for (const [handleName, values] of Object.entries(segHandles)) {
297
+ for (const value of values) {
298
+ if (owner !== undefined) {
299
+ replayLoaderPush(handleStore, handleName, target, value, owner);
300
+ } else {
301
+ handleStore.push(handleName, target, value);
302
+ }
276
303
  }
277
304
  }
278
305
  }
306
+ };
307
+ const granted: string[] = [];
308
+ if (claim) {
309
+ for (const [owner, given] of claims) if (given) granted.push(owner);
279
310
  }
311
+ handleStore.replacePlaceholders(granted, deliver);
280
312
  }
@@ -1,5 +1,6 @@
1
1
  export type {
2
2
  SegmentCacheStore,
3
+ CacheStoreScope,
3
4
  CachedEntryData,
4
5
  CacheGetResult,
5
6
  CacheItemResult,
@@ -56,3 +57,7 @@ export {
56
57
  } from "./document-cache.js";
57
58
 
58
59
  export type { CacheErrorCategory } from "./cache-error.js";
60
+
61
+ // For a custom persistent store: the versions the built-in stores key with.
62
+ export { getCacheVersions } from "../server/build-version-table.js";
63
+ export type { RouterVersions } from "../router-versions.js";
@@ -43,6 +43,59 @@ export interface ExecutionStart {
43
43
  readonly at: number;
44
44
  }
45
45
 
46
+ /**
47
+ * The `taggedAt` / `ta` stamp a store writes on a tagged data entry: the start
48
+ * of the execution that produced the value (`startedAt`), so a marker written
49
+ * while it ran rejects the entry on read (#1068). Write time when the writer
50
+ * knows no start. Never later than now: a start from a skewed clock must not
51
+ * stamp the future, which would make the entry look newer than every marker.
52
+ */
53
+ export function entryStamp(startedAt: number | undefined): number {
54
+ const now = Date.now();
55
+ // 0 is "no start", not the epoch: no marker is older than it, so such an
56
+ // entry would never be rejected.
57
+ return typeof startedAt === "number" &&
58
+ Number.isFinite(startedAt) &&
59
+ startedAt > 0
60
+ ? Math.min(startedAt, now)
61
+ : now;
62
+ }
63
+
64
+ /** The stamp of a data entry: `entryStamp` for a tagged entry, none otherwise. */
65
+ export function taggedStamp(
66
+ tags: readonly string[] | undefined,
67
+ startedAt: number | undefined,
68
+ ): number | undefined {
69
+ return Array.isArray(tags) && tags.length > 0
70
+ ? entryStamp(startedAt)
71
+ : undefined;
72
+ }
73
+
74
+ /**
75
+ * putResponse has no options object to carry the start and takes no new
76
+ * positional parameter, so the writer marks the Response it hands over.
77
+ * On globalThis like the order: a second evaluated copy of this module that
78
+ * wrote the mark must be read by the store's copy, else the stamp silently
79
+ * falls back to write time.
80
+ */
81
+ const responseStarts: WeakMap<Response, number> = ((globalThis as any)[
82
+ Symbol.for("rangojs-router:response-execution-start")
83
+ ] ??= new WeakMap());
84
+
85
+ /** Mark the execution start behind a Response handed to putResponse. */
86
+ export function markResponseStart(
87
+ response: Response,
88
+ start: ExecutionStart,
89
+ ): Response {
90
+ responseStarts.set(response, start.at);
91
+ return response;
92
+ }
93
+
94
+ /** The start a writer marked on `response`, if any. */
95
+ export function responseStartedAt(response: Response): number | undefined {
96
+ return responseStarts.get(response);
97
+ }
98
+
46
99
  /** Record an execution's start, before it reads its data. */
47
100
  export function executionStart(): ExecutionStart {
48
101
  return { seq: order.seq, at: Date.now() };
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type {
9
+ CacheStoreScope,
9
10
  SegmentCacheStore,
10
11
  CachedEntryData,
11
12
  CacheDefaults,
@@ -23,6 +24,7 @@ import {
23
24
  DEFAULT_FUNCTION_TTL,
24
25
  } from "./cache-policy.js";
25
26
  import { reportCacheError } from "./cache-error.js";
27
+ import { responseStartedAt } from "./invalidation-order.js";
26
28
 
27
29
  const CACHE_REGISTRY_KEY = "__rsc_router_segment_cache_registry__";
28
30
  const RESPONSE_CACHE_REGISTRY_KEY = "__rsc_router_response_cache_registry__";
@@ -146,6 +148,26 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
146
148
  * ```
147
149
  */
148
150
  maxEntries?: number;
151
+
152
+ /**
153
+ * Where an entry this store writes can be read; what `router.prerender()`
154
+ * checks before warming a route. Defaults to `"local"`: the maps live in one
155
+ * process, and the router cannot tell one long-running server from several
156
+ * replicas or serverless instances, so a warm into it is refused in
157
+ * production. Set `"global"` only when exactly one process serves the app
158
+ * (a single Node server); a warm then reports `warmed`. Under the Vite dev
159
+ * server a `"local"` instance already counts as shared.
160
+ *
161
+ * Like `defaults` and `keyGenerator`, the scope is per instance: two
162
+ * instances with the same `name` share their maps but each declares its own
163
+ * scope. Keep them identical.
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const store = new MemorySegmentCacheStore({ scope: "global" });
168
+ * ```
169
+ */
170
+ scope?: CacheStoreScope;
149
171
  }
150
172
 
151
173
  /** Default per-family entry cap for MemorySegmentCacheStore (FIFO eviction). */
@@ -188,6 +210,13 @@ const DEFAULT_MAX_ENTRIES = 1000;
188
210
  export class MemorySegmentCacheStore<
189
211
  TEnv = unknown,
190
212
  > implements SegmentCacheStore<TEnv> {
213
+ /**
214
+ * SegmentCacheStore.scope: "local" unless `options.scope` says otherwise, so
215
+ * router.prerender() refuses to warm into these maps in production. Under
216
+ * the Vite dev server the gate counts a "local" instance as shared
217
+ * (store-scope.ts resolveWarmStoreScope).
218
+ */
219
+ readonly scope: CacheStoreScope;
191
220
  readonly supportsPassiveShellReads: true = true;
192
221
  private cache: Map<string, CachedEntryData>;
193
222
  private responseCache: Map<string, CachedResponseEntry>;
@@ -253,6 +282,7 @@ export class MemorySegmentCacheStore<
253
282
  this.defaults = options?.defaults;
254
283
  this.keyGenerator = options?.keyGenerator;
255
284
  this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
285
+ this.scope = options?.scope ?? "local";
256
286
  }
257
287
 
258
288
  /**
@@ -300,6 +330,7 @@ export class MemorySegmentCacheStore<
300
330
  ttl: number,
301
331
  _swr?: number,
302
332
  ): Promise<void> {
333
+ if (this.invalidatedAfter(data.tags, data.taggedAt)) return;
303
334
  const entry: CachedEntryData = {
304
335
  ...data,
305
336
  expiresAt: Date.now() + ttl * 1000,
@@ -358,6 +389,7 @@ export class MemorySegmentCacheStore<
358
389
  tags?: string[],
359
390
  ): Promise<void> {
360
391
  try {
392
+ const startedAt = responseStartedAt(response);
361
393
  const body = await response.clone().arrayBuffer();
362
394
  const headers: [string, string][] = [];
363
395
  response.headers.forEach((value, name) => {
@@ -368,6 +400,8 @@ export class MemorySegmentCacheStore<
368
400
  const swrWindow = resolveSwrWindow(swr, this.defaults);
369
401
  const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
370
402
 
403
+ // After the last await before the write.
404
+ if (this.invalidatedAfter(tags, startedAt)) return;
371
405
  const prefixedKey = `res:${key}`;
372
406
  this.unregisterTags(prefixedKey);
373
407
  this.evictIfNeeded(this.responseCache, key, "res");
@@ -415,6 +449,7 @@ export class MemorySegmentCacheStore<
415
449
  value: string,
416
450
  options?: CacheItemOptions,
417
451
  ): Promise<void> {
452
+ if (this.invalidatedAfter(options?.tags, options?.startedAt)) return;
418
453
  const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
419
454
  const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
420
455
  const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
@@ -486,7 +521,23 @@ export class MemorySegmentCacheStore<
486
521
  return this.tagsInvalidatedSince(tags, sinceMs);
487
522
  }
488
523
 
489
- private tagsInvalidatedSince(tags: string[], sinceMs: number): boolean {
524
+ /**
525
+ * A tag invalidated in a millisecond after the write's execution started
526
+ * (#1068). Strictly after: the start's own millisecond is ambiguous, and
527
+ * invalidation-order.ts orders those exactly.
528
+ */
529
+ private invalidatedAfter(
530
+ tags: readonly string[] | undefined,
531
+ startedAt: number | undefined,
532
+ ): boolean {
533
+ if (!tags || tags.length === 0 || startedAt === undefined) return false;
534
+ return this.tagsInvalidatedSince(tags, startedAt + 1);
535
+ }
536
+
537
+ private tagsInvalidatedSince(
538
+ tags: readonly string[],
539
+ sinceMs: number,
540
+ ): boolean {
490
541
  for (const tag of tags) {
491
542
  const at = this.tagInvalidatedAt.get(tag);
492
543
  // >= so a same-millisecond invalidation wins (freshness over staleness),
@@ -18,7 +18,9 @@
18
18
  * the capture's copy of a value: it is dynamic or has its own cache. A
19
19
  * bake-lane loader body that runs on a HIT reads the store too; its pinned
20
20
  * container paths stay the pin's (overlayLoaderContainer), and its settled
21
- * pushes stay the record's (HandleStore.pushRestored).
21
+ * pushes stay the record's (HandleStore.pushRestored). The two go together:
22
+ * a loader whose pin the entry lost runs fresh, and the record's copies of
23
+ * its pushes give way to that run's (restoreHandles).
22
24
  */
23
25
 
24
26
  import type {
@@ -30,6 +32,7 @@ import type {
30
32
  ShellCacheEntry,
31
33
  ShellSnapshotRecord,
32
34
  ShellSnapshotLoaderValue,
35
+ ShellSnapshotHandlesValue,
33
36
  CacheReadError,
34
37
  } from "./types.js";
35
38
  import { settleGrowing } from "./background-task.js";
@@ -367,17 +370,40 @@ export interface ShellLoaderSeedEntry {
367
370
  * hole-free record: a hole-carrying one always runs.
368
371
  */
369
372
  runs: boolean;
373
+ /**
374
+ * The capture that wrote the pin recorded every settled push of the
375
+ * loader's run in the doc record (CachedEntryData.handleOwners): the pin
376
+ * carries the `runs` bit, which captures write since they record those
377
+ * pushes (v0.18). A replay then drops any other settled push of the loader
378
+ * (HandleStore RecordAuthority "pin"). False for an older pin, whose record
379
+ * may hold none and leaves them to the run.
380
+ */
381
+ complete: boolean;
370
382
  }
371
383
 
384
+ /**
385
+ * A pin's loader. A pin is stored under its loader segment id,
386
+ * `${shortCode}D${index}.${loaderId}` (fresh.ts resolveLoaders), and a
387
+ * shortCode holds no "D" and no "." (server/context.ts getShortCode: M, L, P,
388
+ * R, C, I and digits), so the first `D<index>.` ends it. A loader id can hold
389
+ * both.
390
+ */
391
+ const PIN_KEY_PREFIX = /^[^D.]*D\d+\./;
392
+
372
393
  /**
373
394
  * Materialize the loader-family seed from a shell snapshot for a HIT's tail
374
395
  * render: Flight-deserialize each recorded (promise-elided) bake-lane
375
- * container into a segment-key -> container Map, which serveShellHit assigns
376
- * to the tail context's `_shellLoaderSeed` for the resolveLoaderData overlay.
396
+ * container into a loader-id -> pin Map, which serveShellHit assigns to the
397
+ * tail context's `_shellLoaderSeed` (loader-cache.ts servedPins).
377
398
  * Lives here so every snapshot family is decoded in this module (the segment
378
399
  * family via {@link SeededShellStore}); the loader family is not a store
379
400
  * read, so it seeds the context instead of a store.
380
401
  *
402
+ * Keyed by loader, not by the segment a pin was stored under: a loader runs
403
+ * once per capture, so every segment that registers it pinned the same
404
+ * container, and the loader's value and its handle pushes are both looked up
405
+ * by loader. A key that does not parse pins nothing.
406
+ *
381
407
  * Deserializations run in parallel; a record that fails to decode is skipped
382
408
  * (that loader drifts — the pre-snapshot behavior — instead of failing the
383
409
  * HIT). Returns undefined when the snapshot carries no loader records, without
@@ -396,10 +422,14 @@ export async function buildShellLoaderSeed(
396
422
  const entries = await Promise.all(
397
423
  loaderRecords.map(
398
424
  async (rec): Promise<[string, ShellLoaderSeedEntry] | null> => {
425
+ const prefix = PIN_KEY_PREFIX.exec(rec.key);
426
+ if (!prefix) return null;
399
427
  try {
400
- const stored = rec.value as ShellSnapshotLoaderValue;
428
+ // Typed as always carrying the bits; an older record lacks them.
429
+ const stored = rec.value as Partial<ShellSnapshotLoaderValue> &
430
+ Pick<ShellSnapshotLoaderValue, "value">;
401
431
  return [
402
- rec.key,
432
+ rec.key.slice(prefix[0].length),
403
433
  {
404
434
  container: await deserializeResult(stored.value),
405
435
  // A record stored before the bits existed (v0.17) lacks the
@@ -407,6 +437,7 @@ export async function buildShellLoaderSeed(
407
437
  // so the loader body supplies them.
408
438
  holes: stored.holes !== 0,
409
439
  runs: stored.runs !== 0,
440
+ complete: stored.runs !== undefined,
410
441
  },
411
442
  ];
412
443
  } catch {
@@ -422,6 +453,22 @@ export async function buildShellLoaderSeed(
422
453
  return seed.size > 0 ? seed : undefined;
423
454
  }
424
455
 
456
+ /** The key of a snapshot's one `handles` record (ShellSnapshotHandlesValue). */
457
+ export const SHELL_HANDLES_RECORD_KEY = "handles";
458
+
459
+ /**
460
+ * The `handles` record of a prerender-served entry, which a HIT tail restores
461
+ * after the prerender store's handles (cache-lookup.ts yieldFromStore), or
462
+ * undefined: an entry with a doc record, or one written before the record
463
+ * existed (v0.21), which serves as it did.
464
+ */
465
+ export function shellPrerenderHandles(
466
+ snapshot: readonly ShellSnapshotRecord[],
467
+ ): ShellSnapshotHandlesValue | undefined {
468
+ const record = snapshot.find((r) => r?.family === "handles");
469
+ return record?.value as ShellSnapshotHandlesValue | undefined;
470
+ }
471
+
425
472
  /**
426
473
  * The implicit doc scope's store on a HIT tail and on a partial replay: it
427
474
  * serves the snapshot's segment records, the doc record among them, and
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The store scope `router.prerender()` gates a warm on
3
+ * (prerender/create-prerender-trigger.ts). See
4
+ * docs/design/prerender-every-route.md, "The store declaration".
5
+ */
6
+
7
+ import type { CacheStoreScope, SegmentCacheStore } from "./types.js";
8
+ import { MemorySegmentCacheStore } from "./memory-segment-store.js";
9
+
10
+ /**
11
+ * True under the Vite dev server: only its routes-manifest virtual module sets
12
+ * the dev origin (vite/discovery/virtual-module-codegen.ts). A build, a preview
13
+ * server and a test runner answer false.
14
+ */
15
+ export function isViteDevServer(): boolean {
16
+ return typeof globalThis.__PRERENDER_DEV_URL === "string";
17
+ }
18
+
19
+ /**
20
+ * The scope a warm treats `store` as having: its own declaration, with none
21
+ * or an unknown value being `"local"`.
22
+ *
23
+ * One exception: under the Vite dev server a MemorySegmentCacheStore answers
24
+ * `"global"`, because that one process serves every request. Production reads
25
+ * the class's own `scope` (`"local"`): changing that declaration is what
26
+ * would admit it there.
27
+ */
28
+ export function resolveWarmStoreScope(
29
+ store: SegmentCacheStore<any>,
30
+ devServer: boolean = isViteDevServer(),
31
+ ): CacheStoreScope {
32
+ const declared = store.scope;
33
+ if (declared === "global" || declared === "regional") return declared;
34
+ if (devServer && store instanceof MemorySegmentCacheStore) return "global";
35
+ return "local";
36
+ }
@@ -49,6 +49,7 @@ import {
49
49
  export {
50
50
  executionStart,
51
51
  invalidatedSince,
52
+ markResponseStart,
52
53
  type ExecutionStart,
53
54
  } from "./invalidation-order.js";
54
55
 
@@ -147,11 +148,12 @@ function markFreshReads(stores: SegmentCacheStore[]): void {
147
148
  * The write gate of every cache execution (#977): whether one of `tags` was
148
149
  * invalidated after the execution that produced the value started.
149
150
  *
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:
151
+ * A store stamps an entry with the execution's start (CFCacheStore's
152
+ * `taggedAt`, VercelCacheStore's `ta`, #1068), but only CFCacheStore with KV
153
+ * markers rejects it on a later request's read, and MemorySegmentCacheStore
154
+ * refuses the write. The gate saves the write everywhere and is the only
155
+ * guard where a read compares nothing. Two answers, either one skips the
156
+ * write:
155
157
  *
156
158
  * - this isolate's order (invalidatedSince), which sees every request's
157
159
  * updateTag()/revalidateTag() here, the calling one included;