+115
Cargo.lock
+115
Cargo.lock
···
97
97
]
98
98
99
99
[[package]]
100
+
name = "atproto-jetstream"
101
+
version = "0.11.3"
102
+
source = "registry+https://github.com/rust-lang/crates.io-index"
103
+
checksum = "9e55d1140fcf5c6be0c03e7db773c6a9258986340118015d9bb536bf22689de2"
104
+
dependencies = [
105
+
"anyhow",
106
+
"async-trait",
107
+
"atproto-identity",
108
+
"futures",
109
+
"http",
110
+
"serde",
111
+
"serde_json",
112
+
"thiserror 2.0.16",
113
+
"tokio",
114
+
"tokio-util",
115
+
"tokio-websockets",
116
+
"tracing",
117
+
"tracing-subscriber",
118
+
"urlencoding",
119
+
"zstd",
120
+
]
121
+
122
+
[[package]]
100
123
name = "autocfg"
101
124
version = "1.5.0"
102
125
source = "registry+https://github.com/rust-lang/crates.io-index"
···
285
308
checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54"
286
309
dependencies = [
287
310
"find-msvc-tools",
311
+
"jobserver",
312
+
"libc",
288
313
"shlex",
289
314
]
290
315
···
736
761
]
737
762
738
763
[[package]]
764
+
name = "futures"
765
+
version = "0.3.31"
766
+
source = "registry+https://github.com/rust-lang/crates.io-index"
767
+
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
768
+
dependencies = [
769
+
"futures-channel",
770
+
"futures-core",
771
+
"futures-executor",
772
+
"futures-io",
773
+
"futures-sink",
774
+
"futures-task",
775
+
"futures-util",
776
+
]
777
+
778
+
[[package]]
739
779
name = "futures-channel"
740
780
version = "0.3.31"
741
781
source = "registry+https://github.com/rust-lang/crates.io-index"
···
808
848
source = "registry+https://github.com/rust-lang/crates.io-index"
809
849
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
810
850
dependencies = [
851
+
"futures-channel",
811
852
"futures-core",
812
853
"futures-io",
813
854
"futures-macro",
···
1326
1367
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1327
1368
1328
1369
[[package]]
1370
+
name = "jobserver"
1371
+
version = "0.1.34"
1372
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1373
+
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
1374
+
dependencies = [
1375
+
"getrandom 0.3.3",
1376
+
"libc",
1377
+
]
1378
+
1379
+
[[package]]
1329
1380
name = "js-sys"
1330
1381
version = "0.3.78"
1331
1382
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1874
1925
"anyhow",
1875
1926
"async-trait",
1876
1927
"atproto-identity",
1928
+
"atproto-jetstream",
1877
1929
"axum",
1878
1930
"bincode",
1879
1931
"cadence",
···
2480
2532
"digest",
2481
2533
"rand_core 0.6.4",
2482
2534
]
2535
+
2536
+
[[package]]
2537
+
name = "simdutf8"
2538
+
version = "0.1.5"
2539
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2540
+
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
2483
2541
2484
2542
[[package]]
2485
2543
name = "slab"
···
2902
2960
"io-uring",
2903
2961
"libc",
2904
2962
"mio",
2963
+
"parking_lot",
2905
2964
"pin-project-lite",
2906
2965
"signal-hook-registry",
2907
2966
"slab",
···
2964
3023
"futures-util",
2965
3024
"pin-project-lite",
2966
3025
"tokio",
3026
+
]
3027
+
3028
+
[[package]]
3029
+
name = "tokio-websockets"
3030
+
version = "0.11.4"
3031
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3032
+
checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e"
3033
+
dependencies = [
3034
+
"base64",
3035
+
"bytes",
3036
+
"futures-core",
3037
+
"futures-sink",
3038
+
"http",
3039
+
"httparse",
3040
+
"rand 0.9.2",
3041
+
"ring",
3042
+
"rustls-native-certs",
3043
+
"rustls-pki-types",
3044
+
"simdutf8",
3045
+
"tokio",
3046
+
"tokio-rustls",
3047
+
"tokio-util",
2967
3048
]
2968
3049
2969
3050
[[package]]
···
3158
3239
"percent-encoding",
3159
3240
"serde",
3160
3241
]
3242
+
3243
+
[[package]]
3244
+
name = "urlencoding"
3245
+
version = "2.1.3"
3246
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3247
+
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
3161
3248
3162
3249
[[package]]
3163
3250
name = "utf8_iter"
···
3816
3903
"quote",
3817
3904
"syn",
3818
3905
]
3906
+
3907
+
[[package]]
3908
+
name = "zstd"
3909
+
version = "0.13.3"
3910
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3911
+
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
3912
+
dependencies = [
3913
+
"zstd-safe",
3914
+
]
3915
+
3916
+
[[package]]
3917
+
name = "zstd-safe"
3918
+
version = "7.2.4"
3919
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3920
+
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
3921
+
dependencies = [
3922
+
"zstd-sys",
3923
+
]
3924
+
3925
+
[[package]]
3926
+
name = "zstd-sys"
3927
+
version = "2.0.16+zstd.1.5.7"
3928
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3929
+
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
3930
+
dependencies = [
3931
+
"cc",
3932
+
"pkg-config",
3933
+
]
+1
Cargo.toml
+1
Cargo.toml
+106
-1
src/bin/quickdid.rs
+106
-1
src/bin/quickdid.rs
···
3
3
config::{CertificateBundles, DnsNameservers},
4
4
resolve::HickoryDnsResolver,
5
5
};
6
+
use atproto_jetstream::{Consumer as JetstreamConsumer, ConsumerTaskConfig};
6
7
use quickdid::{
7
8
cache::create_redis_pool,
8
9
config::Config,
···
13
14
},
14
15
handle_resolver_task::{HandleResolverTaskConfig, create_handle_resolver_task_with_config},
15
16
http::{AppContext, create_router},
17
+
jetstream_handler::QuickDidEventHandler,
16
18
metrics::create_metrics_publisher,
17
19
queue::{
18
20
HandleResolutionWork, QueueAdapter, create_mpsc_queue_from_channel, create_noop_queue,
···
151
153
);
152
154
println!(
153
155
" PROACTIVE_REFRESH_THRESHOLD Threshold as percentage of TTL (0.0-1.0, default: 0.8)"
156
+
);
157
+
println!();
158
+
println!(" JETSTREAM:");
159
+
println!(
160
+
" JETSTREAM_ENABLED Enable Jetstream consumer (default: false)"
161
+
);
162
+
println!(
163
+
" JETSTREAM_HOSTNAME Jetstream hostname (default: jetstream.atproto.tools)"
154
164
);
155
165
println!();
156
166
println!(
···
523
533
let app_context = AppContext::new(
524
534
handle_resolver.clone(),
525
535
handle_queue,
526
-
metrics_publisher,
536
+
metrics_publisher.clone(),
527
537
config.etag_seed.clone(),
528
538
config.cache_control_header.clone(),
529
539
config.static_files_dir.clone(),
···
573
583
signal_token.cancel();
574
584
tracing::info!("Signal handler task completed");
575
585
});
586
+
}
587
+
588
+
// Start Jetstream consumer if enabled
589
+
if config.jetstream_enabled {
590
+
let jetstream_resolver = handle_resolver.clone();
591
+
let jetstream_metrics = metrics_publisher.clone();
592
+
let jetstream_hostname = config.jetstream_hostname.clone();
593
+
let jetstream_user_agent = config.user_agent.clone();
594
+
595
+
spawn_cancellable_task(
596
+
&tracker,
597
+
token.clone(),
598
+
"jetstream_consumer",
599
+
move |cancel_token| async move {
600
+
tracing::info!(hostname = %jetstream_hostname, "Starting Jetstream consumer");
601
+
602
+
// Create event handler
603
+
let event_handler = Arc::new(QuickDidEventHandler::new(
604
+
jetstream_resolver,
605
+
jetstream_metrics.clone(),
606
+
));
607
+
608
+
// Reconnection loop
609
+
let mut reconnect_count = 0u32;
610
+
let max_reconnects_per_minute = 5;
611
+
let reconnect_window = std::time::Duration::from_secs(60);
612
+
let mut last_disconnect = std::time::Instant::now() - reconnect_window;
613
+
614
+
while !cancel_token.is_cancelled() {
615
+
let now = std::time::Instant::now();
616
+
if now.duration_since(last_disconnect) < reconnect_window {
617
+
reconnect_count += 1;
618
+
if reconnect_count > max_reconnects_per_minute {
619
+
tracing::warn!(
620
+
count = reconnect_count,
621
+
"Too many Jetstream reconnects, waiting 60 seconds"
622
+
);
623
+
tokio::time::sleep(reconnect_window).await;
624
+
reconnect_count = 0;
625
+
last_disconnect = now;
626
+
continue;
627
+
}
628
+
} else {
629
+
reconnect_count = 0;
630
+
}
631
+
632
+
// Create consumer configuration
633
+
let consumer_config = ConsumerTaskConfig {
634
+
user_agent: jetstream_user_agent.clone(),
635
+
compression: false,
636
+
zstd_dictionary_location: String::new(),
637
+
jetstream_hostname: jetstream_hostname.clone(),
638
+
collections: vec![], // Listen to all collections
639
+
dids: vec![],
640
+
max_message_size_bytes: None,
641
+
cursor: None,
642
+
require_hello: true,
643
+
};
644
+
645
+
let consumer = JetstreamConsumer::new(consumer_config);
646
+
647
+
// Register event handler
648
+
if let Err(e) = consumer.register_handler(event_handler.clone()).await {
649
+
tracing::error!(error = ?e, "Failed to register Jetstream event handler");
650
+
continue;
651
+
}
652
+
653
+
// Run consumer with cancellation support
654
+
match consumer.run_background(cancel_token.clone()).await {
655
+
Ok(()) => {
656
+
tracing::info!("Jetstream consumer stopped normally");
657
+
if cancel_token.is_cancelled() {
658
+
break;
659
+
}
660
+
last_disconnect = std::time::Instant::now();
661
+
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
662
+
}
663
+
Err(e) => {
664
+
tracing::error!(error = ?e, "Jetstream consumer connection failed, will reconnect");
665
+
jetstream_metrics.incr("jetstream.connection.error").await;
666
+
last_disconnect = std::time::Instant::now();
667
+
668
+
if !cancel_token.is_cancelled() {
669
+
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
670
+
}
671
+
}
672
+
}
673
+
}
674
+
675
+
tracing::info!("Jetstream consumer task shutting down");
676
+
Ok(())
677
+
},
678
+
);
679
+
} else {
680
+
tracing::info!("Jetstream consumer disabled");
576
681
}
577
682
578
683
// Start HTTP server with cancellation support
+13
src/config.rs
+13
src/config.rs
···
245
245
/// When set, the root handler will serve files from this directory.
246
246
/// Default: "www" (relative to working directory)
247
247
pub static_files_dir: String,
248
+
249
+
/// Enable Jetstream consumer for AT Protocol events.
250
+
/// When enabled, the service will consume Account and Identity events
251
+
/// to maintain cache consistency.
252
+
/// Default: false
253
+
pub jetstream_enabled: bool,
254
+
255
+
/// Jetstream WebSocket hostname for consuming AT Protocol events.
256
+
/// Example: "jetstream.atproto.tools" or "jetstream1.us-west.bsky.network"
257
+
/// Default: "jetstream.atproto.tools"
258
+
pub jetstream_hostname: String,
248
259
}
249
260
250
261
impl Config {
···
327
338
proactive_refresh_enabled: parse_env("PROACTIVE_REFRESH_ENABLED", false)?,
328
339
proactive_refresh_threshold: parse_env("PROACTIVE_REFRESH_THRESHOLD", 0.8)?,
329
340
static_files_dir: get_env_or_default("STATIC_FILES_DIR", Some("www")).unwrap(),
341
+
jetstream_enabled: parse_env("JETSTREAM_ENABLED", false)?,
342
+
jetstream_hostname: get_env_or_default("JETSTREAM_HOSTNAME", Some("jetstream.atproto.tools")).unwrap(),
330
343
};
331
344
332
345
// Calculate the Cache-Control header value if enabled
+354
src/jetstream_handler.rs
+354
src/jetstream_handler.rs
···
1
+
//! Jetstream event handler for QuickDID
2
+
//!
3
+
//! This module provides the event handler for processing AT Protocol Jetstream events,
4
+
//! specifically handling Account and Identity events to maintain cache consistency.
5
+
6
+
use crate::handle_resolver::HandleResolver;
7
+
use crate::metrics::MetricsPublisher;
8
+
use anyhow::Result;
9
+
use atproto_jetstream::{EventHandler, JetstreamEvent};
10
+
use std::sync::Arc;
11
+
use tracing::{debug, info, warn};
12
+
13
+
/// Jetstream event handler for QuickDID
14
+
///
15
+
/// This handler processes AT Protocol events from the Jetstream firehose to keep
16
+
/// the handle resolver cache in sync with the network state.
17
+
///
18
+
/// # Event Processing
19
+
///
20
+
/// ## Account Events
21
+
/// - When an account is marked as "deleted" or "deactivated", the DID is purged from the cache
22
+
/// - Metrics are tracked for successful and failed purge operations
23
+
///
24
+
/// ## Identity Events
25
+
/// - When an identity event contains a handle, the handle-to-DID mapping is updated
26
+
/// - When an identity event lacks a handle (indicating removal), the DID is purged
27
+
/// - Metrics are tracked for successful and failed update/purge operations
28
+
///
29
+
/// # Example
30
+
///
31
+
/// ```no_run
32
+
/// use quickdid::jetstream_handler::QuickDidEventHandler;
33
+
/// use quickdid::handle_resolver::HandleResolver;
34
+
/// use quickdid::metrics::MetricsPublisher;
35
+
/// use std::sync::Arc;
36
+
///
37
+
/// # async fn example(resolver: Arc<dyn HandleResolver>, metrics: Arc<dyn MetricsPublisher>) {
38
+
/// let handler = QuickDidEventHandler::new(resolver, metrics);
39
+
/// // Register with a JetstreamConsumer
40
+
/// # }
41
+
/// ```
42
+
pub struct QuickDidEventHandler {
43
+
resolver: Arc<dyn HandleResolver>,
44
+
metrics: Arc<dyn MetricsPublisher>,
45
+
}
46
+
47
+
impl QuickDidEventHandler {
48
+
/// Create a new Jetstream event handler
49
+
///
50
+
/// # Arguments
51
+
///
52
+
/// * `resolver` - The handle resolver to use for cache operations
53
+
/// * `metrics` - The metrics publisher for tracking event processing
54
+
pub fn new(resolver: Arc<dyn HandleResolver>, metrics: Arc<dyn MetricsPublisher>) -> Self {
55
+
Self { resolver, metrics }
56
+
}
57
+
}
58
+
59
+
#[async_trait::async_trait]
60
+
impl EventHandler for QuickDidEventHandler {
61
+
fn handler_id(&self) -> String {
62
+
"quickdid_handler".to_string()
63
+
}
64
+
65
+
async fn handle_event(&self, event: JetstreamEvent) -> Result<()> {
66
+
match event {
67
+
JetstreamEvent::Account { did, kind, .. } => {
68
+
// If account kind is "deleted" or "deactivated", purge the DID
69
+
if kind == "deleted" || kind == "deactivated" {
70
+
info!(did = %did, kind = %kind, "Purging account");
71
+
match self.resolver.purge(&did).await {
72
+
Ok(()) => {
73
+
self.metrics.incr("jetstream.account.purged").await;
74
+
}
75
+
Err(e) => {
76
+
warn!(did = %did, error = ?e, "Failed to purge DID");
77
+
self.metrics.incr("jetstream.account.purge_error").await;
78
+
}
79
+
}
80
+
}
81
+
self.metrics.incr("jetstream.account.processed").await;
82
+
}
83
+
JetstreamEvent::Identity { did, identity, .. } => {
84
+
// Extract handle from identity JSON if available
85
+
if !identity.is_null() {
86
+
if let Some(handle_value) = identity.get("handle") {
87
+
if let Some(handle) = handle_value.as_str() {
88
+
info!(handle = %handle, did = %did, "Updating identity mapping");
89
+
match self.resolver.set(handle, &did).await {
90
+
Ok(()) => {
91
+
self.metrics.incr("jetstream.identity.updated").await;
92
+
}
93
+
Err(e) => {
94
+
warn!(handle = %handle, did = %did, error = ?e, "Failed to update mapping");
95
+
self.metrics.incr("jetstream.identity.update_error").await;
96
+
}
97
+
}
98
+
} else {
99
+
// No handle or invalid handle, purge the DID
100
+
info!(did = %did, "Purging identity without valid handle");
101
+
match self.resolver.purge(&did).await {
102
+
Ok(()) => {
103
+
self.metrics.incr("jetstream.identity.purged").await;
104
+
}
105
+
Err(e) => {
106
+
warn!(did = %did, error = ?e, "Failed to purge DID");
107
+
self.metrics.incr("jetstream.identity.purge_error").await;
108
+
}
109
+
}
110
+
}
111
+
} else {
112
+
// No handle field, purge the DID
113
+
info!(did = %did, "Purging identity without handle field");
114
+
match self.resolver.purge(&did).await {
115
+
Ok(()) => {
116
+
self.metrics.incr("jetstream.identity.purged").await;
117
+
}
118
+
Err(e) => {
119
+
warn!(did = %did, error = ?e, "Failed to purge DID");
120
+
self.metrics.incr("jetstream.identity.purge_error").await;
121
+
}
122
+
}
123
+
}
124
+
} else {
125
+
// Null identity means removed, purge the DID
126
+
info!(did = %did, "Purging identity with null info");
127
+
match self.resolver.purge(&did).await {
128
+
Ok(()) => {
129
+
self.metrics.incr("jetstream.identity.purged").await;
130
+
}
131
+
Err(e) => {
132
+
warn!(did = %did, error = ?e, "Failed to purge DID");
133
+
self.metrics.incr("jetstream.identity.purge_error").await;
134
+
}
135
+
}
136
+
}
137
+
self.metrics.incr("jetstream.identity.processed").await;
138
+
}
139
+
_ => {
140
+
// Other event types we don't care about
141
+
debug!("Ignoring unhandled Jetstream event type");
142
+
}
143
+
}
144
+
Ok(())
145
+
}
146
+
}
147
+
148
+
#[cfg(test)]
149
+
mod tests {
150
+
use super::*;
151
+
use crate::handle_resolver::HandleResolverError;
152
+
use crate::metrics::NoOpMetricsPublisher;
153
+
use async_trait::async_trait;
154
+
use serde_json::json;
155
+
156
+
/// Mock resolver for testing
157
+
struct MockResolver {
158
+
purge_called: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
159
+
set_called: std::sync::Arc<std::sync::Mutex<Vec<(String, String)>>>,
160
+
}
161
+
162
+
impl MockResolver {
163
+
fn new() -> Self {
164
+
Self {
165
+
purge_called: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
166
+
set_called: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
167
+
}
168
+
}
169
+
170
+
fn get_purge_calls(&self) -> Vec<String> {
171
+
self.purge_called.lock().unwrap().clone()
172
+
}
173
+
174
+
fn get_set_calls(&self) -> Vec<(String, String)> {
175
+
self.set_called.lock().unwrap().clone()
176
+
}
177
+
}
178
+
179
+
#[async_trait]
180
+
impl HandleResolver for MockResolver {
181
+
async fn resolve(&self, _handle: &str) -> Result<(String, u64), HandleResolverError> {
182
+
unimplemented!("Not needed for tests")
183
+
}
184
+
185
+
async fn purge(&self, subject: &str) -> Result<(), HandleResolverError> {
186
+
self.purge_called.lock().unwrap().push(subject.to_string());
187
+
Ok(())
188
+
}
189
+
190
+
async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> {
191
+
self.set_called
192
+
.lock()
193
+
.unwrap()
194
+
.push((handle.to_string(), did.to_string()));
195
+
Ok(())
196
+
}
197
+
}
198
+
199
+
#[tokio::test]
200
+
async fn test_account_deleted_event() {
201
+
let resolver = Arc::new(MockResolver::new());
202
+
let metrics = Arc::new(NoOpMetricsPublisher::new());
203
+
let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
204
+
205
+
// Create a deleted account event
206
+
let event = JetstreamEvent::Account {
207
+
did: "did:plc:test123".to_string(),
208
+
kind: "deleted".to_string(),
209
+
time_us: 0,
210
+
identity: json!(null),
211
+
};
212
+
213
+
handler.handle_event(event).await.unwrap();
214
+
215
+
// Verify the DID was purged
216
+
let purge_calls = resolver.get_purge_calls();
217
+
assert_eq!(purge_calls.len(), 1);
218
+
assert_eq!(purge_calls[0], "did:plc:test123");
219
+
}
220
+
221
+
#[tokio::test]
222
+
async fn test_account_deactivated_event() {
223
+
let resolver = Arc::new(MockResolver::new());
224
+
let metrics = Arc::new(NoOpMetricsPublisher::new());
225
+
let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
226
+
227
+
// Create a deactivated account event
228
+
let event = JetstreamEvent::Account {
229
+
did: "did:plc:test456".to_string(),
230
+
kind: "deactivated".to_string(),
231
+
time_us: 0,
232
+
identity: json!(null),
233
+
};
234
+
235
+
handler.handle_event(event).await.unwrap();
236
+
237
+
// Verify the DID was purged
238
+
let purge_calls = resolver.get_purge_calls();
239
+
assert_eq!(purge_calls.len(), 1);
240
+
assert_eq!(purge_calls[0], "did:plc:test456");
241
+
}
242
+
243
+
#[tokio::test]
244
+
async fn test_account_active_event() {
245
+
let resolver = Arc::new(MockResolver::new());
246
+
let metrics = Arc::new(NoOpMetricsPublisher::new());
247
+
let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
248
+
249
+
// Create an active account event (should not purge)
250
+
let event = JetstreamEvent::Account {
251
+
did: "did:plc:test789".to_string(),
252
+
kind: "active".to_string(),
253
+
time_us: 0,
254
+
identity: json!(null),
255
+
};
256
+
257
+
handler.handle_event(event).await.unwrap();
258
+
259
+
// Verify the DID was NOT purged
260
+
let purge_calls = resolver.get_purge_calls();
261
+
assert_eq!(purge_calls.len(), 0);
262
+
}
263
+
264
+
#[tokio::test]
265
+
async fn test_identity_with_handle_event() {
266
+
let resolver = Arc::new(MockResolver::new());
267
+
let metrics = Arc::new(NoOpMetricsPublisher::new());
268
+
let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
269
+
270
+
// Create an identity event with a handle
271
+
let event = JetstreamEvent::Identity {
272
+
did: "did:plc:testuser".to_string(),
273
+
kind: "update".to_string(),
274
+
time_us: 0,
275
+
identity: json!({
276
+
"handle": "alice.bsky.social"
277
+
}),
278
+
};
279
+
280
+
handler.handle_event(event).await.unwrap();
281
+
282
+
// Verify the set method was called
283
+
let set_calls = resolver.get_set_calls();
284
+
assert_eq!(set_calls.len(), 1);
285
+
assert_eq!(set_calls[0], ("alice.bsky.social".to_string(), "did:plc:testuser".to_string()));
286
+
287
+
// Verify no purge was called
288
+
let purge_calls = resolver.get_purge_calls();
289
+
assert_eq!(purge_calls.len(), 0);
290
+
}
291
+
292
+
#[tokio::test]
293
+
async fn test_identity_without_handle_event() {
294
+
let resolver = Arc::new(MockResolver::new());
295
+
let metrics = Arc::new(NoOpMetricsPublisher::new());
296
+
let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
297
+
298
+
// Create an identity event without a handle field
299
+
let event = JetstreamEvent::Identity {
300
+
did: "did:plc:nohandle".to_string(),
301
+
kind: "update".to_string(),
302
+
time_us: 0,
303
+
identity: json!({
304
+
"other_field": "value"
305
+
}),
306
+
};
307
+
308
+
handler.handle_event(event).await.unwrap();
309
+
310
+
// Verify the DID was purged
311
+
let purge_calls = resolver.get_purge_calls();
312
+
assert_eq!(purge_calls.len(), 1);
313
+
assert_eq!(purge_calls[0], "did:plc:nohandle");
314
+
315
+
// Verify set was not called
316
+
let set_calls = resolver.get_set_calls();
317
+
assert_eq!(set_calls.len(), 0);
318
+
}
319
+
320
+
#[tokio::test]
321
+
async fn test_identity_with_null_identity() {
322
+
let resolver = Arc::new(MockResolver::new());
323
+
let metrics = Arc::new(NoOpMetricsPublisher::new());
324
+
let handler = QuickDidEventHandler::new(resolver.clone(), metrics);
325
+
326
+
// Create an identity event with null identity
327
+
let event = JetstreamEvent::Identity {
328
+
did: "did:plc:nullidentity".to_string(),
329
+
kind: "delete".to_string(),
330
+
time_us: 0,
331
+
identity: json!(null),
332
+
};
333
+
334
+
handler.handle_event(event).await.unwrap();
335
+
336
+
// Verify the DID was purged
337
+
let purge_calls = resolver.get_purge_calls();
338
+
assert_eq!(purge_calls.len(), 1);
339
+
assert_eq!(purge_calls[0], "did:plc:nullidentity");
340
+
341
+
// Verify set was not called
342
+
let set_calls = resolver.get_set_calls();
343
+
assert_eq!(set_calls.len(), 0);
344
+
}
345
+
346
+
#[tokio::test]
347
+
async fn test_handler_id() {
348
+
let resolver = Arc::new(MockResolver::new());
349
+
let metrics = Arc::new(NoOpMetricsPublisher::new());
350
+
let handler = QuickDidEventHandler::new(resolver, metrics);
351
+
352
+
assert_eq!(handler.handler_id(), "quickdid_handler");
353
+
}
354
+
}
+1
src/lib.rs
+1
src/lib.rs
···
2
2
pub mod config; // Config and Args needed by binary
3
3
pub mod handle_resolver; // Only traits and factory functions exposed
4
4
pub mod http; // Only create_router exposed
5
+
pub mod jetstream_handler; // Jetstream event handler for AT Protocol events
5
6
6
7
// Semi-public modules - needed by binary but with limited exposure
7
8
pub mod cache; // Only create_redis_pool exposed