use redis::AsyncCommands; use std::time::Duration; use uuid::Uuid; #[derive(Clone)] pub struct DeduplicationManager { cache: db::cache::AppCache, window: Duration, } impl DeduplicationManager { pub fn new(cache: db::cache::AppCache) -> Self { Self { cache, window: Duration::from_secs(300), } } pub async fn check_and_mark( &self, message_id: Uuid, room_id: Uuid, ) -> Result { let key = format!("dedup:{}:{}", room_id, message_id); let mut conn = self .cache .conn() .await .map_err(|_| crate::error::AppTransportError::Internal)?; // Use atomic SET NX EX to prevent race conditions. // Returns true if the key was set (not a duplicate), false if it already exists. let result: Option = redis::cmd("SET") .arg(&key) .arg("1") .arg("NX") .arg("EX") .arg(self.window.as_secs()) .query_async(&mut conn) .await .map_err(|_| crate::error::AppTransportError::Internal)?; Ok(result.is_some()) } pub async fn is_duplicate( &self, message_id: Uuid, room_id: Uuid, ) -> Result { let key = format!("dedup:{}:{}", room_id, message_id); let mut conn = self .cache .conn() .await .map_err(|_| crate::error::AppTransportError::Internal)?; let exists: bool = conn .exists(&key) .await .map_err(|_| crate::error::AppTransportError::Internal)?; Ok(exists) } pub async fn cleanup_expired(&self) -> Result<(), crate::error::AppTransportError> { Ok(()) } }