use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Duration;
use serde_json::Value;
use tokio::time::Instant;
pub struct IdempotencyCache {
entries: Mutex<HashMap<String, (Value, Instant)>>,
ttl: Duration,
max_entries: usize,
}
impl IdempotencyCache {
pub fn new(ttl: Duration) -> Self {
Self {
entries: Mutex::new(HashMap::new()),
ttl,
max_entries: 100_000,
}
}
pub fn get(&self, key: &str) -> Option<Value> {
let mut map = self.entries.lock().unwrap();
let now = Instant::now();
match map.get(key) {
Some((_, exp)) if *exp <= now => {
map.remove(key);
None
}
Some((v, _)) => Some(v.clone()),
None => None,
}
}
pub fn put(&self, key: &str, value: Value) {
let now = Instant::now();
let mut map = self.entries.lock().unwrap();
if map.len() >= self.max_entries {
map.retain(|_, (_, exp)| *exp > now);
}
map.insert(key.to_string(), (value, now + self.ttl));
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test(start_paused = true)]
async fn caches_then_expires() {
let c = IdempotencyCache::new(Duration::from_secs(60));
assert!(c.get("k").is_none());
c.put("k", json!({"v": 1}));
assert_eq!(c.get("k"), Some(json!({"v": 1})));
tokio::time::advance(Duration::from_secs(61)).await;
assert!(c.get("k").is_none(), "entry should expire");
}
}