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}