QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.

feature: set

+171
+137
src/handle_resolver/redis.rs
··· 378 378 } 379 379 } 380 380 } 381 + 382 + async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> { 383 + // Normalize the handle to lowercase 384 + let handle = handle.to_lowercase(); 385 + let handle_key = self.make_key(&handle); 386 + let did_key = self.make_key(did); 387 + 388 + match self.pool.get().await { 389 + Ok(mut conn) => { 390 + // Create a resolution result for the successful mapping 391 + let resolution_result = match HandleResolutionResult::success(did) { 392 + Ok(res) => res, 393 + Err(e) => { 394 + tracing::warn!("Failed to create resolution result for set operation: {}", e); 395 + self.metrics.incr("resolver.redis.set_result_create_error").await; 396 + return Err(HandleResolverError::InvalidSubject( 397 + format!("Failed to create resolution result: {}", e) 398 + )); 399 + } 400 + }; 401 + 402 + // Serialize to bytes 403 + match resolution_result.to_bytes() { 404 + Ok(bytes) => { 405 + // Set the handle -> DID mapping with expiration 406 + if let Err(e) = conn 407 + .set_ex::<_, _, ()>(&handle_key, bytes, self.ttl_seconds()) 408 + .await 409 + { 410 + tracing::warn!("Failed to set handle->DID mapping in Redis: {}", e); 411 + self.metrics.incr("resolver.redis.set_cache_error").await; 412 + return Err(HandleResolverError::ResolutionFailed( 413 + format!("Failed to set cache: {}", e) 414 + )); 415 + } 416 + 417 + // Set the reverse DID -> handle mapping 418 + if let Err(e) = conn 419 + .set_ex::<_, _, ()>(&did_key, handle.as_bytes(), self.ttl_seconds()) 420 + .await 421 + { 422 + tracing::warn!("Failed to set DID->handle mapping in Redis: {}", e); 423 + self.metrics.incr("resolver.redis.set_reverse_cache_error").await; 424 + // Don't fail the operation, but log the warning 425 + } 426 + 427 + tracing::debug!("Set handle {} -> DID {} mapping in cache", handle, did); 428 + self.metrics.incr("resolver.redis.set_success").await; 429 + Ok(()) 430 + } 431 + Err(e) => { 432 + tracing::warn!("Failed to serialize resolution result for set operation: {}", e); 433 + self.metrics.incr("resolver.redis.set_serialize_error").await; 434 + Err(HandleResolverError::InvalidSubject( 435 + format!("Failed to serialize: {}", e) 436 + )) 437 + } 438 + } 439 + } 440 + Err(e) => { 441 + tracing::warn!("Failed to get Redis connection for set operation: {}", e); 442 + self.metrics.incr("resolver.redis.set_connection_error").await; 443 + Err(HandleResolverError::ResolutionFailed( 444 + format!("Redis connection error: {}", e) 445 + )) 446 + } 447 + } 448 + } 381 449 } 382 450 383 451 /// Create a new Redis-backed handle resolver with default 90-day TTL. ··· 675 743 let exists: bool = conn.exists(&key).await.unwrap_or(false); 676 744 assert!(!exists, "Key for {} should not exist after purge", input); 677 745 } 746 + } 747 + } 748 + 749 + #[tokio::test] 750 + async fn test_redis_handle_resolver_set_method() { 751 + let pool = match crate::test_helpers::get_test_redis_pool() { 752 + Some(p) => p, 753 + None => return, 754 + }; 755 + 756 + // Create mock resolver 757 + let mock_resolver = Arc::new(MockHandleResolver { 758 + should_fail: false, 759 + expected_did: "did:plc:old".to_string(), 760 + }); 761 + 762 + // Create metrics publisher 763 + let metrics = Arc::new(crate::metrics::NoOpMetricsPublisher); 764 + 765 + // Create Redis-backed resolver with a unique key prefix for testing 766 + let test_prefix = format!( 767 + "test:handle:{}:", 768 + std::time::SystemTime::now() 769 + .duration_since(std::time::UNIX_EPOCH) 770 + .unwrap() 771 + .as_nanos() 772 + ); 773 + let redis_resolver = RedisHandleResolver::with_full_config( 774 + mock_resolver, 775 + pool.clone(), 776 + test_prefix.clone(), 777 + 3600, 778 + metrics, 779 + ); 780 + 781 + let test_handle = "charlie.bsky.social"; 782 + let test_did = "did:plc:newuser123"; 783 + 784 + // Set the mapping using the trait method 785 + redis_resolver.set(test_handle, test_did).await.unwrap(); 786 + 787 + // Verify the mapping by resolving the handle 788 + let (resolved_did, _) = redis_resolver.resolve(test_handle).await.unwrap(); 789 + assert_eq!(resolved_did, test_did); 790 + 791 + // Test that uppercase handles are normalized 792 + redis_resolver.set("DAVE.BSKY.SOCIAL", "did:plc:dave456").await.unwrap(); 793 + let (resolved_did2, _) = redis_resolver.resolve("dave.bsky.social").await.unwrap(); 794 + assert_eq!(resolved_did2, "did:plc:dave456"); 795 + 796 + // Verify both forward and reverse mappings exist 797 + if let Ok(mut conn) = pool.get().await { 798 + let mut h = MetroHash64::default(); 799 + h.write(test_handle.as_bytes()); 800 + let handle_key = format!("{}{}", test_prefix, h.finish()); 801 + 802 + let mut h2 = MetroHash64::default(); 803 + h2.write(test_did.as_bytes()); 804 + let did_key = format!("{}{}", test_prefix, h2.finish()); 805 + 806 + // Check both keys exist 807 + let handle_exists: bool = conn.exists(&handle_key).await.unwrap(); 808 + assert!(handle_exists, "Handle key should exist after set"); 809 + 810 + let did_exists: bool = conn.exists(&did_key).await.unwrap(); 811 + assert!(did_exists, "DID key should exist after set"); 812 + 813 + // Clean up test data 814 + let _: Result<(), _> = conn.del(&[&handle_key, &did_key]).await; 678 815 } 679 816 } 680 817
+34
src/handle_resolver/traits.rs
··· 77 77 async fn purge(&self, _subject: &str) -> Result<(), HandleResolverError> { 78 78 Ok(()) 79 79 } 80 + 81 + /// Set a handle-to-DID mapping in the cache. 82 + /// 83 + /// This method allows manually setting or updating a cached mapping between 84 + /// a handle and its corresponding DID. This is useful for pre-populating 85 + /// caches or updating stale entries. 86 + /// 87 + /// # Arguments 88 + /// 89 + /// * `handle` - The handle to cache (e.g., "alice.bsky.social") 90 + /// * `did` - The DID to associate with the handle (e.g., "did:plc:xyz123") 91 + /// 92 + /// # Returns 93 + /// 94 + /// Ok(()) if the mapping was successfully set or if the implementation 95 + /// doesn't support manual cache updates. Most implementations will simply 96 + /// return Ok(()) as a no-op. 97 + /// 98 + /// # Default Implementation 99 + /// 100 + /// The default implementation is a no-op that always returns Ok(()). 101 + async fn set(&self, _handle: &str, _did: &str) -> Result<(), HandleResolverError> { 102 + Ok(()) 103 + } 80 104 } 81 105 82 106 #[cfg(test)] ··· 102 126 assert!(resolver.purge("alice.bsky.social").await.is_ok()); 103 127 assert!(resolver.purge("did:plc:xyz123").await.is_ok()); 104 128 assert!(resolver.purge("").await.is_ok()); 129 + } 130 + 131 + #[tokio::test] 132 + async fn test_default_set_implementation() { 133 + let resolver = NoOpTestResolver; 134 + 135 + // Default implementation should always return Ok(()) 136 + assert!(resolver.set("alice.bsky.social", "did:plc:xyz123").await.is_ok()); 137 + assert!(resolver.set("bob.example.com", "did:web:example.com").await.is_ok()); 138 + assert!(resolver.set("", "").await.is_ok()); 105 139 } 106 140 }