Something went wrong. Try again.
QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-back
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361//! Jetstream event handler for QuickDID//!//! This module provides the event handler for processing AT Protocol Jetstream events,//! specifically handling Account and Identity events to maintain cache consistency.
use crate::handle_resolver::HandleResolver;use crate::metrics::MetricsPublisher;use anyhow::Result;use atproto_jetstream::{EventHandler, JetstreamEvent};use std::sync::Arc;use tracing::{debug, info, warn};
/// Jetstream event handler for QuickDID////// This handler processes AT Protocol events from the Jetstream firehose to keep/// the handle resolver cache in sync with the network state.////// # Event Processing////// ## Account Events/// - When an account is marked as "deleted" or "deactivated", the DID is purged from the cache/// - Metrics are tracked for successful and failed purge operations////// ## Identity Events/// - When an identity event contains a handle, the handle-to-DID mapping is updated/// - When an identity event lacks a handle (indicating removal), the DID is purged/// - Metrics are tracked for successful and failed update/purge operations////// # Example////// ```no_run/// use quickdid::jetstream_handler::QuickDidEventHandler;/// use quickdid::handle_resolver::HandleResolver;/// use quickdid::metrics::MetricsPublisher;/// use std::sync::Arc;////// # async fn example(resolver: Arc<dyn HandleResolver>, metrics: Arc<dyn MetricsPublisher>) {/// let handler = QuickDidEventHandler::new(resolver, metrics);/// // Register with a JetstreamConsumer/// # }/// ```pub struct QuickDidEventHandler { resolver: Arc<dyn HandleResolver>, metrics: Arc<dyn MetricsPublisher>,}
impl QuickDidEventHandler { /// Create a new Jetstream event handler /// /// # Arguments /// /// * `resolver` - The handle resolver to use for cache operations /// * `metrics` - The metrics publisher for tracking event processing pub fn new(resolver: Arc<dyn HandleResolver>, metrics: Arc<dyn MetricsPublisher>) -> Self { Self { resolver, metrics } }}
#[async_trait::async_trait]impl EventHandler for QuickDidEventHandler { fn handler_id(&self) -> String { "quickdid_handler".to_string() }
async fn handle_event(&self, event: JetstreamEvent) -> Result<()> { match event { JetstreamEvent::Account { did, kind, .. } => { // If account kind is "deleted" or "deactivated", purge the DID if kind == "deleted" || kind == "deactivated" { info!(did = %did, kind = %kind, "Purging account"); match self.resolver.purge(&did).await { Ok(()) => { self.metrics.incr("jetstream.account.purged").await; } Err(e) => { warn!(did = %did, error = ?e, "Failed to purge DID"); self.metrics.incr("jetstream.account.purge_error").await; } } } self.metrics.incr("jetstream.account.processed").await; } JetstreamEvent::Identity { did, identity, .. } => { // Extract handle from identity JSON if available if !identity.is_null() { if let Some(handle_value) = identity.get("handle") { if let Some(handle) = handle_value.as_str() { info!(handle = %handle, did = %did, "Updating identity mapping"); match self.resolver.set(handle, &did).await { Ok(()) => { self.metrics.incr("jetstream.identity.updated").await; } Err(e) => { warn!(handle = %handle, did = %did, error = ?e, "Failed to update mapping"); self.metrics.incr("jetstream.identity.update_error").await; } } } else { // No handle or invalid handle, purge the DID info!(did = %did, "Purging identity without valid handle"); match self.resolver.purge(&did).await { Ok(()) => { self.metrics.incr("jetstream.identity.purged").await; } Err(e) => { warn!(did = %did, error = ?e, "Failed to purge DID"); self.metrics.incr("jetstream.identity.purge_error").await; } } } } else { // No handle field, purge the DID info!(did = %did, "Purging identity without handle field"); match self.resolver.purge(&did).await { Ok(()) => { self.metrics.incr("jetstream.identity.purged").await; } Err(e) => { warn!(did = %did, error = ?e, "Failed to purge DID"); self.metrics.incr("jetstream.identity.purge_error").await; } } } } else { // Null identity means removed, purge the DID info!(did = %did, "Purging identity with null info"); match self.resolver.purge(&did).await { Ok(()) => { self.metrics.incr("jetstream.identity.purged").await; } Err(e) => { warn!(did = %did, error = ?e, "Failed to purge DID"); self.metrics.incr("jetstream.identity.purge_error").await; } } } self.metrics.incr("jetstream.identity.processed").await; } _ => { // Other event types we don't care about debug!("Ignoring unhandled Jetstream event type"); } } Ok(()) }}
#[cfg(test)]mod tests { use super::*; use crate::handle_resolver::HandleResolverError; use crate::metrics::NoOpMetricsPublisher; use async_trait::async_trait; use serde_json::json;
/// Mock resolver for testing struct MockResolver { purge_called: std::sync::Arc<std::sync::Mutex<Vec<String>>>, set_called: std::sync::Arc<std::sync::Mutex<Vec<(String, String)>>>, }
impl MockResolver { fn new() -> Self { Self { purge_called: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), set_called: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), } }
fn get_purge_calls(&self) -> Vec<String> { self.purge_called.lock().unwrap().clone() }
fn get_set_calls(&self) -> Vec<(String, String)> { self.set_called.lock().unwrap().clone() } }
#[async_trait] impl HandleResolver for MockResolver { async fn resolve(&self, _handle: &str) -> Result<(String, u64), HandleResolverError> { unimplemented!("Not needed for tests") }
async fn purge(&self, subject: &str) -> Result<(), HandleResolverError> { self.purge_called.lock().unwrap().push(subject.to_string()); Ok(()) }
async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> { self.set_called .lock() .unwrap() .push((handle.to_string(), did.to_string())); Ok(()) } }
#[tokio::test] async fn test_account_deleted_event() { let resolver = Arc::new(MockResolver::new()); let metrics = Arc::new(NoOpMetricsPublisher::new()); let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
// Create a deleted account event let event = JetstreamEvent::Account { did: "did:plc:test123".to_string(), kind: "deleted".to_string(), time_us: 0, identity: json!(null), };
handler.handle_event(event).await.unwrap();
// Verify the DID was purged let purge_calls = resolver.get_purge_calls(); assert_eq!(purge_calls.len(), 1); assert_eq!(purge_calls[0], "did:plc:test123"); }
#[tokio::test] async fn test_account_deactivated_event() { let resolver = Arc::new(MockResolver::new()); let metrics = Arc::new(NoOpMetricsPublisher::new()); let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
// Create a deactivated account event let event = JetstreamEvent::Account { did: "did:plc:test456".to_string(), kind: "deactivated".to_string(), time_us: 0, identity: json!(null), };
handler.handle_event(event).await.unwrap();
// Verify the DID was purged let purge_calls = resolver.get_purge_calls(); assert_eq!(purge_calls.len(), 1); assert_eq!(purge_calls[0], "did:plc:test456"); }
#[tokio::test] async fn test_account_active_event() { let resolver = Arc::new(MockResolver::new()); let metrics = Arc::new(NoOpMetricsPublisher::new()); let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
// Create an active account event (should not purge) let event = JetstreamEvent::Account { did: "did:plc:test789".to_string(), kind: "active".to_string(), time_us: 0, identity: json!(null), };
handler.handle_event(event).await.unwrap();
// Verify the DID was NOT purged let purge_calls = resolver.get_purge_calls(); assert_eq!(purge_calls.len(), 0); }
#[tokio::test] async fn test_identity_with_handle_event() { let resolver = Arc::new(MockResolver::new()); let metrics = Arc::new(NoOpMetricsPublisher::new()); let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
// Create an identity event with a handle let event = JetstreamEvent::Identity { did: "did:plc:testuser".to_string(), kind: "update".to_string(), time_us: 0, identity: json!({ "handle": "alice.bsky.social" }), };
handler.handle_event(event).await.unwrap();
// Verify the set method was called let set_calls = resolver.get_set_calls(); assert_eq!(set_calls.len(), 1); assert_eq!( set_calls[0], ( "alice.bsky.social".to_string(), "did:plc:testuser".to_string() ) );
// Verify no purge was called let purge_calls = resolver.get_purge_calls(); assert_eq!(purge_calls.len(), 0); }
#[tokio::test] async fn test_identity_without_handle_event() { let resolver = Arc::new(MockResolver::new()); let metrics = Arc::new(NoOpMetricsPublisher::new()); let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
// Create an identity event without a handle field let event = JetstreamEvent::Identity { did: "did:plc:nohandle".to_string(), kind: "update".to_string(), time_us: 0, identity: json!({ "other_field": "value" }), };
handler.handle_event(event).await.unwrap();
// Verify the DID was purged let purge_calls = resolver.get_purge_calls(); assert_eq!(purge_calls.len(), 1); assert_eq!(purge_calls[0], "did:plc:nohandle");
// Verify set was not called let set_calls = resolver.get_set_calls(); assert_eq!(set_calls.len(), 0); }
#[tokio::test] async fn test_identity_with_null_identity() { let resolver = Arc::new(MockResolver::new()); let metrics = Arc::new(NoOpMetricsPublisher::new()); let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
// Create an identity event with null identity let event = JetstreamEvent::Identity { did: "did:plc:nullidentity".to_string(), kind: "delete".to_string(), time_us: 0, identity: json!(null), };
handler.handle_event(event).await.unwrap();
// Verify the DID was purged let purge_calls = resolver.get_purge_calls(); assert_eq!(purge_calls.len(), 1); assert_eq!(purge_calls[0], "did:plc:nullidentity");
// Verify set was not called let set_calls = resolver.get_set_calls(); assert_eq!(set_calls.len(), 0); }
#[tokio::test] async fn test_handler_id() { let resolver = Arc::new(MockResolver::new()); let metrics = Arc::new(NoOpMetricsPublisher::new()); let handler = QuickDidEventHandler::new(resolver, metrics);
assert_eq!(handler.handler_id(), "quickdid_handler"); }}