#[cfg(feature = "alloc")]
use alloc::{string::String, vec::Vec};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use crate::context::CombinedContext;
use crate::core::source::{SourceCapabilities, SourceStats};
pub const DEFAULT_MAX_ENTRIES: usize = 1000;
pub const DEFAULT_TTL_MS: u64 = 300_000;
pub const DEFAULT_CONTEXT_TTL_MS: u64 = 60_000;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry {
pub result: String,
pub source_id: String,
pub cached_at: u64,
pub ttl_ms: u64,
pub hits: u32,
pub last_accessed: u64,
}
impl CacheEntry {
#[must_use]
pub fn new(result: String, source_id: String, ttl_ms: u64) -> Self {
let now = get_time_ms();
Self {
result,
source_id,
cached_at: now,
ttl_ms,
hits: 0,
last_accessed: now,
}
}
#[must_use]
pub fn is_expired(&self, current_time_ms: u64) -> bool {
current_time_ms.saturating_sub(self.cached_at) > self.ttl_ms
}
#[must_use]
pub fn age_ms(&self, current_time_ms: u64) -> u64 {
current_time_ms.saturating_sub(self.cached_at)
}
#[must_use]
pub fn remaining_ttl_ms(&self, current_time_ms: u64) -> u64 {
let age = self.age_ms(current_time_ms);
self.ttl_ms.saturating_sub(age)
}
pub fn record_hit(&mut self) {
self.hits = self.hits.saturating_add(1);
self.last_accessed = get_time_ms();
}
}
#[derive(Debug, Clone)]
struct LruNode {
key: u64,
prev: Option<usize>,
next: Option<usize>,
}
#[derive(Debug)]
pub struct QueryCache {
entries: HashMap<u64, (CacheEntry, usize)>,
lru_nodes: Vec<Option<LruNode>>,
lru_head: Option<usize>,
lru_tail: Option<usize>,
free_list: Vec<usize>,
max_entries: usize,
default_ttl_ms: u64,
stats: CacheStats,
}
impl QueryCache {
#[must_use]
pub fn new() -> Self {
Self::with_config(DEFAULT_MAX_ENTRIES, DEFAULT_TTL_MS)
}
#[must_use]
pub fn with_config(max_entries: usize, default_ttl_ms: u64) -> Self {
Self {
entries: HashMap::with_capacity(max_entries),
lru_nodes: Vec::with_capacity(max_entries),
lru_head: None,
lru_tail: None,
free_list: Vec::new(),
max_entries,
default_ttl_ms,
stats: CacheStats::default(),
}
}
pub fn get(&mut self, query_hash: u64) -> Option<&CacheEntry> {
let now = get_time_ms();
if let Some((entry, node_idx)) = self.entries.get_mut(&query_hash) {
if entry.is_expired(now) {
self.stats.expirations += 1;
let node_idx = *node_idx;
self.remove_from_lru(node_idx);
self.entries.remove(&query_hash);
self.stats.misses += 1;
return None;
}
entry.record_hit();
self.stats.hits += 1;
let node_idx = *node_idx;
self.move_to_front(node_idx);
return self.entries.get(&query_hash).map(|(e, _)| e);
}
self.stats.misses += 1;
None
}
pub fn put(&mut self, query_hash: u64, result: String, source_id: String) {
self.put_with_ttl(query_hash, result, source_id, self.default_ttl_ms);
}
pub fn put_with_ttl(
&mut self,
query_hash: u64,
result: String,
source_id: String,
ttl_ms: u64,
) {
if self.entries.len() >= self.max_entries && !self.entries.contains_key(&query_hash) {
self.evict_lru();
}
if let Some((entry, node_idx)) = self.entries.get_mut(&query_hash) {
*entry = CacheEntry::new(result, source_id, ttl_ms);
let idx = *node_idx;
self.move_to_front(idx);
return;
}
let entry = CacheEntry::new(result, source_id, ttl_ms);
let node_idx = self.allocate_node(query_hash);
self.entries.insert(query_hash, (entry, node_idx));
self.add_to_front(node_idx);
self.stats.inserts += 1;
}
pub fn invalidate(&mut self, query_hash: u64) {
if let Some((_, node_idx)) = self.entries.remove(&query_hash) {
self.remove_from_lru(node_idx);
self.stats.invalidations += 1;
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.lru_nodes.clear();
self.lru_head = None;
self.lru_tail = None;
self.free_list.clear();
self.stats.clears += 1;
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub const fn stats(&self) -> &CacheStats {
&self.stats
}
#[must_use]
pub const fn max_entries(&self) -> usize {
self.max_entries
}
#[must_use]
pub const fn default_ttl_ms(&self) -> u64 {
self.default_ttl_ms
}
pub fn cleanup_expired(&mut self) -> usize {
let now = get_time_ms();
let expired: Vec<u64> = self
.entries
.iter()
.filter(|(_, (entry, _))| entry.is_expired(now))
.map(|(hash, _)| *hash)
.collect();
let count = expired.len();
for hash in expired {
self.invalidate(hash);
self.stats.expirations += 1;
}
count
}
#[must_use]
pub fn contains(&self, query_hash: u64) -> bool {
if let Some((entry, _)) = self.entries.get(&query_hash) {
!entry.is_expired(get_time_ms())
} else {
false
}
}
fn allocate_node(&mut self, key: u64) -> usize {
if let Some(idx) = self.free_list.pop() {
self.lru_nodes[idx] = Some(LruNode {
key,
prev: None,
next: None,
});
idx
} else {
let idx = self.lru_nodes.len();
self.lru_nodes.push(Some(LruNode {
key,
prev: None,
next: None,
}));
idx
}
}
fn add_to_front(&mut self, node_idx: usize) {
if let Some(old_head) = self.lru_head {
if let Some(ref mut node) = self.lru_nodes[node_idx] {
node.next = Some(old_head);
node.prev = None;
}
if let Some(ref mut old_head_node) = self.lru_nodes[old_head] {
old_head_node.prev = Some(node_idx);
}
}
self.lru_head = Some(node_idx);
if self.lru_tail.is_none() {
self.lru_tail = Some(node_idx);
}
}
fn move_to_front(&mut self, node_idx: usize) {
if self.lru_head == Some(node_idx) {
return; }
self.remove_from_lru_list_only(node_idx);
self.add_to_front(node_idx);
}
fn remove_from_lru_list_only(&mut self, node_idx: usize) {
let (prev, next) = if let Some(ref node) = self.lru_nodes[node_idx] {
(node.prev, node.next)
} else {
return;
};
if let Some(prev_idx) = prev {
if let Some(ref mut prev_node) = self.lru_nodes[prev_idx] {
prev_node.next = next;
}
} else {
self.lru_head = next;
}
if let Some(next_idx) = next {
if let Some(ref mut next_node) = self.lru_nodes[next_idx] {
next_node.prev = prev;
}
} else {
self.lru_tail = prev;
}
}
fn remove_from_lru(&mut self, node_idx: usize) {
self.remove_from_lru_list_only(node_idx);
self.lru_nodes[node_idx] = None;
self.free_list.push(node_idx);
}
fn evict_lru(&mut self) {
if let Some(tail_idx) = self.lru_tail {
if let Some(ref node) = self.lru_nodes[tail_idx] {
let key = node.key;
self.entries.remove(&key);
self.remove_from_lru(tail_idx);
self.stats.evictions += 1;
}
}
}
}
impl Default for QueryCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextCacheEntry {
pub context: CombinedContext,
pub cached_at: u64,
pub ttl_ms: u64,
pub hits: u32,
pub last_accessed: u64,
}
impl ContextCacheEntry {
#[must_use]
pub fn new(context: CombinedContext, ttl_ms: u64) -> Self {
let now = get_time_ms();
Self {
context,
cached_at: now,
ttl_ms,
hits: 0,
last_accessed: now,
}
}
#[must_use]
pub fn is_expired(&self, current_time_ms: u64) -> bool {
current_time_ms.saturating_sub(self.cached_at) > self.ttl_ms
}
pub fn record_hit(&mut self) {
self.hits = self.hits.saturating_add(1);
self.last_accessed = get_time_ms();
}
}
#[derive(Debug)]
pub struct ContextCache {
entries: HashMap<String, ContextCacheEntry>,
default_ttl_ms: u64,
refresh_on_access: bool,
stats: CacheStats,
}
impl ContextCache {
#[must_use]
pub fn new() -> Self {
Self::with_config(DEFAULT_CONTEXT_TTL_MS, false)
}
#[must_use]
pub fn with_config(default_ttl_ms: u64, refresh_on_access: bool) -> Self {
Self {
entries: HashMap::new(),
default_ttl_ms,
refresh_on_access,
stats: CacheStats::default(),
}
}
pub fn get(&mut self, provider_id: &str) -> Option<&CombinedContext> {
let now = get_time_ms();
if let Some(entry) = self.entries.get_mut(provider_id) {
if entry.is_expired(now) {
self.stats.expirations += 1;
self.entries.remove(provider_id);
self.stats.misses += 1;
return None;
}
entry.record_hit();
self.stats.hits += 1;
if self.refresh_on_access {
entry.cached_at = now;
}
return self.entries.get(provider_id).map(|e| &e.context);
}
self.stats.misses += 1;
None
}
pub fn get_cloned(&mut self, provider_id: &str) -> Option<CombinedContext> {
self.get(provider_id).cloned()
}
pub fn put(&mut self, provider_id: impl Into<String>, context: CombinedContext) {
self.put_with_ttl(provider_id, context, self.default_ttl_ms);
}
pub fn put_with_ttl(
&mut self,
provider_id: impl Into<String>,
context: CombinedContext,
ttl_ms: u64,
) {
let entry = ContextCacheEntry::new(context, ttl_ms);
self.entries.insert(provider_id.into(), entry);
self.stats.inserts += 1;
}
pub fn invalidate(&mut self, provider_id: &str) {
if self.entries.remove(provider_id).is_some() {
self.stats.invalidations += 1;
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.stats.clears += 1;
}
pub fn cleanup_expired(&mut self) -> usize {
let now = get_time_ms();
let expired: Vec<String> = self
.entries
.iter()
.filter(|(_, entry)| entry.is_expired(now))
.map(|(id, _)| id.clone())
.collect();
let count = expired.len();
for id in expired {
self.entries.remove(&id);
self.stats.expirations += 1;
}
count
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub const fn stats(&self) -> &CacheStats {
&self.stats
}
#[must_use]
pub const fn refresh_on_access(&self) -> bool {
self.refresh_on_access
}
pub fn set_refresh_on_access(&mut self, enabled: bool) {
self.refresh_on_access = enabled;
}
}
impl Default for ContextCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceCacheEntry {
pub capabilities: SourceCapabilities,
pub stats: SourceStats,
pub cached_at: u64,
pub ttl_ms: u64,
pub hits: u32,
}
impl SourceCacheEntry {
#[must_use]
pub fn new(capabilities: SourceCapabilities, stats: SourceStats, ttl_ms: u64) -> Self {
Self {
capabilities,
stats,
cached_at: get_time_ms(),
ttl_ms,
hits: 0,
}
}
#[must_use]
pub fn is_expired(&self, current_time_ms: u64) -> bool {
current_time_ms.saturating_sub(self.cached_at) > self.ttl_ms
}
pub fn record_hit(&mut self) {
self.hits = self.hits.saturating_add(1);
}
}
#[derive(Debug)]
pub struct SourceCache {
entries: HashMap<String, SourceCacheEntry>,
default_ttl_ms: u64,
stats: CacheStats,
}
pub const DEFAULT_SOURCE_TTL_MS: u64 = 600_000;
impl SourceCache {
#[must_use]
pub fn new() -> Self {
Self::with_ttl(DEFAULT_SOURCE_TTL_MS)
}
#[must_use]
pub fn with_ttl(default_ttl_ms: u64) -> Self {
Self {
entries: HashMap::new(),
default_ttl_ms,
stats: CacheStats::default(),
}
}
pub fn get_capabilities(&mut self, source_id: &str) -> Option<&SourceCapabilities> {
let now = get_time_ms();
if let Some(entry) = self.entries.get_mut(source_id) {
if entry.is_expired(now) {
self.stats.expirations += 1;
self.entries.remove(source_id);
self.stats.misses += 1;
return None;
}
entry.record_hit();
self.stats.hits += 1;
return self.entries.get(source_id).map(|e| &e.capabilities);
}
self.stats.misses += 1;
None
}
pub fn get_stats(&mut self, source_id: &str) -> Option<&SourceStats> {
let now = get_time_ms();
if let Some(entry) = self.entries.get_mut(source_id) {
if entry.is_expired(now) {
self.stats.expirations += 1;
self.entries.remove(source_id);
self.stats.misses += 1;
return None;
}
entry.record_hit();
self.stats.hits += 1;
return self.entries.get(source_id).map(|e| &e.stats);
}
self.stats.misses += 1;
None
}
pub fn get(&mut self, source_id: &str) -> Option<&SourceCacheEntry> {
let now = get_time_ms();
if let Some(entry) = self.entries.get_mut(source_id) {
if entry.is_expired(now) {
self.stats.expirations += 1;
self.entries.remove(source_id);
self.stats.misses += 1;
return None;
}
entry.record_hit();
self.stats.hits += 1;
return self.entries.get(source_id);
}
self.stats.misses += 1;
None
}
pub fn put(
&mut self,
source_id: impl Into<String>,
capabilities: SourceCapabilities,
stats: SourceStats,
) {
self.put_with_ttl(source_id, capabilities, stats, self.default_ttl_ms);
}
pub fn put_with_ttl(
&mut self,
source_id: impl Into<String>,
capabilities: SourceCapabilities,
stats: SourceStats,
ttl_ms: u64,
) {
let entry = SourceCacheEntry::new(capabilities, stats, ttl_ms);
self.entries.insert(source_id.into(), entry);
self.stats.inserts += 1;
}
pub fn update_stats(&mut self, source_id: &str, stats: SourceStats) {
if let Some(entry) = self.entries.get_mut(source_id) {
entry.stats = stats;
}
}
pub fn invalidate(&mut self, source_id: &str) {
if self.entries.remove(source_id).is_some() {
self.stats.invalidations += 1;
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.stats.clears += 1;
}
pub fn cleanup_expired(&mut self) -> usize {
let now = get_time_ms();
let expired: Vec<String> = self
.entries
.iter()
.filter(|(_, entry)| entry.is_expired(now))
.map(|(id, _)| id.clone())
.collect();
let count = expired.len();
for id in expired {
self.entries.remove(&id);
self.stats.expirations += 1;
}
count
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub const fn stats(&self) -> &CacheStats {
&self.stats
}
}
impl Default for SourceCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub inserts: u64,
pub evictions: u64,
pub invalidations: u64,
pub expirations: u64,
pub clears: u64,
}
impl CacheStats {
#[must_use]
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
#[must_use]
pub const fn total_requests(&self) -> u64 {
self.hits + self.misses
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
#[derive(Debug)]
pub struct CacheManager {
pub query_cache: QueryCache,
pub context_cache: ContextCache,
pub source_cache: SourceCache,
enabled: bool,
}
impl CacheManager {
#[must_use]
pub fn new() -> Self {
Self {
query_cache: QueryCache::new(),
context_cache: ContextCache::new(),
source_cache: SourceCache::new(),
enabled: true,
}
}
#[must_use]
pub fn with_config(
query_max_entries: usize,
query_ttl_ms: u64,
context_ttl_ms: u64,
source_ttl_ms: u64,
) -> Self {
Self {
query_cache: QueryCache::with_config(query_max_entries, query_ttl_ms),
context_cache: ContextCache::with_config(context_ttl_ms, false),
source_cache: SourceCache::with_ttl(source_ttl_ms),
enabled: true,
}
}
#[must_use]
pub const fn is_enabled(&self) -> bool {
self.enabled
}
pub fn enable(&mut self) {
self.enabled = true;
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn clear_all(&mut self) {
self.query_cache.clear();
self.context_cache.clear();
self.source_cache.clear();
}
pub fn cleanup_all(&mut self) -> usize {
let mut count = 0;
count += self.query_cache.cleanup_expired();
count += self.context_cache.cleanup_expired();
count += self.source_cache.cleanup_expired();
count
}
#[must_use]
pub fn combined_stats(&self) -> CombinedCacheStats {
CombinedCacheStats {
query: *self.query_cache.stats(),
context: *self.context_cache.stats(),
source: *self.source_cache.stats(),
}
}
}
impl Default for CacheManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct CombinedCacheStats {
pub query: CacheStats,
pub context: CacheStats,
pub source: CacheStats,
}
impl CombinedCacheStats {
#[must_use]
pub const fn total_hits(&self) -> u64 {
self.query.hits + self.context.hits + self.source.hits
}
#[must_use]
pub const fn total_misses(&self) -> u64 {
self.query.misses + self.context.misses + self.source.misses
}
#[must_use]
pub fn overall_hit_rate(&self) -> f64 {
let total = self.total_hits() + self.total_misses();
if total == 0 {
0.0
} else {
self.total_hits() as f64 / total as f64
}
}
}
#[inline]
fn get_time_ms() -> u64 {
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
{
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(any(not(feature = "std"), target_arch = "wasm32"))]
{
0 }
}
#[must_use]
pub fn fnv1a_hash(s: &str) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325; for byte in s.bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100000001b3); }
hash
}
#[cfg(feature = "std")]
pub mod sync {
use super::*;
use std::sync::{Arc, RwLock};
pub type SharedQueryCache = Arc<RwLock<QueryCache>>;
pub type SharedContextCache = Arc<RwLock<ContextCache>>;
pub type SharedSourceCache = Arc<RwLock<SourceCache>>;
pub type SharedCacheManager = Arc<RwLock<CacheManager>>;
#[must_use]
pub fn shared_query_cache() -> SharedQueryCache {
Arc::new(RwLock::new(QueryCache::new()))
}
#[must_use]
pub fn shared_context_cache() -> SharedContextCache {
Arc::new(RwLock::new(ContextCache::new()))
}
#[must_use]
pub fn shared_source_cache() -> SharedSourceCache {
Arc::new(RwLock::new(SourceCache::new()))
}
#[must_use]
pub fn shared_cache_manager() -> SharedCacheManager {
Arc::new(RwLock::new(CacheManager::new()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_cache_basic() {
let mut cache = QueryCache::new();
cache.put(12345, "result".to_string(), "source1".to_string());
let entry = cache.get(12345).unwrap();
assert_eq!(entry.result, "result");
assert_eq!(entry.source_id, "source1");
assert_eq!(entry.hits, 1);
}
#[test]
fn test_query_cache_miss() {
let mut cache = QueryCache::new();
assert!(cache.get(99999).is_none());
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_query_cache_lru_eviction() {
let mut cache = QueryCache::with_config(3, DEFAULT_TTL_MS);
cache.put(1, "r1".to_string(), "s1".to_string());
cache.put(2, "r2".to_string(), "s2".to_string());
cache.put(3, "r3".to_string(), "s3".to_string());
assert_eq!(cache.len(), 3);
let _ = cache.get(1);
cache.put(4, "r4".to_string(), "s4".to_string());
assert_eq!(cache.len(), 3);
assert!(cache.contains(1)); assert!(!cache.contains(2)); assert!(cache.contains(3));
assert!(cache.contains(4));
}
#[test]
fn test_query_cache_update() {
let mut cache = QueryCache::new();
cache.put(1, "old".to_string(), "s1".to_string());
cache.put(1, "new".to_string(), "s1".to_string());
let entry = cache.get(1).unwrap();
assert_eq!(entry.result, "new");
assert_eq!(cache.len(), 1);
}
#[test]
fn test_query_cache_invalidate() {
let mut cache = QueryCache::new();
cache.put(1, "r1".to_string(), "s1".to_string());
cache.invalidate(1);
assert!(cache.get(1).is_none());
assert_eq!(cache.stats().invalidations, 1);
}
#[test]
fn test_query_cache_clear() {
let mut cache = QueryCache::new();
cache.put(1, "r1".to_string(), "s1".to_string());
cache.put(2, "r2".to_string(), "s2".to_string());
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.stats().clears, 1);
}
#[test]
fn test_context_cache_basic() {
let mut cache = ContextCache::new();
let ctx = CombinedContext::new().with_timestamp(12345);
cache.put("provider1", ctx.clone());
let cached = cache.get("provider1").unwrap();
assert_eq!(cached.timestamp, 12345);
}
#[test]
fn test_context_cache_miss() {
let mut cache = ContextCache::new();
assert!(cache.get("unknown").is_none());
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_context_cache_refresh_on_access() {
let mut cache = ContextCache::with_config(DEFAULT_CONTEXT_TTL_MS, true);
let ctx = CombinedContext::new();
cache.put("p1", ctx);
let _ = cache.get("p1");
assert!(cache.get("p1").is_some());
}
#[test]
fn test_source_cache_basic() {
let mut cache = SourceCache::new();
let caps = SourceCapabilities::full();
let stats = SourceStats::default();
cache.put("source1", caps, stats);
let cached_caps = cache.get_capabilities("source1").unwrap();
assert!(cached_caps.sparql_1_1);
}
#[test]
fn test_source_cache_update_stats() {
let mut cache = SourceCache::new();
let caps = SourceCapabilities::default();
let mut stats = SourceStats::default();
stats.total_queries = 10;
cache.put("s1", caps, stats);
let mut new_stats = SourceStats::default();
new_stats.total_queries = 20;
cache.update_stats("s1", new_stats);
let cached_stats = cache.get_stats("s1").unwrap();
assert_eq!(cached_stats.total_queries, 20);
}
#[test]
fn test_cache_stats_hit_rate() {
let mut stats = CacheStats::default();
stats.hits = 80;
stats.misses = 20;
assert!((stats.hit_rate() - 0.8).abs() < f64::EPSILON);
}
#[test]
fn test_cache_stats_empty() {
let stats = CacheStats::default();
assert!((stats.hit_rate() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_manager_basic() {
let mut manager = CacheManager::new();
manager
.query_cache
.put(1, "r1".to_string(), "s1".to_string());
manager.context_cache.put("p1", CombinedContext::new());
manager
.source_cache
.put("s1", SourceCapabilities::default(), SourceStats::default());
assert!(!manager.query_cache.is_empty());
assert!(!manager.context_cache.is_empty());
assert!(!manager.source_cache.is_empty());
}
#[test]
fn test_cache_manager_clear_all() {
let mut manager = CacheManager::new();
manager
.query_cache
.put(1, "r1".to_string(), "s1".to_string());
manager.context_cache.put("p1", CombinedContext::new());
manager.clear_all();
assert!(manager.query_cache.is_empty());
assert!(manager.context_cache.is_empty());
}
#[test]
fn test_cache_manager_enable_disable() {
let mut manager = CacheManager::new();
assert!(manager.is_enabled());
manager.disable();
assert!(!manager.is_enabled());
manager.enable();
assert!(manager.is_enabled());
}
#[test]
fn test_fnv1a_hash() {
let hash1 = fnv1a_hash("test");
let hash2 = fnv1a_hash("test");
let hash3 = fnv1a_hash("different");
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_cache_entry_expiration() {
let entry = CacheEntry::new("result".to_string(), "source".to_string(), 1000);
assert!(!entry.is_expired(entry.cached_at));
assert!(entry.is_expired(entry.cached_at + 1001));
}
#[test]
fn test_cache_entry_remaining_ttl() {
let entry = CacheEntry::new("result".to_string(), "source".to_string(), 1000);
assert_eq!(entry.remaining_ttl_ms(entry.cached_at), 1000);
assert_eq!(entry.remaining_ttl_ms(entry.cached_at + 500), 500);
assert_eq!(entry.remaining_ttl_ms(entry.cached_at + 1500), 0);
}
#[cfg(feature = "std")]
#[test]
fn test_shared_cache_creation() {
let _cache = sync::shared_query_cache();
let _ctx_cache = sync::shared_context_cache();
let _src_cache = sync::shared_source_cache();
let _manager = sync::shared_cache_manager();
}
}