use std::collections::HashMap;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
pub struct LessonCache {
store: RwLock<HashMap<String, (Instant, String, Vec<String>)>>,
ttl: Duration,
max_entries: usize,
}
impl LessonCache {
pub fn new(ttl: Duration, max_entries: usize) -> Self {
Self {
store: RwLock::new(HashMap::new()),
ttl,
max_entries,
}
}
pub async fn get(&self, session_id: &str, query: &str) -> Option<(String, Vec<String>)> {
let key = Self::key(session_id, query);
let store = self.store.read().await;
if let Some((ts, value, ids)) = store.get(&key) {
if ts.elapsed() < self.ttl {
return Some((value.clone(), ids.clone()));
}
}
None
}
pub async fn set(&self, session_id: &str, query: &str, context_block: String, ids: Vec<String>) {
let key = Self::key(session_id, query);
let mut store = self.store.write().await;
if store.len() >= self.max_entries {
if let Some(oldest_key) = store
.iter()
.min_by_key(|(_, (ts, _, _))| *ts)
.map(|(k, _)| k.clone())
{
store.remove(&oldest_key);
}
}
store.insert(key, (Instant::now(), context_block, ids));
}
#[allow(dead_code)]
pub async fn clear(&self) {
self.store.write().await.clear();
}
fn key(session_id: &str, query: &str) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
query.hash(&mut hasher);
let qh = format!("{:x}", hasher.finish());
format!("{}:{}", session_id, &qh[..12.min(qh.len())])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn caches_block_with_ids() {
let c = LessonCache::new(Duration::from_secs(30), 100);
c.set("s", "q", "BLOCK".into(), vec!["e1".into(), "e2".into()]).await;
assert_eq!(c.get("s", "q").await, Some(("BLOCK".to_string(), vec!["e1".to_string(), "e2".to_string()])));
assert_eq!(c.get("s", "other").await, None);
}
#[tokio::test]
async fn ids_default_empty_when_omitted() {
let c = LessonCache::new(Duration::from_secs(30), 100);
c.set("s", "q", "BLOCK".into(), Vec::new()).await;
let (block, ids) = c.get("s", "q").await.unwrap();
assert_eq!(block, "BLOCK");
assert!(ids.is_empty());
}
}