@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. 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";
21
- import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.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";
34
+ import { cacheKeyBase } 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,42 +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
  */
45
- function getCacheKeyBase(
46
- host: string,
47
- pathname: string,
48
- params?: Record<string, string>,
49
- searchParams?: URLSearchParams,
50
- ): string {
51
- const paramStr = sortedRouteParams(params);
52
- const searchStr = searchParams ? sortedSearchString(searchParams) : "";
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
+ }
53
76
 
54
- let key = `${host}${pathname}`;
55
- if (paramStr) key += `:${paramStr}`;
56
- if (searchStr) key += `?${searchStr}`;
57
- return key;
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;
58
86
  }
59
87
 
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
88
  function getDefaultRouteCacheKey(
71
89
  pathname: string,
72
90
  params?: Record<string, string>,
@@ -80,7 +98,7 @@ function getDefaultRouteCacheKey(
80
98
  // Intercept navigations get their own cache namespace
81
99
  const prefix = isIntercept ? "intercept" : isPartial ? "partial" : "doc";
82
100
 
83
- return `${prefix}:${getCacheKeyBase(host, pathname, params, searchParams)}`;
101
+ return `${prefix}:${cacheKeyBase(host, pathname, searchParams, params)}`;
84
102
  }
85
103
 
86
104
  // ============================================================================
@@ -128,20 +146,26 @@ export class CacheScope {
128
146
  }
129
147
 
130
148
  /**
131
- * Get effective TTL from config or store defaults
149
+ * Get effective TTL from config or store defaults.
150
+ *
151
+ * Unlike profile-registry.ts (which fails fast at config time), the render
152
+ * path must DEGRADE: a non-finite/negative ttl (NaN/Infinity from a bad
153
+ * defaults config) would make computeExpiration produce NaN deadlines so the
154
+ * entry never evicts, or a guaranteed miss for a negative value. Fall back to
155
+ * DEFAULT_ROUTE_TTL instead of throwing in the foreground render.
132
156
  */
133
157
  get ttl(): number {
134
158
  if (this.config === false) return 0;
135
159
 
136
160
  // Explicit TTL in cache() options
137
161
  if (this.config.ttl !== undefined) {
138
- return this.config.ttl;
162
+ return validatedTtl(this.config.ttl);
139
163
  }
140
164
 
141
165
  // Fall back to store defaults (explicit store first, then app-level)
142
166
  const store = this.getStore();
143
167
  if (store?.defaults?.ttl !== undefined) {
144
- return store.defaults.ttl;
168
+ return validatedTtl(store.defaults.ttl);
145
169
  }
146
170
 
147
171
  // Hardcoded fallback
@@ -149,19 +173,22 @@ export class CacheScope {
149
173
  }
150
174
 
151
175
  /**
152
- * Get SWR window from config or store defaults
176
+ * Get SWR window from config or store defaults.
177
+ *
178
+ * A non-finite/negative swr is degraded to undefined (no SWR window) rather
179
+ * than fed into expiry math; see the ttl getter for the rationale.
153
180
  */
154
181
  get swr(): number | undefined {
155
182
  if (this.config === false) return undefined;
156
183
 
157
184
  // Explicit SWR in cache() options
158
185
  if (this.config.swr !== undefined) {
159
- return this.config.swr;
186
+ return validatedSwr(this.config.swr);
160
187
  }
161
188
 
162
189
  // Fall back to store defaults
163
190
  const store = this.getStore();
164
- return store?.defaults?.swr;
191
+ return validatedSwr(store?.defaults?.swr);
165
192
  }
166
193
 
167
194
  /**
@@ -235,10 +262,16 @@ export class CacheScope {
235
262
  const store = this.getStore();
236
263
  if (!store) return null;
237
264
 
238
- // Resolve cache key (may use custom key functions)
239
- const key = await this.resolveKey(pathname, params, isIntercept);
240
-
265
+ // Resolve cache key INSIDE the try so a throwing consumer key() (or a
266
+ // store.keyGenerator) degrades to a cache miss (return null -> render
267
+ // uncached) instead of crashing the foreground render. resolveCacheKey
268
+ // itself keeps its hard-fail/no-fallback-to-default contract (a throw must
269
+ // not silently collide onto the default slot); the graceful degradation
270
+ // happens here, where a miss is a safe outcome.
271
+ let key: string | undefined;
241
272
  try {
273
+ key = await this.resolveKey(pathname, params, isIntercept);
274
+
242
275
  const result = await store.get(key);
243
276
 
244
277
  if (!result) {
@@ -248,13 +281,44 @@ export class CacheScope {
248
281
 
249
282
  const { data: cached, shouldRevalidate } = result;
250
283
 
251
- // Deserialize segments
252
- const segments = await deserializeSegments(cached.segments);
284
+ // Deserialize segments. A failure means the cached segments are corrupt/
285
+ // partial: evict the entry (self-heal - the re-render re-caches under the
286
+ // same key) and report it as corruption, distinct from a transient infra
287
+ // error (handled by the outer catch).
288
+ let segments: ResolvedSegment[];
289
+ try {
290
+ const { deserializeSegments } = await import("./segment-codec.js");
291
+ segments = await deserializeSegments(cached.segments);
292
+ } catch (error) {
293
+ reportCacheError(
294
+ error,
295
+ "cache-corrupt",
296
+ `[CacheScope] ${key}: corrupt cached segments, evicting`,
297
+ );
298
+ await store
299
+ .delete(key)
300
+ .catch((e) =>
301
+ reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
302
+ );
303
+ return null;
304
+ }
305
+
306
+ // A hit serves content that was tagged at write time, so the document
307
+ // tag union must include this entry's tags for updateTag()/revalidateTag()
308
+ // to invalidate any full-page entry built on top of it. The write path
309
+ // records via cacheRoute (resolveCacheTags); the hit path records here.
310
+ recordRequestTags(cached.tags);
253
311
 
254
- // Replay handle data
312
+ // Replay handle data. An empty string means the route pushed no handles —
313
+ // skip the decode entirely (the common case). Otherwise decode the
314
+ // Flight-encoded blob; a decode failure skips handle restore but keeps the
315
+ // valid cached segments.
255
316
  const handleStore = _getRequestContext()?._handleStore;
256
- if (handleStore) {
257
- restoreHandles(cached.handles, handleStore);
317
+ if (handleStore && cached.handles) {
318
+ const handlesRecord = await decodeHandles(cached.handles);
319
+ if (handlesRecord) {
320
+ restoreHandles(handlesRecord, handleStore);
321
+ }
258
322
  }
259
323
 
260
324
  if (INTERNAL_RANGO_DEBUG) {
@@ -268,11 +332,37 @@ export class CacheScope {
268
332
 
269
333
  return { segments, shouldRevalidate };
270
334
  } catch (error) {
271
- console.error(`[CacheScope] Failed to lookup ${key}:`, error);
335
+ // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
336
+ // (resolveKey). Either way degrade to a cache miss so the render proceeds.
337
+ reportCacheError(
338
+ error,
339
+ "cache-read",
340
+ `[CacheScope] lookup ${key ?? "(key resolution failed)"}`,
341
+ );
272
342
  return null;
273
343
  }
274
344
  }
275
345
 
346
+ /**
347
+ * Record this scope's segment-DSL cache({ tags }) into the request tag union
348
+ * synchronously, under the same gate cacheRoute() uses for a write.
349
+ *
350
+ * cacheRoute() already records these tags, but it is invoked inside
351
+ * requestCtx.waitUntil() by the cache-store middleware (and the proactive path
352
+ * re-resolves the whole tree before calling it), so its recording is deferred
353
+ * and RACES the document cache's post-body-drain snapshot of _requestTags. On a
354
+ * first-write (segment-cache miss) the document tag union could miss these
355
+ * tags, and updateTag()/revalidateTag() would then fail to invalidate the
356
+ * cached document until a later write reseeded it. Calling this synchronously
357
+ * in the request pipeline (before the snapshot) closes that window. Idempotent
358
+ * (the tag union is a Set), so the duplicate record in cacheRoute is harmless.
359
+ */
360
+ recordTags(requestCtx: RequestContext | undefined): void {
361
+ if (!this.enabled) return;
362
+ if (!this.conditionAllows("write")) return;
363
+ recordRequestTags(resolveCacheTags(this.config, requestCtx), requestCtx);
364
+ }
365
+
276
366
  /**
277
367
  * Cache all segments for a route (non-blocking via waitUntil)
278
368
  * Single cache entry per route request.
@@ -311,6 +401,10 @@ export class CacheScope {
311
401
  // Resolve cache key early (while request context is available)
312
402
  const key = await this.resolveKey(pathname, params, isIntercept);
313
403
 
404
+ // Resolve tags early (while request context is available, before waitUntil)
405
+ const tags = resolveCacheTags(this.config, requestCtx);
406
+ recordRequestTags(tags, requestCtx);
407
+
314
408
  // Check if this is a partial request (navigation) vs document request
315
409
  const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
316
410
 
@@ -367,13 +461,20 @@ export class CacheScope {
367
461
  );
368
462
  }
369
463
 
370
- // Serialize non-loader segments only
371
- const serializedSegments = await serializeSegments(nonLoaderSegments);
464
+ // Serialize segments and Flight-encode handles in parallel. Handles go
465
+ // through the codec (not raw into the entry) so Promise/ReactNode handle
466
+ // values survive a JSON-serializing store — see encodeHandles.
467
+ const { serializeSegments } = await import("./segment-codec.js");
468
+ const [serializedSegments, encodedHandles] = await Promise.all([
469
+ serializeSegments(nonLoaderSegments),
470
+ encodeHandles(handles),
471
+ ]);
372
472
 
373
473
  const data: CachedEntryData = {
374
474
  segments: serializedSegments,
375
- handles,
475
+ handles: encodedHandles,
376
476
  expiresAt: Date.now() + ttl * 1000,
477
+ tags,
377
478
  };
378
479
 
379
480
  if (INTERNAL_RANGO_DEBUG) {
@@ -391,7 +492,11 @@ export class CacheScope {
391
492
  );
392
493
  }
393
494
  } catch (error) {
394
- console.error(`[CacheScope] Failed to cache ${key}:`, error);
495
+ reportCacheError(
496
+ error,
497
+ "cache-write",
498
+ `[CacheScope] Failed to cache ${key}`,
499
+ );
395
500
  }
396
501
  });
397
502
  }
@@ -0,0 +1,149 @@
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 content for later invalidation via updateTag() / revalidateTag().
42
+ *
43
+ * cacheTag() serves two forms depending on what is active when it runs:
44
+ *
45
+ * 1. Inside a "use cache" function — the DEFAULT. The tags go to the current
46
+ * cache entry; `revalidateTag(tag)` drops that entry. Tags are additive
47
+ * (multiple calls accumulate), and normalizeTag() is the single chokepoint so
48
+ * a padded write matches an unpadded invalidate.
49
+ *
50
+ * 2. Render-callable (#648) — no "use cache" scope active, but a request context
51
+ * is present. The tags record onto the request's DOCUMENT artifact
52
+ * (ctx._requestTags) instead of throwing. The collection layers already exist:
53
+ * PPR shell capture unions _requestTags into the shell entry, the document
54
+ * cache tags the full-page entry with it, and prerender build contexts seed
55
+ * their own set — so a server component that renders into a shell makes
56
+ * `revalidateTag("campaign:spring")` evict that shell with ZERO cache()/"use
57
+ * cache" in its tree. This is PPR's DERIVATIVE invalidation: PPR is
58
+ * execution-PRESERVING (everything still runs underneath; only document bytes
59
+ * are shortcut), so its tags ride this existing instrument rather than a
60
+ * first-class ppr key/tag API. The shell-expiry invariant holds by
61
+ * construction — baked ⇒ evicts (bake-lane loaders execute during capture and
62
+ * record here), hole ⇒ fresh (masked loaders behind a renderable loading()
63
+ * never execute during capture, so nothing under a hole can tag the shell).
64
+ *
65
+ * Inside a cache() DSL segment the render-callable form records at the DOCUMENT
66
+ * level, not the segment (only the "use cache" runtime enters the tag scope) — a
67
+ * documented semantic, not a filtered one. An empty/whitespace-only tag is
68
+ * dropped in both forms (the render-callable form silently, via normalizeTags in
69
+ * recordRequestTags; the scope form with a dev warning).
70
+ *
71
+ * With neither a scope nor a request context, cacheTag() throws.
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * // Form 1 — inside "use cache":
76
+ * async function getProduct(ctx) {
77
+ * "use cache";
78
+ * cacheTag(`product:${ctx.params.id}`, "products");
79
+ * return db.getProduct(ctx.params.id);
80
+ * }
81
+ *
82
+ * // Form 2 — render-callable, tags the shell/document from a server component:
83
+ * function CampaignBanner() {
84
+ * cacheTag("campaign:spring");
85
+ * return <aside>Spring sale</aside>;
86
+ * }
87
+ * ```
88
+ */
89
+ export function cacheTag(...tags: string[]): void {
90
+ const store = cacheTagStorage.getStore();
91
+ if (store) {
92
+ // Form 1: "use cache" scope wins — tag the cache entry (unchanged).
93
+ for (const tag of tags) {
94
+ const normalized = normalizeTag(tag);
95
+ if (normalized === null) {
96
+ if (process.env.NODE_ENV !== "production") {
97
+ console.warn(`[cacheTag] Ignoring empty or whitespace-only tag.`);
98
+ }
99
+ continue;
100
+ }
101
+ store.add(normalized);
102
+ }
103
+ return;
104
+ }
105
+
106
+ const reqCtx = _getRequestContext();
107
+ if (reqCtx?._requestTags) {
108
+ // Form 2: render-callable — tag the request's document artifact. See the
109
+ // JSDoc above for the composition doctrine and the baked/hole invariant.
110
+ recordRequestTags(tags, reqCtx);
111
+ return;
112
+ }
113
+
114
+ throw new Error(
115
+ 'cacheTag() must be called inside a "use cache" function or during a request render.',
116
+ );
117
+ }
118
+
119
+ export function recordRequestTags(
120
+ tags: Iterable<string> | undefined,
121
+ ctx: RequestContext | undefined = _getRequestContext(),
122
+ ): void {
123
+ if (!tags) return;
124
+ const set = ctx?._requestTags;
125
+ if (!set) return;
126
+ for (const tag of tags) {
127
+ const normalized = normalizeTag(tag);
128
+ if (normalized !== null) set.add(normalized);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
134
+ * accumulate into the returned Set.
135
+ *
136
+ * The returned Set is the LIVE reference - the caller must await `result`
137
+ * before reading `tags`, because an async cached function may call cacheTag()
138
+ * after an await boundary.
139
+ *
140
+ * @internal Used by cache-runtime.ts to wrap "use cache" execution.
141
+ */
142
+ export function runWithCacheTagScope<T>(fn: () => T): {
143
+ result: T;
144
+ tags: Set<string>;
145
+ } {
146
+ const tagSet = new Set<string>();
147
+ const result = cacheTagStorage.run(tagSet, fn);
148
+ return { result, tags: tagSet };
149
+ }
@@ -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;