Skip to main content

xmtp_mls/subscriptions/
mod.rs

1use futures::{Stream, StreamExt};
2use prost::Message;
3use tokio::sync::oneshot;
4use xmtp_common::snippet::Snippet;
5use xmtp_proto::backend_v1::ServerEnvelope;
6
7use tracing::instrument;
8use xmtp_db::prelude::*;
9use xmtp_proto::api_client::XmtpMlsStreams;
10
11use stream_all::StreamAllMessages;
12use stream_conversations::StreamConversations;
13
14const DEFAULT_STREAM_CONSENT_STATES: [ConsentState; 2] =
15    [ConsentState::Allowed, ConsentState::Unknown];
16
17// Live backend tests require native full-duplex HTTP/2.
18#[cfg(all(test, not(target_arch = "wasm32")))]
19mod bidi_tests;
20// Randomized delivery fuzz over the live node (same gating as `bidi_tests`).
21#[cfg(all(test, not(target_arch = "wasm32")))]
22mod bidi_fuzz_tests;
23// One-shot bounded catch-up through the shared receiver.
24pub mod barrier;
25pub mod catch_up;
26pub(crate) mod connection_state;
27pub mod incoming;
28#[doc(hidden)]
29pub use crate::core::events as internal;
30pub mod local_delivery;
31pub mod message_reader;
32pub(crate) use crate::core::policy;
33pub(crate) mod recovery;
34mod stream_all;
35pub mod stream_conversations;
36pub mod stream_failure;
37pub mod stream_messages;
38// Live integration tests for the router (v3 wire; same gating rationale as
39// `bidi_tests` above).
40#[cfg(all(test, not(target_arch = "wasm32")))]
41mod delivery_integration_tests;
42// A message stream against a backend that charges stream rent.
43#[cfg(all(test, not(target_arch = "wasm32")))]
44mod rate_limit_tests;
45// Native callback adapters over the shared backend transport.
46#[cfg(not(target_arch = "wasm32"))]
47pub mod router_callbacks;
48#[cfg(all(test, not(target_arch = "wasm32")))]
49mod router_callbacks_tests;
50pub(crate) mod watchdog;
51
52use crate::messages::enrichment::EnrichMessageError;
53#[cfg(any(test, feature = "test-utils"))]
54use crate::subscriptions::stream_messages::stream_stats::{StreamStatsWrapper, StreamWithStats};
55
56use crate::worker::device_sync::preference_sync::PreferenceUpdate;
57use crate::{
58    context::XmtpSharedContext,
59    groups::{GroupError, MlsGroup, mls_sync::GroupMessageProcessingError},
60    messages::decoded_message::{DecodedMessage, DeletedBy},
61};
62use internal::InternalEvent;
63use thiserror::Error;
64use xmtp_common::{ErrorCode, MaybeSend, RetryableError, StreamHandle, retryable};
65use xmtp_db::{
66    NotFound, StorageError,
67    consent_record::{ConsentState, StoredConsentRecord},
68    group::ConversationType,
69    group_message::StoredGroupMessage,
70};
71use xmtp_events::{EventFilter, Subscription};
72
73pub(crate) type Result<T> = std::result::Result<T, SubscribeError>;
74
75#[derive(Debug, Error)]
76pub enum LocalEventError {
77    #[error("Unable to send event: {0}")]
78    Send(String),
79}
80
81impl RetryableError for LocalEventError {
82    fn is_retryable(&self) -> bool {
83        true
84    }
85}
86
87pub(crate) trait StreamMessages {
88    fn stream_consent_updates(self) -> impl Stream<Item = Result<Vec<StoredConsentRecord>>>;
89    fn stream_preference_updates(self) -> impl Stream<Item = Result<Vec<PreferenceUpdate>>>;
90    fn stream_message_deletions(self) -> impl Stream<Item = Result<DecodedMessage>>;
91}
92
93impl StreamMessages for Subscription<InternalEvent> {
94    #[instrument(level = "trace", skip_all)]
95    fn stream_consent_updates(self) -> impl Stream<Item = Result<Vec<StoredConsentRecord>>> {
96        futures::stream::unfold(self, |subscription| async move {
97            loop {
98                let item = subscription.next().await?;
99                if let Some(xmtp_events::ClientEvent::Lagged(lagged)) = &item.client {
100                    tracing::warn!(discarded = lagged.discarded, "legacy consent stream lagged");
101                }
102                if let Some(InternalEvent::PreferencesChanged { updates, .. }) = item.internal {
103                    let records = updates
104                        .into_iter()
105                        .filter_map(|update| match update {
106                            PreferenceUpdate::Consent(record) => Some(record),
107                            _ => None,
108                        })
109                        .collect();
110                    return Some((Ok(records), subscription));
111                }
112            }
113        })
114    }
115
116    #[instrument(level = "trace", skip_all)]
117    fn stream_preference_updates(self) -> impl Stream<Item = Result<Vec<PreferenceUpdate>>> {
118        futures::stream::unfold(self, |subscription| async move {
119            loop {
120                let item = subscription.next().await?;
121                if let Some(xmtp_events::ClientEvent::Lagged(lagged)) = &item.client {
122                    tracing::warn!(
123                        discarded = lagged.discarded,
124                        "legacy preference stream lagged"
125                    );
126                }
127                if let Some(InternalEvent::PreferencesChanged { updates, .. }) = item.internal {
128                    return Some((Ok(updates), subscription));
129                }
130            }
131        })
132    }
133
134    #[instrument(level = "trace", skip_all)]
135    fn stream_message_deletions(self) -> impl Stream<Item = Result<DecodedMessage>> {
136        futures::stream::unfold(self, |subscription| async move {
137            loop {
138                let item = subscription.next().await?;
139                if let Some(xmtp_events::ClientEvent::Lagged(lagged)) = &item.client {
140                    tracing::warn!(
141                        discarded = lagged.discarded,
142                        "legacy deletion stream lagged"
143                    );
144                }
145                let decoded = match item.internal {
146                    // An item can wait in the broadcast buffer, for example
147                    // behind a slow callback, until after its message expired.
148                    // Check the expiry again at delivery.
149                    Some(InternalEvent::MessagesDeleted(deleted)) => {
150                        let now = xmtp_common::time::now_ns();
151                        deletion_items(deleted, |m| m.expire_at_ns.is_some_and(|at| at <= now))
152                    }
153                    Some(InternalEvent::MessagesExpired(deleted)) => {
154                        deletion_items(deleted, |_| true)
155                    }
156                    _ => continue,
157                };
158                return Some((futures::stream::iter(decoded), subscription));
159            }
160        })
161        .flatten()
162    }
163}
164
165/// Deletion stream items for `deleted`. A row for which `is_expired` holds
166/// is the deleted-message placeholder, with no body and no fallback. Other
167/// rows keep their body; content that does not decode stays in the body as
168/// undecodable content, so every row produces an item.
169// implements: META-051
170fn deletion_items(
171    deleted: internal::DeletedMessages,
172    is_expired: impl Fn(&StoredGroupMessage) -> bool,
173) -> Vec<Result<DecodedMessage>> {
174    let internal::DeletedMessages {
175        messages,
176        deleted_by_inbox_id,
177    } = deleted;
178    messages
179        .into_iter()
180        .map(|message| {
181            if !is_expired(&message) {
182                return Ok(DecodedMessage::from(message));
183            }
184            // With no actor, `Sender`; see `DecodedMessage::placeholder`.
185            let deleted_by = deleted_by_inbox_id
186                .as_deref()
187                .map_or(DeletedBy::Sender, |deleter| {
188                    DeletedBy::new(deleter, &message.sender_inbox_id)
189                });
190            Ok(DecodedMessage::placeholder(message, deleted_by))
191        })
192        .collect()
193}
194
195#[derive(thiserror::Error, Debug, ErrorCode)]
196pub enum SubscribeError {
197    /// Local message delivery failed. May be retryable for a storage failure.
198    #[error(transparent)]
199    #[error_code(inherit)]
200    LocalDelivery(#[from] local_delivery::LocalDeliveryError),
201    /// The shared native transport failed. May be retryable.
202    #[cfg(not(target_arch = "wasm32"))]
203    #[error(transparent)]
204    Transport(#[from] xmtp_api_backend::TransportError),
205    /// Group error.
206    ///
207    /// Group operation failed during subscription. May be retryable.
208    #[error(transparent)]
209    Group(#[from] Box<GroupError>),
210    /// Not found.
211    ///
212    /// Subscribed resource not found. Retryable.
213    #[error(transparent)]
214    NotFound(#[from] NotFound),
215    /// Group message not found.
216    ///
217    /// Expected message missing from database. Retryable.
218    // TODO: Add this to `NotFound`
219    #[error("group message expected in database but is missing")]
220    GroupMessageNotFound,
221    /// Receive group error.
222    ///
223    /// Processing streamed group message failed. May be retryable.
224    #[error("processing group message in stream: {0}")]
225    ReceiveGroup(#[from] Box<GroupMessageProcessingError>),
226    /// Storage error.
227    ///
228    /// Database operation failed. May be retryable.
229    #[error(transparent)]
230    Storage(#[from] StorageError),
231    /// Decode error.
232    ///
233    /// Protobuf decoding failed. Not retryable.
234    #[error(transparent)]
235    Decode(#[from] prost::DecodeError),
236    /// API client error.
237    ///
238    /// Network request failed. Retryable.
239    #[error(transparent)]
240    ApiClient(#[from] xmtp_api::ApiError),
241    /// Boxed error.
242    ///
243    /// Wrapped dynamic error. May be retryable.
244    #[error("{0}")]
245    BoxError(Box<dyn RetryableError>),
246    /// Database connection error.
247    ///
248    /// Database connection failed. Retryable.
249    #[error(transparent)]
250    Db(#[from] xmtp_db::ConnectionError),
251    /// Conversion error.
252    ///
253    /// Proto conversion failed. Not retryable.
254    #[error(transparent)]
255    Conversion(#[from] xmtp_proto::ConversionError),
256    /// Envelope error.
257    ///
258    /// Invalid backend envelope. Not retryable.
259    #[error(transparent)]
260    Envelope(#[from] xmtp_api_backend::envelope::EnvelopeError),
261    /// Enriched Message Error.
262    #[error("error occured during subscription {0}")]
263    Enriched(#[from] EnrichMessageError),
264    /// The connection is blocked by a fatal configuration failure.
265    ///
266    /// Either this database is bound to a different deployment or the
267    /// deployment now requires a newer client. Every open stream is
268    /// closed with it and every later call fails with it. Not retryable.
269    #[error(transparent)]
270    #[error_code(inherit)]
271    Blocked(#[from] crate::server_configuration::BlockedConnection),
272    /// Stream liveness watchdog tripped.
273    ///
274    /// No activity arrived within the idle timeout, so the stream was terminated to force
275    /// a reconnect (resuming from the persisted cursor). Retryable.
276    #[error("stream went stale: no activity within the idle timeout")]
277    StreamStale,
278}
279
280impl SubscribeError {
281    pub fn dyn_err(other: impl RetryableError + 'static) -> Self {
282        SubscribeError::BoxError(Box::new(other) as _)
283    }
284}
285
286impl From<GroupError> for SubscribeError {
287    fn from(value: GroupError) -> Self {
288        SubscribeError::Group(Box::new(value))
289    }
290}
291
292impl From<GroupMessageProcessingError> for SubscribeError {
293    fn from(value: GroupMessageProcessingError) -> Self {
294        SubscribeError::ReceiveGroup(Box::new(value))
295    }
296}
297
298impl RetryableError for SubscribeError {
299    fn is_retryable(&self) -> bool {
300        use SubscribeError::*;
301        match self {
302            #[cfg(not(target_arch = "wasm32"))]
303            Transport(error) => error.is_retryable(),
304            LocalDelivery(e) => retryable!(e),
305            Group(e) => retryable!(e),
306            GroupMessageNotFound => true,
307            ReceiveGroup(e) => retryable!(e),
308            Storage(e) => retryable!(e),
309            Decode(_) => false,
310            NotFound(e) => retryable!(e),
311            ApiClient(e) => retryable!(e),
312            BoxError(e) => retryable!(e),
313            Db(c) => retryable!(c),
314            Conversion(c) => retryable!(c),
315            Envelope(c) => retryable!(c),
316            Enriched(c) => retryable!(c),
317            Blocked(_) => false,
318            StreamStale => true,
319        }
320    }
321
322    fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
323        use SubscribeError::*;
324        match self {
325            #[cfg(not(target_arch = "wasm32"))]
326            Transport(error) => error.rate_limit_delay(),
327            Group(e) => e.rate_limit_delay(),
328            ReceiveGroup(e) => e.rate_limit_delay(),
329            ApiClient(e) => e.rate_limit_delay(),
330            LocalDelivery(e) => e.rate_limit_delay(),
331            BoxError(e) => e.rate_limit_delay(),
332            _ => None,
333        }
334    }
335}
336
337impl crate::worker::NeedsDbReconnect for SubscribeError {
338    /// Forwards a dropped-pool signal so the device-sync worker stops on
339    /// disconnect. `BoxError` wraps an opaque error we can't introspect → `false`.
340    #[deny(clippy::wildcard_enum_match_arm)]
341    fn needs_db_reconnect(&self) -> bool {
342        use SubscribeError::*;
343        match self {
344            Group(e) => e.needs_db_reconnect(),
345            Storage(e) => e.db_needs_connection(),
346            LocalDelivery(e) => e.needs_db_reconnect(),
347            Db(c) => c.db_needs_connection(),
348            ReceiveGroup(e) => e.needs_db_reconnect(),
349            Enriched(EnrichMessageError::Storage(e)) => e.db_needs_connection(),
350            Enriched(EnrichMessageError::DbConnection(c)) => c.db_needs_connection(),
351            #[cfg(not(target_arch = "wasm32"))]
352            Transport(..) => false,
353            NotFound(..) | GroupMessageNotFound | Decode(..) | ApiClient(..) | BoxError(..)
354            | Conversion(..) | Envelope(..) | Blocked(..) | StreamStale => false,
355        }
356    }
357}
358
359/// Use push metadata as a target. Fetch its complete prefix before processing.
360pub(crate) async fn process_streamed_welcome_message<Context>(
361    context: &Context,
362    envelope_bytes: Vec<u8>,
363) -> Result<Vec<MlsGroup<Context>>>
364where
365    Context: XmtpSharedContext + 'static,
366{
367    let wire = ServerEnvelope::decode(envelope_bytes.as_slice())?;
368    let meta = wire
369        .meta
370        .as_ref()
371        .ok_or(xmtp_api::ApiError::InvalidResponse("welcome metadata"))?;
372    let (topic, cursor, _) = xmtp_api_backend::envelope::metadata(
373        meta,
374        xmtp_proto::types::TopicKind::WelcomeMessagesV1,
375    )?;
376    if topic != xmtp_proto::types::Topic::new_welcome_message(context.installation_id()) {
377        return Err(xmtp_api::ApiError::InvalidResponse("welcome installation").into());
378    }
379    barrier::wait_through(context, [(topic, cursor)].into(), None)
380        .await
381        .map_err(|error| {
382            context
383                .server_configuration()
384                .blocked_or(GroupError::from(error))
385        })?;
386    let Some(group) = context.db().find_group_by_sequence_id(cursor)? else {
387        return Ok(Vec::new());
388    };
389    Ok(vec![MlsGroup::new(
390        context.clone(),
391        group.id,
392        group.dm_id,
393        group.conversation_type,
394        group.created_at_ns,
395    )])
396}
397
398/// Use a push envelope for `group` only as a target for ordered receipt and processing.
399pub async fn process_streamed_group_message<Context>(
400    group: &MlsGroup<Context>,
401    envelope_bytes: Vec<u8>,
402) -> Result<Vec<StoredGroupMessage>>
403where
404    Context: XmtpSharedContext + 'static,
405{
406    let wire = ServerEnvelope::decode(envelope_bytes.as_slice())?;
407    let meta = wire
408        .meta
409        .as_ref()
410        .ok_or(xmtp_api::ApiError::InvalidResponse("group metadata"))?;
411    let (topic, cursor, _) =
412        xmtp_api_backend::envelope::metadata(meta, xmtp_proto::types::TopicKind::GroupMessagesV1)?;
413    if topic != xmtp_proto::types::Topic::new_group_message(group.group_id) {
414        return Err(xmtp_api::ApiError::InvalidResponse("group topic").into());
415    }
416    barrier::wait_through(&group.context, [(topic, cursor)].into(), None)
417        .await
418        .map_err(|error| {
419            group
420                .context
421                .server_configuration()
422                .blocked_or(GroupError::from(error))
423        })?;
424    let message = group
425        .context
426        .db()
427        .get_group_message_by_cursor(group.group_id, cursor)?;
428    // implements: META-051
429    let current_time_ns = xmtp_common::time::now_ns();
430    Ok(message
431        .filter(|row| {
432            row.expire_at_ns
433                .is_none_or(|deadline| deadline > current_time_ns)
434        })
435        .into_iter()
436        .collect())
437}
438
439#[xmtp_common::span(prefix = "stream")]
440pub(crate) async fn stream_conversations<'a, Context>(
441    context: &'a Context,
442    conversation_type: Option<ConversationType>,
443    include_duplicate_dms: bool,
444) -> Result<impl Stream<Item = Result<MlsGroup<Context>>> + use<'a, Context>>
445where
446    Context: XmtpSharedContext + 'static,
447    Context::ApiClient: XmtpMlsStreams,
448{
449    StreamConversations::new(context, conversation_type, include_duplicate_dms, None).await
450}
451
452/// Stream conversations but decouple the lifetime of the caller from the stream.
453#[xmtp_common::span(prefix = "stream")]
454pub(crate) async fn stream_conversations_owned<Context>(
455    context: Context,
456    conversation_type: Option<ConversationType>,
457    include_duplicate_dms: bool,
458) -> Result<impl Stream<Item = Result<MlsGroup<Context>>> + 'static + use<Context>>
459where
460    Context: XmtpSharedContext + 'static,
461    Context::ApiClient: XmtpMlsStreams,
462{
463    StreamConversations::new_owned(context, conversation_type, include_duplicate_dms, None).await
464}
465
466/// `keep_alive` lives as long as the subscribe closure.
467pub(crate) fn stream_conversations_with_callback<Context>(
468    context: Context,
469    keep_alive: impl MaybeSend + 'static,
470    conversation_type: Option<ConversationType>,
471    convo_callback: impl FnMut(Result<MlsGroup<Context>>) + MaybeSend + 'static,
472    on_close: impl FnOnce() + MaybeSend + 'static,
473    include_duplicate_dms: bool,
474) -> impl StreamHandle<StreamOutput = Result<()>>
475where
476    Context: XmtpSharedContext + 'static,
477    Context::ApiClient: XmtpMlsStreams + 'static,
478    Context::MlsStorage: 'static,
479{
480    let cancel = watchdog::StreamCancel::new(&context);
481    // Re-subscribing recreates the underlying local event subscription, which
482    // has no replay; the watchdog runner establishes the new subscription *before* its
483    // reconnect wait, so the new receiver is attached while we pause. Network welcomes
484    // are caught up from the persisted cursor, so the only residual gap is a *locally*
485    // created group event in the brief window while the new
486    // subscription is being built — bounded, since the caller already holds that group.
487    watchdog::spawn_watchdog_stream(
488        cancel,
489        "stream_conversations",
490        move || {
491            let _keep_alive = &keep_alive;
492            stream_conversations_owned(context.clone(), conversation_type, include_duplicate_dms)
493        },
494        convo_callback,
495        on_close,
496    )
497}
498
499#[xmtp_common::span(prefix = "stream")]
500pub(crate) async fn stream_all_messages<'a, Context>(
501    context: &'a Context,
502    conversation_type: Option<ConversationType>,
503    consent_state: Option<Vec<ConsentState>>,
504) -> Result<impl Stream<Item = Result<StoredGroupMessage>> + use<'a, Context>>
505where
506    Context: XmtpSharedContext + 'static,
507    Context::ApiClient: XmtpMlsStreams + 'static,
508    Context::MlsStorage: 'static,
509{
510    tracing::debug!(
511        inbox_id = context.inbox_id(),
512        installation_id = context.installation_id().as_slice().snippet(),
513        conversation_type = ?conversation_type,
514        "stream all messages"
515    );
516
517    StreamAllMessages::new(context, conversation_type, consent_state).await
518}
519
520#[xmtp_common::span(prefix = "stream")]
521pub(crate) async fn stream_all_messages_owned<Context>(
522    context: Context,
523    conversation_type: Option<ConversationType>,
524    consent_state: Option<Vec<ConsentState>>,
525) -> Result<impl Stream<Item = Result<StoredGroupMessage>> + 'static + use<Context>>
526where
527    Context: XmtpSharedContext + 'static,
528    Context::ApiClient: XmtpMlsStreams + 'static,
529    Context::MlsStorage: 'static,
530{
531    tracing::debug!(
532        inbox_id = context.inbox_id(),
533        installation_id = context.installation_id().as_slice().snippet(),
534        conversation_type = ?conversation_type,
535        "stream all messages"
536    );
537
538    StreamAllMessages::new_owned(context, conversation_type, consent_state).await
539}
540
541/// `keep_alive` lives as long as the stream task.
542pub(crate) fn stream_consent_with_callback<Context>(
543    context: Context,
544    keep_alive: impl MaybeSend + 'static,
545    mut callback: impl FnMut(Result<Vec<StoredConsentRecord>>) + MaybeSend + 'static,
546    on_close: impl FnOnce() + MaybeSend + 'static,
547) -> impl StreamHandle<StreamOutput = Result<()>>
548where
549    Context: XmtpSharedContext + 'static,
550{
551    let (tx, rx) = oneshot::channel();
552
553    xmtp_common::spawn(
554        Some(rx),
555        xmtp_common::bind_task_hub(async move {
556            let _keep_alive = keep_alive;
557            // Cancellation can carry a blocked connection cause,
558            // and this stream closes with it rather than silently.
559            let cancel = watchdog::StreamCancel::new(&context);
560            let receiver = context.events().subscribe(
561                EventFilter::default().with_internal(|event| {
562                    matches!(event, InternalEvent::PreferencesChanged { .. })
563                }),
564                Some(1024),
565            );
566            let stream = receiver.stream_consent_updates();
567
568            futures::pin_mut!(stream);
569            let _ = tx.send(());
570            let cancelled = loop {
571                tokio::select! {
572                    _ = cancel.cancelled() => break true,
573                    next = stream.next() => match next {
574                        Some(message) => callback(message),
575                        None => break false,
576                    }
577                }
578            };
579            tracing::debug!("`stream_consent` stream ended, dropping stream");
580            let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
581            on_close();
582            result
583        }),
584    )
585}
586
587/// `keep_alive` lives as long as the stream task.
588pub(crate) fn stream_preferences_with_callback<Context>(
589    context: Context,
590    keep_alive: impl MaybeSend + 'static,
591    mut callback: impl FnMut(Result<Vec<PreferenceUpdate>>) + MaybeSend + 'static,
592    on_close: impl FnOnce() + MaybeSend + 'static,
593) -> impl StreamHandle<StreamOutput = Result<()>>
594where
595    Context: XmtpSharedContext + 'static,
596{
597    let (tx, rx) = oneshot::channel();
598
599    xmtp_common::spawn(
600        Some(rx),
601        xmtp_common::bind_task_hub(async move {
602            let _keep_alive = keep_alive;
603            // Cancellation can carry a blocked connection cause,
604            // and this stream closes with it rather than silently.
605            let cancel = watchdog::StreamCancel::new(&context);
606            let receiver = context.events().subscribe(
607                EventFilter::default().with_internal(|event| {
608                    matches!(event, InternalEvent::PreferencesChanged { .. })
609                }),
610                Some(1024),
611            );
612            let stream = receiver.stream_preference_updates();
613
614            futures::pin_mut!(stream);
615            let _ = tx.send(());
616            let cancelled = loop {
617                tokio::select! {
618                    _ = cancel.cancelled() => break true,
619                    next = stream.next() => match next {
620                        Some(message) => callback(message),
621                        None => break false,
622                    }
623                }
624            };
625            tracing::debug!("`stream_preferences` stream ended, dropping stream");
626            let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
627            on_close();
628            result
629        }),
630    )
631}
632
633/// `keep_alive` lives as long as the stream task.
634pub(crate) fn stream_message_deletions_with_callback<Context>(
635    context: Context,
636    keep_alive: impl MaybeSend + 'static,
637    mut callback: impl FnMut(Result<DecodedMessage>) + MaybeSend + 'static,
638    on_close: impl FnOnce() + MaybeSend + 'static,
639) -> impl StreamHandle<StreamOutput = Result<()>>
640where
641    Context: XmtpSharedContext + 'static,
642{
643    let (tx, rx) = oneshot::channel();
644
645    xmtp_common::spawn(
646        Some(rx),
647        xmtp_common::bind_task_hub(async move {
648            let _keep_alive = keep_alive;
649            // Cancellation can carry a blocked connection cause,
650            // and this stream closes with it rather than silently.
651            let cancel = watchdog::StreamCancel::new(&context);
652            let receiver = context.events().subscribe(
653                EventFilter::default().with_internal(InternalEvent::is_message_deletion),
654                Some(1024),
655            );
656            let stream = receiver.stream_message_deletions();
657
658            futures::pin_mut!(stream);
659            let _ = tx.send(());
660            let cancelled = loop {
661                tokio::select! {
662                    _ = cancel.cancelled() => break true,
663                    next = stream.next() => match next {
664                        Some(message) => callback(message),
665                        None => break false,
666                    }
667                }
668            };
669            tracing::debug!("`stream_message_deletions` stream ended, dropping stream");
670            let result = watchdog::close_reason(&cancel, cancelled, &mut callback);
671            on_close();
672            result
673        }),
674    )
675}
676
677#[tracing::instrument(level = "trace", skip_all)]
678#[cfg(any(test, feature = "test-utils"))]
679pub(crate) async fn stream_all_messages_owned_with_stats<Context>(
680    context: Context,
681    conversation_type: Option<ConversationType>,
682    consent_state: Option<Vec<ConsentState>>,
683) -> Result<impl StreamWithStats<Item = Result<StoredGroupMessage>> + 'static>
684where
685    Context: XmtpSharedContext + 'static,
686    Context::ApiClient: XmtpMlsStreams + 'static,
687    Context::MlsStorage: 'static,
688    <Context::ApiClient as XmtpMlsStreams>::GroupMessageStream: Unpin,
689    <Context::ApiClient as XmtpMlsStreams>::WelcomeMessageStream: Unpin,
690{
691    tracing::debug!(
692        inbox_id = context.inbox_id(),
693        installation_id = context.installation_id().as_slice().snippet(),
694        conversation_type = ?conversation_type,
695        "stream all messages"
696    );
697
698    let stream = StreamAllMessages::new_owned(context, conversation_type, consent_state).await?;
699
700    Ok(StreamStatsWrapper::new(stream))
701}
702
703#[derive(Debug, Clone, Copy)]
704pub enum StreamKind {
705    All,
706    Conversations,
707    Messages,
708}
709
710#[cfg(test)]
711mod message_deletion_tests;
712
713#[cfg(test)]
714pub(crate) mod tests {
715    #[cfg(not(target_arch = "wasm32"))]
716    mod disconnect_propagation;
717
718    use crate::context::CoreContext;
719    use crate::tester;
720
721    /// A macro for asserting that a stream yields a specific decrypted message.
722    ///
723    /// # Example
724    /// ```rust
725    /// assert_msg!(stream, b"first");
726    /// ```
727    #[macro_export]
728    macro_rules! assert_msg {
729        ($stream:expr, $expected:expr) => {
730            let next = $stream
731                .next()
732                .await
733                .unwrap()
734                .inspect_err(|e| tracing::error!("{}", e.to_string()))
735                .unwrap();
736
737            assert_eq!(
738                String::from_utf8_lossy(next.decrypted_message_bytes.as_slice()),
739                String::from_utf8_lossy($expected.as_bytes())
740            );
741        };
742    }
743
744    /// A macro for asserting that a stream yields a specific decrypted message.
745    ///
746    /// # Example
747    /// ```rust
748    /// assert_msg!(stream, b"first");
749    /// ```
750    #[macro_export]
751    macro_rules! assert_msg_exists {
752        ($stream:expr) => {
753            assert!(
754                !$stream
755                    .next()
756                    .await
757                    .unwrap()
758                    .unwrap()
759                    .decrypted_message_bytes
760                    .is_empty()
761            );
762        };
763    }
764
765    #[xmtp_common::test(flavor = "multi_thread", worker_threads = 5, unwrap_try = true)]
766    async fn test_process_streamed_welcome_message() {
767        use prost::Message;
768        use xmtp_proto::types::{Cursor, Topic};
769        tester!(alix);
770        tester!(bo);
771        let alix_group = alix.create_group(None, None)?;
772        alix_group.add_members(&[bo.inbox_id()]).await?;
773        let envelopes = alix
774            .context
775            .api()
776            .query_all(
777                [(
778                    Topic::new_welcome_message(bo.context.installation_id()),
779                    Cursor(0),
780                )]
781                .into(),
782                bo.context.api().limits().max_query_limit as u32,
783            )
784            .await?;
785        assert!(!envelopes.is_empty(), "Should have at least one welcome");
786        let groups = bo
787            .process_streamed_welcome_message(envelopes[0].encode_to_vec())
788            .await?;
789        assert_eq!(groups.len(), 1, "Should have exactly one group");
790    }
791}