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}