1use futures::{Stream, StreamExt};
2use prost::Message;
3use std::sync::Arc;
4use tokio::sync::oneshot;
5use xmtp_common::snippet::Snippet;
6use xmtp_proto::backend_v1::ServerEnvelope;
7
8use tracing::instrument;
9use xmtp_db::prelude::*;
10use xmtp_proto::api_client::XmtpMlsStreams;
11
12use stream_all::StreamAllMessages;
13use stream_conversations::StreamConversations;
14
15const DEFAULT_STREAM_CONSENT_STATES: [ConsentState; 2] =
16 [ConsentState::Allowed, ConsentState::Unknown];
17
18#[cfg(all(test, not(target_arch = "wasm32")))]
20mod bidi_tests;
21#[cfg(all(test, not(target_arch = "wasm32")))]
23mod bidi_fuzz_tests;
24pub mod barrier;
26pub mod catch_up;
27pub(crate) mod connection_state;
28pub mod incoming;
29#[doc(hidden)]
30pub mod internal;
31pub mod local_delivery;
32pub mod message_reader;
33pub(crate) mod policy;
34pub(crate) mod recovery;
35mod stream_all;
36pub mod stream_conversations;
37pub mod stream_failure;
38pub mod stream_messages;
39#[cfg(all(test, not(target_arch = "wasm32")))]
42mod delivery_integration_tests;
43#[cfg(not(target_arch = "wasm32"))]
45pub mod router_callbacks;
46#[cfg(all(test, not(target_arch = "wasm32")))]
47mod router_callbacks_tests;
48pub(crate) mod watchdog;
49
50use crate::messages::enrichment::EnrichMessageError;
51#[cfg(any(test, feature = "test-utils"))]
52use crate::subscriptions::stream_messages::stream_stats::{StreamStatsWrapper, StreamWithStats};
53
54use crate::worker::device_sync::preference_sync::PreferenceUpdate;
55use crate::{
56 Client,
57 context::XmtpSharedContext,
58 groups::{GroupError, MlsGroup, mls_sync::GroupMessageProcessingError},
59 messages::decoded_message::{DecodedMessage, DeletedBy},
60};
61use internal::InternalEvent;
62use thiserror::Error;
63use xmtp_common::{ErrorCode, MaybeSend, RetryableError, StreamHandle, retryable};
64use xmtp_db::{
65 NotFound, StorageError,
66 consent_record::{ConsentState, StoredConsentRecord},
67 group::ConversationType,
68 group_message::StoredGroupMessage,
69};
70use xmtp_events::{EventFilter, Subscription};
71
72pub(crate) type Result<T> = std::result::Result<T, SubscribeError>;
73
74#[derive(Debug, Error)]
75pub enum LocalEventError {
76 #[error("Unable to send event: {0}")]
77 Send(String),
78}
79
80impl RetryableError for LocalEventError {
81 fn is_retryable(&self) -> bool {
82 true
83 }
84}
85
86pub(crate) trait StreamMessages {
87 fn stream_consent_updates(self) -> impl Stream<Item = Result<Vec<StoredConsentRecord>>>;
88 fn stream_preference_updates(self) -> impl Stream<Item = Result<Vec<PreferenceUpdate>>>;
89 fn stream_message_deletions(self) -> impl Stream<Item = Result<DecodedMessage>>;
90}
91
92impl StreamMessages for Subscription<InternalEvent> {
93 #[instrument(level = "trace", skip_all)]
94 fn stream_consent_updates(self) -> impl Stream<Item = Result<Vec<StoredConsentRecord>>> {
95 futures::stream::unfold(self, |subscription| async move {
96 loop {
97 let item = subscription.next().await?;
98 if let Some(xmtp_events::ClientEvent::Lagged(lagged)) = &item.client {
99 tracing::warn!(discarded = lagged.discarded, "legacy consent stream lagged");
100 }
101 if let Some(InternalEvent::PreferencesChanged { updates, .. }) = item.internal {
102 let records = updates
103 .into_iter()
104 .filter_map(|update| match update {
105 PreferenceUpdate::Consent(record) => Some(record),
106 _ => None,
107 })
108 .collect();
109 return Some((Ok(records), subscription));
110 }
111 }
112 })
113 }
114
115 #[instrument(level = "trace", skip_all)]
116 fn stream_preference_updates(self) -> impl Stream<Item = Result<Vec<PreferenceUpdate>>> {
117 futures::stream::unfold(self, |subscription| async move {
118 loop {
119 let item = subscription.next().await?;
120 if let Some(xmtp_events::ClientEvent::Lagged(lagged)) = &item.client {
121 tracing::warn!(
122 discarded = lagged.discarded,
123 "legacy preference stream lagged"
124 );
125 }
126 if let Some(InternalEvent::PreferencesChanged { updates, .. }) = item.internal {
127 return Some((Ok(updates), subscription));
128 }
129 }
130 })
131 }
132
133 #[instrument(level = "trace", skip_all)]
134 fn stream_message_deletions(self) -> impl Stream<Item = Result<DecodedMessage>> {
135 futures::stream::unfold(self, |subscription| async move {
136 loop {
137 let item = subscription.next().await?;
138 if let Some(xmtp_events::ClientEvent::Lagged(lagged)) = &item.client {
139 tracing::warn!(
140 discarded = lagged.discarded,
141 "legacy deletion stream lagged"
142 );
143 }
144 let decoded = match item.internal {
145 Some(InternalEvent::MessagesDeleted(deleted)) => {
149 let now = xmtp_common::time::now_ns();
150 deletion_items(deleted, |m| m.expire_at_ns.is_some_and(|at| at <= now))
151 }
152 Some(InternalEvent::MessagesExpired(deleted)) => {
153 deletion_items(deleted, |_| true)
154 }
155 _ => continue,
156 };
157 return Some((futures::stream::iter(decoded), subscription));
158 }
159 })
160 .flatten()
161 }
162}
163
164fn deletion_items(
170 deleted: internal::DeletedMessages,
171 is_expired: impl Fn(&StoredGroupMessage) -> bool,
172) -> Vec<Result<DecodedMessage>> {
173 let internal::DeletedMessages {
174 messages,
175 deleted_by_inbox_id,
176 } = deleted;
177 messages
178 .into_iter()
179 .map(|message| {
180 if !is_expired(&message) {
181 return Ok(DecodedMessage::from(message));
182 }
183 let deleted_by = deleted_by_inbox_id
185 .as_deref()
186 .map_or(DeletedBy::Sender, |deleter| {
187 DeletedBy::new(deleter, &message.sender_inbox_id)
188 });
189 Ok(DecodedMessage::expired(message, deleted_by))
190 })
191 .collect()
192}
193
194#[derive(thiserror::Error, Debug, ErrorCode)]
195pub enum SubscribeError {
196 #[error(transparent)]
198 #[error_code(inherit)]
199 LocalDelivery(#[from] local_delivery::LocalDeliveryError),
200 #[cfg(not(target_arch = "wasm32"))]
202 #[error(transparent)]
203 Transport(#[from] xmtp_api_backend::TransportError),
204 #[error(transparent)]
208 Group(#[from] Box<GroupError>),
209 #[error(transparent)]
213 NotFound(#[from] NotFound),
214 #[error("group message expected in database but is missing")]
219 GroupMessageNotFound,
220 #[error("processing group message in stream: {0}")]
224 ReceiveGroup(#[from] Box<GroupMessageProcessingError>),
225 #[error(transparent)]
229 Storage(#[from] StorageError),
230 #[error(transparent)]
234 Decode(#[from] prost::DecodeError),
235 #[error(transparent)]
239 ApiClient(#[from] xmtp_api::ApiError),
240 #[error("{0}")]
244 BoxError(Box<dyn RetryableError>),
245 #[error(transparent)]
249 Db(#[from] xmtp_db::ConnectionError),
250 #[error(transparent)]
254 Conversion(#[from] xmtp_proto::ConversionError),
255 #[error(transparent)]
259 Envelope(#[from] xmtp_api_backend::envelope::EnvelopeError),
260 #[error("error occured during subscription {0}")]
262 Enriched(#[from] EnrichMessageError),
263 #[error(transparent)]
269 #[error_code(inherit)]
270 Configuration(Box<crate::client::ClientError>),
271 #[error("stream went stale: no activity within the idle timeout")]
276 StreamStale,
277}
278
279impl SubscribeError {
280 pub fn dyn_err(other: impl RetryableError + 'static) -> Self {
281 SubscribeError::BoxError(Box::new(other) as _)
282 }
283}
284
285impl From<GroupError> for SubscribeError {
286 fn from(value: GroupError) -> Self {
287 SubscribeError::Group(Box::new(value))
288 }
289}
290
291impl From<GroupMessageProcessingError> for SubscribeError {
292 fn from(value: GroupMessageProcessingError) -> Self {
293 SubscribeError::ReceiveGroup(Box::new(value))
294 }
295}
296
297impl RetryableError for SubscribeError {
298 fn is_retryable(&self) -> bool {
299 use SubscribeError::*;
300 match self {
301 #[cfg(not(target_arch = "wasm32"))]
302 Transport(error) => error.is_retryable(),
303 LocalDelivery(e) => retryable!(e),
304 Group(e) => retryable!(e),
305 GroupMessageNotFound => true,
306 ReceiveGroup(e) => retryable!(e),
307 Storage(e) => retryable!(e),
308 Decode(_) => false,
309 NotFound(e) => retryable!(e),
310 ApiClient(e) => retryable!(e),
311 BoxError(e) => retryable!(e),
312 Db(c) => retryable!(c),
313 Conversion(c) => retryable!(c),
314 Envelope(c) => retryable!(c),
315 Enriched(c) => retryable!(c),
316 Configuration(_) => false,
317 StreamStale => true,
318 }
319 }
320}
321
322impl crate::worker::NeedsDbReconnect for SubscribeError {
323 fn needs_db_reconnect(&self) -> bool {
326 use SubscribeError::*;
327 match self {
328 Group(e) => e.needs_db_reconnect(),
329 Storage(e) => e.db_needs_connection(),
330 LocalDelivery(local_delivery::LocalDeliveryError::Storage(e)) => {
331 e.db_needs_connection()
332 }
333 LocalDelivery(_) => false,
334 Db(c) => c.db_needs_connection(),
335 #[cfg(not(target_arch = "wasm32"))]
336 Transport(_) => false,
337 GroupMessageNotFound | ReceiveGroup(_) | Decode(_) | NotFound(_) | ApiClient(_)
338 | BoxError(_) | Conversion(_) | Envelope(_) | Enriched(_) | Configuration(_)
339 | StreamStale => false,
340 }
341 }
342}
343
344impl<Context> Client<Context>
345where
346 Context: XmtpSharedContext + 'static,
347{
348 pub async fn process_streamed_welcome_message(
350 &self,
351 envelope_bytes: Vec<u8>,
352 ) -> Result<Vec<MlsGroup<Context>>> {
353 let wire = ServerEnvelope::decode(envelope_bytes.as_slice())?;
354 let meta = wire
355 .meta
356 .as_ref()
357 .ok_or(xmtp_api::ApiError::InvalidResponse("welcome metadata"))?;
358 let (topic, cursor, _) = xmtp_api_backend::envelope::metadata(
359 meta,
360 xmtp_proto::types::TopicKind::WelcomeMessagesV1,
361 )?;
362 if topic != xmtp_proto::types::Topic::new_welcome_message(self.context.installation_id()) {
363 return Err(xmtp_api::ApiError::InvalidResponse("welcome installation").into());
364 }
365 barrier::wait_through(&self.context, [(topic, cursor)].into(), None)
366 .await
367 .map_err(|error| {
368 self.context
369 .server_configuration()
370 .blocked_or(GroupError::from(error))
371 })?;
372 let Some(group) = self.context.db().find_group_by_sequence_id(cursor)? else {
373 return Ok(Vec::new());
374 };
375 Ok(vec![MlsGroup::new(
376 self.context.clone(),
377 group.id,
378 group.dm_id,
379 group.conversation_type,
380 group.created_at_ns,
381 )])
382 }
383
384 #[xmtp_common::span(prefix = "stream")]
385 pub async fn stream_conversations(
386 &self,
387 conversation_type: Option<ConversationType>,
388 include_duplicate_dms: bool,
389 ) -> Result<impl Stream<Item = Result<MlsGroup<Context>>> + use<'_, Context>>
390 where
391 Context::ApiClient: XmtpMlsStreams,
392 {
393 StreamConversations::new(
394 &self.context,
395 conversation_type,
396 include_duplicate_dms,
397 None,
398 )
399 .await
400 }
401
402 #[xmtp_common::span(prefix = "stream")]
404 pub async fn stream_conversations_owned(
405 &self,
406 conversation_type: Option<ConversationType>,
407 include_duplicate_dms: bool,
408 ) -> Result<impl Stream<Item = Result<MlsGroup<Context>>> + 'static + use<Context>>
409 where
410 Context::ApiClient: XmtpMlsStreams,
411 {
412 StreamConversations::new_owned(
413 self.context.clone(),
414 conversation_type,
415 include_duplicate_dms,
416 None,
417 )
418 .await
419 }
420}
421
422impl<Context> Client<Context>
423where
424 Context: XmtpSharedContext + 'static,
425 Context::ApiClient: XmtpMlsStreams + 'static,
426 Context::MlsStorage: 'static,
427{
428 pub fn stream_conversations_with_callback(
429 client: Arc<Client<Context>>,
430 conversation_type: Option<ConversationType>,
431 convo_callback: impl FnMut(Result<MlsGroup<Context>>) + MaybeSend + 'static,
432 on_close: impl FnOnce() + MaybeSend + 'static,
433 include_duplicate_dms: bool,
434 ) -> impl StreamHandle<StreamOutput = Result<()>> {
435 let cancel = watchdog::StreamCancel::new(&client.context);
436 watchdog::spawn_watchdog_stream(
443 cancel,
444 "stream_conversations",
445 move || {
446 let client = client.clone();
447 async move {
448 client
449 .stream_conversations_owned(conversation_type, include_duplicate_dms)
450 .await
451 }
452 },
453 convo_callback,
454 on_close,
455 )
456 }
457
458 #[xmtp_common::span(prefix = "stream")]
459 pub async fn stream_all_messages(
460 &self,
461 conversation_type: Option<ConversationType>,
462 consent_state: Option<Vec<ConsentState>>,
463 ) -> Result<impl Stream<Item = Result<StoredGroupMessage>> + '_> {
464 tracing::debug!(
465 inbox_id = self.inbox_id(),
466 installation_id = self.context.installation_id().as_slice().snippet(),
467 conversation_type = ?conversation_type,
468 "stream all messages"
469 );
470
471 StreamAllMessages::new(&self.context, conversation_type, consent_state).await
472 }
473
474 #[xmtp_common::span(prefix = "stream")]
475 pub async fn stream_all_messages_owned(
476 &self,
477 conversation_type: Option<ConversationType>,
478 consent_state: Option<Vec<ConsentState>>,
479 ) -> Result<impl Stream<Item = Result<StoredGroupMessage>> + 'static + use<Context>> {
480 tracing::debug!(
481 inbox_id = self.inbox_id(),
482 installation_id = self.context.installation_id().as_slice().snippet(),
483 conversation_type = ?conversation_type,
484 "stream all messages"
485 );
486
487 StreamAllMessages::new_owned(self.context.clone(), conversation_type, consent_state).await
488 }
489
490 pub fn stream_consent_with_callback(
491 client: Arc<Client<Context>>,
492 mut callback: impl FnMut(Result<Vec<StoredConsentRecord>>) + MaybeSend + 'static,
493 on_close: impl FnOnce() + MaybeSend + 'static,
494 ) -> impl StreamHandle<StreamOutput = Result<()>> {
495 let (tx, rx) = oneshot::channel();
496
497 xmtp_common::spawn(
498 Some(rx),
499 xmtp_common::bind_task_hub(async move {
500 let cancel = watchdog::StreamCancel::new(&client.context);
503 let receiver = client.context.events().subscribe(
504 EventFilter::default().with_internal(|event| {
505 matches!(event, InternalEvent::PreferencesChanged { .. })
506 }),
507 Some(1024),
508 );
509 let stream = receiver.stream_consent_updates();
510
511 futures::pin_mut!(stream);
512 let _ = tx.send(());
513 let cancelled = loop {
514 tokio::select! {
515 _ = cancel.cancelled() => break true,
516 next = stream.next() => match next {
517 Some(message) => callback(message),
518 None => break false,
519 }
520 }
521 };
522 tracing::debug!("`stream_consent` stream ended, dropping stream");
523 let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
524 on_close();
525 result
526 }),
527 )
528 }
529
530 pub fn stream_preferences_with_callback(
531 client: Arc<Client<Context>>,
532 mut callback: impl FnMut(Result<Vec<PreferenceUpdate>>) + MaybeSend + 'static,
533 on_close: impl FnOnce() + MaybeSend + 'static,
534 ) -> impl StreamHandle<StreamOutput = Result<()>> {
535 let (tx, rx) = oneshot::channel();
536
537 xmtp_common::spawn(
538 Some(rx),
539 xmtp_common::bind_task_hub(async move {
540 let cancel = watchdog::StreamCancel::new(&client.context);
543 let receiver = client.context.events().subscribe(
544 EventFilter::default().with_internal(|event| {
545 matches!(event, InternalEvent::PreferencesChanged { .. })
546 }),
547 Some(1024),
548 );
549 let stream = receiver.stream_preference_updates();
550
551 futures::pin_mut!(stream);
552 let _ = tx.send(());
553 let cancelled = loop {
554 tokio::select! {
555 _ = cancel.cancelled() => break true,
556 next = stream.next() => match next {
557 Some(message) => callback(message),
558 None => break false,
559 }
560 }
561 };
562 tracing::debug!("`stream_preferences` stream ended, dropping stream");
563 let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
564 on_close();
565 result
566 }),
567 )
568 }
569
570 pub fn stream_message_deletions_with_callback(
571 client: Arc<Client<Context>>,
572 mut callback: impl FnMut(Result<DecodedMessage>) + MaybeSend + 'static,
573 on_close: impl FnOnce() + MaybeSend + 'static,
574 ) -> impl StreamHandle<StreamOutput = Result<()>> {
575 let (tx, rx) = oneshot::channel();
576
577 xmtp_common::spawn(
578 Some(rx),
579 xmtp_common::bind_task_hub(async move {
580 let cancel = watchdog::StreamCancel::new(&client.context);
583 let receiver = client.context.events().subscribe(
584 EventFilter::default().with_internal(InternalEvent::is_message_deletion),
585 Some(1024),
586 );
587 let stream = receiver.stream_message_deletions();
588
589 futures::pin_mut!(stream);
590 let _ = tx.send(());
591 let cancelled = loop {
592 tokio::select! {
593 _ = cancel.cancelled() => break true,
594 next = stream.next() => match next {
595 Some(message) => callback(message),
596 None => break false,
597 }
598 }
599 };
600 tracing::debug!("`stream_message_deletions` stream ended, dropping stream");
601 let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
602 on_close();
603 result
604 }),
605 )
606 }
607}
608
609impl<Context> Client<Context>
610where
611 Context: XmtpSharedContext + 'static,
612 Context::ApiClient: XmtpMlsStreams + 'static,
613 Context::MlsStorage: 'static,
614 <Context::ApiClient as XmtpMlsStreams>::GroupMessageStream: Unpin,
615 <Context::ApiClient as XmtpMlsStreams>::WelcomeMessageStream: Unpin,
616{
617 #[tracing::instrument(level = "trace", skip_all)]
618 #[cfg(any(test, feature = "test-utils"))]
619 pub async fn stream_all_messages_owned_with_stats(
620 &self,
621 conversation_type: Option<ConversationType>,
622 consent_state: Option<Vec<ConsentState>>,
623 ) -> Result<impl StreamWithStats<Item = Result<StoredGroupMessage>> + 'static> {
624 tracing::debug!(
625 inbox_id = self.inbox_id(),
626 installation_id = self.context.installation_id().as_slice().snippet(),
627 conversation_type = ?conversation_type,
628 "stream all messages"
629 );
630
631 let stream =
632 StreamAllMessages::new_owned(self.context.clone(), conversation_type, consent_state)
633 .await?;
634
635 Ok(StreamStatsWrapper::new(stream))
636 }
637}
638
639#[derive(Debug, Clone, Copy)]
640pub enum StreamKind {
641 All,
642 Conversations,
643 Messages,
644}
645
646#[cfg(test)]
647pub(crate) mod tests {
648 use crate::context::XmtpSharedContext;
649 use crate::tester;
650
651 #[macro_export]
658 macro_rules! assert_msg {
659 ($stream:expr, $expected:expr) => {
660 let next = $stream
661 .next()
662 .await
663 .unwrap()
664 .inspect_err(|e| tracing::error!("{}", e.to_string()))
665 .unwrap();
666
667 assert_eq!(
668 String::from_utf8_lossy(next.decrypted_message_bytes.as_slice()),
669 String::from_utf8_lossy($expected.as_bytes())
670 );
671 };
672 }
673
674 #[macro_export]
681 macro_rules! assert_msg_exists {
682 ($stream:expr) => {
683 assert!(
684 !$stream
685 .next()
686 .await
687 .unwrap()
688 .unwrap()
689 .decrypted_message_bytes
690 .is_empty()
691 );
692 };
693 }
694
695 #[xmtp_common::test(flavor = "multi_thread", worker_threads = 5, unwrap_try = true)]
696 async fn test_process_streamed_welcome_message() {
697 use prost::Message;
698 use xmtp_proto::types::{Cursor, Topic};
699 tester!(alix);
700 tester!(bo);
701 let alix_group = alix.create_group(None, None)?;
702 alix_group.add_members(&[bo.inbox_id()]).await?;
703 let envelopes = alix
704 .context
705 .api()
706 .query_all(
707 [(
708 Topic::new_welcome_message(bo.context.installation_id()),
709 Cursor(0),
710 )]
711 .into(),
712 bo.context.api().limits().max_query_limit as u32,
713 )
714 .await?;
715 assert!(!envelopes.is_empty(), "Should have at least one welcome");
716 let groups = bo
717 .process_streamed_welcome_message(envelopes[0].encode_to_vec())
718 .await?;
719 assert_eq!(groups.len(), 1, "Should have exactly one group");
720 }
721}