1use crate::queries::bidi::{BidiBinding, Connection, Event, Inbound};
4use crate::queries::bidi_transport::TransportBinding;
5use xmtp_proto::api_client::XmtpMlsBidiStreams;
6use xmtp_proto::backend_v1::{
7 self, Ping, Pong, ServerEnvelope, SubscribeRequest, SubscribeResponse, subscribe_request,
8 subscribe_request::Update, subscribe_response,
9};
10use xmtp_proto::types::{Topic, TopicKind};
11
12pub struct BackendBinding;
14
15pub type BidiConnection = Connection<BackendBinding>;
16pub type BidiEvent = Event<ServerEnvelope, ServerEnvelope>;
17
18fn request_frame(request: subscribe_request::Request) -> SubscribeRequest {
19 SubscribeRequest {
20 request: Some(request),
21 }
22}
23
24fn topic_from_wire(topic: &backend_v1::Topic) -> Option<Topic> {
25 TopicKind::try_from(*topic.topic.first()?).ok()?;
27 Topic::try_from(topic.topic.clone()).ok()
28}
29
30fn envelope_topic(envelope: &ServerEnvelope) -> Option<Topic> {
31 topic_from_wire(envelope.meta.as_ref()?.topic.as_ref()?)
32}
33
34impl BidiBinding for BackendBinding {
35 type Request = SubscribeRequest;
36 type Response = SubscribeResponse;
37 type Mutate = Update;
38 type GroupMessage = ServerEnvelope;
39 type WelcomeMessage = ServerEnvelope;
40
41 fn mutate_frame(update: Update) -> SubscribeRequest {
42 request_frame(subscribe_request::Request::Update(update))
43 }
44
45 fn ping_frame(nonce: u64) -> SubscribeRequest {
46 request_frame(subscribe_request::Request::Ping(Ping { nonce }))
47 }
48
49 fn pong_frame(nonce: u64) -> SubscribeRequest {
50 request_frame(subscribe_request::Request::Pong(Pong { nonce }))
51 }
52
53 fn handle(response: SubscribeResponse) -> Inbound<ServerEnvelope, ServerEnvelope> {
54 use subscribe_response::Response;
55 match response.response {
56 Some(Response::Started(started)) => Inbound::Emit(Event::Started {
57 keepalive_interval_ms: started.keepalive_interval_ms,
58 }),
59 Some(Response::Applied(applied)) => {
60 let targets: Option<Vec<_>> = applied
61 .added_targets
62 .into_iter()
63 .map(|target| {
64 Some((
65 topic_from_wire(target.topic.as_ref()?)?,
66 target.through_sequence_id,
67 ))
68 })
69 .collect();
70 match targets {
71 Some(targets) => Inbound::Emit(Event::Applied {
72 id: applied.id,
73 targets,
74 }),
75 None => Inbound::Invalid("Applied topic"),
76 }
77 }
78 Some(Response::Messages(messages)) => {
79 let mut group = Vec::new();
80 let mut welcome = Vec::new();
81 for envelope in messages.envelopes {
82 match envelope_topic(&envelope).map(|topic| topic.kind()) {
83 Some(TopicKind::GroupMessagesV1 | TopicKind::IdentityUpdatesV1) => {
84 group.push(envelope)
85 }
86 Some(TopicKind::WelcomeMessagesV1) => welcome.push(envelope),
87 _ => return Inbound::Invalid("envelope topic"),
88 }
89 }
90 Inbound::Messages { group, welcome }
91 }
92 Some(Response::Ping(ping)) => Inbound::Ping(ping.nonce),
93 Some(Response::Pong(pong)) => Inbound::Pong(pong.nonce),
94 None => Inbound::Invalid("response"),
95 }
96 }
97}
98
99impl TransportBinding for BackendBinding {
100 type Cursor = u64;
101
102 fn build_mutate(
103 adds: impl IntoIterator<Item = (Topic, u64)>,
104 removes: impl IntoIterator<Item = Topic>,
105 id: u64,
106 ) -> Update {
107 Update {
108 id,
109 adds: adds
110 .into_iter()
111 .map(|(topic, sequence_id)| backend_v1::TopicQuery {
112 topic: Some(backend_v1::Topic {
113 topic: topic.to_bytes().into_vec(),
114 }),
115 cursor: Some(backend_v1::Cursor { sequence_id }),
116 })
117 .collect(),
118 removes: removes
119 .into_iter()
120 .map(|topic| backend_v1::Topic {
121 topic: topic.to_bytes().into_vec(),
122 })
123 .collect(),
124 }
125 }
126
127 fn group_topic(envelope: &ServerEnvelope) -> Option<Topic> {
128 envelope_topic(envelope)
129 }
130
131 fn welcome_topic(envelope: &ServerEnvelope) -> Option<Topic> {
132 envelope_topic(envelope)
133 }
134
135 fn group_cursor(envelope: &ServerEnvelope) -> Option<u64> {
136 Some(envelope.meta.as_ref()?.cursor.as_ref()?.sequence_id)
137 }
138
139 fn welcome_cursor(envelope: &ServerEnvelope) -> Option<u64> {
140 Self::group_cursor(envelope)
141 }
142
143 fn group_envelope(envelope: &ServerEnvelope) -> &ServerEnvelope {
144 envelope
145 }
146 fn welcome_envelope(envelope: &ServerEnvelope) -> &ServerEnvelope {
147 envelope
148 }
149
150 fn advance(position: &mut u64, delivered: u64) {
151 *position = (*position).max(delivered);
152 }
153
154 fn covers(position: &u64, delivered: &u64) -> bool {
155 delivered <= position
156 }
157
158 fn meet(a: u64, b: u64) -> u64 {
159 a.min(b)
160 }
161}
162
163impl BidiConnection {
164 pub async fn open<A>(api: &A, initial: Update) -> Result<Self, A::Error>
166 where
167 A: XmtpMlsBidiStreams,
168 A::SubscribeStream: 'static,
169 {
170 Self::start(initial, |outbound| api.subscribe_bidi(outbound)).await
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::queries::bidi::{
178 BidiError, COMMAND_BUFFER, DEFAULT_KEEPALIVE_MS, EVENT_BUFFER, MAX_PENDING_FRAMES,
179 PROBE_TIMEOUT_MULTIPLIER, TryMutateError, WIRE_BUFFER,
180 };
181 use futures::StreamExt;
182 use futures::stream::BoxStream;
183 use std::sync::Mutex;
184 use std::time::Duration;
185 use tokio::sync::mpsc;
186 use xmtp_proto::api::ApiClientError;
187 use xmtp_proto::backend_v1::subscribe_request::Update as Mutate;
188 use xmtp_proto::types::TopicKind;
189
190 use crate::test::bidi::mock_pair;
191
192 #[derive(Debug, thiserror::Error)]
193 #[error("boom")]
194 struct Boom;
195 impl xmtp_common::RetryableError for Boom {
196 fn is_retryable(&self) -> bool {
197 false
198 }
199 }
200
201 fn started(keepalive: u32) -> subscribe_response::Response {
202 subscribe_response::Response::Started(subscribe_response::Started {
203 keepalive_interval_ms: keepalive,
204 })
205 }
206
207 fn applied(id: u64) -> subscribe_response::Response {
208 subscribe_response::Response::Applied(subscribe_response::Applied {
209 id,
210 added_targets: vec![],
211 })
212 }
213
214 fn wire_topic(kind: TopicKind, identifier: &[u8]) -> backend_v1::Topic {
215 backend_v1::Topic {
216 topic: kind.create(identifier).to_bytes().into_vec(),
217 }
218 }
219
220 fn initial_mutate() -> Mutate {
221 BackendBinding::build_mutate(
222 [
223 (TopicKind::GroupMessagesV1.create(b"group"), 5),
224 (TopicKind::WelcomeMessagesV1.create(b"installation"), 0),
225 ],
226 [],
227 11,
228 )
229 }
230
231 #[xmtp_common::test(unwrap_try = true)]
232 async fn open_sends_initial_mutate_and_emits_started() {
233 let (api, mut server) = mock_pair();
234 let mut conn = BidiConnection::open(&api, initial_mutate()).await?;
235
236 let subscribe_request::Request::Update(sent) = server.next_request().await else {
237 panic!("first frame must be the initial Mutate");
238 };
239 assert_eq!(sent, initial_mutate());
240
241 server.send(started(30_000));
242 assert_eq!(
243 conn.next().await,
244 Some(BidiEvent::Started {
245 keepalive_interval_ms: 30_000,
246 })
247 );
248 }
249
250 #[xmtp_common::test(unwrap_try = true)]
251 async fn auto_pongs_server_ping_without_surfacing_it() {
252 let (api, mut server) = mock_pair();
253 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
254 server.next_request().await; server.send(subscribe_response::Response::Ping(Ping { nonce: 42 }));
257 let subscribe_request::Request::Pong(pong) = server.next_request().await else {
258 panic!("server ping must be answered with a pong");
259 };
260 assert_eq!(pong.nonce, 42);
261
262 server.send(started(15_000));
264 assert_eq!(
265 conn.next().await,
266 Some(BidiEvent::Started {
267 keepalive_interval_ms: 15_000,
268 })
269 );
270 }
271
272 #[xmtp_common::test(unwrap_try = true)]
273 async fn probe_round_trips_and_pong_is_not_an_event() {
274 let (api, mut server) = mock_pair();
275 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
276 server.next_request().await; let server_side = async {
281 let subscribe_request::Request::Ping(ping) = server.next_request().await else {
282 panic!("probe must send a Ping");
283 };
284 server.send(subscribe_response::Response::Pong(Pong {
285 nonce: ping.nonce,
286 }));
287 };
288 let (result, ()) = futures::join!(conn.probe(), server_side);
289 assert!(matches!(result, Ok(())), "probe should resolve on its pong");
290
291 server.send(started(10_000));
293 assert_eq!(
294 conn.next().await,
295 Some(BidiEvent::Started {
296 keepalive_interval_ms: 10_000,
297 })
298 );
299 }
300
301 #[xmtp_common::test(unwrap_try = true)]
302 async fn mutate_is_forwarded_to_the_wire() {
303 let (api, mut server) = mock_pair();
304 let conn = BidiConnection::open(&api, Mutate::default()).await?;
305 server.next_request().await; let m = Mutate {
308 removes: vec![wire_topic(TopicKind::GroupMessagesV1, b"group")],
309 ..Default::default()
310 };
311 conn.mutate(m.clone()).await?;
312 let subscribe_request::Request::Update(sent) = server.next_request().await else {
313 panic!("mutate must reach the wire");
314 };
315 assert_eq!(sent, m);
316 }
317
318 #[xmtp_common::test(unwrap_try = true)]
319 async fn unknown_frames_close_with_a_protocol_error() {
320 let (api, mut server) = mock_pair();
321 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
322 server.next_request().await; server.send_raw(SubscribeResponse { response: None });
326 assert!(conn.next().await.is_none());
327 assert!(matches!(
328 conn.failure().as_deref(),
329 Some(crate::queries::bidi::ConnectionFailure::Protocol(
330 "response"
331 ))
332 ));
333 }
334
335 #[xmtp_common::test(unwrap_try = true)]
336 async fn inbound_error_closes_the_connection() {
337 let (api, mut server) = mock_pair();
338 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
339 server.next_request().await;
340
341 server
342 .to_client
343 .send(Err(ApiClientError::client(Boom)))
344 .unwrap();
345 assert_eq!(conn.next().await, None);
346 }
347
348 #[xmtp_common::test(unwrap_try = true)]
354 async fn closing_inbound_tears_down_sends() {
355 let (api, mut server) = mock_pair();
356 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
357 server.next_request().await; drop(server);
363 assert_eq!(conn.next().await, None, "actor observed end-of-stream");
364
365 assert!(
366 matches!(conn.mutate(Mutate::default()).await, Err(BidiError::Closed)),
367 "mutate after teardown must report Closed, not silently enqueue"
368 );
369 assert!(
370 matches!(conn.probe().await, Err(BidiError::Closed)),
371 "probe after teardown must report Closed"
372 );
373 }
374
375 #[xmtp_common::test(unwrap_try = true)]
376 async fn concurrent_mutate_and_probe_both_reach_the_wire() {
377 let (api, mut server) = mock_pair();
378 let conn = BidiConnection::open(&api, Mutate::default()).await?;
379 server.next_request().await; let m = Mutate {
382 removes: vec![wire_topic(TopicKind::GroupMessagesV1, b"group")],
383 ..Default::default()
384 };
385
386 let expected = m.clone();
389 let server_side = async {
390 let mut saw_mutate = false;
391 let mut pong_nonce = None;
392 for _ in 0..2 {
393 match server.next_request().await {
394 subscribe_request::Request::Update(sent) => {
395 assert_eq!(sent, expected);
396 saw_mutate = true;
397 }
398 subscribe_request::Request::Ping(ping) => pong_nonce = Some(ping.nonce),
399 other => panic!("unexpected frame: {other:?}"),
400 }
401 }
402 server.send(subscribe_response::Response::Pong(Pong {
403 nonce: pong_nonce.expect("probe must send a ping"),
404 }));
405 saw_mutate
406 };
407
408 let (mutate_res, probe_res, saw_mutate) =
409 futures::join!(conn.mutate(m), conn.probe(), server_side);
410 assert!(mutate_res.is_ok());
411 assert!(matches!(probe_res, Ok(())));
412 assert!(saw_mutate, "the mutate must reach the wire");
413 }
414
415 #[xmtp_common::test(unwrap_try = true)]
416 async fn probe_within_times_out_when_no_pong() {
417 let (api, mut server) = mock_pair();
418 let conn = BidiConnection::open(&api, Mutate::default()).await?;
419 server.next_request().await; let result = conn.probe_within(Duration::from_millis(100)).await;
425 assert!(matches!(result, Err(BidiError::ProbeTimedOut)));
426 }
427
428 #[xmtp_common::test(unwrap_try = true)]
429 async fn default_probe_timeout_tracks_server_keepalive() {
430 let (api, mut server) = mock_pair();
431 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
432 server.next_request().await; assert_eq!(
436 conn.default_probe_timeout(),
437 Duration::from_millis(
438 u64::from(DEFAULT_KEEPALIVE_MS) * u64::from(PROBE_TIMEOUT_MULTIPLIER)
439 )
440 );
441
442 server.send(started(5_000));
444 assert_eq!(
445 conn.next().await,
446 Some(BidiEvent::Started {
447 keepalive_interval_ms: 5_000,
448 })
449 );
450 assert_eq!(
451 conn.default_probe_timeout(),
452 Duration::from_millis(5_000 * u64::from(PROBE_TIMEOUT_MULTIPLIER))
453 );
454 }
455
456 struct WedgedWireApi {
459 inbound: Mutex<Option<mpsc::UnboundedReceiver<Result<SubscribeResponse, ApiClientError>>>>,
460 held: Mutex<Option<BoxStream<'static, SubscribeRequest>>>,
461 }
462
463 #[xmtp_common::async_trait]
464 impl XmtpMlsBidiStreams for WedgedWireApi {
465 type SubscribeStream = BoxStream<'static, Result<SubscribeResponse, ApiClientError>>;
466 type Error = ApiClientError;
467
468 fn host(&self) -> &str {
469 "mock://bidi"
470 }
471
472 async fn subscribe_bidi(
473 &self,
474 requests: BoxStream<'static, SubscribeRequest>,
475 ) -> Result<Self::SubscribeStream, Self::Error> {
476 *self.held.lock().unwrap() = Some(requests);
479 let mut inbound = self
480 .inbound
481 .lock()
482 .unwrap()
483 .take()
484 .expect("subscribe_bidi called twice");
485 Ok(Box::pin(futures::stream::poll_fn(move |cx| {
486 inbound.poll_recv(cx)
487 })))
488 }
489 }
490
491 #[xmtp_common::test(unwrap_try = true)]
496 async fn busy_wire_does_not_stall_inbound() {
497 let (to_client, inbound) = mpsc::unbounded_channel();
498 let api = WedgedWireApi {
499 inbound: Mutex::new(Some(inbound)),
500 held: Mutex::new(None),
501 };
502 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
503
504 for _ in 0..(WIRE_BUFFER * 2) {
509 conn.mutate(Mutate::default()).await?;
510 }
511
512 to_client
514 .send(Ok(SubscribeResponse {
515 response: Some(started(7_000)),
516 }))
517 .unwrap();
518 assert_eq!(
519 conn.next().await,
520 Some(BidiEvent::Started {
521 keepalive_interval_ms: 7_000,
522 })
523 );
524 }
525
526 #[xmtp_common::test(unwrap_try = true)]
530 async fn gives_up_when_wire_wedged_past_backlog_cap() {
531 let (to_client, inbound) = mpsc::unbounded_channel();
532 let api = WedgedWireApi {
533 inbound: Mutex::new(Some(inbound)),
534 held: Mutex::new(None),
535 };
536 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
537
538 for _ in 0..(MAX_PENDING_FRAMES + WIRE_BUFFER + 2) {
541 to_client
542 .send(Ok(SubscribeResponse {
543 response: Some(subscribe_response::Response::Ping(Ping { nonce: 1 })),
544 }))
545 .unwrap();
546 }
547
548 assert_eq!(conn.next().await, None);
550 assert!(matches!(
551 conn.mutate(Mutate::default()).await,
552 Err(BidiError::Closed)
553 ));
554 }
555
556 #[xmtp_common::test(unwrap_try = true)]
564 async fn finish_is_processed_under_wire_backpressure() {
565 let (_to_client, inbound) = mpsc::unbounded_channel();
566 let api = WedgedWireApi {
567 inbound: Mutex::new(Some(inbound)),
568 held: Mutex::new(None),
569 };
570 let conn = BidiConnection::open(&api, Mutate::default()).await?;
571
572 for _ in 0..(WIRE_BUFFER * 2) {
576 conn.mutate(Mutate::default()).await?;
577 }
578
579 conn.finish().await?;
581
582 xmtp_common::time::sleep(Duration::from_secs(2)).await;
588 let mut outbound = api.held.lock().unwrap().take().expect("wire was opened");
589 let mut flushed = 0usize;
590 while let Some(_frame) = outbound.next().await {
591 flushed += 1;
592 }
593 assert_eq!(
594 flushed, WIRE_BUFFER,
595 "only the already-accepted wire frames flush; the backlog is dropped \
596 and the request half closes"
597 );
598 }
599
600 #[xmtp_common::test(unwrap_try = true)]
606 async fn mutate_and_probe_report_closed_after_finish() {
607 let (api, mut server) = mock_pair();
608 let conn = BidiConnection::open(&api, Mutate::default()).await?;
609 server.next_request().await; conn.finish().await?;
612
613 assert!(
614 matches!(conn.mutate(Mutate::default()).await, Err(BidiError::Closed)),
615 "mutate after finish must report Closed, not land in the buffer"
616 );
617 assert!(
618 matches!(conn.probe().await, Err(BidiError::Closed)),
619 "probe after finish must report Closed"
620 );
621 }
622
623 #[xmtp_common::test(unwrap_try = true)]
628 async fn finish_resolves_in_flight_probe_to_closed() {
629 let (api, mut server) = mock_pair();
630 let conn = BidiConnection::open(&api, Mutate::default()).await?;
631 server.next_request().await; let driver = async {
637 let req = server.next_request().await;
638 assert!(
639 matches!(req, subscribe_request::Request::Ping(_)),
640 "probe must put a ping on the wire"
641 );
642 conn.finish().await
643 };
644 let (probe_res, finish_res) =
645 futures::join!(conn.probe_within(Duration::from_secs(5)), driver);
646 assert!(
647 matches!(probe_res, Err(BidiError::Closed)),
648 "an in-flight probe must resolve Closed when finish half-closes, got {probe_res:?}"
649 );
650 assert!(finish_res.is_ok());
651 }
652
653 #[xmtp_common::test(unwrap_try = true)]
659 async fn try_mutate_reports_full_and_recovers_after_drain() {
660 let (api, mut server) = mock_pair();
661 let mut conn = BidiConnection::open(&api, initial_mutate()).await?;
662 server.next_request().await; for nonce in 0..(EVENT_BUFFER as u64 + 2) {
667 server.send(applied(nonce));
668 }
669 let mut accepted = 0usize;
672 loop {
673 match conn.try_mutate(initial_mutate()) {
674 Ok(()) => {
675 accepted += 1;
676 assert!(
677 accepted <= EVENT_BUFFER + COMMAND_BUFFER + 8,
678 "actor never parked; try_mutate never reported Full"
679 );
680 }
681 Err(TryMutateError::Full(_)) => break,
682 Err(TryMutateError::Closed(_)) => panic!("connection died under the flood"),
683 }
684 tokio::task::yield_now().await;
686 }
687
688 let mut recovered = false;
690 for _ in 0..(EVENT_BUFFER + COMMAND_BUFFER) {
691 assert!(
692 conn.next().await.is_some(),
693 "flooded events must all arrive"
694 );
695 if conn.try_mutate(initial_mutate()).is_ok() {
696 recovered = true;
697 break;
698 }
699 }
700 assert!(recovered, "try_mutate must accept again after a drain");
701 }
702
703 #[xmtp_common::test(unwrap_try = true)]
705 async fn try_mutate_reports_closed_after_finish() {
706 let (api, mut server) = mock_pair();
707 let conn = BidiConnection::open(&api, initial_mutate()).await?;
708 server.next_request().await; conn.finish().await?;
711 assert!(matches!(
712 conn.try_mutate(initial_mutate()),
713 Err(TryMutateError::Closed(_))
714 ));
715 }
716}