dioxus_provider/
cache.rs

1//! # Cache Management for dioxus-provider
2//!
3//! This module implements a global, type-erased cache for provider results, supporting:
4//! - **Expiration**: Entries are removed after a configurable TTL.
5//! - **Staleness (SWR)**: Entries can be marked stale and revalidated in the background.
6//! - **LRU Eviction**: Least-recently-used entries are evicted to maintain a size limit.
7//! - **Access/Usage Stats**: Provides statistics for cache introspection and tuning.
8//!
9//! ## Example
10//! ```rust,no_run
11//! use dioxus_provider::cache::ProviderCache;
12//! let cache = ProviderCache::new();
13//! cache.set("my_key".to_string(), 42);
14//! let value: Option<i32> = cache.get("my_key");
15//! ```
16//! Cache management and async state types for dioxus-provider
17
18use std::{
19    any::Any,
20    collections::HashMap,
21    sync::{
22        Arc, Mutex,
23        atomic::{AtomicU32, Ordering},
24    },
25    time::Duration,
26};
27
28use crate::platform::{DEFAULT_MAX_CACHE_SIZE, DEFAULT_UNUSED_THRESHOLD};
29
30// Platform-specific time imports
31#[cfg(not(target_family = "wasm"))]
32use std::time::Instant;
33#[cfg(target_family = "wasm")]
34use web_time::Instant;
35
36/// Options for cache retrieval operations
37#[derive(Debug, Clone, Default)]
38pub struct CacheGetOptions {
39    /// Optional expiration duration - entries older than this will be removed
40    pub expiration: Option<Duration>,
41    /// Optional stale time - used to check if data is stale
42    pub stale_time: Option<Duration>,
43    /// Whether to return staleness information
44    pub check_staleness: bool,
45}
46
47impl CacheGetOptions {
48    /// Create new cache get options with default values
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Set the expiration duration
54    pub fn with_expiration(mut self, expiration: Duration) -> Self {
55        self.expiration = Some(expiration);
56        self
57    }
58
59    /// Set the stale time
60    pub fn with_stale_time(mut self, stale_time: Duration) -> Self {
61        self.stale_time = Some(stale_time);
62        self.check_staleness = true;
63        self
64    }
65
66    /// Enable staleness checking
67    pub fn check_staleness(mut self) -> Self {
68        self.check_staleness = true;
69        self
70    }
71}
72
73/// Result type for cache get operations with staleness information
74#[derive(Debug, Clone)]
75pub struct CacheGetResult<T> {
76    /// The cached data
77    pub data: T,
78    /// Whether the data is considered stale
79    pub is_stale: bool,
80}
81
82/// A type-erased cache entry for storing provider results with timestamp and access tracking
83#[derive(Clone)]
84pub struct CacheEntry {
85    data: Arc<dyn Any + Send + Sync>,
86    cached_at: Arc<Mutex<Instant>>,
87    last_accessed: Arc<Mutex<Instant>>,
88    access_count: Arc<AtomicU32>,
89}
90
91impl CacheEntry {
92    /// Creates a new cache entry with the given data.
93    ///
94    /// # Arguments
95    ///
96    /// * `data` - The data to cache.
97    ///
98    /// # Returns
99    ///
100    /// A new `CacheEntry` instance.
101    pub fn new<T: Clone + Send + Sync + 'static>(data: T) -> Self {
102        let now = Instant::now();
103        Self {
104            data: Arc::new(data),
105            cached_at: Arc::new(Mutex::new(now)),
106            last_accessed: Arc::new(Mutex::new(now)),
107            access_count: Arc::new(AtomicU32::new(0)),
108        }
109    }
110
111    /// Retrieves the cached data of type `T`.
112    ///
113    /// # Arguments
114    ///
115    /// * `&self` - A reference to the `CacheEntry`.
116    ///
117    /// # Returns
118    ///
119    /// An `Option<T>` containing the cached data if available, or `None` if the entry is expired or not found.
120    ///
121    /// # Side Effects
122    ///
123    /// Updates the `last_accessed` timestamp and increments the `access_count`.
124    pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
125        // Update last accessed time and access count
126        if let Ok(mut last_accessed) = self.last_accessed.lock() {
127            *last_accessed = Instant::now();
128        }
129        self.access_count.fetch_add(1, Ordering::SeqCst);
130        self.data.downcast_ref::<T>().cloned()
131    }
132
133    /// Refreshes the cached_at timestamp to the current time.
134    ///
135    /// # Arguments
136    ///
137    /// * `&self` - A reference to the `CacheEntry`.
138    ///
139    /// # Side Effects
140    ///
141    /// Updates the `cached_at` timestamp to the current time.
142    pub fn refresh_timestamp(&self) {
143        if let Ok(mut cached_at) = self.cached_at.lock() {
144            *cached_at = Instant::now();
145        }
146    }
147
148    /// Checks if the cache entry has expired based on the given expiration duration.
149    ///
150    /// # Arguments
151    ///
152    /// * `&self` - A reference to the `CacheEntry`.
153    /// * `expiration` - The duration after which the entry is considered expired.
154    ///
155    /// # Returns
156    ///
157    /// A boolean indicating whether the entry has expired.
158    pub fn is_expired(&self, expiration: Duration) -> bool {
159        if let Ok(cached_at) = self.cached_at.lock() {
160            cached_at.elapsed() > expiration
161        } else {
162            false
163        }
164    }
165
166    /// Checks if the cache entry is stale based on the given stale time.
167    ///
168    /// # Arguments
169    ///
170    /// * `&self` - A reference to the `CacheEntry`.
171    /// * `stale_time` - The duration after which the entry is considered stale.
172    ///
173    /// # Returns
174    ///
175    /// A boolean indicating whether the entry is stale.
176    pub fn is_stale(&self, stale_time: Duration) -> bool {
177        if let Ok(cached_at) = self.cached_at.lock() {
178            cached_at.elapsed() > stale_time
179        } else {
180            false
181        }
182    }
183
184    /// Gets the current access count for the cache entry.
185    ///
186    /// # Arguments
187    ///
188    /// * `&self` - A reference to the `CacheEntry`.
189    ///
190    /// # Returns
191    ///
192    /// The current access count as a `u32`.
193    pub fn access_count(&self) -> u32 {
194        self.access_count.load(Ordering::SeqCst)
195    }
196
197    /// Checks if the cache entry hasn't been accessed for the given duration.
198    ///
199    /// # Arguments
200    ///
201    /// * `&self` - A reference to the `CacheEntry`.
202    /// * `duration` - The duration after which the entry is considered unused.
203    ///
204    /// # Returns
205    ///
206    /// A boolean indicating whether the entry is unused.
207    pub fn is_unused_for(&self, duration: Duration) -> bool {
208        if let Ok(last_accessed) = self.last_accessed.lock() {
209            last_accessed.elapsed() > duration
210        } else {
211            false
212        }
213    }
214
215    /// Gets the time since this entry was last accessed.
216    ///
217    /// # Arguments
218    ///
219    /// * `&self` - A reference to the `CacheEntry`.
220    ///
221    /// # Returns
222    ///
223    /// A `Duration` representing the time since last access.
224    pub fn time_since_last_access(&self) -> Duration {
225        if let Ok(last_accessed) = self.last_accessed.lock() {
226            last_accessed.elapsed()
227        } else {
228            Duration::from_secs(0)
229        }
230    }
231
232    /// Gets the age of this cache entry.
233    ///
234    /// # Arguments
235    ///
236    /// * `&self` - A reference to the `CacheEntry`.
237    ///
238    /// # Returns
239    ///
240    /// A `Duration` representing the age of the entry.
241    pub fn age(&self) -> Duration {
242        if let Ok(cached_at) = self.cached_at.lock() {
243            cached_at.elapsed()
244        } else {
245            Duration::from_secs(0)
246        }
247    }
248}
249
250/// Global cache for provider results with automatic cleanup
251#[derive(Clone, Default)]
252pub struct ProviderCache {
253    pub cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
254}
255
256impl ProviderCache {
257    /// Creates a new provider cache.
258    ///
259    /// # Returns
260    ///
261    /// A new `ProviderCache` instance.
262    pub fn new() -> Self {
263        Self::default()
264    }
265
266    /// Retrieves a cached result by key.
267    ///
268    /// # Arguments
269    ///
270    /// * `&self` - A reference to the `ProviderCache`.
271    /// * `key` - The key to retrieve.
272    ///
273    /// # Returns
274    ///
275    /// An `Option<T>` containing the cached data if available, or `None` if not found.
276    ///
277    /// # Side Effects
278    ///
279    /// None.
280    pub fn get<T: Clone + Send + Sync + 'static>(&self, key: &str) -> Option<T> {
281        self.cache.lock().ok()?.get(key)?.get::<T>()
282    }
283
284    /// Retrieves a cached result with configurable options
285    ///
286    /// This unified method handles expiration, staleness checking, and other cache retrieval options.
287    ///
288    /// # Arguments
289    ///
290    /// * `&self` - A reference to the `ProviderCache`.
291    /// * `key` - The key to retrieve.
292    /// * `options` - Cache retrieval options (expiration, stale time, etc.)
293    ///
294    /// # Returns
295    ///
296    /// An `Option<CacheGetResult<T>>` containing the cached data and staleness info if available.
297    ///
298    /// # Example
299    ///
300    /// ```rust,no_run
301    /// use dioxus_provider::cache::{ProviderCache, CacheGetOptions};
302    /// use std::time::Duration;
303    ///
304    /// let cache = ProviderCache::new();
305    /// let options = CacheGetOptions::new()
306    ///     .with_expiration(Duration::from_secs(300))
307    ///     .with_stale_time(Duration::from_secs(60));
308    ///
309    /// if let Some(result) = cache.get_with_options::<String>("my_key", options) {
310    ///     println!("Data: {}, Stale: {}", result.data, result.is_stale);
311    /// }
312    /// ```
313    pub fn get_with_options<T: Clone + Send + Sync + 'static>(
314        &self,
315        key: &str,
316        options: CacheGetOptions,
317    ) -> Option<CacheGetResult<T>> {
318        let cache_guard = self.cache.lock().ok()?;
319        let entry = cache_guard.get(key)?;
320
321        // Check expiration first
322        if let Some(exp_duration) = options.expiration {
323            if entry.is_expired(exp_duration) {
324                drop(cache_guard);
325                // Remove expired entry
326                if let Ok(mut cache) = self.cache.lock() {
327                    cache.remove(key);
328                    crate::debug_log!(
329                        "πŸ—‘οΈ [CACHE-EXPIRATION] Removing expired cache entry for key: {}",
330                        key
331                    );
332                }
333                return None;
334            }
335        }
336
337        // Get the data
338        let data = entry.get::<T>()?;
339
340        // Check staleness if requested
341        let is_stale = if options.check_staleness {
342            if let Some(stale_duration) = options.stale_time {
343                entry.is_stale(stale_duration)
344            } else {
345                false
346            }
347        } else {
348            false
349        };
350
351        Some(CacheGetResult { data, is_stale })
352    }
353
354    /// Retrieves a cached result by key, checking for expiration with a specific expiration duration.
355    ///
356    /// # Deprecated
357    /// Use `get_with_options()` instead for more flexible cache retrieval.
358    ///
359    /// # Arguments
360    ///
361    /// * `&self` - A reference to the `ProviderCache`.
362    /// * `key` - The key to retrieve.
363    /// * `expiration` - An optional duration after which the entry is considered expired.
364    ///
365    /// # Returns
366    ///
367    /// An `Option<T>` containing the cached data if available and not expired, or `None` if expired.
368    ///
369    /// # Side Effects
370    ///
371    /// If expired, the entry is removed from the cache.
372    #[deprecated(
373        since = "0.1.0",
374        note = "Use get_with_options() instead for more flexible cache retrieval"
375    )]
376    pub fn get_with_expiration<T: Clone + Send + Sync + 'static>(
377        &self,
378        key: &str,
379        expiration: Option<Duration>,
380    ) -> Option<T> {
381        // First, check if the entry exists and is expired
382        let is_expired = {
383            let cache_guard = self.cache.lock().ok()?;
384            let entry = cache_guard.get(key)?;
385
386            if let Some(exp_duration) = expiration {
387                entry.is_expired(exp_duration)
388            } else {
389                false
390            }
391        };
392
393        // If expired, remove the entry
394        if is_expired {
395            if let Ok(mut cache) = self.cache.lock() {
396                cache.remove(key);
397                crate::debug_log!(
398                    "πŸ—‘οΈ [CACHE-EXPIRATION] Removing expired cache entry for key: {}",
399                    key
400                );
401            }
402            return None;
403        }
404
405        // Entry is not expired, return the data
406        let cache_guard = self.cache.lock().ok()?;
407        let entry = cache_guard.get(key)?;
408        entry.get::<T>()
409    }
410
411    /// Retrieves cached data with staleness information for SWR behavior.
412    ///
413    /// # Deprecated
414    /// Use `get_with_options()` instead for more flexible cache retrieval.
415    ///
416    /// # Arguments
417    ///
418    /// * `&self` - A reference to the `ProviderCache`.
419    /// * `key` - The key to retrieve.
420    /// * `stale_time` - An optional duration after which the entry is considered stale.
421    /// * `expiration` - An optional duration after which the entry is considered expired.
422    ///
423    /// # Returns
424    ///
425    /// An `Option<(T, bool)>` containing the cached data and a boolean indicating staleness.
426    ///
427    /// # Side Effects
428    ///
429    /// None.
430    #[deprecated(
431        since = "0.1.0",
432        note = "Use get_with_options() instead for more flexible cache retrieval"
433    )]
434    pub fn get_with_staleness<T: Clone + Send + Sync + 'static>(
435        &self,
436        key: &str,
437        stale_time: Option<Duration>,
438        expiration: Option<Duration>,
439    ) -> Option<(T, bool)> {
440        let cache_guard = self.cache.lock().ok()?;
441        let entry = cache_guard.get(key)?;
442
443        // Check if expired first
444        if let Some(exp_duration) = expiration
445            && entry.is_expired(exp_duration)
446        {
447            return None;
448        }
449
450        // Get the data
451        let data = entry.get::<T>()?;
452
453        // Check if stale
454        let is_stale = if let Some(stale_duration) = stale_time {
455            entry.is_stale(stale_duration)
456        } else {
457            false
458        };
459
460        Some((data, is_stale))
461    }
462
463    /// Sets a value for a given key.
464    ///
465    /// # Arguments
466    ///
467    /// * `&self` - A reference to the `ProviderCache`.
468    /// * `key` - The key to set.
469    /// * `value` - The value to set.
470    ///
471    /// # Returns
472    ///
473    /// A boolean indicating whether the value was updated (true) or unchanged (false).
474    ///
475    /// # Side Effects
476    ///
477    /// Updates the `cached_at` timestamp if the value was updated.
478    pub fn set<T: Clone + Send + Sync + PartialEq + 'static>(&self, key: String, value: T) -> bool {
479        if let Ok(mut cache) = self.cache.lock() {
480            if let Some(existing_entry) = cache.get_mut(&key)
481                && let Some(existing_value) = existing_entry.get::<T>()
482                && existing_value == value
483            {
484                existing_entry.refresh_timestamp();
485                crate::debug_log!(
486                    "⏸️ [CACHE-STORE] Value unchanged for key: {}, refreshing timestamp",
487                    key
488                );
489                return false;
490            }
491            cache.insert(key.clone(), CacheEntry::new(value));
492            crate::debug_log!("πŸ“Š [CACHE-STORE] Stored data for key: {}", key);
493            return true;
494        }
495        false
496    }
497
498    /// Removes a cached result by key.
499    ///
500    /// # Arguments
501    ///
502    /// * `&self` - A reference to the `ProviderCache`.
503    /// * `key` - The key to remove.
504    ///
505    /// # Returns
506    ///
507    /// A boolean indicating whether the entry was removed.
508    ///
509    /// # Side Effects
510    ///
511    /// None.
512    pub fn remove(&self, key: &str) -> bool {
513        if let Ok(mut cache) = self.cache.lock() {
514            cache.remove(key).is_some()
515        } else {
516            false
517        }
518    }
519
520    /// Invalidates a cached result by key (alias for remove).
521    ///
522    /// # Arguments
523    ///
524    /// * `&self` - A reference to the `ProviderCache`.
525    /// * `key` - The key to invalidate.
526    ///
527    /// # Side Effects
528    ///
529    /// The entry is removed from the cache.
530    pub fn invalidate(&self, key: &str) {
531        self.remove(key);
532        crate::debug_log!(
533            "πŸ—‘οΈ [CACHE-INVALIDATE] Invalidated cache entry for key: {}",
534            key
535        );
536    }
537
538    /// Clears all cached results.
539    ///
540    /// # Arguments
541    ///
542    /// * `&self` - A reference to the `ProviderCache`.
543    ///
544    /// # Side Effects
545    ///
546    /// All entries are removed from the cache.
547    pub fn clear(&self) {
548        if let Ok(mut cache) = self.cache.lock() {
549            #[cfg(feature = "tracing")]
550            let count = cache.len();
551            cache.clear();
552            #[cfg(feature = "tracing")]
553            crate::debug_log!("πŸ—‘οΈ [CACHE-CLEAR] Cleared {} cache entries", count);
554        }
555    }
556
557    /// Gets the number of cached entries.
558    ///
559    /// # Arguments
560    ///
561    /// * `&self` - A reference to the `ProviderCache`.
562    ///
563    /// # Returns
564    ///
565    /// The number of cached entries as a `usize`.
566    ///
567    /// # Side Effects
568    ///
569    /// None.
570    pub fn size(&self) -> usize {
571        self.cache.lock().map(|cache| cache.len()).unwrap_or(0)
572    }
573
574    /// Cleans up unused entries based on access time.
575    ///
576    /// # Arguments
577    ///
578    /// * `&self` - A reference to the `ProviderCache`.
579    /// * `unused_threshold` - The duration after which an entry is considered unused.
580    ///
581    /// # Returns
582    ///
583    /// The number of unused entries removed.
584    ///
585    /// # Side Effects
586    ///
587    /// Unused entries are removed from the cache.
588    pub fn cleanup_unused_entries(&self, unused_threshold: Duration) -> usize {
589        if let Ok(mut cache) = self.cache.lock() {
590            let initial_size = cache.len();
591            cache.retain(|_key, entry| {
592                let should_keep = !entry.is_unused_for(unused_threshold);
593                #[cfg(feature = "tracing")]
594                if !should_keep {
595                    crate::debug_log!("🧹 [CACHE-CLEANUP] Removing unused entry: {}", _key);
596                }
597                should_keep
598            });
599            let removed = initial_size - cache.len();
600            if removed > 0 {
601                crate::debug_log!("🧹 [CACHE-CLEANUP] Removed {} unused entries", removed);
602            }
603            removed
604        } else {
605            0
606        }
607    }
608
609    /// Evicts least recently used entries to maintain cache size limit.
610    ///
611    /// # Arguments
612    ///
613    /// * `&self` - A reference to the `ProviderCache`.
614    /// * `max_size` - The maximum number of entries to keep.
615    ///
616    /// # Returns
617    ///
618    /// The number of entries evicted.
619    ///
620    /// # Side Effects
621    ///
622    /// Least recently used entries are removed from the cache.
623    pub fn evict_lru_entries(&self, max_size: usize) -> usize {
624        if let Ok(mut cache) = self.cache.lock() {
625            if cache.len() <= max_size {
626                return 0;
627            }
628
629            // Convert to vector for sorting
630            let mut entries: Vec<_> = cache.drain().collect();
631
632            // Sort by last access time (oldest first)
633            entries.sort_by(|(_, a), (_, b)| {
634                a.time_since_last_access().cmp(&b.time_since_last_access())
635            });
636
637            // Keep the most recently used entries
638            let to_keep = entries.split_off(entries.len().saturating_sub(max_size));
639            let evicted = entries.len();
640
641            // Rebuild cache with kept entries
642            cache.extend(to_keep);
643
644            if evicted > 0 {
645                crate::debug_log!(
646                    "πŸ—‘οΈ [LRU-EVICT] Evicted {} entries due to cache size limit",
647                    evicted
648                );
649            }
650            evicted
651        } else {
652            0
653        }
654    }
655
656    /// Performs comprehensive cache maintenance.
657    ///
658    /// # Arguments
659    ///
660    /// * `&self` - A reference to the `ProviderCache`.
661    ///
662    /// # Returns
663    ///
664    /// A `CacheMaintenanceStats` containing statistics about the maintenance.
665    ///
666    /// # Side Effects
667    ///
668    /// Unused entries are removed and LRU entries are evicted.
669    pub fn maintain(&self) -> CacheMaintenanceStats {
670        CacheMaintenanceStats {
671            unused_removed: self.cleanup_unused_entries(DEFAULT_UNUSED_THRESHOLD),
672            lru_evicted: self.evict_lru_entries(DEFAULT_MAX_CACHE_SIZE),
673            final_size: self.size(),
674        }
675    }
676
677    /// Gets cache statistics.
678    ///
679    /// # Arguments
680    ///
681    /// * `&self` - A reference to the `ProviderCache`.
682    ///
683    /// # Returns
684    ///
685    /// A `CacheStats` containing statistics about the cache.
686    ///
687    /// # Side Effects
688    ///
689    /// None.
690    pub fn stats(&self) -> CacheStats {
691        if let Ok(cache) = self.cache.lock() {
692            let mut total_age = Duration::ZERO;
693            let mut total_accesses = 0;
694
695            for entry in cache.values() {
696                total_age += entry.age();
697                total_accesses += entry.access_count();
698            }
699
700            let entry_count = cache.len();
701            let avg_age = if entry_count > 0 {
702                total_age / entry_count as u32
703            } else {
704                Duration::ZERO
705            };
706
707            CacheStats {
708                entry_count,
709                total_accesses,
710                total_references: 0, // No longer tracking references
711                avg_age,
712                total_size_bytes: entry_count * 1024, // Rough estimate
713            }
714        } else {
715            CacheStats::default()
716        }
717    }
718}
719
720/// Statistics for cache maintenance operations
721#[derive(Debug, Clone, Default)]
722pub struct CacheMaintenanceStats {
723    pub unused_removed: usize,
724    pub lru_evicted: usize,
725    pub final_size: usize,
726}
727
728/// General cache statistics
729#[derive(Debug, Clone, Default)]
730pub struct CacheStats {
731    pub entry_count: usize,
732    pub total_accesses: u32,
733    pub total_references: u32,
734    pub avg_age: Duration,
735    pub total_size_bytes: usize,
736}
737
738impl CacheStats {
739    pub fn avg_accesses_per_entry(&self) -> f64 {
740        if self.entry_count > 0 {
741            self.total_accesses as f64 / self.entry_count as f64
742        } else {
743            0.0
744        }
745    }
746
747    pub fn avg_references_per_entry(&self) -> f64 {
748        if self.entry_count > 0 {
749            self.total_references as f64 / self.entry_count as f64
750        } else {
751            0.0
752        }
753    }
754}