use crate::value::Value;
use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
struct ModuleCacheEntry {
exports: HashMap<String, Value>,
loaded_at: Instant,
#[allow(dead_code)]
access_count: usize,
}
#[derive(Debug, Clone)]
pub struct ModuleCacheStats {
pub module_count: usize,
pub total_loads: usize,
pub cache_hits: usize,
pub cache_misses: usize,
pub hit_rate: f64,
}
pub struct ModuleCacheManager {
cache: RwLock<HashMap<String, ModuleCacheEntry>>,
max_size: usize,
ttl_secs: u64,
stats: RwLock<ModuleCacheStats>,
}
impl ModuleCacheManager {
pub fn new(max_size: usize, ttl_secs: u64) -> Self {
Self {
cache: RwLock::new(HashMap::new()),
max_size,
ttl_secs,
stats: RwLock::new(ModuleCacheStats {
module_count: 0,
total_loads: 0,
cache_hits: 0,
cache_misses: 0,
hit_rate: 0.0,
}),
}
}
pub fn get(&self, module_id: &str) -> Option<HashMap<String, Value>> {
let cache = self.cache.read().ok()?;
let entry = cache.get(module_id)?;
if self.ttl_secs > 0 {
let elapsed = entry.loaded_at.elapsed().as_secs();
if elapsed > self.ttl_secs {
return None; }
}
if let Ok(mut stats) = self.stats.write() {
stats.cache_hits += 1;
stats.total_loads += 1;
if stats.total_loads > 0 {
stats.hit_rate = stats.cache_hits as f64 / stats.total_loads as f64;
}
}
Some(entry.exports.clone())
}
pub fn insert(&self, module_id: String, exports: HashMap<String, Value>) {
if self.max_size > 0 {
let mut cache = self.cache.write().unwrap();
if cache.len() >= self.max_size {
let to_remove = (self.max_size / 10).max(1);
self.evict_oldest(&mut cache, to_remove);
}
cache.insert(
module_id.clone(),
ModuleCacheEntry {
exports,
loaded_at: Instant::now(),
access_count: 0,
},
);
if let Ok(mut stats) = self.stats.write() {
stats.cache_misses += 1;
stats.total_loads += 1;
stats.module_count = cache.len();
if stats.total_loads > 0 {
stats.hit_rate = stats.cache_hits as f64 / stats.total_loads as f64;
}
}
}
}
pub fn cleanup_expired(&self) {
if self.ttl_secs == 0 {
return;
}
let ttl = Duration::from_secs(self.ttl_secs);
let mut cache = self.cache.write().unwrap();
let now = Instant::now();
cache.retain(|_, entry| now.duration_since(entry.loaded_at) < ttl);
if let Ok(mut stats) = self.stats.write() {
stats.module_count = cache.len();
}
}
pub fn clear(&self) {
let mut cache = self.cache.write().unwrap();
cache.clear();
if let Ok(mut stats) = self.stats.write() {
stats.module_count = 0;
}
}
pub fn remove(&self, module_id: &str) -> bool {
let mut cache = self.cache.write().unwrap();
let removed = cache.remove(module_id).is_some();
if removed && let Ok(mut stats) = self.stats.write() {
stats.module_count = cache.len();
}
removed
}
pub fn stats(&self) -> ModuleCacheStats {
self.stats.read().unwrap().clone()
}
pub fn cached_modules(&self) -> Vec<String> {
self.cache.read().unwrap().keys().cloned().collect()
}
fn evict_oldest(&self, cache: &mut HashMap<String, ModuleCacheEntry>, count: usize) {
let mut entries: Vec<_> = cache.iter().collect();
entries.sort_by_key(|(_, entry)| entry.loaded_at);
let to_remove: Vec<String> = entries
.iter()
.take(count)
.map(|(module_id, _)| (*module_id).clone())
.collect();
for module_id in to_remove {
cache.remove(&module_id);
}
}
}
impl Default for ModuleCacheManager {
fn default() -> Self {
Self::new(100, 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_module_cache_basic() {
let manager = ModuleCacheManager::new(10, 0);
let mut exports = HashMap::new();
exports.insert("foo".to_string(), Value::Number(42.0));
manager.insert("test_module".to_string(), exports.clone());
let retrieved = manager.get("test_module");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().get("foo").unwrap(), &Value::Number(42.0));
}
#[test]
fn test_module_cache_miss() {
let manager = ModuleCacheManager::new(10, 0);
let retrieved = manager.get("nonexistent");
assert!(retrieved.is_none());
let stats = manager.stats();
assert_eq!(stats.cache_misses, 0); assert_eq!(stats.cache_hits, 0);
}
#[test]
fn test_module_cache_max_size() {
let manager = ModuleCacheManager::new(3, 0); let exports = HashMap::new();
for i in 0..5 {
manager.insert(format!("module{}", i), exports.clone());
}
assert_eq!(manager.cached_modules().len(), 3);
assert_eq!(manager.stats().module_count, 3);
}
#[test]
fn test_module_cache_clear() {
let manager = ModuleCacheManager::new(10, 0);
let mut exports = HashMap::new();
exports.insert("foo".to_string(), Value::Number(42.0));
manager.insert("test".to_string(), exports);
assert_eq!(manager.cached_modules().len(), 1);
manager.clear();
assert_eq!(manager.cached_modules().len(), 0);
}
#[test]
fn test_module_cache_remove() {
let manager = ModuleCacheManager::new(10, 0);
let mut exports = HashMap::new();
exports.insert("foo".to_string(), Value::Number(42.0));
manager.insert("test".to_string(), exports);
assert!(manager.remove("test"));
assert!(!manager.remove("nonexistent"));
assert_eq!(manager.cached_modules().len(), 0);
}
#[test]
fn test_module_cache_ttl() {
let manager = ModuleCacheManager::new(10, 1); let mut exports = HashMap::new();
exports.insert("foo".to_string(), Value::Number(42.0));
manager.insert("test".to_string(), exports.clone());
assert!(manager.get("test").is_some());
std::thread::sleep(Duration::from_secs(2));
assert!(manager.get("test").is_none());
}
#[test]
fn test_module_cache_stats() {
let manager = ModuleCacheManager::new(10, 0);
let mut exports = HashMap::new();
exports.insert("foo".to_string(), Value::Number(42.0));
manager.insert("test".to_string(), exports.clone());
manager.get("test");
manager.get("test");
manager.get("nonexistent");
let stats = manager.stats();
assert_eq!(stats.total_loads, 3);
assert_eq!(stats.cache_hits, 2);
assert_eq!(stats.cache_misses, 1);
assert!((stats.hit_rate - 0.666).abs() < 0.01); }
}