atproto_identity/
storage_lru.rs

1//! LRU cache implementation for DID document storage.
2//!
3//! Thread-safe in-memory storage with automatic eviction of least recently used
4//! DID documents when capacity is reached.
5
6use std::num::NonZeroUsize;
7use std::sync::{Arc, Mutex};
8
9use anyhow::Result;
10use lru::LruCache;
11
12use crate::errors::StorageError;
13use crate::model::Document;
14use crate::storage::DidDocumentStorage;
15
16/// An LRU-based implementation of `DidDocumentStorage` that maintains a fixed-size cache of DID documents.
17///
18/// This storage implementation uses an LRU (Least Recently Used) cache to store DID documents
19/// in memory with automatic eviction of the least recently accessed entries when the cache reaches
20/// its capacity. This is ideal for scenarios where you want to cache frequently accessed DID documents
21/// while keeping memory usage bounded.
22///
23/// ## Thread Safety
24///
25/// This implementation is thread-safe through the use of `Arc<Mutex<LruCache<String, Document>>>`.
26/// All operations are protected by a mutex, ensuring safe concurrent access from multiple threads
27/// or async tasks.
28///
29/// ## Cache Behavior
30///
31/// - **Get operations**: Move accessed entries to the front of the LRU order
32/// - **Store operations**: Add new entries at the front, evicting the least recently used if at capacity
33/// - **Delete operations**: Remove entries from the cache entirely
34/// - **Capacity management**: Automatically evicts least recently used entries when capacity is exceeded
35///
36/// ## Use Cases
37///
38/// This implementation is particularly suitable for:
39/// - Caching frequently accessed DID documents from PLC/web resolution
40/// - Scenarios with bounded memory requirements
41/// - Applications where some document lookup misses are acceptable
42/// - High-performance applications requiring in-memory access
43/// - Identity resolution caching layers
44///
45/// ## Limitations
46///
47/// - **Persistence**: Data is lost when the application restarts
48/// - **Capacity**: Limited to the configured cache size
49/// - **Cache misses**: Older entries may be evicted and need to be re-resolved
50/// - **Memory usage**: All cached data is kept in memory
51/// - **Document size**: Large documents consume more memory per entry
52///
53/// ## Examples
54///
55/// ```rust
56/// use atproto_identity::storage_lru::LruDidDocumentStorage;
57/// use atproto_identity::storage::DidDocumentStorage;
58/// use atproto_identity::model::Document;
59/// use std::num::NonZeroUsize;
60/// use std::collections::HashMap;
61///
62/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
63/// // Create an LRU cache with capacity for 1000 documents
64/// let storage = LruDidDocumentStorage::new(NonZeroUsize::new(1000).unwrap());
65///
66/// // Create a sample document
67/// let document = Document {
68///     context: vec![],
69///     id: "did:plc:bv6ggog3tya2z3vxsub7hnal".to_string(),
70///     also_known_as: vec!["at://alice.bsky.social".to_string()],
71///     service: vec![], // simplified for example
72///     verification_method: vec![], // simplified for example
73///     extra: HashMap::new(),
74/// };
75///
76/// // Store the document
77/// storage.store_document(document.clone()).await?;
78///
79/// // Retrieve the document
80/// let retrieved = storage.get_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
81/// assert_eq!(retrieved.as_ref().map(|d| &d.id), Some(&document.id));
82///
83/// // Delete the document
84/// storage.delete_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
85/// let retrieved = storage.get_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
86/// assert_eq!(retrieved, None);
87/// # Ok::<(), anyhow::Error>(())
88/// # }).unwrap();
89/// ```
90///
91/// ## Capacity Planning
92///
93/// When choosing the cache capacity, consider:
94/// - **Expected number of unique DIDs**: Size cache to hold frequently accessed documents
95/// - **Memory constraints**: Each entry uses approximately (document size + DID length + overhead) bytes
96/// - **Document complexity**: Documents with many services/keys use more memory
97/// - **Access patterns**: Higher capacity reduces cache misses for varied access patterns
98/// - **Performance requirements**: Larger caches may have slightly higher lookup times
99///
100/// ```rust
101/// use atproto_identity::storage_lru::LruDidDocumentStorage;
102/// use std::num::NonZeroUsize;
103///
104/// // Small cache for testing or low-memory environments
105/// let small_cache = LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap());
106///
107/// // Medium cache for typical applications
108/// let medium_cache = LruDidDocumentStorage::new(NonZeroUsize::new(10_000).unwrap());
109///
110/// // Large cache for high-traffic applications
111/// let large_cache = LruDidDocumentStorage::new(NonZeroUsize::new(100_000).unwrap());
112/// ```
113#[derive(Clone)]
114pub struct LruDidDocumentStorage {
115    /// The LRU cache storing DID -> Document mappings, protected by a mutex for thread safety.
116    ///
117    /// We use DID as the key since the primary operation is looking up documents by DID.
118    /// The cache is wrapped in Arc<Mutex<>> to ensure thread-safe access across multiple
119    /// async tasks and threads.
120    cache: Arc<Mutex<LruCache<String, Document>>>,
121}
122
123impl LruDidDocumentStorage {
124    /// Creates a new `LruDidDocumentStorage` with the specified capacity.
125    ///
126    /// The capacity determines the maximum number of DID documents that can be stored
127    /// in the cache. When the cache reaches this capacity, the least recently used
128    /// entries will be automatically evicted to make room for new entries.
129    ///
130    /// # Arguments
131    /// * `capacity` - The maximum number of DID documents to store. Must be greater than 0.
132    ///
133    /// # Examples
134    ///
135    /// ```rust
136    /// use atproto_identity::storage_lru::LruDidDocumentStorage;
137    /// use std::num::NonZeroUsize;
138    ///
139    /// // Create a cache that can hold up to 5000 DID documents
140    /// let storage = LruDidDocumentStorage::new(NonZeroUsize::new(5000).unwrap());
141    /// ```
142    ///
143    /// # Performance Considerations
144    ///
145    /// - Larger capacities provide better cache hit rates but use more memory
146    /// - The underlying LRU implementation has O(1) access time for all operations
147    /// - Memory usage is approximately: capacity * (average_document_size + DID_size + overhead)
148    /// - Document size varies based on number of services, verification methods, and aliases
149    pub fn new(capacity: NonZeroUsize) -> Self {
150        Self {
151            cache: Arc::new(Mutex::new(LruCache::new(capacity))),
152        }
153    }
154
155    /// Returns the current number of entries in the cache.
156    ///
157    /// This method provides visibility into cache usage for monitoring and debugging purposes.
158    /// The count represents the current number of DID documents stored in the cache.
159    ///
160    /// # Returns
161    /// The number of entries currently stored in the cache.
162    ///
163    /// # Examples
164    ///
165    /// ```rust
166    /// use atproto_identity::storage_lru::LruDidDocumentStorage;
167    /// use atproto_identity::storage::DidDocumentStorage;
168    /// use atproto_identity::model::Document;
169    /// use std::num::NonZeroUsize;
170    /// use std::collections::HashMap;
171    ///
172    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
173    /// let storage = LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap());
174    /// assert_eq!(storage.len(), 0);
175    ///
176    /// let doc1 = Document {
177    ///     context: vec![],
178    ///     id: "did:plc:example1".to_string(),
179    ///     also_known_as: vec![],
180    ///     service: vec![],
181    ///     verification_method: vec![],
182    ///     extra: HashMap::new(),
183    /// };
184    /// storage.store_document(doc1).await?;
185    /// assert_eq!(storage.len(), 1);
186    ///
187    /// let doc2 = Document {
188    ///     context: vec![],
189    ///     id: "did:plc:example2".to_string(),
190    ///     also_known_as: vec![],
191    ///     service: vec![],
192    ///     verification_method: vec![],
193    ///     extra: HashMap::new(),
194    /// };
195    /// storage.store_document(doc2).await?;
196    /// assert_eq!(storage.len(), 2);
197    /// # Ok::<(), anyhow::Error>(())
198    /// # }).unwrap();
199    /// ```
200    pub fn len(&self) -> usize {
201        self.cache.lock().unwrap().len()
202    }
203
204    /// Returns whether the cache is empty.
205    ///
206    /// # Returns
207    /// `true` if the cache contains no entries, `false` otherwise.
208    ///
209    /// # Examples
210    ///
211    /// ```rust
212    /// use atproto_identity::storage_lru::LruDidDocumentStorage;
213    /// use std::num::NonZeroUsize;
214    ///
215    /// let storage = LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap());
216    /// assert!(storage.is_empty());
217    /// ```
218    pub fn is_empty(&self) -> bool {
219        self.cache.lock().unwrap().is_empty()
220    }
221
222    /// Returns the maximum capacity of the cache.
223    ///
224    /// This returns the capacity that was set when the cache was created and represents
225    /// the maximum number of DID documents that can be stored before eviction occurs.
226    ///
227    /// # Returns
228    /// The maximum capacity of the cache.
229    ///
230    /// # Examples
231    ///
232    /// ```rust
233    /// use atproto_identity::storage_lru::LruDidDocumentStorage;
234    /// use std::num::NonZeroUsize;
235    ///
236    /// let capacity = NonZeroUsize::new(500).unwrap();
237    /// let storage = LruDidDocumentStorage::new(capacity);
238    /// assert_eq!(storage.capacity().get(), 500);
239    /// ```
240    pub fn capacity(&self) -> NonZeroUsize {
241        self.cache.lock().unwrap().cap()
242    }
243
244    /// Clears all entries from the cache.
245    ///
246    /// This method removes all DID documents from the cache, effectively resetting
247    /// it to an empty state. This can be useful for testing or when you need to
248    /// invalidate all cached data.
249    ///
250    /// # Examples
251    ///
252    /// ```rust
253    /// use atproto_identity::storage_lru::LruDidDocumentStorage;
254    /// use atproto_identity::storage::DidDocumentStorage;
255    /// use atproto_identity::model::Document;
256    /// use std::num::NonZeroUsize;
257    /// use std::collections::HashMap;
258    ///
259    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
260    /// let storage = LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap());
261    /// let document = Document {
262    ///     context: vec![],
263    ///     id: "did:plc:example".to_string(),
264    ///     also_known_as: vec![],
265    ///     service: vec![],
266    ///     verification_method: vec![],
267    ///     extra: HashMap::new(),
268    /// };
269    /// storage.store_document(document).await?;
270    /// assert_eq!(storage.len(), 1);
271    ///
272    /// storage.clear();
273    /// assert_eq!(storage.len(), 0);
274    /// assert!(storage.is_empty());
275    /// # Ok::<(), anyhow::Error>(())
276    /// # }).unwrap();
277    /// ```
278    pub fn clear(&self) {
279        self.cache.lock().unwrap().clear();
280    }
281}
282
283#[async_trait::async_trait]
284impl DidDocumentStorage for LruDidDocumentStorage {
285    /// Retrieves a DID document associated with the given DID from the LRU cache.
286    ///
287    /// This method looks up the complete DID document that is currently cached for the provided
288    /// DID. If the DID is found in the cache, the entry is moved to the front of the LRU
289    /// order (marking it as recently used) and the document is returned.
290    ///
291    /// # Arguments
292    /// * `did` - The DID to look up in the cache
293    ///
294    /// # Returns
295    /// * `Ok(Some(document))` - If the DID is found in the cache
296    /// * `Ok(None)` - If the DID is not found in the cache
297    /// * `Err(error)` - If an error occurs (primarily mutex poisoning, which is very rare)
298    ///
299    /// # Cache Behavior
300    ///
301    /// When a document is successfully retrieved, it's marked as recently used in the LRU order,
302    /// making it less likely to be evicted in future operations.
303    ///
304    /// # Examples
305    ///
306    /// ```rust
307    /// use atproto_identity::storage_lru::LruDidDocumentStorage;
308    /// use atproto_identity::storage::DidDocumentStorage;
309    /// use atproto_identity::model::Document;
310    /// use std::num::NonZeroUsize;
311    /// use std::collections::HashMap;
312    ///
313    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
314    /// let storage = LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap());
315    ///
316    /// // Cache miss - DID not in cache
317    /// let document = storage.get_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
318    /// assert_eq!(document, None);
319    ///
320    /// // Add document to cache
321    /// let doc = Document {
322    ///     context: vec![],
323    ///     id: "did:plc:bv6ggog3tya2z3vxsub7hnal".to_string(),
324    ///     also_known_as: vec!["at://alice.bsky.social".to_string()],
325    ///     service: vec![],
326    ///     verification_method: vec![],
327    ///     extra: HashMap::new(),
328    /// };
329    /// storage.store_document(doc.clone()).await?;
330    ///
331    /// // Cache hit - DID found in cache
332    /// let document = storage.get_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
333    /// assert_eq!(document.as_ref().map(|d| &d.id), Some(&doc.id));
334    /// # Ok::<(), anyhow::Error>(())
335    /// # }).unwrap();
336    /// ```
337    async fn get_document_by_did(&self, did: &str) -> Result<Option<Document>> {
338        let mut cache = self
339            .cache
340            .lock()
341            .map_err(|e| StorageError::CacheLockFailedGet {
342                details: e.to_string(),
343            })?;
344
345        Ok(cache.get(did).cloned())
346    }
347
348    /// Stores or updates a DID document in the LRU cache.
349    ///
350    /// This method stores a complete DID document in the cache. If the DID already exists
351    /// in the cache, its document is updated and the entry is moved to the front of
352    /// the LRU order. If the DID is new and the cache is at capacity, the least recently
353    /// used entry is evicted to make room.
354    ///
355    /// # Arguments
356    /// * `document` - The complete DID document to store. The document's `id` field
357    ///               will be used as the storage key.
358    ///
359    /// # Returns
360    /// * `Ok(())` - If the document was successfully stored
361    /// * `Err(error)` - If an error occurs (primarily mutex poisoning, which is very rare)
362    ///
363    /// # Cache Behavior
364    ///
365    /// - If the cache is at capacity and this is a new DID, the least recently used entry is evicted
366    /// - The new or updated entry is placed at the front of the LRU order
367    /// - Existing entries with the same DID are updated in place
368    ///
369    /// # Examples
370    ///
371    /// ```rust
372    /// use atproto_identity::storage_lru::LruDidDocumentStorage;
373    /// use atproto_identity::storage::DidDocumentStorage;
374    /// use atproto_identity::model::Document;
375    /// use std::num::NonZeroUsize;
376    /// use std::collections::HashMap;
377    ///
378    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
379    /// let storage = LruDidDocumentStorage::new(NonZeroUsize::new(2).unwrap()); // Small cache for demo
380    ///
381    /// // Add first document
382    /// let doc1 = Document {
383    ///     context: vec![],
384    ///     id: "did:plc:user1".to_string(),
385    ///     also_known_as: vec!["at://alice.bsky.social".to_string()],
386    ///     service: vec![],
387    ///     verification_method: vec![],
388    ///     extra: HashMap::new(),
389    /// };
390    /// storage.store_document(doc1).await?;
391    /// assert_eq!(storage.len(), 1);
392    ///
393    /// // Add second document
394    /// let doc2 = Document {
395    ///     context: vec![],
396    ///     id: "did:plc:user2".to_string(),
397    ///     also_known_as: vec!["at://bob.bsky.social".to_string()],
398    ///     service: vec![],
399    ///     verification_method: vec![],
400    ///     extra: HashMap::new(),
401    /// };
402    /// storage.store_document(doc2).await?;
403    /// assert_eq!(storage.len(), 2);
404    ///
405    /// // Add third document - this will evict the least recently used entry (user1)
406    /// let doc3 = Document {
407    ///     context: vec![],
408    ///     id: "did:plc:user3".to_string(),
409    ///     also_known_as: vec!["at://charlie.bsky.social".to_string()],
410    ///     service: vec![],
411    ///     verification_method: vec![],
412    ///     extra: HashMap::new(),
413    /// };
414    /// storage.store_document(doc3).await?;
415    /// assert_eq!(storage.len(), 2); // Still at capacity
416    ///
417    /// // user1 should be evicted
418    /// let document = storage.get_document_by_did("did:plc:user1").await?;
419    /// assert_eq!(document, None);
420    ///
421    /// // user2 and user3 should still be present
422    /// let doc2_retrieved = storage.get_document_by_did("did:plc:user2").await?;
423    /// let doc3_retrieved = storage.get_document_by_did("did:plc:user3").await?;
424    /// assert!(doc2_retrieved.is_some());
425    /// assert!(doc3_retrieved.is_some());
426    /// # Ok::<(), anyhow::Error>(())
427    /// # }).unwrap();
428    /// ```
429    async fn store_document(&self, document: Document) -> Result<()> {
430        let mut cache = self
431            .cache
432            .lock()
433            .map_err(|e| StorageError::CacheLockFailedStore {
434                details: e.to_string(),
435            })?;
436
437        cache.put(document.id.clone(), document);
438        Ok(())
439    }
440
441    /// Deletes a DID document from the LRU cache by DID.
442    ///
443    /// This method removes a DID document from the cache. If the DID exists in the cache,
444    /// it is removed entirely, freeing up space for new entries.
445    ///
446    /// # Arguments
447    /// * `did` - The DID identifying the document to delete
448    ///
449    /// # Returns
450    /// * `Ok(())` - If the document was successfully deleted or didn't exist
451    /// * `Err(error)` - If an error occurs (primarily mutex poisoning, which is very rare)
452    ///
453    /// # Cache Behavior
454    ///
455    /// - If the DID exists in the cache, it is removed completely
456    /// - If the DID doesn't exist, the operation succeeds without error
457    /// - Removing entries frees up capacity for new entries
458    ///
459    /// # Examples
460    ///
461    /// ```rust
462    /// use atproto_identity::storage_lru::LruDidDocumentStorage;
463    /// use atproto_identity::storage::DidDocumentStorage;
464    /// use atproto_identity::model::Document;
465    /// use std::num::NonZeroUsize;
466    /// use std::collections::HashMap;
467    ///
468    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
469    /// let storage = LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap());
470    ///
471    /// // Add a document
472    /// let document = Document {
473    ///     context: vec![],
474    ///     id: "did:plc:bv6ggog3tya2z3vxsub7hnal".to_string(),
475    ///     also_known_as: vec!["at://alice.bsky.social".to_string()],
476    ///     service: vec![],
477    ///     verification_method: vec![],
478    ///     extra: HashMap::new(),
479    /// };
480    /// storage.store_document(document).await?;
481    /// let retrieved = storage.get_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
482    /// assert!(retrieved.is_some());
483    ///
484    /// // Delete the document
485    /// storage.delete_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
486    /// let retrieved = storage.get_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
487    /// assert_eq!(retrieved, None);
488    ///
489    /// // Deleting non-existent entry is safe
490    /// storage.delete_document_by_did("did:plc:nonexistent").await?;
491    /// # Ok::<(), anyhow::Error>(())
492    /// # }).unwrap();
493    /// ```
494    async fn delete_document_by_did(&self, did: &str) -> Result<()> {
495        let mut cache = self
496            .cache
497            .lock()
498            .map_err(|e| StorageError::CacheLockFailedDelete {
499                details: e.to_string(),
500            })?;
501
502        cache.pop(did);
503        Ok(())
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use std::collections::HashMap;
511    use std::num::NonZeroUsize;
512
513    fn create_test_document(did: &str, handle: &str) -> Document {
514        Document {
515            context: vec![],
516            id: did.to_string(),
517            also_known_as: vec![format!("at://{}", handle)],
518            service: vec![],
519            verification_method: vec![],
520            extra: HashMap::new(),
521        }
522    }
523
524    #[tokio::test]
525    async fn test_new_storage() {
526        let storage = LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap());
527        assert_eq!(storage.len(), 0);
528        assert!(storage.is_empty());
529        assert_eq!(storage.capacity().get(), 100);
530    }
531
532    #[tokio::test]
533    async fn test_basic_operations() -> Result<()> {
534        let storage = LruDidDocumentStorage::new(NonZeroUsize::new(10).unwrap());
535
536        // Test get on empty cache
537        let result = storage.get_document_by_did("did:plc:test").await?;
538        assert_eq!(result, None);
539
540        // Test store and get
541        let document = create_test_document("did:plc:test", "test.handle");
542        storage.store_document(document.clone()).await?;
543        let result = storage.get_document_by_did("did:plc:test").await?;
544        assert!(result.is_some());
545        assert_eq!(result.as_ref().unwrap().id, document.id);
546        assert_eq!(storage.len(), 1);
547
548        // Test update existing
549        let updated_document = create_test_document("did:plc:test", "updated.handle");
550        storage.store_document(updated_document.clone()).await?;
551        let result = storage.get_document_by_did("did:plc:test").await?;
552        assert!(result.is_some());
553        assert_eq!(
554            result.as_ref().unwrap().also_known_as,
555            updated_document.also_known_as
556        );
557        assert_eq!(storage.len(), 1); // Should still be 1
558
559        // Test delete
560        storage.delete_document_by_did("did:plc:test").await?;
561        let result = storage.get_document_by_did("did:plc:test").await?;
562        assert_eq!(result, None);
563        assert_eq!(storage.len(), 0);
564
565        Ok(())
566    }
567
568    #[tokio::test]
569    async fn test_lru_eviction() -> Result<()> {
570        let storage = LruDidDocumentStorage::new(NonZeroUsize::new(2).unwrap());
571
572        // Fill cache to capacity
573        let doc1 = create_test_document("did:plc:user1", "user1.handle");
574        let doc2 = create_test_document("did:plc:user2", "user2.handle");
575        storage.store_document(doc1.clone()).await?;
576        storage.store_document(doc2).await?;
577        assert_eq!(storage.len(), 2);
578
579        // Access user1 to make it recently used
580        let _ = storage.get_document_by_did("did:plc:user1").await?;
581
582        // Add user3, which should evict user2 (least recently used)
583        let doc3 = create_test_document("did:plc:user3", "user3.handle");
584        storage.store_document(doc3.clone()).await?;
585        assert_eq!(storage.len(), 2);
586
587        // user1 and user3 should be present, user2 should be evicted
588        let result1 = storage.get_document_by_did("did:plc:user1").await?;
589        assert!(result1.is_some());
590        assert_eq!(result1.unwrap().also_known_as, doc1.also_known_as);
591
592        let result3 = storage.get_document_by_did("did:plc:user3").await?;
593        assert!(result3.is_some());
594        assert_eq!(result3.unwrap().also_known_as, doc3.also_known_as);
595
596        assert_eq!(storage.get_document_by_did("did:plc:user2").await?, None);
597
598        Ok(())
599    }
600
601    #[tokio::test]
602    async fn test_clear() -> Result<()> {
603        let storage = LruDidDocumentStorage::new(NonZeroUsize::new(10).unwrap());
604
605        // Add some entries
606        let doc1 = create_test_document("did:plc:user1", "user1.handle");
607        let doc2 = create_test_document("did:plc:user2", "user2.handle");
608        storage.store_document(doc1).await?;
609        storage.store_document(doc2).await?;
610        assert_eq!(storage.len(), 2);
611
612        // Clear cache
613        storage.clear();
614        assert_eq!(storage.len(), 0);
615        assert!(storage.is_empty());
616
617        // Verify entries are gone
618        assert_eq!(storage.get_document_by_did("did:plc:user1").await?, None);
619        assert_eq!(storage.get_document_by_did("did:plc:user2").await?, None);
620
621        Ok(())
622    }
623
624    #[tokio::test]
625    async fn test_thread_safety() -> Result<()> {
626        let storage = Arc::new(LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap()));
627        let mut handles = Vec::new();
628
629        // Spawn multiple tasks that concurrently access the storage
630        for i in 0..10 {
631            let storage_clone = Arc::clone(&storage);
632            let handle = tokio::spawn(async move {
633                let did = format!("did:plc:user{}", i);
634                let handle_name = format!("user{}.handle", i);
635                let document = create_test_document(&did, &handle_name);
636
637                // Store a document
638                storage_clone.store_document(document.clone()).await?;
639
640                // Get the document back
641                let result = storage_clone.get_document_by_did(&did).await?;
642                assert!(result.is_some());
643                assert_eq!(result.unwrap().id, document.id);
644
645                // Delete the document
646                storage_clone.delete_document_by_did(&did).await?;
647                let result = storage_clone.get_document_by_did(&did).await?;
648                assert_eq!(result, None);
649
650                Ok::<(), anyhow::Error>(())
651            });
652            handles.push(handle);
653        }
654
655        // Wait for all tasks to complete
656        for handle in handles {
657            handle.await??;
658        }
659
660        // Storage should be empty after all deletions
661        assert_eq!(storage.len(), 0);
662        Ok(())
663    }
664
665    #[tokio::test]
666    async fn test_delete_nonexistent() -> Result<()> {
667        let storage = LruDidDocumentStorage::new(NonZeroUsize::new(10).unwrap());
668
669        // Deleting non-existent entry should not error
670        storage
671            .delete_document_by_did("did:plc:nonexistent")
672            .await?;
673        assert_eq!(storage.len(), 0);
674
675        Ok(())
676    }
677
678    #[tokio::test]
679    async fn test_document_content_preservation() -> Result<()> {
680        let storage = LruDidDocumentStorage::new(NonZeroUsize::new(10).unwrap());
681
682        // Create a document with complex content
683        let mut complex_document = Document {
684            context: vec![],
685            id: "did:plc:complex".to_string(),
686            also_known_as: vec![
687                "at://alice.bsky.social".to_string(),
688                "https://alice.example.com".to_string(),
689            ],
690            service: vec![],
691            verification_method: vec![],
692            extra: HashMap::new(),
693        };
694        complex_document.extra.insert(
695            "custom_field".to_string(),
696            serde_json::json!("custom_value"),
697        );
698
699        // Store and retrieve the document
700        storage.store_document(complex_document.clone()).await?;
701        let retrieved = storage.get_document_by_did("did:plc:complex").await?;
702
703        // Verify all content is preserved
704        assert!(retrieved.is_some());
705        let retrieved = retrieved.unwrap();
706        assert_eq!(retrieved.id, complex_document.id);
707        assert_eq!(retrieved.also_known_as, complex_document.also_known_as);
708        assert_eq!(retrieved.extra, complex_document.extra);
709
710        Ok(())
711    }
712}