1use super::{
2 DeviceSyncClient, DeviceSyncError, ReceivedContent, decode_sync_message,
3 preference_sync::apply_preference_updates, selected_sync_group_in, with_plaintext,
4};
5pub use crate::core::metrics::SyncMetric;
6use crate::{
7 context::XmtpSharedContext,
8 identity::IdentityExt,
9 identity_updates::{
10 IdentityUpdateError, load_identity_updates_for_client, store_association_state,
11 },
12 subscriptions::{
13 incoming::{IncomingCoordinator, IncomingScope},
14 internal::{InternalEvent, emit_consent_changes, emit_root_key_changed},
15 },
16 worker::{
17 BoxedWorker, NeedsDbReconnect, NewWorker, Worker, WorkerFactory, WorkerKind, WorkerResult,
18 metrics::WorkerMetrics,
19 },
20};
21use futures::TryFutureExt;
22use std::{collections::HashSet, sync::Arc, time::Duration};
23use tokio::sync::OnceCell;
24use tracing::instrument;
25use xmtp_common::time::Instant;
26use xmtp_common::{Event, RetryableError};
27use xmtp_db::group_message::{DeliveryStatus, StoredGroupMessage};
28use xmtp_db::prelude::*;
29use xmtp_db::processed_device_sync_messages::SyncMessageKey;
30use xmtp_db::{StorageError, TransactionOutcome::Continue, sync_outbox::has_owed};
31use xmtp_events::{EventFilter, Subscription};
32use xmtp_id::associations::{AssociationState, IdentityAction, verify_updates};
33use xmtp_macro::log_event;
34use xmtp_proto::ShortHex;
35use xmtp_proto::xmtp::device_sync::content::{
36 DeviceSyncAcknowledge, PreferenceUpdates as PreferenceUpdatesProto,
37 device_sync_content::Content as ContentProto,
38};
39
40pub(super) fn log_incoming_preference_updates(
41 updates: &[xmtp_proto::xmtp::device_sync::content::PreferenceUpdate],
42) {
43 tracing::info!(update_count = updates.len(), "Incoming preference updates");
44}
45
46const RETRY_FIRST: Duration = Duration::from_secs(1);
47const RETRY_MAX: Duration = Duration::from_secs(10 * 60);
48
49fn backoff(n: u32) -> Duration {
51 RETRY_FIRST.saturating_mul(1 << n.min(10)).min(RETRY_MAX)
52}
53
54#[derive(Default)]
56struct Pass {
57 due: Option<Instant>,
58 failures: u32,
59}
60
61#[derive(Default)]
64struct Passes {
65 inbox: Pass,
66 outbox: Pass,
67}
68
69type SharedPasses = Arc<parking_lot::Mutex<Passes>>;
70
71impl Pass {
72 fn restart(&mut self) {
74 if self.failures == 0 {
75 self.due = Some(Instant::now());
76 }
77 }
78
79 fn wake(&mut self) {
81 if self.failures == 0 {
82 self.due = Some(Instant::now());
83 }
84 }
85
86 fn is_due(&self) -> bool {
87 self.due.is_some_and(|at| at <= Instant::now())
88 }
89
90 fn finish(
92 &mut self,
93 pass: &'static str,
94 result: Result<(), DeviceSyncError>,
95 ) -> Result<(), DeviceSyncError> {
96 match result {
97 Ok(()) => *self = Self::default(),
98 Err(e) if e.needs_db_reconnect() => return Err(e),
99 Err(e) => {
100 self.failures = self.failures.saturating_add(1);
101 let delay = RetryableError::rate_limit_delay(&e).unwrap_or_default();
105 let wait = backoff(self.failures).max(delay);
106 self.due = Some(xmtp_common::time::saturating_after(Instant::now(), wait));
107 tracing::warn!(error = %e, pass, failures = self.failures, "device sync pass failed; it stays owed");
108 }
109 }
110 Ok(())
111 }
112}
113
114pub struct SyncWorker<Context> {
115 client: DeviceSyncClient<Context>,
116 subscription: Option<Arc<Subscription<InternalEvent>>>,
117 passes: SharedPasses,
118 init: OnceCell<()>,
119 metrics: Arc<WorkerMetrics<SyncMetric>>,
120}
121
122impl<Context> SyncWorker<Context>
123where
124 Context: XmtpSharedContext + 'static,
125{
126 fn new(
127 context: Context,
128 metrics: Option<Arc<WorkerMetrics<SyncMetric>>>,
129 passes: SharedPasses,
130 ) -> Self {
131 let metrics =
132 metrics.unwrap_or_else(|| Arc::new(WorkerMetrics::new(context.installation_id())));
133 let client = DeviceSyncClient::new(context, metrics.clone());
134
135 Self {
136 client,
137 subscription: None,
138 passes,
139 init: OnceCell::new(),
140 metrics,
141 }
142 }
143}
144
145struct Factory<Context> {
146 context: Context,
147 passes: SharedPasses,
148 metrics: Arc<WorkerMetrics<SyncMetric>>,
149}
150
151impl<Context> WorkerFactory for Factory<Context>
152where
153 Context: XmtpSharedContext + 'static,
154{
155 fn create(&self) -> BoxedWorker {
156 Box::new(SyncWorker::new(
157 self.context.clone(),
158 Some(self.metrics.clone()),
159 self.passes.clone(),
160 ))
161 }
162
163 fn kind(&self) -> WorkerKind {
164 WorkerKind::DeviceSync
165 }
166
167 fn event_interest(&self) -> Option<(EventFilter<InternalEvent>, Option<usize>)> {
168 let filter = EventFilter::default().with_internal(|event| {
169 matches!(
170 event,
171 InternalEvent::MessageStored { is_sync: true, .. } | InternalEvent::OutboxChanged
172 )
173 });
174 Some((filter, None))
175 }
176}
177
178impl<Context> NewWorker<Context> for SyncWorker<Context>
179where
180 Context: XmtpSharedContext + 'static,
181{
182 fn factory(context: Context) -> impl WorkerFactory + 'static {
183 Factory {
184 metrics: Arc::new(WorkerMetrics::new(context.installation_id())),
185 context,
186 passes: SharedPasses::default(),
187 }
188 }
189}
190
191#[xmtp_common::async_trait]
192impl<Context> Worker for SyncWorker<Context>
193where
194 Context: XmtpSharedContext + 'static,
195{
196 fn kind(&self) -> WorkerKind {
197 WorkerKind::DeviceSync
198 }
199
200 fn set_subscription(&mut self, subscription: Arc<Subscription<InternalEvent>>) {
201 self.subscription = Some(subscription);
202 }
203
204 fn sync_metrics(&self) -> Option<Arc<WorkerMetrics<SyncMetric>>> {
205 Some(self.metrics.clone())
206 }
207
208 async fn run_tasks(&mut self) -> WorkerResult<()> {
209 self.run().map_err(|e| Box::new(e) as Box<_>).await
210 }
211}
212
213impl<Context> SyncWorker<Context>
214where
215 Context: XmtpSharedContext + 'static,
216{
217 async fn run(&mut self) -> Result<(), DeviceSyncError> {
218 Box::pin(self.sync_init()).await?;
220 let _receipt = IncomingCoordinator::for_context(&self.client.context)
222 .acquire(IncomingScope::DeviceSyncGroups);
223 self.metrics.increment_metric(SyncMetric::Init);
224
225 Box::pin(self.run_internal()).await
227 }
228
229 async fn run_internal(&mut self) -> Result<(), DeviceSyncError> {
232 use futures::StreamExt;
233 let (base, jitter) = self
234 .client
235 .context
236 .worker_interval(WorkerKind::DeviceSync, Duration::from_secs(20));
237 let mut intervals = xmtp_common::time::jittered_interval_stream(base, jitter);
238 let mut skip_initial_tick = cfg!(not(target_arch = "wasm32"));
240 let subscription = self
241 .subscription
242 .as_ref()
243 .expect("runner installs subscription")
244 .clone();
245 {
246 let mut passes = self.passes.lock();
247 passes.inbox.restart();
248 passes.outbox.restart();
249 }
250 loop {
251 if self.passes.lock().inbox.is_due() {
252 let result = Box::pin(self.client.process_sync_group_messages()).await;
253 self.passes.lock().inbox.finish("receive", result)?;
254 }
255 if self.passes.lock().outbox.is_due() {
256 let result = Box::pin(self.client.publish_outbox()).await;
257 self.passes.lock().outbox.finish("publish", result)?;
258 }
259 let wait = {
260 let passes = self.passes.lock();
261 [passes.inbox.due, passes.outbox.due]
262 .into_iter()
263 .flatten()
264 .min()
265 .map(|at| at.saturating_duration_since(Instant::now()))
266 };
267 tokio::select! {
268 event = subscription.next() => {
269 let Some(event) = event else { break; };
270 match event.internal {
271 Some(InternalEvent::MessageStored { is_sync: true, .. }) => {
272 self.passes.lock().inbox.wake();
273 }
274 Some(InternalEvent::OutboxChanged) => self.passes.lock().outbox.wake(),
275 _ => {}
276 }
277 }
278 _ = xmtp_common::time::sleep(wait.unwrap_or_default()), if wait.is_some() => {}
279 _ = intervals.next() => {
280 if skip_initial_tick {
281 skip_initial_tick = false;
282 continue;
283 }
284 self.passes.lock().inbox.wake();
285 if has_owed(&self.client.db())? {
286 self.passes.lock().outbox.wake();
287 }
288 },
289 }
290 }
291 Ok(())
292 }
293
294 #[instrument(level = "trace", skip_all)]
296 async fn sync_init(&mut self) -> Result<(), DeviceSyncError> {
297 let Self { init, client, .. } = &self;
298
299 init.get_or_try_init(|| async {
300 log_event!(
301 Event::DeviceSyncInitializing,
302 self.client.context.installation_id()
303 );
304
305 if client.primary_sync_group()?.is_none() {
307 log_event!(
308 Event::DeviceSyncNoPrimarySyncGroup,
309 self.client.context.installation_id()
310 );
311 let sync_group = Box::pin(client.get_sync_group()).await?;
313 log_event!(
314 Event::DeviceSyncCreatedPrimarySyncGroup,
315 self.client.context.installation_id(),
316 group_id = sync_group.group_id
317 );
318 }
319
320 log_event!(
321 Event::DeviceSyncInitializingFinished,
322 self.client.context.installation_id()
323 );
324
325 Ok(())
326 })
327 .await
328 .copied()
329 }
330}
331
332struct OwnTimeline {
335 steps: Vec<(i64, HashSet<Vec<u8>>)>,
336}
337
338enum Sender {
339 Member,
340 Gone,
341 NotYetKnown,
342}
343
344impl OwnTimeline {
345 async fn load(context: &impl XmtpSharedContext) -> Result<Self, DeviceSyncError> {
349 let db = context.db();
350 let inbox_id = context.inbox_id();
351 load_identity_updates_for_client(context, &db, &[inbox_id]).await?;
352 let updates = db.get_identity_updates(inbox_id, None, None)?;
353 let mut states = vec![];
355 for update in &updates {
356 let Some(cached) = db.read_from_cache(inbox_id, update.sequence_id)? else {
357 break;
358 };
359 states.push(AssociationState::try_from(cached).map_err(StorageError::from)?);
360 }
361 let rest = &updates[states.len()..];
364 if !rest.is_empty() {
365 let unverified = rest
366 .iter()
367 .cloned()
368 .map(IdentityExt::to_unverified)
369 .collect::<Result<Vec<_>, _>>()
370 .map_err(IdentityUpdateError::from)?;
371 let verifier = context.scw_verifier();
372 let verified = verify_updates(unverified, &verifier)
373 .await
374 .map_err(IdentityUpdateError::from)?;
375 for (stored, update) in rest.iter().zip(verified) {
376 let state = update
377 .update_state(states.last().cloned(), update.client_timestamp_ns)
378 .map_err(IdentityUpdateError::from)?;
379 store_association_state(context, stored.sequence_id, &state)?;
380 states.push(state);
381 }
382 }
383 let steps = updates
384 .iter()
385 .zip(states)
386 .map(|(update, state)| {
387 let installations = state.installation_ids().into_iter().collect();
388 (update.server_timestamp_ns, installations)
389 })
390 .collect();
391 Ok(Self { steps })
392 }
393
394 fn sender_status(&self, own_inbox: &str, msg: &StoredGroupMessage) -> Sender {
396 if msg.sender_inbox_id != own_inbox {
397 return Sender::Gone;
399 }
400 let id = &msg.sender_installation_id;
401 let n = self
404 .steps
405 .iter()
406 .rposition(|(at, _)| *at <= msg.sent_at_ns)
407 .map_or(0, |last| last + 1);
408 if n > 0 && self.steps[n - 1].1.contains(id) {
409 Sender::Member
410 } else if self.steps[..n].iter().any(|(_, set)| set.contains(id)) {
411 Sender::Gone
412 } else {
413 Sender::NotYetKnown
414 }
415 }
416}
417
418impl<Context> DeviceSyncClient<Context>
419where
420 Context: XmtpSharedContext,
421{
422 pub(super) async fn process_sync_group_messages(&self) -> Result<(), DeviceSyncError> {
426 let mut after = None;
427 loop {
428 let page = self
429 .context
430 .db()
431 .unprocessed_sync_group_messages(after.as_ref())?;
432 let Some(last) = page.last() else {
433 return Ok(());
434 };
435 after = Some(SyncMessageKey::of(last));
436 tracing::info!("Processing {} messages.", page.len());
437 let page = with_plaintext(page);
440 let own = OwnTimeline::load(&self.context).await?;
445 for (msg, plaintext) in page {
446 match msg.delivery_status {
449 DeliveryStatus::Unpublished => continue,
450 DeliveryStatus::Failed => {
451 self.context
452 .db()
453 .mark_device_sync_msg_as_processed(&msg.id)?;
454 continue;
455 }
456 DeliveryStatus::Published => {}
457 }
458 match own.sender_status(self.context.inbox_id(), &msg) {
459 Sender::Member => {}
460 Sender::Gone => {
461 tracing::warn!(
462 group_id = msg.group_id.short_hex(),
463 "dropping a sync message whose sender was not an own installation when it sent"
464 );
465 self.context
466 .db()
467 .mark_device_sync_msg_as_processed(&msg.id)?;
468 continue;
469 }
470 Sender::NotYetKnown => continue,
472 }
473 if let Err(err) = self.apply_message(&msg, &plaintext) {
474 if err.needs_db_reconnect() {
477 return Err(err);
478 }
479 log_event!(
480 Event::DeviceSyncMessageProcessingError,
481 self.context.installation_id(),
482 error = %err,
483 message_id = #msg.id
484 );
485 }
486 }
487 }
488 }
489
490 pub(super) fn apply_message(
494 &self,
495 msg: &StoredGroupMessage,
496 plaintext: &[u8],
497 ) -> Result<(), DeviceSyncError> {
498 let content = decode_sync_message(plaintext);
499 let own = msg.sender_installation_id == self.installation_id().as_slice();
500 let updates = match &content {
501 Some(ReceivedContent(ContentProto::PreferenceUpdates(PreferenceUpdatesProto {
502 updates,
503 }))) => updates.as_slice(),
504 Some(ReceivedContent(ContentProto::Acknowledge(DeviceSyncAcknowledge { .. })))
505 | None => &[],
506 };
507 let msg_type = match &content {
508 Some(ReceivedContent(ContentProto::PreferenceUpdates(_))) => "PreferenceUpdates",
509 Some(ReceivedContent(ContentProto::Acknowledge(_))) => "Acknowledge",
510 None => "Unsupported",
511 };
512 log_event!(
513 Event::DeviceSyncProcessingMessages,
514 self.context.installation_id(),
515 msg_type,
516 external = !own,
517 message_id = #msg.id,
518 group_id = msg.group_id
519 );
520 if !own {
521 log_incoming_preference_updates(updates);
522 }
523 tracing::info!(update_count = updates.len(), "storing preference updates");
525 crate::state_tx::state_write_with_events(
526 self.context.mls_storage(),
527 self.context.events(),
528 |tx, events| {
529 let selected = selected_sync_group_in(tx, self.context.inbox_id())?;
532 let storage = tx.storage();
533 let db = storage.db();
534 let applied =
535 apply_preference_updates(&db, self.context.inbox_id(), msg, updates, selected)?;
536 emit_consent_changes(&self.context, events, &db, &applied.consent)?;
537 if let Some(marked) = applied.root_key {
538 emit_root_key_changed(events, &db, marked)?;
539 }
540 db.mark_device_sync_msg_as_processed(&msg.id)?;
541 Ok::<_, crate::groups::GroupError>(Continue(()))
542 },
543 )?;
544 for update in updates.iter().filter_map(|update| update.update.as_ref()) {
545 if !own {
546 self.metrics.increment_metric(match update {
547 super::PreferenceUpdateProto::Consent(_) => SyncMetric::ConsentReceived,
548 super::PreferenceUpdateProto::Hmac(_) => SyncMetric::HmacReceived,
549 });
550 }
551 }
552 Ok(())
553 }
554}
555
556#[cfg(test)]
557mod startup_tests {
558 use super::*;
559 use crate::prelude::*;
560 use crate::tester;
561
562 fn idle_worker<C: XmtpSharedContext + 'static>(context: C) -> SyncWorker<C> {
563 SyncWorker::new(context, None, SharedPasses::default())
564 }
565
566 #[xmtp_common::test(unwrap_try = true)]
570 fn pass_backs_off_and_stays_owed() {
571 let mut pass = Pass::default();
572 pass.restart();
573 assert!(pass.is_due());
574 pass.finish("test", Err(DeviceSyncError::OutboxPageUnpublished))?;
575 assert_eq!(pass.failures, 1);
576 assert!(pass.due.is_some(), "a failed pass stays owed");
577 assert!(!pass.is_due());
578 pass.wake();
579 assert!(!pass.is_due(), "a wake does not cut a backoff short");
580 pass.finish("test", Err(DeviceSyncError::OutboxPageUnpublished))?;
581 assert_eq!(pass.failures, 2);
582 assert!(backoff(2) > backoff(1));
583 pass.finish("test", Ok(()))?;
584 assert_eq!((pass.due, pass.failures), (None, 0));
585 pass.wake();
586 assert!(pass.is_due());
587 }
588
589 #[xmtp_common::test(unwrap_try = true)]
595 async fn a_rate_limited_pass_waits_out_its_delay() {
596 tester!(alix, disable_workers);
597 let delay = Duration::from_secs(300);
598 let refused = |method| {
599 xmtp_api::ApiError::Api(xmtp_proto::api::NetworkError::new(
600 xmtp_api_grpc::error::GrpcError::Status(xmtp_api_grpc::error::rate_limited_status(
601 method, delay,
602 )),
603 ))
604 };
605 let publish =
606 DeviceSyncError::Group(crate::groups::GroupError::WrappedApi(refused("publish")));
607 let identity = DeviceSyncError::IdentityUpdate(
608 crate::identity_updates::IdentityUpdateError::Api(refused("get_identity_updates")),
609 );
610 for error in [&publish, &identity] {
611 assert_eq!(RetryableError::rate_limit_delay(error), Some(delay));
612 }
613 let factory = Factory {
614 context: alix.context.clone(),
615 passes: SharedPasses::default(),
616 metrics: Arc::new(WorkerMetrics::new(alix.context.installation_id())),
617 };
618 let before = Instant::now();
619 {
620 let mut passes = factory.passes.lock();
621 passes.outbox.finish("publish", Err(publish))?;
622 passes.inbox.finish("receive", Err(identity))?;
623 }
624 let _rebuilt = factory.create();
625 assert_eq!(
626 Arc::strong_count(&factory.passes),
627 2,
628 "a rebuilt worker shares the passes"
629 );
630 let mut guard = factory.passes.lock();
631 let passes = &mut *guard;
632 for pass in [&mut passes.outbox, &mut passes.inbox] {
633 pass.wake();
634 pass.restart();
635 assert!(!pass.is_due());
636 assert!(pass.due.unwrap() >= before + delay);
637 }
638 }
639
640 #[xmtp_common::test]
644 fn sender_is_judged_at_the_envelope_time() {
645 let set = |ids: &[u8]| ids.iter().map(|id| vec![*id]).collect::<HashSet<_>>();
646 let timeline = OwnTimeline {
647 steps: vec![(10, set(&[1])), (20, set(&[1, 2])), (30, set(&[2]))],
648 };
649 let message = |inbox: &str, installation: u8, sent_at_ns| StoredGroupMessage {
650 sender_inbox_id: inbox.into(),
651 sender_installation_id: vec![installation],
652 sent_at_ns,
653 ..crate::test::mock::generate_stored_msg(
654 xmtp_proto::types::Cursor(1),
655 xmtp_proto::types::GroupId::from([1; 16]),
656 )
657 };
658 let stepped_back = OwnTimeline {
661 steps: vec![(40, set(&[3])), (60, set(&[4])), (50, set(&[4]))],
662 };
663 for (at, expected) in [(45, "member"), (55, "gone")] {
664 let status = match stepped_back.sender_status("me", &message("me", 3, at)) {
665 Sender::Member => "member",
666 Sender::Gone => "gone",
667 Sender::NotYetKnown => "not yet known",
668 };
669 assert_eq!(status, expected, "3 at {at} after the step back");
670 }
671 for (inbox, installation, at, expected) in [
672 ("me", 1, 15, "member"),
673 ("me", 1, 29, "member"),
675 ("me", 1, 30, "gone"),
676 ("me", 2, 15, "not yet known"),
677 ("me", 2, 20, "member"),
678 ("me", 3, 40, "not yet known"),
679 ("other", 1, 15, "gone"),
680 ] {
681 let status = match timeline.sender_status("me", &message(inbox, installation, at)) {
682 Sender::Member => "member",
683 Sender::Gone => "gone",
684 Sender::NotYetKnown => "not yet known",
685 };
686 assert_eq!(status, expected, "{inbox} {installation} at {at}");
687 }
688 }
689
690 #[xmtp_common::test(unwrap_try = true)]
694 async fn timeline_states_match_each_prefix() {
695 use diesel::prelude::*;
696 use xmtp_db::{ConnectionExt, schema::association_state::dsl};
697 tester!(alix1, disable_workers);
698 tester!(alix2, from: alix1, disable_workers);
699 tester!(alix3, from: alix1, disable_workers);
700 let db = alix1.context.db();
701 let inbox_id = alix1.inbox_id();
702 load_identity_updates_for_client(&alix1.context, &db, &[inbox_id]).await?;
703 let updates = db.get_identity_updates(inbox_id, None, None)?;
704 assert!(updates.len() >= 3);
705 let mut expected = vec![];
706 for update in &updates {
707 let state = crate::identity_updates::IdentityUpdates::new(&alix1.context)
708 .get_association_state(&db, inbox_id, Some(update.sequence_id))
709 .await?;
710 let installations: HashSet<_> = state.installation_ids().into_iter().collect();
711 expected.push((update.server_timestamp_ns, installations));
712 }
713 db.raw_query(|conn| {
715 diesel::delete(
716 dsl::association_state.filter(dsl::sequence_id.gt(updates[0].sequence_id)),
717 )
718 .execute(conn)
719 })?;
720 let timeline = OwnTimeline::load(&alix1.context).await?;
721 assert_eq!(timeline.steps, expected);
722 for update in &updates {
723 assert!(db.read_from_cache(inbox_id, update.sequence_id)?.is_some());
724 }
725 drop((alix2, alix3));
726 }
727
728 #[xmtp_common::test(unwrap_try = true)]
730 async fn closing_subscription_stops_worker() {
731 tester!(alix, disable_workers);
732 let mut worker = idle_worker(alix.context.clone());
733 let (filter, depth) = SyncWorker::factory(alix.context.clone())
734 .event_interest()
735 .unwrap();
736 let subscription = Arc::new(alix.context.events().subscribe(filter, depth));
737 worker.set_subscription(subscription.clone());
738 subscription.close();
739 xmtp_common::time::timeout(Duration::from_secs(60), worker.run_internal()).await??;
740 alix.close().await?;
741 }
742
743 #[cfg(not(target_arch = "wasm32"))]
745 #[xmtp_common::test(unwrap_try = true)]
746 async fn publish_span_omits_hmac_key() {
747 tester!(alix, disable_workers);
748 let db = alix.context.db();
749 let (key, _) = xmtp_db::user_preferences::ensure_root_key(&db)?;
750 let key_debug = format!("{:?}", key.as_slice());
751 let client = alix.device_sync_client();
752 let (result, spans) =
753 xmtp_logging::test_logging::with_trace_layer_async(true, client.publish_outbox()).await;
754 result?;
755 let span = spans
756 .iter()
757 .find(|span| span.name == "publish_outbox")
758 .expect("the outbox exports its span");
759 let fields = format!("{:?}", span.attributes);
760 assert!(fields.contains("publish_outbox"));
761 assert!(
762 !fields.contains(&key_debug),
763 "the publish span must not export the HMAC root key"
764 );
765 assert!(!has_owed(&db)?, "the root key was published");
766 alix.close().await?;
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::SyncWorker;
773 use crate::core::events::InternalEvent;
774 use crate::test::mock::{NewMockContext, context};
775 use crate::worker::{NewWorker, WorkerFactory, WorkerKind};
776 use xmtp_events::{EventBus, EventWriter};
777
778 fn worker_event_filter(
780 kind: WorkerKind,
781 ) -> Option<(xmtp_events::EventFilter<InternalEvent>, Option<usize>)> {
782 let factory = SyncWorker::<NewMockContext>::factory(context());
783 assert_eq!(factory.kind(), kind);
784 factory.event_interest()
785 }
786
787 #[xmtp_common::test(unwrap_try = true)]
791 async fn device_sync_wakes_for_sync_messages_and_the_outbox() {
792 let (filter, depth) = worker_event_filter(WorkerKind::DeviceSync).unwrap();
793 assert_eq!(depth, None);
794 let bus = EventBus::new();
795 let subscription = bus.subscribe(filter, depth);
796 let group_id = xmtp_proto::types::GroupId::from([9; 16]);
797 let stored = |is_sync| InternalEvent::MessageStored {
798 group_id,
799 message_id: vec![],
800 expires_at_ns: None,
801 is_sync,
802 };
803 for event in [
804 InternalEvent::PreferencesChanged { updates: vec![] },
805 InternalEvent::GroupJoined {
806 group_id,
807 is_sync: true,
808 },
809 stored(false),
810 ] {
811 bus.emit(None, Some(event));
812 }
813 assert!(subscription.drain().is_empty());
814 for _ in 0..1_000 {
815 bus.emit(None, Some(InternalEvent::OutboxChanged));
816 }
817 bus.emit(None, Some(stored(true)));
818 assert_eq!(subscription.drain().len(), 1_001);
819 }
820}