use std::{
any::Any,
collections::HashMap,
sync::{
Arc, Mutex,
atomic::{AtomicU32, Ordering},
},
time::Duration,
};
use crate::platform::{DEFAULT_MAX_CACHE_SIZE, DEFAULT_UNUSED_THRESHOLD};
#[cfg(not(target_family = "wasm"))]
use std::time::Instant;
#[cfg(target_family = "wasm")]
use web_time::Instant;
#[derive(Debug, Clone, Default)]
pub struct CacheGetOptions {
pub expiration: Option<Duration>,
pub stale_time: Option<Duration>,
pub check_staleness: bool,
}
impl CacheGetOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_expiration(mut self, expiration: Duration) -> Self {
self.expiration = Some(expiration);
self
}
pub fn with_stale_time(mut self, stale_time: Duration) -> Self {
self.stale_time = Some(stale_time);
self.check_staleness = true;
self
}
pub fn check_staleness(mut self) -> Self {
self.check_staleness = true;
self
}
}
#[derive(Debug, Clone)]
pub struct CacheGetResult<T> {
pub data: T,
pub is_stale: bool,
}
#[derive(Clone)]
pub struct CacheEntry {
data: Arc<dyn Any + Send + Sync>,
cached_at: Arc<Mutex<Instant>>,
last_accessed: Arc<Mutex<Instant>>,
access_count: Arc<AtomicU32>,
}
impl CacheEntry {
pub fn new<T: Clone + Send + Sync + 'static>(data: T) -> Self {
let now = Instant::now();
Self {
data: Arc::new(data),
cached_at: Arc::new(Mutex::new(now)),
last_accessed: Arc::new(Mutex::new(now)),
access_count: Arc::new(AtomicU32::new(0)),
}
}
pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
if let Ok(mut last_accessed) = self.last_accessed.lock() {
*last_accessed = Instant::now();
}
self.access_count.fetch_add(1, Ordering::SeqCst);
self.data.downcast_ref::<T>().cloned()
}
pub fn refresh_timestamp(&self) {
if let Ok(mut cached_at) = self.cached_at.lock() {
*cached_at = Instant::now();
}
}
pub fn is_expired(&self, expiration: Duration) -> bool {
if let Ok(cached_at) = self.cached_at.lock() {
cached_at.elapsed() > expiration
} else {
false
}
}
pub fn is_stale(&self, stale_time: Duration) -> bool {
if let Ok(cached_at) = self.cached_at.lock() {
cached_at.elapsed() > stale_time
} else {
false
}
}
pub fn access_count(&self) -> u32 {
self.access_count.load(Ordering::SeqCst)
}
pub fn is_unused_for(&self, duration: Duration) -> bool {
if let Ok(last_accessed) = self.last_accessed.lock() {
last_accessed.elapsed() > duration
} else {
false
}
}
pub fn time_since_last_access(&self) -> Duration {
if let Ok(last_accessed) = self.last_accessed.lock() {
last_accessed.elapsed()
} else {
Duration::from_secs(0)
}
}
pub fn age(&self) -> Duration {
if let Ok(cached_at) = self.cached_at.lock() {
cached_at.elapsed()
} else {
Duration::from_secs(0)
}
}
}
#[derive(Clone, Default)]
pub struct ProviderCache {
pub cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
}
impl ProviderCache {
pub fn new() -> Self {
Self::default()
}
pub fn get<T: Clone + Send + Sync + 'static>(&self, key: &str) -> Option<T> {
self.cache.lock().ok()?.get(key)?.get::<T>()
}
pub fn get_with_options<T: Clone + Send + Sync + 'static>(
&self,
key: &str,
options: CacheGetOptions,
) -> Option<CacheGetResult<T>> {
let cache_guard = self.cache.lock().ok()?;
let entry = cache_guard.get(key)?;
if let Some(exp_duration) = options.expiration {
if entry.is_expired(exp_duration) {
drop(cache_guard);
if let Ok(mut cache) = self.cache.lock() {
cache.remove(key);
crate::debug_log!(
"🗑️ [CACHE-EXPIRATION] Removing expired cache entry for key: {}",
key
);
}
return None;
}
}
let data = entry.get::<T>()?;
let is_stale = if options.check_staleness {
if let Some(stale_duration) = options.stale_time {
entry.is_stale(stale_duration)
} else {
false
}
} else {
false
};
Some(CacheGetResult { data, is_stale })
}
#[deprecated(
since = "0.1.0",
note = "Use get_with_options() instead for more flexible cache retrieval"
)]
pub fn get_with_expiration<T: Clone + Send + Sync + 'static>(
&self,
key: &str,
expiration: Option<Duration>,
) -> Option<T> {
let is_expired = {
let cache_guard = self.cache.lock().ok()?;
let entry = cache_guard.get(key)?;
if let Some(exp_duration) = expiration {
entry.is_expired(exp_duration)
} else {
false
}
};
if is_expired {
if let Ok(mut cache) = self.cache.lock() {
cache.remove(key);
crate::debug_log!(
"🗑️ [CACHE-EXPIRATION] Removing expired cache entry for key: {}",
key
);
}
return None;
}
let cache_guard = self.cache.lock().ok()?;
let entry = cache_guard.get(key)?;
entry.get::<T>()
}
#[deprecated(
since = "0.1.0",
note = "Use get_with_options() instead for more flexible cache retrieval"
)]
pub fn get_with_staleness<T: Clone + Send + Sync + 'static>(
&self,
key: &str,
stale_time: Option<Duration>,
expiration: Option<Duration>,
) -> Option<(T, bool)> {
let cache_guard = self.cache.lock().ok()?;
let entry = cache_guard.get(key)?;
if let Some(exp_duration) = expiration
&& entry.is_expired(exp_duration)
{
return None;
}
let data = entry.get::<T>()?;
let is_stale = if let Some(stale_duration) = stale_time {
entry.is_stale(stale_duration)
} else {
false
};
Some((data, is_stale))
}
pub fn set<T: Clone + Send + Sync + PartialEq + 'static>(&self, key: String, value: T) -> bool {
if let Ok(mut cache) = self.cache.lock() {
if let Some(existing_entry) = cache.get_mut(&key)
&& let Some(existing_value) = existing_entry.get::<T>()
&& existing_value == value
{
existing_entry.refresh_timestamp();
crate::debug_log!(
"⏸️ [CACHE-STORE] Value unchanged for key: {}, refreshing timestamp",
key
);
return false;
}
cache.insert(key.clone(), CacheEntry::new(value));
crate::debug_log!("📊 [CACHE-STORE] Stored data for key: {}", key);
return true;
}
false
}
pub fn remove(&self, key: &str) -> bool {
if let Ok(mut cache) = self.cache.lock() {
cache.remove(key).is_some()
} else {
false
}
}
pub fn invalidate(&self, key: &str) {
self.remove(key);
crate::debug_log!(
"🗑️ [CACHE-INVALIDATE] Invalidated cache entry for key: {}",
key
);
}
pub fn clear(&self) {
if let Ok(mut cache) = self.cache.lock() {
#[cfg(feature = "tracing")]
let count = cache.len();
cache.clear();
#[cfg(feature = "tracing")]
crate::debug_log!("🗑️ [CACHE-CLEAR] Cleared {} cache entries", count);
}
}
pub fn size(&self) -> usize {
self.cache.lock().map(|cache| cache.len()).unwrap_or(0)
}
pub fn cleanup_unused_entries(&self, unused_threshold: Duration) -> usize {
if let Ok(mut cache) = self.cache.lock() {
let initial_size = cache.len();
cache.retain(|_key, entry| {
let should_keep = !entry.is_unused_for(unused_threshold);
#[cfg(feature = "tracing")]
if !should_keep {
crate::debug_log!("🧹 [CACHE-CLEANUP] Removing unused entry: {}", _key);
}
should_keep
});
let removed = initial_size - cache.len();
if removed > 0 {
crate::debug_log!("🧹 [CACHE-CLEANUP] Removed {} unused entries", removed);
}
removed
} else {
0
}
}
pub fn evict_lru_entries(&self, max_size: usize) -> usize {
if let Ok(mut cache) = self.cache.lock() {
if cache.len() <= max_size {
return 0;
}
let mut entries: Vec<_> = cache.drain().collect();
entries.sort_by(|(_, a), (_, b)| {
a.time_since_last_access().cmp(&b.time_since_last_access())
});
let to_keep = entries.split_off(entries.len().saturating_sub(max_size));
let evicted = entries.len();
cache.extend(to_keep);
if evicted > 0 {
crate::debug_log!(
"🗑️ [LRU-EVICT] Evicted {} entries due to cache size limit",
evicted
);
}
evicted
} else {
0
}
}
pub fn maintain(&self) -> CacheMaintenanceStats {
CacheMaintenanceStats {
unused_removed: self.cleanup_unused_entries(DEFAULT_UNUSED_THRESHOLD),
lru_evicted: self.evict_lru_entries(DEFAULT_MAX_CACHE_SIZE),
final_size: self.size(),
}
}
pub fn stats(&self) -> CacheStats {
if let Ok(cache) = self.cache.lock() {
let mut total_age = Duration::ZERO;
let mut total_accesses = 0;
for entry in cache.values() {
total_age += entry.age();
total_accesses += entry.access_count();
}
let entry_count = cache.len();
let avg_age = if entry_count > 0 {
total_age / entry_count as u32
} else {
Duration::ZERO
};
CacheStats {
entry_count,
total_accesses,
total_references: 0, avg_age,
total_size_bytes: entry_count * 1024, }
} else {
CacheStats::default()
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheMaintenanceStats {
pub unused_removed: usize,
pub lru_evicted: usize,
pub final_size: usize,
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub entry_count: usize,
pub total_accesses: u32,
pub total_references: u32,
pub avg_age: Duration,
pub total_size_bytes: usize,
}
impl CacheStats {
pub fn avg_accesses_per_entry(&self) -> f64 {
if self.entry_count > 0 {
self.total_accesses as f64 / self.entry_count as f64
} else {
0.0
}
}
pub fn avg_references_per_entry(&self) -> f64 {
if self.entry_count > 0 {
self.total_references as f64 / self.entry_count as f64
} else {
0.0
}
}
}