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}