1use futures_util::{SinkExt, StreamExt};
2use std::time::Duration;
3use tokio::{
4 sync::mpsc,
5 time::{Instant, sleep, sleep_until, timeout},
6};
7use tokio_tungstenite::{connect_async, tungstenite::Message};
8use tracing::{debug, error, info, warn};
9
10use std::collections::HashMap;
11
12use crate::{
13 Topic,
14 serde::serialize_json,
15 ws::{CommandMsg, IncomingMessage, OutgoingMessage, create_outgoing_message_auth_at},
16};
17
18use super::{
19 Command, Config, DisconnectReason, Event, Handle,
20 state::{FrameResult, HeartbeatState, Sink, State},
21};
22
23pub struct Stream {
46 config: Config,
47 cmd_rx: mpsc::Receiver<Command>,
48 evt_tx: mpsc::Sender<Event>,
49 dropped: u64,
51 topics: Vec<Topic>,
53 next_req_id: u64,
55}
56
57const MAX_TOPICS_PER_REQUEST: usize = 10;
60
61#[derive(Default)]
63struct Session {
64 awaiting_auth: bool,
66 ready: bool,
68 pending: HashMap<String, Vec<Topic>>,
70}
71
72impl Stream {
73 #[allow(clippy::new_ret_no_self)]
75 pub fn new(config: Config) -> (Handle, mpsc::Receiver<Event>) {
76 let (cmd_tx, cmd_rx) = mpsc::channel::<Command>(config.command_queue_size);
77 let (evt_tx, evt_rx) = mpsc::channel::<Event>(config.event_queue_size);
78
79 let stream = Self {
80 config,
81 cmd_rx,
82 evt_tx,
83 dropped: 0,
84 topics: Vec::new(),
85 next_req_id: 0,
86 };
87
88 tokio::spawn(stream.run());
89
90 (Handle::new(cmd_tx), evt_rx)
91 }
92
93 async fn run(mut self) {
94 info!("stream started");
95 let mut state = State::Idle;
96
97 loop {
98 state = match state {
99 State::Idle => self.step_idle().await,
100 State::Connecting { attempt } => self.step_connecting(attempt).await,
101 State::Connected {
102 frame_rx,
103 read_task,
104 sink,
105 } => self.step_connected(frame_rx, read_task, sink).await,
106 State::Reconnecting { attempt, delay_ms } => {
107 self.step_reconnecting(attempt, delay_ms).await
108 }
109 State::Closing {
110 frame_rx,
111 read_task,
112 sink,
113 } => self.step_closing(frame_rx, read_task, sink).await,
114 State::Done => break,
115 };
116 }
117
118 info!("stream shut down");
119 }
120
121 fn emit(&mut self, event: Event) {
124 if self.dropped > 0 {
125 match self.evt_tx.try_send(Event::Lagged {
126 dropped: self.dropped,
127 }) {
128 Ok(()) => self.dropped = 0,
129 Err(mpsc::error::TrySendError::Full(_)) => {
130 self.dropped += 1;
131 return;
132 }
133 Err(mpsc::error::TrySendError::Closed(_)) => return,
134 }
135 }
136
137 match self.evt_tx.try_send(event) {
138 Ok(()) => {}
139 Err(mpsc::error::TrySendError::Full(dropped)) => {
140 if self.dropped == 0 {
141 warn!("event queue full, dropping events");
142 }
143 debug!(event = ?dropped, "event dropped");
144 self.dropped += 1;
145 }
146 Err(mpsc::error::TrySendError::Closed(_)) => debug!("event receiver dropped"),
147 }
148 }
149
150 async fn emit_lifecycle(&mut self, event: Event) {
153 if self.dropped > 0 {
154 let lagged = Event::Lagged {
155 dropped: self.dropped,
156 };
157 if self.evt_tx.send(lagged).await.is_err() {
158 debug!("event receiver dropped");
159 return;
160 }
161 self.dropped = 0;
162 }
163 if self.evt_tx.send(event).await.is_err() {
164 debug!("event receiver dropped");
165 }
166 }
167
168 async fn step_idle(&mut self) -> State {
169 loop {
170 match self.cmd_rx.recv().await {
171 Some(Command::Connect) => {
172 return State::Connecting { attempt: 0 };
173 }
174 None => return State::Done,
175 Some(Command::Disconnect) => {
176 warn!("Command disconnect ignored - not connected");
177 }
178 Some(Command::Send(_)) => {
179 warn!("Send ignored - not connected");
180 }
181 Some(Command::Subscribe(topics)) => {
182 self.add_topics(topics);
183 }
184 Some(Command::Unsubscribe(topics)) => {
185 self.remove_topics(topics);
186 }
187 }
188 }
189 }
190
191 async fn step_connecting(&mut self, attempt: u32) -> State {
192 debug!(attempt, "connecting");
193
194 let url = self.config.url.clone();
195 let connect = timeout(self.config.connect_timeout, connect_async(url));
196 tokio::pin!(connect);
197
198 let result = loop {
201 tokio::select! {
202 result = &mut connect => break result,
203 cmd = self.cmd_rx.recv() => match cmd {
204 None | Some(Command::Disconnect) => {
205 info!("disconnect requested while connecting");
206 self.emit_lifecycle(Event::Disconnected {
207 reason: DisconnectReason::Requested,
208 })
209 .await;
210 return State::Idle;
211 }
212 Some(Command::Connect) => debug!("Connect ignored - already connecting"),
213 Some(Command::Send(_)) => warn!("Send ignored - not connected yet"),
214 Some(Command::Subscribe(topics)) => {
215 self.add_topics(topics);
216 }
217 Some(Command::Unsubscribe(topics)) => {
218 self.remove_topics(topics);
219 }
220 },
221 }
222 };
223
224 match result {
225 Ok(Ok((ws_stream, _))) => {
226 info!("websocket connected");
227 self.emit_lifecycle(Event::Connected).await;
228
229 let (sink, stream) = ws_stream.split();
230 let (frame_tx, frame_rx) =
231 mpsc::channel::<FrameResult>(self.config.event_queue_size);
232
233 let read_task = tokio::spawn(async move {
234 let mut stream = stream;
235 while let Some(msg) = stream.next().await {
236 if frame_tx.send(msg).await.is_err() {
237 break;
238 }
239 }
240 });
241
242 State::Connected {
243 frame_rx,
244 read_task,
245 sink: Box::new(sink),
246 }
247 }
248 Ok(Err(e)) => {
249 error!(error = %e, attempt, "connection failed");
250 self.next_reconnect_state(attempt + 1, DisconnectReason::Error(e.to_string()))
251 .await
252 }
253 Err(_) => {
254 error!(attempt, "connection timed out");
255 self.next_reconnect_state(
256 attempt + 1,
257 DisconnectReason::Error(String::from("connect timeout")),
258 )
259 .await
260 }
261 }
262 }
263
264 async fn step_connected(
265 &mut self,
266 mut frame_rx: mpsc::Receiver<FrameResult>,
267 read_task: tokio::task::JoinHandle<()>,
268 mut sink: Sink,
269 ) -> State {
270 let ping_interval = self.config.ping_interval;
271 let pong_timeout = self.config.pong_timeout;
272 let mut ping_timer = Box::pin(match ping_interval {
273 Some(d) => sleep(d),
274 None => sleep(FAR_FUTURE),
275 });
276 let mut pong_timer = Box::pin(sleep(FAR_FUTURE));
277 let mut hb = HeartbeatState::Idle;
278
279 let mut session = Session::default();
280 let opened = match self.auth_message() {
281 Some(auth) => {
282 session.awaiting_auth = true;
283 send_message(&mut sink, &auth).await
284 }
285 None => {
286 session.ready = true;
287 let topics = self.topics.clone();
288 self.send_topics(&mut sink, &mut session, true, &topics)
289 .await
290 }
291 };
292 if let Err(e) = opened {
293 error!(error = %e, "send error");
294 read_task.abort();
295 return self
296 .next_reconnect_state(1, DisconnectReason::Error(e.to_string()))
297 .await;
298 }
299
300 loop {
301 tokio::select! {
302 biased;
303
304 frame = frame_rx.recv() => match frame {
305 None => {
306 info!("remote closed the connection");
307 read_task.abort();
308 return self.next_reconnect_state(1, DisconnectReason::RemoteClosed).await;
309 }
310 Some(Ok(msg)) => {
311 match msg {
312 Message::Text(json) => {
313 match IncomingMessage::from_json(&json){
314 Ok(msg) => {
315 if matches!(hb, HeartbeatState::PingSent) && (msg.is_ping() || msg.is_pong()) {
320 debug!("receiving heartbeat pong");
321 hb = HeartbeatState::Idle;
322 pong_timer.as_mut().reset(far_future_instant());
323 if let Some(d) = ping_interval {
324 ping_timer.as_mut().reset(Instant::now() + d);
325 }
326 }
327
328 if let Err(e) = self.on_command_reply(&msg, &mut session, &mut sink).await {
329 error!(error = %e, "send error");
330 read_task.abort();
331 return self.next_reconnect_state(1, DisconnectReason::Error(e.to_string())).await;
332 }
333 self.emit(Event::Message(msg));
334 }
335 Err(e) => {
336 warn!(error = %e, "parsing IncomingMessage failed");
337 self.emit(Event::ParseError(e.to_string()));
338 }
339 }
340 }
341 Message::Binary(bytes) => debug!("binary message received ({}B)", bytes.len()),
342 Message::Ping(bytes) => debug!("ping received ({}B)", bytes.len()),
343 Message::Pong(bytes) => debug!("pong received ({}B)", bytes.len()),
344 Message::Close(close_frame) => debug!("close frame received [{:?}]", close_frame),
345 Message::Frame(frame) =>debug!("frame received ({}B)", frame.len()),
346 }
347 }
348 Some(Err(e)) => {
349 error!(error = %e, "websocket read error");
350 read_task.abort();
351 return self.next_reconnect_state(1, DisconnectReason::Error(e.to_string())).await;
352 }
353 },
354
355 cmd = self.cmd_rx.recv() => match cmd {
356 None | Some(Command::Disconnect) => {
357 info!("disconnect requested");
358 return State::Closing { frame_rx, read_task, sink };
359 }
360 Some(cmd @ (Command::Send(_) | Command::Subscribe(_) | Command::Unsubscribe(_))) => {
361 let sent = match cmd {
362 Command::Send(msg) => send_message(&mut sink, &msg).await,
363 Command::Subscribe(topics) => {
364 let added = self.add_topics(topics);
365 if session.ready {
366 self.send_topics(&mut sink, &mut session, true, &added).await
367 } else {
368 Ok(())
369 }
370 }
371 Command::Unsubscribe(topics) => {
372 let removed = self.remove_topics(topics);
373 if session.ready {
374 self.send_topics(&mut sink, &mut session, false, &removed).await
375 } else {
376 Ok(())
377 }
378 }
379 Command::Connect | Command::Disconnect => unreachable!(),
380 };
381 if let Err(e) = sent {
382 error!(error = %e, "send error");
383 read_task.abort();
384 return self.next_reconnect_state(1, DisconnectReason::Error(e.to_string())).await;
385 }
386 }
387 Some(Command::Connect) => warn!("Connect ignored - already connected")
388 },
389
390 () = &mut ping_timer, if ping_interval.is_some() => {
391 debug!("sending heartbeat ping");
392 if let Err(e) = sink.send(ping()).await {
393 error!(error = %e, "ping send error");
394 read_task.abort();
395 return self.next_reconnect_state(1, DisconnectReason::Error(e.to_string())).await;
396 }
397
398 hb = HeartbeatState::PingSent;
399 pong_timer.as_mut().reset(Instant::now() + pong_timeout);
400 ping_timer.as_mut().reset(far_future_instant());
401 },
402
403 () = &mut pong_timer, if matches!(hb, HeartbeatState::PingSent) => {
404 warn!("pong timeout - connection appears dead");
405 read_task.abort();
406 return self.next_reconnect_state(1, DisconnectReason::PongTimeout).await;
407 },
408 }
409 }
410 }
411
412 async fn step_reconnecting(&mut self, attempt: u32, delay_ms: u64) -> State {
413 warn!(attempt, delay_ms, "waiting before reconnect");
414 self.emit_lifecycle(Event::Reconnecting { attempt, delay_ms })
415 .await;
416
417 let deadline = Instant::now() + Duration::from_millis(delay_ms);
420 loop {
421 tokio::select! {
422 () = sleep_until(deadline) => return State::Connecting { attempt },
423 cmd = self.cmd_rx.recv() => match cmd {
424 None | Some(Command::Disconnect) => {
425 self.emit_lifecycle(Event::Disconnected {
426 reason: DisconnectReason::Requested,
427 })
428 .await;
429 return State::Idle;
430 }
431 Some(Command::Connect) => debug!("Connect ignored - already reconnecting"),
432 Some(Command::Send(_)) => warn!("Send ignored - not connected"),
433 Some(Command::Subscribe(topics)) => {
434 self.add_topics(topics);
435 }
436 Some(Command::Unsubscribe(topics)) => {
437 self.remove_topics(topics);
438 }
439 },
440 }
441 }
442 }
443
444 async fn step_closing(
445 &mut self,
446 mut frame_rx: mpsc::Receiver<FrameResult>,
447 read_task: tokio::task::JoinHandle<()>,
448 mut sink: Sink,
449 ) -> State {
450 match sink.send(Message::Close(None)).await {
451 Ok(()) => {
452 let handshake = async {
456 while let Some(frame) = frame_rx.recv().await {
457 if matches!(frame, Ok(Message::Close(_)) | Err(_)) {
458 break;
459 }
460 }
461 };
462 if timeout(self.config.close_timeout, handshake).await.is_err() {
463 warn!("close handshake timed out");
464 }
465 }
466 Err(e) => debug!(error = %e, "send close message failed"),
467 }
468 read_task.abort();
469
470 self.emit_lifecycle(Event::Disconnected {
471 reason: DisconnectReason::Requested,
472 })
473 .await;
474 State::Idle
475 }
476
477 fn add_topics(&mut self, topics: Vec<Topic>) -> Vec<Topic> {
479 let mut added = Vec::new();
480 for topic in topics {
481 if !self.topics.contains(&topic) && !added.contains(&topic) {
482 added.push(topic);
483 }
484 }
485 self.topics.extend(added.iter().cloned());
486 added
487 }
488
489 fn remove_topics(&mut self, topics: Vec<Topic>) -> Vec<Topic> {
491 let removed: Vec<Topic> = topics
492 .into_iter()
493 .filter(|topic| self.topics.contains(topic))
494 .collect();
495 self.topics.retain(|topic| !removed.contains(topic));
496 removed
497 }
498
499 fn auth_message(&self) -> Option<OutgoingMessage> {
501 let api_key = self.config.api_key.clone()?;
502 let api_secret = self.config.api_secret.clone()?;
503 Some(create_outgoing_message_auth_at(
504 api_key,
505 api_secret,
506 None,
507 self.config.auth_recv_window,
508 self.config.server_clock.now(),
509 ))
510 }
511
512 async fn send_topics(
516 &mut self,
517 sink: &mut Sink,
518 session: &mut Session,
519 subscribe: bool,
520 topics: &[Topic],
521 ) -> Result<(), tokio_tungstenite::tungstenite::Error> {
522 for chunk in topics.chunks(MAX_TOPICS_PER_REQUEST) {
523 let args = chunk.to_vec();
524 let msg = if subscribe {
525 self.next_req_id += 1;
526 let req_id = format!("sub-{}", self.next_req_id);
527 session.pending.insert(req_id.clone(), args.clone());
528 OutgoingMessage::Subscribe {
529 req_id: Some(req_id),
530 args,
531 }
532 } else {
533 OutgoingMessage::Unsubscribe { req_id: None, args }
534 };
535 send_message(sink, &msg).await?;
536 }
537 Ok(())
538 }
539
540 async fn on_command_reply(
542 &mut self,
543 msg: &IncomingMessage,
544 session: &mut Session,
545 sink: &mut Sink,
546 ) -> Result<(), tokio_tungstenite::tungstenite::Error> {
547 match msg {
548 IncomingMessage::Command(CommandMsg::Auth {
549 success, ret_msg, ..
550 }) if session.awaiting_auth => {
551 session.awaiting_auth = false;
552 if *success {
553 info!("stream authenticated");
554 session.ready = true;
555 self.emit_lifecycle(Event::Authenticated).await;
556 let topics = self.topics.clone();
557 self.send_topics(sink, session, true, &topics).await?;
558 } else {
559 warn!(?ret_msg, "stream authentication failed");
560 self.emit_lifecycle(Event::AuthFailed {
561 ret_msg: ret_msg.clone(),
562 })
563 .await;
564 }
565 }
566 IncomingMessage::TradeReply(reply) if session.awaiting_auth && reply.op == "auth" => {
569 session.awaiting_auth = false;
570 if reply.ret_code == 0 || reply.ret_code == 20001 {
571 info!("stream authenticated");
572 session.ready = true;
573 self.emit_lifecycle(Event::Authenticated).await;
574 let topics = self.topics.clone();
575 self.send_topics(sink, session, true, &topics).await?;
576 } else {
577 warn!(ret_code = reply.ret_code, ret_msg = %reply.ret_msg, "stream authentication failed");
578 self.emit_lifecycle(Event::AuthFailed {
579 ret_msg: Some(reply.ret_msg.clone()),
580 })
581 .await;
582 }
583 }
584 IncomingMessage::Command(CommandMsg::Subscribe {
585 req_id: Some(req_id),
586 success,
587 ret_msg,
588 ..
589 }) => {
590 if let Some(topics) = session.pending.remove(req_id)
591 && !*success
592 {
593 warn!(?topics, ?ret_msg, "subscribe rejected");
594 self.emit_lifecycle(Event::SubscribeFailed {
595 topics,
596 ret_msg: ret_msg.clone(),
597 })
598 .await;
599 }
600 }
601 _ => {}
602 }
603 Ok(())
604 }
605
606 async fn next_reconnect_state(&mut self, next_attempt: u32, reason: DisconnectReason) -> State {
607 match reconnect_delay_ms(&self.config, next_attempt) {
608 Some(delay_ms) => {
609 debug!(next_attempt, delay_ms, ?reason, "scheduling reconnect");
610 State::Reconnecting {
611 attempt: next_attempt,
612 delay_ms,
613 }
614 }
615 None => {
616 warn!(?reason, "giving up reconnecting");
617 self.emit_lifecycle(Event::Disconnected { reason }).await;
618 State::Idle
619 }
620 }
621 }
622}
623
624fn reconnect_delay_ms(config: &Config, attempt: u32) -> Option<u64> {
627 if attempt == 0 || attempt > config.max_reconnect_attempts {
628 return None;
629 }
630 let base_ms = config.reconnect_base_delay.as_millis() as u64;
631 let max_ms = config.reconnect_max_delay.as_millis() as u64;
632 Some(
633 base_ms
634 .saturating_mul(1u64 << (attempt - 1).min(10))
635 .min(max_ms),
636 )
637}
638
639const FAR_FUTURE: Duration = Duration::from_secs(u64::MAX / 4);
640
641#[inline]
642fn far_future_instant() -> Instant {
643 Instant::now() + FAR_FUTURE
644}
645
646async fn send_message(
649 sink: &mut Sink,
650 msg: &OutgoingMessage,
651) -> Result<(), tokio_tungstenite::tungstenite::Error> {
652 match serialize_json(msg) {
653 Ok(json) => sink.send(Message::Text(json.into())).await,
654 Err(e) => {
655 warn!(error = %e, "serializing outgoing message failed, message dropped");
656 Ok(())
657 }
658 }
659}
660
661#[inline]
663fn ping() -> Message {
664 Message::Text(r#"{"op":"ping"}"#.into())
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670 use crate::ws::OutgoingMessage;
671 use tokio::net::TcpListener;
672
673 async fn spawn_server() -> String {
676 spawn_server_with_greeting(Vec::new()).await
677 }
678
679 async fn spawn_server_with_greeting(greeting: Vec<String>) -> String {
682 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
683 let addr = listener.local_addr().unwrap();
684 tokio::spawn(async move {
685 while let Ok((tcp, _)) = listener.accept().await {
686 let greeting = greeting.clone();
687 tokio::spawn(async move {
688 let Ok(mut ws) = tokio_tungstenite::accept_async(tcp).await else {
689 return;
690 };
691 for text in greeting {
692 if ws.send(Message::Text(text.into())).await.is_err() {
693 return;
694 }
695 }
696 while let Some(Ok(_)) = ws.next().await {}
697 });
698 }
699 });
700 format!("ws://{addr}")
701 }
702
703 async fn closed_port_url() -> String {
706 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
707 let addr = listener.local_addr().unwrap();
708 drop(listener);
709 format!("ws://{addr}")
710 }
711
712 const EVENT_TIMEOUT: Duration = Duration::from_secs(10);
715
716 async fn next_event(rx: &mut mpsc::Receiver<Event>) -> Event {
717 timeout(EVENT_TIMEOUT, rx.recv())
718 .await
719 .expect("timed out waiting for event")
720 .expect("event channel closed")
721 }
722
723 fn test_config(url: String) -> Config {
724 Config::new(url)
725 .ping_interval(None)
726 .reconnect_base_delay(Duration::from_millis(10))
727 .reconnect_max_delay(Duration::from_millis(20))
728 }
729
730 #[test]
731 fn reconnect_delay_grows_exponentially_and_is_capped() {
732 let config = Config::new("")
733 .max_reconnect_attempts(5)
734 .reconnect_base_delay(Duration::from_millis(100))
735 .reconnect_max_delay(Duration::from_millis(500));
736
737 let delays: Vec<_> = (0..=6).map(|n| reconnect_delay_ms(&config, n)).collect();
738
739 assert_eq!(
740 delays,
741 vec![
742 None,
743 Some(100),
744 Some(200),
745 Some(400),
746 Some(500),
747 Some(500),
748 None
749 ]
750 );
751 }
752
753 #[test]
754 fn reconnect_is_disabled_with_zero_attempts() {
755 let config = Config::new("").max_reconnect_attempts(0);
756
757 assert_eq!(reconnect_delay_ms(&config, 1), None);
758 }
759
760 #[tokio::test]
761 async fn disconnect_completes_without_waiting_for_close_timeout() {
762 let url = spawn_server().await;
763 let (handle, mut events) =
764 Stream::new(test_config(url).close_timeout(Duration::from_secs(30)));
765
766 handle.connect().await.unwrap();
767 assert!(matches!(next_event(&mut events).await, Event::Connected));
768
769 handle.disconnect().await.unwrap();
770 assert!(matches!(
771 next_event(&mut events).await,
772 Event::Disconnected {
773 reason: DisconnectReason::Requested
774 }
775 ));
776 }
777
778 #[tokio::test]
779 async fn connect_right_after_disconnect_is_not_lost() {
780 let url = spawn_server().await;
781 let (handle, mut events) = Stream::new(test_config(url));
782
783 handle.connect().await.unwrap();
784 assert!(matches!(next_event(&mut events).await, Event::Connected));
785
786 handle.disconnect().await.unwrap();
787 handle.connect().await.unwrap();
788 assert!(matches!(
789 next_event(&mut events).await,
790 Event::Disconnected { .. }
791 ));
792 assert!(matches!(next_event(&mut events).await, Event::Connected));
793 }
794
795 #[tokio::test]
796 async fn initial_connect_failure_retries_max_attempts_then_gives_up() {
797 let url = closed_port_url().await;
798 let (handle, mut events) = Stream::new(test_config(url).max_reconnect_attempts(2));
799
800 handle.connect().await.unwrap();
801
802 assert!(matches!(
803 next_event(&mut events).await,
804 Event::Reconnecting { attempt: 1, .. }
805 ));
806 assert!(matches!(
807 next_event(&mut events).await,
808 Event::Reconnecting { attempt: 2, .. }
809 ));
810 assert!(matches!(
811 next_event(&mut events).await,
812 Event::Disconnected {
813 reason: DisconnectReason::Error(_)
814 }
815 ));
816 }
817
818 #[tokio::test]
819 async fn commands_during_backoff_do_not_skip_the_delay() {
820 let url = closed_port_url().await;
821 let config = test_config(url)
822 .max_reconnect_attempts(1)
823 .reconnect_base_delay(Duration::from_millis(300))
824 .reconnect_max_delay(Duration::from_millis(300));
825 let (handle, mut events) = Stream::new(config);
826
827 handle.connect().await.unwrap();
828 assert!(matches!(
829 next_event(&mut events).await,
830 Event::Reconnecting { attempt: 1, .. }
831 ));
832 let started = Instant::now();
833 handle.connect().await.unwrap();
834 handle
835 .send_command(OutgoingMessage::Ping { req_id: None })
836 .await
837 .unwrap();
838
839 assert!(matches!(
840 next_event(&mut events).await,
841 Event::Disconnected { .. }
842 ));
843 assert!(started.elapsed() >= Duration::from_millis(250));
844 }
845
846 #[tokio::test]
847 async fn dropped_data_events_are_reported_before_the_next_lifecycle_event() {
848 let pong = r#"{"op":"pong","conn_id":"test"}"#.to_string();
849 let url = spawn_server_with_greeting(vec![pong; 10]).await;
850 let (handle, mut events) = Stream::new(test_config(url).event_queue_size(2));
851
852 handle.connect().await.unwrap();
853 sleep(Duration::from_millis(300)).await;
855 assert!(matches!(next_event(&mut events).await, Event::Connected));
856 assert!(matches!(next_event(&mut events).await, Event::Message(_)));
857
858 handle.disconnect().await.unwrap();
859
860 assert!(matches!(
861 next_event(&mut events).await,
862 Event::Lagged { dropped: 9 }
863 ));
864 assert!(matches!(
865 next_event(&mut events).await,
866 Event::Disconnected { .. }
867 ));
868 }
869
870 #[derive(Clone, Copy)]
872 struct ServerScript {
873 auth_ok: bool,
875 close_first_after: Option<&'static str>,
877 }
878
879 type Received = std::sync::Arc<std::sync::Mutex<Vec<(usize, serde_json::Value)>>>;
881
882 async fn spawn_bybit_server(script: ServerScript) -> (String, Received) {
885 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
886 let addr = listener.local_addr().unwrap();
887 let received: Received = Default::default();
888 let recorded = received.clone();
889 tokio::spawn(async move {
890 let mut conn = 0;
891 while let Ok((tcp, _)) = listener.accept().await {
892 conn += 1;
893 let recorded = recorded.clone();
894 tokio::spawn(async move {
895 let Ok(mut ws) = tokio_tungstenite::accept_async(tcp).await else {
896 return;
897 };
898 while let Some(Ok(frame)) = ws.next().await {
899 let Message::Text(text) = frame else { continue };
900 let json: serde_json::Value = serde_json::from_str(&text).unwrap();
901 recorded.lock().unwrap().push((conn, json.clone()));
902 let op = json["op"].as_str().unwrap_or_default().to_owned();
903 let reply = match op.as_str() {
904 "auth" => Some(serde_json::json!({
905 "op": "auth", "conn_id": "c", "success": script.auth_ok,
906 "ret_msg": if script.auth_ok { "" } else { "Invalid apikey" },
907 })),
908 "subscribe" => {
909 let ok = !json["args"].to_string().contains("BAD");
910 Some(serde_json::json!({
911 "op": "subscribe", "conn_id": "c", "success": ok,
912 "ret_msg": if ok { "" } else { "error:handler not found" },
913 "req_id": json["req_id"],
914 }))
915 }
916 _ => None,
917 };
918 if let Some(reply) = reply
919 && ws
920 .send(Message::Text(reply.to_string().into()))
921 .await
922 .is_err()
923 {
924 return;
925 }
926 if conn == 1 && script.close_first_after == Some(op.as_str()) {
927 return;
928 }
929 }
930 });
931 }
932 });
933 (format!("ws://{addr}"), received)
934 }
935
936 async fn wait_for_frame(received: &Received, pred: impl Fn(usize, &serde_json::Value) -> bool) {
938 timeout(EVENT_TIMEOUT, async {
939 loop {
940 if received.lock().unwrap().iter().any(|(c, j)| pred(*c, j)) {
941 return;
942 }
943 sleep(Duration::from_millis(10)).await;
944 }
945 })
946 .await
947 .expect("timed out waiting for a frame");
948 }
949
950 async fn wait_for_event(
952 rx: &mut mpsc::Receiver<Event>,
953 pred: impl Fn(&Event) -> bool,
954 ) -> Event {
955 loop {
956 let event = next_event(rx).await;
957 if pred(&event) {
958 return event;
959 }
960 }
961 }
962
963 fn ops(received: &Received, conn: usize) -> Vec<String> {
964 received
965 .lock()
966 .unwrap()
967 .iter()
968 .filter(|(c, _)| *c == conn)
969 .map(|(_, j)| j["op"].as_str().unwrap_or_default().to_owned())
970 .collect()
971 }
972
973 fn subscribed_args(received: &Received, conn: usize) -> Vec<String> {
974 received
975 .lock()
976 .unwrap()
977 .iter()
978 .filter(|(c, j)| *c == conn && j["op"] == "subscribe")
979 .flat_map(|(_, j)| j["args"].as_array().unwrap().clone())
980 .map(|a| a.as_str().unwrap().to_owned())
981 .collect()
982 }
983
984 fn ticker(symbol: &str) -> Topic {
985 Topic::Ticker(symbol.to_owned())
986 }
987
988 #[tokio::test]
989 async fn authenticates_then_restores_subscriptions_after_reconnect() {
990 let (url, received) = spawn_bybit_server(ServerScript {
991 auth_ok: true,
992 close_first_after: Some("subscribe"),
993 })
994 .await;
995 let (handle, mut events) = Stream::new(test_config(url).credentials("key", "secret"));
996
997 handle
998 .subscribe(vec![ticker("BTCUSDT"), ticker("ETHUSDT")])
999 .await
1000 .unwrap();
1001 handle.connect().await.unwrap();
1002
1003 wait_for_event(&mut events, |e| matches!(e, Event::Authenticated)).await;
1004 wait_for_frame(&received, |c, j| c == 2 && j["op"] == "subscribe").await;
1005
1006 for conn in [1, 2] {
1007 assert_eq!(
1008 ops(&received, conn),
1009 ["auth", "subscribe"],
1010 "connection {conn}"
1011 );
1012 assert_eq!(
1013 subscribed_args(&received, conn),
1014 ["tickers.BTCUSDT", "tickers.ETHUSDT"],
1015 "connection {conn}"
1016 );
1017 }
1018 let auth = &received.lock().unwrap()[0].1;
1019 assert_eq!(auth["args"][0], "key");
1020 }
1021
1022 #[tokio::test]
1023 async fn unsubscribed_topics_are_not_restored() {
1024 let (url, received) = spawn_bybit_server(ServerScript {
1025 auth_ok: true,
1026 close_first_after: Some("unsubscribe"),
1027 })
1028 .await;
1029 let (handle, _events) = Stream::new(test_config(url));
1030 handle.connect().await.unwrap();
1031
1032 handle
1033 .subscribe(vec![ticker("BTCUSDT"), ticker("ETHUSDT")])
1034 .await
1035 .unwrap();
1036 wait_for_frame(&received, |c, j| c == 1 && j["op"] == "subscribe").await;
1037 handle.unsubscribe(vec![ticker("BTCUSDT")]).await.unwrap();
1038 wait_for_frame(&received, |c, j| c == 2 && j["op"] == "subscribe").await;
1039
1040 assert_eq!(ops(&received, 1), ["subscribe", "unsubscribe"]);
1041 assert_eq!(subscribed_args(&received, 2), ["tickers.ETHUSDT"]);
1042 }
1043
1044 #[tokio::test]
1045 async fn rejected_auth_is_reported_and_nothing_is_subscribed() {
1046 let (url, received) = spawn_bybit_server(ServerScript {
1047 auth_ok: false,
1048 close_first_after: None,
1049 })
1050 .await;
1051 let (handle, mut events) = Stream::new(test_config(url).credentials("key", "bad"));
1052 handle.subscribe(vec![ticker("BTCUSDT")]).await.unwrap();
1053 handle.connect().await.unwrap();
1054
1055 let event = wait_for_event(&mut events, |e| matches!(e, Event::AuthFailed { .. })).await;
1056 sleep(Duration::from_millis(200)).await;
1057
1058 assert!(matches!(
1059 event,
1060 Event::AuthFailed { ret_msg: Some(ref m) } if m == "Invalid apikey"
1061 ));
1062 assert_eq!(ops(&received, 1), ["auth"]);
1063 }
1064
1065 #[tokio::test]
1066 async fn rejected_subscribe_is_reported_with_its_topics() {
1067 let (url, _received) = spawn_bybit_server(ServerScript {
1068 auth_ok: true,
1069 close_first_after: None,
1070 })
1071 .await;
1072 let (handle, mut events) = Stream::new(test_config(url));
1073 handle.connect().await.unwrap();
1074
1075 handle.subscribe(vec![ticker("BAD")]).await.unwrap();
1076 let event =
1077 wait_for_event(&mut events, |e| matches!(e, Event::SubscribeFailed { .. })).await;
1078
1079 assert!(matches!(
1080 event,
1081 Event::SubscribeFailed { ref topics, .. } if *topics == [ticker("BAD")]
1082 ));
1083 }
1084
1085 #[tokio::test]
1086 async fn subscriptions_are_sent_in_batches_of_ten() {
1087 let (url, received) = spawn_bybit_server(ServerScript {
1088 auth_ok: true,
1089 close_first_after: None,
1090 })
1091 .await;
1092 let (handle, _events) = Stream::new(test_config(url));
1093 let topics: Vec<Topic> = (0..25).map(|i| ticker(&format!("SYM{i}"))).collect();
1094 handle.subscribe(topics).await.unwrap();
1095 handle.subscribe(vec![ticker("SYM0")]).await.unwrap();
1097 handle.connect().await.unwrap();
1098
1099 wait_for_frame(&received, |_, j| j["args"].to_string().contains("SYM24")).await;
1100 sleep(Duration::from_millis(100)).await;
1101
1102 let sizes: Vec<usize> = received
1103 .lock()
1104 .unwrap()
1105 .iter()
1106 .map(|(_, j)| j["args"].as_array().unwrap().len())
1107 .collect();
1108 assert_eq!(sizes, [10, 10, 5]);
1109 }
1110}