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::policy::EntityPolicyConfig;
50use crate::response::CacheState;
51
52/// A cached value with expiration metadata.
53///
54/// Wraps any data type `T` with optional timestamps for staleness and expiration.
55/// This enables time-based cache invalidation and stale-while-revalidate patterns.
56///
57/// # Type Parameter
58///
59/// * `T` - The cached data type
60///
61/// # Example
62///
63/// ```
64/// use hitbox_core::value::CacheValue;
65/// use chrono::Utc;
66/// use std::time::Duration;
67///
68/// // Create a cache value that expires in 1 hour
69/// let expire_time = Utc::now() + chrono::Duration::hours(1);
70/// let value = CacheValue::new("user_data", Some(expire_time), None);
71///
72/// // Access data via getter
73/// assert_eq!(value.data(), &"user_data");
74///
75/// // Check remaining TTL
76/// if let Some(ttl) = value.ttl() {
77///     println!("Expires in {} seconds", ttl.as_secs());
78/// }
79///
80/// // Extract the data
81/// let data = value.into_inner();
82/// ```
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct CacheValue<T> {
85    data: T,
86    expire: Option<DateTime<Utc>>,
87    stale: Option<DateTime<Utc>>,
88}
89
90impl<T> CacheValue<T> {
91    /// Creates a new cache value with the given data and timestamps.
92    ///
93    /// # Arguments
94    ///
95    /// * `data` - The data to cache
96    /// * `expire` - When the data expires (becomes invalid)
97    /// * `stale` - When the data becomes stale (should refresh in background)
98    pub fn new(data: T, expire: Option<DateTime<Utc>>, stale: Option<DateTime<Utc>>) -> Self {
99        CacheValue {
100            data,
101            expire,
102            stale,
103        }
104    }
105
106    /// Creates a new cache value using timestamps derived from an [`EntityPolicyConfig`].
107    ///
108    /// Converts the config's TTL and stale TTL durations into absolute timestamps
109    /// relative to the current time.
110    pub fn from_config(data: T, config: &EntityPolicyConfig) -> Self {
111        Self::new(
112            data,
113            config.ttl.map(|d| Utc::now() + d),
114            config.stale_ttl.map(|d| Utc::now() + d),
115        )
116    }
117
118    /// Returns a reference to the cached data.
119    #[inline]
120    pub fn data(&self) -> &T {
121        &self.data
122    }
123
124    /// Returns when the data expires (becomes invalid).
125    #[inline]
126    pub fn expire(&self) -> Option<DateTime<Utc>> {
127        self.expire
128    }
129
130    /// Returns when the data becomes stale (should refresh in background).
131    #[inline]
132    pub fn stale(&self) -> Option<DateTime<Utc>> {
133        self.stale
134    }
135
136    /// Consumes the cache value and returns the inner data.
137    ///
138    /// Discards the expiration metadata.
139    pub fn into_inner(self) -> T {
140        self.data
141    }
142
143    /// Consumes the cache value and returns metadata and data separately.
144    ///
145    /// Useful when you need to inspect or modify the metadata independently.
146    pub fn into_parts(self) -> (CacheMeta, T) {
147        (CacheMeta::new(self.expire, self.stale), self.data)
148    }
149
150    /// Calculate TTL (time-to-live) from the expire time.
151    ///
152    /// Returns `Some(Duration)` if there's a valid expire time in the future,
153    /// or `None` if there's no expire time or it's already expired.
154    pub fn ttl(&self) -> Option<Duration> {
155        self.expire.and_then(|expire| {
156            let duration = expire.signed_duration_since(Utc::now());
157            if duration.num_seconds() > 0 {
158                Some(Duration::from_secs(duration.num_seconds() as u64))
159            } else {
160                None
161            }
162        })
163    }
164}
165
166impl<T> CacheValue<T> {
167    /// Check the cache state based on expire/stale timestamps.
168    ///
169    /// Returns `CacheState<CacheValue<T>>` preserving the original value with metadata.
170    /// This is a sync operation - just checks timestamps, no conversion.
171    ///
172    /// The caller is responsible for converting to Response via `from_cached()` when needed.
173    pub fn cache_state(self) -> CacheState<Self> {
174        let now = Utc::now();
175        if let Some(expire) = self.expire
176            && expire <= now
177        {
178            CacheState::Expired(self)
179        } else if let Some(stale) = self.stale
180            && stale <= now
181        {
182            CacheState::Stale(self)
183        } else {
184            CacheState::Actual(self)
185        }
186    }
187}
188
189/// Cache expiration metadata without the data.
190///
191/// Contains just the staleness and expiration timestamps. Useful for
192/// passing metadata around without copying the cached data.
193///
194/// # Fields
195///
196/// * `expire` - When the data expires (becomes invalid)
197/// * `stale` - When the data becomes stale (should refresh in background)
198pub struct CacheMeta {
199    /// When the cached data expires and becomes invalid.
200    pub expire: Option<DateTime<Utc>>,
201    /// When the cached data becomes stale and should be refreshed.
202    pub stale: Option<DateTime<Utc>>,
203}
204
205impl CacheMeta {
206    /// Creates new cache metadata with the given timestamps.
207    pub fn new(expire: Option<DateTime<Utc>>, stale: Option<DateTime<Utc>>) -> CacheMeta {
208        CacheMeta { expire, stale }
209    }
210}
211
212impl CacheValue<Raw> {
213    /// Returns the estimated memory usage of this cache value in bytes.
214    ///
215    /// This includes:
216    /// - Fixed struct overhead (CacheValue fields)
217    /// - The serialized data bytes
218    pub fn memory_size(&self) -> usize {
219        // Fixed overhead: CacheValue struct (data pointer + metadata)
220        let fixed_overhead = size_of::<Self>();
221
222        // Variable content: the actual byte data
223        let content = self.data.len();
224
225        fixed_overhead + content
226    }
227}