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)]
252 async fn auto_pongs_server_ping_without_surfacing_it() {
253 let (api, mut server) = mock_pair();
254 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
255 server.next_request().await; server.send(subscribe_response::Response::Ping(Ping { nonce: 42 }));
258 let subscribe_request::Request::Pong(pong) = server.next_request().await else {
259 panic!("server ping must be answered with a pong");
260 };
261 assert_eq!(pong.nonce, 42);
262
263 server.send(started(15_000));
265 assert_eq!(
266 conn.next().await,
267 Some(BidiEvent::Started {
268 keepalive_interval_ms: 15_000,
269 })
270 );
271 }
272
273 #[xmtp_common::test(unwrap_try = true)]
274 async fn probe_round_trips_and_pong_is_not_an_event() {
275 let (api, mut server) = mock_pair();
276 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
277 server.next_request().await; let server_side = async {
282 let subscribe_request::Request::Ping(ping) = server.next_request().await else {
283 panic!("probe must send a Ping");
284 };
285 server.send(subscribe_response::Response::Pong(Pong {
286 nonce: ping.nonce,
287 }));
288 };
289 let (result, ()) = futures::join!(conn.probe(), server_side);
290 assert!(matches!(result, Ok(())), "probe should resolve on its pong");
291
292 server.send(started(10_000));
294 assert_eq!(
295 conn.next().await,
296 Some(BidiEvent::Started {
297 keepalive_interval_ms: 10_000,
298 })
299 );
300 }
301
302 #[xmtp_common::test(unwrap_try = true)]
303 async fn mutate_is_forwarded_to_the_wire() {
304 let (api, mut server) = mock_pair();
305 let conn = BidiConnection::open(&api, Mutate::default()).await?;
306 server.next_request().await; let m = Mutate {
309 removes: vec![wire_topic(TopicKind::GroupMessagesV1, b"group")],
310 ..Default::default()
311 };
312 conn.mutate(m.clone()).await?;
313 let subscribe_request::Request::Update(sent) = server.next_request().await else {
314 panic!("mutate must reach the wire");
315 };
316 assert_eq!(sent, m);
317 }
318
319 #[xmtp_common::test(unwrap_try = true)]
320 async fn unknown_frames_close_with_a_protocol_error() {
321 let (api, mut server) = mock_pair();
322 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
323 server.next_request().await; server.send_raw(SubscribeResponse { response: None });
327 assert!(conn.next().await.is_none());
328 assert!(matches!(
329 conn.failure().as_deref(),
330 Some(crate::queries::bidi::ConnectionFailure::Protocol(
331 "response"
332 ))
333 ));
334 }
335
336 #[xmtp_common::test(unwrap_try = true)]
337 async fn inbound_error_closes_the_connection() {
338 let (api, mut server) = mock_pair();
339 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
340 server.next_request().await;
341
342 server
343 .to_client
344 .send(Err(ApiClientError::client(Boom)))
345 .unwrap();
346 assert_eq!(conn.next().await, None);
347 }
348
349 #[xmtp_common::test(unwrap_try = true)]
355 async fn closing_inbound_tears_down_sends() {
356 let (api, mut server) = mock_pair();
357 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
358 server.next_request().await; drop(server);
364 assert_eq!(conn.next().await, None, "actor observed end-of-stream");
365
366 assert!(
367 matches!(conn.mutate(Mutate::default()).await, Err(BidiError::Closed)),
368 "mutate after teardown must report Closed, not silently enqueue"
369 );
370 assert!(
371 matches!(conn.probe().await, Err(BidiError::Closed)),
372 "probe after teardown must report Closed"
373 );
374 }
375
376 #[xmtp_common::test(unwrap_try = true)]
377 async fn concurrent_mutate_and_probe_both_reach_the_wire() {
378 let (api, mut server) = mock_pair();
379 let conn = BidiConnection::open(&api, Mutate::default()).await?;
380 server.next_request().await; let m = Mutate {
383 removes: vec![wire_topic(TopicKind::GroupMessagesV1, b"group")],
384 ..Default::default()
385 };
386
387 let expected = m.clone();
390 let server_side = async {
391 let mut saw_mutate = false;
392 let mut pong_nonce = None;
393 for _ in 0..2 {
394 match server.next_request().await {
395 subscribe_request::Request::Update(sent) => {
396 assert_eq!(sent, expected);
397 saw_mutate = true;
398 }
399 subscribe_request::Request::Ping(ping) => pong_nonce = Some(ping.nonce),
400 other => panic!("unexpected frame: {other:?}"),
401 }
402 }
403 server.send(subscribe_response::Response::Pong(Pong {
404 nonce: pong_nonce.expect("probe must send a ping"),
405 }));
406 saw_mutate
407 };
408
409 let (mutate_res, probe_res, saw_mutate) =
410 futures::join!(conn.mutate(m), conn.probe(), server_side);
411 assert!(mutate_res.is_ok());
412 assert!(matches!(probe_res, Ok(())));
413 assert!(saw_mutate, "the mutate must reach the wire");
414 }
415
416 #[xmtp_common::test(unwrap_try = true)]
417 async fn probe_within_times_out_when_no_pong() {
418 let (api, mut server) = mock_pair();
419 let conn = BidiConnection::open(&api, Mutate::default()).await?;
420 server.next_request().await; let result = conn.probe_within(Duration::from_millis(100)).await;
426 assert!(matches!(result, Err(BidiError::ProbeTimedOut)));
427 }
428
429 #[xmtp_common::test(unwrap_try = true)]
430 async fn default_probe_timeout_tracks_server_keepalive() {
431 let (api, mut server) = mock_pair();
432 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
433 server.next_request().await; assert_eq!(
437 conn.default_probe_timeout(),
438 Duration::from_millis(
439 u64::from(DEFAULT_KEEPALIVE_MS) * u64::from(PROBE_TIMEOUT_MULTIPLIER)
440 )
441 );
442
443 server.send(started(5_000));
445 assert_eq!(
446 conn.next().await,
447 Some(BidiEvent::Started {
448 keepalive_interval_ms: 5_000,
449 })
450 );
451 assert_eq!(
452 conn.default_probe_timeout(),
453 Duration::from_millis(5_000 * u64::from(PROBE_TIMEOUT_MULTIPLIER))
454 );
455 }
456
457 struct WedgedWireApi {
460 inbound: Mutex<Option<mpsc::UnboundedReceiver<Result<SubscribeResponse, ApiClientError>>>>,
461 held: Mutex<Option<BoxStream<'static, SubscribeRequest>>>,
462 }
463
464 #[xmtp_common::async_trait]
465 impl XmtpMlsBidiStreams for WedgedWireApi {
466 type SubscribeStream = BoxStream<'static, Result<SubscribeResponse, ApiClientError>>;
467 type Error = ApiClientError;
468
469 fn host(&self) -> &str {
470 "mock://bidi"
471 }
472
473 async fn subscribe_bidi(
474 &self,
475 requests: BoxStream<'static, SubscribeRequest>,
476 ) -> Result<Self::SubscribeStream, Self::Error> {
477 *self.held.lock().unwrap() = Some(requests);
480 let mut inbound = self
481 .inbound
482 .lock()
483 .unwrap()
484 .take()
485 .expect("subscribe_bidi called twice");
486 Ok(Box::pin(futures::stream::poll_fn(move |cx| {
487 inbound.poll_recv(cx)
488 })))
489 }
490 }
491
492 #[xmtp_common::test(unwrap_try = true)]
497 async fn busy_wire_does_not_stall_inbound() {
498 let (to_client, inbound) = mpsc::unbounded_channel();
499 let api = WedgedWireApi {
500 inbound: Mutex::new(Some(inbound)),
501 held: Mutex::new(None),
502 };
503 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
504
505 for _ in 0..(WIRE_BUFFER * 2) {
510 conn.mutate(Mutate::default()).await?;
511 }
512
513 to_client
515 .send(Ok(SubscribeResponse {
516 response: Some(started(7_000)),
517 }))
518 .unwrap();
519 assert_eq!(
520 conn.next().await,
521 Some(BidiEvent::Started {
522 keepalive_interval_ms: 7_000,
523 })
524 );
525 }
526
527 #[xmtp_common::test(unwrap_try = true)]
531 async fn gives_up_when_wire_wedged_past_backlog_cap() {
532 let (to_client, inbound) = mpsc::unbounded_channel();
533 let api = WedgedWireApi {
534 inbound: Mutex::new(Some(inbound)),
535 held: Mutex::new(None),
536 };
537 let mut conn = BidiConnection::open(&api, Mutate::default()).await?;
538
539 for _ in 0..(MAX_PENDING_FRAMES + WIRE_BUFFER + 2) {
542 to_client
543 .send(Ok(SubscribeResponse {
544 response: Some(subscribe_response::Response::Ping(Ping { nonce: 1 })),
545 }))
546 .unwrap();
547 }
548
549 assert_eq!(conn.next().await, None);
551 assert!(matches!(
552 conn.mutate(Mutate::default()).await,
553 Err(BidiError::Closed)
554 ));
555 }
556
557 #[xmtp_common::test(unwrap_try = true)]
565 async fn finish_is_processed_under_wire_backpressure() {
566 let (_to_client, inbound) = mpsc::unbounded_channel();
567 let api = WedgedWireApi {
568 inbound: Mutex::new(Some(inbound)),
569 held: Mutex::new(None),
570 };
571 let conn = BidiConnection::open(&api, Mutate::default()).await?;
572
573 for _ in 0..(WIRE_BUFFER * 2) {
577 conn.mutate(Mutate::default()).await?;
578 }
579
580 conn.finish().await?;
582
583 xmtp_common::time::sleep(Duration::from_secs(2)).await;
589 let mut outbound = api.held.lock().unwrap().take().expect("wire was opened");
590 let mut flushed = 0usize;
591 while let Some(_frame) = outbound.next().await {
592 flushed += 1;
593 }
594 assert_eq!(
595 flushed, WIRE_BUFFER,
596 "only the already-accepted wire frames flush; the backlog is dropped \
597 and the request half closes"
598 );
599 }
600
601 #[xmtp_common::test(unwrap_try = true)]
607 async fn mutate_and_probe_report_closed_after_finish() {
608 let (api, mut server) = mock_pair();
609 let conn = BidiConnection::open(&api, Mutate::default()).await?;
610 server.next_request().await; conn.finish().await?;
613
614 assert!(
615 matches!(conn.mutate(Mutate::default()).await, Err(BidiError::Closed)),
616 "mutate after finish must report Closed, not land in the buffer"
617 );
618 assert!(
619 matches!(conn.probe().await, Err(BidiError::Closed)),
620 "probe after finish must report Closed"
621 );
622 }
623
624 #[xmtp_common::test(unwrap_try = true)]
629 async fn finish_resolves_in_flight_probe_to_closed() {
630 let (api, mut server) = mock_pair();
631 let conn = BidiConnection::open(&api, Mutate::default()).await?;
632 server.next_request().await; let driver = async {
638 let req = server.next_request().await;
639 assert!(
640 matches!(req, subscribe_request::Request::Ping(_)),
641 "probe must put a ping on the wire"
642 );
643 conn.finish().await
644 };
645 let (probe_res, finish_res) =
646 futures::join!(conn.probe_within(Duration::from_secs(5)), driver);
647 assert!(
648 matches!(probe_res, Err(BidiError::Closed)),
649 "an in-flight probe must resolve Closed when finish half-closes, got {probe_res:?}"
650 );
651 assert!(finish_res.is_ok());
652 }
653
654 #[xmtp_common::test(unwrap_try = true)]
660 async fn try_mutate_reports_full_and_recovers_after_drain() {
661 let (api, mut server) = mock_pair();
662 let mut conn = BidiConnection::open(&api, initial_mutate()).await?;
663 server.next_request().await; for nonce in 0..(EVENT_BUFFER as u64 + 2) {
668 server.send(applied(nonce));
669 }
670 let mut accepted = 0usize;
673 loop {
674 match conn.try_mutate(initial_mutate()) {
675 Ok(()) => {
676 accepted += 1;
677 assert!(
678 accepted <= EVENT_BUFFER + COMMAND_BUFFER + 8,
679 "actor never parked; try_mutate never reported Full"
680 );
681 }
682 Err(TryMutateError::Full(_)) => break,
683 Err(TryMutateError::Closed(_)) => panic!("connection died under the flood"),
684 }
685 tokio::task::yield_now().await;
687 }
688
689 let mut recovered = false;
691 for _ in 0..(EVENT_BUFFER + COMMAND_BUFFER) {
692 assert!(
693 conn.next().await.is_some(),
694 "flooded events must all arrive"
695 );
696 if conn.try_mutate(initial_mutate()).is_ok() {
697 recovered = true;
698 break;
699 }
700 }
701 assert!(recovered, "try_mutate must accept again after a drain");
702 }
703
704 #[xmtp_common::test(unwrap_try = true)]
706 async fn try_mutate_reports_closed_after_finish() {
707 let (api, mut server) = mock_pair();
708 let conn = BidiConnection::open(&api, initial_mutate()).await?;
709 server.next_request().await; conn.finish().await?;
712 assert!(matches!(
713 conn.try_mutate(initial_mutate()),
714 Err(TryMutateError::Closed(_))
715 ));
716 }
717}