Skip to main content

hitbox_core/
value.rs

1//! Cached value types with expiration metadata.
2//!
3//! This module provides types for wrapping cached data with expiration
4//! and staleness timestamps:
5//!
6//! - [`CacheValue`] - Cached data with optional expire and stale timestamps
7//! - [`CacheMeta`] - Just the metadata without the data
8//!
9//! ## Expiration vs Staleness
10//!
11//! Cache entries have two time-based states:
12//!
13//! - **Stale** - The data is still usable but should be refreshed in the background
14//! - **Expired** - The data is no longer valid and must be refreshed before use
15//!
16//! This allows implementing "stale-while-revalidate" caching patterns where
17//! stale data is served immediately while fresh data is fetched asynchronously.
18//!
19//! ## Cache States
20//!
21//! The [`CacheValue::cache_state`] method evaluates timestamps and returns:
22//!
23//! - [`CacheState::Actual`] - Data is fresh (neither stale nor expired)
24//! - [`CacheState::Stale`] - Data is stale but not expired
25//! - [`CacheState::Expired`] - Data has expired
26//!
27//! ```ignore
28//! use hitbox_core::value::CacheValue;
29//! use chrono::Utc;
30//!
31//! let value = CacheValue::new(
32//!     "cached data",
33//!     Some(Utc::now() + chrono::Duration::hours(1)),  // expires in 1 hour
34//!     Some(Utc::now() + chrono::Duration::minutes(5)), // stale in 5 minutes
35//! );
36//!
37//! match value.cache_state() {
38//!     CacheState::Actual(v) => println!("Fresh: {:?}", v.data()),
39//!     CacheState::Stale(v) => println!("Stale, refresh in background"),
40//!     CacheState::Expired(v) => println!("Expired, must refresh"),
41//! }
42//! ```
43
44use chrono::{DateTime, Utc};
45use std::mem::size_of;
46use std::time::Duration;
47
48use crate::Raw;
49use crate::response::CacheState;
50
51/// A cached value with expiration metadata.
52///
53/// Wraps any data type `T` with optional timestamps for staleness and expiration.
54/// This enables time-based cache invalidation and stale-while-revalidate patterns.
55///
56/// # Type Parameter
57///
58/// * `T` - The cached data type
59///
60/// # Example
61///
62/// ```
63/// use hitbox_core::value::CacheValue;
64/// use chrono::Utc;
65/// use std::time::Duration;
66///
67/// // Create a cache value that expires in 1 hour
68/// let expire_time = Utc::now() + chrono::Duration::hours(1);
69/// let value = CacheValue::new("user_data", Some(expire_time), None);
70///
71/// // Access data via getter
72/// assert_eq!(value.data(), &"user_data");
73///
74/// // Check remaining TTL
75/// if let Some(ttl) = value.ttl() {
76///     println!("Expires in {} seconds", ttl.as_secs());
77/// }
78///
79/// // Extract the data
80/// let data = value.into_inner();
81/// ```
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct CacheValue<T> {
84    data: T,
85    expire: Option<DateTime<Utc>>,
86    stale: Option<DateTime<Utc>>,
87}
88
89impl<T> CacheValue<T> {
90    /// Creates a new cache value with the given data and timestamps.
91    ///
92    /// # Arguments
93    ///
94    /// * `data` - The data to cache
95    /// * `expire` - When the data expires (becomes invalid)
96    /// * `stale` - When the data becomes stale (should refresh in background)
97    pub fn new(data: T, expire: Option<DateTime<Utc>>, stale: Option<DateTime<Utc>>) -> Self {
98        CacheValue {
99            data,
100            expire,
101            stale,
102        }
103    }
104
105    /// Returns a reference to the cached data.
106    #[inline]
107    pub fn data(&self) -> &T {
108        &self.data
109    }
110
111    /// Returns when the data expires (becomes invalid).
112    #[inline]
113    pub fn expire(&self) -> Option<DateTime<Utc>> {
114        self.expire
115    }
116
117    /// Returns when the data becomes stale (should refresh in background).
118    #[inline]
119    pub fn stale(&self) -> Option<DateTime<Utc>> {
120        self.stale
121    }
122
123    /// Consumes the cache value and returns the inner data.
124    ///
125    /// Discards the expiration metadata.
126    pub fn into_inner(self) -> T {
127        self.data
128    }
129
130    /// Consumes the cache value and returns metadata and data separately.
131    ///
132    /// Useful when you need to inspect or modify the metadata independently.
133    pub fn into_parts(self) -> (CacheMeta, T) {
134        (CacheMeta::new(self.expire, self.stale), self.data)
135    }
136
137    /// Calculate TTL (time-to-live) from the expire time.
138    ///
139    /// Returns `Some(Duration)` if there's a valid expire time in the future,
140    /// or `None` if there's no expire time or it's already expired.
141    pub fn ttl(&self) -> Option<Duration> {
142        self.expire.and_then(|expire| {
143            let duration = expire.signed_duration_since(Utc::now());
144            if duration.num_seconds() > 0 {
145                Some(Duration::from_secs(duration.num_seconds() as u64))
146            } else {
147                None
148            }
149        })
150    }
151}
152
153impl<T> CacheValue<T> {
154    /// Check the cache state based on expire/stale timestamps.
155    ///
156    /// Returns `CacheState<CacheValue<T>>` preserving the original value with metadata.
157    /// This is a sync operation - just checks timestamps, no conversion.
158    ///
159    /// The caller is responsible for converting to Response via `from_cached()` when needed.
160    pub fn cache_state(self) -> CacheState<Self> {
161        let now = Utc::now();
162        if let Some(expire) = self.expire
163            && expire <= now
164        {
165            CacheState::Expired(self)
166        } else if let Some(stale) = self.stale
167            && stale <= now
168        {
169            CacheState::Stale(self)
170        } else {
171            CacheState::Actual(self)
172        }
173    }
174}
175
176/// Cache expiration metadata without the data.
177///
178/// Contains just the staleness and expiration timestamps. Useful for
179/// passing metadata around without copying the cached data.
180///
181/// # Fields
182///
183/// * `expire` - When the data expires (becomes invalid)
184/// * `stale` - When the data becomes stale (should refresh in background)
185pub struct CacheMeta {
186    /// When the cached data expires and becomes invalid.
187    pub expire: Option<DateTime<Utc>>,
188    /// When the cached data becomes stale and should be refreshed.
189    pub stale: Option<DateTime<Utc>>,
190}
191
192impl CacheMeta {
193    /// Creates new cache metadata with the given timestamps.
194    pub fn new(expire: Option<DateTime<Utc>>, stale: Option<DateTime<Utc>>) -> CacheMeta {
195        CacheMeta { expire, stale }
196    }
197}
198
199impl CacheValue<Raw> {
200    /// Returns the estimated memory usage of this cache value in bytes.
201    ///
202    /// This includes:
203    /// - Fixed struct overhead (CacheValue fields)
204    /// - The serialized data bytes
205    pub fn memory_size(&self) -> usize {
206        // Fixed overhead: CacheValue struct (data pointer + metadata)
207        let fixed_overhead = size_of::<Self>();
208
209        // Variable content: the actual byte data
210        let content = self.data.len();
211
212        fixed_overhead + content
213    }
214}