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}