Skip to main content

stygian_plugin/storage/
memory_idempotency_store.rs

1//! In-memory idempotency key store for tracking extraction results
2
3use crate::Result;
4use crate::domain::{ExtractionResult, IdempotencyKey};
5use crate::ports::IdempotencyKeyStore;
6use async_trait::async_trait;
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// In-memory idempotency store
12///
13/// Tracks extraction results by idempotency key to prevent duplicate processing.
14/// Useful for development and testing; production should use persistent storage.
15///
16/// # Example
17///
18/// ```
19/// use stygian_plugin::storage::MemoryIdempotencyStore;
20/// use stygian_plugin::domain::{IdempotencyKey, ExtractionResult};
21///
22/// let store = MemoryIdempotencyStore::new();
23/// ```
24pub struct MemoryIdempotencyStore {
25    results: Arc<RwLock<HashMap<IdempotencyKey, ExtractionResult>>>,
26}
27
28impl MemoryIdempotencyStore {
29    /// Create a new memory-based idempotency store
30    #[must_use]
31    pub fn new() -> Self {
32        Self {
33            results: Arc::new(RwLock::new(HashMap::new())),
34        }
35    }
36}
37
38impl Default for MemoryIdempotencyStore {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44#[async_trait]
45impl IdempotencyKeyStore for MemoryIdempotencyStore {
46    async fn store_result(&self, key: &IdempotencyKey, result: &ExtractionResult) -> Result<()> {
47        self.results.write().await.insert(*key, result.clone());
48        Ok(())
49    }
50
51    async fn get_result(&self, key: &IdempotencyKey) -> Result<Option<ExtractionResult>> {
52        let results = self.results.read().await;
53        Ok(results.get(key).cloned())
54    }
55
56    async fn delete_result(&self, key: &IdempotencyKey) -> Result<()> {
57        self.results.write().await.remove(key);
58        Ok(())
59    }
60
61    async fn clear_all(&self) -> Result<()> {
62        self.results.write().await.clear();
63        Ok(())
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[tokio::test]
72    async fn test_store_and_get_result() -> std::result::Result<(), Box<dyn std::error::Error>> {
73        let store = MemoryIdempotencyStore::new();
74        let key = IdempotencyKey::new();
75        let result = ExtractionResult::new(key);
76
77        store.store_result(&key, &result).await?;
78        let retrieved = store.get_result(&key).await?;
79
80        assert!(retrieved.is_some());
81        let retrieved = retrieved.ok_or("expected Some result")?;
82        assert_eq!(retrieved.metadata.idempotency_key, key);
83        Ok(())
84    }
85
86    #[tokio::test]
87    async fn test_get_nonexistent_result() -> std::result::Result<(), Box<dyn std::error::Error>> {
88        let store = MemoryIdempotencyStore::new();
89        let key = IdempotencyKey::new();
90
91        let result = store.get_result(&key).await?;
92        assert!(result.is_none());
93        Ok(())
94    }
95
96    #[tokio::test]
97    async fn test_delete_result() -> std::result::Result<(), Box<dyn std::error::Error>> {
98        let store = MemoryIdempotencyStore::new();
99        let key = IdempotencyKey::new();
100        let result = ExtractionResult::new(key);
101
102        store.store_result(&key, &result).await?;
103        store.delete_result(&key).await?;
104
105        let retrieved = store.get_result(&key).await?;
106        assert!(retrieved.is_none());
107        Ok(())
108    }
109
110    #[tokio::test]
111    async fn test_clear_all() -> std::result::Result<(), Box<dyn std::error::Error>> {
112        let store = MemoryIdempotencyStore::new();
113
114        let key1 = IdempotencyKey::new();
115        let key2 = IdempotencyKey::new();
116
117        store
118            .store_result(&key1, &ExtractionResult::new(key1))
119            .await?;
120        store
121            .store_result(&key2, &ExtractionResult::new(key2))
122            .await?;
123
124        store.clear_all().await?;
125
126        assert!(store.get_result(&key1).await?.is_none());
127        assert!(store.get_result(&key2).await?.is_none());
128        Ok(())
129    }
130}