use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use tokio::sync::RwLock;
pub(super) use ppoppo_token::SV_CACHE_TTL;
const MAX_ENTRIES: usize = 10_000;
#[async_trait]
pub trait SvCachePort: Send + Sync + 'static {
async fn load(&self, key: &str) -> Option<i64>;
async fn store(&self, key: &str, value: i64, ttl: Duration);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CheckResult {
Fresh,
Stale,
Unknown,
}
#[derive(Clone)]
pub struct MemorySvBackend {
inner: Arc<RwLock<HashMap<String, (i64, Instant, Duration)>>>,
}
impl MemorySvBackend {
#[must_use]
pub fn new() -> Self {
Self { inner: Arc::new(RwLock::new(HashMap::new())) }
}
}
impl Default for MemorySvBackend {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl SvCachePort for MemorySvBackend {
async fn load(&self, key: &str) -> Option<i64> {
let guard = self.inner.read().await;
let (sv, written_at, ttl) = guard.get(key)?;
if written_at.elapsed() >= *ttl {
return None;
}
Some(*sv)
}
async fn store(&self, key: &str, value: i64, ttl: Duration) {
let mut guard = self.inner.write().await;
if guard.len() >= MAX_ENTRIES && !guard.contains_key(key) {
guard.retain(|_, (_, written_at, slot_ttl)| written_at.elapsed() < *slot_ttl);
if guard.len() >= MAX_ENTRIES {
let oldest_key = guard
.iter()
.min_by_key(|(_, (_, written, _))| *written)
.map(|(k, _)| k.clone());
if let Some(k) = oldest_key {
guard.remove(&k);
}
}
}
guard.insert(key.to_string(), (value, Instant::now(), ttl));
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[tokio::test]
async fn memory_backend_respects_ttl_on_load() {
let backend = MemorySvBackend::new();
backend.store("sv:abc", 42, SV_CACHE_TTL).await;
assert_eq!(backend.load("sv:abc").await, Some(42));
assert_eq!(backend.load("sv:missing").await, None);
}
#[tokio::test]
async fn memory_backend_overwrite() {
let backend = MemorySvBackend::new();
backend.store("sv:xyz", 1, SV_CACHE_TTL).await;
backend.store("sv:xyz", 2, SV_CACHE_TTL).await;
assert_eq!(backend.load("sv:xyz").await, Some(2));
}
#[tokio::test]
async fn memory_backend_bounded_by_max_entries() {
let backend = MemorySvBackend::new();
for i in 0..(MAX_ENTRIES + 100) {
backend.store(&format!("sv:{i}"), i as i64, SV_CACHE_TTL).await;
}
let len = backend.inner.read().await.len();
assert!(len <= MAX_ENTRIES, "cache exceeded cap: {len} > {MAX_ENTRIES}");
let last_key = format!("sv:{}", MAX_ENTRIES + 99);
assert_eq!(
backend.load(&last_key).await,
Some((MAX_ENTRIES + 99) as i64),
"newest entry must survive eviction",
);
}
#[tokio::test]
async fn memory_backend_honors_provided_ttl() {
let backend = MemorySvBackend::new();
backend.store("sv:short", 7, Duration::from_nanos(1)).await;
tokio::task::yield_now().await;
assert_eq!(
backend.load("sv:short").await,
None,
"backend must honor caller-provided TTL, not hardcoded SV_CACHE_TTL",
);
}
}