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    pub fn new() -> Self {
31        Self {
32            results: Arc::new(RwLock::new(HashMap::new())),
33        }
34    }
35}
36
37impl Default for MemoryIdempotencyStore {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43#[async_trait]
44impl IdempotencyKeyStore for MemoryIdempotencyStore {
45    async fn store_result(&self, key: &IdempotencyKey, result: &ExtractionResult) -> Result<()> {
46        self.results.write().await.insert(*key, result.clone());
47        Ok(())
48    }
49
50    async fn get_result(&self, key: &IdempotencyKey) -> Result<Option<ExtractionResult>> {
51        let results = self.results.read().await;
52        Ok(results.get(key).cloned())
53    }
54
55    async fn delete_result(&self, key: &IdempotencyKey) -> Result<()> {
56        self.results.write().await.remove(key);
57        Ok(())
58    }
59
60    async fn clear_all(&self) -> Result<()> {
61        self.results.write().await.clear();
62        Ok(())
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[tokio::test]
71    async fn test_store_and_get_result() -> std::result::Result<(), Box<dyn std::error::Error>> {
72        let store = MemoryIdempotencyStore::new();
73        let key = IdempotencyKey::new();
74        let result = ExtractionResult::new(key);
75
76        store.store_result(&key, &result).await?;
77        let retrieved = store.get_result(&key).await?;
78
79        assert!(retrieved.is_some());
80        let retrieved = retrieved.ok_or("expected Some result")?;
81        assert_eq!(retrieved.metadata.idempotency_key, key);
82        Ok(())
83    }
84
85    #[tokio::test]
86    async fn test_get_nonexistent_result() -> std::result::Result<(), Box<dyn std::error::Error>> {
87        let store = MemoryIdempotencyStore::new();
88        let key = IdempotencyKey::new();
89
90        let result = store.get_result(&key).await?;
91        assert!(result.is_none());
92        Ok(())
93    }
94
95    #[tokio::test]
96    async fn test_delete_result() -> std::result::Result<(), Box<dyn std::error::Error>> {
97        let store = MemoryIdempotencyStore::new();
98        let key = IdempotencyKey::new();
99        let result = ExtractionResult::new(key);
100
101        store.store_result(&key, &result).await?;
102        store.delete_result(&key).await?;
103
104        let retrieved = store.get_result(&key).await?;
105        assert!(retrieved.is_none());
106        Ok(())
107    }
108
109    #[tokio::test]
110    async fn test_clear_all() -> std::result::Result<(), Box<dyn std::error::Error>> {
111        let store = MemoryIdempotencyStore::new();
112
113        let key1 = IdempotencyKey::new();
114        let key2 = IdempotencyKey::new();
115
116        store
117            .store_result(&key1, &ExtractionResult::new(key1))
118            .await?;
119        store
120            .store_result(&key2, &ExtractionResult::new(key2))
121            .await?;
122
123        store.clear_all().await?;
124
125        assert!(store.get_result(&key1).await?.is_none());
126        assert!(store.get_result(&key2).await?.is_none());
127        Ok(())
128    }
129}