use axum::http::HeaderMap; use brk_types::{BlockHashPrefix, Version}; use crate::{VERSION, etag::Etag, extended::HeaderMapExtended}; use super::{ mode::{CDN_LIVE, cdn_cached}, strategy::CacheStrategy, }; // Browser-facing: always revalidate via ETag. `no-cache` means "cache it but // check before use" (not "don't cache"); ETag makes the check cheap. const CC: &str = "public, no-cache, stale-if-error=86400"; // Errors: short, must-revalidate, no `stale-if-error` (we don't want a 24h-old // error served when origin recovers). Same string for browser and CDN. const CC_ERROR: &str = "public, max-age=1, must-revalidate"; /// Resolved cache parameters: an ETag plus the two Cache-Control directives. pub struct CacheParams { pub etag: Etag, cache_control: &'static str, cdn_cache_control: &'static str, } impl CacheParams { fn tip(tip: BlockHashPrefix) -> Self { Self { etag: format!("t{:x}", *tip).into(), cache_control: CC, cdn_cache_control: CDN_LIVE, } } fn immutable(version: Version) -> Self { Self { etag: format!("i{version}").into(), cache_control: CC, cdn_cache_control: cdn_cached(), } } fn block_bound(version: Version, prefix: BlockHashPrefix) -> Self { Self { etag: format!("b{version}-{:x}", *prefix).into(), cache_control: CC, cdn_cache_control: cdn_cached(), } } /// Deploy-tied response: etag from the build version. Used directly /// by static handlers (OpenAPI spec, scalar bundle) that don't have /// a [`CacheStrategy`] context. pub fn deploy() -> Self { Self { etag: format!("d{VERSION}").into(), cache_control: CC, cdn_cache_control: cdn_cached(), } } fn mempool_hash(hash: u64) -> Self { Self { etag: format!("m{hash:x}").into(), cache_control: CC, cdn_cache_control: CDN_LIVE, } } /// Series query: tail-bound (`end >= total`) gets LIVE, historical gets CACHED. /// Etag distinguishes the two: tail uses tip hash (per-block + reorgs), /// historical uses total length (only changes when new data is appended). pub fn series(version: Version, total: usize, end: usize, hash: BlockHashPrefix) -> Self { let v = u32::from(version); if end >= total { Self { etag: format!("s{v}-{:x}", *hash).into(), cache_control: CC, cdn_cache_control: CDN_LIVE, } } else { Self { etag: format!("s{v}-{total}").into(), cache_control: CC, cdn_cache_control: cdn_cached(), } } } /// Error response: keeps the originating ETag (so retries can 304), /// uses [`CC_ERROR`] for both browser and CDN. pub fn error(etag: Etag) -> Self { Self { etag, cache_control: CC_ERROR, cdn_cache_control: CC_ERROR, } } /// Apply error cache-control headers without an ETag. Used for synthesized /// errors (panics, fallback handlers) that have no resource etag. pub fn apply_error_cache_control(headers: &mut HeaderMap) { headers.insert_cache_control(CC_ERROR); headers.insert_cdn_cache_control(CC_ERROR); } pub fn matches_etag(&self, headers: &HeaderMap) -> bool { headers.has_etag(self.etag.as_str()) } /// Write this cache policy (etag + cache-control + cdn-cache-control) onto a response's headers. pub fn apply_to(&self, headers: &mut HeaderMap) { headers.insert_etag(self.etag.as_str()); headers.insert_cache_control(self.cache_control); headers.insert_cdn_cache_control(self.cdn_cache_control); } pub fn resolve(strategy: &CacheStrategy, tip: BlockHashPrefix) -> Self { match strategy { CacheStrategy::Tip => Self::tip(tip), CacheStrategy::Immutable(v) => Self::immutable(*v), CacheStrategy::BlockBound(v, prefix) => Self::block_bound(*v, *prefix), CacheStrategy::Deploy => Self::deploy(), CacheStrategy::MempoolHash(hash) => Self::mempool_hash(*hash), } } } #[cfg(test)] mod tests { use super::*; fn v(n: u32) -> Version { Version::new(n) } fn h(n: u64) -> BlockHashPrefix { BlockHashPrefix::from(n) } #[test] fn series_tail_uses_tip_hash() { let p = CacheParams::series(v(3), 100, 100, h(0xabcd)); assert_eq!(p.etag.as_str(), "s3-abcd"); } #[test] fn series_historical_uses_total() { let p = CacheParams::series(v(3), 100, 50, h(0xabcd)); assert_eq!(p.etag.as_str(), "s3-100"); } #[test] fn series_historical_ignores_tip_hash() { let a = CacheParams::series(v(3), 100, 50, h(0xabcd)); let b = CacheParams::series(v(3), 100, 50, h(0xdead)); assert_eq!(a.etag.as_str(), b.etag.as_str()); } #[test] fn series_tail_changes_with_tip_hash() { let a = CacheParams::series(v(3), 100, 100, h(0xabcd)); let b = CacheParams::series(v(3), 100, 100, h(0xdead)); assert_ne!(a.etag.as_str(), b.etag.as_str()); } }