A library for ATProtocol identities.
at main 27 kB view raw
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}