dioxus_provider/
cache.rs

1//! Cache management and async state types for dioxus-provider
2
3use std::{
4    any::Any,
5    collections::HashMap,
6    sync::{
7        Arc, Mutex,
8        atomic::{AtomicU32, Ordering},
9    },
10    time::Duration,
11};
12use tracing::debug;
13
14use crate::platform::{DEFAULT_MAX_CACHE_SIZE, DEFAULT_UNUSED_THRESHOLD};
15
16// Platform-specific time imports
17#[cfg(not(target_family = "wasm"))]
18use std::time::Instant;
19#[cfg(target_family = "wasm")]
20use web_time::Instant;
21
22/// Represents the state of an async operation
23#[derive(Clone, PartialEq)]
24pub enum AsyncState<T, E> {
25    /// The operation is currently loading
26    Loading,
27    /// The operation completed successfully with data
28    Success(T),
29    /// The operation failed with an error
30    Error(E),
31}
32
33impl<T, E> AsyncState<T, E> {
34    /// Returns true if the state is currently loading
35    pub fn is_loading(&self) -> bool {
36        matches!(self, AsyncState::Loading)
37    }
38
39    /// Returns true if the state contains successful data
40    pub fn is_success(&self) -> bool {
41        matches!(self, AsyncState::Success(_))
42    }
43
44    /// Returns true if the state contains an error
45    pub fn is_error(&self) -> bool {
46        matches!(self, AsyncState::Error(_))
47    }
48
49    /// Returns the data if successful, None otherwise
50    pub fn data(&self) -> Option<&T> {
51        match self {
52            AsyncState::Success(data) => Some(data),
53            _ => None,
54        }
55    }
56
57    /// Returns the error if failed, None otherwise
58    pub fn error(&self) -> Option<&E> {
59        match self {
60            AsyncState::Error(error) => Some(error),
61            _ => None,
62        }
63    }
64}
65
66/// A type-erased cache entry for storing provider results with timestamp and reference counting
67#[derive(Clone)]
68pub struct CacheEntry {
69    data: Arc<dyn Any + Send + Sync>,
70    cached_at: Instant,
71    reference_count: Arc<AtomicU32>,
72    last_accessed: Arc<Mutex<Instant>>,
73    access_count: Arc<AtomicU32>,
74}
75
76impl CacheEntry {
77    pub fn new<T: Clone + Send + Sync + 'static>(data: T) -> Self {
78        let now = Instant::now();
79        Self {
80            data: Arc::new(data),
81            cached_at: now,
82            reference_count: Arc::new(AtomicU32::new(0)),
83            last_accessed: Arc::new(Mutex::new(now)),
84            access_count: Arc::new(AtomicU32::new(0)),
85        }
86    }
87
88    pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
89        // Update last accessed time and access count
90        if let Ok(mut last_accessed) = self.last_accessed.lock() {
91            *last_accessed = Instant::now();
92        }
93        self.access_count.fetch_add(1, Ordering::SeqCst);
94        self.data.downcast_ref::<T>().cloned()
95    }
96
97    pub fn is_expired(&self, expiration: Duration) -> bool {
98        self.cached_at.elapsed() > expiration
99    }
100
101    pub fn is_stale(&self, stale_time: Duration) -> bool {
102        self.cached_at.elapsed() > stale_time
103    }
104
105    /// Increment reference count when a provider hook starts using this entry
106    pub fn add_reference(&self) {
107        self.reference_count.fetch_add(1, Ordering::SeqCst);
108    }
109
110    /// Decrement reference count when a provider hook stops using this entry
111    pub fn remove_reference(&self) {
112        self.reference_count.fetch_sub(1, Ordering::SeqCst);
113    }
114
115    /// Get current reference count
116    pub fn reference_count(&self) -> u32 {
117        self.reference_count.load(Ordering::SeqCst)
118    }
119
120    /// Get current access count
121    pub fn access_count(&self) -> u32 {
122        self.access_count.load(Ordering::SeqCst)
123    }
124
125    /// Check if this entry hasn't been accessed for the given duration
126    pub fn is_unused_for(&self, duration: Duration) -> bool {
127        if let Ok(last_accessed) = self.last_accessed.lock() {
128            last_accessed.elapsed() > duration
129        } else {
130            false
131        }
132    }
133
134    /// Get the time since this entry was last accessed
135    pub fn time_since_last_access(&self) -> Duration {
136        if let Ok(last_accessed) = self.last_accessed.lock() {
137            last_accessed.elapsed()
138        } else {
139            Duration::from_secs(0)
140        }
141    }
142
143    /// Get the age of this entry
144    pub fn age(&self) -> Duration {
145        self.cached_at.elapsed()
146    }
147}
148
149/// Global cache for provider results with automatic cleanup
150#[derive(Clone, Default)]
151pub struct ProviderCache {
152    pub cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
153}
154
155impl ProviderCache {
156    /// Create a new provider cache
157    pub fn new() -> Self {
158        Self::default()
159    }
160
161    /// Get a cached result by key
162    pub fn get<T: Clone + Send + Sync + 'static>(&self, key: &str) -> Option<T> {
163        self.cache.lock().ok()?.get(key)?.get::<T>()
164    }
165
166    /// Get a cached result by key, checking for expiration with a specific expiration duration
167    pub fn get_with_expiration<T: Clone + Send + Sync + 'static>(
168        &self,
169        key: &str,
170        expiration: Option<Duration>,
171    ) -> Option<T> {
172        // First, check if the entry exists and is expired
173        let is_expired = {
174            let cache_guard = self.cache.lock().ok()?;
175            let entry = cache_guard.get(key)?;
176
177            if let Some(exp_duration) = expiration {
178                entry.is_expired(exp_duration)
179            } else {
180                false
181            }
182        };
183
184        // If expired, remove the entry
185        if is_expired {
186            if let Ok(mut cache) = self.cache.lock() {
187                cache.remove(key);
188                debug!(
189                    "๐Ÿ—‘๏ธ [CACHE EXPIRATION] Removing expired cache entry for key: {}",
190                    key
191                );
192            }
193            return None;
194        }
195
196        // Entry is not expired, return the data
197        let cache_guard = self.cache.lock().ok()?;
198        let entry = cache_guard.get(key)?;
199        entry.get::<T>()
200    }
201
202    /// Get cached data with staleness information for SWR behavior
203    pub fn get_with_staleness<T: Clone + Send + Sync + 'static>(
204        &self,
205        key: &str,
206        stale_time: Option<Duration>,
207        expiration: Option<Duration>,
208    ) -> Option<(T, bool)> {
209        let cache_guard = self.cache.lock().ok()?;
210        let entry = cache_guard.get(key)?;
211
212        // Check if expired first
213        if let Some(exp_duration) = expiration {
214            if entry.is_expired(exp_duration) {
215                return None;
216            }
217        }
218
219        // Get the data
220        let data = entry.get::<T>()?;
221
222        // Check if stale
223        let is_stale = if let Some(stale_duration) = stale_time {
224            entry.is_stale(stale_duration)
225        } else {
226            false
227        };
228
229        Some((data, is_stale))
230    }
231
232    /// Set a cached result by key
233    pub fn set<T: Clone + Send + Sync + 'static>(&self, key: String, value: T) {
234        if let Ok(mut cache) = self.cache.lock() {
235            cache.insert(key.clone(), CacheEntry::new(value));
236            debug!("๐Ÿ“Š [CACHE-STORE] Stored data for key: {}", key);
237        }
238    }
239
240    /// Remove a cached result by key
241    pub fn remove(&self, key: &str) -> bool {
242        if let Ok(mut cache) = self.cache.lock() {
243            cache.remove(key).is_some()
244        } else {
245            false
246        }
247    }
248
249    /// Invalidate a cached result by key (alias for remove)
250    pub fn invalidate(&self, key: &str) {
251        self.remove(key);
252        debug!(
253            "๐Ÿ—‘๏ธ [CACHE-INVALIDATE] Invalidated cache entry for key: {}",
254            key
255        );
256    }
257
258    /// Clear all cached results
259    pub fn clear(&self) {
260        if let Ok(mut cache) = self.cache.lock() {
261            let count = cache.len();
262            cache.clear();
263            debug!("๐Ÿ—‘๏ธ [CACHE-CLEAR] Cleared {} cache entries", count);
264        }
265    }
266
267    /// Get the number of cached entries
268    pub fn size(&self) -> usize {
269        self.cache.lock().map(|cache| cache.len()).unwrap_or(0)
270    }
271
272    /// Clean up unused entries based on access time
273    pub fn cleanup_unused_entries(&self, unused_threshold: Duration) -> usize {
274        if let Ok(mut cache) = self.cache.lock() {
275            let initial_size = cache.len();
276            cache.retain(|key, entry| {
277                let should_keep =
278                    !entry.is_unused_for(unused_threshold) || entry.reference_count() > 0;
279                if !should_keep {
280                    debug!("๐Ÿงน [CACHE-CLEANUP] Removing unused entry: {}", key);
281                }
282                should_keep
283            });
284            let removed = initial_size - cache.len();
285            if removed > 0 {
286                debug!("๐Ÿงน [CACHE-CLEANUP] Removed {} unused entries", removed);
287            }
288            removed
289        } else {
290            0
291        }
292    }
293
294    /// Evict least recently used entries to maintain cache size limit
295    pub fn evict_lru_entries(&self, max_size: usize) -> usize {
296        if let Ok(mut cache) = self.cache.lock() {
297            if cache.len() <= max_size {
298                return 0;
299            }
300
301            // Convert to vector for sorting
302            let mut entries: Vec<_> = cache.drain().collect();
303
304            // Sort by last access time (oldest first)
305            entries.sort_by(|(_, a), (_, b)| {
306                a.time_since_last_access().cmp(&b.time_since_last_access())
307            });
308
309            // Keep the most recently used entries
310            let to_keep = entries.split_off(entries.len().saturating_sub(max_size));
311            let evicted = entries.len();
312
313            // Rebuild cache with kept entries
314            cache.extend(to_keep);
315
316            if evicted > 0 {
317                debug!(
318                    "๐Ÿ—‘๏ธ [LRU-EVICT] Evicted {} entries due to cache size limit",
319                    evicted
320                );
321            }
322            evicted
323        } else {
324            0
325        }
326    }
327
328    /// Perform comprehensive cache maintenance
329    pub fn maintain(&self) -> CacheMaintenanceStats {
330        CacheMaintenanceStats {
331            unused_removed: self.cleanup_unused_entries(DEFAULT_UNUSED_THRESHOLD),
332            lru_evicted: self.evict_lru_entries(DEFAULT_MAX_CACHE_SIZE),
333            final_size: self.size(),
334        }
335    }
336
337    /// Get cache statistics
338    pub fn stats(&self) -> CacheStats {
339        if let Ok(cache) = self.cache.lock() {
340            let mut total_age = Duration::ZERO;
341            let mut total_accesses = 0;
342            let mut total_references = 0;
343
344            for entry in cache.values() {
345                total_age += entry.age();
346                total_accesses += entry.access_count();
347                total_references += entry.reference_count();
348            }
349
350            let entry_count = cache.len();
351            let avg_age = if entry_count > 0 {
352                total_age / entry_count as u32
353            } else {
354                Duration::ZERO
355            };
356
357            CacheStats {
358                entry_count,
359                total_accesses,
360                total_references,
361                avg_age,
362                total_size_bytes: entry_count * 1024, // Rough estimate
363            }
364        } else {
365            CacheStats::default()
366        }
367    }
368}
369
370/// Statistics for cache maintenance operations
371#[derive(Debug, Clone, Default)]
372pub struct CacheMaintenanceStats {
373    pub unused_removed: usize,
374    pub lru_evicted: usize,
375    pub final_size: usize,
376}
377
378/// General cache statistics
379#[derive(Debug, Clone, Default)]
380pub struct CacheStats {
381    pub entry_count: usize,
382    pub total_accesses: u32,
383    pub total_references: u32,
384    pub avg_age: Duration,
385    pub total_size_bytes: usize,
386}
387
388impl CacheStats {
389    pub fn avg_accesses_per_entry(&self) -> f64 {
390        if self.entry_count > 0 {
391            self.total_accesses as f64 / self.entry_count as f64
392        } else {
393            0.0
394        }
395    }
396
397    pub fn avg_references_per_entry(&self) -> f64 {
398        if self.entry_count > 0 {
399            self.total_references as f64 / self.entry_count as f64
400        } else {
401            0.0
402        }
403    }
404}