1use std::collections::HashMap;
10use std::sync::{Arc, LazyLock, Weak};
11
12use parking_lot::Mutex;
13
14use tokio::sync::oneshot;
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::{
28 Result, StreamKind, SubscribeError,
29 watchdog::{StreamCancel, close_reason},
30};
31use crate::Client;
32use crate::context::XmtpSharedContext;
33use crate::groups::MlsGroup;
34
35pub trait ApiClientIdentity {
37 fn api_client_identity(&self) -> usize;
38}
39
40impl<C: ?Sized> ApiClientIdentity for Arc<C> {
41 fn api_client_identity(&self) -> usize {
42 Arc::as_ptr(self).cast::<()>() as usize
43 }
44}
45
46type WireKey = (String, usize);
47
48struct SharedWires {
49 transports: HashMap<WireKey, Weak<BidiTransport<BackendBinding>>>,
50 suspend_requested: bool,
51}
52
53static SHARED_WIRES: LazyLock<Mutex<SharedWires>> = LazyLock::new(|| {
54 Mutex::new(SharedWires {
55 transports: HashMap::new(),
56 suspend_requested: false,
57 })
58});
59
60pub(crate) fn bidi_streams_suspended() -> bool {
62 SHARED_WIRES.lock().suspend_requested
63}
64
65#[cfg(test)]
66pub(crate) fn shared_transport_count() -> usize {
67 let mut wires = SHARED_WIRES.lock();
68 wires
69 .transports
70 .retain(|_, transport| transport.strong_count() > 0);
71 wires.transports.len()
72}
73
74pub(crate) fn shared_transport<C>(api: C) -> Arc<BidiTransport<BackendBinding>>
83where
84 C: XmtpMlsBidiStreams + ApiClientIdentity + Clone + Send + Sync + 'static,
85 C::SubscribeStream: 'static,
86{
87 let key = (api.host().to_owned(), api.api_client_identity());
88 let mutate = xmtp_api_backend::MutateLimits::from_limits(&api.bidi_limits());
93 let mut wires = SHARED_WIRES.lock();
94 let born_suspended = wires.suspend_requested;
97 wires
98 .transports
99 .retain(|_, transport| transport.strong_count() > 0);
100 if let Some(transport) = wires.transports.get(&key).and_then(Weak::upgrade) {
101 return transport;
102 }
103 tracing::info!(
104 host = %key.0,
105 suspended = born_suspended,
106 "bidi: initializing the shared transport for a destination"
107 );
108 let transport = Arc::new(BidiTransport::new_within(
109 move |initial| {
110 let api = api.clone();
111 async move {
112 BidiConnection::open(&api, initial)
113 .await
114 .map_err(OpenError::new)
115 }
116 },
117 born_suspended,
118 mutate,
119 ));
120 wires.transports.insert(key, Arc::downgrade(&transport));
121 transport
122}
123
124pub async fn suspend_bidi_streams() -> Result<()> {
126 let (wires, pending): (Vec<_>, Vec<_>) = {
132 let mut shared = SHARED_WIRES.lock();
133 shared.suspend_requested = true;
134 shared
135 .transports
136 .iter()
137 .filter_map(|(host, weak)| {
138 weak.upgrade().map(|transport| {
139 let pending = transport.enqueue_suspend();
140 ((host.clone(), transport), pending)
141 })
142 })
143 .unzip()
144 };
145 settle_lifecycle(&wires, await_lifecycle_acks(pending).await)
146}
147
148pub async fn resume_bidi_streams() -> Result<()> {
150 let mut shared = SHARED_WIRES.lock();
156 shared.suspend_requested = false;
157 shared.transports.retain(|_, weak| {
158 if let Some(transport) = weak.upgrade() {
159 let _ = transport.enqueue_resume();
160 true
161 } else {
162 false
163 }
164 });
165 Ok(())
166}
167
168async fn await_lifecycle_acks(
173 pending: Vec<std::result::Result<tokio::sync::oneshot::Receiver<()>, TransportError>>,
174) -> Vec<std::result::Result<(), TransportError>> {
175 futures::future::join_all(pending.into_iter().map(|reply| async move {
176 match reply {
177 Ok(rx) => rx.await.map_err(|_| TransportError::Closed),
178 Err(e) => Err(e),
179 }
180 }))
181 .await
182}
183
184fn settle_lifecycle(
186 wires: &[(WireKey, Arc<BidiTransport<BackendBinding>>)],
187 results: Vec<std::result::Result<(), TransportError>>,
188) -> Result<()> {
189 let mut first_error = None;
190 for (_, result) in wires.iter().zip(results) {
191 match result {
192 Ok(()) => {}
193 Err(TransportError::Closed) => {}
194 Err(e) => first_error = first_error.or(Some(e)),
195 }
196 }
197 match first_error {
198 None => Ok(()),
199 Some(e) => Err(SubscribeError::Transport(e)),
200 }
201}
202
203struct StreamClosedGuard<F: FnOnce()> {
205 kind: StreamKind,
206 installation: InstallationId,
207 on_close: Option<F>,
208}
209
210impl<F: FnOnce()> Drop for StreamClosedGuard<F> {
211 fn drop(&mut self) {
212 log_event!(Event::StreamClosed, self.installation, kind = ?self.kind);
213 if let Some(on_close) = self.on_close.take() {
214 on_close();
215 }
216 }
217}
218
219pub(crate) struct StreamOrigin {
220 pub(crate) kind: StreamKind,
221 pub(crate) installation: InstallationId,
222 pub(crate) cancel: StreamCancel,
223}
224
225impl StreamOrigin {
226 fn new<Context: XmtpSharedContext>(kind: StreamKind, context: &Context) -> Self {
227 Self {
228 kind,
229 installation: context.installation_id(),
230 cancel: StreamCancel::new(context),
231 }
232 }
233}
234
235pub(crate) fn pump_stream<T, S, St>(
237 origin: StreamOrigin,
238 subscribe: S,
239 mut callback: impl FnMut(Result<T>) + MaybeSend + 'static,
240 on_close: impl FnOnce() + MaybeSend + 'static,
241) -> impl StreamHandle<StreamOutput = Result<()>>
242where
243 T: MaybeSend + 'static,
244 St: futures::Stream<Item = Result<T>> + MaybeSend + Unpin + 'static,
245 S: Future<Output = Result<St>> + MaybeSend + 'static,
246{
247 use futures::StreamExt;
248 let (tx, rx) = oneshot::channel();
249 let task = async move {
250 let StreamOrigin {
251 kind,
252 installation,
253 cancel,
254 } = origin;
255 log_event!(Event::StreamOpened, installation, kind = ?kind);
256 let _closed = StreamClosedGuard {
257 kind,
258 installation,
259 on_close: Some(on_close),
260 };
261 let mut stream = tokio::select! {
262 biased;
263 _ = cancel.cancelled() => return close_reason(&cancel, true, &mut callback),
264 result = subscribe => match result {
265 Ok(stream) => stream,
266 Err(error) => {
267 close_reason(&cancel, true, &mut callback)?;
268 callback(Err(error));
269 return Ok(());
270 }
271 },
272 };
273 let _ = tx.send(());
274 loop {
275 tokio::select! {
276 biased;
277 _ = cancel.cancelled() => return close_reason(&cancel, true, &mut callback),
278 next = stream.next() => match next {
279 Some(item) => {
280 close_reason(&cancel, true, &mut callback)?;
283 let terminal = item.is_err();
284 callback(item);
285 if terminal {
286 return Ok(());
287 }
288 },
289 None => return close_reason(&cancel, true, &mut callback),
290 }
291 }
292 }
293 };
294 xmtp_common::spawn(Some(rx), xmtp_common::bind_task_hub(task))
295}
296
297impl<C> Client<C>
298where
299 C: XmtpSharedContext + 'static,
300 C::ApiClient:
301 XmtpMlsBidiStreams + XmtpMlsStreams + ApiClientIdentity + Clone + Send + Sync + 'static,
302 <C::ApiClient as XmtpMlsBidiStreams>::SubscribeStream: 'static,
303{
304 pub fn stream_all_messages_with_callback_dispatch(
305 client: Arc<Client<C>>,
306 conversation_type: Option<ConversationType>,
307 consent_states: Option<Vec<ConsentState>>,
308 callback: impl FnMut(Result<StoredGroupMessage>) + MaybeSend + 'static,
309 on_close: impl FnOnce() + MaybeSend + 'static,
310 ) -> impl StreamHandle<StreamOutput = Result<()>> {
311 let origin = StreamOrigin::new(StreamKind::All, &client.context);
312 let subscribe = async move {
313 super::stream_all::StreamAllMessages::new_owned(
314 client.context.clone(),
315 conversation_type,
316 consent_states,
317 )
318 .await
319 };
320 pump_stream(origin, subscribe, callback, on_close)
321 }
322
323 pub fn stream_conversations_with_callback_dispatch(
324 client: Arc<Client<C>>,
325 conversation_type: Option<ConversationType>,
326 include_duplicate_dms: bool,
327 callback: impl FnMut(Result<MlsGroup<C>>) + MaybeSend + 'static,
328 on_close: impl FnOnce() + MaybeSend + 'static,
329 ) -> impl StreamHandle<StreamOutput = Result<()>> {
330 let origin = StreamOrigin::new(StreamKind::Conversations, &client.context);
331 let subscribe = async move {
332 super::stream_conversations::StreamConversations::new_owned(
333 client.context.clone(),
334 conversation_type,
335 include_duplicate_dms,
336 None,
337 )
338 .await
339 };
340 pump_stream(origin, subscribe, callback, on_close)
341 }
342}
343
344pub fn stream_conversation_messages_with_callback_dispatch<C>(
345 context: C,
346 group_id: GroupId,
347 callback: impl FnMut(Result<StoredGroupMessage>) + MaybeSend + 'static,
348 on_close: impl FnOnce() + MaybeSend + 'static,
349) -> impl StreamHandle<StreamOutput = Result<()>>
350where
351 C: XmtpSharedContext + 'static,
352 C::ApiClient:
353 XmtpMlsBidiStreams + XmtpMlsStreams + ApiClientIdentity + Clone + Send + Sync + 'static,
354 <C::ApiClient as XmtpMlsBidiStreams>::SubscribeStream: 'static,
355{
356 let origin = StreamOrigin::new(StreamKind::Messages, &context);
357 let subscribe = async move {
358 super::stream_messages::StreamGroupMessages::new_owned(context, vec![group_id]).await
359 };
360 pump_stream(origin, subscribe, callback, on_close)
361}