@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80daa23e

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 (1746) hide show
  1. package/AGENTS.md +13 -0
  2. package/README.md +472 -7
  3. package/dist/__internal.d.ts +83 -0
  4. package/dist/__internal.d.ts.map +1 -0
  5. package/dist/__internal.js +19 -0
  6. package/dist/__internal.js.map +1 -0
  7. package/dist/__mocks__/version.d.ts +7 -0
  8. package/dist/__mocks__/version.d.ts.map +1 -0
  9. package/dist/__mocks__/version.js +7 -0
  10. package/dist/__mocks__/version.js.map +1 -0
  11. package/dist/__tests__/client-href.test.d.ts +2 -0
  12. package/dist/__tests__/client-href.test.d.ts.map +1 -0
  13. package/dist/__tests__/client-href.test.js +74 -0
  14. package/dist/__tests__/client-href.test.js.map +1 -0
  15. package/dist/__tests__/component-utils.test.d.ts +2 -0
  16. package/dist/__tests__/component-utils.test.d.ts.map +1 -0
  17. package/dist/__tests__/component-utils.test.js +51 -0
  18. package/dist/__tests__/component-utils.test.js.map +1 -0
  19. package/dist/__tests__/event-controller.test.d.ts +2 -0
  20. package/dist/__tests__/event-controller.test.d.ts.map +1 -0
  21. package/dist/__tests__/event-controller.test.js +538 -0
  22. package/dist/__tests__/event-controller.test.js.map +1 -0
  23. package/dist/__tests__/helpers/route-tree.d.ts +118 -0
  24. package/dist/__tests__/helpers/route-tree.d.ts.map +1 -0
  25. package/dist/__tests__/helpers/route-tree.js +374 -0
  26. package/dist/__tests__/helpers/route-tree.js.map +1 -0
  27. package/dist/__tests__/match-result.test.d.ts +2 -0
  28. package/dist/__tests__/match-result.test.d.ts.map +1 -0
  29. package/dist/__tests__/match-result.test.js +154 -0
  30. package/dist/__tests__/match-result.test.js.map +1 -0
  31. package/dist/__tests__/navigation-store.test.d.ts +2 -0
  32. package/dist/__tests__/navigation-store.test.d.ts.map +1 -0
  33. package/dist/__tests__/navigation-store.test.js +440 -0
  34. package/dist/__tests__/navigation-store.test.js.map +1 -0
  35. package/dist/__tests__/partial-update.test.d.ts +2 -0
  36. package/dist/__tests__/partial-update.test.d.ts.map +1 -0
  37. package/dist/__tests__/partial-update.test.js +1009 -0
  38. package/dist/__tests__/partial-update.test.js.map +1 -0
  39. package/dist/__tests__/reverse-types.test.d.ts +8 -0
  40. package/dist/__tests__/reverse-types.test.d.ts.map +1 -0
  41. package/dist/__tests__/reverse-types.test.js +656 -0
  42. package/dist/__tests__/reverse-types.test.js.map +1 -0
  43. package/dist/__tests__/route-definition.test.d.ts +2 -0
  44. package/dist/__tests__/route-definition.test.d.ts.map +1 -0
  45. package/dist/__tests__/route-definition.test.js +55 -0
  46. package/dist/__tests__/route-definition.test.js.map +1 -0
  47. package/dist/__tests__/router-helpers.test.d.ts +2 -0
  48. package/dist/__tests__/router-helpers.test.d.ts.map +1 -0
  49. package/dist/__tests__/router-helpers.test.js +377 -0
  50. package/dist/__tests__/router-helpers.test.js.map +1 -0
  51. package/dist/__tests__/router-integration-2.test.d.ts +2 -0
  52. package/dist/__tests__/router-integration-2.test.d.ts.map +1 -0
  53. package/dist/__tests__/router-integration-2.test.js +426 -0
  54. package/dist/__tests__/router-integration-2.test.js.map +1 -0
  55. package/dist/__tests__/router-integration.test.d.ts +2 -0
  56. package/dist/__tests__/router-integration.test.d.ts.map +1 -0
  57. package/dist/__tests__/router-integration.test.js +1051 -0
  58. package/dist/__tests__/router-integration.test.js.map +1 -0
  59. package/dist/__tests__/search-params.test.d.ts +5 -0
  60. package/dist/__tests__/search-params.test.d.ts.map +1 -0
  61. package/dist/__tests__/search-params.test.js +306 -0
  62. package/dist/__tests__/search-params.test.js.map +1 -0
  63. package/dist/__tests__/segment-system.test.d.ts +2 -0
  64. package/dist/__tests__/segment-system.test.d.ts.map +1 -0
  65. package/dist/__tests__/segment-system.test.js +627 -0
  66. package/dist/__tests__/segment-system.test.js.map +1 -0
  67. package/dist/__tests__/static-handler-types.test.d.ts +8 -0
  68. package/dist/__tests__/static-handler-types.test.d.ts.map +1 -0
  69. package/dist/__tests__/static-handler-types.test.js +63 -0
  70. package/dist/__tests__/static-handler-types.test.js.map +1 -0
  71. package/dist/__tests__/urls.test.d.ts +2 -0
  72. package/dist/__tests__/urls.test.d.ts.map +1 -0
  73. package/dist/__tests__/urls.test.js +421 -0
  74. package/dist/__tests__/urls.test.js.map +1 -0
  75. package/dist/__tests__/use-mount.test.d.ts +2 -0
  76. package/dist/__tests__/use-mount.test.d.ts.map +1 -0
  77. package/dist/__tests__/use-mount.test.js +35 -0
  78. package/dist/__tests__/use-mount.test.js.map +1 -0
  79. package/dist/bin/rango.d.ts +2 -0
  80. package/dist/bin/rango.d.ts.map +1 -0
  81. package/dist/bin/rango.js +2343 -0
  82. package/dist/bin/rango.js.map +1 -0
  83. package/dist/browser/event-controller.d.ts +191 -0
  84. package/dist/browser/event-controller.d.ts.map +1 -0
  85. package/dist/browser/event-controller.js +559 -0
  86. package/dist/browser/event-controller.js.map +1 -0
  87. package/dist/browser/index.d.ts +2 -0
  88. package/dist/browser/index.d.ts.map +1 -0
  89. package/dist/browser/index.js +14 -0
  90. package/dist/browser/index.js.map +1 -0
  91. package/dist/browser/link-interceptor.d.ts +38 -0
  92. package/dist/browser/link-interceptor.d.ts.map +1 -0
  93. package/dist/browser/link-interceptor.js +99 -0
  94. package/dist/browser/link-interceptor.js.map +1 -0
  95. package/dist/browser/logging.d.ts +10 -0
  96. package/dist/browser/logging.d.ts.map +1 -0
  97. package/dist/browser/logging.js +29 -0
  98. package/dist/browser/logging.js.map +1 -0
  99. package/dist/browser/lru-cache.d.ts +17 -0
  100. package/dist/browser/lru-cache.d.ts.map +1 -0
  101. package/dist/browser/lru-cache.js +50 -0
  102. package/dist/browser/lru-cache.js.map +1 -0
  103. package/dist/browser/merge-segment-loaders.d.ts +39 -0
  104. package/dist/browser/merge-segment-loaders.d.ts.map +1 -0
  105. package/dist/browser/merge-segment-loaders.js +102 -0
  106. package/dist/browser/merge-segment-loaders.js.map +1 -0
  107. package/dist/browser/navigation-bridge.d.ts +102 -0
  108. package/dist/browser/navigation-bridge.d.ts.map +1 -0
  109. package/dist/browser/navigation-bridge.js +708 -0
  110. package/dist/browser/navigation-bridge.js.map +1 -0
  111. package/dist/browser/navigation-client.d.ts +25 -0
  112. package/dist/browser/navigation-client.d.ts.map +1 -0
  113. package/dist/browser/navigation-client.js +157 -0
  114. package/dist/browser/navigation-client.js.map +1 -0
  115. package/dist/browser/navigation-store.d.ts +101 -0
  116. package/dist/browser/navigation-store.d.ts.map +1 -0
  117. package/dist/browser/navigation-store.js +625 -0
  118. package/dist/browser/navigation-store.js.map +1 -0
  119. package/dist/browser/partial-update.d.ts +75 -0
  120. package/dist/browser/partial-update.d.ts.map +1 -0
  121. package/dist/browser/partial-update.js +426 -0
  122. package/dist/browser/partial-update.js.map +1 -0
  123. package/dist/browser/react/Link.d.ts +86 -0
  124. package/dist/browser/react/Link.d.ts.map +1 -0
  125. package/dist/browser/react/Link.js +128 -0
  126. package/dist/browser/react/Link.js.map +1 -0
  127. package/dist/browser/react/NavigationProvider.d.ts +63 -0
  128. package/dist/browser/react/NavigationProvider.d.ts.map +1 -0
  129. package/dist/browser/react/NavigationProvider.js +216 -0
  130. package/dist/browser/react/NavigationProvider.js.map +1 -0
  131. package/dist/browser/react/ScrollRestoration.d.ts +75 -0
  132. package/dist/browser/react/ScrollRestoration.d.ts.map +1 -0
  133. package/dist/browser/react/ScrollRestoration.js +57 -0
  134. package/dist/browser/react/ScrollRestoration.js.map +1 -0
  135. package/dist/browser/react/context.d.ts +46 -0
  136. package/dist/browser/react/context.d.ts.map +1 -0
  137. package/dist/browser/react/context.js +10 -0
  138. package/dist/browser/react/context.js.map +1 -0
  139. package/dist/browser/react/index.d.ts +11 -0
  140. package/dist/browser/react/index.d.ts.map +1 -0
  141. package/dist/browser/react/index.js +22 -0
  142. package/dist/browser/react/index.js.map +1 -0
  143. package/dist/browser/react/location-state-shared.d.ts +63 -0
  144. package/dist/browser/react/location-state-shared.d.ts.map +1 -0
  145. package/dist/browser/react/location-state-shared.js +81 -0
  146. package/dist/browser/react/location-state-shared.js.map +1 -0
  147. package/dist/browser/react/location-state.d.ts +23 -0
  148. package/dist/browser/react/location-state.d.ts.map +1 -0
  149. package/dist/browser/react/location-state.js +29 -0
  150. package/dist/browser/react/location-state.js.map +1 -0
  151. package/dist/browser/react/mount-context.d.ts +24 -0
  152. package/dist/browser/react/mount-context.d.ts.map +1 -0
  153. package/dist/browser/react/mount-context.js +24 -0
  154. package/dist/browser/react/mount-context.js.map +1 -0
  155. package/dist/browser/react/use-action.d.ts +64 -0
  156. package/dist/browser/react/use-action.d.ts.map +1 -0
  157. package/dist/browser/react/use-action.js +134 -0
  158. package/dist/browser/react/use-action.js.map +1 -0
  159. package/dist/browser/react/use-client-cache.d.ts +41 -0
  160. package/dist/browser/react/use-client-cache.d.ts.map +1 -0
  161. package/{src/browser/react/use-client-cache.ts → dist/browser/react/use-client-cache.js} +9 -26
  162. package/dist/browser/react/use-client-cache.js.map +1 -0
  163. package/dist/browser/react/use-handle.d.ts +31 -0
  164. package/dist/browser/react/use-handle.d.ts.map +1 -0
  165. package/dist/browser/react/use-handle.js +144 -0
  166. package/dist/browser/react/use-handle.js.map +1 -0
  167. package/dist/browser/react/use-href.d.ts +33 -0
  168. package/dist/browser/react/use-href.d.ts.map +1 -0
  169. package/dist/browser/react/use-href.js +39 -0
  170. package/dist/browser/react/use-href.js.map +1 -0
  171. package/dist/browser/react/use-link-status.d.ts +37 -0
  172. package/dist/browser/react/use-link-status.d.ts.map +1 -0
  173. package/dist/browser/react/use-link-status.js +99 -0
  174. package/dist/browser/react/use-link-status.js.map +1 -0
  175. package/dist/browser/react/use-mount.d.ts +25 -0
  176. package/dist/browser/react/use-mount.d.ts.map +1 -0
  177. package/dist/browser/react/use-mount.js +30 -0
  178. package/dist/browser/react/use-mount.js.map +1 -0
  179. package/dist/browser/react/use-navigation.d.ts +27 -0
  180. package/dist/browser/react/use-navigation.d.ts.map +1 -0
  181. package/dist/browser/react/use-navigation.js +87 -0
  182. package/dist/browser/react/use-navigation.js.map +1 -0
  183. package/dist/browser/react/use-segments.d.ts +38 -0
  184. package/dist/browser/react/use-segments.d.ts.map +1 -0
  185. package/dist/browser/react/use-segments.js +130 -0
  186. package/dist/browser/react/use-segments.js.map +1 -0
  187. package/dist/browser/request-controller.d.ts +26 -0
  188. package/dist/browser/request-controller.d.ts.map +1 -0
  189. package/dist/browser/request-controller.js +147 -0
  190. package/dist/browser/request-controller.js.map +1 -0
  191. package/dist/browser/rsc-router.d.ts +129 -0
  192. package/dist/browser/rsc-router.d.ts.map +1 -0
  193. package/dist/browser/rsc-router.js +195 -0
  194. package/dist/browser/rsc-router.js.map +1 -0
  195. package/dist/browser/scroll-restoration.d.ts +93 -0
  196. package/dist/browser/scroll-restoration.d.ts.map +1 -0
  197. package/dist/browser/scroll-restoration.js +321 -0
  198. package/dist/browser/scroll-restoration.js.map +1 -0
  199. package/dist/browser/segment-structure-assert.d.ts +17 -0
  200. package/dist/browser/segment-structure-assert.d.ts.map +1 -0
  201. package/dist/browser/segment-structure-assert.js +59 -0
  202. package/dist/browser/segment-structure-assert.js.map +1 -0
  203. package/dist/browser/server-action-bridge.d.ts +26 -0
  204. package/dist/browser/server-action-bridge.d.ts.map +1 -0
  205. package/dist/browser/server-action-bridge.js +668 -0
  206. package/dist/browser/server-action-bridge.js.map +1 -0
  207. package/dist/browser/shallow.d.ts +12 -0
  208. package/dist/browser/shallow.d.ts.map +1 -0
  209. package/dist/browser/shallow.js +34 -0
  210. package/dist/browser/shallow.js.map +1 -0
  211. package/dist/browser/types.d.ts +369 -0
  212. package/dist/browser/types.d.ts.map +1 -0
  213. package/dist/browser/types.js +2 -0
  214. package/dist/browser/types.js.map +1 -0
  215. package/dist/build/__tests__/generate-cli.test.d.ts +2 -0
  216. package/dist/build/__tests__/generate-cli.test.d.ts.map +1 -0
  217. package/dist/build/__tests__/generate-cli.test.js +237 -0
  218. package/dist/build/__tests__/generate-cli.test.js.map +1 -0
  219. package/dist/build/__tests__/generate-manifest.test.d.ts +2 -0
  220. package/dist/build/__tests__/generate-manifest.test.d.ts.map +1 -0
  221. package/dist/build/__tests__/generate-manifest.test.js +119 -0
  222. package/dist/build/__tests__/generate-manifest.test.js.map +1 -0
  223. package/dist/build/__tests__/generate-route-types.test.d.ts +2 -0
  224. package/dist/build/__tests__/generate-route-types.test.d.ts.map +1 -0
  225. package/dist/build/__tests__/generate-route-types.test.js +620 -0
  226. package/dist/build/__tests__/generate-route-types.test.js.map +1 -0
  227. package/dist/build/__tests__/per-router-manifest.test.d.ts +2 -0
  228. package/dist/build/__tests__/per-router-manifest.test.d.ts.map +1 -0
  229. package/dist/build/__tests__/per-router-manifest.test.js +308 -0
  230. package/dist/build/__tests__/per-router-manifest.test.js.map +1 -0
  231. package/dist/build/generate-manifest.d.ts +81 -0
  232. package/dist/build/generate-manifest.d.ts.map +1 -0
  233. package/dist/build/generate-manifest.js +276 -0
  234. package/dist/build/generate-manifest.js.map +1 -0
  235. package/dist/build/generate-route-types.d.ts +115 -0
  236. package/dist/build/generate-route-types.d.ts.map +1 -0
  237. package/dist/build/generate-route-types.js +740 -0
  238. package/dist/build/generate-route-types.js.map +1 -0
  239. package/dist/build/index.d.ts +21 -0
  240. package/dist/build/index.d.ts.map +1 -0
  241. package/dist/build/index.js +21 -0
  242. package/dist/build/index.js.map +1 -0
  243. package/dist/build/route-trie.d.ts +71 -0
  244. package/dist/build/route-trie.d.ts.map +1 -0
  245. package/dist/build/route-trie.js +175 -0
  246. package/dist/build/route-trie.js.map +1 -0
  247. package/dist/cache/__tests__/cache-scope.test.d.ts +2 -0
  248. package/dist/cache/__tests__/cache-scope.test.d.ts.map +1 -0
  249. package/dist/cache/__tests__/cache-scope.test.js +208 -0
  250. package/dist/cache/__tests__/cache-scope.test.js.map +1 -0
  251. package/dist/cache/__tests__/document-cache.test.d.ts +2 -0
  252. package/dist/cache/__tests__/document-cache.test.d.ts.map +1 -0
  253. package/dist/cache/__tests__/document-cache.test.js +345 -0
  254. package/dist/cache/__tests__/document-cache.test.js.map +1 -0
  255. package/dist/cache/__tests__/memory-segment-store.test.d.ts +2 -0
  256. package/dist/cache/__tests__/memory-segment-store.test.d.ts.map +1 -0
  257. package/dist/cache/__tests__/memory-segment-store.test.js +425 -0
  258. package/dist/cache/__tests__/memory-segment-store.test.js.map +1 -0
  259. package/dist/cache/__tests__/memory-store.test.d.ts +2 -0
  260. package/dist/cache/__tests__/memory-store.test.d.ts.map +1 -0
  261. package/dist/cache/__tests__/memory-store.test.js +367 -0
  262. package/dist/cache/__tests__/memory-store.test.js.map +1 -0
  263. package/dist/cache/cache-scope.d.ts +102 -0
  264. package/dist/cache/cache-scope.d.ts.map +1 -0
  265. package/dist/cache/cache-scope.js +440 -0
  266. package/dist/cache/cache-scope.js.map +1 -0
  267. package/dist/cache/cf/__tests__/cf-cache-store.test.d.ts +2 -0
  268. package/dist/cache/cf/__tests__/cf-cache-store.test.d.ts.map +1 -0
  269. package/dist/cache/cf/__tests__/cf-cache-store.test.js +330 -0
  270. package/dist/cache/cf/__tests__/cf-cache-store.test.js.map +1 -0
  271. package/dist/cache/cf/cf-cache-store.d.ts +165 -0
  272. package/dist/cache/cf/cf-cache-store.d.ts.map +1 -0
  273. package/dist/cache/cf/cf-cache-store.js +242 -0
  274. package/dist/cache/cf/cf-cache-store.js.map +1 -0
  275. package/dist/cache/cf/index.d.ts +14 -0
  276. package/dist/cache/cf/index.d.ts.map +1 -0
  277. package/dist/cache/cf/index.js +17 -0
  278. package/dist/cache/cf/index.js.map +1 -0
  279. package/dist/cache/document-cache.d.ts +64 -0
  280. package/dist/cache/document-cache.d.ts.map +1 -0
  281. package/dist/cache/document-cache.js +228 -0
  282. package/dist/cache/document-cache.js.map +1 -0
  283. package/dist/cache/index.d.ts +19 -0
  284. package/dist/cache/index.d.ts.map +1 -0
  285. package/dist/cache/index.js +21 -0
  286. package/dist/cache/index.js.map +1 -0
  287. package/dist/cache/memory-segment-store.d.ts +110 -0
  288. package/dist/cache/memory-segment-store.d.ts.map +1 -0
  289. package/dist/cache/memory-segment-store.js +117 -0
  290. package/dist/cache/memory-segment-store.js.map +1 -0
  291. package/dist/cache/memory-store.d.ts +41 -0
  292. package/dist/cache/memory-store.d.ts.map +1 -0
  293. package/dist/cache/memory-store.js +191 -0
  294. package/dist/cache/memory-store.js.map +1 -0
  295. package/dist/cache/types.d.ts +317 -0
  296. package/dist/cache/types.d.ts.map +1 -0
  297. package/dist/cache/types.js +12 -0
  298. package/dist/cache/types.js.map +1 -0
  299. package/dist/client.d.ts +248 -0
  300. package/dist/client.d.ts.map +1 -0
  301. package/dist/client.js +367 -0
  302. package/dist/client.js.map +1 -0
  303. package/dist/client.rsc.d.ts +26 -0
  304. package/dist/client.rsc.d.ts.map +1 -0
  305. package/dist/client.rsc.js +46 -0
  306. package/dist/client.rsc.js.map +1 -0
  307. package/dist/component-utils.d.ts +36 -0
  308. package/dist/component-utils.d.ts.map +1 -0
  309. package/dist/component-utils.js +61 -0
  310. package/dist/component-utils.js.map +1 -0
  311. package/dist/components/DefaultDocument.d.ts +13 -0
  312. package/dist/components/DefaultDocument.d.ts.map +1 -0
  313. package/dist/components/DefaultDocument.js +15 -0
  314. package/dist/components/DefaultDocument.js.map +1 -0
  315. package/dist/debug.d.ts +58 -0
  316. package/dist/debug.d.ts.map +1 -0
  317. package/dist/debug.js +157 -0
  318. package/dist/debug.js.map +1 -0
  319. package/dist/default-error-boundary.d.ts +11 -0
  320. package/dist/default-error-boundary.d.ts.map +1 -0
  321. package/dist/default-error-boundary.js +45 -0
  322. package/dist/default-error-boundary.js.map +1 -0
  323. package/dist/deps/browser.d.ts +2 -0
  324. package/dist/deps/browser.d.ts.map +1 -0
  325. package/dist/deps/browser.js +3 -0
  326. package/dist/deps/browser.js.map +1 -0
  327. package/dist/deps/html-stream-client.d.ts +2 -0
  328. package/dist/deps/html-stream-client.d.ts.map +1 -0
  329. package/dist/deps/html-stream-client.js +3 -0
  330. package/dist/deps/html-stream-client.js.map +1 -0
  331. package/dist/deps/html-stream-server.d.ts +2 -0
  332. package/dist/deps/html-stream-server.d.ts.map +1 -0
  333. package/dist/deps/html-stream-server.js +3 -0
  334. package/dist/deps/html-stream-server.js.map +1 -0
  335. package/dist/deps/rsc.d.ts +2 -0
  336. package/dist/deps/rsc.d.ts.map +1 -0
  337. package/dist/deps/rsc.js +4 -0
  338. package/dist/deps/rsc.js.map +1 -0
  339. package/dist/deps/ssr.d.ts +2 -0
  340. package/dist/deps/ssr.d.ts.map +1 -0
  341. package/dist/deps/ssr.js +3 -0
  342. package/dist/deps/ssr.js.map +1 -0
  343. package/dist/errors.d.ts +174 -0
  344. package/dist/errors.d.ts.map +1 -0
  345. package/dist/errors.js +241 -0
  346. package/dist/errors.js.map +1 -0
  347. package/dist/handle.d.ts +78 -0
  348. package/dist/handle.d.ts.map +1 -0
  349. package/dist/handle.js +82 -0
  350. package/dist/handle.js.map +1 -0
  351. package/dist/handles/MetaTags.d.ts +14 -0
  352. package/dist/handles/MetaTags.d.ts.map +1 -0
  353. package/dist/handles/MetaTags.js +136 -0
  354. package/dist/handles/MetaTags.js.map +1 -0
  355. package/dist/handles/index.d.ts +6 -0
  356. package/dist/handles/index.d.ts.map +1 -0
  357. package/{src/handles/index.ts → dist/handles/index.js} +1 -1
  358. package/dist/handles/index.js.map +1 -0
  359. package/dist/handles/meta.d.ts +39 -0
  360. package/dist/handles/meta.d.ts.map +1 -0
  361. package/dist/handles/meta.js +202 -0
  362. package/dist/handles/meta.js.map +1 -0
  363. package/dist/host/__tests__/errors.test.d.ts +2 -0
  364. package/dist/host/__tests__/errors.test.d.ts.map +1 -0
  365. package/dist/host/__tests__/errors.test.js +76 -0
  366. package/dist/host/__tests__/errors.test.js.map +1 -0
  367. package/dist/host/__tests__/pattern-comprehensive.test.d.ts +2 -0
  368. package/dist/host/__tests__/pattern-comprehensive.test.d.ts.map +1 -0
  369. package/dist/host/__tests__/pattern-comprehensive.test.js +732 -0
  370. package/dist/host/__tests__/pattern-comprehensive.test.js.map +1 -0
  371. package/dist/host/__tests__/pattern-matcher.test.d.ts +2 -0
  372. package/dist/host/__tests__/pattern-matcher.test.d.ts.map +1 -0
  373. package/dist/host/__tests__/pattern-matcher.test.js +251 -0
  374. package/dist/host/__tests__/pattern-matcher.test.js.map +1 -0
  375. package/dist/host/__tests__/router.test.d.ts +2 -0
  376. package/dist/host/__tests__/router.test.d.ts.map +1 -0
  377. package/dist/host/__tests__/router.test.js +241 -0
  378. package/dist/host/__tests__/router.test.js.map +1 -0
  379. package/dist/host/__tests__/testing.test.d.ts +2 -0
  380. package/dist/host/__tests__/testing.test.d.ts.map +1 -0
  381. package/dist/host/__tests__/testing.test.js +64 -0
  382. package/dist/host/__tests__/testing.test.js.map +1 -0
  383. package/dist/host/__tests__/utils.test.d.ts +2 -0
  384. package/dist/host/__tests__/utils.test.d.ts.map +1 -0
  385. package/dist/host/__tests__/utils.test.js +29 -0
  386. package/dist/host/__tests__/utils.test.js.map +1 -0
  387. package/dist/host/cookie-handler.d.ts +34 -0
  388. package/dist/host/cookie-handler.d.ts.map +1 -0
  389. package/dist/host/cookie-handler.js +124 -0
  390. package/dist/host/cookie-handler.js.map +1 -0
  391. package/dist/host/errors.d.ts +56 -0
  392. package/dist/host/errors.d.ts.map +1 -0
  393. package/dist/host/errors.js +79 -0
  394. package/dist/host/errors.js.map +1 -0
  395. package/dist/host/index.d.ts +29 -0
  396. package/dist/host/index.d.ts.map +1 -0
  397. package/dist/host/index.js +32 -0
  398. package/dist/host/index.js.map +1 -0
  399. package/dist/host/pattern-matcher.d.ts +36 -0
  400. package/dist/host/pattern-matcher.d.ts.map +1 -0
  401. package/dist/host/pattern-matcher.js +172 -0
  402. package/dist/host/pattern-matcher.js.map +1 -0
  403. package/dist/host/router.d.ts +26 -0
  404. package/dist/host/router.d.ts.map +1 -0
  405. package/dist/host/router.js +218 -0
  406. package/dist/host/router.js.map +1 -0
  407. package/dist/host/testing.d.ts +36 -0
  408. package/dist/host/testing.d.ts.map +1 -0
  409. package/dist/host/testing.js +55 -0
  410. package/dist/host/testing.js.map +1 -0
  411. package/dist/host/types.d.ts +115 -0
  412. package/dist/host/types.d.ts.map +1 -0
  413. package/dist/host/types.js +7 -0
  414. package/dist/host/types.js.map +1 -0
  415. package/dist/host/utils.d.ts +21 -0
  416. package/dist/host/utils.d.ts.map +1 -0
  417. package/dist/host/utils.js +23 -0
  418. package/dist/host/utils.js.map +1 -0
  419. package/dist/href-client.d.ts +131 -0
  420. package/dist/href-client.d.ts.map +1 -0
  421. package/dist/href-client.js +64 -0
  422. package/dist/href-client.js.map +1 -0
  423. package/{src/href-context.ts → dist/href-context.d.ts} +7 -11
  424. package/dist/href-context.d.ts.map +1 -0
  425. package/dist/href-context.js +21 -0
  426. package/dist/href-context.js.map +1 -0
  427. package/dist/index.d.ts +73 -0
  428. package/dist/index.d.ts.map +1 -0
  429. package/dist/index.js +91 -0
  430. package/dist/index.js.map +1 -0
  431. package/dist/index.rsc.d.ts +32 -0
  432. package/dist/index.rsc.d.ts.map +1 -0
  433. package/dist/index.rsc.js +40 -0
  434. package/dist/index.rsc.js.map +1 -0
  435. package/dist/internal-debug.d.ts +2 -0
  436. package/dist/internal-debug.d.ts.map +1 -0
  437. package/dist/internal-debug.js +5 -0
  438. package/dist/internal-debug.js.map +1 -0
  439. package/dist/loader.d.ts +14 -0
  440. package/dist/loader.d.ts.map +1 -0
  441. package/dist/loader.js +20 -0
  442. package/dist/loader.js.map +1 -0
  443. package/dist/loader.rsc.d.ts +19 -0
  444. package/dist/loader.rsc.d.ts.map +1 -0
  445. package/dist/loader.rsc.js +99 -0
  446. package/dist/loader.rsc.js.map +1 -0
  447. package/{src/network-error-thrower.tsx → dist/network-error-thrower.d.ts} +4 -8
  448. package/dist/network-error-thrower.d.ts.map +1 -0
  449. package/dist/network-error-thrower.js +14 -0
  450. package/dist/network-error-thrower.js.map +1 -0
  451. package/dist/outlet-context.d.ts +13 -0
  452. package/dist/outlet-context.d.ts.map +1 -0
  453. package/dist/outlet-context.js +3 -0
  454. package/dist/outlet-context.js.map +1 -0
  455. package/dist/prerender/__tests__/param-hash.test.d.ts +2 -0
  456. package/dist/prerender/__tests__/param-hash.test.d.ts.map +1 -0
  457. package/dist/prerender/__tests__/param-hash.test.js +148 -0
  458. package/dist/prerender/__tests__/param-hash.test.js.map +1 -0
  459. package/dist/prerender/param-hash.d.ts +16 -0
  460. package/dist/prerender/param-hash.d.ts.map +1 -0
  461. package/dist/prerender/param-hash.js +36 -0
  462. package/dist/prerender/param-hash.js.map +1 -0
  463. package/dist/prerender/store.d.ts +38 -0
  464. package/dist/prerender/store.d.ts.map +1 -0
  465. package/dist/prerender/store.js +61 -0
  466. package/dist/prerender/store.js.map +1 -0
  467. package/dist/prerender.d.ts +66 -0
  468. package/dist/prerender.d.ts.map +1 -0
  469. package/dist/prerender.js +57 -0
  470. package/dist/prerender.js.map +1 -0
  471. package/dist/reverse.d.ts +196 -0
  472. package/dist/reverse.d.ts.map +1 -0
  473. package/dist/reverse.js +78 -0
  474. package/dist/reverse.js.map +1 -0
  475. package/dist/root-error-boundary.d.ts +33 -0
  476. package/dist/root-error-boundary.d.ts.map +1 -0
  477. package/dist/root-error-boundary.js +165 -0
  478. package/dist/root-error-boundary.js.map +1 -0
  479. package/dist/route-content-wrapper.d.ts +46 -0
  480. package/dist/route-content-wrapper.d.ts.map +1 -0
  481. package/dist/route-content-wrapper.js +77 -0
  482. package/dist/route-content-wrapper.js.map +1 -0
  483. package/dist/route-definition.d.ts +421 -0
  484. package/dist/route-definition.d.ts.map +1 -0
  485. package/dist/route-definition.js +868 -0
  486. package/dist/route-definition.js.map +1 -0
  487. package/dist/route-map-builder.d.ts +155 -0
  488. package/dist/route-map-builder.d.ts.map +1 -0
  489. package/dist/route-map-builder.js +237 -0
  490. package/dist/route-map-builder.js.map +1 -0
  491. package/dist/route-types.d.ts +165 -0
  492. package/dist/route-types.d.ts.map +1 -0
  493. package/dist/route-types.js +7 -0
  494. package/dist/route-types.js.map +1 -0
  495. package/dist/router/__tests__/handler-context.test.d.ts +2 -0
  496. package/dist/router/__tests__/handler-context.test.d.ts.map +1 -0
  497. package/dist/router/__tests__/handler-context.test.js +65 -0
  498. package/dist/router/__tests__/handler-context.test.js.map +1 -0
  499. package/dist/router/__tests__/loader-cycle-detection.test.d.ts +2 -0
  500. package/dist/router/__tests__/loader-cycle-detection.test.d.ts.map +1 -0
  501. package/dist/router/__tests__/loader-cycle-detection.test.js +221 -0
  502. package/dist/router/__tests__/loader-cycle-detection.test.js.map +1 -0
  503. package/dist/router/__tests__/match-context.test.d.ts +2 -0
  504. package/dist/router/__tests__/match-context.test.d.ts.map +1 -0
  505. package/dist/router/__tests__/match-context.test.js +92 -0
  506. package/dist/router/__tests__/match-context.test.js.map +1 -0
  507. package/dist/router/__tests__/match-pipelines.test.d.ts +2 -0
  508. package/dist/router/__tests__/match-pipelines.test.d.ts.map +1 -0
  509. package/dist/router/__tests__/match-pipelines.test.js +417 -0
  510. package/dist/router/__tests__/match-pipelines.test.js.map +1 -0
  511. package/dist/router/__tests__/match-result.test.d.ts +2 -0
  512. package/dist/router/__tests__/match-result.test.d.ts.map +1 -0
  513. package/dist/router/__tests__/match-result.test.js +457 -0
  514. package/dist/router/__tests__/match-result.test.js.map +1 -0
  515. package/dist/router/__tests__/on-error.test.d.ts +2 -0
  516. package/dist/router/__tests__/on-error.test.d.ts.map +1 -0
  517. package/dist/router/__tests__/on-error.test.js +678 -0
  518. package/dist/router/__tests__/on-error.test.js.map +1 -0
  519. package/dist/router/__tests__/pattern-matching.test.d.ts +2 -0
  520. package/dist/router/__tests__/pattern-matching.test.d.ts.map +1 -0
  521. package/dist/router/__tests__/pattern-matching.test.js +629 -0
  522. package/dist/router/__tests__/pattern-matching.test.js.map +1 -0
  523. package/dist/router/__tests__/segment-resolution-parallel-loading.test.d.ts +2 -0
  524. package/dist/router/__tests__/segment-resolution-parallel-loading.test.d.ts.map +1 -0
  525. package/dist/router/__tests__/segment-resolution-parallel-loading.test.js +155 -0
  526. package/dist/router/__tests__/segment-resolution-parallel-loading.test.js.map +1 -0
  527. package/dist/router/error-handling.d.ts +77 -0
  528. package/dist/router/error-handling.d.ts.map +1 -0
  529. package/dist/router/error-handling.js +202 -0
  530. package/dist/router/error-handling.js.map +1 -0
  531. package/dist/router/handler-context.d.ts +20 -0
  532. package/dist/router/handler-context.d.ts.map +1 -0
  533. package/dist/router/handler-context.js +198 -0
  534. package/dist/router/handler-context.js.map +1 -0
  535. package/dist/router/intercept-resolution.d.ts +66 -0
  536. package/dist/router/intercept-resolution.d.ts.map +1 -0
  537. package/dist/router/intercept-resolution.js +246 -0
  538. package/dist/router/intercept-resolution.js.map +1 -0
  539. package/dist/router/loader-resolution.d.ts +64 -0
  540. package/dist/router/loader-resolution.d.ts.map +1 -0
  541. package/dist/router/loader-resolution.js +284 -0
  542. package/dist/router/loader-resolution.js.map +1 -0
  543. package/dist/router/logging.d.ts +15 -0
  544. package/dist/router/logging.d.ts.map +1 -0
  545. package/dist/router/logging.js +99 -0
  546. package/dist/router/logging.js.map +1 -0
  547. package/dist/router/manifest.d.ts +22 -0
  548. package/dist/router/manifest.d.ts.map +1 -0
  549. package/dist/router/manifest.js +181 -0
  550. package/dist/router/manifest.js.map +1 -0
  551. package/dist/router/match-api.d.ts +35 -0
  552. package/dist/router/match-api.d.ts.map +1 -0
  553. package/dist/router/match-api.js +406 -0
  554. package/dist/router/match-api.js.map +1 -0
  555. package/dist/router/match-context.d.ts +206 -0
  556. package/dist/router/match-context.d.ts.map +1 -0
  557. package/dist/router/match-context.js +17 -0
  558. package/dist/router/match-context.js.map +1 -0
  559. package/dist/router/match-middleware/background-revalidation.d.ts +127 -0
  560. package/dist/router/match-middleware/background-revalidation.d.ts.map +1 -0
  561. package/dist/router/match-middleware/background-revalidation.js +75 -0
  562. package/dist/router/match-middleware/background-revalidation.js.map +1 -0
  563. package/dist/router/match-middleware/cache-lookup.d.ts +112 -0
  564. package/dist/router/match-middleware/cache-lookup.d.ts.map +1 -0
  565. package/dist/router/match-middleware/cache-lookup.js +257 -0
  566. package/dist/router/match-middleware/cache-lookup.js.map +1 -0
  567. package/dist/router/match-middleware/cache-store.d.ts +113 -0
  568. package/dist/router/match-middleware/cache-store.d.ts.map +1 -0
  569. package/dist/router/match-middleware/cache-store.js +108 -0
  570. package/dist/router/match-middleware/cache-store.js.map +1 -0
  571. package/dist/router/match-middleware/index.d.ts +81 -0
  572. package/dist/router/match-middleware/index.d.ts.map +1 -0
  573. package/dist/router/match-middleware/index.js +80 -0
  574. package/dist/router/match-middleware/index.js.map +1 -0
  575. package/dist/router/match-middleware/intercept-resolution.d.ts +117 -0
  576. package/dist/router/match-middleware/intercept-resolution.d.ts.map +1 -0
  577. package/dist/router/match-middleware/intercept-resolution.js +134 -0
  578. package/dist/router/match-middleware/intercept-resolution.js.map +1 -0
  579. package/dist/router/match-middleware/segment-resolution.d.ts +99 -0
  580. package/dist/router/match-middleware/segment-resolution.d.ts.map +1 -0
  581. package/dist/router/match-middleware/segment-resolution.js +53 -0
  582. package/dist/router/match-middleware/segment-resolution.js.map +1 -0
  583. package/dist/router/match-pipelines.d.ts +147 -0
  584. package/dist/router/match-pipelines.d.ts.map +1 -0
  585. package/dist/router/match-pipelines.js +82 -0
  586. package/dist/router/match-pipelines.js.map +1 -0
  587. package/dist/router/match-result.d.ts +126 -0
  588. package/dist/router/match-result.d.ts.map +1 -0
  589. package/dist/router/match-result.js +93 -0
  590. package/dist/router/match-result.js.map +1 -0
  591. package/dist/router/metrics.d.ts +20 -0
  592. package/dist/router/metrics.d.ts.map +1 -0
  593. package/dist/router/metrics.js +47 -0
  594. package/dist/router/metrics.js.map +1 -0
  595. package/dist/router/middleware.d.ts +249 -0
  596. package/dist/router/middleware.d.ts.map +1 -0
  597. package/dist/router/middleware.js +434 -0
  598. package/dist/router/middleware.js.map +1 -0
  599. package/dist/router/middleware.test.d.ts +2 -0
  600. package/dist/router/middleware.test.d.ts.map +1 -0
  601. package/dist/router/middleware.test.js +816 -0
  602. package/dist/router/middleware.test.js.map +1 -0
  603. package/dist/router/pattern-matching.d.ts +149 -0
  604. package/dist/router/pattern-matching.d.ts.map +1 -0
  605. package/dist/router/pattern-matching.js +349 -0
  606. package/dist/router/pattern-matching.js.map +1 -0
  607. package/dist/router/revalidation.d.ts +44 -0
  608. package/dist/router/revalidation.d.ts.map +1 -0
  609. package/dist/router/revalidation.js +147 -0
  610. package/dist/router/revalidation.js.map +1 -0
  611. package/dist/router/router-context.d.ts +135 -0
  612. package/dist/router/router-context.d.ts.map +1 -0
  613. package/dist/router/router-context.js +36 -0
  614. package/dist/router/router-context.js.map +1 -0
  615. package/dist/router/segment-resolution.d.ts +127 -0
  616. package/dist/router/segment-resolution.d.ts.map +1 -0
  617. package/dist/router/segment-resolution.js +919 -0
  618. package/dist/router/segment-resolution.js.map +1 -0
  619. package/dist/router/trie-matching.d.ts +40 -0
  620. package/dist/router/trie-matching.d.ts.map +1 -0
  621. package/dist/router/trie-matching.js +127 -0
  622. package/dist/router/trie-matching.js.map +1 -0
  623. package/dist/router/types.d.ts +136 -0
  624. package/dist/router/types.d.ts.map +1 -0
  625. package/dist/router/types.js +7 -0
  626. package/dist/router/types.js.map +1 -0
  627. package/dist/router.d.ts +753 -0
  628. package/dist/router.d.ts.map +1 -0
  629. package/dist/router.gen.d.ts +6 -0
  630. package/dist/router.gen.d.ts.map +1 -0
  631. package/dist/router.gen.js +6 -0
  632. package/dist/router.gen.js.map +1 -0
  633. package/dist/router.js +1304 -0
  634. package/dist/router.js.map +1 -0
  635. package/dist/rsc/__tests__/helpers.test.d.ts +2 -0
  636. package/dist/rsc/__tests__/helpers.test.d.ts.map +1 -0
  637. package/dist/rsc/__tests__/helpers.test.js +140 -0
  638. package/dist/rsc/__tests__/helpers.test.js.map +1 -0
  639. package/dist/rsc/handler.d.ts +45 -0
  640. package/dist/rsc/handler.d.ts.map +1 -0
  641. package/dist/rsc/handler.js +1172 -0
  642. package/dist/rsc/handler.js.map +1 -0
  643. package/dist/rsc/helpers.d.ts +16 -0
  644. package/dist/rsc/helpers.d.ts.map +1 -0
  645. package/dist/rsc/helpers.js +55 -0
  646. package/dist/rsc/helpers.js.map +1 -0
  647. package/dist/rsc/index.d.ts +22 -0
  648. package/dist/rsc/index.d.ts.map +1 -0
  649. package/dist/rsc/index.js +23 -0
  650. package/dist/rsc/index.js.map +1 -0
  651. package/dist/rsc/nonce.d.ts +9 -0
  652. package/dist/rsc/nonce.d.ts.map +1 -0
  653. package/dist/rsc/nonce.js +18 -0
  654. package/dist/rsc/nonce.js.map +1 -0
  655. package/dist/rsc/types.d.ts +206 -0
  656. package/dist/rsc/types.d.ts.map +1 -0
  657. package/dist/rsc/types.js +8 -0
  658. package/dist/rsc/types.js.map +1 -0
  659. package/dist/search-params.d.ts +103 -0
  660. package/dist/search-params.d.ts.map +1 -0
  661. package/dist/search-params.js +74 -0
  662. package/dist/search-params.js.map +1 -0
  663. package/dist/segment-system.d.ts +75 -0
  664. package/dist/segment-system.d.ts.map +1 -0
  665. package/dist/segment-system.js +336 -0
  666. package/dist/segment-system.js.map +1 -0
  667. package/dist/server/context.d.ts +245 -0
  668. package/dist/server/context.d.ts.map +1 -0
  669. package/dist/server/context.js +197 -0
  670. package/dist/server/context.js.map +1 -0
  671. package/dist/server/fetchable-loader-store.d.ts +18 -0
  672. package/dist/server/fetchable-loader-store.d.ts.map +1 -0
  673. package/dist/server/fetchable-loader-store.js +18 -0
  674. package/dist/server/fetchable-loader-store.js.map +1 -0
  675. package/dist/server/handle-store.d.ts +85 -0
  676. package/dist/server/handle-store.d.ts.map +1 -0
  677. package/dist/server/handle-store.js +142 -0
  678. package/dist/server/handle-store.js.map +1 -0
  679. package/dist/server/loader-registry.d.ts +55 -0
  680. package/dist/server/loader-registry.d.ts.map +1 -0
  681. package/dist/server/loader-registry.js +132 -0
  682. package/dist/server/loader-registry.js.map +1 -0
  683. package/dist/server/request-context.d.ts +226 -0
  684. package/dist/server/request-context.d.ts.map +1 -0
  685. package/dist/server/request-context.js +290 -0
  686. package/dist/server/request-context.js.map +1 -0
  687. package/dist/server/root-layout.d.ts +4 -0
  688. package/dist/server/root-layout.d.ts.map +1 -0
  689. package/dist/server/root-layout.js +5 -0
  690. package/dist/server/root-layout.js.map +1 -0
  691. package/dist/server.d.ts +15 -0
  692. package/dist/server.d.ts.map +1 -0
  693. package/dist/server.js +20 -0
  694. package/dist/server.js.map +1 -0
  695. package/dist/ssr/__tests__/ssr-handler.test.d.ts +2 -0
  696. package/dist/ssr/__tests__/ssr-handler.test.d.ts.map +1 -0
  697. package/dist/ssr/__tests__/ssr-handler.test.js +132 -0
  698. package/dist/ssr/__tests__/ssr-handler.test.js.map +1 -0
  699. package/dist/ssr/index.d.ts +98 -0
  700. package/dist/ssr/index.d.ts.map +1 -0
  701. package/dist/ssr/index.js +158 -0
  702. package/dist/ssr/index.js.map +1 -0
  703. package/dist/static-handler.d.ts +50 -0
  704. package/dist/static-handler.d.ts.map +1 -0
  705. package/dist/static-handler.gen.d.ts +5 -0
  706. package/dist/static-handler.gen.d.ts.map +1 -0
  707. package/dist/static-handler.gen.js +5 -0
  708. package/dist/static-handler.gen.js.map +1 -0
  709. package/dist/static-handler.js +29 -0
  710. package/dist/static-handler.js.map +1 -0
  711. package/dist/testing/vitest.js +82 -0
  712. package/dist/theme/ThemeProvider.d.ts +20 -0
  713. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  714. package/dist/theme/ThemeProvider.js +240 -0
  715. package/dist/theme/ThemeProvider.js.map +1 -0
  716. package/dist/theme/ThemeScript.d.ts +48 -0
  717. package/dist/theme/ThemeScript.d.ts.map +1 -0
  718. package/dist/theme/ThemeScript.js +13 -0
  719. package/dist/theme/ThemeScript.js.map +1 -0
  720. package/dist/theme/__tests__/theme.test.d.ts +2 -0
  721. package/dist/theme/__tests__/theme.test.d.ts.map +1 -0
  722. package/dist/theme/__tests__/theme.test.js +103 -0
  723. package/dist/theme/__tests__/theme.test.js.map +1 -0
  724. package/dist/theme/constants.d.ts +29 -0
  725. package/dist/theme/constants.d.ts.map +1 -0
  726. package/dist/theme/constants.js +48 -0
  727. package/dist/theme/constants.js.map +1 -0
  728. package/dist/theme/index.d.ts +31 -0
  729. package/dist/theme/index.d.ts.map +1 -0
  730. package/dist/theme/index.js +36 -0
  731. package/dist/theme/index.js.map +1 -0
  732. package/dist/theme/theme-context.d.ts +40 -0
  733. package/dist/theme/theme-context.d.ts.map +1 -0
  734. package/dist/theme/theme-context.js +60 -0
  735. package/dist/theme/theme-context.js.map +1 -0
  736. package/dist/theme/theme-script.d.ts +27 -0
  737. package/dist/theme/theme-script.d.ts.map +1 -0
  738. package/dist/theme/theme-script.js +147 -0
  739. package/dist/theme/theme-script.js.map +1 -0
  740. package/dist/theme/types.d.ts +163 -0
  741. package/dist/theme/types.d.ts.map +1 -0
  742. package/dist/theme/types.js +11 -0
  743. package/dist/theme/types.js.map +1 -0
  744. package/dist/theme/use-theme.d.ts +12 -0
  745. package/dist/theme/use-theme.d.ts.map +1 -0
  746. package/dist/theme/use-theme.js +40 -0
  747. package/dist/theme/use-theme.js.map +1 -0
  748. package/dist/types/__internal.d.ts +127 -0
  749. package/dist/types/bin/rango.d.ts +1 -0
  750. package/dist/types/browser/action-coordinator.d.ts +57 -0
  751. package/dist/types/browser/action-fence.d.ts +33 -0
  752. package/dist/types/browser/app-shell.d.ts +34 -0
  753. package/dist/types/browser/app-version.d.ts +6 -0
  754. package/dist/types/browser/connection-warmup.d.ts +31 -0
  755. package/dist/types/browser/cookie-name.d.ts +66 -0
  756. package/dist/types/browser/dev-discovery.d.ts +11 -0
  757. package/dist/types/browser/event-controller.d.ts +231 -0
  758. package/dist/types/browser/history-state.d.ts +26 -0
  759. package/dist/types/browser/index.d.ts +1 -0
  760. package/dist/types/browser/intercept-utils.d.ts +30 -0
  761. package/dist/types/browser/invalidate-client-cache.d.ts +17 -0
  762. package/dist/types/browser/link-interceptor.d.ts +53 -0
  763. package/dist/types/browser/logging.d.ts +33 -0
  764. package/dist/types/browser/merge-segment-loaders.d.ts +38 -0
  765. package/dist/types/browser/navigation-bridge.d.ts +39 -0
  766. package/dist/types/browser/navigation-client.d.ts +17 -0
  767. package/dist/types/browser/navigation-store-handle.d.ts +25 -0
  768. package/dist/types/browser/navigation-store.d.ts +85 -0
  769. package/dist/types/browser/navigation-transaction.d.ts +75 -0
  770. package/dist/types/browser/network-error-handler.d.ts +35 -0
  771. package/dist/types/browser/notify-listeners.d.ts +2 -0
  772. package/dist/types/browser/partial-update.d.ts +61 -0
  773. package/dist/types/browser/prefetch/cache.d.ts +224 -0
  774. package/dist/types/browser/prefetch/default-strategy.d.ts +31 -0
  775. package/dist/types/browser/prefetch/fetch.d.ts +52 -0
  776. package/dist/types/browser/prefetch/invalidation.d.ts +6 -0
  777. package/dist/types/browser/prefetch/loader.d.ts +12 -0
  778. package/dist/types/browser/prefetch/observer.d.ts +24 -0
  779. package/dist/types/browser/prefetch/policy.d.ts +13 -0
  780. package/dist/types/browser/prefetch/queue.d.ts +48 -0
  781. package/dist/types/browser/prefetch/resource-ready.d.ts +28 -0
  782. package/dist/types/browser/prefetch/runtime.d.ts +2 -0
  783. package/dist/types/browser/rango-state.d.ts +56 -0
  784. package/dist/types/browser/react/Link.d.ts +132 -0
  785. package/dist/types/browser/react/NavigationProvider.d.ts +88 -0
  786. package/dist/types/browser/react/ScrollRestoration.d.ts +78 -0
  787. package/dist/types/browser/react/context.d.ts +57 -0
  788. package/dist/types/browser/react/filter-segment-order.d.ts +35 -0
  789. package/dist/types/browser/react/index.d.ts +1 -0
  790. package/dist/types/browser/react/location-state-shared.d.ts +162 -0
  791. package/dist/types/browser/react/location-state.d.ts +29 -0
  792. package/dist/types/browser/react/mount-context.d.ts +23 -0
  793. package/dist/types/browser/react/nonce-context.d.ts +14 -0
  794. package/dist/types/browser/react/shallow-equal.d.ts +5 -0
  795. package/dist/types/browser/react/use-action.d.ts +61 -0
  796. package/dist/types/browser/react/use-handle.d.ts +21 -0
  797. package/dist/types/browser/react/use-href.d.ts +32 -0
  798. package/dist/types/browser/react/use-link-status.d.ts +36 -0
  799. package/dist/types/browser/react/use-mount.d.ts +24 -0
  800. package/dist/types/browser/react/use-navigation.d.ts +15 -0
  801. package/dist/types/browser/react/use-params.d.ts +21 -0
  802. package/dist/types/browser/react/use-pathname.d.ts +13 -0
  803. package/dist/types/browser/react/use-reverse.d.ts +40 -0
  804. package/dist/types/browser/react/use-router.d.ts +30 -0
  805. package/dist/types/browser/react/use-search-params.d.ts +66 -0
  806. package/dist/types/browser/react/use-segments.d.ts +29 -0
  807. package/dist/types/browser/response-adapter.d.ts +58 -0
  808. package/dist/types/browser/rsc-router.d.ts +146 -0
  809. package/dist/types/browser/scroll-restoration.d.ts +103 -0
  810. package/dist/types/browser/segment-reconciler.d.ts +74 -0
  811. package/dist/types/browser/segment-structure-assert.d.ts +16 -0
  812. package/dist/types/browser/server-action-bridge.d.ts +29 -0
  813. package/dist/types/browser/types.d.ts +546 -0
  814. package/dist/types/browser/validate-redirect-origin.d.ts +28 -0
  815. package/dist/types/build/collect-fallback-refs.d.ts +5 -0
  816. package/dist/types/build/generate-manifest.d.ts +106 -0
  817. package/dist/types/build/generate-route-types.d.ts +8 -0
  818. package/dist/types/build/index.d.ts +21 -0
  819. package/dist/types/build/merge-full-manifests.d.ts +3 -0
  820. package/dist/types/build/prefix-tree-utils.d.ts +56 -0
  821. package/dist/types/build/route-trie.d.ts +20 -0
  822. package/dist/types/build/route-types/ast-helpers.d.ts +3 -0
  823. package/dist/types/build/route-types/ast-route-extraction.d.ts +13 -0
  824. package/dist/types/build/route-types/codegen.d.ts +16 -0
  825. package/dist/types/build/route-types/include-resolution.d.ts +74 -0
  826. package/dist/types/build/route-types/param-extraction.d.ts +13 -0
  827. package/dist/types/build/route-types/per-module-writer.d.ts +20 -0
  828. package/dist/types/build/route-types/router-processing.d.ts +81 -0
  829. package/dist/types/build/route-types/scan-filter.d.ts +17 -0
  830. package/dist/types/build/route-types/source-scan.d.ts +13 -0
  831. package/dist/types/build/runtime-discovery.d.ts +24 -0
  832. package/dist/types/cache/background-task.d.ts +21 -0
  833. package/dist/types/cache/cache-error.d.ts +71 -0
  834. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  835. package/dist/types/cache/cache-key-utils.d.ts +43 -0
  836. package/dist/types/cache/cache-policy.d.ts +59 -0
  837. package/dist/types/cache/cache-runtime.d.ts +51 -0
  838. package/dist/types/cache/cache-scope.d.ts +215 -0
  839. package/dist/types/cache/cache-tag.d.ts +79 -0
  840. package/dist/types/cache/cf/cf-base64.d.ts +4 -0
  841. package/dist/types/cache/cf/cf-cache-constants.d.ts +112 -0
  842. package/dist/types/cache/cf/cf-cache-store.d.ts +630 -0
  843. package/dist/types/cache/cf/cf-cache-types.d.ts +365 -0
  844. package/dist/types/cache/cf/cf-kv-utils.d.ts +43 -0
  845. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +15 -0
  846. package/dist/types/cache/cf/cf-zone-purge.d.ts +31 -0
  847. package/dist/types/cache/cf/index.d.ts +4 -0
  848. package/dist/types/cache/document-cache.d.ts +71 -0
  849. package/dist/types/cache/handle-capture.d.ts +23 -0
  850. package/dist/types/cache/handle-snapshot.d.ts +39 -0
  851. package/dist/types/cache/index.d.ts +9 -0
  852. package/dist/types/cache/memory-segment-store.d.ts +163 -0
  853. package/dist/types/cache/profile-registry.d.ts +40 -0
  854. package/dist/types/cache/read-through-swr.d.ts +61 -0
  855. package/dist/types/cache/search-params-filter.d.ts +64 -0
  856. package/dist/types/cache/segment-codec.d.ts +78 -0
  857. package/dist/types/cache/shell-snapshot.d.ts +186 -0
  858. package/dist/types/cache/tag-invalidation.d.ts +74 -0
  859. package/dist/types/cache/taint.d.ts +77 -0
  860. package/dist/types/cache/types.d.ts +522 -0
  861. package/dist/types/cache/vercel/index.d.ts +1 -0
  862. package/dist/types/cache/vercel/vercel-cache-store.d.ts +268 -0
  863. package/dist/types/client-urls/client-root.d.ts +38 -0
  864. package/dist/types/client-urls/client-urls.d.ts +5 -0
  865. package/dist/types/client-urls/navigation.d.ts +38 -0
  866. package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
  867. package/dist/types/client-urls/server-projection.d.ts +71 -0
  868. package/dist/types/client-urls/types.d.ts +148 -0
  869. package/dist/types/client.d.ts +192 -0
  870. package/dist/types/client.rsc.d.ts +42 -0
  871. package/dist/types/cloudflare/index.d.ts +7 -0
  872. package/dist/types/cloudflare/tracing.d.ts +57 -0
  873. package/dist/types/component-utils.d.ts +46 -0
  874. package/dist/types/components/DefaultDocument.d.ts +13 -0
  875. package/dist/types/context-var.d.ts +84 -0
  876. package/dist/types/debug.d.ts +57 -0
  877. package/dist/types/decode-loader-results.d.ts +42 -0
  878. package/dist/types/default-error-boundary.d.ts +10 -0
  879. package/dist/types/defer.d.ts +89 -0
  880. package/dist/types/deps/browser.d.ts +1 -0
  881. package/dist/types/deps/html-stream-client.d.ts +1 -0
  882. package/dist/types/deps/html-stream-server.d.ts +1 -0
  883. package/dist/types/deps/rsc.d.ts +1 -0
  884. package/dist/types/deps/ssr.d.ts +1 -0
  885. package/dist/types/dev-discovery-protocol.d.ts +5 -0
  886. package/dist/types/encode-kv.d.ts +35 -0
  887. package/dist/types/errors.d.ts +227 -0
  888. package/dist/types/escape-script.d.ts +44 -0
  889. package/dist/types/handle.d.ts +93 -0
  890. package/dist/types/handles/MetaTags.d.ts +17 -0
  891. package/dist/types/handles/Scripts.d.ts +38 -0
  892. package/dist/types/handles/breadcrumbs.d.ts +43 -0
  893. package/dist/types/handles/deferred-resolution.d.ts +53 -0
  894. package/dist/types/handles/is-thenable.d.ts +10 -0
  895. package/dist/types/handles/meta.d.ts +43 -0
  896. package/dist/types/handles/script.d.ts +139 -0
  897. package/dist/types/host/cookie-handler.d.ts +8 -0
  898. package/dist/types/host/errors.d.ts +40 -0
  899. package/dist/types/host/index.d.ts +33 -0
  900. package/dist/types/host/pattern-matcher.d.ts +30 -0
  901. package/dist/types/host/router.d.ts +12 -0
  902. package/dist/types/host/testing.d.ts +41 -0
  903. package/dist/types/host/types.d.ts +148 -0
  904. package/dist/types/host/utils.d.ts +20 -0
  905. package/dist/types/href-client.d.ts +214 -0
  906. package/dist/types/index.d.ts +113 -0
  907. package/dist/types/index.rsc.d.ts +52 -0
  908. package/dist/types/internal-debug.d.ts +1 -0
  909. package/dist/types/loader-redirect.d.ts +27 -0
  910. package/dist/types/loader-store.d.ts +193 -0
  911. package/dist/types/loader.d.ts +18 -0
  912. package/dist/types/loader.rsc.d.ts +18 -0
  913. package/dist/types/missing-id-error.d.ts +1 -0
  914. package/dist/types/outlet-context.d.ts +32 -0
  915. package/dist/types/outlet-provider.d.ts +15 -0
  916. package/dist/types/prerender/build-shell-capture.d.ts +104 -0
  917. package/dist/types/prerender/param-hash.d.ts +6 -0
  918. package/dist/types/prerender/shell-manifest-key.d.ts +18 -0
  919. package/dist/types/prerender/store.d.ts +63 -0
  920. package/dist/types/prerender.d.ts +292 -0
  921. package/dist/types/redirect-origin.d.ts +71 -0
  922. package/dist/types/regex-escape.d.ts +6 -0
  923. package/dist/types/render-error-thrower.d.ts +13 -0
  924. package/dist/types/response-utils.d.ts +35 -0
  925. package/dist/types/reverse.d.ts +206 -0
  926. package/dist/types/root-error-boundary.d.ts +32 -0
  927. package/dist/types/route-content-wrapper.d.ts +84 -0
  928. package/dist/types/route-definition/dsl-helpers.d.ts +130 -0
  929. package/dist/types/route-definition/helper-factories.d.ts +22 -0
  930. package/dist/types/route-definition/helpers-types.d.ts +405 -0
  931. package/dist/types/route-definition/index.d.ts +7 -0
  932. package/dist/types/route-definition/redirect.d.ts +48 -0
  933. package/dist/types/route-definition/resolve-handler-use.d.ts +19 -0
  934. package/dist/types/route-definition/use-item-types.d.ts +1 -0
  935. package/dist/types/route-definition.d.ts +1 -0
  936. package/dist/types/route-map-builder.d.ts +102 -0
  937. package/dist/types/route-name.d.ts +27 -0
  938. package/dist/types/route-types.d.ts +172 -0
  939. package/dist/types/router/basename.d.ts +10 -0
  940. package/dist/types/router/content-negotiation.d.ts +91 -0
  941. package/dist/types/router/debug-manifest.d.ts +7 -0
  942. package/dist/types/router/error-handling.d.ts +110 -0
  943. package/dist/types/router/find-match.d.ts +19 -0
  944. package/dist/types/router/handler-context.d.ts +41 -0
  945. package/dist/types/router/instrument.d.ts +191 -0
  946. package/dist/types/router/intercept-resolution.d.ts +91 -0
  947. package/dist/types/router/lazy-includes.d.ts +26 -0
  948. package/dist/types/router/loader-resolution.d.ts +85 -0
  949. package/dist/types/router/logging.d.ts +41 -0
  950. package/dist/types/router/manifest.d.ts +8 -0
  951. package/dist/types/router/match-api.d.ts +19 -0
  952. package/dist/types/router/match-context.d.ts +184 -0
  953. package/dist/types/router/match-handlers.d.ts +48 -0
  954. package/dist/types/router/match-middleware/background-revalidation.d.ts +113 -0
  955. package/dist/types/router/match-middleware/cache-lookup.d.ts +126 -0
  956. package/dist/types/router/match-middleware/cache-store.d.ts +112 -0
  957. package/dist/types/router/match-middleware/index.d.ts +80 -0
  958. package/dist/types/router/match-middleware/intercept-resolution.d.ts +116 -0
  959. package/dist/types/router/match-middleware/segment-resolution.d.ts +94 -0
  960. package/dist/types/router/match-pipelines.d.ts +103 -0
  961. package/dist/types/router/match-result.d.ts +114 -0
  962. package/dist/types/router/metrics.d.ts +6 -0
  963. package/dist/types/router/middleware-types.d.ts +74 -0
  964. package/dist/types/router/middleware.d.ts +116 -0
  965. package/dist/types/router/navigation-snapshot.d.ts +51 -0
  966. package/dist/types/router/params-util.d.ts +8 -0
  967. package/dist/types/router/parse-pattern.d.ts +38 -0
  968. package/dist/types/router/pattern-matching.d.ts +169 -0
  969. package/dist/types/router/prefetch-cache-ttl.d.ts +27 -0
  970. package/dist/types/router/prefetch-default.d.ts +28 -0
  971. package/dist/types/router/prefetch-limits.d.ts +20 -0
  972. package/dist/types/router/prerender-match.d.ts +53 -0
  973. package/dist/types/router/preview-match.d.ts +22 -0
  974. package/dist/types/router/request-classification.d.ts +104 -0
  975. package/dist/types/router/revalidation.d.ts +64 -0
  976. package/dist/types/router/route-snapshot.d.ts +112 -0
  977. package/dist/types/router/route-trie-builder.d.ts +77 -0
  978. package/dist/types/router/router-context.d.ts +137 -0
  979. package/dist/types/router/router-interfaces.d.ts +461 -0
  980. package/dist/types/router/router-options.d.ts +792 -0
  981. package/dist/types/router/router-registry.d.ts +15 -0
  982. package/dist/types/router/segment-resolution/fresh.d.ts +55 -0
  983. package/dist/types/router/segment-resolution/helpers.d.ts +93 -0
  984. package/dist/types/router/segment-resolution/loader-cache.d.ts +33 -0
  985. package/dist/types/router/segment-resolution/loader-mask.d.ts +44 -0
  986. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +90 -0
  987. package/dist/types/router/segment-resolution/mask-nested.d.ts +53 -0
  988. package/dist/types/router/segment-resolution/revalidation.d.ts +85 -0
  989. package/dist/types/router/segment-resolution/static-store.d.ts +17 -0
  990. package/dist/types/router/segment-resolution/streamed-handler-telemetry.d.ts +16 -0
  991. package/dist/types/router/segment-resolution/view-transition-default.d.ts +27 -0
  992. package/dist/types/router/segment-resolution.d.ts +3 -0
  993. package/dist/types/router/segment-wrappers.d.ts +53 -0
  994. package/dist/types/router/state-cookie-name.d.ts +1 -0
  995. package/dist/types/router/substitute-pattern-params.d.ts +23 -0
  996. package/dist/types/router/telemetry-otel.d.ts +113 -0
  997. package/dist/types/router/telemetry.d.ts +216 -0
  998. package/dist/types/router/timeout.d.ts +107 -0
  999. package/dist/types/router/tracing.d.ts +139 -0
  1000. package/dist/types/router/transition-when.d.ts +13 -0
  1001. package/dist/types/router/trie-matching.d.ts +32 -0
  1002. package/dist/types/router/types.d.ts +98 -0
  1003. package/dist/types/router/url-params.d.ts +26 -0
  1004. package/dist/types/router.d.ts +7 -0
  1005. package/dist/types/rsc/capture-queue.d.ts +49 -0
  1006. package/dist/types/rsc/full-payload.d.ts +22 -0
  1007. package/dist/types/rsc/handler-context.d.ts +32 -0
  1008. package/dist/types/rsc/handler.d.ts +9 -0
  1009. package/dist/types/rsc/helpers.d.ts +127 -0
  1010. package/dist/types/rsc/index.d.ts +17 -0
  1011. package/dist/types/rsc/json-route-result.d.ts +20 -0
  1012. package/dist/types/rsc/loader-fetch.d.ts +14 -0
  1013. package/dist/types/rsc/manifest-init.d.ts +18 -0
  1014. package/dist/types/rsc/nonce.d.ts +38 -0
  1015. package/dist/types/rsc/origin-guard.d.ts +50 -0
  1016. package/dist/types/rsc/progressive-enhancement.d.ts +19 -0
  1017. package/dist/types/rsc/redirect-guard.d.ts +37 -0
  1018. package/dist/types/rsc/render-pipeline.d.ts +148 -0
  1019. package/dist/types/rsc/response-cache-serve.d.ts +46 -0
  1020. package/dist/types/rsc/response-error.d.ts +19 -0
  1021. package/dist/types/rsc/response-route-handler.d.ts +29 -0
  1022. package/dist/types/rsc/routine-plan.d.ts +124 -0
  1023. package/dist/types/rsc/rsc-rendering.d.ts +18 -0
  1024. package/dist/types/rsc/runtime-warnings.d.ts +22 -0
  1025. package/dist/types/rsc/server-action.d.ts +68 -0
  1026. package/dist/types/rsc/shell-build-manifest.d.ts +90 -0
  1027. package/dist/types/rsc/shell-capture-constants.d.ts +44 -0
  1028. package/dist/types/rsc/shell-capture.d.ts +425 -0
  1029. package/dist/types/rsc/shell-serve.d.ts +200 -0
  1030. package/dist/types/rsc/ssr-setup.d.ts +73 -0
  1031. package/dist/types/rsc/stream-idle.d.ts +60 -0
  1032. package/dist/types/rsc/transition-gate.d.ts +23 -0
  1033. package/dist/types/rsc/types.d.ts +353 -0
  1034. package/dist/types/runtime-env.d.ts +1 -0
  1035. package/dist/types/search-params.d.ts +125 -0
  1036. package/dist/types/segment-content-promise.d.ts +13 -0
  1037. package/dist/types/segment-fragments.d.ts +79 -0
  1038. package/dist/types/segment-loader-promise.d.ts +22 -0
  1039. package/dist/types/segment-system.d.ts +86 -0
  1040. package/dist/types/serialize.d.ts +164 -0
  1041. package/dist/types/server/context.d.ts +520 -0
  1042. package/dist/types/server/cookie-parse.d.ts +10 -0
  1043. package/dist/types/server/cookie-store.d.ts +107 -0
  1044. package/dist/types/server/fetchable-loader-store.d.ts +20 -0
  1045. package/dist/types/server/handle-store.d.ts +131 -0
  1046. package/dist/types/server/loader-registry.d.ts +32 -0
  1047. package/dist/types/server/request-context.d.ts +717 -0
  1048. package/dist/types/server/root-layout.d.ts +3 -0
  1049. package/dist/types/server.d.ts +16 -0
  1050. package/dist/types/ssr/index.d.ts +271 -0
  1051. package/dist/types/ssr/inject-rsc-eager.d.ts +3 -0
  1052. package/dist/types/ssr/preinit-client-references.d.ts +71 -0
  1053. package/dist/types/ssr/ssr-root.d.ts +84 -0
  1054. package/dist/types/ssr-suspension-warning.d.ts +19 -0
  1055. package/dist/types/static-handler.d.ts +71 -0
  1056. package/dist/types/testing/cache-status.d.ts +63 -0
  1057. package/dist/types/testing/collect-handle.d.ts +20 -0
  1058. package/dist/types/testing/dispatch.d.ts +127 -0
  1059. package/dist/types/testing/dom.entry.d.ts +15 -0
  1060. package/dist/types/testing/e2e/fixture.d.ts +37 -0
  1061. package/dist/types/testing/e2e/index.d.ts +31 -0
  1062. package/dist/types/testing/e2e/matchers.d.ts +17 -0
  1063. package/dist/types/testing/e2e/page-helpers.d.ts +86 -0
  1064. package/dist/types/testing/e2e/parity.d.ts +111 -0
  1065. package/dist/types/testing/e2e/server.d.ts +35 -0
  1066. package/dist/types/testing/flight-matchers.d.ts +55 -0
  1067. package/dist/types/testing/flight-normalize.d.ts +1 -0
  1068. package/dist/types/testing/flight-tree.d.ts +192 -0
  1069. package/dist/types/testing/flight.d.ts +115 -0
  1070. package/dist/types/testing/flight.entry.d.ts +27 -0
  1071. package/dist/types/testing/generated-routes.d.ts +66 -0
  1072. package/dist/types/testing/index.d.ts +54 -0
  1073. package/dist/types/testing/internal/context.d.ts +225 -0
  1074. package/dist/types/testing/internal/flight-client-globals.d.ts +1 -0
  1075. package/dist/types/testing/internal/seed-vars.d.ts +30 -0
  1076. package/dist/types/testing/render-handler.d.ts +160 -0
  1077. package/dist/types/testing/render-route.d.ts +265 -0
  1078. package/dist/types/testing/run-loader.d.ts +195 -0
  1079. package/dist/types/testing/run-middleware.d.ts +132 -0
  1080. package/dist/types/testing/run-transition-when.d.ts +78 -0
  1081. package/dist/types/testing/shell-status.d.ts +81 -0
  1082. package/dist/types/testing/vitest-stubs/cloudflare-email.d.ts +6 -0
  1083. package/dist/types/testing/vitest-stubs/cloudflare-workers.d.ts +13 -0
  1084. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +7 -0
  1085. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  1086. package/dist/types/testing/vitest.d.ts +205 -0
  1087. package/dist/types/theme/ThemeProvider.d.ts +13 -0
  1088. package/dist/types/theme/ThemeScript.d.ts +45 -0
  1089. package/dist/types/theme/constants.d.ts +39 -0
  1090. package/dist/types/theme/index.d.ts +29 -0
  1091. package/dist/types/theme/theme-context.d.ts +21 -0
  1092. package/dist/types/theme/theme-script.d.ts +26 -0
  1093. package/dist/types/theme/types.d.ts +162 -0
  1094. package/dist/types/theme/use-theme.d.ts +8 -0
  1095. package/dist/types/types/boundaries.d.ts +115 -0
  1096. package/dist/types/types/cache-types.d.ts +191 -0
  1097. package/dist/types/types/error-types.d.ts +114 -0
  1098. package/dist/types/types/global-namespace.d.ts +90 -0
  1099. package/dist/types/types/handler-context.d.ts +658 -0
  1100. package/dist/types/types/index.d.ts +11 -0
  1101. package/dist/types/types/loader-types.d.ts +240 -0
  1102. package/dist/types/types/request-scope.d.ts +93 -0
  1103. package/dist/types/types/route-config.d.ts +105 -0
  1104. package/dist/types/types/route-entry.d.ts +98 -0
  1105. package/dist/types/types/segments.d.ts +254 -0
  1106. package/dist/types/types.d.ts +1 -0
  1107. package/dist/types/urls/include-helper.d.ts +17 -0
  1108. package/dist/types/urls/include-provider.d.ts +27 -0
  1109. package/dist/types/urls/index.d.ts +6 -0
  1110. package/dist/types/urls/path-helper-types.d.ts +207 -0
  1111. package/dist/types/urls/path-helper.d.ts +12 -0
  1112. package/dist/types/urls/pattern-types.d.ts +166 -0
  1113. package/dist/types/urls/response-types.d.ts +67 -0
  1114. package/dist/types/urls/type-extraction.d.ts +157 -0
  1115. package/dist/types/urls/urls-function.d.ts +24 -0
  1116. package/dist/types/urls.d.ts +1 -0
  1117. package/dist/types/use-loader.d.ts +150 -0
  1118. package/dist/types/vercel/index.d.ts +10 -0
  1119. package/dist/types/vercel/tracing.d.ts +71 -0
  1120. package/dist/types/vite/debug.d.ts +80 -0
  1121. package/dist/types/vite/discovery/bundle-postprocess.d.ts +12 -0
  1122. package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
  1123. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +65 -0
  1124. package/dist/types/vite/discovery/discover-routers.d.ts +17 -0
  1125. package/dist/types/vite/discovery/discovery-errors.d.ts +113 -0
  1126. package/dist/types/vite/discovery/gate-state.d.ts +79 -0
  1127. package/dist/types/vite/discovery/prerender-collection.d.ts +24 -0
  1128. package/dist/types/vite/discovery/route-types-writer.d.ts +32 -0
  1129. package/dist/types/vite/discovery/self-gen-tracking.d.ts +22 -0
  1130. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +40 -0
  1131. package/dist/types/vite/discovery/state.d.ts +170 -0
  1132. package/dist/types/vite/discovery/virtual-module-codegen.d.ts +15 -0
  1133. package/dist/types/vite/encryption-key.d.ts +2 -0
  1134. package/dist/types/vite/index.d.ts +11 -0
  1135. package/dist/types/vite/inject-client-debug.d.ts +56 -0
  1136. package/dist/types/vite/plugin-types.d.ts +298 -0
  1137. package/dist/types/vite/plugins/cjs-to-esm.d.ts +6 -0
  1138. package/dist/types/vite/plugins/client-ref-dedup.d.ts +29 -0
  1139. package/dist/types/vite/plugins/client-ref-hashing.d.ts +35 -0
  1140. package/dist/types/vite/plugins/cloudflare-protocol-stub.d.ts +64 -0
  1141. package/dist/types/vite/plugins/expose-action-id.d.ts +18 -0
  1142. package/dist/types/vite/plugins/expose-id-utils.d.ts +37 -0
  1143. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +19 -0
  1144. package/dist/types/vite/plugins/expose-ids/handler-transform.d.ts +10 -0
  1145. package/dist/types/vite/plugins/expose-ids/loader-transform.d.ts +8 -0
  1146. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +13 -0
  1147. package/dist/types/vite/plugins/expose-ids/types.d.ts +29 -0
  1148. package/dist/types/vite/plugins/expose-internal-ids.d.ts +16 -0
  1149. package/dist/types/vite/plugins/performance-tracks.d.ts +25 -0
  1150. package/dist/types/vite/plugins/refresh-cmd.d.ts +20 -0
  1151. package/dist/types/vite/plugins/server-ref-hashing.d.ts +24 -0
  1152. package/dist/types/vite/plugins/server-reference-pattern.d.ts +1 -0
  1153. package/dist/types/vite/plugins/use-cache-transform.d.ts +20 -0
  1154. package/dist/types/vite/plugins/vercel-output.d.ts +85 -0
  1155. package/dist/types/vite/plugins/version-injector.d.ts +21 -0
  1156. package/dist/types/vite/plugins/version-plugin.d.ts +19 -0
  1157. package/dist/types/vite/plugins/virtual-entries.d.ts +36 -0
  1158. package/dist/types/vite/plugins/virtual-stub-plugin.d.ts +7 -0
  1159. package/dist/types/vite/rango.d.ts +29 -0
  1160. package/dist/types/vite/router-discovery.d.ts +23 -0
  1161. package/dist/types/vite/utils/ast-handler-extract.d.ts +64 -0
  1162. package/dist/types/vite/utils/banner.d.ts +2 -0
  1163. package/dist/types/vite/utils/bundle-analysis.d.ts +28 -0
  1164. package/dist/types/vite/utils/client-chunks.d.ts +55 -0
  1165. package/dist/types/vite/utils/directive-prologue.d.ts +16 -0
  1166. package/dist/types/vite/utils/forward-user-plugins.d.ts +37 -0
  1167. package/dist/types/vite/utils/manifest-utils.d.ts +7 -0
  1168. package/dist/types/vite/utils/package-resolution.d.ts +6 -0
  1169. package/dist/types/vite/utils/prerender-utils.d.ts +32 -0
  1170. package/dist/types/vite/utils/shared-utils.d.ts +67 -0
  1171. package/dist/types.d.ts +1479 -0
  1172. package/dist/types.d.ts.map +1 -0
  1173. package/dist/types.js +10 -0
  1174. package/dist/types.js.map +1 -0
  1175. package/dist/urls.d.ts +441 -0
  1176. package/dist/urls.d.ts.map +1 -0
  1177. package/dist/urls.gen.d.ts +8 -0
  1178. package/dist/urls.gen.d.ts.map +1 -0
  1179. package/dist/urls.gen.js +8 -0
  1180. package/dist/urls.gen.js.map +1 -0
  1181. package/dist/urls.js +443 -0
  1182. package/dist/urls.js.map +1 -0
  1183. package/dist/use-loader.d.ts +127 -0
  1184. package/dist/use-loader.d.ts.map +1 -0
  1185. package/dist/use-loader.js +237 -0
  1186. package/dist/use-loader.js.map +1 -0
  1187. package/dist/vite/__tests__/ast-handler-extract.test.d.ts +2 -0
  1188. package/dist/vite/__tests__/ast-handler-extract.test.d.ts.map +1 -0
  1189. package/dist/vite/__tests__/ast-handler-extract.test.js +294 -0
  1190. package/dist/vite/__tests__/ast-handler-extract.test.js.map +1 -0
  1191. package/dist/vite/__tests__/expose-id-utils.test.d.ts +2 -0
  1192. package/dist/vite/__tests__/expose-id-utils.test.d.ts.map +1 -0
  1193. package/dist/vite/__tests__/expose-id-utils.test.js +224 -0
  1194. package/dist/vite/__tests__/expose-id-utils.test.js.map +1 -0
  1195. package/dist/vite/__tests__/expose-internal-ids.test.d.ts +2 -0
  1196. package/dist/vite/__tests__/expose-internal-ids.test.d.ts.map +1 -0
  1197. package/dist/vite/__tests__/expose-internal-ids.test.js +647 -0
  1198. package/dist/vite/__tests__/expose-internal-ids.test.js.map +1 -0
  1199. package/dist/vite/__tests__/expose-router-id.test.d.ts +2 -0
  1200. package/dist/vite/__tests__/expose-router-id.test.d.ts.map +1 -0
  1201. package/dist/vite/__tests__/expose-router-id.test.js +39 -0
  1202. package/dist/vite/__tests__/expose-router-id.test.js.map +1 -0
  1203. package/dist/vite/ast-handler-extract.d.ts +49 -0
  1204. package/dist/vite/ast-handler-extract.d.ts.map +1 -0
  1205. package/dist/vite/ast-handler-extract.js +249 -0
  1206. package/dist/vite/ast-handler-extract.js.map +1 -0
  1207. package/dist/vite/expose-action-id.d.ts +19 -0
  1208. package/dist/vite/expose-action-id.d.ts.map +1 -0
  1209. package/dist/vite/expose-action-id.js +250 -0
  1210. package/dist/vite/expose-action-id.js.map +1 -0
  1211. package/dist/vite/expose-id-utils.d.ts +69 -0
  1212. package/dist/vite/expose-id-utils.d.ts.map +1 -0
  1213. package/dist/vite/expose-id-utils.js +289 -0
  1214. package/dist/vite/expose-id-utils.js.map +1 -0
  1215. package/dist/vite/expose-internal-ids.d.ts +22 -0
  1216. package/dist/vite/expose-internal-ids.d.ts.map +1 -0
  1217. package/dist/vite/expose-internal-ids.js +886 -0
  1218. package/dist/vite/expose-internal-ids.js.map +1 -0
  1219. package/dist/vite/index.d.ts +149 -0
  1220. package/dist/vite/index.d.ts.map +1 -0
  1221. package/dist/vite/index.js +12042 -1346
  1222. package/dist/vite/index.js.bak +5448 -0
  1223. package/dist/vite/index.js.map +1 -0
  1224. package/dist/vite/index.named-routes.gen.ts +103 -0
  1225. package/dist/vite/package-resolution.d.ts +43 -0
  1226. package/dist/vite/package-resolution.d.ts.map +1 -0
  1227. package/{src/vite/package-resolution.ts → dist/vite/package-resolution.js} +53 -66
  1228. package/dist/vite/package-resolution.js.map +1 -0
  1229. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  1230. package/dist/vite/virtual-entries.d.ts +25 -0
  1231. package/dist/vite/virtual-entries.d.ts.map +1 -0
  1232. package/{src/vite/virtual-entries.ts → dist/vite/virtual-entries.js} +12 -16
  1233. package/dist/vite/virtual-entries.js.map +1 -0
  1234. package/package.json +170 -77
  1235. package/skills/api-client/SKILL.md +211 -0
  1236. package/skills/breadcrumbs/SKILL.md +359 -0
  1237. package/skills/bundle-analysis/SKILL.md +159 -0
  1238. package/skills/cache-guide/SKILL.md +492 -0
  1239. package/skills/caching/SKILL.md +452 -26
  1240. package/skills/catalog.json +289 -0
  1241. package/skills/client-urls/SKILL.md +339 -0
  1242. package/skills/cloudflare/SKILL.md +153 -0
  1243. package/skills/cloudflare/agents/openai.yaml +4 -0
  1244. package/skills/cloudflare/references/d1-and-local-dev.md +131 -0
  1245. package/skills/cloudflare/references/streaming-and-deploy.md +107 -0
  1246. package/skills/cloudflare/references/webhooks-and-crypto.md +147 -0
  1247. package/skills/comparison/SKILL.md +50 -0
  1248. package/skills/comparison/agents/openai.yaml +4 -0
  1249. package/skills/comparison/references/framework-comparison.md +858 -0
  1250. package/skills/composability/SKILL.md +292 -0
  1251. package/skills/css/SKILL.md +76 -0
  1252. package/skills/debug-manifest/SKILL.md +16 -18
  1253. package/skills/defer-hydration/SKILL.md +235 -0
  1254. package/skills/deployment-caching/SKILL.md +176 -0
  1255. package/skills/document-cache/SKILL.md +125 -65
  1256. package/skills/fonts/SKILL.md +167 -0
  1257. package/skills/handler-use/SKILL.md +364 -0
  1258. package/skills/hooks/SKILL.md +76 -432
  1259. package/skills/hooks/data.md +282 -0
  1260. package/skills/hooks/handle-and-actions.md +110 -0
  1261. package/skills/hooks/navigation.md +110 -0
  1262. package/skills/hooks/outlets.md +62 -0
  1263. package/skills/hooks/state.md +228 -0
  1264. package/skills/hooks/urls.md +172 -0
  1265. package/skills/host-router/SKILL.md +320 -0
  1266. package/skills/i18n/SKILL.md +276 -0
  1267. package/skills/intercept/SKILL.md +215 -16
  1268. package/skills/layout/SKILL.md +147 -7
  1269. package/skills/links/SKILL.md +360 -33
  1270. package/skills/loader/SKILL.md +762 -51
  1271. package/skills/middleware/SKILL.md +251 -38
  1272. package/skills/migrate-nextjs/SKILL.md +854 -0
  1273. package/skills/migrate-nextjs/backend-host-swap.md +120 -0
  1274. package/skills/migrate-react-router/SKILL.md +189 -0
  1275. package/skills/migrate-react-router/component-migration.md +201 -0
  1276. package/skills/migrate-react-router/data-and-actions.md +236 -0
  1277. package/skills/migrate-react-router/route-mapping.md +284 -0
  1278. package/skills/mime-routes/SKILL.md +165 -0
  1279. package/skills/observability/SKILL.md +231 -0
  1280. package/skills/parallel/SKILL.md +327 -4
  1281. package/skills/ppr/SKILL.md +918 -0
  1282. package/skills/prerender/SKILL.md +751 -0
  1283. package/skills/rango/SKILL.md +387 -17
  1284. package/skills/react-compiler/SKILL.md +168 -0
  1285. package/skills/response-routes/SKILL.md +539 -0
  1286. package/skills/route/SKILL.md +399 -15
  1287. package/skills/router-setup/SKILL.md +270 -34
  1288. package/skills/scripts/SKILL.md +189 -0
  1289. package/skills/server-actions/SKILL.md +776 -0
  1290. package/skills/shell-manifest/SKILL.md +194 -0
  1291. package/skills/streams-and-websockets/SKILL.md +283 -0
  1292. package/skills/tailwind/SKILL.md +153 -0
  1293. package/skills/testing/SKILL.md +131 -0
  1294. package/skills/testing/bindings.md +103 -0
  1295. package/skills/testing/cache-prerender.md +172 -0
  1296. package/skills/testing/client-components.md +131 -0
  1297. package/skills/testing/e2e-parity.md +125 -0
  1298. package/skills/testing/flight.md +91 -0
  1299. package/skills/testing/handles.md +153 -0
  1300. package/skills/testing/loader.md +130 -0
  1301. package/skills/testing/middleware.md +99 -0
  1302. package/skills/testing/render-handler.md +122 -0
  1303. package/skills/testing/response-routes.md +95 -0
  1304. package/skills/testing/reverse-and-types.md +85 -0
  1305. package/skills/testing/server-actions.md +107 -0
  1306. package/skills/testing/server-tree.md +128 -0
  1307. package/skills/testing/setup.md +123 -0
  1308. package/skills/theme/SKILL.md +16 -12
  1309. package/skills/typesafety/SKILL.md +45 -387
  1310. package/skills/typesafety/env-and-bindings.md +254 -0
  1311. package/skills/typesafety/generated-files-and-cli.md +342 -0
  1312. package/skills/typesafety/params-and-search.md +153 -0
  1313. package/skills/typesafety/route-types.md +213 -0
  1314. package/skills/use-cache/SKILL.md +430 -0
  1315. package/skills/vercel/SKILL.md +149 -0
  1316. package/skills/view-transitions/SKILL.md +341 -0
  1317. package/src/__augment-tests__/augment.ts +81 -0
  1318. package/src/__augment-tests__/augmented.check.ts +116 -0
  1319. package/src/__internal.ts +77 -44
  1320. package/src/bin/rango.ts +325 -0
  1321. package/src/browser/action-coordinator.ts +114 -0
  1322. package/src/browser/action-fence.ts +47 -0
  1323. package/src/browser/app-shell.ts +39 -0
  1324. package/src/browser/app-version.ts +14 -0
  1325. package/src/browser/connection-warmup.ts +134 -0
  1326. package/src/browser/cookie-name.ts +140 -0
  1327. package/src/browser/dev-discovery.ts +66 -0
  1328. package/src/browser/event-controller.ts +477 -217
  1329. package/src/browser/history-state.ts +101 -0
  1330. package/src/browser/index.ts +3 -3
  1331. package/src/browser/intercept-utils.ts +52 -0
  1332. package/src/browser/invalidate-client-cache.ts +52 -0
  1333. package/src/browser/link-interceptor.ts +487 -18
  1334. package/src/browser/logging.ts +83 -0
  1335. package/src/browser/merge-segment-loaders.ts +23 -13
  1336. package/src/browser/navigation-bridge.ts +471 -570
  1337. package/src/browser/navigation-client.ts +343 -79
  1338. package/src/browser/navigation-store-handle.ts +38 -0
  1339. package/src/browser/navigation-store.ts +228 -366
  1340. package/src/browser/navigation-transaction.ts +247 -0
  1341. package/src/browser/network-error-handler.ts +88 -0
  1342. package/src/browser/notify-listeners.ts +22 -0
  1343. package/src/browser/partial-update.ts +491 -365
  1344. package/src/browser/prefetch/cache.ts +457 -0
  1345. package/src/browser/prefetch/default-strategy.ts +74 -0
  1346. package/src/browser/prefetch/fetch.ts +548 -0
  1347. package/src/browser/prefetch/invalidation.ts +30 -0
  1348. package/src/browser/prefetch/loader.ts +111 -0
  1349. package/src/browser/prefetch/observer.ts +93 -0
  1350. package/src/browser/prefetch/policy.ts +48 -0
  1351. package/src/browser/prefetch/queue.ts +209 -0
  1352. package/src/browser/prefetch/resource-ready.ts +77 -0
  1353. package/src/browser/prefetch/runtime.ts +6 -0
  1354. package/src/browser/rango-state.ts +215 -0
  1355. package/src/browser/react/Link.tsx +332 -85
  1356. package/src/browser/react/NavigationProvider.tsx +275 -109
  1357. package/src/browser/react/ScrollRestoration.tsx +10 -6
  1358. package/src/browser/react/context.ts +15 -0
  1359. package/src/browser/react/filter-segment-order.ts +70 -0
  1360. package/src/browser/react/index.ts +0 -48
  1361. package/src/browser/react/location-state-shared.ts +272 -60
  1362. package/src/browser/react/location-state.ts +90 -20
  1363. package/src/browser/react/mount-context.ts +6 -1
  1364. package/src/browser/react/nonce-context.ts +23 -0
  1365. package/src/browser/react/shallow-equal.ts +27 -0
  1366. package/src/browser/react/use-action.ts +35 -66
  1367. package/src/browser/react/use-handle.ts +39 -132
  1368. package/src/browser/react/use-href.tsx +9 -2
  1369. package/src/browser/react/use-link-status.ts +39 -13
  1370. package/src/browser/react/use-navigation.ts +53 -69
  1371. package/src/browser/react/use-params.ts +75 -0
  1372. package/src/browser/react/use-pathname.ts +47 -0
  1373. package/src/browser/react/use-reverse.ts +106 -0
  1374. package/src/browser/react/use-router.ts +127 -0
  1375. package/src/browser/react/use-search-params.ts +174 -0
  1376. package/src/browser/react/use-segments.ts +72 -99
  1377. package/src/browser/response-adapter.ts +164 -0
  1378. package/src/browser/rsc-router.tsx +446 -74
  1379. package/src/browser/scroll-restoration.ts +140 -50
  1380. package/src/browser/segment-reconciler.ts +253 -0
  1381. package/src/browser/segment-structure-assert.ts +17 -1
  1382. package/src/browser/server-action-bridge.ts +707 -603
  1383. package/src/browser/types.ts +266 -75
  1384. package/src/browser/validate-redirect-origin.ts +56 -0
  1385. package/src/build/collect-fallback-refs.ts +107 -0
  1386. package/src/build/generate-manifest.ts +324 -84
  1387. package/src/build/generate-route-types.ts +41 -0
  1388. package/src/build/index.ts +22 -1
  1389. package/src/build/merge-full-manifests.ts +161 -0
  1390. package/src/build/prefix-tree-utils.ts +123 -0
  1391. package/src/build/route-trie.ts +54 -0
  1392. package/src/build/route-types/ast-helpers.ts +25 -0
  1393. package/src/build/route-types/ast-route-extraction.ts +105 -0
  1394. package/src/build/route-types/codegen.ts +113 -0
  1395. package/src/build/route-types/include-resolution.ts +867 -0
  1396. package/src/build/route-types/param-extraction.ts +51 -0
  1397. package/src/build/route-types/per-module-writer.ts +149 -0
  1398. package/src/build/route-types/router-processing.ts +726 -0
  1399. package/src/build/route-types/scan-filter.ts +85 -0
  1400. package/src/build/route-types/source-scan.ts +216 -0
  1401. package/src/build/runtime-discovery.ts +234 -0
  1402. package/src/cache/background-task.ts +34 -0
  1403. package/src/cache/cache-error.ts +104 -0
  1404. package/src/cache/cache-exec-scope.ts +47 -0
  1405. package/src/cache/cache-key-utils.ts +102 -0
  1406. package/src/cache/cache-policy.ts +199 -0
  1407. package/src/cache/cache-runtime.ts +787 -0
  1408. package/src/cache/cache-scope.ts +478 -342
  1409. package/src/cache/cache-tag.ts +149 -0
  1410. package/src/cache/cf/cf-base64.ts +33 -0
  1411. package/src/cache/cf/cf-cache-constants.ts +134 -0
  1412. package/src/cache/cf/cf-cache-store.ts +3397 -152
  1413. package/src/cache/cf/cf-cache-types.ts +417 -0
  1414. package/src/cache/cf/cf-kv-utils.ts +84 -0
  1415. package/src/cache/cf/cf-tag-marker-memo.ts +108 -0
  1416. package/src/cache/cf/cf-zone-purge.ts +101 -0
  1417. package/src/cache/cf/index.ts +21 -16
  1418. package/src/cache/document-cache.ts +275 -105
  1419. package/src/cache/handle-capture.ts +81 -0
  1420. package/src/cache/handle-snapshot.ts +132 -0
  1421. package/src/cache/index.ts +33 -35
  1422. package/src/cache/memory-segment-store.ts +457 -30
  1423. package/src/cache/profile-registry.ts +88 -0
  1424. package/src/cache/read-through-swr.ts +184 -0
  1425. package/src/cache/search-params-filter.ts +118 -0
  1426. package/src/cache/segment-codec.ts +311 -0
  1427. package/src/cache/shell-snapshot.ts +516 -0
  1428. package/src/cache/tag-invalidation.ts +230 -0
  1429. package/src/cache/taint.ts +172 -0
  1430. package/src/cache/types.ts +405 -212
  1431. package/src/cache/vercel/index.ts +11 -0
  1432. package/src/cache/vercel/vercel-cache-store.ts +1264 -0
  1433. package/src/client-urls/client-root.tsx +168 -0
  1434. package/src/client-urls/client-urls.ts +779 -0
  1435. package/src/client-urls/navigation.ts +237 -0
  1436. package/src/client-urls/revalidation-protocol.ts +56 -0
  1437. package/src/client-urls/server-projection.ts +670 -0
  1438. package/src/client-urls/types.ts +202 -0
  1439. package/src/client.rsc.tsx +55 -21
  1440. package/src/client.tsx +179 -330
  1441. package/src/cloudflare/index.ts +11 -0
  1442. package/src/cloudflare/tracing.ts +112 -0
  1443. package/src/component-utils.ts +23 -4
  1444. package/src/components/DefaultDocument.tsx +13 -3
  1445. package/src/context-var.ts +168 -0
  1446. package/src/debug.ts +19 -9
  1447. package/src/decode-loader-results.ts +165 -0
  1448. package/src/defer.ts +185 -0
  1449. package/src/deps/ssr.ts +4 -2
  1450. package/src/dev-discovery-protocol.ts +11 -0
  1451. package/src/encode-kv.ts +49 -0
  1452. package/src/errors.ts +149 -3
  1453. package/src/escape-script.ts +52 -0
  1454. package/src/handle.ts +111 -55
  1455. package/src/handles/MetaTags.tsx +83 -59
  1456. package/src/handles/Scripts.tsx +183 -0
  1457. package/src/handles/breadcrumbs.ts +93 -0
  1458. package/src/handles/deferred-resolution.ts +134 -0
  1459. package/src/handles/is-thenable.ts +16 -0
  1460. package/src/handles/meta.ts +44 -53
  1461. package/src/handles/script.ts +244 -0
  1462. package/src/host/cookie-handler.ts +20 -65
  1463. package/src/host/errors.ts +21 -30
  1464. package/src/host/index.ts +13 -9
  1465. package/src/host/pattern-matcher.ts +50 -79
  1466. package/src/host/router.ts +151 -121
  1467. package/src/host/testing.ts +45 -32
  1468. package/src/host/types.ts +52 -11
  1469. package/src/host/utils.ts +2 -2
  1470. package/src/href-client.ts +236 -31
  1471. package/src/index.rsc.ts +252 -20
  1472. package/src/index.ts +343 -30
  1473. package/src/internal-debug.ts +12 -0
  1474. package/src/loader-redirect.tsx +64 -0
  1475. package/src/loader-store.ts +500 -0
  1476. package/src/loader.rsc.ts +39 -163
  1477. package/src/loader.ts +31 -13
  1478. package/src/missing-id-error.ts +68 -0
  1479. package/src/outlet-context.ts +21 -1
  1480. package/src/outlet-provider.tsx +59 -0
  1481. package/src/prerender/build-shell-capture.ts +453 -0
  1482. package/src/prerender/param-hash.ts +37 -0
  1483. package/src/prerender/shell-manifest-key.ts +20 -0
  1484. package/src/prerender/store.ts +195 -0
  1485. package/src/prerender.ts +592 -0
  1486. package/src/redirect-origin.ts +172 -0
  1487. package/src/regex-escape.ts +8 -0
  1488. package/src/render-error-thrower.tsx +20 -0
  1489. package/src/response-utils.ts +62 -0
  1490. package/src/reverse.ts +380 -0
  1491. package/src/root-error-boundary.tsx +42 -48
  1492. package/src/route-content-wrapper.tsx +123 -78
  1493. package/src/route-definition/dsl-helpers.ts +1150 -0
  1494. package/src/route-definition/helper-factories.ts +88 -0
  1495. package/src/route-definition/helpers-types.ts +522 -0
  1496. package/src/route-definition/index.ts +54 -0
  1497. package/src/route-definition/redirect.ts +133 -0
  1498. package/src/route-definition/resolve-handler-use.ts +160 -0
  1499. package/src/route-definition/use-item-types.ts +29 -0
  1500. package/src/route-definition.ts +1 -1428
  1501. package/src/route-map-builder.ts +249 -127
  1502. package/src/route-name.ts +53 -0
  1503. package/src/route-types.ts +87 -45
  1504. package/src/router/basename.ts +14 -0
  1505. package/src/router/content-negotiation.ts +305 -0
  1506. package/src/router/debug-manifest.ts +72 -0
  1507. package/src/router/error-handling.ts +119 -38
  1508. package/src/router/find-match.ts +259 -0
  1509. package/src/router/handler-context.ts +439 -86
  1510. package/src/router/instrument.ts +401 -0
  1511. package/src/router/intercept-resolution.ts +479 -0
  1512. package/src/router/lazy-includes.ts +261 -0
  1513. package/src/router/loader-resolution.ts +714 -142
  1514. package/src/router/logging.ts +245 -0
  1515. package/src/router/manifest.ts +225 -63
  1516. package/src/router/match-api.ts +591 -0
  1517. package/src/router/match-context.ts +5 -25
  1518. package/src/router/match-handlers.ts +537 -0
  1519. package/src/router/match-middleware/background-revalidation.ts +128 -97
  1520. package/src/router/match-middleware/cache-lookup.ts +537 -73
  1521. package/src/router/match-middleware/cache-store.ts +214 -60
  1522. package/src/router/match-middleware/intercept-resolution.ts +56 -38
  1523. package/src/router/match-middleware/segment-resolution.ts +71 -19
  1524. package/src/router/match-pipelines.ts +11 -87
  1525. package/src/router/match-result.ts +150 -53
  1526. package/src/router/metrics.ts +235 -29
  1527. package/src/router/middleware-types.ts +110 -0
  1528. package/src/router/middleware.ts +526 -441
  1529. package/src/router/navigation-snapshot.ts +173 -0
  1530. package/src/router/params-util.ts +23 -0
  1531. package/src/router/parse-pattern.ts +115 -0
  1532. package/src/router/pattern-matching.ts +348 -149
  1533. package/src/router/prefetch-cache-ttl.ts +51 -0
  1534. package/src/router/prefetch-default.ts +59 -0
  1535. package/src/router/prefetch-limits.ts +37 -0
  1536. package/src/router/prerender-match.ts +592 -0
  1537. package/src/router/preview-match.ts +102 -0
  1538. package/src/router/request-classification.ts +291 -0
  1539. package/src/router/revalidation.ts +252 -90
  1540. package/src/router/route-snapshot.ts +256 -0
  1541. package/src/router/route-trie-builder.ts +334 -0
  1542. package/src/router/router-context.ts +45 -48
  1543. package/src/router/router-interfaces.ts +614 -0
  1544. package/src/router/router-options.ts +842 -0
  1545. package/src/router/router-registry.ts +21 -0
  1546. package/src/router/segment-resolution/fresh.ts +879 -0
  1547. package/src/router/segment-resolution/helpers.ts +346 -0
  1548. package/src/router/segment-resolution/loader-cache.ts +368 -0
  1549. package/src/router/segment-resolution/loader-mask.ts +60 -0
  1550. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  1551. package/src/router/segment-resolution/mask-nested.ts +99 -0
  1552. package/src/router/segment-resolution/revalidation.ts +1376 -0
  1553. package/src/router/segment-resolution/static-store.ts +107 -0
  1554. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  1555. package/src/router/segment-resolution/view-transition-default.ts +60 -0
  1556. package/src/router/segment-resolution.ts +25 -0
  1557. package/src/router/segment-wrappers.ts +292 -0
  1558. package/src/router/state-cookie-name.ts +33 -0
  1559. package/src/router/substitute-pattern-params.ts +75 -0
  1560. package/src/router/telemetry-otel.ts +270 -0
  1561. package/src/router/telemetry.ts +386 -0
  1562. package/src/router/timeout.ts +179 -0
  1563. package/src/router/tracing.ts +235 -0
  1564. package/src/router/transition-when.ts +76 -0
  1565. package/src/router/trie-matching.ts +351 -0
  1566. package/src/router/types.ts +60 -39
  1567. package/src/router/url-params.ts +57 -0
  1568. package/src/router.ts +911 -4253
  1569. package/src/rsc/capture-queue.ts +218 -0
  1570. package/src/rsc/full-payload.ts +77 -0
  1571. package/src/rsc/handler-context.ts +47 -0
  1572. package/src/rsc/handler.ts +1226 -834
  1573. package/src/rsc/helpers.ts +351 -19
  1574. package/src/rsc/index.ts +2 -25
  1575. package/src/rsc/json-route-result.ts +38 -0
  1576. package/src/rsc/loader-fetch.ts +305 -0
  1577. package/src/rsc/manifest-init.ts +89 -0
  1578. package/src/rsc/nonce.ts +39 -0
  1579. package/src/rsc/origin-guard.ts +155 -0
  1580. package/src/rsc/progressive-enhancement.ts +686 -0
  1581. package/src/rsc/redirect-guard.ts +102 -0
  1582. package/src/rsc/render-pipeline.ts +611 -0
  1583. package/src/rsc/response-cache-serve.ts +259 -0
  1584. package/src/rsc/response-error.ts +104 -0
  1585. package/src/rsc/response-route-handler.ts +221 -0
  1586. package/src/rsc/routine-plan.ts +359 -0
  1587. package/src/rsc/rsc-rendering.ts +1729 -0
  1588. package/src/rsc/runtime-warnings.ts +55 -0
  1589. package/src/rsc/server-action.ts +639 -0
  1590. package/src/rsc/shell-build-manifest.ts +343 -0
  1591. package/src/rsc/shell-capture-constants.ts +45 -0
  1592. package/src/rsc/shell-capture.ts +2135 -0
  1593. package/src/rsc/shell-serve.ts +350 -0
  1594. package/src/rsc/ssr-setup.ts +236 -0
  1595. package/src/rsc/stream-idle.ts +137 -0
  1596. package/src/rsc/transition-gate.ts +86 -0
  1597. package/src/rsc/types.ts +167 -14
  1598. package/src/runtime-env.ts +18 -0
  1599. package/src/search-params.ts +235 -0
  1600. package/src/segment-content-promise.ts +67 -0
  1601. package/src/segment-fragments.ts +165 -0
  1602. package/src/segment-loader-promise.ts +167 -0
  1603. package/src/segment-system.tsx +544 -136
  1604. package/src/serialize.ts +243 -0
  1605. package/src/server/context.ts +741 -90
  1606. package/src/server/cookie-parse.ts +32 -0
  1607. package/src/server/cookie-store.ts +337 -0
  1608. package/src/server/fetchable-loader-store.ts +37 -0
  1609. package/src/server/handle-store.ts +278 -60
  1610. package/src/server/loader-registry.ts +51 -100
  1611. package/src/server/request-context.ts +1296 -161
  1612. package/src/server.ts +38 -128
  1613. package/src/ssr/index.tsx +716 -140
  1614. package/src/ssr/inject-rsc-eager.ts +167 -0
  1615. package/src/ssr/preinit-client-references.ts +106 -0
  1616. package/src/ssr/ssr-root.tsx +305 -0
  1617. package/src/ssr-suspension-warning.ts +67 -0
  1618. package/src/static-handler.ts +139 -0
  1619. package/src/testing/cache-status.ts +162 -0
  1620. package/src/testing/collect-handle.ts +46 -0
  1621. package/src/testing/dispatch.ts +951 -0
  1622. package/src/testing/dom.entry.ts +22 -0
  1623. package/src/testing/e2e/fixture.ts +188 -0
  1624. package/src/testing/e2e/index.ts +147 -0
  1625. package/src/testing/e2e/matchers.ts +35 -0
  1626. package/src/testing/e2e/page-helpers.ts +319 -0
  1627. package/src/testing/e2e/parity.ts +400 -0
  1628. package/src/testing/e2e/server.ts +195 -0
  1629. package/src/testing/flight-matchers.ts +97 -0
  1630. package/src/testing/flight-normalize.ts +11 -0
  1631. package/src/testing/flight-runtime.d.ts +57 -0
  1632. package/src/testing/flight-tree.ts +682 -0
  1633. package/src/testing/flight.entry.ts +52 -0
  1634. package/src/testing/flight.ts +257 -0
  1635. package/src/testing/generated-routes.ts +199 -0
  1636. package/src/testing/index.ts +121 -0
  1637. package/src/testing/internal/context.ts +380 -0
  1638. package/src/testing/internal/flight-client-globals.ts +30 -0
  1639. package/src/testing/internal/seed-vars.ts +54 -0
  1640. package/src/testing/render-handler.ts +371 -0
  1641. package/src/testing/render-route.tsx +667 -0
  1642. package/src/testing/run-loader.ts +423 -0
  1643. package/src/testing/run-middleware.ts +219 -0
  1644. package/src/testing/run-transition-when.ts +197 -0
  1645. package/src/testing/shell-status.ts +206 -0
  1646. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  1647. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  1648. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  1649. package/src/testing/vitest-stubs/version.ts +5 -0
  1650. package/src/testing/vitest.ts +305 -0
  1651. package/src/theme/ThemeProvider.tsx +76 -98
  1652. package/src/theme/ThemeScript.tsx +12 -14
  1653. package/src/theme/constants.ts +57 -15
  1654. package/src/theme/index.ts +3 -20
  1655. package/src/theme/theme-context.ts +5 -35
  1656. package/src/theme/theme-script.ts +43 -39
  1657. package/src/theme/use-theme.ts +0 -3
  1658. package/src/types/boundaries.ts +142 -0
  1659. package/src/types/cache-types.ts +207 -0
  1660. package/src/types/error-types.ts +132 -0
  1661. package/src/types/global-namespace.ts +113 -0
  1662. package/src/types/handler-context.ts +850 -0
  1663. package/src/types/index.ts +82 -0
  1664. package/src/types/loader-types.ts +273 -0
  1665. package/src/types/request-scope.ts +112 -0
  1666. package/src/types/route-config.ts +138 -0
  1667. package/src/types/route-entry.ts +117 -0
  1668. package/src/types/segments.ts +291 -0
  1669. package/src/types.ts +1 -1623
  1670. package/src/urls/include-helper.ts +181 -0
  1671. package/src/urls/include-provider.ts +71 -0
  1672. package/src/urls/index.ts +44 -0
  1673. package/src/urls/path-helper-types.ts +435 -0
  1674. package/src/urls/path-helper.ts +284 -0
  1675. package/src/urls/pattern-types.ts +189 -0
  1676. package/src/urls/response-types.ts +109 -0
  1677. package/src/urls/type-extraction.ts +316 -0
  1678. package/src/urls/urls-function.ts +80 -0
  1679. package/src/urls.ts +1 -802
  1680. package/src/use-loader.tsx +487 -138
  1681. package/src/vercel/index.ts +11 -0
  1682. package/src/vercel/tracing.ts +89 -0
  1683. package/src/vite/debug.ts +185 -0
  1684. package/src/vite/discovery/bundle-postprocess.ts +185 -0
  1685. package/src/vite/discovery/client-urls-projection.ts +322 -0
  1686. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  1687. package/src/vite/discovery/discover-routers.ts +375 -0
  1688. package/src/vite/discovery/discovery-errors.ts +255 -0
  1689. package/src/vite/discovery/gate-state.ts +171 -0
  1690. package/src/vite/discovery/prerender-collection.ts +494 -0
  1691. package/src/vite/discovery/route-types-writer.ts +214 -0
  1692. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  1693. package/src/vite/discovery/shell-prerender-phase.ts +397 -0
  1694. package/src/vite/discovery/state.ts +215 -0
  1695. package/src/vite/discovery/virtual-module-codegen.ts +342 -0
  1696. package/src/vite/encryption-key.ts +29 -0
  1697. package/src/vite/index.ts +31 -1137
  1698. package/src/vite/inject-client-debug.ts +88 -0
  1699. package/src/vite/plugin-types.ts +336 -0
  1700. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  1701. package/src/vite/plugins/client-ref-dedup.ts +382 -0
  1702. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  1703. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  1704. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  1705. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  1706. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +130 -136
  1707. package/src/vite/plugins/expose-id-utils.ts +332 -0
  1708. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  1709. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  1710. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  1711. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  1712. package/src/vite/plugins/expose-ids/types.ts +45 -0
  1713. package/src/vite/plugins/expose-internal-ids.ts +851 -0
  1714. package/src/vite/plugins/performance-tracks.ts +89 -0
  1715. package/src/vite/plugins/refresh-cmd.ts +127 -0
  1716. package/src/vite/plugins/server-ref-hashing.ts +74 -0
  1717. package/src/vite/plugins/server-reference-pattern.ts +10 -0
  1718. package/src/vite/plugins/use-cache-transform.ts +313 -0
  1719. package/src/vite/plugins/vercel-output.ts +384 -0
  1720. package/src/vite/plugins/version-injector.ts +93 -0
  1721. package/src/vite/plugins/version-plugin.ts +271 -0
  1722. package/src/vite/plugins/virtual-entries.ts +286 -0
  1723. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  1724. package/src/vite/rango.ts +609 -0
  1725. package/src/vite/router-discovery.ts +2400 -0
  1726. package/src/vite/utils/ast-handler-extract.ts +508 -0
  1727. package/src/vite/utils/banner.ts +36 -0
  1728. package/src/vite/utils/bundle-analysis.ts +132 -0
  1729. package/src/vite/utils/client-chunks.ts +184 -0
  1730. package/src/vite/utils/directive-prologue.ts +40 -0
  1731. package/src/vite/utils/forward-user-plugins.ts +171 -0
  1732. package/src/vite/utils/manifest-utils.ts +12 -0
  1733. package/src/vite/utils/package-resolution.ts +90 -0
  1734. package/src/vite/utils/prerender-utils.ts +268 -0
  1735. package/src/vite/utils/shared-utils.ts +283 -0
  1736. package/CLAUDE.md +0 -43
  1737. package/src/browser/lru-cache.ts +0 -69
  1738. package/src/browser/request-controller.ts +0 -164
  1739. package/src/browser/shallow.ts +0 -35
  1740. package/src/cache/memory-store.ts +0 -253
  1741. package/src/href.ts +0 -255
  1742. package/src/server/route-manifest-cache.ts +0 -173
  1743. package/src/vite/expose-handle-id.ts +0 -209
  1744. package/src/vite/expose-loader-id.ts +0 -426
  1745. package/src/vite/expose-location-state-id.ts +0 -177
  1746. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- /// <reference path="../../vite/version.d.ts" />
1
+ /// <reference path="../../vite/plugins/version.d.ts" />
2
2
 
3
3
  // Extend CacheStorage with Cloudflare's default cache property
4
4
  declare global {
@@ -10,14 +10,21 @@ declare global {
10
10
  /**
11
11
  * Cloudflare Edge Cache Store
12
12
  *
13
- * Production cache store using Cloudflare's Cache API.
14
- * Handles SWR atomically - get() checks staleness and marks REVALIDATING in one operation.
13
+ * Production cache store using Cloudflare's Cache API (L1) with optional
14
+ * KV persistence (L2).
15
+ *
16
+ * L1 (Cache API): Per-colo, fast, ephemeral. Handles SWR atomically.
17
+ * L2 (KV): Global, persistent, ~50ms reads. Auto-warms cold colos.
18
+ *
19
+ * Read flow: L1 hit → serve | L1 miss → L2 hit → serve + promote to L1 | both miss → render
20
+ * Write flow: L1 write + L2 write (both via waitUntil)
15
21
  *
16
22
  * Features:
17
23
  * - Extended TTL for SWR window (max-age = ttl + swr)
18
24
  * - Staleness via x-edge-cache-stale-at header
19
- * - Atomic REVALIDATING status for thundering herd prevention
25
+ * - Atomic REVALIDATING status for thundering herd prevention (L1 only)
20
26
  * - Non-blocking writes via waitUntil
27
+ * - KV L2 for cross-colo cache persistence
21
28
  */
22
29
 
23
30
  import type {
@@ -25,127 +32,387 @@ import type {
25
32
  CachedEntryData,
26
33
  CacheDefaults,
27
34
  CacheGetResult,
35
+ CacheItemResult,
36
+ CacheItemOptions,
37
+ ShellCacheEntry,
38
+ CacheReadError,
28
39
  } from "../types.js";
40
+ import { CACHE_READ_ERROR } from "../types.js";
29
41
  import {
30
- getRequestContext,
42
+ _getRequestContext,
31
43
  type RequestContext,
32
44
  } from "../../server/request-context.js";
45
+ import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
33
46
  import { VERSION } from "@rangojs/router:version";
47
+ import {
48
+ isPerClientSignalHeader,
49
+ stripPerClientSignals,
50
+ } from "../../browser/cookie-name.js";
51
+ import {
52
+ resolveTtl,
53
+ resolveSwrWindow,
54
+ DEFAULT_FUNCTION_TTL,
55
+ } from "../cache-policy.js";
56
+ import { reportCacheError, reportingAsync } from "../cache-error.js";
57
+ import type { CacheErrorCategory } from "../cache-error.js";
58
+ import { bufferToBase64, base64ToBuffer } from "./cf-base64.js";
59
+ import {
60
+ KV_KEY_PRESERVED_PREFIX_BYTES,
61
+ KV_MAX_KEY_BYTES,
62
+ KV_MIN_EXPIRATION_TTL,
63
+ kvKeyByteLength,
64
+ kvKeyDigest,
65
+ remainingCacheControl,
66
+ truncateToBytes,
67
+ } from "./cf-kv-utils.js";
68
+ import {
69
+ TAG_MARKER_CACHE_PREFIX,
70
+ TAG_MARKER_ABSENT,
71
+ getTagMarkerMemo,
72
+ getTagMarkerInflight,
73
+ } from "./cf-tag-marker-memo.js";
74
+ import { createCloudflareZonePurge } from "./cf-zone-purge.js";
34
75
 
35
76
  // ============================================================================
36
77
  // Constants
37
78
  // ============================================================================
79
+ //
80
+ // Header names, KV prefixes, and timeout/interval defaults live in
81
+ // cf-cache-constants.ts so collaborator modules can share them without a
82
+ // circular import back to this class. They are re-exported below so existing
83
+ // import paths (`../cf-cache-store`, `./cf-cache-store.js`) still resolve.
84
+ import {
85
+ CACHE_STALE_AT_HEADER,
86
+ CACHE_STATUS_HEADER,
87
+ CACHE_TAGS_HEADER,
88
+ CACHE_TAGGED_AT_HEADER,
89
+ TAG_MARKER_PREFIX,
90
+ CACHE_REVALIDATING_AT_HEADER,
91
+ CACHE_EXPIRES_AT_HEADER,
92
+ CACHE_ORIG_CC_HEADER,
93
+ MAX_REVALIDATION_INTERVAL,
94
+ EDGE_LOOKUP_TIMEOUT_MS,
95
+ EDGE_READ_TIMEOUT_MS,
96
+ KV_READ_TIMEOUT_MS,
97
+ } from "./cf-cache-constants.js";
98
+
99
+ // Re-export the public constants so consumers/tests importing them from
100
+ // cf-cache-store keep working after the move.
101
+ export {
102
+ CACHE_STALE_AT_HEADER,
103
+ CACHE_STATUS_HEADER,
104
+ CACHE_TAGS_HEADER,
105
+ CACHE_TAGGED_AT_HEADER,
106
+ TAG_MARKER_PREFIX,
107
+ CACHE_REVALIDATING_AT_HEADER,
108
+ MAX_REVALIDATION_INTERVAL,
109
+ EDGE_LOOKUP_TIMEOUT_MS,
110
+ EDGE_READ_TIMEOUT_MS,
111
+ KV_READ_TIMEOUT_MS,
112
+ };
113
+
114
+ // The tag-marker prefix/sentinel and per-request memo helpers (with their
115
+ // module-singleton WeakMaps) live in cf-tag-marker-memo.ts; imported above.
116
+
117
+ /**
118
+ * Per-request memo of the derived cache-key base URL.
119
+ *
120
+ * deriveBaseUrl() is a pure function of the live request URL, but keyToRequest
121
+ * calls it on EVERY cache operation (each segment/item get/set/delete, each
122
+ * KV->L1 promote, each tag-marker read), so a page composed of many cached
123
+ * entries re-parses the same request.url and re-runs the host validation tens
124
+ * of times. Keying by the request-context object collapses that to one derive
125
+ * per request. Keyed by ctx alone (not by store) because the derived value
126
+ * depends only on the request URL, not on which store asked.
127
+ */
128
+ const derivedBaseUrlMemo = new WeakMap<object, string>();
129
+
130
+ // Pure KV helpers (key byte-length limits, expirationTtl floor, stale-path
131
+ // Cache-Control recompute) live in cf-kv-utils.ts; imported above.
132
+
133
+ /**
134
+ * Stores (by namespace) already warned about tag machinery configured without a
135
+ * KV namespace, so the warning fires once per process rather than per request
136
+ * (CFCacheStore is constructed per request).
137
+ */
138
+ const warnedNoKvReadInvalidation = new Set<string>();
139
+
140
+ /**
141
+ * Stores (by namespace) already warned about a tagInvalidationTtl below KV's
142
+ * expirationTtl floor, so the floor warning fires once per process rather than
143
+ * once per request (CFCacheStore is constructed per request).
144
+ */
145
+ const warnedTagInvalidationTtlFloor = new Set<string>();
146
+
147
+ /**
148
+ * Stores (by namespace) already warned that a TAGGED shell was written on a
149
+ * KV-less store without tagPurge: no markers and no purge means updateTag()
150
+ * cannot reach the shell (freshness is ttl/swr only). Once per isolate, not
151
+ * per capture (CFCacheStore is constructed per request).
152
+ */
153
+ const warnedShellTagsNoEviction = new Set<string>();
154
+
155
+ /**
156
+ * Stores (by namespace) already warned that tag invalidation is writing KV
157
+ * markers with no expiry (tagInvalidationTtl unset), so the unbounded-growth
158
+ * warning fires once per process rather than once per invalidateTags call
159
+ * (CFCacheStore is constructed per request; invalidateTags runs per marker
160
+ * batch). Distinct from the floor warning: that one only fires for a positive
161
+ * below-floor value, never for the unset (no-expiry) default that this bounds.
162
+ */
163
+ const warnedNoTagInvalidationTtl = new Set<string>();
164
+
165
+ /**
166
+ * Stores (by namespace) already warned that an entry's tag set produced a
167
+ * Cache-Tag header over Cloudflare's aggregate limit, so the header was
168
+ * omitted (the entry stays cacheable and marker-invalidatable; it just cannot
169
+ * be evicted per-tag by a purge). Once per process, not per write.
170
+ */
171
+ const warnedCacheTagHeaderOverflow = new Set<string>();
172
+
173
+ /**
174
+ * Stores (by namespace) already warned that an over-limit tag set made an
175
+ * entry UNCACHEABLE in KV-less purge mode: with no Cache-Tag tokens a purge
176
+ * cannot evict it, and with no KV there is no marker fallback either, so
177
+ * caching it would serve stale until TTL while updateTag() reports success.
178
+ * Once per process, not per write.
179
+ */
180
+ const warnedCacheTagOverflowUncacheable = new Set<string>();
38
181
 
39
- /** Header storing timestamp when entry becomes stale */
40
- export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
182
+ /**
183
+ * Max length of one emitted `rg:*` Cache-Tag token. Cloudflare caps a purge
184
+ * API tag value at 1,024 characters and the aggregate Cache-Tag header at
185
+ * 16 KB; an application tag is unbounded, so an over-long token is collapsed
186
+ * to a deterministic hash (see boundedTagToken) instead of being allowed to
187
+ * fail the whole L1 write. 256 keeps headers compact while leaving room for
188
+ * long-but-reasonable tag names under any namespace.
189
+ */
190
+ const CACHE_TAG_TOKEN_MAX = 256;
41
191
 
42
- /** Header storing cache status: HIT | REVALIDATING */
43
- export const CACHE_STATUS_HEADER = "x-edge-cache-status";
192
+ /**
193
+ * Cloudflare's documented aggregate Cache-Tag header limit (16 KB). A tagged
194
+ * entry whose tokens would exceed it gets NO Cache-Tag header (plus a
195
+ * once-per-namespace warning) rather than a failed cache write; the read path
196
+ * then falls back to the marker check for that entry (see isL1Invalidated).
197
+ */
198
+ const CACHE_TAG_HEADER_MAX_BYTES = 16 * 1024;
44
199
 
45
200
  /**
46
- * Maximum age in seconds for REVALIDATING status before allowing new revalidation.
47
- * After this period, a stale entry in REVALIDATING status will trigger revalidation again.
201
+ * FNV-1a 64-bit hash of a tag value, hex-encoded. Used to bound over-long
202
+ * Cache-Tag tokens: deterministic (write-time token === purge-time token) and
203
+ * collision-safe in the failure direction — a collision over-purges (an extra
204
+ * eviction, healed by the next render), never serves stale.
48
205
  * @internal
49
206
  */
50
- export const MAX_REVALIDATION_INTERVAL = 30;
207
+ function fnv1a64(input: string): string {
208
+ let hash = 0xcbf29ce484222325n;
209
+ for (let i = 0; i < input.length; i++) {
210
+ hash ^= BigInt(input.charCodeAt(i));
211
+ hash = (hash * 0x100000001b3n) & 0xffffffffffffffffn;
212
+ }
213
+ return hash.toString(16).padStart(16, "0");
214
+ }
51
215
 
52
216
  // ============================================================================
53
217
  // Types
54
218
  // ============================================================================
219
+ //
220
+ // The shared public types (KVNamespace, CFCacheReadDebugEvent, CFCacheDebug,
221
+ // CFCacheStoreOptions) live in cf-cache-types.ts; imported and re-exported below
222
+ // so existing import paths still resolve. The private KV envelope interfaces
223
+ // stay here with the methods that read/write them.
224
+ import type {
225
+ KVNamespace,
226
+ CFCacheReadDebugEvent,
227
+ CFCacheDebug,
228
+ CFCacheStoreOptions,
229
+ } from "./cf-cache-types.js";
230
+ export type {
231
+ KVNamespace,
232
+ CFCacheReadDebugEvent,
233
+ CFCacheDebug,
234
+ CFCacheStoreOptions,
235
+ };
55
236
 
56
237
  /**
57
- * Cloudflare Workers ExecutionContext (subset we need)
238
+ * KV envelope for segment cache entries.
239
+ * @internal
58
240
  */
59
- export interface ExecutionContext {
60
- waitUntil(promise: Promise<any>): void;
61
- passThroughOnException(): void;
241
+ interface KVSegmentEnvelope {
242
+ /** Cached segment data */
243
+ d: CachedEntryData;
244
+ /** When entry becomes stale (ms epoch) */
245
+ s: number;
246
+ /** When entry hard-expires (ms epoch) */
247
+ e: number;
62
248
  }
63
249
 
64
- export interface CFCacheStoreOptions<TEnv = unknown> {
65
- /**
66
- * Cache namespace. If not provided, uses caches.default (recommended).
67
- * Only set this if you need isolated cache storage.
68
- */
69
- namespace?: string;
250
+ /**
251
+ * KV envelope for function cache entries ("use cache").
252
+ * @internal
253
+ */
254
+ interface KVItemEnvelope {
255
+ /** RSC-serialized return value */
256
+ v: string;
257
+ /** RSC-encoded handle data (see handle-snapshot.ts encodeHandles) */
258
+ h?: string;
259
+ /** When entry becomes stale (ms epoch) */
260
+ s: number;
261
+ /** When entry hard-expires (ms epoch) */
262
+ e: number;
263
+ /** Cache tags (for distributed tag invalidation) */
264
+ t?: string[];
265
+ /** Timestamp when tags were attached (ms epoch) */
266
+ ta?: number;
267
+ }
70
268
 
269
+ /**
270
+ * Coupled Cache API/KV envelope for PPR shell cache entries.
271
+ * @internal
272
+ */
273
+ interface CFShellEnvelope {
71
274
  /**
72
- * Base URL for cache keys.
73
- *
74
- * If not provided, derives from request hostname via requestContext:
75
- * - Production domains → uses `https://{hostname}/`
76
- * - Dev/preview (localhost, workers.dev, pages.dev) → uses internal fallback URL
275
+ * base64-encoded prelude bytes. Absent iff `no` (navigationOnly entries
276
+ * store no document half — ShellCacheEntry.prelude).
77
277
  */
78
- baseUrl?: string;
79
-
80
- /** Default cache options */
81
- defaults?: CacheDefaults;
82
-
278
+ p?: string;
279
+ /** postponed state JSON, or null (DATA variant — no holes). Absent iff `no`. */
280
+ po?: string | null;
281
+ /** React.version captured at prerender time */
282
+ rv: string;
283
+ /** Build version captured at prerender time (ShellCacheEntry.buildVersion) */
284
+ bv?: string;
285
+ /** Capture-generation start time (ms epoch), used by tag marker checks. */
286
+ c: number;
287
+ /** When entry becomes stale (ms epoch) */
288
+ s: number;
289
+ /** When entry hard-expires (ms epoch) */
290
+ e: number;
291
+ /** Cache tags (for distributed tag invalidation) */
292
+ t?: string[];
293
+ /** Timestamp when tags were attached (ms epoch) */
294
+ ta?: number;
295
+ /** initialTheme the capture render was built with (resume theme fidelity) */
296
+ i?: string;
297
+ /** Capture data snapshot: recorded cache-store hits/writes for HIT parity */
298
+ sn?: import("../types.js").ShellSnapshotRecord[];
83
299
  /**
84
- * Cloudflare ExecutionContext for non-blocking cache writes.
85
- * Pass the `ctx` from your worker's fetch handler.
86
- *
87
- * @example
88
- * ```typescript
89
- * new CFCacheStore({ ctx: env.ctx })
90
- * ```
300
+ * ShellCacheEntry.docKey. Must round-trip: navigation-replay eligibility
301
+ * requires the exact canonical doc segment record named here — dropping the
302
+ * field reads back as "no consumable record" and every partial navigation
303
+ * reports `no-segment-snapshot` after a KV round trip (the memory store
304
+ * passes the entry by reference, so only envelope stores can lose it).
91
305
  */
92
- ctx: ExecutionContext;
93
-
306
+ dk?: string;
94
307
  /**
95
- * Cache version string override. When this changes, all cached entries are
96
- * effectively invalidated (new keys won't match old entries).
97
- *
98
- * Defaults to the auto-generated VERSION from `rsc-router:version` virtual module.
99
- * Only set this if you need a custom versioning strategy.
308
+ * ShellCacheEntry.handlerLiveHoles. Must round-trip: the serve side arms the
309
+ * handler-free fast path on `!entry.handlerLiveHoles`, so dropping the flag
310
+ * here silently fast-pathed handler-live entries after a KV round trip —
311
+ * their holes only a handler re-run can fill.
100
312
  */
101
- version?: string;
313
+ lh?: boolean;
314
+ /** ShellCacheEntry.transitionWhen; conditional transitions must re-run. */
315
+ tw?: true;
316
+ /** ShellCacheEntry.navigationOnly; its partial-context prelude is not document-safe. */
317
+ no?: true;
318
+ }
102
319
 
103
- /**
104
- * Custom key generator applied to all cache operations.
105
- * Receives the full RequestContext (including env) and the default-generated key.
106
- * Return value becomes the final cache key (unless route overrides with `key` option).
107
- *
108
- * @example Using headers for user segmentation
109
- * ```typescript
110
- * keyGenerator: (ctx, defaultKey) => {
111
- * const segment = ctx.request.headers.get('x-user-segment') || 'default';
112
- * return `${segment}:${defaultKey}`;
113
- * }
114
- * ```
115
- *
116
- * @example Using env bindings for multi-region
117
- * ```typescript
118
- * keyGenerator: (ctx, defaultKey) => {
119
- * const region = ctx.env.REGION || 'us';
120
- * return `${region}:${defaultKey}`;
121
- * }
122
- * ```
123
- *
124
- * @example Using cookies for locale-aware caching
125
- * ```typescript
126
- * keyGenerator: (ctx, defaultKey) => {
127
- * const locale = ctx.cookie('locale') || 'en';
128
- * return `${locale}:${defaultKey}`;
129
- * }
130
- * ```
131
- */
132
- keyGenerator?: (
133
- ctx: RequestContext<TEnv>,
134
- defaultKey: string,
135
- ) => string | Promise<string>;
320
+ type CFShellDebugOutcome =
321
+ | "l1-hit"
322
+ | "l1-miss"
323
+ | "kv-hit"
324
+ | "kv-miss"
325
+ | "kv-promoted"
326
+ | "marker-invalidated"
327
+ | "l1-stored"
328
+ | "kv-stored"
329
+ | "write-invalidated";
330
+
331
+ interface CFShellDebugDetails {
332
+ tier?: "l1" | "kv";
333
+ reason?:
334
+ | "absent"
335
+ | "timeout"
336
+ | "error"
337
+ | "non-200"
338
+ | "corrupt"
339
+ | "malformed"
340
+ | "expired"
341
+ | "unavailable";
342
+ freshness?: "fresh" | "stale";
343
+ status?: number;
344
+ matchMs?: number;
345
+ bodyReadMs?: number;
346
+ markerMs?: number;
347
+ readMs?: number;
348
+ expiresAt?: number;
349
+ remainingTtl?: number;
350
+ }
351
+
352
+ /** Validate the coupled PPR shell envelope before any field reaches resume. */
353
+ function isShellEnvelope(value: unknown): value is CFShellEnvelope {
354
+ if (value == null || typeof value !== "object") return false;
355
+ const envelope = value as Partial<CFShellEnvelope>;
356
+ return (
357
+ // Document half: required unless the envelope is navigationOnly (`no`),
358
+ // which stores neither field. Tolerate a legacy navigationOnly envelope
359
+ // that still carries them.
360
+ (typeof envelope.p === "string" ||
361
+ (envelope.p === undefined && envelope.no === true)) &&
362
+ (envelope.po === null ||
363
+ typeof envelope.po === "string" ||
364
+ (envelope.po === undefined && envelope.no === true)) &&
365
+ typeof envelope.rv === "string" &&
366
+ (envelope.bv === undefined || typeof envelope.bv === "string") &&
367
+ typeof envelope.c === "number" &&
368
+ Number.isFinite(envelope.c) &&
369
+ typeof envelope.s === "number" &&
370
+ Number.isFinite(envelope.s) &&
371
+ typeof envelope.e === "number" &&
372
+ Number.isFinite(envelope.e) &&
373
+ (envelope.t === undefined ||
374
+ (Array.isArray(envelope.t) &&
375
+ envelope.t.every((tag) => typeof tag === "string"))) &&
376
+ (envelope.ta === undefined ||
377
+ (typeof envelope.ta === "number" && Number.isFinite(envelope.ta))) &&
378
+ (envelope.i === undefined || typeof envelope.i === "string") &&
379
+ (envelope.sn === undefined || Array.isArray(envelope.sn)) &&
380
+ (envelope.dk === undefined || typeof envelope.dk === "string") &&
381
+ (envelope.lh === undefined || typeof envelope.lh === "boolean") &&
382
+ (envelope.tw === undefined || envelope.tw === true) &&
383
+ (envelope.no === undefined || envelope.no === true)
384
+ );
136
385
  }
137
386
 
138
387
  /**
139
- * Cache status values for the x-edge-cache-status header.
388
+ * KV envelope for document cache entries.
140
389
  * @internal
141
390
  */
142
- export type CacheStatus = "HIT" | "REVALIDATING";
391
+ interface KVResponseEnvelope {
392
+ /** Response body as base64-encoded string (safe for binary payloads) */
393
+ b: string;
394
+ /** HTTP status code */
395
+ st: number;
396
+ /** HTTP status text */
397
+ stx: string;
398
+ /** Serialized headers as key-value pairs (client-facing; no internal headers) */
399
+ hd: [string, string][];
400
+ /** When entry becomes stale (ms epoch) */
401
+ s: number;
402
+ /** When entry hard-expires (ms epoch) */
403
+ e: number;
404
+ /** Cache tags (for distributed tag invalidation) */
405
+ t?: string[];
406
+ /** Timestamp when tags were attached (ms epoch) */
407
+ ta?: number;
408
+ }
143
409
 
144
410
  // ============================================================================
145
411
  // CFCacheStore Implementation
146
412
  // ============================================================================
147
413
 
148
414
  export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
415
+ readonly supportsPassiveShellReads: true = true;
149
416
  readonly defaults?: CacheDefaults;
150
417
  readonly keyGenerator?: (
151
418
  ctx: RequestContext<TEnv>,
@@ -153,9 +420,20 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
153
420
  ) => string | Promise<string>;
154
421
 
155
422
  private readonly namespace?: string;
156
- private readonly baseUrl: string;
423
+ private readonly explicitBaseUrl?: string;
157
424
  private readonly waitUntil?: (fn: () => Promise<void>) => void;
158
425
  private readonly version?: string;
426
+ private readonly edgeLookupTimeoutMs: number;
427
+ private readonly edgeReadTimeoutMs: number;
428
+ private readonly kvReadTimeoutMs: number;
429
+ private readonly debug?: (event: CFCacheReadDebugEvent) => void;
430
+ private readonly kv?: KVNamespace;
431
+ /** True when constructed without KV: no durable tag history (see ctor). */
432
+ readonly tagHistoryInert?: boolean;
433
+ private readonly onRevalidateTag?: (tags: string[]) => Promise<void>;
434
+ private readonly tagPurge?: (cacheTags: string[]) => Promise<void>;
435
+ private readonly tagInvalidationTtl?: number;
436
+ private readonly tagCacheTtl: number;
159
437
 
160
438
  constructor(options: CFCacheStoreOptions<TEnv>) {
161
439
  if (!options.ctx) {
@@ -167,45 +445,256 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
167
445
  }
168
446
 
169
447
  this.namespace = options.namespace;
170
- this.baseUrl = options.baseUrl ?? this.deriveBaseUrl();
448
+ // Base URL is resolved lazily per cache operation (see resolveBaseUrl).
449
+ // The store is constructed before the per-request context ALS is entered
450
+ // (the cache factory runs ahead of runWithRequestContext in the handler),
451
+ // so deriving the host here would always miss the request and fall back to
452
+ // the internal host. Only the explicit override can be captured eagerly.
453
+ this.explicitBaseUrl = options.baseUrl;
171
454
  this.defaults = options.defaults;
172
455
  this.version = options.version ?? VERSION;
456
+ // Coalesce only finite numbers to the override; a non-finite value (NaN from
457
+ // `Number(env.UNSET)`, or Infinity) would otherwise sail past `?? DEFAULT`
458
+ // (which only replaces null/undefined) into setTimeout, where NaN/Infinity
459
+ // are spec-coerced to ~1ms and silently turn the budget into a near-100%
460
+ // false-miss on that tier. A genuine finite 0 or negative still passes
461
+ // through and disables the budget per the documented `<= 0` contract.
462
+ const finiteBudget = (
463
+ value: number | undefined,
464
+ fallback: number,
465
+ ): number =>
466
+ typeof value === "number" && Number.isFinite(value) ? value : fallback;
467
+ this.edgeLookupTimeoutMs = finiteBudget(
468
+ options.edgeLookupTimeoutMs,
469
+ EDGE_LOOKUP_TIMEOUT_MS,
470
+ );
471
+ this.edgeReadTimeoutMs = finiteBudget(
472
+ options.edgeReadTimeoutMs,
473
+ EDGE_READ_TIMEOUT_MS,
474
+ );
475
+ this.kvReadTimeoutMs = finiteBudget(
476
+ options.kvReadTimeoutMs,
477
+ KV_READ_TIMEOUT_MS,
478
+ );
479
+ this.debug =
480
+ options.debug === true
481
+ ? (event) =>
482
+ console.log(`[CFCacheStore:debug] ${JSON.stringify(event)}`)
483
+ : typeof options.debug === "function"
484
+ ? options.debug
485
+ : undefined;
173
486
  this.keyGenerator = options.keyGenerator;
174
487
  this.waitUntil = (fn) => options.ctx.waitUntil(fn());
488
+ this.kv = options.kv;
489
+ // Without KV, isTagsInvalidatedSince has no durable history — it answers
490
+ // from the per-request memo at best. Runtime shells tolerate that (purge
491
+ // eviction + ttl/swr bound the staleness), but an immutable TAGGED
492
+ // build-manifest shell must not serve on such a store: nothing could ever
493
+ // evict it (purge cannot delete a build asset), so the manifest gate
494
+ // declines on this flag (shell-build-manifest.ts).
495
+ this.tagHistoryInert = options.kv ? undefined : true;
496
+ this.onRevalidateTag = options.onRevalidateTag;
497
+ // tagPurge accepts a ready purge function or a credentials object; the
498
+ // object form is normalized through the built-in zone purge client, which
499
+ // validates zoneId/apiToken eagerly so an unset env var fails at
500
+ // construction instead of on the first updateTag().
501
+ this.tagPurge =
502
+ typeof options.tagPurge === "function"
503
+ ? options.tagPurge
504
+ : options.tagPurge
505
+ ? createCloudflareZonePurge(options.tagPurge)
506
+ : undefined;
507
+ // tagInvalidationTtl feeds KV's expirationTtl, which CF rejects below
508
+ // KV_MIN_EXPIRATION_TTL (60s) -- a too-small finite value would make EVERY
509
+ // marker write throw and break ALL invalidation. Floor it (and warn once);
510
+ // a non-finite/non-positive value falls back to the no-expiry default
511
+ // (markers persist) rather than silently sailing a NaN into expirationTtl.
512
+ // KV-less the option is dead config — no markers to expire, and the
513
+ // retention cap it used to imply is KV-conditional (putShell) — so it is
514
+ // dropped without the KV-floor validation/warning, which would misdirect.
515
+ this.tagInvalidationTtl = options.kv
516
+ ? this.sanitizeTagInvalidationTtl(options.tagInvalidationTtl)
517
+ : undefined;
518
+ // tagCacheTtl gates the L1 marker cache via `> 0`. A non-finite value (NaN
519
+ // from `Number(env.UNSET)`) is not null/undefined, so `?? 0` would let it
520
+ // through and silently disable the cache while reading as "configured".
521
+ // finiteBudget coerces non-finite/null/undefined to 0; the `> 0` guard then
522
+ // collapses a finite non-positive value to the documented 0 = disabled.
523
+ const tagCacheTtl = finiteBudget(options.tagCacheTtl, 0);
524
+ this.tagCacheTtl = tagCacheTtl > 0 ? tagCacheTtl : 0;
525
+
526
+ // Read-side tag invalidation requires KV: isGloballyInvalidated() compares an
527
+ // entry's taggedAt against the per-tag KV marker and short-circuits to "not
528
+ // invalidated" when no KV namespace is configured. A consumer who wires the
529
+ // tag machinery (tagCacheTtl for L1 markers, or onRevalidateTag for CDN purge)
530
+ // but omits kv gets only the purge fired - marker writes are skipped without
531
+ // kv - yet every tagged read still serves stale data with no other signal.
532
+ // Surface that misconfiguration. Exception: with tagPurge configured (purge
533
+ // mode) L1 eviction is the purge itself, so a KV-less store is a supported
534
+ // L1-only configuration, not a silent no-op.
535
+ if (
536
+ !this.kv &&
537
+ !this.tagPurge &&
538
+ (this.tagCacheTtl > 0 || this.onRevalidateTag)
539
+ ) {
540
+ this.warnOncePerNamespace(
541
+ warnedNoKvReadInvalidation,
542
+ `[CFCacheStore] tagCacheTtl/onRevalidateTag is configured without a KV ` +
543
+ `namespace, so tag invalidation has NO read-side effect: tagged reads ` +
544
+ `are never treated as invalidated and serve stale data. Configure ` +
545
+ `{ kv } for distributed tag invalidation.`,
546
+ );
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Warn about a namespace-scoped misconfiguration once per namespace per
552
+ * isolate. `seen` is the module-level Set for that message family -- Sets
553
+ * are module-level (not instance fields) so re-constructed stores in the
554
+ * same isolate don't re-warn.
555
+ * @internal
556
+ */
557
+ private warnOncePerNamespace(seen: Set<string>, message: string): void {
558
+ const id = this.namespace ?? "default";
559
+ if (seen.has(id)) return;
560
+ seen.add(id);
561
+ console.warn(message);
562
+ }
563
+
564
+ /**
565
+ * Validate a consumer-supplied tagInvalidationTtl against CF KV's expirationTtl
566
+ * floor. A finite value below KV_MIN_EXPIRATION_TTL is raised to it (with a
567
+ * one-time warning) so invalidation keeps working instead of every marker
568
+ * write throwing; a non-finite or non-positive value returns undefined (the
569
+ * no-expiry default). The warning still notes the sizing rule: the TTL must
570
+ * exceed the largest entry TTL+SWR or invalidated entries can resurrect.
571
+ * @internal
572
+ */
573
+ private sanitizeTagInvalidationTtl(
574
+ value: number | undefined,
575
+ ): number | undefined {
576
+ if (value == null) return undefined;
577
+ if (!Number.isFinite(value) || value <= 0) return undefined;
578
+ if (value < KV_MIN_EXPIRATION_TTL) {
579
+ this.warnOncePerNamespace(
580
+ warnedTagInvalidationTtlFloor,
581
+ `[CFCacheStore] tagInvalidationTtl ${value} is below Cloudflare KV's ` +
582
+ `${KV_MIN_EXPIRATION_TTL}s expirationTtl floor; raising to ` +
583
+ `${KV_MIN_EXPIRATION_TTL}. It must still exceed your largest entry ` +
584
+ `TTL+SWR or invalidated entries can resurrect when the marker expires.`,
585
+ );
586
+ return KV_MIN_EXPIRATION_TTL;
587
+ }
588
+ return value;
589
+ }
590
+
591
+ /**
592
+ * Emit a debug event if `debug` is enabled. Swallows sink errors so a faulty
593
+ * debug callback can never break a cache read.
594
+ * @internal
595
+ */
596
+ private emitDebug(event: CFCacheReadDebugEvent): void {
597
+ if (!this.debug) return;
598
+ try {
599
+ this.debug(event);
600
+ } catch {
601
+ // A broken debug sink must not affect the request.
602
+ }
603
+ }
604
+
605
+ /** Build-time-gated shell tier trace for deployed cross-colo diagnostics. */
606
+ private debugShell(
607
+ key: string,
608
+ outcome: CFShellDebugOutcome,
609
+ details: CFShellDebugDetails = {},
610
+ ): void {
611
+ if (!INTERNAL_RANGO_DEBUG) return;
612
+ const request = _getRequestContext()?.request as
613
+ | (Request & { cf?: { colo?: unknown } })
614
+ | undefined;
615
+ const ray = request?.headers.get("cf-ray") ?? undefined;
616
+ const cfColo = request?.cf?.colo;
617
+ const colo =
618
+ typeof cfColo === "string"
619
+ ? cfColo
620
+ : ray?.includes("-")
621
+ ? ray.slice(ray.lastIndexOf("-") + 1)
622
+ : undefined;
623
+ console.log(
624
+ `[CFCacheStore][shell] ${JSON.stringify({
625
+ key,
626
+ outcome,
627
+ at: Date.now(),
628
+ ray,
629
+ colo,
630
+ ...details,
631
+ })}`,
632
+ );
633
+ }
634
+
635
+ /**
636
+ * Resolve the cache-key base URL for the current cache operation.
637
+ * Prefers an explicit `baseUrl` option; otherwise derives it from the live
638
+ * request. Called per operation (from keyToRequest), which runs inside the
639
+ * request-context ALS, so deriveBaseUrl sees the request and can use the
640
+ * production host instead of the internal fallback.
641
+ * @internal
642
+ */
643
+ private resolveBaseUrl(): string {
644
+ return this.explicitBaseUrl ?? this.deriveBaseUrl();
175
645
  }
176
646
 
177
647
  /**
178
648
  * Derive base URL from request hostname via requestContext.
179
- * Uses internal fallback for dev/preview environments.
649
+ * Uses internal fallback for dev/preview environments and untrusted hostnames.
650
+ * Must run inside the request context (invoked lazily via resolveBaseUrl).
180
651
  * @internal
181
652
  */
182
653
  private deriveBaseUrl(): string {
183
- const fallback = "https://rsc-cache.internal.com/";
654
+ const fallback = "https://rsc-dummy-host-1.com/";
184
655
 
185
- const ctx = getRequestContext();
656
+ const ctx = _getRequestContext();
186
657
  if (!ctx?.request) {
187
658
  return fallback;
188
659
  }
189
660
 
190
- try {
191
- const url = new URL(ctx.request.url);
192
- const hostname = url.hostname;
661
+ // The result is deterministic per request, but keyToRequest calls this on
662
+ // every cache operation; memoize per request context (see derivedBaseUrlMemo).
663
+ const memoized = derivedBaseUrlMemo.get(ctx);
664
+ if (memoized !== undefined) {
665
+ return memoized;
666
+ }
193
667
 
194
- // Use fallback for dev/preview environments
195
- if (
196
- hostname === "localhost" ||
197
- hostname === "127.0.0.1" ||
198
- hostname.endsWith(".workers.dev") ||
199
- hostname.endsWith(".pages.dev")
200
- ) {
668
+ const derived = ((): string => {
669
+ try {
670
+ const url = new URL(ctx.request.url);
671
+ const hostname = url.hostname;
672
+
673
+ // Use fallback for dev/preview environments
674
+ if (
675
+ hostname === "localhost" ||
676
+ hostname === "127.0.0.1" ||
677
+ hostname.endsWith(".workers.dev") ||
678
+ hostname.endsWith(".pages.dev")
679
+ ) {
680
+ return fallback;
681
+ }
682
+
683
+ // Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
684
+ // to prevent host header injection into cache keys
685
+ if (!/^[a-zA-Z0-9.-]+$/.test(hostname) || hostname.length > 253) {
686
+ return fallback;
687
+ }
688
+
689
+ // Use actual hostname for production
690
+ return `https://${hostname}/`;
691
+ } catch {
201
692
  return fallback;
202
693
  }
694
+ })();
203
695
 
204
- // Use actual hostname for production
205
- return `https://${hostname}/`;
206
- } catch {
207
- return fallback;
208
- }
696
+ derivedBaseUrlMemo.set(ctx, derived);
697
+ return derived;
209
698
  }
210
699
 
211
700
  /**
@@ -219,6 +708,297 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
219
708
  return caches.default;
220
709
  }
221
710
 
711
+ /**
712
+ * Race an async cache read against a latency budget. Shared by all three read
713
+ * tiers (L1 match, L1 body, L2/KV) so the timeout policy lives in one place:
714
+ * on timeout it returns `{ value: undefined, timedOut: true }` and logs
715
+ * `${label} exceeded ${budgetMs}ms; treating as miss`; the abandoned read is
716
+ * left to settle in the background (late rejection swallowed) rather than
717
+ * aborted, since the underlying CF primitives expose no cancellation. A budget
718
+ * <= 0 disables the bound and awaits the read directly. `read` is a thunk so
719
+ * the disabled path and the raced path start the read identically.
720
+ * @internal
721
+ */
722
+ private async readWithTimeout<T>(
723
+ read: () => Promise<T>,
724
+ budgetMs: number,
725
+ label: string,
726
+ ): Promise<{ value: T | undefined; timedOut: boolean }> {
727
+ if (budgetMs <= 0) return { value: await read(), timedOut: false };
728
+
729
+ let timer: ReturnType<typeof setTimeout> | undefined;
730
+ const timeout = new Promise<{ timedOut: true }>((resolve) => {
731
+ timer = setTimeout(() => resolve({ timedOut: true }), budgetMs);
732
+ });
733
+ try {
734
+ const readPromise = read();
735
+ // The losing branch keeps running; ensure a late rejection can't surface
736
+ // as an unhandled rejection once we've stopped awaiting it.
737
+ readPromise.catch(() => {});
738
+ const result = await Promise.race([
739
+ readPromise.then((value) => ({ timedOut: false as const, value })),
740
+ timeout,
741
+ ]);
742
+ if (result.timedOut) {
743
+ console.warn(
744
+ `[CFCacheStore] ${label} exceeded ${budgetMs}ms; treating as miss`,
745
+ );
746
+ return { value: undefined, timedOut: true };
747
+ }
748
+ return { value: result.value, timedOut: false };
749
+ } finally {
750
+ if (timer) clearTimeout(timer);
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Read from the L1 edge cache under the edgeLookupTimeoutMs budget. A `match`
756
+ * slower than the budget is abandoned and reported as a miss
757
+ * (`{ response: undefined, timedOut: true }`) so a degraded colo cannot stall
758
+ * the request; callers fall through to their normal miss path (L2/KV or
759
+ * render). The `timedOut` flag lets callers distinguish an abandoned slow
760
+ * match from a genuine miss for debug reporting; `error` is set when the
761
+ * `match` itself rejected (a transient L1 infra error) so the caller can
762
+ * report it as cache-read while still degrading to L2/KV -- distinct from a
763
+ * genuine miss (no entry), which sets neither flag.
764
+ * @internal
765
+ */
766
+ private async matchWithTimeout(
767
+ cache: Cache,
768
+ request: Request,
769
+ ): Promise<{
770
+ response: Response | undefined;
771
+ timedOut: boolean;
772
+ error?: unknown;
773
+ }> {
774
+ let matchError: unknown;
775
+ const { value, timedOut } = await this.readWithTimeout(
776
+ // A fast match rejection is caught at the thunk and reported as a miss
777
+ // (response undefined), so the caller falls through to L2/KV rather than
778
+ // escaping to the outer catch -- symmetric with the body-read thunk. The
779
+ // error is captured (not swallowed) so the caller can surface it via
780
+ // onError as a cache-read degradation.
781
+ () =>
782
+ cache.match(request).catch((e) => {
783
+ matchError = e;
784
+ return undefined;
785
+ }),
786
+ this.edgeLookupTimeoutMs,
787
+ "edge cache lookup",
788
+ );
789
+ return { response: value, timedOut, error: matchError };
790
+ }
791
+
792
+ /**
793
+ * Read and JSON-parse a matched L1 Response's body under the edgeReadTimeoutMs
794
+ * budget. CF resolves `match()` with a lazily-streamed body, so the latency
795
+ * tail surfaces here -- after matchWithTimeout has already passed -- not in the
796
+ * match itself. On timeout `undefined` is returned so the caller falls through
797
+ * to L2/KV or render.
798
+ * @internal
799
+ */
800
+ private async readJsonWithTimeout<T>(
801
+ response: Response,
802
+ ): Promise<{ value: T | undefined; errored: boolean; error?: unknown }> {
803
+ // A FAST json() rejection (a corrupt body, or a foreign 200 non-JSON
804
+ // response that collided on this key) is caught at the thunk and turned into
805
+ // a miss, so the caller falls through to L2/KV exactly like a body-timeout
806
+ // -- instead of escaping to get()/getItem()'s outer catch, which returns
807
+ // null WITHOUT ever consulting KV. The catch lives here, not in
808
+ // readWithTimeout, so the L2/KV tier keeps propagating a genuine kv.get
809
+ // rejection to its own error sink. The `errored` flag lets the caller emit a
810
+ // distinct "body-error" debug outcome rather than masquerading as a timeout.
811
+ // On a TIMEOUT the json() promise is still pending, so the catch has not
812
+ // fired: errored stays false and the outcome is correctly a body-timeout. A
813
+ // late rejection after the timeout only mutates the closure flag, which the
814
+ // already-returned object no longer reads.
815
+ let errored = false;
816
+ let error: unknown;
817
+ const { value } = await this.readWithTimeout<T | undefined>(
818
+ () =>
819
+ (response.json() as Promise<T>).catch((e) => {
820
+ errored = true;
821
+ error = e;
822
+ return undefined;
823
+ }),
824
+ this.edgeReadTimeoutMs,
825
+ "edge cache body read",
826
+ );
827
+ return { value, errored, error };
828
+ }
829
+
830
+ /**
831
+ * Self-heal a corrupt L1 entry, then return the fall-through result. Reports
832
+ * the corruption as cache-corrupt (so an onError consumer sees it distinctly
833
+ * from a transient outage), runs the caller's L2/KV fall-through, and evicts
834
+ * the faulty per-colo entry ONLY when that fall-through found no good copy.
835
+ *
836
+ * The conditional evict is the load-bearing detail: when KV DOES serve a copy,
837
+ * kvGet* has already scheduled a same-key promote (`cache.put`); an eager
838
+ * `cache.delete` here would race that put with no CF Cache API ordering
839
+ * guarantee and could clobber the freshly-restored entry. So in that case we
840
+ * lean on #558's heal-by-overwrite (the non-suppressed fall-through promotes /
841
+ * a fresh render re-`set`s over the bad entry) and skip the delete. Only when
842
+ * this request's fall-through found no copy (=== null) is the eager evict
843
+ * scheduled -- useful then, since nothing else will overwrite the poison entry.
844
+ * A null fall-through can also be a KV-read TIMEOUT rather than a genuine miss:
845
+ * a concurrent request that read KV successfully may be promoting the same key,
846
+ * and this evict could race it. That is benign -- the worst case is one wasted
847
+ * colo-local promote, never a wrong served value, and the next read self-heals
848
+ * -- so we accept it rather than suppressing the evict on a timeout (which
849
+ * would strand the poison entry when KV really is empty). The evict is
850
+ * non-blocking (waitUntil) so it never adds latency to the degraded read.
851
+ * @internal
852
+ */
853
+ private async healCorruptL1<T>(
854
+ cache: Cache,
855
+ request: Request,
856
+ error: unknown,
857
+ label: string,
858
+ fallThrough: () => Promise<T | null>,
859
+ ): Promise<T | null> {
860
+ reportCacheError(
861
+ error ?? new Error("corrupt/partial L1 body"),
862
+ "cache-corrupt",
863
+ `[CFCacheStore] ${label}: corrupt L1 body`,
864
+ );
865
+ const result = await fallThrough();
866
+ if (result === null) {
867
+ const evict = (): Promise<void> =>
868
+ reportingAsync(
869
+ () => cache.delete(request),
870
+ "cache-delete",
871
+ `[CFCacheStore] ${label}: evict corrupt L1`,
872
+ );
873
+ if (this.waitUntil) this.waitUntil(evict);
874
+ else void evict();
875
+ }
876
+ return result;
877
+ }
878
+
879
+ /**
880
+ * Re-put a stale L1 entry marked REVALIDATING, so concurrent requests serve it
881
+ * without each triggering a revalidation. Shared by get()/getItem().
882
+ *
883
+ * The write is NON-BLOCKING (waitUntil) and best-effort by design:
884
+ * - It runs in waitUntil, so it never adds the put latency to the served stale
885
+ * read and a put failure can never turn that good read into a miss. The put
886
+ * is still initiated synchronously (this.waitUntil invokes its callback
887
+ * immediately), so concurrent readers see the marker land at the same time an
888
+ * awaited write would -- awaiting only blocks the current request.
889
+ * - The background revalidation's fresh set() is gated behind a full re-render,
890
+ * so it lands well after this put; a stale-clobbers-fresh race would require
891
+ * this single put to be slower than that entire render+set, and self-heals
892
+ * within MAX_REVALIDATION_INTERVAL.
893
+ *
894
+ * Cache-Control is recomputed to the REMAINING ttl from the stored hard-expiry
895
+ * deadline (see remainingCacheControl), not copied from the original
896
+ * full-window header -- copying it would restart CF retention on every re-arm
897
+ * and pin a perpetually-failing entry past hard-expiry. A legacy/tampered entry
898
+ * without a valid deadline floors to max-age=1 and self-heals via KV.
899
+ * @internal
900
+ */
901
+ private markRevalidating(
902
+ cache: Cache,
903
+ request: Request,
904
+ sourceHeaders: Headers,
905
+ status: number,
906
+ body: string,
907
+ ): void {
908
+ const reputNow = Date.now();
909
+ const headers = new Headers(sourceHeaders);
910
+ headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
911
+ headers.set(CACHE_REVALIDATING_AT_HEADER, String(reputNow));
912
+ headers.set("Cache-Control", remainingCacheControl(headers, reputNow));
913
+ const markerResponse = new Response(body, { status, headers });
914
+ const write = async (): Promise<void> => {
915
+ try {
916
+ await cache.put(request, markerResponse);
917
+ } catch {
918
+ // Best-effort: a failed marker write must not affect the served read;
919
+ // the entry simply re-arms on the next stale read.
920
+ }
921
+ };
922
+ if (this.waitUntil) this.waitUntil(write);
923
+ else void write();
924
+ }
925
+
926
+ /**
927
+ * Document-tier counterpart of markRevalidating for getResponse's herd guard.
928
+ * The segment/item tiers JSON-parse the body, so they re-put with a string
929
+ * body; document bodies are streamed verbatim, so we re-put with a CLONED
930
+ * response body (`response.clone()`) supplied by the caller -- the original
931
+ * body still streams to the client while the marker carries the clone. Same
932
+ * REVALIDATING status header, same revalidating-at stamp, same
933
+ * remainingCacheControl re-put math as markRevalidating, so the document tier
934
+ * suppresses concurrent revalidation for the identical MAX_REVALIDATION_INTERVAL
935
+ * window the segment tier does. Best-effort and non-blocking: a failed marker
936
+ * write must not affect the served stale read.
937
+ * @internal
938
+ */
939
+ private markResponseRevalidating(
940
+ cache: Cache,
941
+ request: Request,
942
+ clonedResponse: Response,
943
+ ): void {
944
+ const reputNow = Date.now();
945
+ const headers = new Headers(clonedResponse.headers);
946
+ headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
947
+ headers.set(CACHE_REVALIDATING_AT_HEADER, String(reputNow));
948
+ headers.set("Cache-Control", remainingCacheControl(headers, reputNow));
949
+ const markerResponse = new Response(clonedResponse.body, {
950
+ status: clonedResponse.status,
951
+ statusText: clonedResponse.statusText,
952
+ headers,
953
+ });
954
+ const write = async (): Promise<void> => {
955
+ try {
956
+ await cache.put(request, markerResponse);
957
+ } catch {
958
+ // Best-effort: see markRevalidating.
959
+ }
960
+ };
961
+ if (this.waitUntil) this.waitUntil(write);
962
+ else void write();
963
+ }
964
+
965
+ // ============================================================================
966
+ // Segment Cache Methods
967
+ // ============================================================================
968
+
969
+ /**
970
+ * Guard the segment tier against a `keyGenerator` that returns a key colliding
971
+ * with a reserved tag-marker namespace: `__tag__/` (the KV marker key) or
972
+ * `__tagmarker__/` (the L1 Cache API marker request). The item/doc tiers are
973
+ * internally prefixed (`fn:`/`doc:`) so only the bare segment key can collide;
974
+ * a collision would let a segment write clobber - or a segment read/delete
975
+ * evict - a live tag marker, silently breaking invalidation. Report loudly
976
+ * (so a misconfigured keyGenerator surfaces immediately) and treat the segment
977
+ * operation as a miss/no-op rather than corrupting the marker namespace.
978
+ * @internal
979
+ */
980
+ private isReservedSegmentKey(
981
+ key: string,
982
+ category: CacheErrorCategory,
983
+ ): boolean {
984
+ const reserved = key.startsWith(TAG_MARKER_PREFIX)
985
+ ? TAG_MARKER_PREFIX
986
+ : key.startsWith(TAG_MARKER_CACHE_PREFIX)
987
+ ? TAG_MARKER_CACHE_PREFIX
988
+ : null;
989
+ if (!reserved) return false;
990
+ reportCacheError(
991
+ new Error(
992
+ `segment key "${key}" collides with the reserved "${reserved}" ` +
993
+ `tag-marker namespace; the operation is ignored. Fix the store ` +
994
+ `keyGenerator so it does not produce keys with this prefix.`,
995
+ ),
996
+ category,
997
+ "[CFCacheStore] reserved key",
998
+ );
999
+ return true;
1000
+ }
1001
+
222
1002
  /**
223
1003
  * Get cached entry data by key.
224
1004
  *
@@ -227,58 +1007,247 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
227
1007
  * - If already REVALIDATING (and recent), returns shouldRevalidate: false
228
1008
  * - If fresh, returns shouldRevalidate: false
229
1009
  *
230
- * The atomic mark prevents thundering herd - only first request triggers revalidation.
1010
+ * On L1 miss, falls back to KV (L2) if configured.
1011
+ * KV hits are promoted to L1 in the background.
231
1012
  */
232
- async get(key: string): Promise<CacheGetResult | null> {
1013
+ async get(key: string): Promise<CacheGetResult | null | CacheReadError> {
1014
+ if (this.isReservedSegmentKey(key, "cache-read")) return null;
233
1015
  try {
234
1016
  const cache = await this.getCache();
235
1017
  const request = this.keyToRequest(key);
236
- const response = await cache.match(request);
1018
+ const matchStart = Date.now();
1019
+ const {
1020
+ response,
1021
+ timedOut,
1022
+ error: matchError,
1023
+ } = await this.matchWithTimeout(cache, request);
1024
+ const matchMs = Date.now() - matchStart;
237
1025
 
238
1026
  if (!response) {
1027
+ if (matchError) {
1028
+ // A match REJECTION is reported as cache-read and still degrades to
1029
+ // L2/KV -- a real KV value (or KV's own CACHE_READ_ERROR) stands on
1030
+ // its own. But a null KV result (unconfigured namespace, kv-miss,
1031
+ // kv-timeout) is NOT proof of absence under a rejected L1 match:
1032
+ // the only real signal this read produced is the failure, so
1033
+ // surface CACHE_READ_ERROR instead of a replayable miss (the PPR
1034
+ // seeded fallback must render uncached, not substitute the doc
1035
+ // record for a partition the store could not actually read).
1036
+ reportCacheError(
1037
+ matchError,
1038
+ "cache-read",
1039
+ "[CFCacheStore] get L1 match",
1040
+ );
1041
+ if (this.debug)
1042
+ this.emitDebug({
1043
+ op: "get",
1044
+ key,
1045
+ // Distinct from a genuine absence: surface it as match-error so
1046
+ // debug agrees with the cache-read already routed to onError,
1047
+ // instead of masquerading as l1-miss.
1048
+ outcome: "match-error",
1049
+ matchMs,
1050
+ });
1051
+ const kvResult = await this.kvGetSegment(key);
1052
+ return kvResult ?? CACHE_READ_ERROR;
1053
+ }
1054
+ // An abandoned slow match (timedOut) keeps the fail-open latency-budget
1055
+ // policy: degrade to L2/KV, and a KV null stays a miss.
1056
+ if (this.debug)
1057
+ this.emitDebug({
1058
+ op: "get",
1059
+ key,
1060
+ outcome: timedOut ? "match-timeout" : "l1-miss",
1061
+ matchMs,
1062
+ });
1063
+ return this.kvGetSegment(key);
1064
+ }
1065
+
1066
+ // A non-200 entry (a cached error response, or a foreign response that
1067
+ // landed on this key) is not valid segment data; treat it as a miss
1068
+ // rather than JSON-parsing garbage and serving it as a hit.
1069
+ if (response.status !== 200) {
1070
+ if (this.debug)
1071
+ this.emitDebug({
1072
+ op: "get",
1073
+ key,
1074
+ outcome: "non-200",
1075
+ status: response.status,
1076
+ matchMs,
1077
+ });
1078
+ // Degraded fall-through: suppress revalidation so a broken L1 entry hit
1079
+ // concurrently serves KV-stale, not a herd. See kvGetSegment.
1080
+ return this.kvGetSegment(key, { suppressRevalidate: true });
1081
+ }
1082
+
1083
+ // Tag invalidation: an entry whose tags were invalidated after it was
1084
+ // cached is treated as a miss, so the next render re-populates it. We
1085
+ // return null (re-render locally) rather than falling through to KV. In
1086
+ // the common case the L1 entry and its KV twin were written together with
1087
+ // the same taggedAt, so kvGetSegment's own tag check would miss too and a
1088
+ // fall-through is pure cost. The tiers CAN diverge -- another colo may have
1089
+ // already re-rendered and written a fresher KV envelope -- in which case a
1090
+ // fall-through could serve that copy instead of re-rendering here.
1091
+ // Capturing that cross-colo optimization is a deferred follow-up, not a
1092
+ // correctness gap: this colo's next read after its own re-render self-heals.
1093
+ const tagInfo = this.readTagInfo(response.headers);
1094
+ // Measure the marker-resolution tail (memo -> L1 marker cache -> KV) only
1095
+ // when debug is on, so the hot path pays nothing. It is the serial read
1096
+ // that sits between matchMs and bodyReadMs for a tagged entry.
1097
+ const markerStart = this.debug ? Date.now() : 0;
1098
+ const invalidated = await this.isL1Invalidated(
1099
+ tagInfo.tags,
1100
+ tagInfo.taggedAt,
1101
+ response.headers,
1102
+ );
1103
+ const markerMs = this.debug ? Date.now() - markerStart : undefined;
1104
+ if (invalidated) {
1105
+ if (this.debug)
1106
+ this.emitDebug({
1107
+ op: "get",
1108
+ key,
1109
+ outcome: "tag-invalidated",
1110
+ status: response.status,
1111
+ matchMs,
1112
+ markerMs,
1113
+ });
239
1114
  return null;
240
1115
  }
241
1116
 
242
1117
  // Read status headers
243
1118
  const status = response.headers.get(CACHE_STATUS_HEADER);
244
- const age = Number(response.headers.get("age") ?? "0");
245
1119
  const staleAt = Number(
246
1120
  response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
247
1121
  );
1122
+ const revalidatingAt = Number(
1123
+ response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
1124
+ );
248
1125
 
249
- const isStale = staleAt > 0 && Date.now() > staleAt;
1126
+ const now = Date.now();
1127
+ const isStale = staleAt > 0 && now > staleAt;
1128
+ // Recency comes from our explicit revalidating-at stamp, not CF's `Age`
1129
+ // header (see CACHE_REVALIDATING_AT_HEADER). An absent/zero stamp counts
1130
+ // as "not recent" so a dropped revalidation re-arms instead of pinning.
250
1131
  const isRevalidating =
251
- status === "REVALIDATING" && age < MAX_REVALIDATION_INTERVAL;
1132
+ status === "REVALIDATING" &&
1133
+ revalidatingAt > 0 &&
1134
+ now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
1135
+
1136
+ // Single emitter for the post-header L1 outcomes. Undefined (so the event
1137
+ // object is never allocated) when debug is off; the informational-only
1138
+ // `age` header is read lazily inside for the same reason.
1139
+ const debugRead = this.debug
1140
+ ? (
1141
+ outcome: CFCacheReadDebugEvent["outcome"],
1142
+ bodyReadMs: number,
1143
+ shouldRevalidate?: boolean,
1144
+ ) =>
1145
+ this.emitDebug({
1146
+ op: "get",
1147
+ key,
1148
+ outcome,
1149
+ status: response.status,
1150
+ cacheStatus: status,
1151
+ staleAt,
1152
+ revalidatingAt,
1153
+ ageHeader: response.headers.get("age"),
1154
+ isStale,
1155
+ isRevalidating,
1156
+ shouldRevalidate,
1157
+ matchMs,
1158
+ markerMs,
1159
+ bodyReadMs,
1160
+ })
1161
+ : undefined;
252
1162
 
253
1163
  // Case 1: Fresh or already being revalidated - just return data
254
1164
  if (!isStale || isRevalidating) {
255
- const data = (await response.json()) as CachedEntryData;
1165
+ const bodyStart = Date.now();
1166
+ const {
1167
+ value: data,
1168
+ errored,
1169
+ error,
1170
+ } = await this.readJsonWithTimeout<CachedEntryData>(response);
1171
+ const bodyReadMs = Date.now() - bodyStart;
1172
+ if (data === undefined) {
1173
+ debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
1174
+ // A body-ERROR (corrupt/foreign body) self-heals via healCorruptL1:
1175
+ // report cache-corrupt, fall through to L2/KV (which overwrites the
1176
+ // bad entry), and evict only if KV had no good copy to promote. A
1177
+ // body-TIMEOUT is a degraded read of a likely-valid entry: leave it
1178
+ // intact and suppress revalidation so a stalling colo cannot herd.
1179
+ if (errored)
1180
+ return this.healCorruptL1(cache, request, error, "get", () =>
1181
+ this.kvGetSegment(key, { suppressRevalidate: false }),
1182
+ );
1183
+ return this.kvGetSegment(key, { suppressRevalidate: true });
1184
+ }
1185
+ debugRead?.(
1186
+ isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
1187
+ bodyReadMs,
1188
+ false,
1189
+ );
256
1190
  return { data, shouldRevalidate: false };
257
1191
  }
258
1192
 
259
- // Case 2: Stale and needs revalidation - atomically mark REVALIDATING
260
- const [b1, b2] = response.body!.tee();
261
-
262
- const headers = new Headers(response.headers);
263
- headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
1193
+ // Case 2: Stale and needs revalidation.
1194
+ // Read the body under the edge-read budget BEFORE writing the REVALIDATING
1195
+ // marker. CF can resolve match() fast but stall the body stream; the prior
1196
+ // approach teed the stream and awaited cache.put(b1) first, which blocked
1197
+ // on that same stalled stream so the read budget could never fire on a
1198
+ // stale hit. Reading first bounds the stall and lets us skip marking an
1199
+ // entry we could not even read.
1200
+ const bodyStart = Date.now();
1201
+ const {
1202
+ value: data,
1203
+ errored,
1204
+ error,
1205
+ } = await this.readJsonWithTimeout<CachedEntryData>(response);
1206
+ const bodyReadMs = Date.now() - bodyStart;
1207
+ if (data === undefined) {
1208
+ debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
1209
+ // Heal + conditionally evict a body-error, suppress a body-timeout; see
1210
+ // Case 1.
1211
+ if (errored)
1212
+ return this.healCorruptL1(
1213
+ cache,
1214
+ request,
1215
+ error,
1216
+ "get(revalidating)",
1217
+ () => this.kvGetSegment(key, { suppressRevalidate: false }),
1218
+ );
1219
+ return this.kvGetSegment(key, { suppressRevalidate: true });
1220
+ }
264
1221
 
265
- // Blocking write - must complete before returning to prevent race
266
- await cache.put(
1222
+ // Mark REVALIDATING so concurrent requests don't all revalidate, then
1223
+ // return the stale data. The marker write is non-blocking and best-effort
1224
+ // (see markRevalidating) -- it must not add latency to, or fail, the served
1225
+ // stale read.
1226
+ this.markRevalidating(
1227
+ cache,
267
1228
  request,
268
- new Response(b1, { status: response.status, headers }),
1229
+ response.headers,
1230
+ response.status,
1231
+ JSON.stringify(data),
269
1232
  );
270
1233
 
271
- const data = (await new Response(b2).json()) as CachedEntryData;
1234
+ debugRead?.("l1-stale-revalidate", bodyReadMs, true);
272
1235
  return { data, shouldRevalidate: true };
273
1236
  } catch (error) {
274
- console.error("[CFCacheStore] get failed:", error);
275
- return null;
1237
+ // reportCacheError logs and routes to onError (cache-read); the debug
1238
+ // emit is the separate wrangler-tail signal. Keep both observability paths.
1239
+ reportCacheError(error, "cache-read", "[CFCacheStore] get");
1240
+ if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
1241
+ // Distinct from a miss so the PPR replay composition renders uncached
1242
+ // instead of substituting the seeded doc record (CACHE_READ_ERROR).
1243
+ return CACHE_READ_ERROR;
276
1244
  }
277
1245
  }
278
1246
 
279
1247
  /**
280
1248
  * Store entry data with TTL and optional SWR window.
281
1249
  * Uses waitUntil for non-blocking write when available.
1250
+ * When KV is configured, also persists to L2.
282
1251
  */
283
1252
  async set(
284
1253
  key: string,
@@ -286,49 +1255,98 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
286
1255
  ttl: number,
287
1256
  swr?: number,
288
1257
  ): Promise<void> {
1258
+ if (this.isReservedSegmentKey(key, "cache-write")) return;
1259
+ if (this.skipUncacheableTagSet(data.tags)) return;
289
1260
  try {
290
1261
  const cache = await this.getCache();
291
1262
  const request = this.keyToRequest(key);
292
1263
 
293
1264
  // Extended TTL covers SWR window
294
- const swrWindow = swr ?? this.defaults?.swr ?? 0;
1265
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
295
1266
  const totalTtl = ttl + swrWindow;
296
1267
  const staleAt = Date.now() + ttl * 1000;
297
1268
 
298
- const response = new Response(JSON.stringify(data), {
1269
+ // Stamp the tag timestamp at write time and carry it (with the tags)
1270
+ // into both the L1 body and the KV envelope so reads can run the
1271
+ // invalidation check.
1272
+ const taggedAt =
1273
+ Array.isArray(data.tags) && data.tags.length > 0
1274
+ ? Date.now()
1275
+ : undefined;
1276
+ const dataToStore: CachedEntryData = taggedAt
1277
+ ? { ...data, taggedAt }
1278
+ : data;
1279
+
1280
+ // Serialize the segment payload exactly once: L1 stores the JSON body
1281
+ // directly, and kvSetSegment embeds the same string as envelope.d so the
1282
+ // (potentially large) Flight/segment tree is not walked a second time.
1283
+ const body = JSON.stringify(dataToStore);
1284
+ const response = new Response(body, {
299
1285
  headers: {
300
1286
  "Content-Type": "application/json",
301
1287
  "Cache-Control": `public, max-age=${totalTtl}`,
302
1288
  [CACHE_STALE_AT_HEADER]: String(staleAt),
1289
+ // Absolute hard-expiry deadline so a stale-path re-put can recompute a
1290
+ // shrinking max-age instead of restarting retention (see
1291
+ // remainingCacheControl / CACHE_EXPIRES_AT_HEADER).
1292
+ [CACHE_EXPIRES_AT_HEADER]: String(staleAt + swrWindow * 1000),
303
1293
  [CACHE_STATUS_HEADER]: "HIT",
1294
+ ...this.tagHeaderEntries(dataToStore.tags, taggedAt),
304
1295
  },
305
1296
  });
306
1297
 
307
1298
  const putPromise = cache.put(request, response);
308
1299
 
309
1300
  if (this.waitUntil) {
310
- // Non-blocking write
311
- this.waitUntil(async () => {
312
- await putPromise;
313
- });
1301
+ // Non-blocking write. These store-level background tasks intentionally
1302
+ // omit the reportingAsync ctx argument: the store is a request-agnostic
1303
+ // singleton and this.waitUntil is the execution context's, not a single
1304
+ // request's, so a failure is reported console-loud only (it cannot be
1305
+ // attributed to one request's onError). The request-scoped tag verbs
1306
+ // (revalidateTag / stale-revalidation) DO thread their captured ctx.
1307
+ this.waitUntil(() =>
1308
+ reportingAsync(
1309
+ () => putPromise,
1310
+ "cache-write",
1311
+ "[CFCacheStore] L1 write",
1312
+ ),
1313
+ );
314
1314
  } else {
315
1315
  // Blocking fallback
316
1316
  await putPromise;
317
1317
  }
1318
+
1319
+ // L2: persist to KV (reuses `body` as envelope.d)
1320
+ this.kvSetSegment(key, body, staleAt, totalTtl, swrWindow);
318
1321
  } catch (error) {
319
- console.error("[CFCacheStore] set failed:", error);
1322
+ reportCacheError(error, "cache-write", "[CFCacheStore] set");
320
1323
  }
321
1324
  }
322
1325
 
323
1326
  /**
324
- * Delete a cached entry
1327
+ * Delete a cached entry from L1 and L2.
325
1328
  */
326
1329
  async delete(key: string): Promise<boolean> {
1330
+ if (this.isReservedSegmentKey(key, "cache-delete")) return false;
327
1331
  try {
328
1332
  const cache = await this.getCache();
329
- return await cache.delete(this.keyToRequest(key));
1333
+ const result = await cache.delete(this.keyToRequest(key));
1334
+
1335
+ // L2: delete from KV
1336
+ if (this.kv && this.waitUntil) {
1337
+ const kvKey = await this.toKVKey(key);
1338
+ this.waitUntil(() =>
1339
+ reportingAsync(
1340
+ () => this.kv!.delete(kvKey),
1341
+ "cache-delete",
1342
+ "[CFCacheStore] delete L2",
1343
+ ),
1344
+ );
1345
+ }
1346
+
1347
+ return result;
330
1348
  } catch (error) {
331
- console.error("[CFCacheStore] delete failed:", error);
1349
+ reportCacheError(error, "cache-delete", "[CFCacheStore] delete");
332
1350
  return false;
333
1351
  }
334
1352
  }
@@ -340,6 +1358,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
340
1358
  /**
341
1359
  * Get a cached Response by key (for document-level caching).
342
1360
  * Returns the response and whether it should be revalidated (SWR).
1361
+ * Falls back to KV (L2) on L1 miss.
343
1362
  */
344
1363
  async getResponse(
345
1364
  key: string,
@@ -347,50 +1366,185 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
347
1366
  try {
348
1367
  const cache = await this.getCache();
349
1368
  const request = this.keyToRequest(`doc:${key}`);
350
- const response = await cache.match(request);
1369
+ // The document path is outside the debug surface (op is only get/getItem),
1370
+ // so the match-timeout flag is not surfaced as an event here -- though
1371
+ // matchWithTimeout still warns on a slow match. A miss or timeout falls
1372
+ // through to the KV document path and then render.
1373
+ const { response, error: matchError } = await this.matchWithTimeout(
1374
+ cache,
1375
+ request,
1376
+ );
351
1377
 
352
1378
  if (!response || response.status !== 200) {
1379
+ // A transient L1 match rejection (matchError set; only ever set when
1380
+ // response is undefined) is surfaced as cache-read before degrading to
1381
+ // L2/KV -- matching get()/getItem(). A genuine miss or a non-200 hit
1382
+ // carries no matchError and reports nothing.
1383
+ if (matchError)
1384
+ reportCacheError(
1385
+ matchError,
1386
+ "cache-read",
1387
+ "[CFCacheStore] getResponse L1 match",
1388
+ );
1389
+ return this.kvGetResponse(key);
1390
+ }
1391
+
1392
+ // Tag invalidation check (treat invalidated entry as a miss).
1393
+ const tagInfo = this.readTagInfo(response.headers);
1394
+ if (
1395
+ await this.isL1Invalidated(
1396
+ tagInfo.tags,
1397
+ tagInfo.taggedAt,
1398
+ response.headers,
1399
+ )
1400
+ ) {
353
1401
  return null;
354
1402
  }
355
1403
 
356
1404
  // Check staleness
357
1405
  const staleAt = Number(response.headers.get(CACHE_STALE_AT_HEADER) || 0);
358
- const isStale = staleAt > 0 && Date.now() > staleAt;
1406
+ const now = Date.now();
1407
+ const isStale = staleAt > 0 && now > staleAt;
1408
+
1409
+ // Thundering-herd guard, mirroring the segment (get) and item (getItem)
1410
+ // tiers. Without it, every concurrent stale reader returned
1411
+ // shouldRevalidate=true and document-cache.ts scheduled a fresh render for
1412
+ // each one. Recency comes from our own revalidating-at stamp, not CF's Age
1413
+ // header (see CACHE_REVALIDATING_AT_HEADER); an absent/zero stamp counts as
1414
+ // "not recent" so a dropped revalidation re-arms instead of pinning.
1415
+ const status = response.headers.get(CACHE_STATUS_HEADER);
1416
+ const revalidatingAt = Number(
1417
+ response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
1418
+ );
1419
+ const isRevalidating =
1420
+ status === "REVALIDATING" &&
1421
+ revalidatingAt > 0 &&
1422
+ now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
1423
+
1424
+ // L1 document bodies are streamed through verbatim - unlike the segment/
1425
+ // item tiers (which JSON-parse and so structurally detect corruption) and
1426
+ // the KV doc tier (validated in kvGetResponse, KV being the real partial-
1427
+ // read vector). Integrity here relies on the Cache API: cache.put stores a
1428
+ // response atomically or fails, so a truncated body is not served back. We
1429
+ // deliberately do NOT buffer+hash the body to re-verify it: that would
1430
+ // defeat streaming the document and add a full read to every cache hit.
1431
+
1432
+ if (isStale && !isRevalidating) {
1433
+ // First stale reader within the window: mark REVALIDATING (non-blocking,
1434
+ // best-effort) so concurrent readers below see the guard and suppress,
1435
+ // then return shouldRevalidate=true so this caller revalidates. Clone the
1436
+ // matched response for the marker since its original body must still
1437
+ // stream to the client.
1438
+ this.markResponseRevalidating(cache, request, response.clone());
1439
+ return {
1440
+ response: this.toClientResponse(response),
1441
+ shouldRevalidate: true,
1442
+ };
1443
+ }
359
1444
 
1445
+ // Fresh, or stale-but-already-REVALIDATING: serve without scheduling a
1446
+ // (re-)revalidation. A recent marker already has a render in flight.
360
1447
  return {
361
- response,
362
- shouldRevalidate: isStale,
1448
+ response: this.toClientResponse(response),
1449
+ shouldRevalidate: false,
363
1450
  };
364
1451
  } catch (error) {
365
- console.error("[CFCacheStore] getResponse failed:", error);
1452
+ reportCacheError(error, "cache-read", "[CFCacheStore] getResponse");
366
1453
  return null;
367
1454
  }
368
1455
  }
369
1456
 
1457
+ /**
1458
+ * Strip internal edge headers and restore the author's Cache-Control before a
1459
+ * cached document Response is served to a client. L1 entries carry the
1460
+ * internal staleness/status headers and a rewritten Cache-Control; none of
1461
+ * those should reach the browser or an upstream CDN.
1462
+ */
1463
+ private toClientResponse(response: Response): Response {
1464
+ const headers = new Headers(response.headers);
1465
+ const originalCacheControl = headers.get(CACHE_ORIG_CC_HEADER);
1466
+ if (originalCacheControl !== null) {
1467
+ headers.set("Cache-Control", originalCacheControl);
1468
+ } else {
1469
+ headers.delete("Cache-Control");
1470
+ }
1471
+ headers.delete(CACHE_ORIG_CC_HEADER);
1472
+ headers.delete(CACHE_STALE_AT_HEADER);
1473
+ headers.delete(CACHE_STATUS_HEADER);
1474
+ headers.delete(CACHE_TAGS_HEADER);
1475
+ headers.delete(CACHE_TAGGED_AT_HEADER);
1476
+ // Remove OUR namespaced tokens from Cache-Tag while preserving any the
1477
+ // document author set (setTagHeaders appended ours onto theirs). The
1478
+ // author's tags may be load-bearing for their own CDN purging; ours are
1479
+ // internal storage bookkeeping and must not leak to clients.
1480
+ this.stripInternalCacheTags(headers);
1481
+ // Internal stale-path bookkeeping (hard-expiry deadline + REVALIDATING
1482
+ // stamp). Carried on doc L1 entries for the herd guard; never serve them.
1483
+ headers.delete(CACHE_EXPIRES_AT_HEADER);
1484
+ headers.delete(CACHE_REVALIDATING_AT_HEADER);
1485
+ // Finding #3 (read side): strip per-client signals a pre-fix or
1486
+ // pinned-version L1 entry may carry. See the read-side note in the design doc.
1487
+ stripPerClientSignals(headers);
1488
+ return new Response(response.body, {
1489
+ status: response.status,
1490
+ statusText: response.statusText,
1491
+ headers,
1492
+ });
1493
+ }
1494
+
370
1495
  /**
371
1496
  * Store a Response with TTL and optional SWR window (for document-level caching).
1497
+ * When KV is configured, also persists to L2.
372
1498
  */
373
1499
  async putResponse(
374
1500
  key: string,
375
1501
  response: Response,
376
1502
  ttl: number,
377
1503
  swr?: number,
1504
+ tags?: string[],
378
1505
  ): Promise<void> {
1506
+ if (this.skipUncacheableTagSet(tags)) return;
379
1507
  try {
380
1508
  const cache = await this.getCache();
381
1509
  const request = this.keyToRequest(`doc:${key}`);
382
1510
 
383
1511
  // Extended TTL covers SWR window
384
- const swrWindow = swr ?? this.defaults?.swr ?? 0;
1512
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
385
1513
  const totalTtl = ttl + swrWindow;
386
1514
  const staleAt = Date.now() + ttl * 1000;
1515
+ const taggedAt =
1516
+ Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
387
1517
 
388
- // Clone and add cache headers
1518
+ // Clone body for potential KV write before consuming it for L1
1519
+ const [l1Body, kvBody] = this.kv
1520
+ ? response.body
1521
+ ? response.body.tee()
1522
+ : [null, null]
1523
+ : [response.body, null];
1524
+
1525
+ // Clone and add cache headers. The author's Cache-Control is stashed and
1526
+ // replaced with a long max-age so the CF Cache API holds the entry across
1527
+ // the SWR window; getResponse restores the original before serving.
389
1528
  const headers = new Headers(response.headers);
1529
+ // Finding #3: never persist a per-client signal in the shared L1 entry
1530
+ // (the platform's Set-Cookie rejection is unverified and ignores the
1531
+ // directive anyway). See stripPerClientSignals.
1532
+ stripPerClientSignals(headers);
1533
+ const originalCacheControl = response.headers.get("Cache-Control");
1534
+ if (originalCacheControl !== null) {
1535
+ headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
1536
+ }
390
1537
  headers.set("Cache-Control", `public, max-age=${totalTtl}`);
391
1538
  headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
1539
+ // Absolute hard-expiry deadline so a stale-path REVALIDATING re-put can
1540
+ // recompute a shrinking max-age (remainingCacheControl) instead of
1541
+ // restarting retention. Mirrors set()/setItem(). Stripped by
1542
+ // toClientResponse before serving.
1543
+ headers.set(CACHE_EXPIRES_AT_HEADER, String(staleAt + swrWindow * 1000));
1544
+ // Internal tag headers (stripped by toClientResponse before serving).
1545
+ this.setTagHeaders(headers, tags, taggedAt);
392
1546
 
393
- const toCache = new Response(response.body, {
1547
+ const toCache = new Response(l1Body, {
394
1548
  status: response.status,
395
1549
  statusText: response.statusText,
396
1550
  headers,
@@ -400,29 +1554,2120 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
400
1554
 
401
1555
  if (this.waitUntil) {
402
1556
  // Non-blocking write
403
- this.waitUntil(async () => {
404
- await putPromise;
405
- });
1557
+ this.waitUntil(() =>
1558
+ reportingAsync(
1559
+ () => putPromise,
1560
+ "cache-write",
1561
+ "[CFCacheStore] L1 write",
1562
+ ),
1563
+ );
406
1564
  } else {
407
1565
  // Blocking fallback
408
1566
  await putPromise;
409
1567
  }
1568
+
1569
+ // L2: persist to KV (KV requires expirationTtl >= 60s)
1570
+ if (this.kv && this.waitUntil && totalTtl >= 60) {
1571
+ const kvKey = await this.toDocKVKey(key);
1572
+ // Finding #3: never persist a per-client signal in the KV envelope.
1573
+ const headersArray: [string, string][] = [];
1574
+ response.headers.forEach((v, k) => {
1575
+ if (isPerClientSignalHeader(k)) return;
1576
+ headersArray.push([k, v]);
1577
+ });
1578
+ // Read body as ArrayBuffer and encode to base64 to preserve binary payloads
1579
+ const bodyBuf = kvBody
1580
+ ? await new Response(kvBody).arrayBuffer()
1581
+ : new ArrayBuffer(0);
1582
+ const bodyBase64 = bufferToBase64(bodyBuf);
1583
+
1584
+ this.waitUntil(() =>
1585
+ reportingAsync(
1586
+ () => {
1587
+ const envelope: KVResponseEnvelope = {
1588
+ b: bodyBase64,
1589
+ st: response.status,
1590
+ stx: response.statusText,
1591
+ hd: headersArray,
1592
+ s: staleAt,
1593
+ e: staleAt + swrWindow * 1000,
1594
+ t: tags,
1595
+ ta: taggedAt,
1596
+ };
1597
+ return this.kv!.put(kvKey, JSON.stringify(envelope), {
1598
+ expirationTtl: totalTtl,
1599
+ });
1600
+ },
1601
+ "cache-write",
1602
+ "[CFCacheStore] kvPutResponse",
1603
+ ),
1604
+ );
1605
+ }
410
1606
  } catch (error) {
411
- console.error("[CFCacheStore] putResponse failed:", error);
1607
+ reportCacheError(error, "cache-write", "[CFCacheStore] putResponse");
412
1608
  }
413
1609
  }
414
1610
 
1611
+ // ============================================================================
1612
+ // Function Cache Methods (for "use cache" directive)
1613
+ // ============================================================================
1614
+
415
1615
  /**
416
- * Convert string key to Request object for CF Cache API.
417
- * Includes version in URL if specified (for cache invalidation on code changes).
418
- * @internal
1616
+ * Get a cached function result by key.
1617
+ * Follows the same SWR pattern as get() for segment caching.
1618
+ * Falls back to KV (L2) on L1 miss.
419
1619
  */
420
- private keyToRequest(key: string): Request {
421
- const encodedKey = encodeURIComponent(key);
422
- // Include version in URL path to invalidate cache when version changes
423
- const versionPath = this.version ? `v/${this.version}/` : "";
424
- return new Request(`${this.baseUrl}${versionPath}${encodedKey}`, {
425
- method: "GET",
426
- });
1620
+ async getItem(key: string): Promise<CacheItemResult | null> {
1621
+ try {
1622
+ const cache = await this.getCache();
1623
+ const request = this.keyToRequest(`fn:${key}`);
1624
+ const matchStart = Date.now();
1625
+ const {
1626
+ response,
1627
+ timedOut,
1628
+ error: matchError,
1629
+ } = await this.matchWithTimeout(cache, request);
1630
+ const matchMs = Date.now() - matchStart;
1631
+
1632
+ if (!response) {
1633
+ // Transient match error reported cache-read; still degrades to L2/KV.
1634
+ if (matchError)
1635
+ reportCacheError(
1636
+ matchError,
1637
+ "cache-read",
1638
+ "[CFCacheStore] getItem L1 match",
1639
+ );
1640
+ if (this.debug)
1641
+ this.emitDebug({
1642
+ op: "getItem",
1643
+ key,
1644
+ // match-error (rejection) vs l1-miss (absence); see get().
1645
+ outcome: matchError
1646
+ ? "match-error"
1647
+ : timedOut
1648
+ ? "match-timeout"
1649
+ : "l1-miss",
1650
+ matchMs,
1651
+ });
1652
+ return this.kvGetItem(key);
1653
+ }
1654
+
1655
+ // Non-200 entry is not a valid cached function result; treat as a miss.
1656
+ if (response.status !== 200) {
1657
+ if (this.debug)
1658
+ this.emitDebug({
1659
+ op: "getItem",
1660
+ key,
1661
+ outcome: "non-200",
1662
+ status: response.status,
1663
+ matchMs,
1664
+ });
1665
+ // Degraded fall-through: suppress revalidation so a broken L1 entry hit
1666
+ // concurrently serves KV-stale instead of spawning a herd (see get()).
1667
+ return this.kvGetItem(key, { suppressRevalidate: true });
1668
+ }
1669
+
1670
+ // Tag invalidation check (treat invalidated entry as a miss). Measure the
1671
+ // marker-resolution tail only under debug (see get()).
1672
+ const tagInfo = this.readTagInfo(response.headers);
1673
+ const markerStart = this.debug ? Date.now() : 0;
1674
+ const invalidated = await this.isL1Invalidated(
1675
+ tagInfo.tags,
1676
+ tagInfo.taggedAt,
1677
+ response.headers,
1678
+ );
1679
+ const markerMs = this.debug ? Date.now() - markerStart : undefined;
1680
+ if (invalidated) {
1681
+ if (this.debug)
1682
+ this.emitDebug({
1683
+ op: "getItem",
1684
+ key,
1685
+ outcome: "tag-invalidated",
1686
+ status: response.status,
1687
+ matchMs,
1688
+ markerMs,
1689
+ });
1690
+ return null;
1691
+ }
1692
+
1693
+ const staleAt = Number(
1694
+ response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
1695
+ );
1696
+ const status = response.headers.get(CACHE_STATUS_HEADER);
1697
+ const revalidatingAt = Number(
1698
+ response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
1699
+ );
1700
+
1701
+ const now = Date.now();
1702
+ const isStale = staleAt > 0 && now > staleAt;
1703
+ // Recency from our explicit stamp, not CF's `Age` header (see get()).
1704
+ const isRevalidating =
1705
+ status === "REVALIDATING" &&
1706
+ revalidatingAt > 0 &&
1707
+ now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
1708
+
1709
+ // Single emitter for the post-header L1 outcomes (see get()). Undefined
1710
+ // when debug is off, so the event object is never allocated on the hot
1711
+ // path; the informational-only `age` header is read lazily inside.
1712
+ const debugRead = this.debug
1713
+ ? (
1714
+ outcome: CFCacheReadDebugEvent["outcome"],
1715
+ bodyReadMs: number,
1716
+ shouldRevalidate?: boolean,
1717
+ ) =>
1718
+ this.emitDebug({
1719
+ op: "getItem",
1720
+ key,
1721
+ outcome,
1722
+ status: response.status,
1723
+ cacheStatus: status,
1724
+ staleAt,
1725
+ revalidatingAt,
1726
+ ageHeader: response.headers.get("age"),
1727
+ isStale,
1728
+ isRevalidating,
1729
+ shouldRevalidate,
1730
+ matchMs,
1731
+ markerMs,
1732
+ bodyReadMs,
1733
+ })
1734
+ : undefined;
1735
+
1736
+ const bodyStart = Date.now();
1737
+ const {
1738
+ value: data,
1739
+ errored,
1740
+ error,
1741
+ } = await this.readJsonWithTimeout<{
1742
+ value: string;
1743
+ handles?: string;
1744
+ }>(response);
1745
+ const bodyReadMs = Date.now() - bodyStart;
1746
+ if (data === undefined) {
1747
+ debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
1748
+ // Heal + conditionally evict a body-error, suppress a body-timeout; see
1749
+ // get().
1750
+ if (errored)
1751
+ return this.healCorruptL1(cache, request, error, "getItem", () =>
1752
+ this.kvGetItem(key, { suppressRevalidate: false }),
1753
+ );
1754
+ return this.kvGetItem(key, { suppressRevalidate: true });
1755
+ }
1756
+
1757
+ if (!isStale || isRevalidating) {
1758
+ debugRead?.(
1759
+ isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
1760
+ bodyReadMs,
1761
+ false,
1762
+ );
1763
+ return {
1764
+ value: data.value,
1765
+ handles: data.handles,
1766
+ shouldRevalidate: false,
1767
+ tags: tagInfo.tags,
1768
+ };
1769
+ }
1770
+
1771
+ // Stale and needs revalidation -- mark REVALIDATING (non-blocking,
1772
+ // best-effort, remaining-ttl) and return the stale value. See get() /
1773
+ // markRevalidating for the full rationale.
1774
+ this.markRevalidating(
1775
+ cache,
1776
+ request,
1777
+ response.headers,
1778
+ 200,
1779
+ JSON.stringify(data),
1780
+ );
1781
+
1782
+ debugRead?.("l1-stale-revalidate", bodyReadMs, true);
1783
+ return {
1784
+ value: data.value,
1785
+ handles: data.handles,
1786
+ shouldRevalidate: true,
1787
+ tags: tagInfo.tags,
1788
+ };
1789
+ } catch (error) {
1790
+ reportCacheError(error, "cache-read", "[CFCacheStore] getItem");
1791
+ if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
1792
+ return null;
1793
+ }
1794
+ }
1795
+
1796
+ /**
1797
+ * Store a function result with TTL and optional SWR window.
1798
+ * When KV is configured, also persists to L2.
1799
+ */
1800
+ async setItem(
1801
+ key: string,
1802
+ value: string,
1803
+ options?: CacheItemOptions,
1804
+ ): Promise<void> {
1805
+ if (this.skipUncacheableTagSet(options?.tags)) return;
1806
+ try {
1807
+ const cache = await this.getCache();
1808
+ const request = this.keyToRequest(`fn:${key}`);
1809
+
1810
+ const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
1811
+ const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
1812
+ const totalTtl = ttl + swrWindow;
1813
+ const staleAt = Date.now() + ttl * 1000;
1814
+
1815
+ const tags = options?.tags;
1816
+ const taggedAt =
1817
+ Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
1818
+
1819
+ // Serialize value/handles once; L1 body and KV envelope.v/h share the
1820
+ // escaped strings so a large RSC payload is not re-escaped for L2.
1821
+ const valueJson = JSON.stringify(value);
1822
+ const handlesJson =
1823
+ options?.handles !== undefined
1824
+ ? JSON.stringify(options.handles)
1825
+ : undefined;
1826
+ const body =
1827
+ handlesJson !== undefined
1828
+ ? `{"value":${valueJson},"handles":${handlesJson}}`
1829
+ : `{"value":${valueJson}}`;
1830
+ const response = new Response(body, {
1831
+ headers: {
1832
+ "Content-Type": "application/json",
1833
+ "Cache-Control": `public, max-age=${totalTtl}`,
1834
+ [CACHE_STALE_AT_HEADER]: String(staleAt),
1835
+ // Absolute hard-expiry deadline; see set() / remainingCacheControl.
1836
+ [CACHE_EXPIRES_AT_HEADER]: String(staleAt + swrWindow * 1000),
1837
+ [CACHE_STATUS_HEADER]: "HIT",
1838
+ ...this.tagHeaderEntries(tags, taggedAt),
1839
+ },
1840
+ });
1841
+
1842
+ const putPromise = cache.put(request, response);
1843
+
1844
+ if (this.waitUntil) {
1845
+ this.waitUntil(() =>
1846
+ reportingAsync(
1847
+ () => putPromise,
1848
+ "cache-write",
1849
+ "[CFCacheStore] L1 write",
1850
+ ),
1851
+ );
1852
+ } else {
1853
+ await putPromise;
1854
+ }
1855
+
1856
+ // L2: persist to KV (KV requires expirationTtl >= 60s). Wire shape matches
1857
+ // JSON.stringify(KVItemEnvelope); field names differ from L1 so we assemble
1858
+ // from the pre-escaped value/handles pieces rather than re-stringifying.
1859
+ if (this.kv && this.waitUntil && totalTtl >= 60) {
1860
+ const kvKey = await this.toKVKey(`fn:${key}`);
1861
+ const expiresAt = staleAt + swrWindow * 1000;
1862
+ let envelopeJson = `{"v":${valueJson}`;
1863
+ if (handlesJson !== undefined) envelopeJson += `,"h":${handlesJson}`;
1864
+ envelopeJson += `,"s":${staleAt},"e":${expiresAt}`;
1865
+ if (tags !== undefined) envelopeJson += `,"t":${JSON.stringify(tags)}`;
1866
+ if (taggedAt !== undefined) envelopeJson += `,"ta":${taggedAt}`;
1867
+ envelopeJson += `}`;
1868
+ this.waitUntil(() =>
1869
+ reportingAsync(
1870
+ () =>
1871
+ this.kv!.put(kvKey, envelopeJson, {
1872
+ expirationTtl: totalTtl,
1873
+ }),
1874
+ "cache-write",
1875
+ "[CFCacheStore] kvSetItem",
1876
+ ),
1877
+ );
1878
+ }
1879
+ } catch (error) {
1880
+ reportCacheError(error, "cache-write", "[CFCacheStore] setItem");
1881
+ }
1882
+ }
1883
+
1884
+ // ============================================================================
1885
+ // Shell Cache Methods (PPR shell resume) — Cache API L1 + optional KV L2
1886
+ // ============================================================================
1887
+ //
1888
+ // With KV it is the durable, cross-colo shell tier: writes populate both
1889
+ // tiers, a valid KV hit promotes the same coupled envelope into L1, and
1890
+ // shell L1 hits deliberately keep the KV generation-marker check even in
1891
+ // purge mode. A shell's taggedAt is its CAPTURE START, not its write time:
1892
+ // an invalidation can purge while an older capture is still running, then
1893
+ // that capture can land after the purge. The marker check rejects that
1894
+ // resurrection.
1895
+ //
1896
+ // Without KV the family is L1-only (edge-only ppr): every colo captures and
1897
+ // serves its own shell from the Cache API. Tag eviction then needs purge
1898
+ // mode (tagPurge) — shell L1 entries carry the same namespaced Cache-Tag
1899
+ // tokens as the data families, read-your-own-writes comes from the
1900
+ // per-request marker memo, and the capture resurrection race narrows to
1901
+ // cross-request timing bounded by ttl+swr (the data families' documented
1902
+ // purge-mode stance). KV-less WITHOUT tagPurge still caches: freshness is
1903
+ // ttl/swr only, and a tagged write warns once that invalidation cannot
1904
+ // reach it (see warnShellTagsNoEvictionOnce).
1905
+
1906
+ /**
1907
+ * Warn once per isolate that a TAGGED shell landed on a store with no
1908
+ * eviction path for it: no KV (markers) and no tagPurge (purge-by-tag).
1909
+ * The shell still caches and expires by ttl+swr, but updateTag()/
1910
+ * revalidateTag() cannot reach it — silent staleness a consumer who tagged
1911
+ * the route clearly did not intend. Fired from putShell (not the
1912
+ * constructor) so an untagged edge-only ppr config stays warning-free.
1913
+ * @internal
1914
+ */
1915
+ private warnShellTagsNoEvictionOnce(): void {
1916
+ this.warnOncePerNamespace(
1917
+ warnedShellTagsNoEviction,
1918
+ `[CFCacheStore] a ppr shell with tags was stored on a KV-less store ` +
1919
+ `without tagPurge: tag invalidation cannot evict it (no KV markers, ` +
1920
+ `no purge-by-tag), so updateTag()/revalidateTag() will not reach ` +
1921
+ `this shell — it serves until ttl+swr expiry. Configure { kv } for ` +
1922
+ `marker invalidation or { tagPurge } for purge-by-tag eviction; ` +
1923
+ `untagged ppr routes (ttl/swr freshness) are unaffected.`,
1924
+ );
1925
+ }
1926
+
1927
+ /**
1928
+ * Generation gate for shell writes and the capture scheduler
1929
+ * (isTagsInvalidatedSince). With KV it is the durable marker cascade.
1930
+ * Without KV there are no markers: in purge mode the per-request memo is
1931
+ * the only signal — a capture racing THIS request's updateTag() is still
1932
+ * rejected (read-your-own-writes), while cross-request races are bounded
1933
+ * by ttl+swr exactly like the data families' purge-mode writes. Without
1934
+ * either, fail open (ttl/swr-only semantics, warned at putShell).
1935
+ * @internal
1936
+ */
1937
+ private async isShellGenerationInvalidated(
1938
+ tags: string[] | undefined,
1939
+ since: number | undefined,
1940
+ ): Promise<boolean> {
1941
+ if (this.kv) return this.isGloballyInvalidated(tags, since);
1942
+ if (!this.tagPurge || !Array.isArray(tags) || tags.length === 0 || !since)
1943
+ return false;
1944
+ const ctx = _getRequestContext();
1945
+ if (!ctx) return false;
1946
+ const memo = getTagMarkerMemo(ctx, this);
1947
+ for (const tag of tags) {
1948
+ const marker = memo.get(tag);
1949
+ if (marker != null && marker >= since) return true;
1950
+ }
1951
+ return false;
1952
+ }
1953
+
1954
+ /**
1955
+ * Get a cached PPR shell entry from Cache API, falling through to KV and
1956
+ * promoting a valid KV hit. Both tiers store one envelope so the prelude,
1957
+ * postponed state, snapshot, versions, and generation metadata cannot mix.
1958
+ * SWR remains a plain staleness flag; the capture scheduler's module-level
1959
+ * in-flight set is the recapture stampede guard.
1960
+ */
1961
+ async getShell(
1962
+ key: string,
1963
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
1964
+ try {
1965
+ const cache = await this.getCache();
1966
+ const request = this.keyToRequest(`shell:${key}`);
1967
+ const matchStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
1968
+ const {
1969
+ response,
1970
+ timedOut,
1971
+ error: matchError,
1972
+ } = await this.matchWithTimeout(cache, request);
1973
+ const matchMs = INTERNAL_RANGO_DEBUG
1974
+ ? Date.now() - matchStartedAt
1975
+ : undefined;
1976
+
1977
+ if (!response) {
1978
+ if (matchError) {
1979
+ reportCacheError(
1980
+ matchError,
1981
+ "cache-read",
1982
+ "[CFCacheStore] getShell L1 match",
1983
+ );
1984
+ }
1985
+ this.debugShell(key, "l1-miss", {
1986
+ reason: matchError ? "error" : timedOut ? "timeout" : "absent",
1987
+ matchMs,
1988
+ });
1989
+ return this.kvGetShell(key);
1990
+ }
1991
+ if (response.status !== 200) {
1992
+ this.debugShell(key, "l1-miss", {
1993
+ reason: "non-200",
1994
+ status: response.status,
1995
+ matchMs,
1996
+ });
1997
+ return this.kvGetShell(key);
1998
+ }
1999
+
2000
+ const bodyStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
2001
+ const { value, errored, error } =
2002
+ await this.readJsonWithTimeout<unknown>(response);
2003
+ const bodyReadMs = INTERNAL_RANGO_DEBUG
2004
+ ? Date.now() - bodyStartedAt
2005
+ : undefined;
2006
+ if (value === undefined) {
2007
+ this.debugShell(key, "l1-miss", {
2008
+ reason: errored ? "corrupt" : "timeout",
2009
+ matchMs,
2010
+ bodyReadMs,
2011
+ });
2012
+ if (errored) {
2013
+ return this.healCorruptL1(cache, request, error, "getShell", () =>
2014
+ this.kvGetShell(key),
2015
+ );
2016
+ }
2017
+ return this.kvGetShell(key);
2018
+ }
2019
+ if (!isShellEnvelope(value)) {
2020
+ this.debugShell(key, "l1-miss", {
2021
+ reason: "malformed",
2022
+ matchMs,
2023
+ bodyReadMs,
2024
+ });
2025
+ return this.healCorruptL1(
2026
+ cache,
2027
+ request,
2028
+ new Error("malformed/partial L1 shell envelope"),
2029
+ "getShell",
2030
+ () => this.kvGetShell(key),
2031
+ );
2032
+ }
2033
+
2034
+ const now = Date.now();
2035
+ if (now > value.e) {
2036
+ this.debugShell(key, "l1-miss", {
2037
+ reason: "expired",
2038
+ matchMs,
2039
+ bodyReadMs,
2040
+ expiresAt: value.e,
2041
+ });
2042
+ return this.kvGetShell(key);
2043
+ }
2044
+
2045
+ // Unlike other L1 families, shells with KV always check the durable
2046
+ // generation marker — see the capture-start/purge race documented
2047
+ // above. Without KV there are no markers: L1-only shells adopt the
2048
+ // data families' purge-mode read semantics (a hit that survived the
2049
+ // purge is trusted; the per-request memo masks this request's own
2050
+ // updateTag() writes; entries a purge cannot reach fall back to the
2051
+ // marker check, which fails open KV-less).
2052
+ const markerStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
2053
+ const invalidated = this.kv
2054
+ ? await this.isGloballyInvalidated(value.t, value.ta)
2055
+ : await this.isL1Invalidated(value.t, value.ta, response.headers);
2056
+ const markerMs = INTERNAL_RANGO_DEBUG
2057
+ ? Date.now() - markerStartedAt
2058
+ : undefined;
2059
+ if (invalidated) {
2060
+ this.debugShell(key, "marker-invalidated", {
2061
+ tier: "l1",
2062
+ matchMs,
2063
+ bodyReadMs,
2064
+ markerMs,
2065
+ });
2066
+ return null;
2067
+ }
2068
+
2069
+ const shouldRevalidate = value.s > 0 && now > value.s;
2070
+ this.debugShell(key, "l1-hit", {
2071
+ freshness: shouldRevalidate ? "stale" : "fresh",
2072
+ matchMs,
2073
+ bodyReadMs,
2074
+ markerMs,
2075
+ expiresAt: value.e,
2076
+ });
2077
+ return {
2078
+ entry: this.shellEnvelopeToEntry(value),
2079
+ shouldRevalidate,
2080
+ };
2081
+ } catch (error) {
2082
+ reportCacheError(error, "cache-read", "[CFCacheStore] getShell");
2083
+ this.debugShell(key, "l1-miss", { reason: "error" });
2084
+ return this.kvGetShell(key);
2085
+ }
2086
+ }
2087
+
2088
+ /**
2089
+ * Store a PPR shell envelope in Cache API and, when KV is configured and
2090
+ * the retention meets its 60-second floor, KV. The shared write is
2091
+ * registered with waitUntil and awaited so invalidation rejection can be
2092
+ * acknowledged to the capture scheduler. Short-lived shells remain useful
2093
+ * in L1 even though KV rejects them; a KV-less store is L1-only by design
2094
+ * (edge-only ppr — see the section comment).
2095
+ */
2096
+ async putShell(
2097
+ key: string,
2098
+ entry: ShellCacheEntry,
2099
+ ttlSeconds?: number,
2100
+ swrSeconds?: number,
2101
+ tags?: string[],
2102
+ ): Promise<"stored" | "invalidated" | "uncacheable" | void> {
2103
+ if (!this.waitUntil) return;
2104
+ // Same write gate as the data families: in KV-less purge mode an
2105
+ // over-limit tag set has NO eviction path (no tokens, no markers), so the
2106
+ // shell is not cached rather than becoming un-invalidatable. Unlike the
2107
+ // void-returning data puts, this is ACKNOWLEDGED — the capture scheduler
2108
+ // must back the key off (every write would refuse identically), not
2109
+ // treat the capture as stored and re-render on every MISS.
2110
+ if (this.skipUncacheableTagSet(tags)) return "uncacheable";
2111
+ if (!this.kv && !this.tagPurge && Array.isArray(tags) && tags.length > 0) {
2112
+ this.warnShellTagsNoEvictionOnce();
2113
+ }
2114
+ try {
2115
+ const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
2116
+ const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
2117
+ const totalTtl = ttl + swrWindow;
2118
+
2119
+ // The tagInvalidationTtl cap exists so a tagged entry can never outlive
2120
+ // its KV markers (an expired marker would resurrect it). Without KV
2121
+ // there are no markers to outlive — capping would just hard-expire the
2122
+ // shell below its declared ttl+swr — so the cap is KV-conditional.
2123
+ const retentionTtl =
2124
+ tags && tags.length > 0 && this.kv && this.tagInvalidationTtl
2125
+ ? Math.min(totalTtl, this.tagInvalidationTtl)
2126
+ : totalTtl;
2127
+ const now = Date.now();
2128
+ const staleAt = now + ttl * 1000;
2129
+ const expiresAt = now + retentionTtl * 1000;
2130
+ const taggedAt =
2131
+ Array.isArray(tags) && tags.length > 0 ? entry.createdAt : undefined;
2132
+
2133
+ const writeKv = !!this.kv && retentionTtl >= KV_MIN_EXPIRATION_TTL;
2134
+ const kvKey = writeKv ? await this.toKVKey(`shell:${key}`) : null;
2135
+
2136
+ const write = (async (): Promise<"stored" | "invalidated" | void> => {
2137
+ if (
2138
+ tags &&
2139
+ tags.length > 0 &&
2140
+ (await this.isShellGenerationInvalidated(tags, entry.createdAt))
2141
+ ) {
2142
+ this.debugShell(key, "write-invalidated");
2143
+ return "invalidated";
2144
+ }
2145
+
2146
+ const envelope: CFShellEnvelope = {
2147
+ // Presence-keyed: a navigationOnly entry has no document half, and an
2148
+ // omitted key (vs an explicit undefined) also keeps it out of the
2149
+ // serialized JSON.
2150
+ ...(entry.prelude !== undefined ? { p: entry.prelude } : {}),
2151
+ ...(entry.postponed !== undefined ? { po: entry.postponed } : {}),
2152
+ rv: entry.reactVersion,
2153
+ bv: entry.buildVersion,
2154
+ c: entry.createdAt,
2155
+ s: staleAt,
2156
+ e: expiresAt,
2157
+ t: tags,
2158
+ ta: taggedAt,
2159
+ i: entry.initialTheme,
2160
+ sn: entry.snapshot,
2161
+ dk: entry.docKey,
2162
+ lh: entry.handlerLiveHoles,
2163
+ tw: entry.transitionWhen,
2164
+ no: entry.navigationOnly,
2165
+ };
2166
+ const body = JSON.stringify(envelope);
2167
+ const writes: Promise<boolean>[] = [
2168
+ (async () => {
2169
+ try {
2170
+ const cache = await this.getCache();
2171
+ await cache.put(
2172
+ this.keyToRequest(`shell:${key}`),
2173
+ this.shellEnvelopeResponse(body, envelope),
2174
+ );
2175
+ this.debugShell(key, "l1-stored", {
2176
+ expiresAt: envelope.e,
2177
+ });
2178
+ return true;
2179
+ } catch (error) {
2180
+ reportCacheError(
2181
+ error,
2182
+ "cache-write",
2183
+ "[CFCacheStore] putShell L1",
2184
+ );
2185
+ return false;
2186
+ }
2187
+ })(),
2188
+ ];
2189
+ if (writeKv && kvKey !== null) {
2190
+ writes.push(
2191
+ (async () => {
2192
+ try {
2193
+ await this.kv!.put(kvKey, body, {
2194
+ expirationTtl: retentionTtl,
2195
+ });
2196
+ this.debugShell(key, "kv-stored", {
2197
+ expiresAt: envelope.e,
2198
+ });
2199
+ return true;
2200
+ } catch (error) {
2201
+ reportCacheError(
2202
+ error,
2203
+ "cache-write",
2204
+ "[CFCacheStore] putShell L2",
2205
+ );
2206
+ return false;
2207
+ }
2208
+ })(),
2209
+ );
2210
+ }
2211
+ return (await Promise.all(writes)).some(Boolean) ? "stored" : undefined;
2212
+ })();
2213
+ this.waitUntil(async () => {
2214
+ await write;
2215
+ });
2216
+ return await write;
2217
+ } catch (error) {
2218
+ reportCacheError(error, "cache-write", "[CFCacheStore] putShell");
2219
+ }
2220
+ }
2221
+
2222
+ /** Rebuild the public shell entry from its validated storage envelope. */
2223
+ private shellEnvelopeToEntry(envelope: CFShellEnvelope): ShellCacheEntry {
2224
+ return {
2225
+ prelude: envelope.p,
2226
+ postponed: envelope.po,
2227
+ reactVersion: envelope.rv,
2228
+ buildVersion: envelope.bv,
2229
+ initialTheme: envelope.i,
2230
+ snapshot: envelope.sn,
2231
+ docKey: envelope.dk,
2232
+ handlerLiveHoles: envelope.lh,
2233
+ transitionWhen: envelope.tw,
2234
+ navigationOnly: envelope.no,
2235
+ createdAt: envelope.c,
2236
+ };
2237
+ }
2238
+
2239
+ /** Build the Cache API representation of the coupled shell envelope. */
2240
+ private shellEnvelopeResponse(
2241
+ body: string,
2242
+ envelope: CFShellEnvelope,
2243
+ ): Response {
2244
+ const remainingTtl = Math.max(
2245
+ 1,
2246
+ Math.floor((envelope.e - Date.now()) / 1000),
2247
+ );
2248
+ return new Response(body, {
2249
+ headers: {
2250
+ "Content-Type": "application/json",
2251
+ "Cache-Control": `public, max-age=${remainingTtl}`,
2252
+ [CACHE_STALE_AT_HEADER]: String(envelope.s),
2253
+ [CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
2254
+ [CACHE_STATUS_HEADER]: "HIT",
2255
+ ...this.tagHeaderEntries(envelope.t, envelope.ta),
2256
+ },
2257
+ });
2258
+ }
2259
+
2260
+ /** KV shell fallback with corruption checks and background L1 promotion. */
2261
+ private async kvGetShell(
2262
+ key: string,
2263
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
2264
+ if (!this.kv) return null;
2265
+ try {
2266
+ const readStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
2267
+ const kvKey = await this.toKVKey(`shell:${key}`);
2268
+ const { value: envelope, timedOut } =
2269
+ await this.kvGetOrEvict<CFShellEnvelope>(
2270
+ kvKey,
2271
+ isShellEnvelope,
2272
+ "getShell",
2273
+ );
2274
+ const readMs = INTERNAL_RANGO_DEBUG
2275
+ ? Date.now() - readStartedAt
2276
+ : undefined;
2277
+ if (timedOut || !envelope) {
2278
+ this.debugShell(key, "kv-miss", {
2279
+ reason: timedOut ? "timeout" : "unavailable",
2280
+ readMs,
2281
+ });
2282
+ return null;
2283
+ }
2284
+
2285
+ const now = Date.now();
2286
+ if (now > envelope.e) {
2287
+ this.debugShell(key, "kv-miss", {
2288
+ reason: "expired",
2289
+ readMs,
2290
+ expiresAt: envelope.e,
2291
+ });
2292
+ return null;
2293
+ }
2294
+ const markerStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
2295
+ const invalidated = await this.isGloballyInvalidated(
2296
+ envelope.t,
2297
+ envelope.ta,
2298
+ );
2299
+ const markerMs = INTERNAL_RANGO_DEBUG
2300
+ ? Date.now() - markerStartedAt
2301
+ : undefined;
2302
+ if (invalidated) {
2303
+ this.debugShell(key, "marker-invalidated", {
2304
+ tier: "kv",
2305
+ readMs,
2306
+ markerMs,
2307
+ });
2308
+ return null;
2309
+ }
2310
+
2311
+ const shouldRevalidate = envelope.s > 0 && now > envelope.s;
2312
+ this.debugShell(key, "kv-hit", {
2313
+ freshness: shouldRevalidate ? "stale" : "fresh",
2314
+ readMs,
2315
+ markerMs,
2316
+ expiresAt: envelope.e,
2317
+ });
2318
+ this.promoteShellToL1(key, envelope);
2319
+ return {
2320
+ entry: this.shellEnvelopeToEntry(envelope),
2321
+ shouldRevalidate,
2322
+ };
2323
+ } catch (error) {
2324
+ reportCacheError(error, "cache-read", "[CFCacheStore] kvGetShell");
2325
+ this.debugShell(key, "kv-miss", { reason: "error" });
2326
+ return null;
2327
+ }
2328
+ }
2329
+
2330
+ /** Promote a valid KV shell into the per-colo Cache API tier. */
2331
+ private promoteShellToL1(key: string, envelope: CFShellEnvelope): void {
2332
+ if (!this.waitUntil) return;
2333
+ this.waitUntil(() =>
2334
+ reportingAsync(
2335
+ async () => {
2336
+ if (Date.now() > envelope.e) return;
2337
+ const cache = await this.getCache();
2338
+ const body = JSON.stringify(envelope);
2339
+ await cache.put(
2340
+ this.keyToRequest(`shell:${key}`),
2341
+ this.shellEnvelopeResponse(body, envelope),
2342
+ );
2343
+ this.debugShell(key, "kv-promoted", {
2344
+ remainingTtl: Math.max(
2345
+ 1,
2346
+ Math.floor((envelope.e - Date.now()) / 1000),
2347
+ ),
2348
+ expiresAt: envelope.e,
2349
+ });
2350
+ },
2351
+ "cache-write",
2352
+ "[CFCacheStore] promoteShellToL1",
2353
+ ),
2354
+ );
2355
+ }
2356
+
2357
+ // ============================================================================
2358
+ // Key Helpers
2359
+ // ============================================================================
2360
+
2361
+ /**
2362
+ * Convert string key to Request object for CF Cache API.
2363
+ * Includes version in URL if specified (for cache invalidation on code changes).
2364
+ * @internal
2365
+ */
2366
+ private keyToRequest(key: string): Request {
2367
+ const encodedKey = encodeURIComponent(key);
2368
+ // Include version in URL path to invalidate cache when version changes
2369
+ const versionPath = this.version ? `v/${this.version}/` : "";
2370
+ return new Request(`${this.resolveBaseUrl()}${versionPath}${encodedKey}`, {
2371
+ method: "GET",
2372
+ });
2373
+ }
2374
+
2375
+ /**
2376
+ * Convert string key to KV key string.
2377
+ * Uses same version prefix as Cache API for consistent invalidation.
2378
+ *
2379
+ * Single chokepoint for EVERY KV family (segments, items, shells, documents
2380
+ * via toDocKVKey, tag markers via tagMarkerKey): a composed key over
2381
+ * Cloudflare KV's 512-byte limit is normalized to a preserved readable
2382
+ * prefix plus a SHA-256-derived 128-bit digest of the FULL key. Without
2383
+ * this, kv.put/get reject with `414 ... exceeds key length limit of 512`
2384
+ * and the entry silently never reaches L2 (observed in production for
2385
+ * "use cache" items whose serialized args — e.g. a CMS query object — blow
2386
+ * the cap; autobarn pilot). Keys are opaque storage identifiers, so
2387
+ * normalization is semantics-preserving as long as distinct logical keys
2388
+ * stay distinct: colliding requires an identical 400-byte prefix AND a
2389
+ * 128-bit SHA-256 collision. Deterministic, so every family's read, write,
2390
+ * and delete paths agree on the stored key.
2391
+ * @internal
2392
+ */
2393
+ private async toKVKey(key: string): Promise<string> {
2394
+ const versionPath = this.version ? `v/${this.version}/` : "";
2395
+ const composed = `${versionPath}${key}`;
2396
+ if (kvKeyByteLength(composed) <= KV_MAX_KEY_BYTES) return composed;
2397
+ const prefix = truncateToBytes(composed, KV_KEY_PRESERVED_PREFIX_BYTES);
2398
+ return `${prefix}~${await kvKeyDigest(composed)}`;
2399
+ }
2400
+
2401
+ /**
2402
+ * Host token for the current request, used to namespace the document KV key.
2403
+ * Derived from the same resolveBaseUrl() that namespaces the L1 (Cache API)
2404
+ * tier, so a doc entry's KV twin lands under the identical host bucket.
2405
+ * Falls back to "_" if the base URL cannot be parsed (it always carries a
2406
+ * trailing-slash origin, so parsing succeeds in practice).
2407
+ * @internal
2408
+ */
2409
+ private docKVHost(): string {
2410
+ try {
2411
+ return new URL(this.resolveBaseUrl()).host || "_";
2412
+ } catch {
2413
+ return "_";
2414
+ }
2415
+ }
2416
+
2417
+ /**
2418
+ * Convert a document key to its host-namespaced KV key. The L1 tier already
2419
+ * namespaces document entries by host via keyToRequest/resolveBaseUrl, but the
2420
+ * KV fallback keyed only on `doc:{key}`, so two hosts serving the same path
2421
+ * could collide on the KV tier (one host serving another's cached document).
2422
+ * Prefixing the host closes that cross-host collision. Deterministic per
2423
+ * (host, key). Segment/fn/tag-marker KV keys keep toKVKey unchanged: tag
2424
+ * markers are intentionally global (invalidation must cross hosts), and the
2425
+ * document tier is the one with a request-host context here.
2426
+ * @internal
2427
+ */
2428
+ private toDocKVKey(key: string): Promise<string> {
2429
+ return this.toKVKey(`h/${this.docKVHost()}/doc:${key}`);
2430
+ }
2431
+
2432
+ /**
2433
+ * Best-effort delete of a single KV key, reporting (not swallowing) a delete
2434
+ * failure as cache-delete. Used by the corrupt-entry self-heal paths.
2435
+ * @internal
2436
+ */
2437
+ private async evictKvKey(kvKey: string, label: string): Promise<void> {
2438
+ try {
2439
+ await this.kv!.delete(kvKey);
2440
+ } catch (error) {
2441
+ reportCacheError(
2442
+ error,
2443
+ "cache-delete",
2444
+ `[CFCacheStore] ${label}: evict failed`,
2445
+ );
2446
+ }
2447
+ }
2448
+
2449
+ /**
2450
+ * Schedule a corrupt-entry KV eviction as a NON-BLOCKING background task
2451
+ * (waitUntil) instead of awaiting it on the request path. The corrupt read has
2452
+ * already resolved to a miss; awaiting an unbounded kv.delete here would re-add
2453
+ * exactly the multi-second stall the read budgets exist to prevent when the KV
2454
+ * namespace is degraded. evictKvKey never rejects (it reports its own failure),
2455
+ * so the fire-and-forget fallback is safe when no waitUntil is available.
2456
+ * @internal
2457
+ */
2458
+ private scheduleKvEvict(kvKey: string, label: string): void {
2459
+ const evict = (): Promise<void> => this.evictKvKey(kvKey, label);
2460
+ if (this.waitUntil) this.waitUntil(evict);
2461
+ else void evict();
2462
+ }
2463
+
2464
+ /**
2465
+ * KV-get a JSON envelope, EVICTING the key only when it is genuinely corrupt.
2466
+ *
2467
+ * Reads as { type: "text" }, NOT { type: "json" }, on purpose: the "json" form
2468
+ * fuses the network read and the JSON parse, so a transient KV outage (5xx/429/
2469
+ * network blip) is indistinguishable from a malformed body and would delete a
2470
+ * still-good cross-colo entry - a self-inflicted miss storm. Reading text lets a
2471
+ * transient read error propagate to the caller's outer catch (reported
2472
+ * cache-read, the entry left intact); only a JSON.parse failure on a body that
2473
+ * WAS successfully read - or an envelope that parses but fails `validate`
2474
+ * (fields missing from a truncated write) - is true corruption that evicts +
2475
+ * reports cache-corrupt. A MISSING key (kv.get -> null) is a normal miss.
2476
+ * @internal
2477
+ */
2478
+ private async kvGetOrEvict<T>(
2479
+ kvKey: string,
2480
+ validate: (envelope: T) => boolean,
2481
+ label: string,
2482
+ ): Promise<{ value: T | null; timedOut: boolean }> {
2483
+ // Bound the read with the KV latency budget (inherited from #558) so a
2484
+ // degraded namespace cannot pin the request. readWithTimeout reports
2485
+ // timedOut on budget expiry; a transient read REJECTION (5xx/429/network)
2486
+ // instead propagates out to the caller's outer catch (reported cache-read,
2487
+ // the entry left intact) -- deliberately NOT caught as corruption.
2488
+ const { value: raw, timedOut } = await this.readWithTimeout<unknown>(
2489
+ () => this.kv!.get(kvKey, { type: "text" }),
2490
+ this.kvReadTimeoutMs,
2491
+ "KV read",
2492
+ );
2493
+ if (timedOut) return { value: null, timedOut: true };
2494
+ if (raw == null) return { value: null, timedOut: false }; // missing = miss
2495
+
2496
+ // Real CF KV with { type: "text" } returns a string: parse + structurally
2497
+ // validate it; a parse/validate failure on a successfully-read body is the
2498
+ // only true corruption (evict + cache-corrupt). A KV binding that already
2499
+ // returns a parsed object (some shims/tests) is used as-is.
2500
+ let envelope: T;
2501
+ if (typeof raw === "string") {
2502
+ try {
2503
+ envelope = JSON.parse(raw) as T;
2504
+ } catch (error) {
2505
+ reportCacheError(
2506
+ error,
2507
+ "cache-corrupt",
2508
+ `[CFCacheStore] ${label}: corrupt JSON in KV, evicting`,
2509
+ );
2510
+ this.scheduleKvEvict(kvKey, label);
2511
+ return { value: null, timedOut: false };
2512
+ }
2513
+ } else {
2514
+ envelope = raw as T;
2515
+ }
2516
+
2517
+ // A body that parses to null or a primitive ('null', '42', 'true', '"x"')
2518
+ // is not a valid envelope. Guard it BEFORE validate(): the property-reading
2519
+ // validators throw on a null/primitive rather than returning false, which
2520
+ // would escape to the caller's outer catch as a transient cache-read and
2521
+ // leave the bad key un-evicted (re-failing every read until its KV TTL). The
2522
+ // typeof check short-circuits validate() so it only ever runs on an object.
2523
+ if (
2524
+ envelope == null ||
2525
+ typeof envelope !== "object" ||
2526
+ !validate(envelope)
2527
+ ) {
2528
+ reportCacheError(
2529
+ new Error("malformed/partial KV envelope"),
2530
+ "cache-corrupt",
2531
+ `[CFCacheStore] ${label}: malformed envelope, evicting`,
2532
+ );
2533
+ this.scheduleKvEvict(kvKey, label);
2534
+ return { value: null, timedOut: false };
2535
+ }
2536
+ return { value: envelope, timedOut: false };
2537
+ }
2538
+
2539
+ // ============================================================================
2540
+ // Tag Invalidation (single-store: markers live in this.kv)
2541
+ // ============================================================================
2542
+
2543
+ /** KV key for a tag's invalidation marker. */
2544
+ private tagMarkerKey(tag: string): Promise<string> {
2545
+ return this.toKVKey(`${TAG_MARKER_PREFIX}${tag}`);
2546
+ }
2547
+
2548
+ /**
2549
+ * Header entries carrying an entry's tags (JSON-encoded, comma-safe) and the
2550
+ * timestamp they were attached. Returns an empty object when there are no
2551
+ * tags so untagged entries stay header-free and skip the invalidation check.
2552
+ *
2553
+ * Also stamps the namespaced `Cache-Tag` header (see entryCacheTags) so a
2554
+ * Cloudflare purge-by-tag can evict the entry — the mechanism purge mode
2555
+ * (tagPurge) relies on. Written unconditionally (not only in purge mode):
2556
+ * it costs a small header and makes existing entries purgeable the moment a
2557
+ * consumer turns purge mode on, with no re-render needed.
2558
+ */
2559
+ private tagHeaderEntries(
2560
+ tags: string[] | undefined,
2561
+ taggedAt: number | undefined,
2562
+ ): Record<string, string> {
2563
+ if (!Array.isArray(tags) || tags.length === 0 || !taggedAt) return {};
2564
+ const entries: Record<string, string> = {
2565
+ // encodeURIComponent so the value is pure ASCII: HTTP header values are
2566
+ // ByteStrings, but JSON.stringify leaves codepoints > U+00FF (emoji/CJK)
2567
+ // verbatim, which makes new Response({ headers }) throw and the outer
2568
+ // try/catch silently drop the whole entry from cache. Decoded in
2569
+ // readTagInfo. The L1 marker Cache-Tag path encodes for the same reason.
2570
+ [CACHE_TAGS_HEADER]: encodeURIComponent(JSON.stringify(tags)),
2571
+ [CACHE_TAGGED_AT_HEADER]: String(taggedAt),
2572
+ };
2573
+ // Over Cloudflare's aggregate Cache-Tag limit the header is OMITTED — the
2574
+ // entry still caches and stays marker-invalidatable; it just cannot be
2575
+ // purge-evicted per tag (isL1Invalidated falls back to the marker check
2576
+ // for such entries, so purge mode stays correct). Emitting a header over
2577
+ // the limit would instead risk failing the whole L1 write. The KV-LESS
2578
+ // purge-mode combination never reaches here: there the marker fallback
2579
+ // has no KV to consult, so skipUncacheableTagSet rejects the write first.
2580
+ const cacheTag = this.entryCacheTagHeader(tags);
2581
+ if (cacheTag !== null) {
2582
+ entries["Cache-Tag"] = cacheTag;
2583
+ } else {
2584
+ this.warnOncePerNamespace(
2585
+ warnedCacheTagHeaderOverflow,
2586
+ `[CFCacheStore] an entry's ${tags.length} tags produce a Cache-Tag ` +
2587
+ `header over Cloudflare's ${CACHE_TAG_HEADER_MAX_BYTES}-byte ` +
2588
+ `limit; the header was omitted. The entry stays cacheable and ` +
2589
+ `marker-invalidatable, but a purge-by-tag cannot evict it (purge ` +
2590
+ `mode falls back to the marker check for it). Reduce the number ` +
2591
+ `of tags per entry.`,
2592
+ );
2593
+ }
2594
+ return entries;
2595
+ }
2596
+
2597
+ /**
2598
+ * Joined entry Cache-Tag header value for `tags`, or null when it would
2599
+ * exceed Cloudflare's aggregate header limit. Tokens are pure ASCII
2600
+ * (encodeURIComponent output), so .length is bytes.
2601
+ * @internal
2602
+ */
2603
+ private entryCacheTagHeader(tags: string[]): string | null {
2604
+ const joined = this.entryCacheTags(tags).join(",");
2605
+ return joined.length <= CACHE_TAG_HEADER_MAX_BYTES ? joined : null;
2606
+ }
2607
+
2608
+ /**
2609
+ * Write-path gate for the one configuration where an over-limit tag set has
2610
+ * NO invalidation path: purge mode WITHOUT KV. There the entry Cache-Tag
2611
+ * tokens are the only eviction mechanism, and a tag set whose header
2612
+ * overflows CACHE_TAG_HEADER_MAX_BYTES gets no tokens — a purge could never
2613
+ * evict the entry and there is no KV marker fallback, so it would serve
2614
+ * stale until TTL while updateTag() reports success. Returns true (and
2615
+ * warns once) so the caller SKIPS caching: the route simply renders fresh,
2616
+ * which is the fail-safe direction. With KV configured the omitted-header
2617
+ * entry falls back to the marker check (see tagHeaderEntries), and KV-less
2618
+ * MARKER mode keeps its documented no-read-side-invalidation semantics —
2619
+ * neither is gated.
2620
+ * @internal
2621
+ */
2622
+ private skipUncacheableTagSet(tags: string[] | undefined): boolean {
2623
+ if (!this.tagPurge || this.kv) return false;
2624
+ if (!Array.isArray(tags) || tags.length === 0) return false;
2625
+ if (this.entryCacheTagHeader(tags) !== null) return false;
2626
+ this.warnOncePerNamespace(
2627
+ warnedCacheTagOverflowUncacheable,
2628
+ `[CFCacheStore] an entry's ${tags.length} tags produce a Cache-Tag ` +
2629
+ `header over Cloudflare's ${CACHE_TAG_HEADER_MAX_BYTES}-byte limit. ` +
2630
+ `In purge mode without KV those tokens are the only invalidation ` +
2631
+ `path, so the entry was NOT cached (it renders fresh instead of ` +
2632
+ `becoming un-invalidatable). Reduce the number of tags per entry, ` +
2633
+ `or configure { kv } to get the marker fallback.`,
2634
+ );
2635
+ return true;
2636
+ }
2637
+
2638
+ /**
2639
+ * Merge the internal tag headers onto an existing Headers instance. The
2640
+ * from-scratch paths spread tagHeaderEntries() into an object-literal init;
2641
+ * the document put/promote paths build a Headers first, so they .set() each
2642
+ * entry instead — except `Cache-Tag`, which is APPENDED: a document author
2643
+ * may have set their own Cache-Tag, and clobbering it would break their CDN
2644
+ * purging. Append produces the comma-merged list CF expects.
2645
+ */
2646
+ private setTagHeaders(
2647
+ headers: Headers,
2648
+ tags: string[] | undefined,
2649
+ taggedAt: number | undefined,
2650
+ ): void {
2651
+ for (const [name, value] of Object.entries(
2652
+ this.tagHeaderEntries(tags, taggedAt),
2653
+ )) {
2654
+ if (name === "Cache-Tag") headers.append(name, value);
2655
+ else headers.set(name, value);
2656
+ }
2657
+ }
2658
+
2659
+ /** Read an entry's tags/taggedAt back from its headers. */
2660
+ private readTagInfo(headers: Headers): {
2661
+ tags?: string[];
2662
+ taggedAt?: number;
2663
+ } {
2664
+ const rawTags = headers.get(CACHE_TAGS_HEADER);
2665
+ const rawTaggedAt = headers.get(CACHE_TAGGED_AT_HEADER);
2666
+ if (!rawTags || !rawTaggedAt) return {};
2667
+ try {
2668
+ const taggedAt = Number(rawTaggedAt);
2669
+ // A corrupt/non-numeric tagged-at header yields NaN. isGloballyInvalidated
2670
+ // short-circuits on a falsy taggedAt (NaN is falsy), so returning
2671
+ // { taggedAt: NaN } would make the entry permanently NON-invalidatable -
2672
+ // a revalidateTag could never evict it. Treat a non-finite stamp the same
2673
+ // as the missing-header case (untagged): drop both tags and taggedAt so the
2674
+ // entry is re-rendered/re-tagged rather than silently un-invalidatable.
2675
+ if (!Number.isFinite(taggedAt)) return {};
2676
+ return {
2677
+ tags: JSON.parse(decodeURIComponent(rawTags)) as string[],
2678
+ taggedAt,
2679
+ };
2680
+ } catch {
2681
+ return {};
2682
+ }
2683
+ }
2684
+
2685
+ /**
2686
+ * Whether an entry tagged at `taggedAt` with `tags` has been invalidated since.
2687
+ * Reads the per-tag invalidation markers from KV and returns true if any tag's
2688
+ * latest invalidation is at or after taggedAt (>= so a same-millisecond
2689
+ * invalidate wins, favouring freshness over staleness). Fails open: KV errors
2690
+ * never turn a hit into a wrongful miss-storm beyond this single read.
2691
+ */
2692
+ private async isGloballyInvalidated(
2693
+ tags: string[] | undefined,
2694
+ taggedAt: number | undefined,
2695
+ ): Promise<boolean> {
2696
+ // Array.isArray (not just truthiness): a non-array tags value - direct store
2697
+ // misuse like setItem(k, v, { tags: "products" }), or a skewed KV envelope -
2698
+ // must fail safe to "not invalidated" rather than throwing `.map` on every
2699
+ // read (which the outer catch would mis-report as a transient cache-read).
2700
+ if (!this.kv || !Array.isArray(tags) || tags.length === 0 || !taggedAt)
2701
+ return false;
2702
+ const ctx = _getRequestContext();
2703
+ const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
2704
+ const inflight = ctx ? getTagMarkerInflight(ctx, this) : undefined;
2705
+ try {
2706
+ const markers = await Promise.all(
2707
+ tags.map((tag) => this.readTagMarker(tag, memo, inflight)),
2708
+ );
2709
+ for (const marker of markers) {
2710
+ if (marker != null && marker >= taggedAt) return true;
2711
+ }
2712
+ return false;
2713
+ } catch (error) {
2714
+ reportCacheError(
2715
+ error,
2716
+ "cache-read",
2717
+ "[CFCacheStore] tag invalidation check",
2718
+ );
2719
+ return false;
2720
+ }
2721
+ }
2722
+
2723
+ /**
2724
+ * Tag-invalidation check for an L1 (Cache API) hit. In purge mode (tagPurge
2725
+ * configured), invalidateTags() evicts L1 entries via a Cloudflare
2726
+ * purge-by-tag call, so a hit that SURVIVED is trusted without a per-read
2727
+ * marker lookup — that skipped lookup is the entire point of purge mode.
2728
+ * Only the per-request memo is consulted (synchronous, no KV read) so a
2729
+ * request that ran updateTag() still masks its own entries during the purge
2730
+ * propagation window (read-your-own-writes).
2731
+ *
2732
+ * The trust is conditional on the entry actually CARRYING this store's
2733
+ * entry Cache-Tag tokens (`headers`): an entry a purge cannot reach — one
2734
+ * written before the tokens existed, or whose tag set overflowed the
2735
+ * Cache-Tag header limit — keeps the full marker check, or purge mode
2736
+ * would serve it stale until TTL with no eviction path.
2737
+ *
2738
+ * Without tagPurge this is the full marker cascade. KV-tier reads and
2739
+ * shells always use isGloballyInvalidated directly: purge cannot reach KV,
2740
+ * so the markers stay their invalidation mechanism.
2741
+ * @internal
2742
+ */
2743
+ private async isL1Invalidated(
2744
+ tags: string[] | undefined,
2745
+ taggedAt: number | undefined,
2746
+ headers: Headers,
2747
+ ): Promise<boolean> {
2748
+ if (!this.tagPurge) return this.isGloballyInvalidated(tags, taggedAt);
2749
+ if (!Array.isArray(tags) || tags.length === 0 || !taggedAt) return false;
2750
+ if (!this.hasEntryCacheTags(headers)) {
2751
+ return this.isGloballyInvalidated(tags, taggedAt);
2752
+ }
2753
+ const ctx = _getRequestContext();
2754
+ if (!ctx) return false;
2755
+ const memo = getTagMarkerMemo(ctx, this);
2756
+ for (const tag of tags) {
2757
+ const marker = memo.get(tag);
2758
+ if (marker != null && marker >= taggedAt) return true;
2759
+ }
2760
+ return false;
2761
+ }
2762
+
2763
+ /** Synthetic Cache API request for a tag's L1-cached invalidation marker. */
2764
+ private tagMarkerRequest(tag: string): Request {
2765
+ return this.keyToRequest(`${TAG_MARKER_CACHE_PREFIX}${tag}`);
2766
+ }
2767
+
2768
+ /**
2769
+ * Read a tag's latest invalidation timestamp (or null if never invalidated)
2770
+ * through the cascade: per-request memo -> per-colo L1 cache (only when
2771
+ * tagCacheTtl > 0) -> KV (the global truth). The memo is always consulted
2772
+ * first so it stays authoritative within a request (read-your-own-writes),
2773
+ * and every KV/L1 result is written back into the memo. A Cache API miss
2774
+ * always falls through to KV; absence is represented by a cached sentinel,
2775
+ * never by a miss.
2776
+ *
2777
+ * Concurrent reads of the same tag within a request share one in-flight read
2778
+ * (the resolved-value memo only collapses sequential reads; parallel segment
2779
+ * loading would otherwise issue one KV read per concurrent reader).
2780
+ * @internal
2781
+ */
2782
+ private async readTagMarker(
2783
+ tag: string,
2784
+ memo: Map<string, number | null> | undefined,
2785
+ inflight: Map<string, Promise<number | null>> | undefined,
2786
+ ): Promise<number | null> {
2787
+ if (memo && memo.has(tag)) return memo.get(tag) ?? null;
2788
+
2789
+ // Collapse concurrent (not-yet-resolved) reads of this tag onto one promise.
2790
+ if (inflight) {
2791
+ const pending = inflight.get(tag);
2792
+ if (pending) return pending;
2793
+ const read = this.fetchTagMarker(tag, memo);
2794
+ inflight.set(tag, read);
2795
+ try {
2796
+ return await read;
2797
+ } finally {
2798
+ // Resolved values now live in the memo; drop the in-flight entry.
2799
+ inflight.delete(tag);
2800
+ }
2801
+ }
2802
+
2803
+ return this.fetchTagMarker(tag, memo);
2804
+ }
2805
+
2806
+ /**
2807
+ * Uncached body of readTagMarker: L1 (per-colo Cache API, opt-in via
2808
+ * tagCacheTtl) -> KV. Writes the resolved value back into the memo.
2809
+ * @internal
2810
+ */
2811
+ private async fetchTagMarker(
2812
+ tag: string,
2813
+ memo: Map<string, number | null> | undefined,
2814
+ ): Promise<number | null> {
2815
+ // Write the resolved marker into the memo WITHOUT clobbering a value a
2816
+ // concurrent invalidateTags() wrote during our await. The router resolves
2817
+ // sibling slots in parallel, so a slot's updateTag() can land the
2818
+ // authoritative invalidatedAt into the memo while this read is still in
2819
+ // flight; overwriting it with our (pre-invalidation) read result would break
2820
+ // read-your-own-writes for the rest of the request. If the tag was memoized
2821
+ // mid-read, that value wins and is returned. Without a memo, the read result
2822
+ // stands as-is.
2823
+ const memoize = (read: number | null): number | null => {
2824
+ if (memo && memo.has(tag)) return memo.get(tag) ?? null;
2825
+ memo?.set(tag, read);
2826
+ return read;
2827
+ };
2828
+
2829
+ // L1 (per-colo) marker cache - opt-in via tagCacheTtl. Bounded by the same
2830
+ // edge budgets as data reads (inherited from #558) so a degraded colo cannot
2831
+ // stall a tagged read; a miss, timeout, or error all fall through to KV.
2832
+ if (this.tagCacheTtl > 0) {
2833
+ try {
2834
+ const cache = await this.getCache();
2835
+ const { response: hit, error: matchError } =
2836
+ await this.matchWithTimeout(cache, this.tagMarkerRequest(tag));
2837
+ // A transient match REJECTION is captured (not thrown) by
2838
+ // matchWithTimeout; surface it as cache-read like the data read paths
2839
+ // before falling through to KV, rather than silently dropping it.
2840
+ if (matchError)
2841
+ reportCacheError(
2842
+ matchError,
2843
+ "cache-read",
2844
+ "[CFCacheStore] tag marker L1 match",
2845
+ );
2846
+ if (hit) {
2847
+ const { value: body } = await this.readWithTimeout(
2848
+ () => hit.text(),
2849
+ this.edgeReadTimeoutMs,
2850
+ "tag marker L1 body read",
2851
+ );
2852
+ if (body !== undefined) {
2853
+ const value = body === TAG_MARKER_ABSENT ? null : Number(body);
2854
+ return memoize(value);
2855
+ }
2856
+ }
2857
+ } catch {
2858
+ // Fall through to KV on any L1 read error.
2859
+ }
2860
+ }
2861
+
2862
+ // KV (global truth), bounded by the KV budget. On TIMEOUT fail OPEN: treat
2863
+ // the marker as absent (-> entry not invalidated -> served) so a degraded
2864
+ // namespace cannot pin every tagged read behind a slow global lookup. A
2865
+ // transient REJECTION instead propagates to isGloballyInvalidated's catch
2866
+ // (reported cache-read), which also fails open. Either way one slow tag
2867
+ // never amplifies into a per-segment stall.
2868
+ const { value: raw, timedOut } = await this.readWithTimeout<string | null>(
2869
+ async () => this.kv!.get(await this.tagMarkerKey(tag), { type: "text" }),
2870
+ this.kvReadTimeoutMs,
2871
+ "tag marker KV read",
2872
+ );
2873
+ if (timedOut) {
2874
+ // Memoize the fail-open result so the rest of this request is consistent
2875
+ // (and does not re-pay the timeout per segment sharing the tag).
2876
+ return memoize(null);
2877
+ }
2878
+ const value = raw != null ? Number(raw) : null;
2879
+ const resolved = memoize(value);
2880
+
2881
+ // Populate L1 for subsequent reads in this colo (non-blocking). Use the
2882
+ // resolved (memo-aware) value so a marker invalidated mid-read is not
2883
+ // re-cached stale into this colo's L1.
2884
+ if (this.tagCacheTtl > 0) {
2885
+ const put = () => this.putTagMarkerL1(tag, resolved);
2886
+ if (this.waitUntil) this.waitUntil(put);
2887
+ else void put();
2888
+ }
2889
+ return resolved;
2890
+ }
2891
+
2892
+ /**
2893
+ * Namespace token used inside every `rg:*` Cache-Tag. encodeURIComponent'd
2894
+ * like the tag values: a raw namespace containing a comma would split into
2895
+ * bogus tokens inside the comma-delimited Cache-Tag header — breaking both
2896
+ * stripInternalCacheTags (internal tokens would leak to clients) and the
2897
+ * purge match (the purged tag string never equals any stored token).
2898
+ * @internal
2899
+ */
2900
+ private nsToken(): string {
2901
+ return encodeURIComponent(this.namespace ?? "default");
2902
+ }
2903
+
2904
+ /**
2905
+ * Cloudflare Cache-Tags written on a tag's L1 marker entry, namespaced per
2906
+ * store so purges never collide with other Cache-Tags in the zone. Three
2907
+ * tiers, broad to specific:
2908
+ * rg:{ns} - everything this store cached (deploy/nuclear reset)
2909
+ * rg:{ns}:lk - all tag-lookup markers
2910
+ * rg:{ns}:lk:{tag} - this tag's lookup (the normal updateTag purge target)
2911
+ * The namespace and tag value are encodeURIComponent'd so commas/spaces
2912
+ * can't corrupt the comma-delimited Cache-Tag header.
2913
+ * @internal
2914
+ */
2915
+ private lookupCacheTags(tag: string): string[] {
2916
+ const ns = this.nsToken();
2917
+ return [`rg:${ns}`, `rg:${ns}:lk`, this.lookupPurgeTag(tag)];
2918
+ }
2919
+
2920
+ /**
2921
+ * Build one `{prefix}{tag}` Cache-Tag token, bounded to CACHE_TAG_TOKEN_MAX:
2922
+ * an over-long encoded tag collapses to `{prefix}h:{fnv1a64(tag)}` so a
2923
+ * legally-long application tag can never blow Cloudflare's per-tag purge
2924
+ * limit (1,024 chars) or bloat the header. Deterministic, so the write-time
2925
+ * token and the invalidate-time purge token always agree.
2926
+ * @internal
2927
+ */
2928
+ private boundedTagToken(prefix: string, tag: string): string {
2929
+ const token = `${prefix}${encodeURIComponent(tag)}`;
2930
+ if (token.length <= CACHE_TAG_TOKEN_MAX) return token;
2931
+ return `${prefix}h:${fnv1a64(tag)}`;
2932
+ }
2933
+
2934
+ /** The specific Cache-Tag a consumer purges to evict tag `tag`'s lookup. */
2935
+ private lookupPurgeTag(tag: string): string {
2936
+ return this.boundedTagToken(`rg:${this.nsToken()}:lk:`, tag);
2937
+ }
2938
+
2939
+ /**
2940
+ * Cloudflare Cache-Tags written on a tagged DATA entry (segment/item/doc),
2941
+ * mirroring the lookup-marker tiers but under `:e` (entry):
2942
+ * rg:{ns} - everything this store cached (deploy/nuclear reset)
2943
+ * rg:{ns}:e - all data entries
2944
+ * rg:{ns}:e:{t} - entries carrying tag `t` (the purge-mode invalidation
2945
+ * target; see CFCacheStoreOptions.tagPurge)
2946
+ * Namespace and tag value encodeURIComponent'd like the lookup tier.
2947
+ * @internal
2948
+ */
2949
+ private entryCacheTags(tags: string[]): string[] {
2950
+ const ns = this.nsToken();
2951
+ return [
2952
+ `rg:${ns}`,
2953
+ `rg:${ns}:e`,
2954
+ ...tags.map((tag) => this.entryPurgeTag(tag)),
2955
+ ];
2956
+ }
2957
+
2958
+ /** The specific Cache-Tag purged to evict entries carrying tag `tag`. */
2959
+ private entryPurgeTag(tag: string): string {
2960
+ return this.boundedTagToken(`rg:${this.nsToken()}:e:`, tag);
2961
+ }
2962
+
2963
+ /**
2964
+ * Whether an L1 entry's stored headers carry this store's entry Cache-Tag
2965
+ * tokens — i.e. whether a purge-by-tag can actually evict it. False for an
2966
+ * entry written before this feature existed, or one whose tag set exceeded
2967
+ * CACHE_TAG_HEADER_MAX_BYTES (header omitted). Purge mode only trusts
2968
+ * entries a purge can reach; the rest keep the marker check.
2969
+ * @internal
2970
+ */
2971
+ private hasEntryCacheTags(headers: Headers): boolean {
2972
+ const raw = headers.get("Cache-Tag");
2973
+ if (raw === null) return false;
2974
+ const tier = `rg:${this.nsToken()}:e`;
2975
+ return raw.split(",").some((token) => {
2976
+ const trimmed = token.trim();
2977
+ return trimmed === tier || trimmed.startsWith(`${tier}:`);
2978
+ });
2979
+ }
2980
+
2981
+ /**
2982
+ * Drop this store's namespaced tokens (`rg:{ns}` and `rg:{ns}:*`) from a
2983
+ * Cache-Tag header, keeping author-set tokens intact. Deletes the header
2984
+ * when nothing remains. Serve-path counterpart of setTagHeaders' append.
2985
+ * An author token that exactly equals a reserved `rg:{ns}` tier is stripped
2986
+ * too — `rg:` is this store's documented-reserved Cache-Tag prefix.
2987
+ * @internal
2988
+ */
2989
+ private stripInternalCacheTags(headers: Headers): void {
2990
+ const raw = headers.get("Cache-Tag");
2991
+ if (raw === null) return;
2992
+ const ns = this.nsToken();
2993
+ const kept = raw
2994
+ .split(",")
2995
+ .map((token) => token.trim())
2996
+ .filter(
2997
+ (token) =>
2998
+ token.length > 0 &&
2999
+ token !== `rg:${ns}` &&
3000
+ !token.startsWith(`rg:${ns}:`),
3001
+ );
3002
+ if (kept.length > 0) headers.set("Cache-Tag", kept.join(","));
3003
+ else headers.delete("Cache-Tag");
3004
+ }
3005
+
3006
+ /**
3007
+ * Write a tag marker value into the per-colo L1 Cache API with tagCacheTtl.
3008
+ * `null` is stored as the TAG_MARKER_ABSENT sentinel so "no marker yet" is
3009
+ * cacheable (most tags are never invalidated - that is where the read savings
3010
+ * come from). The entry also carries a namespaced Cache-Tag so an external
3011
+ * purge-by-tag (via onRevalidateTag) can evict it across colos promptly,
3012
+ * rather than waiting out tagCacheTtl. Best-effort.
3013
+ * @internal
3014
+ */
3015
+ private async putTagMarkerL1(
3016
+ tag: string,
3017
+ value: number | null,
3018
+ opts?: { critical?: boolean },
3019
+ ): Promise<void> {
3020
+ if (this.tagCacheTtl <= 0) return;
3021
+ try {
3022
+ const cache = await this.getCache();
3023
+ const body = value != null ? String(value) : TAG_MARKER_ABSENT;
3024
+ await cache.put(
3025
+ this.tagMarkerRequest(tag),
3026
+ new Response(body, {
3027
+ headers: {
3028
+ "Cache-Control": `public, max-age=${this.tagCacheTtl}`,
3029
+ "Cache-Tag": this.lookupCacheTags(tag).join(","),
3030
+ },
3031
+ }),
3032
+ );
3033
+ } catch (error) {
3034
+ // The read-path populate is best-effort: a failed populate just means the
3035
+ // next read consults KV. The invalidation WRITE-THROUGH (critical) is not
3036
+ // - silently swallowing it would leave this colo's stale marker (often the
3037
+ // ABSENT sentinel) authoritative for tagCacheTtl while updateTag reports
3038
+ // success. Surface it, and best-effort delete the L1 marker so the next
3039
+ // read re-reads KV, which already holds the fresh marker (written before
3040
+ // this write-through in invalidateTags).
3041
+ if (opts?.critical) {
3042
+ reportCacheError(
3043
+ error,
3044
+ "cache-invalidate",
3045
+ "[CFCacheStore] tag marker L1 write-through",
3046
+ );
3047
+ await reportingAsync(
3048
+ async () => {
3049
+ const cache = await this.getCache();
3050
+ await cache.delete(this.tagMarkerRequest(tag));
3051
+ },
3052
+ "cache-delete",
3053
+ "[CFCacheStore] tag marker L1 evict after failed write-through",
3054
+ );
3055
+ }
3056
+ }
3057
+ }
3058
+
3059
+ /**
3060
+ * Invalidate every entry tagged with any of `tags`. Receives the whole batch
3061
+ * from one updateTag()/revalidateTag() call so the eager-purge hook fires
3062
+ * ONCE (one CDN purge request, not one per tag). For each tag: records the KV
3063
+ * marker (the durable cross-colo truth that reads compare taggedAt against),
3064
+ * writes the fresh marker straight into this colo's L1 (write-through, NOT
3065
+ * delete - a delete would let the next read re-read a not-yet-converged KV
3066
+ * value and re-arm the stale window), and memoizes it for same-request
3067
+ * read-your-own-writes. In purge mode (tagPurge) it then AWAITS the
3068
+ * consumer's purge-by-tag call with the entry Cache-Tags — the eviction the
3069
+ * per-read marker skip on L1 hits relies on. Finally fires onRevalidateTag
3070
+ * with the namespaced lookup Cache-Tags so a consumer purge evicts the
3071
+ * cached lookups in other colos promptly (otherwise they converge within
3072
+ * tagCacheTtl).
3073
+ *
3074
+ * Durable-write integrity: the in-memory write-through (memo + L1) for a tag
3075
+ * runs ONLY after that tag's KV marker write is confirmed. If any KV write
3076
+ * fails (transient error; over-limit keys are normalized by toKVKey rather
3077
+ * than rejected), this rejects with the
3078
+ * failed tags so an awaiting updateTag() surfaces the failure instead of
3079
+ * silently reporting success while other requests/colos serve stale data. The
3080
+ * eager purge still fires for the whole batch first (it is additive).
3081
+ */
3082
+ /**
3083
+ * Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince): the
3084
+ * SAME KV markers used by runtime envelopes also evict immutable build shells
3085
+ * and captures whose write races updateTag(). Thin public wrapper over the
3086
+ * shell generation check (marker >= since, fail open); KV-less it degrades
3087
+ * to the per-request memo in purge mode and to false otherwise — see
3088
+ * isShellGenerationInvalidated.
3089
+ */
3090
+ async isTagsInvalidatedSince(
3091
+ tags: string[],
3092
+ sinceMs: number,
3093
+ ): Promise<boolean> {
3094
+ return this.isShellGenerationInvalidated(tags, sinceMs);
3095
+ }
3096
+
3097
+ async invalidateTags(tags: string[]): Promise<void> {
3098
+ if (tags.length === 0) return;
3099
+ const invalidatedAt = Date.now();
3100
+ const ctx = _getRequestContext();
3101
+ const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
3102
+
3103
+ if (!this.kv && !this.onRevalidateTag && !this.tagPurge) {
3104
+ console.warn(
3105
+ `[CFCacheStore] invalidateTags had no effect: configure a KV namespace ` +
3106
+ `for distributed invalidation, a tagPurge hook for purge-by-tag ` +
3107
+ `eviction, or an onRevalidateTag hook.`,
3108
+ );
3109
+ }
3110
+
3111
+ const failedTags = new Set<string>();
3112
+ const errors: unknown[] = [];
3113
+ if (this.kv) {
3114
+ // Markers written with no expiry (tagInvalidationTtl unset) never expire,
3115
+ // so high-cardinality tags accumulate KV keys unboundedly with no reaper.
3116
+ // Warn once per namespace at the batch entry point (not per marker write,
3117
+ // which would fire once per tag). Kept separate from the floor warning:
3118
+ // that path only fires for a positive below-floor value, never the unset
3119
+ // default sanitizeTagInvalidationTtl passes through as undefined.
3120
+ if (!this.tagInvalidationTtl) {
3121
+ this.warnOncePerNamespace(
3122
+ warnedNoTagInvalidationTtl,
3123
+ `[CFCacheStore] invalidateTags is writing KV markers with no expiry ` +
3124
+ `(tagInvalidationTtl is unset): high-cardinality tags accumulate KV ` +
3125
+ `keys unboundedly (storage + list-scan cost) with no reaper. Set ` +
3126
+ `tagInvalidationTtl above your largest entry TTL+SWR to bound marker ` +
3127
+ `growth; setting it too small resurrects invalidated entries.`,
3128
+ );
3129
+ }
3130
+ await Promise.all(
3131
+ tags.map(async (tag) => {
3132
+ // An over-limit tag no longer rejects: tagMarkerKey normalizes
3133
+ // through toKVKey, and the marker read path derives the key the same
3134
+ // way, so oversized tags invalidate correctly instead of erroring.
3135
+ const markerKey = await this.tagMarkerKey(tag);
3136
+ try {
3137
+ await this.kv!.put(markerKey, String(invalidatedAt), {
3138
+ ...(this.tagInvalidationTtl
3139
+ ? { expirationTtl: this.tagInvalidationTtl }
3140
+ : {}),
3141
+ });
3142
+ } catch (error) {
3143
+ failedTags.add(tag);
3144
+ errors.push(error);
3145
+ }
3146
+ }),
3147
+ );
3148
+ }
3149
+
3150
+ // Write-through memo + L1 only for tags with a confirmed durable marker, and
3151
+ // only when KV is configured. Markers are read exclusively through
3152
+ // isGloballyInvalidated(), which short-circuits to "not invalidated" when
3153
+ // !this.kv; writing memo/L1 markers without KV would be dead state no read
3154
+ // path ever consults — EXCEPT the memo in purge mode: isL1Invalidated()
3155
+ // consults it (and only it) on every L1 hit, so a KV-less purge-mode store
3156
+ // still writes the memo for same-request read-your-own-writes. The
3157
+ // onRevalidateTag purge below still fires regardless (it is additive and
3158
+ // external to the marker cascade). The memo write is synchronous
3159
+ // (read-your-own-writes); the L1 Cache API writes are independent, so fan
3160
+ // them out in parallel rather than awaiting each.
3161
+ const lookupMarkerCacheActive = Boolean(this.kv) && this.tagCacheTtl > 0;
3162
+ if (this.kv || this.tagPurge) {
3163
+ const l1Writes: Promise<void>[] = [];
3164
+ for (const tag of tags) {
3165
+ if (failedTags.has(tag)) continue;
3166
+ memo?.set(tag, invalidatedAt);
3167
+ if (lookupMarkerCacheActive) {
3168
+ l1Writes.push(
3169
+ this.putTagMarkerL1(tag, invalidatedAt, { critical: true }),
3170
+ );
3171
+ }
3172
+ }
3173
+ if (l1Writes.length > 0) await Promise.all(l1Writes);
3174
+ }
3175
+
3176
+ // Purge mode: evict the tagged L1 entries across every colo via the
3177
+ // consumer's purge-by-tag call. AWAITED, and a failure is correctness-
3178
+ // bearing (unlike onRevalidateTag): with the per-read marker lookup skipped
3179
+ // on L1 hits, a dropped purge leaves L1 serving stale until TTL — so it
3180
+ // surfaces through updateTag() like a failed marker write. Fired for the
3181
+ // whole batch regardless of marker outcome (purging is additive and
3182
+ // idempotent; a retry re-runs both). Lookup Cache-Tags ride along when the
3183
+ // L1 marker cache is active (tagCacheTtl > 0 AND kv — without kv no lookup
3184
+ // entries are ever written) so a single purge call also converges other
3185
+ // colos' cached lookups (no separate onRevalidateTag needed).
3186
+ let purgeError: unknown;
3187
+ if (this.tagPurge) {
3188
+ const purgeTags = tags.flatMap((tag) =>
3189
+ lookupMarkerCacheActive
3190
+ ? [this.entryPurgeTag(tag), this.lookupPurgeTag(tag)]
3191
+ : [this.entryPurgeTag(tag)],
3192
+ );
3193
+ try {
3194
+ await this.tagPurge(purgeTags);
3195
+ } catch (error) {
3196
+ purgeError = error ?? new Error("tagPurge rejected");
3197
+ reportCacheError(
3198
+ purgeError,
3199
+ "cache-invalidate",
3200
+ "[CFCacheStore] tagPurge hook",
3201
+ );
3202
+ }
3203
+ }
3204
+
3205
+ // One batched eager purge of the lookup markers for the whole call. Fired
3206
+ // regardless of KV write outcome (it is additive and uses pure string ops).
3207
+ if (this.onRevalidateTag) {
3208
+ try {
3209
+ await this.onRevalidateTag(tags.map((tag) => this.lookupPurgeTag(tag)));
3210
+ } catch (error) {
3211
+ reportCacheError(
3212
+ error,
3213
+ "cache-invalidate",
3214
+ "[CFCacheStore] onRevalidateTag hook",
3215
+ );
3216
+ }
3217
+ }
3218
+
3219
+ if (failedTags.size > 0 || purgeError) {
3220
+ const parts: string[] = [];
3221
+ if (failedTags.size > 0) {
3222
+ parts.push(
3223
+ `${failedTags.size}/${tags.length} tag marker write(s) failed: ` +
3224
+ `${[...failedTags].join(", ")}`,
3225
+ );
3226
+ }
3227
+ if (purgeError) {
3228
+ parts.push(
3229
+ `the tagPurge purge-by-tag call failed (tagged L1 entries stay ` +
3230
+ `stale until TTL)`,
3231
+ );
3232
+ }
3233
+ const err = new Error(
3234
+ `[CFCacheStore] ${parts.join("; ")}. Those tags may still serve ` +
3235
+ `stale data across requests/colos; retry the invalidation.`,
3236
+ );
3237
+ (err as Error & { cause?: unknown }).cause = errors[0] ?? purgeError;
3238
+ throw err;
3239
+ }
3240
+ }
3241
+
3242
+ // ============================================================================
3243
+ // KV L2 Helpers
3244
+ // ============================================================================
3245
+
3246
+ /**
3247
+ * KV fallback for segment cache reads.
3248
+ * Returns null if KV is not configured, entry is missing, or expired.
3249
+ * Promotes hits to L1 via waitUntil.
3250
+ * @internal
3251
+ */
3252
+ private async kvGetSegment(
3253
+ key: string,
3254
+ opts?: { suppressRevalidate?: boolean },
3255
+ ): Promise<CacheGetResult | null | CacheReadError> {
3256
+ if (!this.kv) return null;
3257
+
3258
+ try {
3259
+ const kvKey = await this.toKVKey(key);
3260
+ const { value: envelope, timedOut } =
3261
+ await this.kvGetOrEvict<KVSegmentEnvelope>(
3262
+ kvKey,
3263
+ (e) =>
3264
+ typeof e.e === "number" && typeof e.s === "number" && e.d != null,
3265
+ "kvGetSegment",
3266
+ );
3267
+ if (timedOut) {
3268
+ // Abandoned slow KV read: no envelope, so no promote-to-L1. Distinct
3269
+ // from a genuine kv-miss so the degradation is visible on wrangler tail.
3270
+ if (this.debug)
3271
+ this.emitDebug({ op: "get", key, outcome: "kv-timeout" });
3272
+ return null;
3273
+ }
3274
+ if (!envelope) {
3275
+ // Missing key, or a corrupt entry already evicted + reported by
3276
+ // kvGetOrEvict. Either way a miss.
3277
+ if (this.debug) this.emitDebug({ op: "get", key, outcome: "kv-miss" });
3278
+ return null;
3279
+ }
3280
+
3281
+ const now = Date.now();
3282
+
3283
+ // Hard-expired — treat as miss
3284
+ if (now > envelope.e) {
3285
+ if (this.debug) this.emitDebug({ op: "get", key, outcome: "kv-miss" });
3286
+ return null;
3287
+ }
3288
+
3289
+ // Tag invalidation check (also covers the KV tier, not just L1).
3290
+ if (
3291
+ await this.isGloballyInvalidated(envelope.d.tags, envelope.d.taggedAt)
3292
+ ) {
3293
+ if (this.debug)
3294
+ this.emitDebug({ op: "get", key, outcome: "tag-invalidated" });
3295
+ return null;
3296
+ }
3297
+
3298
+ // When this is a degraded L1 fall-through (body-timeout / non-200), the
3299
+ // caller asks us to suppress revalidation: KV has no REVALIDATING herd
3300
+ // guard, so N concurrent degraded reads would otherwise each spawn a
3301
+ // render exactly when the colo is already struggling. We still serve the
3302
+ // stale data and still promote to L1; only the revalidation is withheld.
3303
+ const stale = now > envelope.s;
3304
+ const shouldRevalidate = stale && !opts?.suppressRevalidate;
3305
+
3306
+ // Promote to L1 in background
3307
+ this.promoteSegmentToL1(key, envelope);
3308
+
3309
+ if (this.debug)
3310
+ this.emitDebug({
3311
+ op: "get",
3312
+ key,
3313
+ outcome: !stale
3314
+ ? "kv-fresh"
3315
+ : opts?.suppressRevalidate
3316
+ ? "kv-stale-suppressed"
3317
+ : "kv-stale",
3318
+ shouldRevalidate,
3319
+ });
3320
+ return { data: envelope.d, shouldRevalidate };
3321
+ } catch (error) {
3322
+ reportCacheError(error, "cache-read", "[CFCacheStore] kvGetSegment");
3323
+ if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
3324
+ // A KV failure is NOT proof of absence: returning null classified it a
3325
+ // real miss and let the PPR seeded fallback substitute the doc record
3326
+ // for a key partition the store could not actually read. Same sentinel
3327
+ // as get()'s own catch — lookupRouteDetailed classifies it `error` and
3328
+ // the render stays uncached. (A kvGetOrEvict TIMEOUT above stays null
3329
+ // by design: it is a bounded-latency degrade of a likely-healthy read,
3330
+ // and serving the equivalent seeded record there is the fallback
3331
+ // working as intended, not a masked failure.)
3332
+ return CACHE_READ_ERROR;
3333
+ }
3334
+ }
3335
+
3336
+ /**
3337
+ * Write segment data to KV.
3338
+ *
3339
+ * `dataJson` is the already-serialized CachedEntryData body also stored in
3340
+ * L1 — embedded as envelope.d without a second JSON.stringify walk.
3341
+ * @internal
3342
+ */
3343
+ private kvSetSegment(
3344
+ key: string,
3345
+ dataJson: string,
3346
+ staleAt: number,
3347
+ totalTtl: number,
3348
+ swrWindow: number,
3349
+ ): void {
3350
+ // KV requires expirationTtl >= 60s. Skip write for short-lived entries.
3351
+ if (!this.kv || !this.waitUntil || totalTtl < 60) return;
3352
+
3353
+ const expiresAt = staleAt + swrWindow * 1000;
3354
+ // Same wire shape as JSON.stringify({ d, s, e }) — dataJson is already
3355
+ // valid JSON for CachedEntryData, so embedding it avoids re-walking the tree.
3356
+ const envelopeJson = `{"d":${dataJson},"s":${staleAt},"e":${expiresAt}}`;
3357
+
3358
+ this.waitUntil(() =>
3359
+ reportingAsync(
3360
+ async () =>
3361
+ this.kv!.put(await this.toKVKey(key), envelopeJson, {
3362
+ expirationTtl: totalTtl,
3363
+ }),
3364
+ "cache-write",
3365
+ "[CFCacheStore] kvSetSegment",
3366
+ ),
3367
+ );
3368
+ }
3369
+
3370
+ /**
3371
+ * Promote segment data from KV to L1 Cache API.
3372
+ * @internal
3373
+ */
3374
+ private promoteSegmentToL1(key: string, envelope: KVSegmentEnvelope): void {
3375
+ if (!this.waitUntil) return;
3376
+
3377
+ this.waitUntil(() =>
3378
+ reportingAsync(
3379
+ async () => {
3380
+ const now = Date.now();
3381
+ const remainingTtl = Math.max(
3382
+ 1,
3383
+ Math.floor((envelope.e - now) / 1000),
3384
+ );
3385
+ const cache = await this.getCache();
3386
+ const request = this.keyToRequest(key);
3387
+
3388
+ const response = new Response(JSON.stringify(envelope.d), {
3389
+ headers: {
3390
+ "Content-Type": "application/json",
3391
+ "Cache-Control": `public, max-age=${remainingTtl}`,
3392
+ [CACHE_STALE_AT_HEADER]: String(envelope.s),
3393
+ // Carry the hard-expiry deadline so a promoted entry that later
3394
+ // goes stale re-puts with the correct remaining ttl (see set()).
3395
+ [CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
3396
+ [CACHE_STATUS_HEADER]: "HIT",
3397
+ // Preserve tags across KV->L1 promotion so the promoted entry
3398
+ // stays tag-invalidatable.
3399
+ ...this.tagHeaderEntries(envelope.d.tags, envelope.d.taggedAt),
3400
+ },
3401
+ });
3402
+
3403
+ await cache.put(request, response);
3404
+ },
3405
+ "cache-write",
3406
+ "[CFCacheStore] promoteSegmentToL1",
3407
+ ),
3408
+ );
3409
+ }
3410
+
3411
+ /**
3412
+ * KV fallback for function cache reads.
3413
+ * @internal
3414
+ */
3415
+ private async kvGetItem(
3416
+ key: string,
3417
+ opts?: { suppressRevalidate?: boolean },
3418
+ ): Promise<CacheItemResult | null> {
3419
+ if (!this.kv) return null;
3420
+
3421
+ try {
3422
+ const kvKey = await this.toKVKey(`fn:${key}`);
3423
+ const { value: envelope, timedOut } =
3424
+ await this.kvGetOrEvict<KVItemEnvelope>(
3425
+ kvKey,
3426
+ (e) =>
3427
+ typeof e.v === "string" &&
3428
+ typeof e.e === "number" &&
3429
+ typeof e.s === "number",
3430
+ "kvGetItem",
3431
+ );
3432
+ if (timedOut) {
3433
+ if (this.debug)
3434
+ this.emitDebug({ op: "getItem", key, outcome: "kv-timeout" });
3435
+ return null;
3436
+ }
3437
+ if (!envelope) {
3438
+ if (this.debug)
3439
+ this.emitDebug({ op: "getItem", key, outcome: "kv-miss" });
3440
+ return null;
3441
+ }
3442
+
3443
+ const now = Date.now();
3444
+
3445
+ if (now > envelope.e) {
3446
+ if (this.debug)
3447
+ this.emitDebug({ op: "getItem", key, outcome: "kv-miss" });
3448
+ return null;
3449
+ }
3450
+
3451
+ // Tag invalidation check (also covers the KV tier, not just L1).
3452
+ if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
3453
+ if (this.debug)
3454
+ this.emitDebug({ op: "getItem", key, outcome: "tag-invalidated" });
3455
+ return null;
3456
+ }
3457
+
3458
+ // Degraded fall-through suppresses revalidation (no KV herd guard); see
3459
+ // kvGetSegment. Still serves stale and still promotes.
3460
+ const stale = now > envelope.s;
3461
+ const shouldRevalidate = stale && !opts?.suppressRevalidate;
3462
+
3463
+ // Promote to L1
3464
+ this.promoteItemToL1(key, envelope);
3465
+
3466
+ if (this.debug)
3467
+ this.emitDebug({
3468
+ op: "getItem",
3469
+ key,
3470
+ outcome: !stale
3471
+ ? "kv-fresh"
3472
+ : opts?.suppressRevalidate
3473
+ ? "kv-stale-suppressed"
3474
+ : "kv-stale",
3475
+ shouldRevalidate,
3476
+ });
3477
+ return {
3478
+ value: envelope.v,
3479
+ handles: envelope.h,
3480
+ shouldRevalidate,
3481
+ tags: envelope.t,
3482
+ };
3483
+ } catch (error) {
3484
+ reportCacheError(error, "cache-read", "[CFCacheStore] kvGetItem");
3485
+ if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
3486
+ return null;
3487
+ }
3488
+ }
3489
+
3490
+ /**
3491
+ * Promote function cache data from KV to L1.
3492
+ * @internal
3493
+ */
3494
+ private promoteItemToL1(key: string, envelope: KVItemEnvelope): void {
3495
+ if (!this.waitUntil) return;
3496
+
3497
+ this.waitUntil(() =>
3498
+ reportingAsync(
3499
+ async () => {
3500
+ const now = Date.now();
3501
+ const remainingTtl = Math.max(
3502
+ 1,
3503
+ Math.floor((envelope.e - now) / 1000),
3504
+ );
3505
+ const cache = await this.getCache();
3506
+ const request = this.keyToRequest(`fn:${key}`);
3507
+
3508
+ const body = JSON.stringify({
3509
+ value: envelope.v,
3510
+ handles: envelope.h,
3511
+ });
3512
+ const response = new Response(body, {
3513
+ headers: {
3514
+ "Content-Type": "application/json",
3515
+ "Cache-Control": `public, max-age=${remainingTtl}`,
3516
+ [CACHE_STALE_AT_HEADER]: String(envelope.s),
3517
+ // Carry the hard-expiry deadline; see promoteSegmentToL1 / set().
3518
+ [CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
3519
+ [CACHE_STATUS_HEADER]: "HIT",
3520
+ // Preserve tags across KV->L1 promotion (the item tier previously
3521
+ // dropped them, permanently disabling tag invalidation here).
3522
+ ...this.tagHeaderEntries(envelope.t, envelope.ta),
3523
+ },
3524
+ });
3525
+
3526
+ await cache.put(request, response);
3527
+ },
3528
+ "cache-write",
3529
+ "[CFCacheStore] promoteItemToL1",
3530
+ ),
3531
+ );
3532
+ }
3533
+
3534
+ /**
3535
+ * KV fallback for document cache reads.
3536
+ * @internal
3537
+ */
3538
+ private async kvGetResponse(
3539
+ key: string,
3540
+ ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
3541
+ if (!this.kv) return null;
3542
+
3543
+ try {
3544
+ const kvKey = await this.toDocKVKey(key);
3545
+ // The document path is debug-silent (op is only get/getItem): a KV-read
3546
+ // timeout here is bounded for resilience parity (kvGetOrEvict applies the
3547
+ // budget) but emits no kv-timeout event, so its absence from the debug
3548
+ // stream is expected. A null envelope is a miss -- missing key, a budget
3549
+ // timeout, or a corrupt entry already evicted + reported by kvGetOrEvict.
3550
+ const { value: envelope } = await this.kvGetOrEvict<KVResponseEnvelope>(
3551
+ kvKey,
3552
+ (e) =>
3553
+ typeof e.b === "string" &&
3554
+ typeof e.st === "number" &&
3555
+ typeof e.e === "number" &&
3556
+ typeof e.s === "number" &&
3557
+ // stx is optional but, if present, must be a string (feeds Response).
3558
+ (e.stx === undefined || typeof e.stx === "string") &&
3559
+ // hd must be an array of [name, value] string tuples; a malformed
3560
+ // shape would otherwise throw in `new Headers(hd)`. Validate it here
3561
+ // so a faulty envelope is a fail-open MISS, never a thrown read.
3562
+ Array.isArray(e.hd) &&
3563
+ e.hd.every(
3564
+ (entry) =>
3565
+ Array.isArray(entry) &&
3566
+ entry.length === 2 &&
3567
+ typeof entry[0] === "string" &&
3568
+ typeof entry[1] === "string",
3569
+ ),
3570
+ "kvGetResponse",
3571
+ );
3572
+ if (!envelope) return null;
3573
+
3574
+ const now = Date.now();
3575
+
3576
+ if (now > envelope.e) return null;
3577
+
3578
+ // Tag invalidation check (also covers the KV tier, not just L1).
3579
+ if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
3580
+ return null;
3581
+ }
3582
+
3583
+ const shouldRevalidate = now > envelope.s;
3584
+
3585
+ // Reconstruct Response: decode base64 -> binary, rebuild headers/status.
3586
+ // Corrupt/partial base64 throws in atob; malformed `hd` or an out-of-range
3587
+ // `st` throws in new Headers/new Response. Any of these is a faulty entry,
3588
+ // so evict it and miss rather than re-failing every read until TTL.
3589
+ let response: Response;
3590
+ try {
3591
+ // Finding #3 (read side): strip per-client signals a stale envelope may
3592
+ // carry. Inside the try so a malformed `hd` evicts (not throws through);
3593
+ // mutates `hd` in place so promoteResponseToL1 re-seeds from it too.
3594
+ envelope.hd = envelope.hd.filter(
3595
+ ([name]) => !isPerClientSignalHeader(name),
3596
+ );
3597
+ const bodyBuffer = base64ToBuffer(envelope.b);
3598
+ const headers = new Headers(envelope.hd);
3599
+ response = new Response(bodyBuffer, {
3600
+ status: envelope.st,
3601
+ statusText: envelope.stx,
3602
+ headers,
3603
+ });
3604
+ } catch (error) {
3605
+ reportCacheError(
3606
+ error,
3607
+ "cache-corrupt",
3608
+ "[CFCacheStore] kvGetResponse: corrupt response envelope, evicting",
3609
+ );
3610
+ this.scheduleKvEvict(kvKey, "kvGetResponse");
3611
+ return null;
3612
+ }
3613
+
3614
+ // Promote to L1
3615
+ this.promoteResponseToL1(key, envelope);
3616
+
3617
+ return { response, shouldRevalidate };
3618
+ } catch (error) {
3619
+ reportCacheError(error, "cache-read", "[CFCacheStore] kvGetResponse");
3620
+ return null;
3621
+ }
3622
+ }
3623
+
3624
+ /**
3625
+ * Promote document cache data from KV to L1.
3626
+ * @internal
3627
+ */
3628
+ private promoteResponseToL1(key: string, envelope: KVResponseEnvelope): void {
3629
+ if (!this.waitUntil) return;
3630
+
3631
+ this.waitUntil(() =>
3632
+ reportingAsync(
3633
+ async () => {
3634
+ const now = Date.now();
3635
+ const remainingTtl = Math.max(
3636
+ 1,
3637
+ Math.floor((envelope.e - now) / 1000),
3638
+ );
3639
+ const cache = await this.getCache();
3640
+ const request = this.keyToRequest(`doc:${key}`);
3641
+
3642
+ const headers = new Headers(envelope.hd);
3643
+ const originalCacheControl = headers.get("Cache-Control");
3644
+ if (originalCacheControl !== null) {
3645
+ headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
3646
+ }
3647
+ headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
3648
+ headers.set(CACHE_STALE_AT_HEADER, String(envelope.s));
3649
+ // Carry the hard-expiry deadline so the document herd guard's
3650
+ // markResponseRevalidating re-put can compute the remaining window
3651
+ // (matches promoteSegmentToL1/promoteItemToL1); without it a stale
3652
+ // re-put would floor to max-age=1 and churn the KV-promoted twin.
3653
+ headers.set(CACHE_EXPIRES_AT_HEADER, String(envelope.e));
3654
+ // Re-attach the internal tag headers (envelope.hd is client-facing
3655
+ // and intentionally excludes them) so the promoted entry stays
3656
+ // invalidatable.
3657
+ this.setTagHeaders(headers, envelope.t, envelope.ta);
3658
+
3659
+ const bodyBuffer = base64ToBuffer(envelope.b);
3660
+ const response = new Response(bodyBuffer, {
3661
+ status: envelope.st,
3662
+ statusText: envelope.stx,
3663
+ headers,
3664
+ });
3665
+
3666
+ await cache.put(request, response);
3667
+ },
3668
+ "cache-write",
3669
+ "[CFCacheStore] promoteResponseToL1",
3670
+ ),
3671
+ );
427
3672
  }
428
3673
  }