use lru::LruCache;
use std::num::NonZeroUsize;
use std::sync::Mutex;
use crate::element::AXElement;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct CacheKey {
pub pid: i32,
pub query: String,
}
pub struct CacheEntry {
pub element: AXElement,
pub timestamp: std::time::Instant,
}
pub struct ElementCache {
cache: Mutex<LruCache<CacheKey, CacheEntry>>,
max_age_ms: u64,
}
impl ElementCache {
#[must_use]
pub fn new(capacity: usize, max_age_ms: u64) -> Self {
Self {
cache: Mutex::new(LruCache::new(
NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(100).unwrap()),
)),
max_age_ms,
}
}
pub fn get(&self, key: &CacheKey) -> Option<AXElement> {
let mut cache = self.cache.lock().ok()?;
if let Some(entry) = cache.get(key) {
if entry.timestamp.elapsed().as_millis() < u128::from(self.max_age_ms) {
return Some(entry.element.clone());
}
cache.pop(key);
}
None
}
pub fn put(&self, key: CacheKey, element: AXElement) {
if let Ok(mut cache) = self.cache.lock() {
cache.put(
key,
CacheEntry {
element,
timestamp: std::time::Instant::now(),
},
);
}
}
pub fn clear(&self) {
if let Ok(mut cache) = self.cache.lock() {
cache.clear();
}
}
pub fn stats(&self) -> CacheStats {
if let Ok(cache) = self.cache.lock() {
CacheStats {
size: cache.len(),
capacity: cache.cap().get(),
}
} else {
CacheStats {
size: 0,
capacity: 0,
}
}
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub size: usize,
pub capacity: usize,
}
static GLOBAL_CACHE: std::sync::OnceLock<ElementCache> = std::sync::OnceLock::new();
pub fn global_cache() -> &'static ElementCache {
GLOBAL_CACHE.get_or_init(|| ElementCache::new(500, 5000))
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
#[test]
fn test_cache_key_equality() {
let key1 = CacheKey {
pid: 123,
query: "Save".to_string(),
};
let key2 = CacheKey {
pid: 123,
query: "Save".to_string(),
};
assert_eq!(key1, key2);
}
fn test_element(role: &str, title: &str) -> AXElement {
AXElement {
element: std::ptr::null(),
role: Some(role.to_string()),
title: Some(title.to_string()),
}
}
#[test]
fn test_cache_returns_cloned_element_before_expiry() {
let cache = ElementCache::new(10, 5_000);
let key = CacheKey {
pid: 123,
query: "button:Save".to_string(),
};
let element = test_element("AXButton", "Save");
cache.put(key.clone(), element);
let cached = cache.get(&key).expect("expected cached element");
assert_eq!(cached.role, Some("AXButton".to_string()));
assert_eq!(cached.title, Some("Save".to_string()));
}
#[test]
fn test_cache_removes_expired_entries() {
let cache = ElementCache::new(10, 5_000);
let key = CacheKey {
pid: 123,
query: "button:Save".to_string(),
};
cache.put(key.clone(), test_element("AXButton", "Save"));
{
let mut inner = cache
.cache
.lock()
.expect("cache lock should not be poisoned");
let entry = inner
.get_mut(&key)
.expect("expected inserted cache entry to exist");
entry.timestamp = Instant::now() - Duration::from_secs(10);
}
assert!(cache.get(&key).is_none());
assert_eq!(cache.stats().size, 0);
}
}