@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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 (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +122 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -16,14 +16,38 @@ import {
16
16
  getRequestContext,
17
17
  _getRequestContext,
18
18
  } from "../server/request-context.js";
19
- import { serializeSegments, deserializeSegments } from "./segment-codec.js";
20
- import { captureHandles, restoreHandles } from "./handle-snapshot.js";
19
+ import { recordRequestTags } from "./cache-tag.js";
20
+ import { reportCacheError } from "./cache-error.js";
21
+ // segment-codec is the only module on cache-scope's import graph that eagerly
22
+ // pulls @vitejs/plugin-rsc (a virtual: module the plain node/vitest runner cannot
23
+ // resolve). It is imported LAZILY at the two call sites below (deserializeSegments
24
+ // in lookupRoute, serializeSegments in cacheRoute) so that requiring cache-scope —
25
+ // e.g. dispatch's lazy `import("../cache/cache-scope.js")` for the response-route
26
+ // cache path — does not crash a consumer test that never mocks plugin-rsc. Behavior
27
+ // is unchanged: both methods are async and already awaited the codec.
28
+ import {
29
+ captureHandles,
30
+ restoreHandles,
31
+ encodeHandles,
32
+ decodeHandles,
33
+ } from "./handle-snapshot.js";
21
34
  import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
22
35
  import {
23
36
  DEFAULT_ROUTE_TTL,
37
+ isFiniteNonNegativeSeconds,
24
38
  resolveCacheKey,
25
39
  resolveCacheStore,
40
+ resolveTagsOption,
26
41
  } from "./cache-policy.js";
42
+ import type { RequestContext } from "../server/request-context.js";
43
+
44
+ export function resolveCacheTags(
45
+ config: PartialCacheOptions | false,
46
+ ctx: RequestContext | undefined,
47
+ ): string[] | undefined {
48
+ if (config === false) return undefined;
49
+ return resolveTagsOption(config.tags, ctx, "CacheScope");
50
+ }
27
51
 
28
52
  function debugCacheLog(message: string): void {
29
53
  if (INTERNAL_RANGO_DEBUG) {
@@ -31,17 +55,36 @@ function debugCacheLog(message: string): void {
31
55
  }
32
56
  }
33
57
 
34
- // ============================================================================
35
- // Key Generation (internal)
36
- // ============================================================================
37
-
38
58
  /**
39
- * Generate cache key base from host, pathname, route params, and search params.
40
- * Host is included to prevent cross-host cache collisions on shared stores.
41
- * Route params and search params are sorted alphabetically for deterministic keys.
42
- * Internal _rsc* and __* query params are excluded.
43
- * @internal
59
+ * A finite, non-negative seconds value? A NaN/Infinity ttl/swr (from a bad
60
+ * cache() option or store defaults) flows into computeExpiration ->
61
+ * staleAt/expiresAt = NaN, where every `now > NaN` is false so the entry never
62
+ * evicts and is served fresh forever; a negative value makes every read a miss.
63
+ * Mirror profile-registry.ts's Number.isFinite + >= 0 check, but the callers
64
+ * degrade to a default (warning in dev) rather than throw — this runs on the
65
+ * foreground render.
44
66
  */
67
+ function isValidCacheSeconds(value: number, label: string): boolean {
68
+ if (isFiniteNonNegativeSeconds(value)) return true;
69
+ if (process.env.NODE_ENV !== "production") {
70
+ console.warn(
71
+ `[CacheScope] Invalid ${label} ${value}; falling back to default`,
72
+ );
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /** Coerce a resolved ttl to a finite, non-negative number (default on invalid). */
78
+ function validatedTtl(value: number): number {
79
+ return isValidCacheSeconds(value, "ttl") ? value : DEFAULT_ROUTE_TTL;
80
+ }
81
+
82
+ /** Coerce a resolved swr to a finite, non-negative number, or undefined (no SWR window). */
83
+ function validatedSwr(value: number | undefined): number | undefined {
84
+ if (value === undefined) return undefined;
85
+ return isValidCacheSeconds(value, "swr") ? value : undefined;
86
+ }
87
+
45
88
  function getCacheKeyBase(
46
89
  host: string,
47
90
  pathname: string,
@@ -57,16 +100,6 @@ function getCacheKeyBase(
57
100
  return key;
58
101
  }
59
102
 
60
- /**
61
- * Generate default cache key for a route request.
62
- * Includes pathname, route params, and user-facing search params for
63
- * correct scoping. Internal _rsc* params are excluded.
64
- * Includes request type prefix since they produce different segment sets:
65
- * - doc: document requests (full page load)
66
- * - partial: navigation requests (client-side navigation)
67
- * - intercept: intercept navigation (modal/overlay routes)
68
- * @internal
69
- */
70
103
  function getDefaultRouteCacheKey(
71
104
  pathname: string,
72
105
  params?: Record<string, string>,
@@ -128,20 +161,26 @@ export class CacheScope {
128
161
  }
129
162
 
130
163
  /**
131
- * Get effective TTL from config or store defaults
164
+ * Get effective TTL from config or store defaults.
165
+ *
166
+ * Unlike profile-registry.ts (which fails fast at config time), the render
167
+ * path must DEGRADE: a non-finite/negative ttl (NaN/Infinity from a bad
168
+ * defaults config) would make computeExpiration produce NaN deadlines so the
169
+ * entry never evicts, or a guaranteed miss for a negative value. Fall back to
170
+ * DEFAULT_ROUTE_TTL instead of throwing in the foreground render.
132
171
  */
133
172
  get ttl(): number {
134
173
  if (this.config === false) return 0;
135
174
 
136
175
  // Explicit TTL in cache() options
137
176
  if (this.config.ttl !== undefined) {
138
- return this.config.ttl;
177
+ return validatedTtl(this.config.ttl);
139
178
  }
140
179
 
141
180
  // Fall back to store defaults (explicit store first, then app-level)
142
181
  const store = this.getStore();
143
182
  if (store?.defaults?.ttl !== undefined) {
144
- return store.defaults.ttl;
183
+ return validatedTtl(store.defaults.ttl);
145
184
  }
146
185
 
147
186
  // Hardcoded fallback
@@ -149,19 +188,22 @@ export class CacheScope {
149
188
  }
150
189
 
151
190
  /**
152
- * Get SWR window from config or store defaults
191
+ * Get SWR window from config or store defaults.
192
+ *
193
+ * A non-finite/negative swr is degraded to undefined (no SWR window) rather
194
+ * than fed into expiry math; see the ttl getter for the rationale.
153
195
  */
154
196
  get swr(): number | undefined {
155
197
  if (this.config === false) return undefined;
156
198
 
157
199
  // Explicit SWR in cache() options
158
200
  if (this.config.swr !== undefined) {
159
- return this.config.swr;
201
+ return validatedSwr(this.config.swr);
160
202
  }
161
203
 
162
204
  // Fall back to store defaults
163
205
  const store = this.getStore();
164
- return store?.defaults?.swr;
206
+ return validatedSwr(store?.defaults?.swr);
165
207
  }
166
208
 
167
209
  /**
@@ -187,6 +229,32 @@ export class CacheScope {
187
229
  return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
188
230
  }
189
231
 
232
+ /**
233
+ * Evaluate the cache `condition` predicate. Returns false (skip the cache
234
+ * operation) when the predicate returns false or throws; returns true when
235
+ * there is no condition or no request context to evaluate it against.
236
+ */
237
+ private conditionAllows(op: "read" | "write"): boolean {
238
+ if (this.config === false || !this.config.condition) return true;
239
+ const requestCtx = getRequestContext();
240
+ if (!requestCtx) return true;
241
+ try {
242
+ if (!this.config.condition(requestCtx)) {
243
+ debugCacheLog(
244
+ `[CacheScope] condition returned false, skipping cache ${op}`,
245
+ );
246
+ return false;
247
+ }
248
+ return true;
249
+ } catch (error) {
250
+ console.error(
251
+ `[CacheScope] condition function threw, skipping cache ${op}:`,
252
+ error,
253
+ );
254
+ return false;
255
+ }
256
+ }
257
+
190
258
  /**
191
259
  * Lookup cached segments for a route (single cache entry per request).
192
260
  * Returns { segments, shouldRevalidate } or null if cache miss.
@@ -204,35 +272,21 @@ export class CacheScope {
204
272
  shouldRevalidate: boolean;
205
273
  } | null> {
206
274
  if (!this.enabled) return null;
207
-
208
- // Evaluate condition — skip cache read when condition returns false
209
- if (this.config !== false && this.config.condition) {
210
- const requestCtx = getRequestContext();
211
- if (requestCtx) {
212
- try {
213
- if (!this.config.condition(requestCtx)) {
214
- debugCacheLog(
215
- `[CacheScope] condition returned false, skipping cache read`,
216
- );
217
- return null;
218
- }
219
- } catch (error) {
220
- console.error(
221
- `[CacheScope] condition function threw, skipping cache read:`,
222
- error,
223
- );
224
- return null;
225
- }
226
- }
227
- }
275
+ if (!this.conditionAllows("read")) return null;
228
276
 
229
277
  const store = this.getStore();
230
278
  if (!store) return null;
231
279
 
232
- // Resolve cache key (may use custom key functions)
233
- const key = await this.resolveKey(pathname, params, isIntercept);
234
-
280
+ // Resolve cache key INSIDE the try so a throwing consumer key() (or a
281
+ // store.keyGenerator) degrades to a cache miss (return null -> render
282
+ // uncached) instead of crashing the foreground render. resolveCacheKey
283
+ // itself keeps its hard-fail/no-fallback-to-default contract (a throw must
284
+ // not silently collide onto the default slot); the graceful degradation
285
+ // happens here, where a miss is a safe outcome.
286
+ let key: string | undefined;
235
287
  try {
288
+ key = await this.resolveKey(pathname, params, isIntercept);
289
+
236
290
  const result = await store.get(key);
237
291
 
238
292
  if (!result) {
@@ -242,13 +296,44 @@ export class CacheScope {
242
296
 
243
297
  const { data: cached, shouldRevalidate } = result;
244
298
 
245
- // Deserialize segments
246
- const segments = await deserializeSegments(cached.segments);
299
+ // Deserialize segments. A failure means the cached segments are corrupt/
300
+ // partial: evict the entry (self-heal - the re-render re-caches under the
301
+ // same key) and report it as corruption, distinct from a transient infra
302
+ // error (handled by the outer catch).
303
+ let segments: ResolvedSegment[];
304
+ try {
305
+ const { deserializeSegments } = await import("./segment-codec.js");
306
+ segments = await deserializeSegments(cached.segments);
307
+ } catch (error) {
308
+ reportCacheError(
309
+ error,
310
+ "cache-corrupt",
311
+ `[CacheScope] ${key}: corrupt cached segments, evicting`,
312
+ );
313
+ await store
314
+ .delete(key)
315
+ .catch((e) =>
316
+ reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
317
+ );
318
+ return null;
319
+ }
320
+
321
+ // A hit serves content that was tagged at write time, so the document
322
+ // tag union must include this entry's tags for updateTag()/revalidateTag()
323
+ // to invalidate any full-page entry built on top of it. The write path
324
+ // records via cacheRoute (resolveCacheTags); the hit path records here.
325
+ recordRequestTags(cached.tags);
247
326
 
248
- // Replay handle data
327
+ // Replay handle data. An empty string means the route pushed no handles —
328
+ // skip the decode entirely (the common case). Otherwise decode the
329
+ // Flight-encoded blob; a decode failure skips handle restore but keeps the
330
+ // valid cached segments.
249
331
  const handleStore = _getRequestContext()?._handleStore;
250
- if (handleStore) {
251
- restoreHandles(cached.handles, handleStore);
332
+ if (handleStore && cached.handles) {
333
+ const handlesRecord = await decodeHandles(cached.handles);
334
+ if (handlesRecord) {
335
+ restoreHandles(handlesRecord, handleStore);
336
+ }
252
337
  }
253
338
 
254
339
  if (INTERNAL_RANGO_DEBUG) {
@@ -262,11 +347,37 @@ export class CacheScope {
262
347
 
263
348
  return { segments, shouldRevalidate };
264
349
  } catch (error) {
265
- console.error(`[CacheScope] Failed to lookup ${key}:`, error);
350
+ // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
351
+ // (resolveKey). Either way degrade to a cache miss so the render proceeds.
352
+ reportCacheError(
353
+ error,
354
+ "cache-read",
355
+ `[CacheScope] lookup ${key ?? "(key resolution failed)"}`,
356
+ );
266
357
  return null;
267
358
  }
268
359
  }
269
360
 
361
+ /**
362
+ * Record this scope's segment-DSL cache({ tags }) into the request tag union
363
+ * synchronously, under the same gate cacheRoute() uses for a write.
364
+ *
365
+ * cacheRoute() already records these tags, but it is invoked inside
366
+ * requestCtx.waitUntil() by the cache-store middleware (and the proactive path
367
+ * re-resolves the whole tree before calling it), so its recording is deferred
368
+ * and RACES the document cache's post-body-drain snapshot of _requestTags. On a
369
+ * first-write (segment-cache miss) the document tag union could miss these
370
+ * tags, and updateTag()/revalidateTag() would then fail to invalidate the
371
+ * cached document until a later write reseeded it. Calling this synchronously
372
+ * in the request pipeline (before the snapshot) closes that window. Idempotent
373
+ * (the tag union is a Set), so the duplicate record in cacheRoute is harmless.
374
+ */
375
+ recordTags(requestCtx: RequestContext | undefined): void {
376
+ if (!this.enabled) return;
377
+ if (!this.conditionAllows("write")) return;
378
+ recordRequestTags(resolveCacheTags(this.config, requestCtx), requestCtx);
379
+ }
380
+
270
381
  /**
271
382
  * Cache all segments for a route (non-blocking via waitUntil)
272
383
  * Single cache entry per route request.
@@ -284,27 +395,7 @@ export class CacheScope {
284
395
  isIntercept?: boolean,
285
396
  ): Promise<void> {
286
397
  if (!this.enabled || segments.length === 0) return;
287
-
288
- // Evaluate condition — skip cache write when condition returns false
289
- if (this.config !== false && this.config.condition) {
290
- const conditionCtx = getRequestContext();
291
- if (conditionCtx) {
292
- try {
293
- if (!this.config.condition(conditionCtx)) {
294
- debugCacheLog(
295
- `[CacheScope] condition returned false, skipping cache write`,
296
- );
297
- return;
298
- }
299
- } catch (error) {
300
- console.error(
301
- `[CacheScope] condition function threw, skipping cache write:`,
302
- error,
303
- );
304
- return;
305
- }
306
- }
307
- }
398
+ if (!this.conditionAllows("write")) return;
308
399
 
309
400
  const store = this.getStore();
310
401
  if (!store) return;
@@ -325,6 +416,10 @@ export class CacheScope {
325
416
  // Resolve cache key early (while request context is available)
326
417
  const key = await this.resolveKey(pathname, params, isIntercept);
327
418
 
419
+ // Resolve tags early (while request context is available, before waitUntil)
420
+ const tags = resolveCacheTags(this.config, requestCtx);
421
+ recordRequestTags(tags, requestCtx);
422
+
328
423
  // Check if this is a partial request (navigation) vs document request
329
424
  const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
330
425
 
@@ -381,13 +476,20 @@ export class CacheScope {
381
476
  );
382
477
  }
383
478
 
384
- // Serialize non-loader segments only
385
- const serializedSegments = await serializeSegments(nonLoaderSegments);
479
+ // Serialize segments and Flight-encode handles in parallel. Handles go
480
+ // through the codec (not raw into the entry) so Promise/ReactNode handle
481
+ // values survive a JSON-serializing store — see encodeHandles.
482
+ const { serializeSegments } = await import("./segment-codec.js");
483
+ const [serializedSegments, encodedHandles] = await Promise.all([
484
+ serializeSegments(nonLoaderSegments),
485
+ encodeHandles(handles),
486
+ ]);
386
487
 
387
488
  const data: CachedEntryData = {
388
489
  segments: serializedSegments,
389
- handles,
490
+ handles: encodedHandles,
390
491
  expiresAt: Date.now() + ttl * 1000,
492
+ tags,
391
493
  };
392
494
 
393
495
  if (INTERNAL_RANGO_DEBUG) {
@@ -405,7 +507,11 @@ export class CacheScope {
405
507
  );
406
508
  }
407
509
  } catch (error) {
408
- console.error(`[CacheScope] Failed to cache ${key}:`, error);
510
+ reportCacheError(
511
+ error,
512
+ "cache-write",
513
+ `[CacheScope] Failed to cache ${key}`,
514
+ );
409
515
  }
410
516
  });
411
517
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Cache Tag API
3
+ *
4
+ * Provides cacheTag() for tagging cached entries at runtime inside "use cache"
5
+ * functions. Tags are scoped via AsyncLocalStorage; calling cacheTag() outside
6
+ * a "use cache" execution throws.
7
+ *
8
+ * The runtime (cache-runtime.ts) wraps "use cache" execution in
9
+ * runWithCacheTagScope(), collects the runtime tags, and merges them with the
10
+ * profile/DSL tags before storing.
11
+ */
12
+
13
+ import { AsyncLocalStorage } from "node:async_hooks";
14
+ import {
15
+ _getRequestContext,
16
+ type RequestContext,
17
+ } from "../server/request-context.js";
18
+
19
+ const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
20
+
21
+ export function normalizeTag(tag: string): string | null {
22
+ // Trim and return the canonical (trimmed) form, not the raw tag. Both the
23
+ // write path (cacheTag) and the invalidate path (updateTag/revalidateTag)
24
+ // route through here, and matching is exact-string: returning the untrimmed
25
+ // tag made cacheTag(" products ") and updateTag("products") two different
26
+ // logical tags, a silent failure-to-invalidate (stale data served forever).
27
+ const trimmed = tag?.trim();
28
+ return trimmed ? trimmed : null;
29
+ }
30
+
31
+ export function normalizeTags(tags: Iterable<string>): string[] {
32
+ const out: string[] = [];
33
+ for (const tag of tags) {
34
+ const normalized = normalizeTag(tag);
35
+ if (normalized !== null) out.push(normalized);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /**
41
+ * Tag the current "use cache" entry for later invalidation via
42
+ * updateTag() / revalidateTag().
43
+ *
44
+ * Must be called inside a function marked with "use cache".
45
+ * Tags are additive - multiple calls accumulate.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * async function getProduct(ctx) {
50
+ * "use cache";
51
+ * cacheTag(`product:${ctx.params.id}`, "products");
52
+ * return db.getProduct(ctx.params.id);
53
+ * }
54
+ * ```
55
+ */
56
+ export function cacheTag(...tags: string[]): void {
57
+ const store = cacheTagStorage.getStore();
58
+ if (!store) {
59
+ throw new Error('cacheTag() must be called inside a "use cache" function.');
60
+ }
61
+ for (const tag of tags) {
62
+ const normalized = normalizeTag(tag);
63
+ if (normalized === null) {
64
+ if (process.env.NODE_ENV !== "production") {
65
+ console.warn(`[cacheTag] Ignoring empty or whitespace-only tag.`);
66
+ }
67
+ continue;
68
+ }
69
+ store.add(normalized);
70
+ }
71
+ }
72
+
73
+ export function recordRequestTags(
74
+ tags: Iterable<string> | undefined,
75
+ ctx: RequestContext | undefined = _getRequestContext(),
76
+ ): void {
77
+ if (!tags) return;
78
+ const set = ctx?._requestTags;
79
+ if (!set) return;
80
+ for (const tag of tags) {
81
+ const normalized = normalizeTag(tag);
82
+ if (normalized !== null) set.add(normalized);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
88
+ * accumulate into the returned Set.
89
+ *
90
+ * The returned Set is the LIVE reference - the caller must await `result`
91
+ * before reading `tags`, because an async cached function may call cacheTag()
92
+ * after an await boundary.
93
+ *
94
+ * @internal Used by cache-runtime.ts to wrap "use cache" execution.
95
+ */
96
+ export function runWithCacheTagScope<T>(fn: () => T): {
97
+ result: T;
98
+ tags: Set<string>;
99
+ } {
100
+ const tagSet = new Set<string>();
101
+ const result = cacheTagStorage.run(tagSet, fn);
102
+ return { result, tags: tagSet };
103
+ }
@@ -0,0 +1,33 @@
1
+ // ============================================================================
2
+ // Base64 Helpers (binary-safe response body encoding for KV)
3
+ // ============================================================================
4
+
5
+ // Chunk size for String.fromCharCode.apply: large enough to amortize the call
6
+ // overhead, small enough to stay well under the JS engine argument-count limit
7
+ // (~65k). 8192 turns a per-byte concat loop into O(n/8192) apply calls.
8
+ const FROM_CHARCODE_CHUNK = 8192;
9
+
10
+ /** Encode ArrayBuffer to base64 string. */
11
+ export function bufferToBase64(buffer: ArrayBuffer): string {
12
+ const bytes = new Uint8Array(buffer);
13
+ // Build the binary (latin1) string in fixed-size chunks instead of one
14
+ // String.fromCharCode per byte. Identical output to the per-byte loop (each
15
+ // byte maps to the same code unit); just far fewer string concatenations for
16
+ // large document payloads.
17
+ let binary = "";
18
+ for (let i = 0; i < bytes.length; i += FROM_CHARCODE_CHUNK) {
19
+ const chunk = bytes.subarray(i, i + FROM_CHARCODE_CHUNK);
20
+ binary += String.fromCharCode.apply(null, chunk as unknown as number[]);
21
+ }
22
+ return btoa(binary);
23
+ }
24
+
25
+ /** Decode base64 string to ArrayBuffer. */
26
+ export function base64ToBuffer(base64: string): ArrayBuffer {
27
+ const binary = atob(base64);
28
+ const bytes = new Uint8Array(binary.length);
29
+ for (let i = 0; i < binary.length; i++) {
30
+ bytes[i] = binary.charCodeAt(i);
31
+ }
32
+ return bytes.buffer;
33
+ }
@@ -0,0 +1,127 @@
1
+ // ============================================================================
2
+ // Constants
3
+ // ============================================================================
4
+ //
5
+ // Header names, KV prefixes, and timeout/interval defaults for the CF cache
6
+ // store. Extracted from cf-cache-store.ts so the constants can be shared by the
7
+ // store and its sibling collaborator modules without a circular import back to
8
+ // the class. The public ones (CACHE_*_HEADER, TAG_MARKER_PREFIX,
9
+ // MAX_REVALIDATION_INTERVAL, EDGE_*_TIMEOUT_MS, KV_READ_TIMEOUT_MS) are
10
+ // re-exported from cf-cache-store.ts so existing import paths still resolve.
11
+
12
+ /** Header storing timestamp when entry becomes stale */
13
+ export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
14
+
15
+ /** Header storing cache status: HIT | REVALIDATING */
16
+ export const CACHE_STATUS_HEADER = "x-edge-cache-status";
17
+
18
+ /**
19
+ * Header storing this entry's cache tags as a JSON array. JSON-encoded (not the
20
+ * comma-delimited CF `Cache-Tag` format) so tags containing commas round-trip
21
+ * safely; the read paths parse this to run the tag-invalidation check.
22
+ */
23
+ export const CACHE_TAGS_HEADER = "x-edge-cache-tags";
24
+
25
+ /** Header storing the ms-epoch timestamp when this entry's tags were attached. */
26
+ export const CACHE_TAGGED_AT_HEADER = "x-edge-cache-tagged-at";
27
+
28
+ /**
29
+ * KV key prefix for tag-invalidation markers. A marker stores the ms-epoch
30
+ * timestamp of the most recent invalidation of a tag; reads treat any entry
31
+ * whose taggedAt is older than its tags' latest marker as invalidated. Markers
32
+ * live in the SAME KV namespace as the cached entries - there is no separate
33
+ * tag-invalidation store.
34
+ */
35
+ export const TAG_MARKER_PREFIX = "__tag__/";
36
+
37
+ /**
38
+ * Header storing the epoch-ms timestamp when an entry was marked REVALIDATING.
39
+ * The SWR thundering-herd guard reads this to decide whether the in-flight
40
+ * revalidation is still recent. It replaces a prior reliance on the HTTP `Age`
41
+ * header: CF's Cache API does not populate `Age` reliably per-colo (and our own
42
+ * unit MockCache never set it), so an absent `Age` defaulted to 0 and made every
43
+ * REVALIDATING entry look "just revalidated" forever -- a dropped/never-finished
44
+ * background revalidation could then pin an entry stale until hard expiry. An
45
+ * explicit timestamp we write ourselves (same pattern as CACHE_STALE_AT_HEADER)
46
+ * is reliable and lets the MAX_REVALIDATION_INTERVAL re-arm actually fire.
47
+ */
48
+ export const CACHE_REVALIDATING_AT_HEADER = "x-edge-cache-revalidating-at";
49
+
50
+ /**
51
+ * Header storing the absolute epoch-ms hard-expiry deadline (staleAt +
52
+ * swrWindow*1000) of an L1 entry. The stale-path REVALIDATING re-put reads this
53
+ * to recompute a SHRINKING Cache-Control max-age instead of copying set()'s
54
+ * original full-window max-age. Without it, every MAX_REVALIDATION_INTERVAL
55
+ * re-arm re-puts the full window and restarts CF's retention clock, pinning a
56
+ * perpetually-stale entry (one whose background revalidation keeps failing) past
57
+ * its intended hard-expiry indefinitely. Mirrors the KVSegmentEnvelope `e`
58
+ * field and the remaining-ttl math in promoteSegmentToL1/promoteItemToL1.
59
+ * @internal
60
+ */
61
+ export const CACHE_EXPIRES_AT_HEADER = "x-edge-cache-expires-at";
62
+
63
+ /**
64
+ * Header stashing the route author's original Cache-Control on L1 document
65
+ * entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
66
+ * `max-age` so the CF Cache API retains the entry across the whole SWR window;
67
+ * getResponse restores this original value before serving so the client and any
68
+ * upstream CDN see the author's intended directive, not the internal edge TTL.
69
+ */
70
+ export const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
71
+
72
+ /**
73
+ * Maximum age in seconds for REVALIDATING status before allowing new revalidation.
74
+ * After this period, a stale entry in REVALIDATING status will trigger revalidation again.
75
+ * @internal
76
+ */
77
+ export const MAX_REVALIDATION_INTERVAL = 30;
78
+
79
+ /**
80
+ * Maximum time (ms) to wait for an L1 edge cache (CF Cache API) read before
81
+ * giving up and treating it as a miss. The Cache API is normally sub-millisecond
82
+ * per-colo, so a slow `match` signals a degraded colo; we don't want it adding
83
+ * latency to the request. On timeout the lookup is abandoned, a warning is
84
+ * logged, and the read falls through to its normal miss path (L2/KV or render).
85
+ *
86
+ * This is the default; override per store via
87
+ * `CFCacheStoreOptions.edgeLookupTimeoutMs` (<= 0 disables the budget).
88
+ */
89
+ export const EDGE_LOOKUP_TIMEOUT_MS = 10;
90
+
91
+ /**
92
+ * Maximum time (ms) to wait for the BODY of a matched L1 entry to be read
93
+ * (response.json()) before treating the read as a miss.
94
+ *
95
+ * This is separate from {@link EDGE_LOOKUP_TIMEOUT_MS} on purpose. CF's Cache
96
+ * API resolves `match()` with a lazily-streamed body, so a fast `match` can be
97
+ * followed by a multi-second stall while the body bytes are fetched -- the
98
+ * latency tail lives here, after the match budget has already passed. The
99
+ * default bounds that tail aggressively: a healthy per-colo body read (fetch +
100
+ * JSON parse) settles in low single-digit milliseconds, so 20ms clears a
101
+ * healthy read while still failing fast to L2/KV (or render) on a degraded colo
102
+ * instead of pinning the request behind a seconds-long read. Raise it per store
103
+ * if large Flight payloads legitimately need longer.
104
+ *
105
+ * Override per store via `CFCacheStoreOptions.edgeReadTimeoutMs` (<= 0 disables).
106
+ */
107
+ export const EDGE_READ_TIMEOUT_MS = 20;
108
+
109
+ /**
110
+ * Maximum time (ms) to wait for an L2 (KV) read (`kv.get(key, {type:"json"})`)
111
+ * before treating it as a miss. Unlike the L1 budgets, KV is a GLOBAL store: the
112
+ * file header documents ~50ms healthy reads, and a degraded namespace can tail
113
+ * to seconds. KV is the LAST cache tier before a full render, so an unbounded
114
+ * read here pins the whole request behind a degraded global lookup.
115
+ *
116
+ * The default (170ms) sits a few multiples above the documented ~50ms healthy
117
+ * read, leaving headroom for legitimate latency tails (larger payloads,
118
+ * far-from-colo regions) so a healthy-but-slow read does not false-miss into a
119
+ * render, while still abandoning a genuinely degraded namespace well before its
120
+ * multi-second tail can pin the request. A deployment with a tighter SLA can
121
+ * lower it, and one whose healthy p99 runs higher should raise it: measure the
122
+ * KV read p99 (Workers Analytics) and add margin. It is a degradation
123
+ * guard-rail, not a tuning lever for "slow KV is normal here".
124
+ *
125
+ * Override per store via `CFCacheStoreOptions.kvReadTimeoutMs` (<= 0 disables).
126
+ */
127
+ export const KV_READ_TIMEOUT_MS = 170;