1use futures::{Stream, StreamExt};
2use prost::Message;
3use std::sync::Arc;
4use tokio::sync::{broadcast, oneshot};
5use tokio_stream::wrappers::BroadcastStream;
6use xmtp_proto::backend_v1::ServerEnvelope;
7use xmtp_proto::types::GroupId;
8
9use tracing::instrument;
10use xmtp_db::prelude::*;
11use xmtp_proto::api_client::XmtpMlsStreams;
12
13use stream_all::StreamAllMessages;
14use stream_conversations::StreamConversations;
15
16#[cfg(all(test, not(target_arch = "wasm32")))]
18mod bidi_tests;
19#[cfg(all(test, not(target_arch = "wasm32")))]
21mod bidi_fuzz_tests;
22pub mod barrier;
25#[cfg(not(target_arch = "wasm32"))]
26pub mod catch_up;
27pub mod incoming;
28pub mod local_delivery;
29pub mod message_reader;
30pub(crate) mod policy;
31mod stream_all;
32mod stream_conversations;
33pub mod stream_failure;
34pub mod stream_messages;
35#[cfg(all(test, not(target_arch = "wasm32")))]
38mod delivery_integration_tests;
39#[cfg(not(target_arch = "wasm32"))]
41pub mod router_callbacks;
42#[cfg(all(test, not(target_arch = "wasm32")))]
43mod router_callbacks_tests;
44pub(crate) mod watchdog;
45
46use crate::messages::enrichment::EnrichMessageError;
47#[cfg(any(test, feature = "test-utils"))]
48use crate::subscriptions::stream_messages::stream_stats::{StreamStatsWrapper, StreamWithStats};
49
50use crate::worker::device_sync::preference_sync::PreferenceUpdate;
51use crate::{
52 Client,
53 context::XmtpSharedContext,
54 groups::{GroupError, MlsGroup, mls_sync::GroupMessageProcessingError},
55 messages::decoded_message::DecodedMessage,
56};
57use thiserror::Error;
58use xmtp_common::{ErrorCode, MaybeSend, RetryableError, StreamHandle, retryable};
59use xmtp_db::{
60 NotFound, StorageError,
61 consent_record::{ConsentState, StoredConsentRecord},
62 group::ConversationType,
63 group_message::StoredGroupMessage,
64};
65
66pub(crate) type Result<T> = std::result::Result<T, SubscribeError>;
67
68#[derive(Debug, Error)]
69pub enum LocalEventError {
70 #[error("Unable to send event: {0}")]
71 Send(String),
72}
73
74impl RetryableError for LocalEventError {
75 fn is_retryable(&self) -> bool {
76 true
77 }
78}
79
80#[derive(Debug, Clone)]
83pub enum LocalEvents {
84 NewGroup(GroupId),
86 MessagesStored,
88 PreferencesChanged(Vec<PreferenceUpdate>),
89 MsgsDeleted(Vec<StoredGroupMessage>),
91}
92
93#[derive(Clone)]
94pub enum SyncWorkerEvent {
95 NewSyncGroupFromWelcome(Vec<u8>),
96 NewSyncGroupMsg,
97 SyncPreferences(Vec<PreferenceUpdate>),
99 CycleHMAC,
100 Tick,
101}
102
103impl std::fmt::Debug for SyncWorkerEvent {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::NewSyncGroupFromWelcome(arg0) => f
107 .debug_tuple("NewSyncGroupFromWelcome")
108 .field(&hex::encode(arg0))
109 .finish(),
110 Self::NewSyncGroupMsg => write!(f, "NewSyncGroupMsg"),
111 Self::SyncPreferences(arg0) => f.debug_tuple("SyncPreferences").field(arg0).finish(),
112 Self::CycleHMAC => write!(f, "CycleHMAC"),
113 Self::Tick => write!(f, "Tick"),
114 }
115 }
116}
117
118impl LocalEvents {
119 fn consent_filter(self) -> Option<Vec<StoredConsentRecord>> {
120 match self {
121 Self::PreferencesChanged(updates) => {
122 let updates = updates
123 .into_iter()
124 .filter_map(|pu| match pu {
125 PreferenceUpdate::Consent(cr) => Some(cr),
126 _ => None,
127 })
128 .collect();
129 Some(updates)
130 }
131
132 _ => None,
133 }
134 }
135
136 fn preference_filter(self) -> Option<Vec<PreferenceUpdate>> {
137 match self {
138 Self::PreferencesChanged(updates) => Some(updates),
139 _ => None,
140 }
141 }
142
143 fn message_deletion_filter(self) -> Option<Vec<StoredGroupMessage>> {
144 match self {
145 Self::MsgsDeleted(msgs) => Some(msgs),
146 _ => None,
147 }
148 }
149}
150
151pub(crate) trait StreamMessages {
152 fn stream_consent_updates(self) -> impl Stream<Item = Result<Vec<StoredConsentRecord>>>;
153 fn stream_preference_updates(self) -> impl Stream<Item = Result<Vec<PreferenceUpdate>>>;
154 fn stream_message_deletions(self) -> impl Stream<Item = Result<DecodedMessage>>;
155}
156
157impl StreamMessages for broadcast::Receiver<LocalEvents> {
158 #[instrument(level = "trace", skip_all)]
159 fn stream_consent_updates(self) -> impl Stream<Item = Result<Vec<StoredConsentRecord>>> {
160 BroadcastStream::new(self).filter_map(|event| async {
161 xmtp_common::optify!(event, "Missed message due to event queue lag")
162 .and_then(LocalEvents::consent_filter)
163 .map(Result::Ok)
164 })
165 }
166
167 #[instrument(level = "trace", skip_all)]
168 fn stream_preference_updates(self) -> impl Stream<Item = Result<Vec<PreferenceUpdate>>> {
169 BroadcastStream::new(self).filter_map(|event| async {
170 xmtp_common::optify!(event, "Missed message due to event queue lag")
171 .and_then(LocalEvents::preference_filter)
172 .map(Result::Ok)
173 })
174 }
175
176 #[instrument(level = "trace", skip_all)]
177 fn stream_message_deletions(self) -> impl Stream<Item = Result<DecodedMessage>> {
178 BroadcastStream::new(self)
179 .filter_map(|event| async {
180 xmtp_common::optify!(event, "Missed message due to event queue lag")
181 .and_then(LocalEvents::message_deletion_filter)
182 .map(futures::stream::iter)
183 })
184 .flatten()
185 .map(|m| DecodedMessage::try_from(m).map_err(Into::into))
188 }
189}
190
191#[derive(thiserror::Error, Debug, ErrorCode)]
192pub enum SubscribeError {
193 #[error(transparent)]
195 #[error_code(inherit)]
196 LocalDelivery(#[from] local_delivery::LocalDeliveryError),
197 #[cfg(not(target_arch = "wasm32"))]
199 #[error(transparent)]
200 Transport(#[from] xmtp_api_backend::TransportError),
201 #[error(transparent)]
205 Group(#[from] Box<GroupError>),
206 #[error(transparent)]
210 NotFound(#[from] NotFound),
211 #[error("group message expected in database but is missing")]
216 GroupMessageNotFound,
217 #[error("processing group message in stream: {0}")]
221 ReceiveGroup(#[from] Box<GroupMessageProcessingError>),
222 #[error(transparent)]
226 Storage(#[from] StorageError),
227 #[error(transparent)]
231 Decode(#[from] prost::DecodeError),
232 #[error(transparent)]
236 ApiClient(#[from] xmtp_api::ApiError),
237 #[error("{0}")]
241 BoxError(Box<dyn RetryableError>),
242 #[error(transparent)]
246 Db(#[from] xmtp_db::ConnectionError),
247 #[error(transparent)]
251 Conversion(#[from] xmtp_proto::ConversionError),
252 #[error(transparent)]
256 Envelope(#[from] xmtp_api_backend::envelope::EnvelopeError),
257 #[error("error occured during subscription {0}")]
259 Enriched(#[from] EnrichMessageError),
260 #[error(transparent)]
266 #[error_code(inherit)]
267 Configuration(Box<crate::client::ClientError>),
268 #[error("stream went stale: no activity within the idle timeout")]
273 StreamStale,
274}
275
276impl SubscribeError {
277 pub fn dyn_err(other: impl RetryableError + 'static) -> Self {
278 SubscribeError::BoxError(Box::new(other) as _)
279 }
280}
281
282impl From<GroupError> for SubscribeError {
283 fn from(value: GroupError) -> Self {
284 SubscribeError::Group(Box::new(value))
285 }
286}
287
288impl From<GroupMessageProcessingError> for SubscribeError {
289 fn from(value: GroupMessageProcessingError) -> Self {
290 SubscribeError::ReceiveGroup(Box::new(value))
291 }
292}
293
294impl RetryableError for SubscribeError {
295 fn is_retryable(&self) -> bool {
296 use SubscribeError::*;
297 match self {
298 #[cfg(not(target_arch = "wasm32"))]
299 Transport(error) => error.is_retryable(),
300 LocalDelivery(e) => retryable!(e),
301 Group(e) => retryable!(e),
302 GroupMessageNotFound => true,
303 ReceiveGroup(e) => retryable!(e),
304 Storage(e) => retryable!(e),
305 Decode(_) => false,
306 NotFound(e) => retryable!(e),
307 ApiClient(e) => retryable!(e),
308 BoxError(e) => retryable!(e),
309 Db(c) => retryable!(c),
310 Conversion(c) => retryable!(c),
311 Envelope(c) => retryable!(c),
312 Enriched(c) => retryable!(c),
313 Configuration(_) => false,
314 StreamStale => true,
315 }
316 }
317}
318
319impl crate::worker::NeedsDbReconnect for SubscribeError {
320 fn needs_db_reconnect(&self) -> bool {
323 use SubscribeError::*;
324 match self {
325 Group(e) => e.needs_db_reconnect(),
326 Storage(e) => e.db_needs_connection(),
327 LocalDelivery(local_delivery::LocalDeliveryError::Storage(e)) => {
328 e.db_needs_connection()
329 }
330 LocalDelivery(_) => false,
331 Db(c) => c.db_needs_connection(),
332 #[cfg(not(target_arch = "wasm32"))]
333 Transport(_) => false,
334 GroupMessageNotFound | ReceiveGroup(_) | Decode(_) | NotFound(_) | ApiClient(_)
335 | BoxError(_) | Conversion(_) | Envelope(_) | Enriched(_) | Configuration(_)
336 | StreamStale => false,
337 }
338 }
339}
340
341impl<Context> Client<Context>
342where
343 Context: XmtpSharedContext + 'static,
344{
345 pub async fn process_streamed_welcome_message(
347 &self,
348 envelope_bytes: Vec<u8>,
349 ) -> Result<Vec<MlsGroup<Context>>> {
350 let wire = ServerEnvelope::decode(envelope_bytes.as_slice())?;
351 let meta = wire
352 .meta
353 .as_ref()
354 .ok_or(xmtp_api::ApiError::InvalidResponse("welcome metadata"))?;
355 let (topic, cursor, _) = xmtp_api_backend::envelope::metadata(
356 meta,
357 xmtp_proto::types::TopicKind::WelcomeMessagesV1,
358 )?;
359 if topic != xmtp_proto::types::Topic::new_welcome_message(self.context.installation_id()) {
360 return Err(xmtp_api::ApiError::InvalidResponse("welcome installation").into());
361 }
362 barrier::wait_through(&self.context, [(topic, cursor)].into(), None)
363 .await
364 .map_err(GroupError::from)?;
365 let Some(group) = self.context.db().find_group_by_sequence_id(cursor)? else {
366 return Ok(Vec::new());
367 };
368 Ok(vec![MlsGroup::new(
369 self.context.clone(),
370 group.id,
371 group.dm_id,
372 group.conversation_type,
373 group.created_at_ns,
374 )])
375 }
376
377 #[xmtp_common::span(prefix = "stream")]
378 pub async fn stream_conversations(
379 &self,
380 conversation_type: Option<ConversationType>,
381 include_duplicate_dms: bool,
382 ) -> Result<impl Stream<Item = Result<MlsGroup<Context>>> + use<'_, Context>>
383 where
384 Context::ApiClient: XmtpMlsStreams,
385 {
386 StreamConversations::new(
387 &self.context,
388 conversation_type,
389 include_duplicate_dms,
390 None,
391 )
392 .await
393 }
394
395 #[xmtp_common::span(prefix = "stream")]
397 pub async fn stream_conversations_owned(
398 &self,
399 conversation_type: Option<ConversationType>,
400 include_duplicate_dms: bool,
401 ) -> Result<impl Stream<Item = Result<MlsGroup<Context>>> + 'static + use<Context>>
402 where
403 Context::ApiClient: XmtpMlsStreams,
404 {
405 StreamConversations::new_owned(
406 self.context.clone(),
407 conversation_type,
408 include_duplicate_dms,
409 None,
410 )
411 .await
412 }
413}
414
415impl<Context> Client<Context>
416where
417 Context: XmtpSharedContext + 'static,
418 Context::ApiClient: XmtpMlsStreams + 'static,
419 Context::MlsStorage: 'static,
420{
421 pub fn stream_conversations_with_callback(
422 client: Arc<Client<Context>>,
423 conversation_type: Option<ConversationType>,
424 convo_callback: impl FnMut(Result<MlsGroup<Context>>) + MaybeSend + 'static,
425 on_close: impl FnOnce() + MaybeSend + 'static,
426 include_duplicate_dms: bool,
427 ) -> impl StreamHandle<StreamOutput = Result<()>> {
428 let cancel = watchdog::StreamCancel::new(&client.context);
429 watchdog::spawn_watchdog_stream(
436 cancel,
437 "stream_conversations",
438 move || {
439 let client = client.clone();
440 async move {
441 client
442 .stream_conversations_owned(conversation_type, include_duplicate_dms)
443 .await
444 }
445 },
446 convo_callback,
447 on_close,
448 )
449 }
450
451 #[xmtp_common::span(prefix = "stream")]
452 pub async fn stream_all_messages(
453 &self,
454 conversation_type: Option<ConversationType>,
455 consent_state: Option<Vec<ConsentState>>,
456 ) -> Result<impl Stream<Item = Result<StoredGroupMessage>> + '_> {
457 tracing::debug!(
458 inbox_id = self.inbox_id(),
459 installation_id = %self.context.installation_id(),
460 conversation_type = ?conversation_type,
461 "stream all messages"
462 );
463
464 StreamAllMessages::new(&self.context, conversation_type, consent_state).await
465 }
466
467 #[xmtp_common::span(prefix = "stream")]
468 pub async fn stream_all_messages_owned(
469 &self,
470 conversation_type: Option<ConversationType>,
471 consent_state: Option<Vec<ConsentState>>,
472 ) -> Result<impl Stream<Item = Result<StoredGroupMessage>> + 'static + use<Context>> {
473 tracing::debug!(
474 inbox_id = self.inbox_id(),
475 installation_id = %self.context.installation_id(),
476 conversation_type = ?conversation_type,
477 "stream all messages"
478 );
479
480 StreamAllMessages::new_owned(self.context.clone(), conversation_type, consent_state).await
481 }
482
483 pub fn stream_all_messages_with_callback(
484 context: Context,
485 conversation_type: Option<ConversationType>,
486 consent_state: Option<Vec<ConsentState>>,
487 callback: impl FnMut(Result<StoredGroupMessage>) + MaybeSend + 'static,
488 on_close: impl FnOnce() + MaybeSend + 'static,
489 ) -> impl StreamHandle<StreamOutput = Result<()>> {
490 let cancel = watchdog::StreamCancel::new(&context);
491 watchdog::spawn_watchdog_stream(
492 cancel,
493 "stream_all_messages",
494 move || {
495 let context = context.clone();
496 let consent_state = consent_state.clone();
497 async move {
498 StreamAllMessages::new_owned(context, conversation_type, consent_state).await
499 }
500 },
501 callback,
502 on_close,
503 )
504 }
505
506 pub fn stream_consent_with_callback(
507 client: Arc<Client<Context>>,
508 mut callback: impl FnMut(Result<Vec<StoredConsentRecord>>) + MaybeSend + 'static,
509 on_close: impl FnOnce() + MaybeSend + 'static,
510 ) -> impl StreamHandle<StreamOutput = Result<()>> {
511 let (tx, rx) = oneshot::channel();
512
513 xmtp_common::spawn(
514 Some(rx),
515 xmtp_common::bind_task_hub(async move {
516 let cancel = watchdog::StreamCancel::new(&client.context);
519 let receiver = client.local_events.subscribe();
520 let stream = receiver.stream_consent_updates();
521
522 futures::pin_mut!(stream);
523 let _ = tx.send(());
524 let cancelled = loop {
525 tokio::select! {
526 _ = cancel.cancelled() => break true,
527 next = stream.next() => match next {
528 Some(message) => callback(message),
529 None => break false,
530 }
531 }
532 };
533 tracing::debug!("`stream_consent` stream ended, dropping stream");
534 let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
535 on_close();
536 result
537 }),
538 )
539 }
540
541 pub fn stream_preferences_with_callback(
542 client: Arc<Client<Context>>,
543 mut callback: impl FnMut(Result<Vec<PreferenceUpdate>>) + MaybeSend + 'static,
544 on_close: impl FnOnce() + MaybeSend + 'static,
545 ) -> impl StreamHandle<StreamOutput = Result<()>> {
546 let (tx, rx) = oneshot::channel();
547
548 xmtp_common::spawn(
549 Some(rx),
550 xmtp_common::bind_task_hub(async move {
551 let cancel = watchdog::StreamCancel::new(&client.context);
554 let receiver = client.local_events.subscribe();
555 let stream = receiver.stream_preference_updates();
556
557 futures::pin_mut!(stream);
558 let _ = tx.send(());
559 let cancelled = loop {
560 tokio::select! {
561 _ = cancel.cancelled() => break true,
562 next = stream.next() => match next {
563 Some(message) => callback(message),
564 None => break false,
565 }
566 }
567 };
568 tracing::debug!("`stream_preferences` stream ended, dropping stream");
569 let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
570 on_close();
571 result
572 }),
573 )
574 }
575
576 pub fn stream_message_deletions_with_callback(
577 client: Arc<Client<Context>>,
578 mut callback: impl FnMut(Result<DecodedMessage>) + MaybeSend + 'static,
579 on_close: impl FnOnce() + MaybeSend + 'static,
580 ) -> impl StreamHandle<StreamOutput = Result<()>> {
581 let (tx, rx) = oneshot::channel();
582
583 xmtp_common::spawn(
584 Some(rx),
585 xmtp_common::bind_task_hub(async move {
586 let cancel = watchdog::StreamCancel::new(&client.context);
589 let receiver = client.local_events.subscribe();
590 let stream = receiver.stream_message_deletions();
591
592 futures::pin_mut!(stream);
593 let _ = tx.send(());
594 let cancelled = loop {
595 tokio::select! {
596 _ = cancel.cancelled() => break true,
597 next = stream.next() => match next {
598 Some(message) => callback(message),
599 None => break false,
600 }
601 }
602 };
603 tracing::debug!("`stream_message_deletions` stream ended, dropping stream");
604 let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
605 on_close();
606 result
607 }),
608 )
609 }
610}
611
612impl<Context> Client<Context>
613where
614 Context: XmtpSharedContext + 'static,
615 Context::ApiClient: XmtpMlsStreams + 'static,
616 Context::MlsStorage: 'static,
617 <Context::ApiClient as XmtpMlsStreams>::GroupMessageStream: Unpin,
618 <Context::ApiClient as XmtpMlsStreams>::WelcomeMessageStream: Unpin,
619{
620 #[tracing::instrument(level = "trace", skip_all)]
621 #[cfg(any(test, feature = "test-utils"))]
622 pub async fn stream_all_messages_owned_with_stats(
623 &self,
624 conversation_type: Option<ConversationType>,
625 consent_state: Option<Vec<ConsentState>>,
626 ) -> Result<impl StreamWithStats<Item = Result<StoredGroupMessage>> + 'static> {
627 tracing::debug!(
628 inbox_id = self.inbox_id(),
629 installation_id = %self.context.installation_id(),
630 conversation_type = ?conversation_type,
631 "stream all messages"
632 );
633
634 let stream =
635 StreamAllMessages::new_owned(self.context.clone(), conversation_type, consent_state)
636 .await?;
637
638 Ok(StreamStatsWrapper::new(stream))
639 }
640}
641
642#[derive(Debug, Clone, Copy)]
643pub enum StreamKind {
644 All,
645 Conversations,
646 Messages,
647}
648
649#[cfg(test)]
650pub(crate) mod tests {
651 use crate::context::XmtpSharedContext;
652 use crate::tester;
653
654 #[macro_export]
661 macro_rules! assert_msg {
662 ($stream:expr, $expected:expr) => {
663 let next = $stream
664 .next()
665 .await
666 .unwrap()
667 .inspect_err(|e| tracing::error!("{}", e.to_string()))
668 .unwrap();
669
670 assert_eq!(
671 String::from_utf8_lossy(next.decrypted_message_bytes.as_slice()),
672 String::from_utf8_lossy($expected.as_bytes())
673 );
674 };
675 }
676
677 #[macro_export]
684 macro_rules! assert_msg_exists {
685 ($stream:expr) => {
686 assert!(
687 !$stream
688 .next()
689 .await
690 .unwrap()
691 .unwrap()
692 .decrypted_message_bytes
693 .is_empty()
694 );
695 };
696 }
697
698 #[xmtp_common::test(flavor = "multi_thread", worker_threads = 5, unwrap_try = true)]
699 async fn test_process_streamed_welcome_message() {
700 use prost::Message;
701 use xmtp_proto::types::{Cursor, Topic};
702 tester!(alix);
703 tester!(bo);
704 let alix_group = alix.create_group(None, None)?;
705 alix_group.add_members(&[bo.inbox_id()]).await?;
706 let envelopes = alix
707 .context
708 .api()
709 .query_all(
710 [(
711 Topic::new_welcome_message(bo.context.installation_id()),
712 Cursor(0),
713 )]
714 .into(),
715 bo.context.api().limits().max_query_limit as u32,
716 )
717 .await?;
718 assert!(!envelopes.is_empty(), "Should have at least one welcome");
719 let groups = bo
720 .process_streamed_welcome_message(envelopes[0].encode_to_vec())
721 .await?;
722 assert_eq!(groups.len(), 1, "Should have exactly one group");
723 }
724}