A library for ATProtocol identities.
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::traits::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::traits::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::traits::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::traits::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::traits::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::traits::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::traits::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 crate::traits::DidDocumentStorage;
511 use std::collections::HashMap;
512 use std::num::NonZeroUsize;
513
514 fn create_test_document(did: &str, handle: &str) -> Document {
515 Document {
516 context: vec![],
517 id: did.to_string(),
518 also_known_as: vec![format!("at://{}", handle)],
519 service: vec![],
520 verification_method: vec![],
521 extra: HashMap::new(),
522 }
523 }
524
525 #[tokio::test]
526 async fn test_new_storage() {
527 let storage = LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap());
528 assert_eq!(storage.len(), 0);
529 assert!(storage.is_empty());
530 assert_eq!(storage.capacity().get(), 100);
531 }
532
533 #[tokio::test]
534 async fn test_basic_operations() -> Result<()> {
535 let storage = LruDidDocumentStorage::new(NonZeroUsize::new(10).unwrap());
536
537 // Test get on empty cache
538 let result = storage.get_document_by_did("did:plc:test").await?;
539 assert_eq!(result, None);
540
541 // Test store and get
542 let document = create_test_document("did:plc:test", "test.handle");
543 storage.store_document(document.clone()).await?;
544 let result = storage.get_document_by_did("did:plc:test").await?;
545 assert!(result.is_some());
546 assert_eq!(result.as_ref().unwrap().id, document.id);
547 assert_eq!(storage.len(), 1);
548
549 // Test update existing
550 let updated_document = create_test_document("did:plc:test", "updated.handle");
551 storage.store_document(updated_document.clone()).await?;
552 let result = storage.get_document_by_did("did:plc:test").await?;
553 assert!(result.is_some());
554 assert_eq!(
555 result.as_ref().unwrap().also_known_as,
556 updated_document.also_known_as
557 );
558 assert_eq!(storage.len(), 1); // Should still be 1
559
560 // Test delete
561 storage.delete_document_by_did("did:plc:test").await?;
562 let result = storage.get_document_by_did("did:plc:test").await?;
563 assert_eq!(result, None);
564 assert_eq!(storage.len(), 0);
565
566 Ok(())
567 }
568
569 #[tokio::test]
570 async fn test_lru_eviction() -> Result<()> {
571 let storage = LruDidDocumentStorage::new(NonZeroUsize::new(2).unwrap());
572
573 // Fill cache to capacity
574 let doc1 = create_test_document("did:plc:user1", "user1.handle");
575 let doc2 = create_test_document("did:plc:user2", "user2.handle");
576 storage.store_document(doc1.clone()).await?;
577 storage.store_document(doc2).await?;
578 assert_eq!(storage.len(), 2);
579
580 // Access user1 to make it recently used
581 let _ = storage.get_document_by_did("did:plc:user1").await?;
582
583 // Add user3, which should evict user2 (least recently used)
584 let doc3 = create_test_document("did:plc:user3", "user3.handle");
585 storage.store_document(doc3.clone()).await?;
586 assert_eq!(storage.len(), 2);
587
588 // user1 and user3 should be present, user2 should be evicted
589 let result1 = storage.get_document_by_did("did:plc:user1").await?;
590 assert!(result1.is_some());
591 assert_eq!(result1.unwrap().also_known_as, doc1.also_known_as);
592
593 let result3 = storage.get_document_by_did("did:plc:user3").await?;
594 assert!(result3.is_some());
595 assert_eq!(result3.unwrap().also_known_as, doc3.also_known_as);
596
597 assert_eq!(storage.get_document_by_did("did:plc:user2").await?, None);
598
599 Ok(())
600 }
601
602 #[tokio::test]
603 async fn test_clear() -> Result<()> {
604 let storage = LruDidDocumentStorage::new(NonZeroUsize::new(10).unwrap());
605
606 // Add some entries
607 let doc1 = create_test_document("did:plc:user1", "user1.handle");
608 let doc2 = create_test_document("did:plc:user2", "user2.handle");
609 storage.store_document(doc1).await?;
610 storage.store_document(doc2).await?;
611 assert_eq!(storage.len(), 2);
612
613 // Clear cache
614 storage.clear();
615 assert_eq!(storage.len(), 0);
616 assert!(storage.is_empty());
617
618 // Verify entries are gone
619 assert_eq!(storage.get_document_by_did("did:plc:user1").await?, None);
620 assert_eq!(storage.get_document_by_did("did:plc:user2").await?, None);
621
622 Ok(())
623 }
624
625 #[tokio::test]
626 async fn test_thread_safety() -> Result<()> {
627 let storage = Arc::new(LruDidDocumentStorage::new(NonZeroUsize::new(100).unwrap()));
628 let mut handles = Vec::new();
629
630 // Spawn multiple tasks that concurrently access the storage
631 for i in 0..10 {
632 let storage_clone = Arc::clone(&storage);
633 let handle = tokio::spawn(async move {
634 let did = format!("did:plc:user{}", i);
635 let handle_name = format!("user{}.handle", i);
636 let document = create_test_document(&did, &handle_name);
637
638 // Store a document
639 storage_clone.store_document(document.clone()).await?;
640
641 // Get the document back
642 let result = storage_clone.get_document_by_did(&did).await?;
643 assert!(result.is_some());
644 assert_eq!(result.unwrap().id, document.id);
645
646 // Delete the document
647 storage_clone.delete_document_by_did(&did).await?;
648 let result = storage_clone.get_document_by_did(&did).await?;
649 assert_eq!(result, None);
650
651 Ok::<(), anyhow::Error>(())
652 });
653 handles.push(handle);
654 }
655
656 // Wait for all tasks to complete
657 for handle in handles {
658 handle.await??;
659 }
660
661 // Storage should be empty after all deletions
662 assert_eq!(storage.len(), 0);
663 Ok(())
664 }
665
666 #[tokio::test]
667 async fn test_delete_nonexistent() -> Result<()> {
668 let storage = LruDidDocumentStorage::new(NonZeroUsize::new(10).unwrap());
669
670 // Deleting non-existent entry should not error
671 storage
672 .delete_document_by_did("did:plc:nonexistent")
673 .await?;
674 assert_eq!(storage.len(), 0);
675
676 Ok(())
677 }
678
679 #[tokio::test]
680 async fn test_document_content_preservation() -> Result<()> {
681 let storage = LruDidDocumentStorage::new(NonZeroUsize::new(10).unwrap());
682
683 // Create a document with complex content
684 let mut complex_document = Document {
685 context: vec![],
686 id: "did:plc:complex".to_string(),
687 also_known_as: vec![
688 "at://alice.bsky.social".to_string(),
689 "https://alice.example.com".to_string(),
690 ],
691 service: vec![],
692 verification_method: vec![],
693 extra: HashMap::new(),
694 };
695 complex_document.extra.insert(
696 "custom_field".to_string(),
697 serde_json::json!("custom_value"),
698 );
699
700 // Store and retrieve the document
701 storage.store_document(complex_document.clone()).await?;
702 let retrieved = storage.get_document_by_did("did:plc:complex").await?;
703
704 // Verify all content is preserved
705 assert!(retrieved.is_some());
706 let retrieved = retrieved.unwrap();
707 assert_eq!(retrieved.id, complex_document.id);
708 assert_eq!(retrieved.also_known_as, complex_document.also_known_as);
709 assert_eq!(retrieved.extra, complex_document.extra);
710
711 Ok(())
712 }
713}