1use std::collections::HashMap;
9use std::sync::{Arc, LazyLock};
10
11use parking_lot::Mutex;
12
13use tokio::sync::oneshot;
14use tokio_util::sync::CancellationToken;
15
16use xmtp_api_backend::{BackendBinding, BidiConnection, BidiTransport, OpenError, TransportError};
17use xmtp_common::{MaybeSend, StreamHandle};
18use xmtp_db::consent_record::ConsentState;
19use xmtp_db::group::ConversationType;
20use xmtp_db::group_message::StoredGroupMessage;
21use xmtp_proto::api_client::{XmtpMlsBidiStreams, XmtpMlsStreams};
22use xmtp_proto::types::{GroupId, InstallationId};
23
24use xmtp_common::Event;
25use xmtp_macro::log_event;
26
27use super::{Result, StreamKind, SubscribeError};
28use crate::Client;
29use crate::context::XmtpSharedContext;
30use crate::groups::MlsGroup;
31
32pub trait ApiClientIdentity {
34 fn api_client_identity(&self) -> usize;
35}
36
37impl<C: ?Sized> ApiClientIdentity for Arc<C> {
38 fn api_client_identity(&self) -> usize {
39 Arc::as_ptr(self).cast::<()>() as usize
40 }
41}
42
43type WireKey = (String, usize);
44
45struct SharedWires {
46 transports: HashMap<WireKey, BidiTransport<BackendBinding>>,
47 suspend_requested: bool,
48}
49
50static SHARED_WIRES: LazyLock<Mutex<SharedWires>> = LazyLock::new(|| {
51 Mutex::new(SharedWires {
52 transports: HashMap::new(),
53 suspend_requested: false,
54 })
55});
56
57pub(crate) fn bidi_streams_suspended() -> bool {
59 SHARED_WIRES.lock().suspend_requested
60}
61
62#[cfg(test)]
63pub(crate) fn shared_transport_count() -> usize {
64 SHARED_WIRES.lock().transports.len()
65}
66
67pub(crate) fn shared_transport<C>(api: C) -> BidiTransport<BackendBinding>
76where
77 C: XmtpMlsBidiStreams + ApiClientIdentity + Clone + Send + Sync + 'static,
78 C::SubscribeStream: 'static,
79{
80 let key = (api.host().to_owned(), api.api_client_identity());
81 let mutate = xmtp_api_backend::MutateLimits::from_limits(&api.bidi_limits());
86 let mut wires = SHARED_WIRES.lock();
87 let born_suspended = wires.suspend_requested;
90 wires
91 .transports
92 .entry(key)
93 .or_insert_with_key(|(host, _)| {
94 tracing::info!(
97 %host,
98 suspended = born_suspended,
99 "bidi: initializing the shared transport for a destination"
100 );
101 BidiTransport::new_within(
102 move |initial| {
103 let api = api.clone();
104 async move {
105 BidiConnection::open(&api, initial)
106 .await
107 .map_err(OpenError::new)
108 }
109 },
110 born_suspended,
111 mutate,
112 )
113 })
114 .clone()
115}
116
117pub async fn suspend_bidi_streams() -> Result<()> {
119 let (wires, pending): (Vec<_>, Vec<_>) = {
125 let mut shared = SHARED_WIRES.lock();
126 shared.suspend_requested = true;
127 shared
128 .transports
129 .iter()
130 .map(|(host, t)| ((host.clone(), t.clone()), t.enqueue_suspend()))
131 .unzip()
132 };
133 settle_lifecycle(&wires, await_lifecycle_acks(pending).await)
134}
135
136pub async fn resume_bidi_streams() -> Result<()> {
138 let mut shared = SHARED_WIRES.lock();
144 shared.suspend_requested = false;
145 for t in shared.transports.values() {
146 let _ = t.enqueue_resume();
147 }
148 Ok(())
149}
150
151async fn await_lifecycle_acks(
156 pending: Vec<std::result::Result<tokio::sync::oneshot::Receiver<()>, TransportError>>,
157) -> Vec<std::result::Result<(), TransportError>> {
158 futures::future::join_all(pending.into_iter().map(|reply| async move {
159 match reply {
160 Ok(rx) => rx.await.map_err(|_| TransportError::Closed),
161 Err(e) => Err(e),
162 }
163 }))
164 .await
165}
166
167fn settle_lifecycle(
169 wires: &[(WireKey, BidiTransport<BackendBinding>)],
170 results: Vec<std::result::Result<(), TransportError>>,
171) -> Result<()> {
172 let mut first_error = None;
173 for (_, result) in wires.iter().zip(results) {
174 match result {
175 Ok(()) => {}
176 Err(TransportError::Closed) => {}
177 Err(e) => first_error = first_error.or(Some(e)),
178 }
179 }
180 match first_error {
181 None => Ok(()),
182 Some(e) => Err(SubscribeError::Transport(e)),
183 }
184}
185
186struct StreamClosedGuard<F: FnOnce()> {
188 kind: StreamKind,
189 installation: InstallationId,
190 on_close: Option<F>,
191}
192
193impl<F: FnOnce()> Drop for StreamClosedGuard<F> {
194 fn drop(&mut self) {
195 log_event!(Event::StreamClosed, self.installation, kind = ?self.kind);
196 if let Some(on_close) = self.on_close.take() {
197 on_close();
198 }
199 }
200}
201
202pub(crate) struct StreamOrigin {
203 pub(crate) kind: StreamKind,
204 pub(crate) installation: InstallationId,
205 pub(crate) cancel: CancellationToken,
206}
207
208impl StreamOrigin {
209 fn new<Context: XmtpSharedContext>(kind: StreamKind, context: &Context) -> Self {
210 Self {
211 kind,
212 installation: context.installation_id(),
213 cancel: context.cancellation_token().clone(),
214 }
215 }
216}
217
218pub(crate) fn pump_stream<T, S, St>(
220 origin: StreamOrigin,
221 subscribe: S,
222 mut callback: impl FnMut(Result<T>) + MaybeSend + 'static,
223 on_close: impl FnOnce() + MaybeSend + 'static,
224) -> impl StreamHandle<StreamOutput = Result<()>>
225where
226 T: MaybeSend + 'static,
227 St: futures::Stream<Item = Result<T>> + MaybeSend + Unpin + 'static,
228 S: Future<Output = Result<St>> + MaybeSend + 'static,
229{
230 use futures::StreamExt;
231 let (tx, rx) = oneshot::channel();
232 let task = async move {
233 let StreamOrigin {
234 kind,
235 installation,
236 cancel,
237 } = origin;
238 log_event!(Event::StreamOpened, installation, kind = ?kind);
239 let _closed = StreamClosedGuard {
240 kind,
241 installation,
242 on_close: Some(on_close),
243 };
244 let mut stream = tokio::select! {
245 _ = cancel.cancelled() => return Ok(()),
246 result = subscribe => result?,
247 };
248 let _ = tx.send(());
249 loop {
250 tokio::select! {
251 _ = cancel.cancelled() => break,
252 next = stream.next() => match next {
253 Some(item) => callback(item),
254 None => break,
255 }
256 }
257 }
258 Ok(())
259 };
260 xmtp_common::spawn(Some(rx), xmtp_common::bind_task_hub(task))
261}
262
263impl<C> Client<C>
264where
265 C: XmtpSharedContext + 'static,
266 C::ApiClient:
267 XmtpMlsBidiStreams + XmtpMlsStreams + ApiClientIdentity + Clone + Send + Sync + 'static,
268 <C::ApiClient as XmtpMlsBidiStreams>::SubscribeStream: 'static,
269{
270 pub fn stream_all_messages_with_callback_dispatch(
271 client: Arc<Client<C>>,
272 conversation_type: Option<ConversationType>,
273 consent_states: Option<Vec<ConsentState>>,
274 callback: impl FnMut(Result<StoredGroupMessage>) + MaybeSend + 'static,
275 on_close: impl FnOnce() + MaybeSend + 'static,
276 ) -> impl StreamHandle<StreamOutput = Result<()>> {
277 let origin = StreamOrigin::new(StreamKind::All, &client.context);
278 let subscribe = async move {
279 super::stream_all::StreamAllMessages::new_owned(
280 client.context.clone(),
281 conversation_type,
282 consent_states,
283 )
284 .await
285 };
286 pump_stream(origin, subscribe, callback, on_close)
287 }
288
289 pub fn stream_conversations_with_callback_dispatch(
290 client: Arc<Client<C>>,
291 conversation_type: Option<ConversationType>,
292 include_duplicate_dms: bool,
293 callback: impl FnMut(Result<MlsGroup<C>>) + MaybeSend + 'static,
294 on_close: impl FnOnce() + MaybeSend + 'static,
295 ) -> impl StreamHandle<StreamOutput = Result<()>> {
296 let origin = StreamOrigin::new(StreamKind::Conversations, &client.context);
297 let subscribe = async move {
298 super::stream_conversations::StreamConversations::new_owned(
299 client.context.clone(),
300 conversation_type,
301 include_duplicate_dms,
302 None,
303 )
304 .await
305 };
306 pump_stream(origin, subscribe, callback, on_close)
307 }
308}
309
310pub fn stream_conversation_messages_with_callback_dispatch<C>(
311 context: C,
312 group_id: GroupId,
313 callback: impl FnMut(Result<StoredGroupMessage>) + MaybeSend + 'static,
314 on_close: impl FnOnce() + MaybeSend + 'static,
315) -> impl StreamHandle<StreamOutput = Result<()>>
316where
317 C: XmtpSharedContext + 'static,
318 C::ApiClient:
319 XmtpMlsBidiStreams + XmtpMlsStreams + ApiClientIdentity + Clone + Send + Sync + 'static,
320 <C::ApiClient as XmtpMlsBidiStreams>::SubscribeStream: 'static,
321{
322 let origin = StreamOrigin::new(StreamKind::Messages, &context);
323 let subscribe = async move {
324 super::stream_messages::StreamGroupMessages::new_owned(context, vec![group_id]).await
325 };
326 pump_stream(origin, subscribe, callback, on_close)
327}