Skip to main content

bybit/ws/
stream.rs

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
23/// WebSocket connection driver.
24///
25/// [`Stream::new`] spawns the driver on the current Tokio runtime and returns a
26/// [`Handle`] for commands and a receiver of [`Event`]s.
27///
28/// # Reconnects
29/// After an unexpected disconnect the driver reconnects automatically (see
30/// [`Config::max_reconnect_attempts`]) and emits [`Event::Connected`] again.
31///
32/// Topics subscribed with [`Handle::subscribe`] are restored on every
33/// connection. With [`Config::credentials`] the driver first sends `auth`
34/// (expiry from [`Config::server_clock`]) and subscribes only after
35/// [`Event::Authenticated`]; a rejected `auth` is reported as
36/// [`Event::AuthFailed`]. Messages sent with [`Handle::send_command`] are not
37/// restored.
38///
39/// # Event delivery
40/// Lifecycle events (`Connected`, `Reconnecting`, `Disconnected`) are always
41/// delivered: the driver waits for free space in the event queue. Market and
42/// account data (`Message`, `ParseError`) are dropped when the queue is full;
43/// the number of dropped events is reported by [`Event::Lagged`] as soon as
44/// there is room again.
45pub struct Stream {
46    config: Config,
47    cmd_rx: mpsc::Receiver<Command>,
48    evt_tx: mpsc::Sender<Event>,
49    /// Data events dropped since the last [`Event::Lagged`].
50    dropped: u64,
51    /// Topics to (re)subscribe on every connection, in subscription order.
52    topics: Vec<Topic>,
53    /// Counter for the `req_id` of driver-sent `subscribe` messages.
54    next_req_id: u64,
55}
56
57/// Bybit accepts at most 10 topics in one `subscribe` for some streams
58/// (e.g. spot); use that limit everywhere.
59const MAX_TOPICS_PER_REQUEST: usize = 10;
60
61/// Per-connection authentication and subscription progress.
62#[derive(Default)]
63struct Session {
64    /// `auth` was sent and its reply has not arrived yet.
65    awaiting_auth: bool,
66    /// Topics may be subscribed (no credentials, or `auth` succeeded).
67    ready: bool,
68    /// Topics of driver-sent `subscribe` messages, by `req_id`.
69    pending: HashMap<String, Vec<Topic>>,
70}
71
72impl Stream {
73    /// Spawn the driver on the current Tokio runtime; returns the command handle and the event receiver. The driver does not connect until [`Handle::connect`].
74    #[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    /// Deliver a data event without blocking the driver; drop it (and count
122    /// it) when the event queue is full.
123    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    /// Deliver a lifecycle event, waiting for space in the event queue.
151    /// A pending [`Event::Lagged`] is delivered first to keep the order.
152    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        // Keep serving commands while the handshake is in flight so that a
199        // `Disconnect` is not blocked behind a slow or hanging connection.
200        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                                        // INFO: Bybit heartbeat process.
316                                        // - send to server: { "op": "ping" }
317                                        // - for public channels receive from server: { "op": "ping", "ret_msg": "pong", "success": true }
318                                        // - for private channels receive from server: { "op": "pong" }
319                                        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        // Commands that arrive during the back-off must neither shorten the
418        // delay nor vanish silently, so the deadline is fixed up front.
419        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                // Wait for the server to answer with its own Close frame (or to
453                // drop the connection). Commands are not read here, so nothing
454                // the user sends meanwhile is lost: it is handled in `Idle`.
455                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    /// Remember `topics`; returns those that were not subscribed yet.
478    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    /// Forget `topics`; returns those that were subscribed.
490    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    /// The `auth` message, if credentials are configured.
500    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    /// Send `subscribe` (or `unsubscribe`) for `topics` in batches of
513    /// [`MAX_TOPICS_PER_REQUEST`]; `subscribe` batches get a `req_id` so that
514    /// a rejection can be reported with its topics.
515    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    /// React to replies to driver-sent `auth` and `subscribe` messages.
541    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            // The order entry stream replies to `auth` in its own format;
567            // 20001 means "already authenticated".
568            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
624/// Back-off delay before reconnect attempt number `attempt` (1-based), or
625/// `None` when the attempt exceeds `max_reconnect_attempts`.
626fn 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
646/// Serialize and send `msg`; a serialization failure is logged and the message
647/// dropped (it cannot be fixed by reconnecting).
648async 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/// Bybit application-level heartbeat (`OutgoingMessage::Ping` without `req_id`).
662#[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    /// Local WebSocket server that accepts any number of connections and
674    /// answers the close handshake. Returns its `ws://` URL.
675    async fn spawn_server() -> String {
676        spawn_server_with_greeting(Vec::new()).await
677    }
678
679    /// Like [`spawn_server`], but sends `greeting` text frames right after
680    /// every handshake.
681    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    /// An address nothing listens on. Connecting to it fails at once on
704    /// Linux and macOS, but takes about 2 s on Windows (SYN retries).
705    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    /// Generous upper bound for a single event; tests do not wait this long
713    /// unless something is wrong (see [`closed_port_url`] for Windows).
714    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        // Let the driver fill the queue (Connected + 1 message) and drop the rest.
854        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    /// Behaviour of [`spawn_bybit_server`].
871    #[derive(Clone, Copy)]
872    struct ServerScript {
873        /// Reply `success` to `auth`.
874        auth_ok: bool,
875        /// On the first connection, close right after receiving this `op`.
876        close_first_after: Option<&'static str>,
877    }
878
879    /// Text frames received, as (connection number, parsed JSON).
880    type Received = std::sync::Arc<std::sync::Mutex<Vec<(usize, serde_json::Value)>>>;
881
882    /// Local server that answers `auth` and `subscribe` like Bybit (topics
883    /// containing "BAD" are rejected) and records what it receives.
884    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    /// Wait until `received` holds a frame matching `pred`.
937    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    /// Skip events until one matches `pred`.
951    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        // Subscribing again to known topics sends nothing.
1096        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}