use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ReplayError {
#[error("replay store unavailable: {0}")]
Unavailable(String),
}
#[async_trait]
pub trait ReplayStore: Send + Sync + 'static {
async fn put_if_absent(&self, jti: &str, ttl: Duration) -> Result<bool, ReplayError>;
}
struct Entry {
inserted_at: Instant,
ttl: Duration,
}
impl Entry {
fn is_expired(&self, now: Instant) -> bool {
now.duration_since(self.inserted_at) >= self.ttl
}
}
#[derive(Default)]
pub struct InMemoryReplayStore {
entries: Mutex<HashMap<String, Entry>>,
}
impl InMemoryReplayStore {
pub fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
}
#[async_trait]
impl ReplayStore for InMemoryReplayStore {
async fn put_if_absent(&self, jti: &str, ttl: Duration) -> Result<bool, ReplayError> {
let now = Instant::now();
let mut entries = self.entries.lock().expect("replay store mutex poisoned");
entries.retain(|_, entry| !entry.is_expired(now));
if entries.contains_key(jti) {
return Ok(false);
}
entries.insert(
jti.to_string(),
Entry {
inserted_at: now,
ttl,
},
);
Ok(true)
}
}
pub struct UnavailableReplayStore;
#[async_trait]
impl ReplayStore for UnavailableReplayStore {
async fn put_if_absent(&self, _jti: &str, _ttl: Duration) -> Result<bool, ReplayError> {
Err(ReplayError::Unavailable("simulated outage".to_string()))
}
}