use crate::HeaderValue;
use coarsetime::Instant;
use std::{
cell::RefCell,
time::{Duration, SystemTime, UNIX_EPOCH},
};
struct CachedDate {
expires: Instant,
value: HeaderValue,
}
thread_local! {
static CACHE: RefCell<Option<CachedDate>> = const { RefCell::new(None) };
}
pub(crate) fn current_date_header() -> HeaderValue {
let now = Instant::now_without_cache_update();
CACHE.with_borrow_mut(|cache| date_header(cache, now, SystemTime::now))
}
fn date_header(
cache: &mut Option<CachedDate>,
now: Instant,
wall_clock: impl FnOnce() -> SystemTime,
) -> HeaderValue {
if let Some(cached) = cache
&& now < cached.expires
{
return cached.value.clone();
}
let wall = wall_clock();
let into_second = wall.duration_since(UNIX_EPOCH).map_or(Duration::ZERO, |d| {
Duration::from_nanos(u64::from(d.subsec_nanos()))
});
let value = HeaderValue::from(httpdate::fmt_http_date(wall));
*cache = Some(CachedDate {
expires: now + Duration::from_secs(1).saturating_sub(into_second).into(),
value: value.clone(),
});
value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn well_formed() {
let s = current_date_header();
let s = s.as_str().expect("date is utf8");
assert_eq!(s.len(), 29, "IMF-fixdate is fixed-length");
assert!(s.ends_with(" GMT"), "got {s:?}");
}
#[test]
fn cached_until_the_next_second() {
let start = Instant::now_without_cache_update();
let wall = UNIX_EPOCH + Duration::from_millis(1_700_000_000_250);
let mut cache = None;
let first = date_header(&mut cache, start, || wall);
let later = start + Duration::from_millis(740).into();
let cached = date_header(&mut cache, later, || panic!("should not read the clock"));
assert_eq!(cached, first);
let next_second = start + Duration::from_millis(760).into();
let refreshed = date_header(&mut cache, next_second, || {
wall + Duration::from_millis(760)
});
assert_ne!(refreshed, first);
assert_eq!(refreshed.as_str(), Some("Tue, 14 Nov 2023 22:13:21 GMT"));
}
}