Skip to main content

binance_sdk/common/
websocket.rs

1use async_trait::async_trait;
2use flate2::read::ZlibDecoder;
3use futures::{SinkExt, StreamExt, future::try_join_all, stream::FuturesUnordered};
4use http::header::USER_AGENT;
5use serde::de::DeserializeOwned;
6use serde_json::{Value, json};
7use std::{
8    collections::{BTreeMap, HashMap, VecDeque},
9    io::Read,
10    marker::PhantomData,
11    mem::take,
12    sync::{
13        Arc, Weak,
14        atomic::{AtomicBool, AtomicUsize, Ordering},
15    },
16    time::Duration,
17};
18use tokio::{
19    net::TcpStream,
20    select, spawn,
21    sync::{
22        Mutex, Notify,
23        mpsc::{
24            Receiver, Sender, UnboundedSender, WeakUnboundedSender, channel, unbounded_channel,
25        },
26        oneshot,
27    },
28    task::JoinHandle,
29    time::{sleep, timeout},
30};
31use tokio_tungstenite::{
32    MaybeTlsStream, WebSocketStream, connect_async_tls_with_config,
33    tungstenite::{
34        Message,
35        client::IntoClientRequest,
36        protocol::{CloseFrame, WebSocketConfig, frame::coding::CloseCode},
37    },
38};
39use tokio_util::{sync::CancellationToken, time::DelayQueue};
40use tracing::{debug, error, info, warn};
41
42use super::{
43    config::{
44        AgentConnector, ConfigurationWebsocketApi, ConfigurationWebsocketStreams,
45        WebsocketHandshakeFn,
46    },
47    errors::{WebsocketConnectionFailureReason, WebsocketError},
48    models::{StreamId, WebsocketApiResponse, WebsocketEvent, WebsocketMode},
49    utils::{
50        SignatureGenerator, build_websocket_api_message, get_timestamp_from, normalize_stream_id,
51        random_string, validate_time_unit,
52    },
53};
54
55pub type WebSocketClient = WebSocketStream<MaybeTlsStream<TcpStream>>;
56
57const MAX_CONN_DURATION: Duration = Duration::from_secs(23 * 60 * 60);
58const SIGNED_STREAMS_PING_INTERVAL: Duration = Duration::from_secs(30);
59const SIGNED_STREAMS_RECV_WINDOW: u64 = 30000;
60const RECONNECT_RETRY_BASE_DELAY: Duration = Duration::from_secs(1);
61const RECONNECT_RETRY_MAX_DELAY: Duration = Duration::from_secs(60);
62const WRITER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
63
64pub struct Subscription {
65    handle: JoinHandle<()>,
66}
67
68impl Subscription {
69    /// Cancels the ongoing WebSocket event subscription and stops the event processing task.
70    ///
71    /// This method aborts the background task responsible for receiving and processing
72    /// WebSocket events, effectively unsubscribing from further event notifications.
73    ///
74    /// # Examples
75    ///
76    ///
77    /// let emitter = `WebsocketEventEmitter::new()`;
78    /// let subscription = emitter.subscribe(|event| {
79    ///     // Handle WebSocket event
80    /// });
81    /// `subscription.unsubscribe()`; // Stop receiving events
82    ///
83    pub fn unsubscribe(self) {
84        self.handle.abort();
85    }
86}
87
88#[derive(Clone)]
89pub enum WebsocketBase {
90    WebsocketApi(Arc<WebsocketApi>),
91    WebsocketStreams(Arc<WebsocketStreams>),
92}
93
94pub struct WebsocketEventEmitter {
95    subscribers: Arc<std::sync::Mutex<Vec<UnboundedSender<WebsocketEvent>>>>,
96}
97
98impl Default for WebsocketEventEmitter {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl WebsocketEventEmitter {
105    #[must_use]
106    pub fn new() -> Self {
107        Self {
108            subscribers: Arc::new(std::sync::Mutex::new(Vec::new())),
109        }
110    }
111
112    /// Subscribes to WebSocket events and returns a `Subscription` that allows event processing.
113    ///
114    /// This method creates an unbounded channel for receiving WebSocket events and
115    /// spawns an asynchronous task to process these events using the provided callback function.
116    ///
117    /// # Arguments
118    ///
119    /// * `callback` - A mutable function that will be called for each received WebSocket event.
120    ///   The callback must be thread-safe and have a static lifetime.
121    ///
122    /// # Returns
123    ///
124    /// A `Subscription` that can be used to unsubscribe and stop event processing.
125    ///
126    /// # Examples
127    ///
128    ///
129    /// let emitter = `WebsocketEventEmitter::new()`;
130    /// let subscription = emitter.subscribe(|event| {
131    ///     // Handle WebSocket event
132    ///     println!("Received event: {:?}", event);
133    /// });
134    ///
135    /// // Later, when no longer needed
136    /// `subscription.unsubscribe()`;
137    ///
138    pub fn subscribe<F>(&self, mut callback: F) -> Subscription
139    where
140        F: FnMut(WebsocketEvent) + Send + 'static,
141    {
142        let (tx, mut rx) = unbounded_channel();
143        let mut guard = match self.subscribers.lock() {
144            Ok(guard) => guard,
145            Err(poisoned) => poisoned.into_inner(),
146        };
147        guard.push(tx);
148        drop(guard);
149
150        let handle = spawn(async move {
151            while let Some(event) = rx.recv().await {
152                callback(event);
153            }
154        });
155        Subscription { handle }
156    }
157
158    /// Emits a WebSocket event to all registered subscribers.
159    ///
160    /// This method sends the given event to all active subscribers. If a subscriber
161    /// has been dropped without unsubscribing, a warning is logged and the subscriber
162    /// is removed from the list.
163    ///
164    /// # Arguments
165    ///
166    /// * `event` - The WebSocket event to be emitted to all subscribers.
167    pub fn emit(&self, event: &WebsocketEvent) {
168        let mut guard = match self.subscribers.lock() {
169            Ok(guard) => guard,
170            Err(poisoned) => poisoned.into_inner(),
171        };
172
173        guard.retain(|tx| {
174            if tx.send(event.clone()).is_ok() {
175                true
176            } else {
177                warn!("subscriber dropped without unsubscribing");
178                false
179            }
180        });
181    }
182}
183
184/// A trait defining the lifecycle and behavior of a WebSocket connection.
185///
186/// This trait provides methods for handling WebSocket connection events,
187/// including connection opening, message handling, and reconnection URL retrieval.
188///
189/// # Methods
190///
191/// * `on_open`: Called when a WebSocket connection is established
192/// * `on_message`: Called when a message is received over the WebSocket
193/// * `get_reconnect_url`: Determines the URL to use for reconnecting
194///
195/// # Thread Safety
196///
197/// Implementors must be safely shareable across threads, as indicated by the `Send + Sync + 'static` bounds.
198#[async_trait]
199pub trait WebsocketHandler: Send + Sync + 'static {
200    async fn on_open(&self, url: String, connection: Arc<WebsocketConnection>);
201    async fn on_message(&self, data: String, connection: Arc<WebsocketConnection>);
202    async fn get_reconnect_url(
203        &self,
204        default_url: String,
205        connection: Arc<WebsocketConnection>,
206    ) -> String;
207}
208
209/// A [`WebsocketHandler`] that forwards to another handler through a weak reference.
210///
211/// `WebsocketApi` and `WebsocketStreams` register themselves as the handler of every
212/// connection in their pool. Those connections live inside the `WebsocketCommon` that
213/// the client itself owns, so storing a strong reference there would form a cycle
214/// (client -> common -> connection -> handler -> client) and the client would never be
215/// freed. Once the client is dropped, the callbacks become no-ops.
216struct WeakHandler<H: WebsocketHandler>(Weak<H>);
217
218#[async_trait]
219impl<H: WebsocketHandler> WebsocketHandler for WeakHandler<H> {
220    async fn on_open(&self, url: String, connection: Arc<WebsocketConnection>) {
221        if let Some(handler) = self.0.upgrade() {
222            handler.on_open(url, connection).await;
223        }
224    }
225
226    async fn on_message(&self, data: String, connection: Arc<WebsocketConnection>) {
227        if let Some(handler) = self.0.upgrade() {
228            handler.on_message(data, connection).await;
229        }
230    }
231
232    async fn get_reconnect_url(
233        &self,
234        default_url: String,
235        connection: Arc<WebsocketConnection>,
236    ) -> String {
237        match self.0.upgrade() {
238            Some(handler) => handler.get_reconnect_url(default_url, connection).await,
239            None => default_url,
240        }
241    }
242}
243
244pub struct PendingRequest {
245    pub completion: oneshot::Sender<Result<Value, WebsocketError>>,
246}
247
248#[derive(Clone)]
249pub struct WebsocketSessionLogonReq {
250    pub method: String,
251    pub payload: BTreeMap<String, Value>,
252    pub options: WebsocketMessageSendOptions,
253}
254
255pub struct WebsocketConnectionState {
256    pub reconnection_pending: bool,
257    pub renewal_pending: bool,
258    pub close_initiated: bool,
259    pub pending_requests: HashMap<String, PendingRequest>,
260    pub pending_subscriptions: VecDeque<String>,
261    pub stream_callbacks: HashMap<String, Vec<Arc<dyn Fn(&Value) + Send + Sync + 'static>>>,
262    pub is_session_logged_on: bool,
263    pub session_logon_req: Option<WebsocketSessionLogonReq>,
264    pub url_path: Option<String>,
265    pub handler: Option<Arc<dyn WebsocketHandler>>,
266    pub ws_write_tx: Option<UnboundedSender<Message>>,
267    pub ws_writer_cancel: Option<CancellationToken>,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271enum ReconnectDisposition {
272    Started,
273    StaleWriter,
274    Rejected,
275}
276
277impl Default for WebsocketConnectionState {
278    fn default() -> Self {
279        Self::new()
280    }
281}
282
283impl WebsocketConnectionState {
284    #[must_use]
285    pub fn new() -> Self {
286        Self {
287            reconnection_pending: false,
288            renewal_pending: false,
289            close_initiated: false,
290            pending_requests: HashMap::new(),
291            pending_subscriptions: VecDeque::new(),
292            stream_callbacks: HashMap::new(),
293            is_session_logged_on: false,
294            session_logon_req: None,
295            url_path: None,
296            handler: None,
297            ws_write_tx: None,
298            ws_writer_cancel: None,
299        }
300    }
301
302    fn is_current_writer(&self, writer: &UnboundedSender<Message>) -> bool {
303        self.ws_write_tx
304            .as_ref()
305            .is_some_and(|current| current.same_channel(writer))
306    }
307
308    fn begin_reconnect(
309        &mut self,
310        writer: &UnboundedSender<Message>,
311        failure_reason: WebsocketConnectionFailureReason,
312    ) -> ReconnectDisposition {
313        if !self.is_current_writer(writer) {
314            return ReconnectDisposition::StaleWriter;
315        }
316        if self.close_initiated || !failure_reason.should_reconnect() {
317            return ReconnectDisposition::Rejected;
318        }
319
320        self.reconnection_pending = true;
321        self.is_session_logged_on = false;
322        self.ws_write_tx = None;
323        if let Some(cancel) = self.ws_writer_cancel.take() {
324            cancel.cancel();
325        }
326        ReconnectDisposition::Started
327    }
328}
329
330pub struct WebsocketConnection {
331    pub id: String,
332    pub drain_notify: Notify,
333    pub state: Mutex<WebsocketConnectionState>,
334    recovery_lock: Mutex<()>,
335}
336
337impl WebsocketConnection {
338    pub fn new(id: impl Into<String>) -> Arc<Self> {
339        Arc::new(Self {
340            id: id.into(),
341            drain_notify: Notify::new(),
342            state: Mutex::new(WebsocketConnectionState::new()),
343            recovery_lock: Mutex::new(()),
344        })
345    }
346
347    pub async fn set_handler(&self, handler: Arc<dyn WebsocketHandler>) {
348        let mut conn_state = self.state.lock().await;
349        conn_state.handler = Some(handler);
350    }
351}
352
353struct ReconnectEntry {
354    connection_id: String,
355    url: String,
356    is_renewal: bool,
357    attempt: u32,
358    expected_writer: Option<WeakUnboundedSender<Message>>,
359}
360
361impl ReconnectEntry {
362    /// Whether this entry still applies to the current state of its connection.
363    fn is_still_needed(&self, state: &WebsocketConnectionState) -> bool {
364        if state.close_initiated {
365            return false;
366        }
367        if !self.is_renewal {
368            return state.reconnection_pending;
369        }
370        match &self.expected_writer {
371            // Plain renewal (timer / serverShutdown): no fencing.
372            None => true,
373            Some(expected) => match (state.ws_write_tx.as_ref(), expected.upgrade()) {
374                (Some(current), Some(expected)) => current.same_channel(&expected),
375                _ => false,
376            },
377        }
378    }
379}
380
381pub struct WebsocketCommon {
382    pub events: WebsocketEventEmitter,
383    mode: WebsocketMode,
384    round_robin_index: AtomicUsize,
385    connection_pool: Vec<Arc<WebsocketConnection>>,
386    reconnect_tx: Sender<ReconnectEntry>,
387    renewal_tx: Sender<(String, String)>,
388    reconnect_delay: usize,
389    agent: Option<AgentConnector>,
390    handshake: Option<WebsocketHandshakeFn>,
391    user_agent: Option<String>,
392    extra_headers: std::sync::RwLock<Vec<(String, String)>>,
393}
394
395impl WebsocketCommon {
396    #[must_use]
397    pub fn new(
398        initial_pool: Vec<Arc<WebsocketConnection>>,
399        mode: WebsocketMode,
400        reconnect_delay: usize,
401        agent: Option<AgentConnector>,
402        user_agent: Option<String>,
403    ) -> Arc<Self> {
404        Self::new_with_handshake(initial_pool, mode, reconnect_delay, agent, user_agent, None)
405    }
406
407    /// Same as [`Self::new`], but additionally accepts a caller-supplied
408    /// [`WebsocketHandshakeFn`] used in place of the SDK's default handshake path.
409    /// Kept private: reachable only via `ConfigurationWebsocketApi::handshake` /
410    /// `ConfigurationWebsocketStreams::handshake` to avoid growing the public
411    /// constructor surface further.
412    fn new_with_handshake(
413        mut initial_pool: Vec<Arc<WebsocketConnection>>,
414        mode: WebsocketMode,
415        reconnect_delay: usize,
416        agent: Option<AgentConnector>,
417        user_agent: Option<String>,
418        handshake: Option<WebsocketHandshakeFn>,
419    ) -> Arc<Self> {
420        if initial_pool.is_empty() {
421            for _ in 0..mode.pool_size() {
422                let id = random_string();
423                initial_pool.push(WebsocketConnection::new(id));
424            }
425        }
426
427        let (reconnect_tx, reconnect_rx) = channel::<ReconnectEntry>(mode.pool_size());
428        let (renewal_tx, renewal_rx) = channel::<(String, String)>(mode.pool_size());
429
430        let common = Arc::new(Self {
431            events: WebsocketEventEmitter::new(),
432            mode,
433            round_robin_index: AtomicUsize::new(0),
434            connection_pool: initial_pool,
435            reconnect_tx,
436            renewal_tx,
437            reconnect_delay,
438            agent,
439            handshake,
440            user_agent,
441            extra_headers: std::sync::RwLock::new(Vec::new()),
442        });
443
444        // The loops hold only weak references: they must not keep `WebsocketCommon`
445        // alive, otherwise a dropped client would never be freed (its `reconnect_tx`
446        // and `renewal_tx` live inside it, so the channels would never close).
447        Self::spawn_reconnect_loop(Arc::downgrade(&common), reconnect_rx);
448        Self::spawn_renewal_loop(Arc::downgrade(&common), renewal_rx);
449
450        common
451    }
452
453    /// Spawns an asynchronous loop to handle websocket reconnection attempts
454    ///
455    /// This method manages reconnection logic for websocket connections, including:
456    /// - Scheduling reconnects with a configurable delay
457    /// - Finding the appropriate connection in the connection pool
458    /// - Attempting to reinitialize the connection
459    /// - Logging reconnection failures or warnings
460    ///
461    /// # Arguments
462    /// * `common` - A weak reference to the `WebsocketCommon` instance
463    /// * `reconnect_rx` - A receiver channel for reconnection entries
464    ///
465    /// # Behavior
466    /// - Waits for reconnection entries from the channel
467    /// - Handles each entry in its own task, so a slow or stalled handshake of one
468    ///   pool connection does not delay the recovery of the others
469    /// - Serializes the attempts of the same connection (`recovery_lock`)
470    /// - Applies a configurable delay before attempting reconnection
471    /// - Attempts to reinitialize the connection with the provided URL
472    /// - Handles and logs any reconnection errors
473    /// - Exits once the `WebsocketCommon` has been dropped
474    fn spawn_reconnect_loop(common: Weak<Self>, mut reconnect_rx: Receiver<ReconnectEntry>) {
475        spawn(async move {
476            while let Some(entry) = reconnect_rx.recv().await {
477                // Only check that the client is still alive; each worker upgrades
478                // `common` on its own so it never keeps a dropped client alive while
479                // it waits.
480                let Some(alive) = common.upgrade() else {
481                    break;
482                };
483                let Some(conn_arc) = alive
484                    .connection_pool
485                    .iter()
486                    .find(|c| c.id == entry.connection_id)
487                    .cloned()
488                else {
489                    warn!("No connection {} found for reconnect", entry.connection_id);
490                    continue;
491                };
492                let reconnect_delay = Duration::from_millis(alive.reconnect_delay as u64);
493                drop(alive);
494
495                let common = common.clone();
496                spawn(async move {
497                    Self::handle_reconnect_entry(common, conn_arc, entry, reconnect_delay).await;
498                });
499            }
500            debug!("Reconnect loop exiting");
501        });
502    }
503
504    /// Performs one automatic reconnect / renewal attempt for `connection`.
505    ///
506    /// Attempts of the same connection are serialized by its `recovery_lock`; attempts
507    /// of different connections run concurrently. After an attempt, the lock is held
508    /// for one more second so that back-to-back entries of the same connection are
509    /// paced, without delaying other connections.
510    async fn handle_reconnect_entry(
511        common: Weak<Self>,
512        connection: Arc<WebsocketConnection>,
513        entry: ReconnectEntry,
514        reconnect_delay: Duration,
515    ) {
516        let _recovery = connection.recovery_lock.lock().await;
517        info!("Scheduling reconnect for id {}", entry.connection_id);
518
519        if !entry.is_renewal {
520            sleep(reconnect_delay).await;
521        }
522
523        let Some(common) = common.upgrade() else {
524            return;
525        };
526        // A fenced renewal whose socket was replaced while this entry was queued must
527        // not replace the newer socket.
528        if entry.expected_writer.is_some()
529            && !entry.is_still_needed(&*connection.state.lock().await)
530        {
531            debug!("Dropping stale renewal for {}", entry.connection_id);
532            return;
533        }
534        if let Err(err) = Arc::clone(&common)
535            .init_connect(&entry.url, entry.is_renewal, Some(connection.clone()))
536            .await
537        {
538            error!(
539                "Reconnect failed for {} → {} (attempt {}): {:?}",
540                entry.connection_id,
541                entry.url,
542                entry.attempt + 1,
543                err
544            );
545            common.schedule_reconnect_retry(&connection, entry).await;
546        }
547        drop(common);
548
549        sleep(Duration::from_secs(1)).await;
550    }
551
552    /// Computes the backoff applied before retry number `attempt + 1` of a failed
553    /// automatic reconnect: exponential in `attempt`, capped at
554    /// [`RECONNECT_RETRY_MAX_DELAY`], with "equal jitter" (a random value in the
555    /// upper half of the capped delay) so pool connections do not retry in lockstep.
556    fn reconnect_retry_delay(attempt: u32) -> Duration {
557        let exp = RECONNECT_RETRY_BASE_DELAY
558            .saturating_mul(2u32.saturating_pow(attempt))
559            .min(RECONNECT_RETRY_MAX_DELAY);
560        let half = exp / 2;
561        half + half.mul_f64(rand::random::<f64>())
562    }
563
564    /// Re-enqueues a reconnect entry whose connection attempt failed, after a backoff.
565    ///
566    /// A retry is scheduled only while the connection still needs it:
567    /// - never once a close has been initiated (`disconnect()` or client drop);
568    /// - for a reconnect, only while `reconnection_pending` is still set (it is
569    ///   cleared once a writer is installed or the reconnect is abandoned);
570    /// - for a renewal, `renewal_pending` is reset (otherwise every later renewal
571    ///   would be skipped as "in progress") and the renewal is retried only while the
572    ///   previous socket is still alive; if it has died meanwhile, its reader has
573    ///   already scheduled an ordinary reconnect.
574    ///
575    /// The backoff runs in its own task so a failing connection does not block the
576    /// recovery of the other connections of the pool. That task holds only a weak
577    /// reference, so it never keeps a dropped client alive.
578    async fn schedule_reconnect_retry(
579        self: &Arc<Self>,
580        connection: &Arc<WebsocketConnection>,
581        entry: ReconnectEntry,
582    ) {
583        let mut entry = entry;
584        {
585            let mut conn_state = connection.state.lock().await;
586            if entry.is_renewal {
587                conn_state.renewal_pending = false;
588                // Fence the retry to the socket it is meant to replace.
589                if entry.expected_writer.is_none() {
590                    entry.expected_writer = conn_state
591                        .ws_write_tx
592                        .as_ref()
593                        .map(UnboundedSender::downgrade);
594                }
595            }
596            let needed = if entry.is_renewal {
597                conn_state.ws_write_tx.is_some() && entry.is_still_needed(&conn_state)
598            } else {
599                conn_state.reconnection_pending
600            };
601            if conn_state.close_initiated || !needed {
602                debug!(
603                    "Not retrying {} for {} (close_initiated={}, needed={})",
604                    if entry.is_renewal {
605                        "renewal"
606                    } else {
607                        "reconnect"
608                    },
609                    connection.id,
610                    conn_state.close_initiated,
611                    needed
612                );
613                return;
614            }
615        }
616
617        let delay = Self::reconnect_retry_delay(entry.attempt);
618        warn!(
619            "Retrying {} for {} in {:?} (attempt {})",
620            if entry.is_renewal {
621                "renewal"
622            } else {
623                "reconnect"
624            },
625            connection.id,
626            delay,
627            entry.attempt + 2
628        );
629
630        let common = Arc::downgrade(self);
631        let connection = Arc::clone(connection);
632        spawn(async move {
633            sleep(delay).await;
634            let Some(common) = common.upgrade() else {
635                return;
636            };
637            // The connection may have recovered, been renewed or been closed meanwhile.
638            if !entry.is_still_needed(&*connection.state.lock().await) {
639                debug!(
640                    "Dropping stale {} retry for {}",
641                    if entry.is_renewal {
642                        "renewal"
643                    } else {
644                        "reconnect"
645                    },
646                    connection.id
647                );
648                return;
649            }
650            let retry = ReconnectEntry {
651                attempt: entry.attempt.saturating_add(1),
652                ..entry
653            };
654            if let Err(e) = common.reconnect_tx.send(retry).await {
655                error!("Failed to enqueue reconnect retry: {:?}", e);
656            }
657        });
658    }
659
660    /// Spawns an asynchronous loop to manage connection renewals
661    ///
662    /// This method handles the periodic renewal of websocket connections by:
663    /// - Maintaining a delay queue for connection expiration
664    /// - Receiving renewal requests for specific connections
665    /// - Triggering reconnection when a connection reaches its maximum duration
666    /// - Attempting to find and renew connections in the connection pool
667    ///
668    /// # Behavior
669    /// - Listens for renewal requests on a channel
670    /// - Tracks connection expiration using a delay queue
671    /// - Initiates reconnection process when a connection expires
672    /// - Handles and logs any renewal failures
673    /// - Exits once the `WebsocketCommon` has been dropped (its `renewal_tx` closes)
674    fn spawn_renewal_loop(common: Weak<Self>, renewal_rx: Receiver<(String, String)>) {
675        spawn(async move {
676            let mut dq = DelayQueue::new();
677            let mut renewal_rx = renewal_rx;
678
679            loop {
680                select! {
681                    msg = renewal_rx.recv() => {
682                        // `None` means every sender is gone, i.e. `WebsocketCommon` was
683                        // dropped. Exit now rather than waiting for pending renewals.
684                        let Some((conn_id, url)) = msg else {
685                            break;
686                        };
687                        debug!("Scheduling renewal for {}", conn_id);
688                        dq.insert((conn_id, url), MAX_CONN_DURATION);
689                    }
690
691                    Some(expired) = dq.next() => {
692                        // Hold a strong reference only while handling this entry.
693                        let Some(common) = common.upgrade() else {
694                            break;
695                        };
696                        let (conn_id, default_url) = expired.into_inner();
697
698                        if let Some(conn_arc) = common
699                            .connection_pool
700                            .iter()
701                            .find(|c| c.id == conn_id)
702                            .cloned()
703                        {
704                            debug!("Renewing connection {}", conn_id);
705                            let url = common
706                                .get_reconnect_url(&default_url, Arc::clone(&conn_arc))
707                                .await;
708                            if let Err(e) = common.reconnect_tx.send(ReconnectEntry {
709                                connection_id: conn_id.clone(),
710                                url,
711                                is_renewal: true,
712                                attempt: 0,
713                                expected_writer: None,
714                            }).await {
715                                error!(
716                                    "Failed to enqueue renewal for {}: {:?}",
717                                    conn_id, e
718                                );
719                            }
720                        } else {
721                            warn!("No connection {} found for renewal", conn_id);
722                        }
723                    }
724                }
725            }
726            debug!("Renewal loop exiting");
727        });
728    }
729
730    /// Returns a snapshot of the additional HTTP headers sent on every handshake.
731    fn extra_headers(&self) -> Vec<(String, String)> {
732        match self.extra_headers.read() {
733            Ok(guard) => guard.clone(),
734            Err(poisoned) => poisoned.into_inner().clone(),
735        }
736    }
737
738    /// Sets additional HTTP headers to be sent on every (re)connection handshake.
739    fn set_extra_headers(&self, headers: Vec<(String, String)>) {
740        match self.extra_headers.write() {
741            Ok(mut guard) => *guard = headers,
742            Err(poisoned) => *poisoned.into_inner() = headers,
743        }
744    }
745
746    /// Checks if a WebSocket connection is ready for use.
747    ///
748    /// # Arguments
749    ///
750    /// * `connection` - The WebSocket connection to check
751    /// * `allow_non_established` - If true, allows connections that are not fully established
752    ///
753    /// # Returns
754    ///
755    /// `true` if the connection is ready, `false` otherwise
756    ///
757    /// # Behavior
758    ///
759    /// A connection is considered ready if:
760    /// - It has a write channel (unless `allow_non_established` is true)
761    /// - No reconnection is pending
762    /// - No close has been initiated
763    pub async fn is_connection_ready(
764        &self,
765        connection: &WebsocketConnection,
766        allow_non_established: bool,
767    ) -> bool {
768        let conn_state = connection.state.lock().await;
769        (allow_non_established || conn_state.ws_write_tx.is_some())
770            && !conn_state.reconnection_pending
771            && !conn_state.close_initiated
772    }
773
774    /// Checks if a WebSocket connection is established.
775    ///
776    /// # Arguments
777    ///
778    /// * `connection` - Optional specific WebSocket connection to check
779    ///
780    /// # Returns
781    ///
782    /// `true` if a connection is ready and established, `false` otherwise
783    ///
784    /// # Behavior
785    ///
786    /// - If a specific connection is provided, checks only that connection
787    /// - If no connection is provided, checks all connections in the pool
788    /// - A connection is considered established if it is ready and not in a non-established state
789    async fn is_connected(&self, connection: Option<&Arc<WebsocketConnection>>) -> bool {
790        if let Some(conn_arc) = connection {
791            return self.is_connection_ready(conn_arc, false).await;
792        }
793
794        for conn_arc in &self.connection_pool {
795            if self.is_connection_ready(conn_arc, false).await {
796                return true;
797            }
798        }
799
800        false
801    }
802
803    /// Retrieves available WebSocket connections from the connection pool.
804    ///
805    /// # Arguments
806    ///
807    /// * `allow_non_established` - If `true`, includes connections that are not fully established
808    /// * `url_path` - Optional URL path to filter connections
809    ///
810    /// # Returns
811    ///
812    /// A vector of `Arc<WebsocketConnection>` that are ready based on the `allow_non_established` flag
813    ///
814    /// # Behavior
815    ///
816    /// - For single connection mode, returns the first connection
817    /// - For multi-connection mode, filters connections based on readiness
818    /// - Uses `is_connection_ready` to determine connection availability
819    async fn get_available_connections(
820        &self,
821        allow_non_established: bool,
822        url_path: Option<&str>,
823    ) -> Vec<Arc<WebsocketConnection>> {
824        if matches!(self.mode, WebsocketMode::Single) && url_path.is_none() {
825            return vec![Arc::clone(&self.connection_pool[0])];
826        }
827
828        let mut ready = Vec::new();
829        for conn in &self.connection_pool {
830            if self.is_connection_ready(conn, allow_non_established).await {
831                ready.push(Arc::clone(conn));
832            }
833        }
834
835        ready
836    }
837
838    /// Retrieves a WebSocket connection from the connection pool.
839    ///
840    /// # Arguments
841    ///
842    /// * `allow_non_established` - If `true`, allows selecting a connection that is not fully established
843    /// * `url_path` - Optional URL path to filter connections
844    ///
845    /// # Returns
846    ///
847    /// An `Arc` to a `WebsocketConnection` from the pool, selected using round-robin strategy
848    ///
849    /// # Errors
850    ///
851    /// Returns `WebsocketError::NotConnected` if no suitable connection is available
852    ///
853    /// # Behavior
854    ///
855    /// - For single connection mode, returns the first connection
856    /// - For multi-connection mode, selects a ready connection using round-robin
857    /// - Filters connections based on `allow_non_established` parameter
858    async fn get_connection(
859        &self,
860        allow_non_established: bool,
861        url_path: Option<&str>,
862    ) -> Result<Arc<WebsocketConnection>, WebsocketError> {
863        let candidates = self
864            .get_available_connections(allow_non_established, url_path)
865            .await;
866
867        let mut ready = Vec::new();
868        for conn in candidates {
869            if let Some(path) = url_path {
870                let st = conn.state.lock().await;
871                if st.url_path.as_deref() != Some(path) {
872                    continue;
873                }
874            }
875            ready.push(conn);
876        }
877
878        if ready.is_empty() {
879            return Err(WebsocketError::NotConnected);
880        }
881
882        let idx = self.round_robin_index.fetch_add(1, Ordering::Relaxed) % ready.len();
883
884        Ok(Arc::clone(&ready[idx]))
885    }
886
887    /// Gracefully closes a WebSocket connection by waiting for pending requests to complete.
888    ///
889    /// # Arguments
890    ///
891    /// * `ws_write_tx_to_close` - Sender channel for sending close message
892    /// * `connection` - Shared reference to the WebSocket connection
893    ///
894    /// # Behavior
895    ///
896    /// - Waits up to 30 seconds for all pending requests to complete
897    /// - Logs debug and warning messages during the closing process
898    /// - Sends a normal close frame to the WebSocket
899    ///
900    /// # Returns
901    ///
902    /// `Ok(())` if connection closes successfully, otherwise a `WebsocketError`
903    async fn close_connection_gracefully(
904        &self,
905        ws_write_tx_to_close: UnboundedSender<Message>,
906        connection: Arc<WebsocketConnection>,
907    ) -> Result<(), WebsocketError> {
908        debug!("Waiting for pending requests to complete before disconnecting.");
909
910        let drain = async {
911            loop {
912                {
913                    let conn_state = connection.state.lock().await;
914                    if conn_state.pending_requests.is_empty() {
915                        debug!("All pending requests completed, proceeding to close.");
916                        break;
917                    }
918                }
919                connection.drain_notify.notified().await;
920            }
921        };
922
923        if timeout(Duration::from_secs(30), drain).await.is_err() {
924            warn!("Timeout waiting for pending requests; forcing close.");
925        }
926
927        info!("Closing WebSocket connection for {}", connection.id);
928        let _ = ws_write_tx_to_close.send(Message::Close(Some(CloseFrame {
929            code: CloseCode::Normal,
930            reason: "".into(),
931        })));
932
933        Ok(())
934    }
935
936    /// Retrieves the URL to use for reconnecting to the WebSocket.
937    ///
938    /// # Arguments
939    ///
940    /// * `default_url` - The default URL to use if no custom reconnect URL is provided
941    /// * `connection` - A shared reference to the WebSocket connection
942    ///
943    /// # Returns
944    ///
945    /// The URL to use for reconnecting, either from a custom handler or the default URL
946    ///
947    /// # Behavior
948    ///
949    /// - Checks if a connection handler is available
950    /// - If a handler exists, calls its `get_reconnect_url` method
951    /// - Otherwise, returns the default URL
952    async fn get_reconnect_url(
953        &self,
954        default_url: &str,
955        connection: Arc<WebsocketConnection>,
956    ) -> String {
957        if let Some(handler) = {
958            let conn_state = connection.state.lock().await;
959            conn_state.handler.clone()
960        } {
961            return handler
962                .get_reconnect_url(default_url.to_string(), Arc::clone(&connection))
963                .await;
964        }
965
966        default_url.to_string()
967    }
968
969    /// Handles the WebSocket connection opening event.
970    ///
971    /// This method is called when a WebSocket connection is successfully established. It performs
972    /// the following key actions:
973    /// - Invokes the connection handler's `on_open` method if a handler is present
974    /// - Logs connection information
975    /// - Handles connection renewal and close scenarios
976    /// - Emits a WebSocket open event
977    ///
978    /// # Arguments
979    ///
980    /// * `url` - The URL of the WebSocket server
981    /// * `connection` - A shared reference to the WebSocket connection
982    /// * `old_ws_writer` - Optional previous WebSocket writer for graceful connection handling
983    ///
984    /// # Behavior
985    ///
986    /// - If a connection handler exists, calls its `on_open` method
987    /// - Checks for pending renewal or close states
988    /// - Closes the previous connection if renewal is in progress
989    /// - Emits an open event if the connection is successfully established
990    async fn on_open(
991        &self,
992        url: String,
993        connection: Arc<WebsocketConnection>,
994        old_ws_writer: Option<UnboundedSender<Message>>,
995    ) {
996        if let Some(handler) = {
997            let conn_state = connection.state.lock().await;
998            conn_state.handler.clone()
999        } {
1000            handler.on_open(url.clone(), Arc::clone(&connection)).await;
1001        }
1002
1003        let conn_id = &connection.id;
1004        info!("Connected to WebSocket Server with id {}: {}", conn_id, url);
1005
1006        {
1007            let mut conn_state = connection.state.lock().await;
1008
1009            if conn_state.renewal_pending {
1010                conn_state.renewal_pending = false;
1011                let close_initiated = conn_state.close_initiated;
1012                let new_writer = conn_state.ws_write_tx.clone();
1013                drop(conn_state);
1014                if let Some(tx) = old_ws_writer {
1015                    info!("Connection renewal in progress; closing previous connection.");
1016                    let _ = self
1017                        .close_connection_gracefully(tx, Arc::clone(&connection))
1018                        .await;
1019                }
1020                if close_initiated {
1021                    if let Some(tx) = new_writer {
1022                        info!("Close initiated during renewal; closing renewed connection.");
1023                        let _ = self
1024                            .close_connection_gracefully(tx, Arc::clone(&connection))
1025                            .await;
1026                    }
1027                }
1028                return;
1029            }
1030
1031            if conn_state.close_initiated {
1032                // Take the writer while still holding the lock, then release it:
1033                // `close_connection_gracefully` locks the state again.
1034                let writer = conn_state.ws_write_tx.clone();
1035                drop(conn_state);
1036                if let Some(tx) = writer {
1037                    info!("Close initiated; closing connection.");
1038                    let _ = self
1039                        .close_connection_gracefully(tx, Arc::clone(&connection))
1040                        .await;
1041                }
1042                return;
1043            }
1044
1045            self.events.emit(&WebsocketEvent::Open);
1046        }
1047    }
1048
1049    /// Handles an incoming WebSocket message
1050    ///
1051    /// # Arguments
1052    ///
1053    /// * `msg` - The received message as a string
1054    /// * `connection` - A shared reference to the WebSocket connection
1055    ///
1056    /// # Behavior
1057    ///
1058    /// - If a connection handler exists, awaits its `on_message` method
1059    /// - Emits a `WebsocketEvent::Message` event with the received message
1060    async fn on_message(&self, msg: String, connection: Arc<WebsocketConnection>) {
1061        let handler = connection.state.lock().await.handler.clone();
1062        if let Some(handler) = handler {
1063            handler
1064                .on_message(msg.clone(), Arc::clone(&connection))
1065                .await;
1066        }
1067        self.events.emit(&WebsocketEvent::Message(msg));
1068    }
1069
1070    /// Creates a WebSocket connection with optional configuration and agent
1071    ///
1072    /// # Arguments
1073    ///
1074    /// * `url` - The WebSocket server URL to connect to
1075    /// * `agent` - Optional agent connector for configuring the connection
1076    /// * `user_agent` - Optional custom user agent string
1077    ///
1078    /// # Returns
1079    ///
1080    /// A `Result` containing the established WebSocket stream or a `WebsocketError`
1081    ///
1082    /// # Errors
1083    ///
1084    /// Returns a `WebsocketError` if:
1085    /// - The WebSocket handshake fails
1086    /// - The connection times out after 10 seconds
1087    ///
1088    /// # Behavior
1089    ///
1090    /// Attempts to establish a WebSocket connection with a configurable timeout,
1091    /// supporting optional TLS, custom user agent, and connection connectors
1092    #[cfg(test)]
1093    async fn create_websocket(
1094        url: &str,
1095        agent: Option<AgentConnector>,
1096        user_agent: Option<String>,
1097        handshake: Option<WebsocketHandshakeFn>,
1098    ) -> Result<WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>, WebsocketError> {
1099        Self::create_websocket_with_headers(url, agent, user_agent, handshake, Vec::new()).await
1100    }
1101
1102    /// Same as [`Self::create_websocket`], but additionally injects the provided
1103    /// HTTP headers in the handshake request (e.g. `X-MBX-APIKEY` for signed streams).
1104    ///
1105    /// # Errors
1106    ///
1107    /// Returns a `WebsocketError` if a header name/value is invalid, the handshake
1108    /// fails or the connection times out after 10 seconds.
1109    async fn create_websocket_with_headers(
1110        url: &str,
1111        agent: Option<AgentConnector>,
1112        user_agent: Option<String>,
1113        handshake: Option<WebsocketHandshakeFn>,
1114        extra_headers: Vec<(String, String)>,
1115    ) -> Result<WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>, WebsocketError> {
1116        let mut req = url
1117            .into_client_request()
1118            .map_err(|e| WebsocketError::Handshake(e.to_string()))?;
1119
1120        if let Some(ua) = user_agent {
1121            req.headers_mut().insert(USER_AGENT, ua.parse().unwrap());
1122        }
1123
1124        for (name, value) in extra_headers {
1125            let header_name = http::header::HeaderName::from_bytes(name.as_bytes())
1126                .map_err(|e| WebsocketError::Handshake(e.to_string()))?;
1127            let header_value = http::header::HeaderValue::from_str(&value)
1128                .map_err(|e| WebsocketError::Handshake(e.to_string()))?;
1129            req.headers_mut().insert(header_name, header_value);
1130        }
1131
1132        let ws_config: Option<WebSocketConfig> = None;
1133        let disable_nagle = false;
1134
1135        let timeout_duration = Duration::from_secs(10);
1136        let connector = agent.map(|agent| agent.0);
1137        let handshake = match handshake {
1138            Some(handshake) => handshake(req, ws_config, disable_nagle, connector),
1139            None => Box::pin(connect_async_tls_with_config(
1140                req,
1141                ws_config,
1142                disable_nagle,
1143                connector,
1144            )),
1145        };
1146        match timeout(timeout_duration, handshake).await {
1147            Ok(Ok((ws_stream, response))) => {
1148                debug!("WebSocket connected: {:?}", response);
1149                Ok(ws_stream)
1150            }
1151            Ok(Err(e)) => {
1152                let msg = e.to_string();
1153                error!("WebSocket handshake failed: {}", msg);
1154                Err(WebsocketError::Handshake(msg))
1155            }
1156            Err(_) => {
1157                error!(
1158                    "WebSocket connection timed out after {}s",
1159                    timeout_duration.as_secs()
1160                );
1161                Err(WebsocketError::Timeout)
1162            }
1163        }
1164    }
1165
1166    /// Connects to a WebSocket URL for all connections in the connection pool concurrently
1167    ///
1168    /// # Arguments
1169    ///
1170    /// * `url` - The WebSocket server URL to connect to
1171    /// * `connections` - Optional specific connections to use, otherwise uses the entire pool
1172    ///
1173    /// # Returns
1174    ///
1175    /// A `Result` indicating whether all connections were successfully established
1176    ///
1177    /// # Errors
1178    ///
1179    /// Returns a `WebsocketError` if any connection in the pool fails to establish
1180    ///
1181    /// # Behavior
1182    ///
1183    /// Attempts to initialize a WebSocket connection for each connection in the pool
1184    /// concurrently, logging successes and failures for each connection attempt
1185    async fn connect_pool(
1186        self: Arc<Self>,
1187        url: &str,
1188        connections: Option<Vec<Arc<WebsocketConnection>>>,
1189    ) -> Result<(), WebsocketError> {
1190        let pool: Vec<Arc<WebsocketConnection>> = match connections {
1191            Some(v) => v,
1192            None => self.connection_pool.clone(),
1193        };
1194
1195        let mut tasks = FuturesUnordered::new();
1196
1197        for conn in pool {
1198            let common = Arc::clone(&self);
1199            let url = url.to_owned();
1200
1201            tasks.push(async move {
1202                match common.init_connect(&url, false, Some(conn)).await {
1203                    Ok(()) => {
1204                        info!("Successfully connected to {}", url);
1205                        Ok(())
1206                    }
1207                    Err(err) => {
1208                        error!("Failed to connect to {}: {:?}", url, err);
1209                        Err(err)
1210                    }
1211                }
1212            });
1213        }
1214
1215        while let Some(result) = tasks.next().await {
1216            result?;
1217        }
1218
1219        Ok(())
1220    }
1221
1222    /// Initializes a WebSocket connection for a specific connection in the pool
1223    ///
1224    /// # Arguments
1225    ///
1226    /// * `url` - The WebSocket server URL to connect to
1227    /// * `is_renewal` - Flag indicating whether this is a connection renewal attempt
1228    /// * `connection` - Optional specific WebSocket connection to use, otherwise selects from the pool
1229    ///
1230    /// # Returns
1231    ///
1232    /// A `Result` indicating whether the connection was successfully established
1233    ///
1234    /// # Errors
1235    ///
1236    /// Returns a `WebsocketError` if the connection fails to initialize or establish
1237    ///
1238    /// # Behavior
1239    ///
1240    /// Handles connection establishment, splitting read/write streams, spawning reader/writer tasks,
1241    /// and managing connection state including renewal, reconnection, and error handling
1242    async fn init_connect(
1243        self: Arc<Self>,
1244        url: &str,
1245        is_renewal: bool,
1246        connection: Option<Arc<WebsocketConnection>>,
1247    ) -> Result<(), WebsocketError> {
1248        let conn = match connection {
1249            Some(conn) => conn,
1250            None => self.get_connection(true, None).await?,
1251        };
1252
1253        {
1254            let mut conn_state = conn.state.lock().await;
1255            if conn_state.close_initiated && (is_renewal || conn_state.reconnection_pending) {
1256                info!(
1257                    "Close initiated on {}; skipping automatic {} to {}",
1258                    conn.id,
1259                    if is_renewal { "renewal" } else { "reconnect" },
1260                    url
1261                );
1262                conn_state.reconnection_pending = false;
1263                return Ok(());
1264            }
1265            if conn_state.renewal_pending && is_renewal {
1266                info!("Renewal in progress {}→{}", conn.id, url);
1267                return Ok(());
1268            }
1269            if conn_state.ws_write_tx.is_some() && !is_renewal && !conn_state.reconnection_pending {
1270                info!("Exists {}; skipping {}", conn.id, url);
1271                return Ok(());
1272            }
1273            if is_renewal {
1274                conn_state.renewal_pending = true;
1275            }
1276
1277            conn_state.is_session_logged_on = false;
1278        }
1279
1280        let ws = Self::create_websocket_with_headers(
1281            url,
1282            self.agent.clone(),
1283            self.user_agent.clone(),
1284            self.handshake.clone(),
1285            self.extra_headers(),
1286        )
1287        .await
1288        .map_err(|e| {
1289            error!("Handshake failed {}: {:?}", url, e);
1290            e
1291        })?;
1292
1293        info!("Established {} → {}", conn.id, url);
1294
1295        if let Err(e) = self.renewal_tx.try_send((conn.id.clone(), url.to_string())) {
1296            error!("Failed to schedule renewal for {}: {:?}", conn.id, e);
1297        }
1298
1299        let (write_half, mut read_half) = ws.split();
1300        let (tx, mut rx) = unbounded_channel::<Message>();
1301        let writer_cancel = CancellationToken::new();
1302
1303        let old_writer = {
1304            let mut conn_state = conn.state.lock().await;
1305            conn_state.reconnection_pending = false;
1306            conn_state.ws_writer_cancel = Some(writer_cancel.clone());
1307            conn_state.ws_write_tx.replace(tx.clone())
1308        };
1309
1310        {
1311            let wconn = conn.clone();
1312            let common_clone = self.clone();
1313            let writer_url = url.to_string();
1314            let writer_tx = tx.clone();
1315            let writer_cancel = writer_cancel.clone();
1316
1317            spawn(async move {
1318                let mut sink = write_half;
1319                loop {
1320                    let msg = select! {
1321                        biased;
1322                        () = writer_cancel.cancelled() => {
1323                            debug!("Socket of {} abandoned; shutting down writer", wconn.id);
1324                            let _ = timeout(WRITER_SHUTDOWN_TIMEOUT, sink.close()).await;
1325                            break;
1326                        }
1327                        msg = rx.recv() => match msg {
1328                            Some(msg) => msg,
1329                            None => break,
1330                        },
1331                    };
1332                    // Nothing is sent after a Close frame. Exiting here also matters for
1333                    // freeing the client: the sender for `rx` lives in the connection
1334                    // state, which this task keeps alive, so `rx` would never close.
1335                    let is_close = matches!(msg, Message::Close(_));
1336                    // A send blocked on a stalled socket must not outlive the socket.
1337                    let sent = select! {
1338                        biased;
1339                        () = writer_cancel.cancelled() => {
1340                            debug!("Socket of {} abandoned during a send; dropping writer", wconn.id);
1341                            break;
1342                        }
1343                        sent = sink.send(msg) => sent,
1344                    };
1345                    if let Err(e) = sent {
1346                        let failure_reason =
1347                            WebsocketConnectionFailureReason::from_tungstenite_error(&e);
1348
1349                        error!(
1350                            "Write error on {}: {:?}, classified as {:?}",
1351                            wconn.id, e, failure_reason
1352                        );
1353
1354                        // Apply same reconnection logic as reader errors
1355                        let mut conn_state = wconn.state.lock().await;
1356                        match conn_state.begin_reconnect(&writer_tx, failure_reason) {
1357                            ReconnectDisposition::Started => {
1358                                info!(
1359                                    "Writer connection {} has recoverable error, attempting reconnection: {:?}",
1360                                    wconn.id, failure_reason
1361                                );
1362                                drop(conn_state);
1363                                let reconnect_url = common_clone
1364                                    .get_reconnect_url(&writer_url, Arc::clone(&wconn))
1365                                    .await;
1366
1367                                let _ = common_clone
1368                                    .reconnect_tx
1369                                    .send(ReconnectEntry {
1370                                        connection_id: wconn.id.clone(),
1371                                        url: reconnect_url,
1372                                        is_renewal: false,
1373                                        attempt: 0,
1374                                        expected_writer: None,
1375                                    })
1376                                    .await;
1377                            }
1378                            ReconnectDisposition::StaleWriter => {
1379                                debug!(
1380                                    "Ignoring writer error from replaced connection {}: {:?}",
1381                                    wconn.id, failure_reason
1382                                );
1383                            }
1384                            ReconnectDisposition::Rejected => {
1385                                warn!(
1386                                    "Writer connection {} has permanent error, will not reconnect: {:?}",
1387                                    wconn.id, failure_reason
1388                                );
1389                            }
1390                        }
1391
1392                        break;
1393                    }
1394                    if is_close {
1395                        break;
1396                    }
1397                }
1398                debug!("Writer {} exit", wconn.id);
1399            });
1400        }
1401
1402        {
1403            let common = self.clone();
1404            let conn = conn.clone();
1405            let url = url.to_string();
1406            spawn(async move {
1407                common.on_open(url, conn, old_writer).await;
1408            });
1409        }
1410
1411        {
1412            let common = self.clone();
1413            let reader_conn = conn.clone();
1414            let read_url = url.to_string();
1415            let reader_tx = tx;
1416
1417            spawn(async move {
1418                // Cancelled when this task finishes, however it finishes, so the writer
1419                // of this socket always shuts down together with its reader.
1420                let _writer_cancel = writer_cancel.drop_guard();
1421                let mut stream_end_reason = None;
1422                while let Some(item) = read_half.next().await {
1423                    match item {
1424                        Ok(Message::Text(msg)) => {
1425                            common
1426                                .on_message(msg.to_string(), Arc::clone(&reader_conn))
1427                                .await;
1428                        }
1429                        Ok(Message::Binary(bin)) => {
1430                            let mut decoder = ZlibDecoder::new(&bin[..]);
1431                            let mut decompressed = String::new();
1432                            if let Err(err) = decoder.read_to_string(&mut decompressed) {
1433                                error!("Binary message decompress failed: {:?}", err);
1434                                continue;
1435                            }
1436                            common
1437                                .on_message(decompressed, Arc::clone(&reader_conn))
1438                                .await;
1439                        }
1440                        Ok(Message::Ping(payload)) => {
1441                            info!("PING received from server on {}", reader_conn.id);
1442                            common.events.emit(&WebsocketEvent::Ping);
1443                            if let Some(tx) = reader_conn.state.lock().await.ws_write_tx.clone() {
1444                                let _ = tx.send(Message::Pong(payload));
1445                                info!(
1446                                    "Responded PONG to server's PING message on {}",
1447                                    reader_conn.id
1448                                );
1449                            }
1450                        }
1451                        Ok(Message::Pong(_)) => {
1452                            info!("Received PONG from server on {}", reader_conn.id);
1453                            common.events.emit(&WebsocketEvent::Pong);
1454                        }
1455                        Ok(Message::Close(frame)) => {
1456                            let (code, reason) = frame
1457                                .map_or((1000, String::new()), |CloseFrame { code, reason }| {
1458                                    (code.into(), reason.to_string())
1459                                });
1460                            common
1461                                .events
1462                                .emit(&WebsocketEvent::Close(code, reason.clone()));
1463
1464                            // Classify the close reason
1465                            let user_initiated = {
1466                                let conn_state = reader_conn.state.lock().await;
1467                                conn_state.close_initiated
1468                            };
1469
1470                            let failure_reason = WebsocketConnectionFailureReason::from_close_code(
1471                                code,
1472                                user_initiated,
1473                            );
1474                            stream_end_reason = Some(failure_reason);
1475
1476                            info!(
1477                                "Connection {} received close frame: code={}, reason='{}', classified as {:?}",
1478                                reader_conn.id, code, reason, failure_reason
1479                            );
1480
1481                            let mut conn_state = reader_conn.state.lock().await;
1482                            match conn_state.begin_reconnect(&reader_tx, failure_reason) {
1483                                ReconnectDisposition::Started => {
1484                                    info!(
1485                                        "Connection {} received close frame with reconnectable failure: {:?}",
1486                                        reader_conn.id, failure_reason
1487                                    );
1488                                    drop(conn_state);
1489                                    let reconnect_url = common
1490                                        .get_reconnect_url(&read_url, Arc::clone(&reader_conn))
1491                                        .await;
1492
1493                                    let _ = common
1494                                        .reconnect_tx
1495                                        .send(ReconnectEntry {
1496                                            connection_id: reader_conn.id.clone(),
1497                                            url: reconnect_url,
1498                                            is_renewal: false,
1499                                            attempt: 0,
1500                                            expected_writer: None,
1501                                        })
1502                                        .await;
1503                                }
1504                                ReconnectDisposition::StaleWriter => {
1505                                    debug!(
1506                                        "Ignoring close frame from replaced connection {}: {:?}",
1507                                        reader_conn.id, failure_reason
1508                                    );
1509                                }
1510                                ReconnectDisposition::Rejected => {
1511                                    warn!(
1512                                        "Connection {} received close frame with non-reconnectable failure: {:?}",
1513                                        reader_conn.id, failure_reason
1514                                    );
1515
1516                                    common.events.emit(&WebsocketEvent::Error(format!(
1517                                        "[CRITICAL] Connection {} permanently failed: {:?}",
1518                                        reader_conn.id, failure_reason
1519                                    )));
1520                                }
1521                            }
1522
1523                            break;
1524                        }
1525                        Err(e) => {
1526                            // Classify the error type for reconnection decision
1527                            let failure_reason =
1528                                WebsocketConnectionFailureReason::from_tungstenite_error(&e);
1529
1530                            stream_end_reason = Some(failure_reason);
1531                            error!(
1532                                "WebSocket error on {}: {:?}, classified as {:?}",
1533                                reader_conn.id, e, failure_reason
1534                            );
1535
1536                            // Apply the same reconnection logic as Close frames
1537                            let mut conn_state = reader_conn.state.lock().await;
1538                            match conn_state.begin_reconnect(&reader_tx, failure_reason) {
1539                                ReconnectDisposition::Started => {
1540                                    drop(conn_state);
1541                                    common.events.emit(&WebsocketEvent::Error(e.to_string()));
1542                                    info!(
1543                                        "Connection {} has recoverable error, attempting reconnection: {:?}",
1544                                        reader_conn.id, failure_reason
1545                                    );
1546                                    let reconnect_url = common
1547                                        .get_reconnect_url(&read_url, Arc::clone(&reader_conn))
1548                                        .await;
1549
1550                                    let _ = common
1551                                        .reconnect_tx
1552                                        .send(ReconnectEntry {
1553                                            connection_id: reader_conn.id.clone(),
1554                                            url: reconnect_url,
1555                                            is_renewal: false,
1556                                            attempt: 0,
1557                                            expected_writer: None,
1558                                        })
1559                                        .await;
1560                                }
1561                                ReconnectDisposition::StaleWriter => {
1562                                    debug!(
1563                                        "Ignoring error from replaced connection {}: {:?}",
1564                                        reader_conn.id, failure_reason
1565                                    );
1566                                }
1567                                ReconnectDisposition::Rejected => {
1568                                    drop(conn_state);
1569                                    common.events.emit(&WebsocketEvent::Error(e.to_string()));
1570                                    warn!(
1571                                        "Connection {} has permanent error, will not reconnect: {:?}",
1572                                        reader_conn.id, failure_reason
1573                                    );
1574
1575                                    common.events.emit(&WebsocketEvent::Error(format!(
1576                                        "[CRITICAL] Connection {} permanently failed: {:?}",
1577                                        reader_conn.id, failure_reason
1578                                    )));
1579                                }
1580                            }
1581
1582                            break;
1583                        }
1584                        _ => {}
1585                    }
1586                }
1587
1588                // Handle case where stream ends unexpectedly (e.g., network disconnection)
1589                info!("WebSocket stream ended for connection {}", reader_conn.id);
1590
1591                // Handle possibly unexpected stream end with same logic as other errors
1592                let failure_reason =
1593                    stream_end_reason.unwrap_or(WebsocketConnectionFailureReason::StreamEnded);
1594
1595                info!(
1596                    "WebSocket stream ended for connection {}, classified as {:?}",
1597                    reader_conn.id, failure_reason
1598                );
1599
1600                let mut conn_state = reader_conn.state.lock().await;
1601                match conn_state.begin_reconnect(&reader_tx, failure_reason) {
1602                    ReconnectDisposition::Started => {
1603                        info!(
1604                            "Connection {} stream ended unexpectedly, attempting reconnection",
1605                            reader_conn.id
1606                        );
1607                        drop(conn_state);
1608                        let reconnect_url = common
1609                            .get_reconnect_url(&read_url, Arc::clone(&reader_conn))
1610                            .await;
1611
1612                        let _ = common
1613                            .reconnect_tx
1614                            .send(ReconnectEntry {
1615                                connection_id: reader_conn.id.clone(),
1616                                url: reconnect_url,
1617                                is_renewal: false,
1618                                attempt: 0,
1619                                expected_writer: None,
1620                            })
1621                            .await;
1622                    }
1623                    ReconnectDisposition::StaleWriter => {
1624                        debug!(
1625                            "Ignoring stream end from replaced connection {}: {:?}",
1626                            reader_conn.id, failure_reason
1627                        );
1628                    }
1629                    ReconnectDisposition::Rejected => {
1630                        debug!(
1631                            "Connection {} stream ended normally (close_initiated={}, is_renewal={})",
1632                            reader_conn.id, conn_state.close_initiated, is_renewal
1633                        );
1634                    }
1635                }
1636
1637                debug!("Reader actor for {} exiting", reader_conn.id);
1638            });
1639        }
1640
1641        Ok(())
1642    }
1643    /// Gracefully disconnects all active WebSocket connections.
1644    ///
1645    /// This method attempts to close all connections in the connection pool within a 30-second timeout.
1646    /// It marks each connection as close-initiated and attempts to close them gracefully.
1647    ///
1648    /// # Returns
1649    ///
1650    /// - `Ok(())` if all connections are successfully closed
1651    /// - `Err(WebsocketError)` if there are errors during disconnection or a timeout occurs
1652    ///
1653    /// # Errors
1654    ///
1655    /// Returns `WebsocketError::Timeout` if disconnection takes longer than 30 seconds
1656    ///
1657    async fn disconnect(&self) -> Result<(), WebsocketError> {
1658        for conn in &self.connection_pool {
1659            let mut st = conn.state.lock().await;
1660            if st.reconnection_pending || st.renewal_pending {
1661                st.close_initiated = true;
1662            }
1663        }
1664
1665        if !self.is_connected(None).await {
1666            warn!("No active connection to close.");
1667            return Ok(());
1668        }
1669
1670        let mut shutdowns = FuturesUnordered::new();
1671        for conn in &self.connection_pool {
1672            {
1673                let mut conn_state = conn.state.lock().await;
1674                conn_state.close_initiated = true;
1675                if let Some(tx) = &conn_state.ws_write_tx {
1676                    shutdowns.push(self.close_connection_gracefully(tx.clone(), Arc::clone(conn)));
1677                }
1678            }
1679        }
1680
1681        let close_all = async {
1682            while let Some(result) = shutdowns.next().await {
1683                result?;
1684            }
1685            Ok::<(), WebsocketError>(())
1686        };
1687
1688        match timeout(Duration::from_secs(30), close_all).await {
1689            Ok(Ok(())) => {
1690                info!("Disconnected all WebSocket connections successfully.");
1691                for conn in &self.connection_pool {
1692                    let mut st = conn.state.lock().await;
1693                    st.is_session_logged_on = false;
1694                    st.session_logon_req = None;
1695                }
1696                Ok(())
1697            }
1698            Ok(Err(err)) => {
1699                error!("Error while disconnecting: {:?}", err);
1700                Err(err)
1701            }
1702            Err(_) => {
1703                error!("Timed out while disconnecting WebSocket connections.");
1704                Err(WebsocketError::Timeout)
1705            }
1706        }
1707    }
1708
1709    /// Closes every connection without waiting, for use when the client is dropped.
1710    ///
1711    /// Marks each connection close-initiated (so no reconnect or renewal reopens it)
1712    /// and sends a Close frame to its writer. Unlike [`Self::disconnect`], it does not
1713    /// wait for pending requests, which is not possible from `Drop`.
1714    fn close_on_drop(self: &Arc<Self>) {
1715        fn close(state: &mut WebsocketConnectionState) {
1716            state.close_initiated = true;
1717            state.reconnection_pending = false;
1718            state.is_session_logged_on = false;
1719            if let Some(tx) = state.ws_write_tx.take() {
1720                let _ = tx.send(Message::Close(Some(CloseFrame {
1721                    code: CloseCode::Normal,
1722                    reason: "".into(),
1723                })));
1724            }
1725        }
1726
1727        for conn in &self.connection_pool {
1728            if let Ok(mut state) = conn.state.try_lock() {
1729                close(&mut state);
1730            } else if let Ok(rt) = tokio::runtime::Handle::try_current() {
1731                // The state is locked by a task that is still running; close it there.
1732                let conn = Arc::clone(conn);
1733                rt.spawn(async move {
1734                    close(&mut *conn.state.lock().await);
1735                });
1736            }
1737        }
1738    }
1739
1740    /// Sends a PING message to all ready WebSocket connections.
1741    ///
1742    /// This method iterates through the connection pool, identifies ready connections,
1743    /// and sends a PING message to each of them. It logs the number of connections
1744    /// being pinged and handles any send errors individually.
1745    ///
1746    /// # Behavior
1747    ///
1748    /// - Skips connections that are not ready
1749    /// - Logs a warning if no connections are ready
1750    /// - Sends PING messages concurrently
1751    /// - Logs debug/error messages for each PING attempt
1752    async fn ping_server(&self) {
1753        let mut ready = Vec::new();
1754        for conn in &self.connection_pool {
1755            if self.is_connection_ready(conn, false).await {
1756                let id = conn.id.clone();
1757                let ws_write_tx = {
1758                    let conn_state = conn.state.lock().await;
1759                    conn_state.ws_write_tx.clone()
1760                };
1761                ready.push((id, ws_write_tx));
1762            }
1763        }
1764
1765        if ready.is_empty() {
1766            warn!("No ready connections for PING.");
1767            return;
1768        }
1769        info!("Sending PING to {} WebSocket connections.", ready.len());
1770
1771        let mut tasks = FuturesUnordered::new();
1772        for (id, ws_write_tx_opt) in ready {
1773            if let Some(tx) = ws_write_tx_opt {
1774                tasks.push(async move {
1775                    if let Err(e) = tx.send(Message::Ping(Vec::new().into())) {
1776                        error!("Failed to send PING to {}: {:?}", id, e);
1777                    } else {
1778                        debug!("Sent PING to connection {}", id);
1779                    }
1780                });
1781            } else {
1782                error!("Connection {} was ready but has no write channel", id);
1783            }
1784        }
1785
1786        while tasks.next().await.is_some() {}
1787    }
1788
1789    /// Sends a WebSocket message and optionally waits for a reply.
1790    ///
1791    /// # Arguments
1792    ///
1793    /// * `payload` - The message payload to send
1794    /// * `id` - Optional request identifier, required when waiting for a reply
1795    /// * `wait_for_reply` - Whether to wait for a response to the message
1796    /// * `timeout` - Maximum duration to wait for a reply
1797    /// * `connection` - Optional specific WebSocket connection to use
1798    ///
1799    /// # Returns
1800    ///
1801    /// A receiver for the response if `wait_for_reply` is true, otherwise `None`
1802    ///
1803    /// # Errors
1804    ///
1805    /// Returns a `WebsocketError` if the connection is not ready or the send fails
1806    async fn send(
1807        &self,
1808        payload: String,
1809        id: Option<String>,
1810        wait_for_reply: bool,
1811        timeout: Duration,
1812        connection: Option<Arc<WebsocketConnection>>,
1813    ) -> Result<Option<oneshot::Receiver<Result<Value, WebsocketError>>>, WebsocketError> {
1814        let conn = if let Some(c) = connection {
1815            c
1816        } else {
1817            self.get_connection(false, None).await?
1818        };
1819
1820        if !self.is_connected(Some(&conn)).await {
1821            warn!("Send attempted on a non-connected socket");
1822            return Err(WebsocketError::NotConnected);
1823        }
1824
1825        let ws_write_tx = {
1826            let conn_state = conn.state.lock().await;
1827            conn_state
1828                .ws_write_tx
1829                .clone()
1830                .ok_or(WebsocketError::NotConnected)?
1831        };
1832
1833        let pending_setup = if wait_for_reply {
1834            let request_id = id.ok_or_else(|| {
1835                error!("id is required when waiting for a reply");
1836                WebsocketError::NotConnected
1837            })?;
1838
1839            let (tx, rx) = oneshot::channel();
1840            {
1841                let mut conn_state = conn.state.lock().await;
1842                conn_state
1843                    .pending_requests
1844                    .insert(request_id.clone(), PendingRequest { completion: tx });
1845            }
1846
1847            Some((request_id, rx))
1848        } else {
1849            None
1850        };
1851
1852        debug!("Sending message to WebSocket on connection {}", conn.id);
1853
1854        if ws_write_tx
1855            .send(Message::Text(payload.clone().into()))
1856            .is_err()
1857        {
1858            if let Some((request_id, _)) = &pending_setup {
1859                let mut conn_state = conn.state.lock().await;
1860                conn_state.pending_requests.remove(request_id);
1861            }
1862            return Err(WebsocketError::NotConnected);
1863        }
1864
1865        let rx = if let Some((request_id, rx)) = pending_setup {
1866            let conn_clone = Arc::clone(&conn);
1867            let timeout_id = request_id.clone();
1868            spawn(async move {
1869                sleep(timeout).await;
1870                let mut conn_state = conn_clone.state.lock().await;
1871                if let Some(pending_req) = conn_state.pending_requests.remove(&timeout_id) {
1872                    let _ = pending_req.completion.send(Err(WebsocketError::Timeout));
1873                }
1874            });
1875            Some(rx)
1876        } else {
1877            None
1878        };
1879
1880        Ok(rx)
1881    }
1882}
1883
1884#[derive(Debug, Default, Clone)]
1885pub struct WebsocketMessageSendOptions {
1886    pub with_api_key: bool,
1887    pub is_signed: bool,
1888    pub is_session_logon: Option<bool>,
1889    pub is_session_logout: Option<bool>,
1890}
1891
1892impl WebsocketMessageSendOptions {
1893    #[must_use]
1894    pub fn new() -> Self {
1895        Self::default()
1896    }
1897
1898    #[must_use]
1899    pub fn with_api_key(mut self) -> Self {
1900        self.with_api_key = true;
1901        self
1902    }
1903
1904    #[must_use]
1905    pub fn signed(mut self) -> Self {
1906        self.is_signed = true;
1907        self
1908    }
1909
1910    #[must_use]
1911    pub fn session_logon(mut self) -> Self {
1912        self.is_session_logon = Some(true);
1913        self
1914    }
1915
1916    #[must_use]
1917    pub fn session_logout(mut self) -> Self {
1918        self.is_session_logout = Some(true);
1919        self
1920    }
1921}
1922
1923#[derive(Debug)]
1924pub enum SendWebsocketMessageResult<R> {
1925    Single(WebsocketApiResponse<R>),
1926    Multiple(Vec<WebsocketApiResponse<R>>),
1927}
1928
1929impl<R> IntoIterator for SendWebsocketMessageResult<R> {
1930    type Item = WebsocketApiResponse<R>;
1931    type IntoIter = std::vec::IntoIter<Self::Item>;
1932
1933    fn into_iter(self) -> Self::IntoIter {
1934        match self {
1935            SendWebsocketMessageResult::Single(resp) => vec![resp].into_iter(),
1936            SendWebsocketMessageResult::Multiple(v) => v.into_iter(),
1937        }
1938    }
1939}
1940
1941pub struct WebsocketApi {
1942    pub common: Arc<WebsocketCommon>,
1943    configuration: ConfigurationWebsocketApi,
1944    is_connecting: Arc<Mutex<bool>>,
1945    stream_callbacks: Mutex<HashMap<String, Vec<Arc<dyn Fn(&Value) + Send + Sync + 'static>>>>,
1946}
1947
1948impl Drop for WebsocketApi {
1949    /// Closes any open connection when the last handle to the client is dropped
1950    /// without calling `disconnect()`, so its sockets and tasks do not outlive it.
1951    fn drop(&mut self) {
1952        self.common.close_on_drop();
1953    }
1954}
1955
1956impl WebsocketApi {
1957    #[must_use]
1958    /// Creates a new WebSocket API instance with the given configuration and connection pool.
1959    ///
1960    /// # Arguments
1961    ///
1962    /// * `configuration` - Configuration settings for the WebSocket API
1963    /// * `connection_pool` - A vector of WebSocket connections to be used
1964    ///
1965    /// # Returns
1966    ///
1967    /// An `Arc`-wrapped `WebsocketApi` instance ready for use
1968    ///
1969    /// # Panics
1970    ///
1971    /// This function will panic if the configuration is not valid.
1972    ///
1973    /// # Examples
1974    ///
1975    ///
1976    /// let api = `WebsocketApi::new(config`, `connection_pool`);
1977    ///
1978    pub fn new(
1979        configuration: ConfigurationWebsocketApi,
1980        connection_pool: Vec<Arc<WebsocketConnection>>,
1981    ) -> Arc<Self> {
1982        let agent_clone = configuration.agent.clone();
1983        let handshake_clone = configuration.handshake.clone();
1984        let user_agent_clone = configuration.user_agent.clone();
1985        let common = WebsocketCommon::new_with_handshake(
1986            connection_pool,
1987            configuration.mode.clone(),
1988            usize::try_from(configuration.reconnect_delay)
1989                .expect("reconnect_delay should fit in usize"),
1990            agent_clone,
1991            Some(user_agent_clone),
1992            handshake_clone,
1993        );
1994
1995        Arc::new(Self {
1996            common: Arc::clone(&common),
1997            configuration,
1998            is_connecting: Arc::new(Mutex::new(false)),
1999            stream_callbacks: Mutex::new(HashMap::new()),
2000        })
2001    }
2002
2003    /// Connects to a WebSocket server with a configurable timeout and connection handling.
2004    ///
2005    /// This method attempts to establish a WebSocket connection if not already connected.
2006    /// It prevents multiple simultaneous connection attempts and supports a connection pool.
2007    ///
2008    /// # Errors
2009    ///
2010    /// Returns a `WebsocketError` if:
2011    /// - Connection fails
2012    /// - Connection times out after 10 seconds
2013    ///
2014    /// # Behavior
2015    ///
2016    /// - Checks if already connected and returns early if so
2017    /// - Prevents multiple concurrent connection attempts
2018    /// - Sets a WebSocket handler for the connection pool
2019    /// - Attempts to connect with a 10-second timeout
2020    ///
2021    /// # Returns
2022    ///
2023    /// `Ok(())` if connection is successful, otherwise a `WebsocketError`
2024    pub async fn connect(self: Arc<Self>) -> Result<(), WebsocketError> {
2025        if self.common.is_connected(None).await {
2026            info!("WebSocket connection already established");
2027            return Ok(());
2028        }
2029
2030        {
2031            let mut flag = self.is_connecting.lock().await;
2032            if *flag {
2033                info!("Already connecting...");
2034                return Ok(());
2035            }
2036            *flag = true;
2037        }
2038
2039        let url = self.prepare_url(self.configuration.ws_url.as_deref().unwrap_or_default());
2040
2041        let handler: Arc<dyn WebsocketHandler> = Arc::new(WeakHandler(Arc::downgrade(&self)));
2042        for slot in &self.common.connection_pool {
2043            slot.set_handler(handler.clone()).await;
2044        }
2045
2046        let result = select! {
2047            () = sleep(Duration::from_secs(10)) => Err(WebsocketError::Timeout),
2048            r = self.common.clone().connect_pool(&url, None) => r,
2049        };
2050
2051        {
2052            let mut flag = self.is_connecting.lock().await;
2053            *flag = false;
2054        }
2055
2056        result
2057    }
2058
2059    /// Disconnects the WebSocket connection.
2060    ///
2061    /// # Returns
2062    ///
2063    /// `Ok(())` if disconnection is successful, otherwise a `WebsocketError`
2064    ///
2065    /// # Errors
2066    ///
2067    /// Returns a `WebsocketError` if:
2068    /// - Disconnection fails
2069    /// - Connection is not established
2070    ///
2071    pub async fn disconnect(&self) -> Result<(), WebsocketError> {
2072        self.common.disconnect().await
2073    }
2074
2075    /// Checks if the WebSocket connection is currently established.
2076    ///
2077    /// # Returns
2078    ///
2079    /// `true` if the connection is active, `false` otherwise.
2080    pub async fn is_connected(&self) -> bool {
2081        self.common.is_connected(None).await
2082    }
2083
2084    /// Sends a ping to the WebSocket server to maintain the connection.
2085    ///
2086    /// This method calls the underlying connection's ping mechanism to check
2087    /// and keep the WebSocket connection alive.
2088    pub async fn ping_server(&self) {
2089        self.common.ping_server().await;
2090    }
2091
2092    /// Sends a WebSocket message with the specified method and payload.
2093    ///
2094    /// This method prepares and sends a WebSocket request with optional API key and signature.
2095    /// It handles connection status, generates a unique request ID, and processes the response.
2096    ///
2097    /// # Arguments
2098    ///
2099    /// * `method` - The WebSocket API method to be called
2100    /// * `payload` - A map of parameters to be sent with the request
2101    /// * `options` - Configuration options for message sending (API key, signing)
2102    ///
2103    /// # Returns
2104    ///
2105    /// A deserialized response of type `R` or a `WebsocketError` if the request fails
2106    ///
2107    /// # Panics
2108    ///
2109    /// Panics if:
2110    ///
2111    /// - The WebSocket is not connected
2112    /// - The request cannot be processed
2113    /// - No response is received within the timeout
2114    ///
2115    /// # Errors
2116    ///
2117    /// Returns `WebsocketError` if:
2118    /// - The WebSocket is not connected
2119    /// - The request cannot be processed
2120    /// - No response is received within the timeout
2121    pub async fn send_message<R>(
2122        &self,
2123        method: &str,
2124        payload: BTreeMap<String, Value>,
2125        options: WebsocketMessageSendOptions,
2126    ) -> Result<SendWebsocketMessageResult<R>, WebsocketError>
2127    where
2128        R: DeserializeOwned + Send + Sync + 'static,
2129    {
2130        if !self.common.is_connected(None).await {
2131            return Err(WebsocketError::NotConnected);
2132        }
2133
2134        let do_multi =
2135            options.is_session_logon.unwrap_or(false) || options.is_session_logout.unwrap_or(false);
2136
2137        let connections = if do_multi {
2138            self.common.get_available_connections(false, None).await
2139        } else {
2140            vec![self.common.get_connection(false, None).await?]
2141        };
2142
2143        let skip_auth = if do_multi {
2144            false
2145        } else {
2146            let connection = &connections[0];
2147            let conn_state = connection.state.lock().await;
2148            self.configuration.auto_session_relogon && conn_state.is_session_logged_on
2149        };
2150
2151        let payload_clone = payload.clone();
2152
2153        let (id, request) =
2154            build_websocket_api_message(&self.configuration, method, payload, &options, skip_auth);
2155        let raw_payload = serde_json::to_string(&request).unwrap();
2156        debug!("Sending message to WebSocket API: {:?}", request);
2157
2158        let timeout = Duration::from_millis(self.configuration.timeout);
2159
2160        let mut receivers = Vec::with_capacity(connections.len());
2161        for connection in &connections {
2162            let opt_rx = self
2163                .common
2164                .send(
2165                    raw_payload.clone(),
2166                    Some(id.clone()),
2167                    true,
2168                    timeout,
2169                    Some(connection.clone()),
2170                )
2171                .await?;
2172            receivers.push((connection.clone(), opt_rx));
2173        }
2174
2175        let mut raw_msgs = Vec::with_capacity(receivers.len());
2176        for (_conn, opt_rx) in receivers {
2177            let rx = opt_rx.ok_or(WebsocketError::NoResponse)?;
2178            let msg = rx.await.unwrap_or(Err(WebsocketError::Timeout))?;
2179            raw_msgs.push(msg);
2180        }
2181
2182        let mut responses = Vec::with_capacity(raw_msgs.len());
2183        for msg in raw_msgs {
2184            let raw = msg
2185                .get("result")
2186                .or_else(|| msg.get("response"))
2187                .cloned()
2188                .unwrap_or(Value::Null);
2189
2190            let rate_limits = msg
2191                .get("rateLimits")
2192                .and_then(Value::as_array)
2193                .map(|arr| {
2194                    arr.iter()
2195                        .filter_map(|v| serde_json::from_value(v.clone()).ok())
2196                        .collect()
2197                })
2198                .unwrap_or_default();
2199
2200            responses.push(WebsocketApiResponse {
2201                raw,
2202                rate_limits,
2203                _marker: PhantomData,
2204            });
2205        }
2206
2207        if do_multi && self.configuration.auto_session_relogon {
2208            for connection in &connections {
2209                let mut state = connection.state.lock().await;
2210                if options.is_session_logon.unwrap_or(false) {
2211                    state.is_session_logged_on = true;
2212                    state.session_logon_req = Some(WebsocketSessionLogonReq {
2213                        method: method.to_string(),
2214                        payload: payload_clone.clone(),
2215                        options: options.clone(),
2216                    });
2217                } else {
2218                    state.is_session_logged_on = false;
2219                    state.session_logon_req = None;
2220                }
2221            }
2222        }
2223
2224        Ok(if responses.len() == 1 && !do_multi {
2225            SendWebsocketMessageResult::Single(responses.into_iter().next().unwrap())
2226        } else {
2227            SendWebsocketMessageResult::Multiple(responses)
2228        })
2229    }
2230
2231    /// Prepares a WebSocket URL by appending a validated time unit parameter.
2232    ///
2233    /// This method checks if a time unit is configured and validates it. If valid,
2234    /// the time unit is appended to the URL as a query parameter. If no time unit
2235    /// is specified or the validation fails, the original URL is returned.
2236    ///
2237    /// # Arguments
2238    ///
2239    /// * `ws_url` - The base WebSocket URL to be modified
2240    ///
2241    /// # Returns
2242    ///
2243    /// A modified URL with the time unit parameter, or the original URL if no
2244    /// modification is possible
2245    fn prepare_url(&self, ws_url: &str) -> String {
2246        let mut url = ws_url.to_string();
2247
2248        let time_unit = match &self.configuration.time_unit {
2249            Some(u) => u.to_string(),
2250            None => return url,
2251        };
2252
2253        match validate_time_unit(&time_unit) {
2254            Ok(Some(validated)) => {
2255                let sep = if url.contains('?') { '&' } else { '?' };
2256                url.push(sep);
2257                url.push_str("timeUnit=");
2258                url.push_str(validated);
2259            }
2260            Ok(None) => {}
2261            Err(e) => {
2262                error!("Invalid time unit provided: {:?}", e);
2263            }
2264        }
2265
2266        url
2267    }
2268}
2269
2270#[async_trait]
2271impl WebsocketHandler for WebsocketApi {
2272    /// Handles the WebSocket connection opening event, attempting to re-establish a session logon if needed.
2273    ///
2274    /// This method checks if a session logon request exists and has not already been logged on.
2275    /// If conditions are met, it attempts to send a session re-logon message and update the connection state.
2276    ///
2277    /// # Arguments
2278    ///
2279    /// * `_url` - The WebSocket connection URL (unused)
2280    /// * `connection` - The WebSocket connection context
2281    ///
2282    /// # Behavior
2283    ///
2284    /// - Checks for an existing session logon request
2285    /// - Verifies the session is not already logged on
2286    /// - Attempts to send a re-logon message
2287    /// - Updates connection state upon successful re-logon
2288    /// - Logs errors if re-logon dispatch fails
2289    async fn on_open(&self, _url: String, connection: Arc<WebsocketConnection>) {
2290        let session_req = {
2291            let conn_state = connection.state.lock().await;
2292            conn_state.session_logon_req.clone()
2293        };
2294
2295        let Some(req) = session_req else {
2296            return;
2297        };
2298
2299        let already_logged_on = {
2300            let conn_state = connection.state.lock().await;
2301            conn_state.is_session_logged_on
2302        };
2303
2304        if already_logged_on {
2305            debug!(
2306                "Connection {} already logged on, skipping re-logon",
2307                connection.id
2308            );
2309            return;
2310        }
2311
2312        let conn = connection.clone();
2313        let common = Arc::clone(&self.common);
2314        let configuration = self.configuration.clone();
2315        let method = req.method.clone();
2316        let payload = req.payload.clone();
2317        let options = req.options.clone();
2318
2319        spawn(async move {
2320            let (id, json_msg) =
2321                build_websocket_api_message(&configuration, &method, payload, &options, false);
2322
2323            let raw_message = match serde_json::to_string(&json_msg) {
2324                Ok(msg) => msg,
2325                Err(e) => {
2326                    warn!(
2327                        "Failed to serialize session logon message for connection {}: {}",
2328                        conn.id, e
2329                    );
2330                    return;
2331                }
2332            };
2333
2334            debug!(
2335                "Session re-logon on connection {}: {}",
2336                conn.id, raw_message
2337            );
2338
2339            let rx = match common
2340                .send(
2341                    raw_message,
2342                    Some(id.clone()),
2343                    true,
2344                    Duration::from_millis(configuration.timeout),
2345                    Some(conn.clone()),
2346                )
2347                .await
2348            {
2349                Ok(Some(rx)) => rx,
2350                Ok(None) => {
2351                    warn!(
2352                        "Session re-logon dispatch returned None for connection {}",
2353                        conn.id
2354                    );
2355                    return;
2356                }
2357                Err(e) => {
2358                    warn!(
2359                        "Session re-logon dispatch failed on connection {}: {}",
2360                        conn.id, e
2361                    );
2362                    return;
2363                }
2364            };
2365
2366            let Ok(result) = timeout(Duration::from_millis(configuration.timeout), rx).await else {
2367                warn!("Session re-logon timed out on connection {}", conn.id);
2368                return;
2369            };
2370
2371            let final_result = match result {
2372                Ok(final_result) => final_result,
2373                Err(e) => {
2374                    warn!(
2375                        "Session re-logon receiver error on connection {}: {}",
2376                        conn.id, e
2377                    );
2378                    return;
2379                }
2380            };
2381
2382            let payload = match final_result {
2383                Ok(payload) => payload,
2384                Err(e) => {
2385                    warn!(
2386                        "Session re-logon payload error on connection {}: {}",
2387                        conn.id, e
2388                    );
2389                    return;
2390                }
2391            };
2392
2393            debug!(
2394                "Session re-logon succeeded on connection {}: {}",
2395                conn.id, payload
2396            );
2397            let mut conn_state = conn.state.lock().await;
2398            conn_state.is_session_logged_on = true;
2399        });
2400    }
2401
2402    /// Handles incoming WebSocket messages by parsing the JSON payload and processing pending requests.
2403    ///
2404    /// This method is responsible for:
2405    /// - Parsing the received WebSocket message as JSON
2406    /// - Matching the message to a pending request by its ID
2407    /// - Sending the response back to the original request's completion channel
2408    /// - Handling both successful and error responses
2409    ///
2410    /// # Arguments
2411    ///
2412    /// * `data` - The raw WebSocket message as a string
2413    /// * `connection` - The WebSocket connection context associated with the message
2414    ///
2415    /// # Behavior
2416    ///
2417    /// - If message parsing fails, logs an error and returns
2418    /// - For known request IDs, sends the response to the corresponding completion channel
2419    /// - Warns about responses for unknown or timed-out requests
2420    /// - Differentiates between successful (status < 400) and error responses
2421    async fn on_message(&self, data: String, connection: Arc<WebsocketConnection>) {
2422        let msg: Value = match serde_json::from_str(&data) {
2423            Ok(v) => v,
2424            Err(err) => {
2425                error!("Failed to parse WebSocket message {} – {}", data, err);
2426                return;
2427            }
2428        };
2429
2430        if let Some(id) = msg.get("id").and_then(Value::as_str) {
2431            let maybe_sender = {
2432                let mut conn_state = connection.state.lock().await;
2433                conn_state.pending_requests.remove(id)
2434            };
2435
2436            if let Some(PendingRequest { completion }) = maybe_sender {
2437                connection.drain_notify.notify_one();
2438                let status = msg.get("status").and_then(Value::as_u64).unwrap_or(200);
2439                if status >= 400 {
2440                    let error_map = msg
2441                        .get("error")
2442                        .and_then(Value::as_object)
2443                        .unwrap_or(&serde_json::Map::new())
2444                        .clone();
2445
2446                    let code = error_map
2447                        .get("code")
2448                        .and_then(Value::as_i64)
2449                        .unwrap_or(status.try_into().unwrap());
2450
2451                    let message = error_map
2452                        .get("msg")
2453                        .and_then(Value::as_str)
2454                        .unwrap_or("Unknown error")
2455                        .to_string();
2456
2457                    let retry_after = error_map
2458                        .get("data")
2459                        .and_then(|d| d.get("retryAfter"))
2460                        .and_then(Value::as_i64);
2461
2462                    let _ = completion.send(Err(WebsocketError::ResponseError {
2463                        code,
2464                        message,
2465                        retry_after,
2466                    }));
2467                } else {
2468                    let _ = completion.send(Ok(msg.clone()));
2469                }
2470            }
2471
2472            return;
2473        }
2474
2475        if let Some(event) = msg.get("event") {
2476            if let Some(event_type) = event.get("e").and_then(Value::as_str) {
2477                if event_type == "serverShutdown" {
2478                    warn!(
2479                        "Received serverShutdown event on connection {}",
2480                        connection.id
2481                    );
2482
2483                    let mut conn_state = connection.state.lock().await;
2484
2485                    if !conn_state.renewal_pending && !conn_state.close_initiated {
2486                        conn_state.renewal_pending = true;
2487
2488                        let url = conn_state.url_path.clone().unwrap_or_default();
2489
2490                        drop(conn_state);
2491
2492                        if let Err(e) = self
2493                            .common
2494                            .reconnect_tx
2495                            .send(ReconnectEntry {
2496                                connection_id: connection.id.clone(),
2497                                url,
2498                                is_renewal: true,
2499                                attempt: 0,
2500                                expected_writer: None,
2501                            })
2502                            .await
2503                        {
2504                            error!("Failed to enqueue serverShutdown renewal: {:?}", e);
2505                        }
2506                    }
2507
2508                    return;
2509                }
2510            }
2511        }
2512
2513        if let Some(event) = msg.get("event") {
2514            if event.get("e").is_some() {
2515                for callbacks in self.stream_callbacks.lock().await.values() {
2516                    for callback in callbacks {
2517                        callback(event);
2518                    }
2519                }
2520
2521                return;
2522            }
2523        }
2524
2525        warn!(
2526            "Received response for unknown or timed-out request: {}",
2527            data
2528        );
2529    }
2530
2531    /// Generates the URL to use for reconnecting to a WebSocket connection.
2532    ///
2533    /// # Arguments
2534    ///
2535    /// * `default_url` - The original URL to potentially modify for reconnection
2536    /// * `_connection` - The WebSocket connection context (currently unused)
2537    ///
2538    /// # Returns
2539    ///
2540    /// A `String` representing the URL to use for reconnecting
2541    async fn get_reconnect_url(
2542        &self,
2543        default_url: String,
2544        _connection: Arc<WebsocketConnection>,
2545    ) -> String {
2546        default_url
2547    }
2548}
2549
2550pub struct WebsocketStreams {
2551    pub common: Arc<WebsocketCommon>,
2552    pub stream_id_is_strictly_number: AtomicBool,
2553    pub signed_streams: AtomicBool,
2554    signature_gen: SignatureGenerator,
2555    url_paths: Vec<String>,
2556    is_connecting: Mutex<bool>,
2557    connection_streams: Mutex<HashMap<String, Arc<WebsocketConnection>>>,
2558    configuration: ConfigurationWebsocketStreams,
2559}
2560
2561impl Drop for WebsocketStreams {
2562    /// Closes any open connection when the last handle to the client is dropped
2563    /// without calling `disconnect()`, so its sockets and tasks do not outlive it.
2564    fn drop(&mut self) {
2565        self.common.close_on_drop();
2566    }
2567}
2568
2569impl WebsocketStreams {
2570    /// Creates a new `WebsocketStreams` instance with the given configuration and connection pool.
2571    ///
2572    /// # Arguments
2573    ///
2574    /// * `configuration` - Configuration settings for the WebSocket streams
2575    /// * `connection_pool` - A vector of WebSocket connections to use
2576    /// * `url_paths` - A vector of URL paths for the streams
2577    ///
2578    /// # Returns
2579    ///
2580    /// An `Arc`-wrapped `WebsocketStreams` instance
2581    ///
2582    /// # Panics
2583    ///
2584    /// Panics if the `reconnect_delay` cannot be converted to `usize`
2585    #[must_use]
2586    pub fn new(
2587        configuration: ConfigurationWebsocketStreams,
2588        mut connection_pool: Vec<Arc<WebsocketConnection>>,
2589        url_paths: Vec<String>,
2590    ) -> Arc<Self> {
2591        if !url_paths.is_empty() {
2592            let base_pool_size = configuration.mode.pool_size();
2593            let expected = base_pool_size * url_paths.len();
2594
2595            while connection_pool.len() < expected {
2596                connection_pool.push(WebsocketConnection::new(random_string()));
2597            }
2598        }
2599
2600        let agent_clone = configuration.agent.clone();
2601        let handshake_clone = configuration.handshake.clone();
2602        let user_agent_clone = configuration.user_agent.clone();
2603        let common = WebsocketCommon::new_with_handshake(
2604            connection_pool,
2605            configuration.mode.clone(),
2606            usize::try_from(configuration.reconnect_delay)
2607                .expect("reconnect_delay should fit in usize"),
2608            agent_clone,
2609            Some(user_agent_clone),
2610            handshake_clone,
2611        );
2612        let signature_gen = SignatureGenerator::new(
2613            configuration.api_secret.clone(),
2614            configuration.private_key.clone(),
2615            configuration.private_key_passphrase.clone(),
2616        );
2617        Arc::new(Self {
2618            common,
2619            is_connecting: Mutex::new(false),
2620            connection_streams: Mutex::new(HashMap::new()),
2621            configuration,
2622            stream_id_is_strictly_number: AtomicBool::new(false),
2623            signed_streams: AtomicBool::new(false),
2624            signature_gen,
2625            url_paths,
2626        })
2627    }
2628
2629    /// Establishes a WebSocket connection for the given streams.
2630    ///
2631    /// This method attempts to connect to a WebSocket server using the connection pool.
2632    /// If a connection is already established or in progress, it returns immediately.
2633    ///
2634    /// # Arguments
2635    ///
2636    /// * `streams` - A vector of stream identifiers to connect to
2637    ///
2638    /// # Returns
2639    ///
2640    /// A `Result` indicating whether the connection was successful or an error occurred
2641    ///
2642    /// # Errors
2643    ///
2644    /// Returns a `WebsocketError` if the connection fails or times out after 10 seconds
2645    pub async fn connect(self: Arc<Self>, streams: Vec<String>) -> Result<(), WebsocketError> {
2646        if self.common.is_connected(None).await {
2647            info!("WebSocket connection already established");
2648            return Ok(());
2649        }
2650
2651        let signed = self.is_signed_streams();
2652        if signed {
2653            self.assert_signed_streams_credentials()?;
2654            if let Some(api_key) = self.configuration.api_key.clone() {
2655                self.common
2656                    .set_extra_headers(vec![("X-MBX-APIKEY".to_string(), api_key)]);
2657            }
2658        }
2659
2660        {
2661            let mut flag = self.is_connecting.lock().await;
2662            if *flag {
2663                info!("Already connecting...");
2664                return Ok(());
2665            }
2666            *flag = true;
2667        }
2668
2669        let handler: Arc<dyn WebsocketHandler> = Arc::new(WeakHandler(Arc::downgrade(&self)));
2670        for conn in &self.common.connection_pool {
2671            conn.set_handler(handler.clone()).await;
2672        }
2673
2674        let base_pool_size = self.configuration.mode.pool_size();
2675
2676        let connect_fut = async {
2677            if self.url_paths.is_empty() {
2678                let url = self.prepare_url(&streams, None);
2679                self.common.clone().connect_pool(&url, None).await
2680            } else {
2681                let mut futures = Vec::with_capacity(self.url_paths.len());
2682
2683                for (i, path) in self.url_paths.iter().enumerate() {
2684                    let start = i * base_pool_size;
2685
2686                    let subset: Vec<Arc<WebsocketConnection>> = self
2687                        .common
2688                        .connection_pool
2689                        .iter()
2690                        .skip(start)
2691                        .take(base_pool_size)
2692                        .cloned()
2693                        .collect();
2694
2695                    if subset.len() != base_pool_size {
2696                        return Err(WebsocketError::ServerError(format!(
2697                            "connection_pool too small for url_paths: need {} per path, got {} for path index {}",
2698                            base_pool_size,
2699                            subset.len(),
2700                            i
2701                        )));
2702                    }
2703
2704                    for c in &subset {
2705                        let mut st = c.state.lock().await;
2706                        st.url_path = Some(path.clone());
2707                    }
2708
2709                    let url = self.prepare_url(&streams, Some(path.as_str()));
2710                    let common = self.common.clone();
2711
2712                    futures.push(async move { common.connect_pool(&url, Some(subset)).await });
2713                }
2714
2715                try_join_all(futures).await?;
2716                Ok(())
2717            }
2718        };
2719
2720        let connect_res = select! {
2721            () = sleep(Duration::from_secs(10)) => Err(WebsocketError::Timeout),
2722            r = connect_fut => r,
2723        };
2724
2725        {
2726            let mut flag = self.is_connecting.lock().await;
2727            *flag = false;
2728        }
2729
2730        // Signed streams are not part of the URL, they have to be subscribed explicitly.
2731        if connect_res.is_ok() && signed && !streams.is_empty() {
2732            Arc::clone(&self).subscribe(streams, None, None).await;
2733        }
2734
2735        connect_res
2736    }
2737
2738    /// Returns `true` when the signed SAPI websocket streams protocol is enabled.
2739    fn is_signed_streams(&self) -> bool {
2740        self.signed_streams.load(Ordering::Relaxed)
2741    }
2742
2743    /// Ensures that the configuration contains the credentials required by signed streams.
2744    ///
2745    /// # Errors
2746    ///
2747    /// Returns a `WebsocketError::Handshake` if the API key or the signing credentials are missing.
2748    fn assert_signed_streams_credentials(&self) -> Result<(), WebsocketError> {
2749        if self
2750            .configuration
2751            .api_key
2752            .as_deref()
2753            .unwrap_or("")
2754            .is_empty()
2755        {
2756            return Err(WebsocketError::Handshake(
2757                "'api_key' is required for signed websocket streams.".to_string(),
2758            ));
2759        }
2760        if self.configuration.api_secret.is_none() && self.configuration.private_key.is_none() {
2761            return Err(WebsocketError::Handshake(
2762                "Either 'api_secret' or 'private_key' must be provided for signed websocket streams."
2763                    .to_string(),
2764            ));
2765        }
2766        Ok(())
2767    }
2768
2769    /// Builds a signed WebSocket URL for signed streams.
2770    ///
2771    /// The query string `random=<random>&recvWindow=<recvWindow>&timestamp=<timestamp>` is
2772    /// signed with the configured API secret or private key and the signature is appended.
2773    /// A fresh timestamp and signature are generated on every call, so reconnections and
2774    /// renewals always use a valid URL.
2775    fn prepare_signed_url(&self) -> String {
2776        let base = self.configuration.ws_url.as_deref().unwrap_or("");
2777        let query = format!(
2778            "random={}&recvWindow={}&timestamp={}",
2779            random_string(),
2780            SIGNED_STREAMS_RECV_WINDOW,
2781            get_timestamp_from(self.configuration.timestamp_provider.as_ref())
2782        );
2783
2784        let signature = match self.signature_gen.get_signature_from_str(&query) {
2785            Ok(sig) => sig,
2786            Err(e) => {
2787                error!("Failed to sign websocket streams URL: {:?}", e);
2788                String::new()
2789            }
2790        };
2791        let encoded_signature: String =
2792            url::form_urlencoded::byte_serialize(signature.as_bytes()).collect();
2793        let sep = if base.contains('?') { '&' } else { '?' };
2794
2795        format!("{base}{sep}{query}&signature={encoded_signature}")
2796    }
2797
2798    /// Parses the payload of a signed stream message.
2799    ///
2800    /// Signed streams deliver `data` as a JSON-encoded string, so it is parsed when possible.
2801    fn parse_signed_stream_data(data: Value) -> Value {
2802        match data {
2803            Value::String(ref s) => serde_json::from_str::<Value>(s).unwrap_or(data),
2804            other => other,
2805        }
2806    }
2807
2808    /// Spawns a task sending a PING frame every 30 seconds on the given connection, as
2809    /// required by signed streams. The task stops when the connection writer changes
2810    /// (reconnection / renewal) or is closed.
2811    fn spawn_signed_streams_ping(connection: &Arc<WebsocketConnection>) {
2812        let conn = Arc::clone(connection);
2813        spawn(async move {
2814            let Some(writer) = conn.state.lock().await.ws_write_tx.clone() else {
2815                return;
2816            };
2817
2818            loop {
2819                sleep(SIGNED_STREAMS_PING_INTERVAL).await;
2820
2821                let still_current = {
2822                    let st = conn.state.lock().await;
2823                    !st.close_initiated
2824                        && st
2825                            .ws_write_tx
2826                            .as_ref()
2827                            .is_some_and(|tx| tx.same_channel(&writer))
2828                };
2829                if !still_current {
2830                    break;
2831                }
2832
2833                if writer.send(Message::Ping(Vec::new().into())).is_err() {
2834                    break;
2835                }
2836                debug!("PING sent to connection {}", conn.id);
2837            }
2838        });
2839    }
2840
2841    /// Disconnects all WebSocket connections and clears associated state.
2842    ///
2843    /// # Returns
2844    ///
2845    /// A `Result` indicating whether the disconnection was successful or an error occurred
2846    ///
2847    /// # Errors
2848    ///
2849    /// Returns a `WebsocketError` if there are issues during the disconnection process
2850    ///
2851    /// # Side Effects
2852    ///
2853    /// - Clears stream callbacks for all connections
2854    /// - Clears pending subscriptions for all connections
2855    /// - Removes all connection stream mappings
2856    pub async fn disconnect(&self) -> Result<(), WebsocketError> {
2857        for connection in &self.common.connection_pool {
2858            let mut conn_state = connection.state.lock().await;
2859            conn_state.stream_callbacks.clear();
2860            conn_state.pending_subscriptions.clear();
2861        }
2862        self.connection_streams.lock().await.clear();
2863        self.common.disconnect().await
2864    }
2865
2866    /// Checks if the WebSocket connection is currently active.
2867    ///
2868    /// # Returns
2869    ///
2870    /// `true` if the WebSocket connection is established, `false` otherwise.
2871    pub async fn is_connected(&self) -> bool {
2872        self.common.is_connected(None).await
2873    }
2874
2875    /// Sends a ping to the WebSocket server to maintain the connection.
2876    ///
2877    /// This method delegates the ping operation to the underlying common WebSocket connection.
2878    /// It is typically used to keep the connection alive and check its status.
2879    ///
2880    /// # Side Effects
2881    ///
2882    /// Sends a ping request to the WebSocket server through the common connection.
2883    pub async fn ping_server(&self) {
2884        self.common.ping_server().await;
2885    }
2886
2887    /// Subscribes to multiple WebSocket streams, handling connection and queuing logic.
2888    ///
2889    /// # Arguments
2890    ///
2891    /// * `streams` - A vector of stream names to subscribe to
2892    /// * `id` - An optional request identifier for the subscription
2893    /// * `url_path` - An optional URL path for the subscription
2894    ///
2895    /// # Behavior
2896    ///
2897    /// - Filters out streams already subscribed
2898    /// - Assigns streams to appropriate connections
2899    /// - Handles subscription for active connections
2900    /// - Queues subscriptions for inactive connections
2901    ///
2902    /// # Side Effects
2903    ///
2904    /// - Sends subscription payloads for active connections
2905    /// - Adds pending subscriptions for inactive connections
2906    pub async fn subscribe(
2907        self: Arc<Self>,
2908        streams: Vec<String>,
2909        id: Option<StreamId>,
2910        url_path: Option<&str>,
2911    ) {
2912        let streams: Vec<String> = {
2913            let map = self.connection_streams.lock().await;
2914            streams
2915                .into_iter()
2916                .filter(|s| {
2917                    let key = self.stream_key(s, url_path);
2918                    !map.contains_key(&key)
2919                })
2920                .collect()
2921        };
2922
2923        if streams.is_empty() {
2924            return;
2925        }
2926
2927        let connection_streams = self.handle_stream_assignment(streams, url_path).await;
2928
2929        for (conn, assigned_streams) in connection_streams {
2930            if !self.common.is_connected(Some(&conn)).await {
2931                info!(
2932                    "Connection {} is not ready. Queuing subscription for streams: {:?}",
2933                    conn.id, assigned_streams
2934                );
2935
2936                let mut conn_state = conn.state.lock().await;
2937                conn_state
2938                    .pending_subscriptions
2939                    .extend(assigned_streams.iter().cloned());
2940
2941                continue;
2942            }
2943
2944            self.send_subscription_payload(&conn, &assigned_streams, id.clone());
2945        }
2946    }
2947
2948    /// Unsubscribes from specified WebSocket streams.
2949    ///
2950    /// # Arguments
2951    ///
2952    /// * `streams` - A vector of stream names to unsubscribe from
2953    /// * `id` - An optional request identifier for the unsubscription
2954    /// * `url_path` - An optional URL path for the unsubscription
2955    ///
2956    /// # Behavior
2957    ///
2958    /// - Validates the request identifier or generates a random one
2959    /// - Checks for active connections and subscribed streams
2960    /// - Sends unsubscribe payload for streams with active callbacks
2961    /// - Removes stream from connection streams and callbacks
2962    ///
2963    /// # Side Effects
2964    ///
2965    /// - Sends unsubscribe request to WebSocket server
2966    /// - Removes stream tracking from internal state
2967    ///
2968    /// # Async
2969    ///
2970    /// This method is asynchronous and requires `.await` when called
2971    ///
2972    /// # Panics
2973    ///
2974    /// This method may panic if the request identifier is not valid.
2975    ///
2976    pub async fn unsubscribe(
2977        &self,
2978        streams: Vec<String>,
2979        id: Option<StreamId>,
2980        url_path: Option<&str>,
2981    ) {
2982        let request_id = normalize_stream_id(
2983            id.clone(),
2984            self.stream_id_is_strictly_number.load(Ordering::Relaxed),
2985        );
2986        let signed = self.is_signed_streams();
2987        let mut signed_unsubscriptions: Vec<(Arc<WebsocketConnection>, Vec<String>)> = Vec::new();
2988
2989        for stream in streams {
2990            let key = self.stream_key(&stream, url_path);
2991            let maybe_conn = { self.connection_streams.lock().await.get(&key).cloned() };
2992
2993            let conn = match maybe_conn {
2994                Some(c) => {
2995                    if !self.common.is_connected(Some(&c)).await {
2996                        warn!(
2997                            "Stream {} not associated with an active connection.",
2998                            stream
2999                        );
3000                        continue;
3001                    }
3002                    c
3003                }
3004                None => {
3005                    warn!("Stream {} was not subscribed.", stream);
3006                    continue;
3007                }
3008            };
3009
3010            let has_callbacks = {
3011                let conn_state = conn.state.lock().await;
3012                conn_state
3013                    .stream_callbacks
3014                    .get(&key)
3015                    .is_some_and(|v| !v.is_empty())
3016            };
3017
3018            if has_callbacks {
3019                continue;
3020            }
3021
3022            if signed {
3023                match signed_unsubscriptions
3024                    .iter_mut()
3025                    .find(|(c, _)| Arc::ptr_eq(c, &conn))
3026                {
3027                    Some((_, pending)) => pending.push(stream.clone()),
3028                    None => signed_unsubscriptions.push((Arc::clone(&conn), vec![stream.clone()])),
3029                }
3030            } else {
3031                let payload = json!({
3032                    "method": "UNSUBSCRIBE",
3033                    "params": [stream.clone()],
3034                    "id": request_id,
3035                });
3036
3037                info!("UNSUBSCRIBE → {:?}", payload);
3038
3039                let common = Arc::clone(&self.common);
3040                let conn_clone = Arc::clone(&conn);
3041                let msg = serde_json::to_string(&payload).unwrap();
3042                spawn(async move {
3043                    let _ = common
3044                        .send(msg, None, false, Duration::ZERO, Some(conn_clone))
3045                        .await;
3046                });
3047            }
3048
3049            {
3050                let mut connection_streams = self.connection_streams.lock().await;
3051                connection_streams.remove(&key);
3052            }
3053            {
3054                let mut conn_state = conn.state.lock().await;
3055                conn_state.stream_callbacks.remove(&key);
3056            }
3057        }
3058
3059        // A single command per connection with `|`-joined topics keeps within the
3060        // 5 messages/second limit.
3061        for (conn, pending) in signed_unsubscriptions {
3062            let payload = json!({
3063                "command": "UNSUBSCRIBE",
3064                "value": pending.join("|"),
3065            });
3066
3067            info!("UNSUBSCRIBE → {:?}", payload);
3068
3069            let common = Arc::clone(&self.common);
3070            let msg = serde_json::to_string(&payload).unwrap();
3071            spawn(async move {
3072                let _ = common
3073                    .send(msg, None, false, Duration::ZERO, Some(conn))
3074                    .await;
3075            });
3076        }
3077    }
3078
3079    /// Checks if a specific stream is currently subscribed.
3080    ///
3081    /// # Arguments
3082    ///
3083    /// * `stream` - The stream identifier to check for subscription status
3084    ///
3085    /// # Returns
3086    ///
3087    /// `true` if the stream is subscribed, `false` otherwise
3088    ///
3089    /// # Async
3090    ///
3091    /// This method is asynchronous and requires `.await` when called
3092    pub async fn is_subscribed(&self, stream: &str) -> bool {
3093        let map = self.connection_streams.lock().await;
3094
3095        if map.contains_key(stream) {
3096            return true;
3097        }
3098
3099        let suffix = format!("::{}", stream);
3100        map.keys().any(|k| k.ends_with(&suffix))
3101    }
3102
3103    /// Generates a unique key for a stream based on its name and optional URL path.
3104    ///
3105    /// # Arguments
3106    ///
3107    /// * `stream` - The name of the stream
3108    /// * `url_path` - An optional URL path associated with the stream
3109    ///
3110    /// # Returns
3111    ///
3112    /// A `String` representing the unique key for the stream
3113    ///
3114    fn stream_key(&self, stream: &str, url_path: Option<&str>) -> String {
3115        match url_path {
3116            Some(p) if !p.is_empty() => format!("{p}::{stream}"),
3117            _ => stream.to_string(),
3118        }
3119    }
3120
3121    /// Prepares a WebSocket URL for streaming with optional stream names and time unit configuration.
3122    ///
3123    /// # Arguments
3124    ///
3125    /// * `streams` - A slice of stream names to be included in the URL
3126    /// * `url_path` - An optional path to append to the base WebSocket URL
3127    ///
3128    /// # Returns
3129    ///
3130    /// A fully constructed WebSocket URL with optional stream and time unit parameters
3131    ///
3132    /// # Notes
3133    ///
3134    /// - If no time unit is specified, the base URL is returned
3135    /// - Validates and appends the time unit parameter if provided and valid
3136    /// - Handles URL parameter separator based on existing query parameters
3137    fn prepare_url(&self, streams: &[String], url_path: Option<&str>) -> String {
3138        if self.is_signed_streams() {
3139            return self.prepare_signed_url();
3140        }
3141
3142        let mut url = format!(
3143            "{}/stream?streams={}",
3144            match url_path {
3145                Some(path) => format!(
3146                    "{}/{}",
3147                    self.configuration.ws_url.as_deref().unwrap_or(""),
3148                    path
3149                ),
3150                None => self
3151                    .configuration
3152                    .ws_url
3153                    .as_deref()
3154                    .unwrap_or("")
3155                    .to_string(),
3156            },
3157            streams.join("/")
3158        );
3159
3160        let time_unit = match &self.configuration.time_unit {
3161            Some(u) => u.to_string(),
3162            None => return url,
3163        };
3164
3165        match validate_time_unit(&time_unit) {
3166            Ok(Some(validated)) => {
3167                let sep = if url.contains('?') { '&' } else { '?' };
3168                url.push(sep);
3169                url.push_str("timeUnit=");
3170                url.push_str(validated);
3171            }
3172            Ok(None) => {}
3173            Err(e) => {
3174                error!("Invalid time unit provided: {:?}", e);
3175            }
3176        }
3177
3178        url
3179    }
3180
3181    /// Handles stream assignment by finding or creating WebSocket connections for a list of streams.
3182    ///
3183    /// This method attempts to assign streams to existing WebSocket connections or creates new
3184    /// connections if needed. It groups streams by their assigned connections and handles scenarios
3185    /// such as closed or pending reconnection connections.
3186    ///
3187    /// # Arguments
3188    ///
3189    /// * `streams` - A vector of stream names to be assigned
3190    /// * `url_path` - An optional URL path associated with the streams
3191    ///
3192    /// # Returns
3193    ///
3194    /// A vector of tuples containing WebSocket connections and their associated streams
3195    ///
3196    /// # Errors
3197    ///
3198    /// Returns an empty result if no connections can be established for the streams
3199    async fn handle_stream_assignment(
3200        &self,
3201        streams: Vec<String>,
3202        url_path: Option<&str>,
3203    ) -> Vec<(Arc<WebsocketConnection>, Vec<String>)> {
3204        let mut connection_streams: Vec<(String, Arc<WebsocketConnection>)> = Vec::new();
3205
3206        for stream in streams {
3207            let key = self.stream_key(&stream, url_path);
3208
3209            let mut conn_opt = {
3210                let map = self.connection_streams.lock().await;
3211                map.get(&key).cloned()
3212            };
3213
3214            let need_new = if let Some(conn) = &conn_opt {
3215                let state = conn.state.lock().await;
3216                state.close_initiated || state.reconnection_pending
3217            } else {
3218                true
3219            };
3220
3221            if need_new {
3222                match self.common.get_connection(true, url_path).await {
3223                    Ok(new_conn) => {
3224                        let mut map = self.connection_streams.lock().await;
3225                        map.insert(key.clone(), new_conn.clone());
3226                        conn_opt = Some(new_conn);
3227                    }
3228                    Err(err) => {
3229                        warn!(
3230                            "No available WebSocket connection to subscribe stream `{}` (key `{}`): {:?}",
3231                            stream, key, err
3232                        );
3233                        continue;
3234                    }
3235                }
3236            }
3237
3238            if let Some(conn) = conn_opt {
3239                {
3240                    let mut conn_state = conn.state.lock().await;
3241                    conn_state.stream_callbacks.entry(key.clone()).or_default();
3242                }
3243                connection_streams.push((stream, conn));
3244            }
3245        }
3246
3247        let mut groups: Vec<(Arc<WebsocketConnection>, Vec<String>)> = Vec::new();
3248        for (stream, conn) in connection_streams {
3249            if let Some((_, vec)) = groups.iter_mut().find(|(c, _)| Arc::ptr_eq(c, &conn)) {
3250                vec.push(stream);
3251            } else {
3252                groups.push((conn, vec![stream]));
3253            }
3254        }
3255
3256        groups
3257    }
3258
3259    /// Sends a WebSocket subscription payload for the specified streams.
3260    ///
3261    /// # Arguments
3262    ///
3263    /// * `connection` - The WebSocket connection to send the subscription on
3264    /// * `streams` - A vector of stream names to subscribe to
3265    /// * `id` - An optional request ID for the subscription (will be randomly generated if not provided)
3266    ///
3267    /// # Remarks
3268    ///
3269    /// This method constructs a SUBSCRIBE payload, logs it, and sends it asynchronously using the WebSocket connection.
3270    /// If serialization fails, an error is logged and the method returns without sending.
3271    fn send_subscription_payload(
3272        &self,
3273        connection: &Arc<WebsocketConnection>,
3274        streams: &Vec<String>,
3275        id: Option<StreamId>,
3276    ) {
3277        let payloads: Vec<Value> = if self.is_signed_streams() {
3278            // A single command with `|`-joined topics keeps within the 5 messages/second limit.
3279            if streams.is_empty() {
3280                return;
3281            }
3282            vec![json!({ "command": "SUBSCRIBE", "value": streams.join("|") })]
3283        } else {
3284            let request_id = normalize_stream_id(
3285                id.clone(),
3286                self.stream_id_is_strictly_number.load(Ordering::Relaxed),
3287            );
3288
3289            vec![json!({
3290                "method": "SUBSCRIBE",
3291                "params": streams,
3292                "id": request_id,
3293            })]
3294        };
3295
3296        let mut messages = Vec::with_capacity(payloads.len());
3297        for payload in payloads {
3298            info!("SUBSCRIBE → {:?}", payload);
3299            match serde_json::to_string(&payload) {
3300                Ok(s) => messages.push(s),
3301                Err(e) => {
3302                    error!("Failed to serialize SUBSCRIBE payload: {}", e);
3303                    return;
3304                }
3305            }
3306        }
3307
3308        let common = Arc::clone(&self.common);
3309        let conn_clone = Arc::clone(connection);
3310
3311        spawn(async move {
3312            for msg in messages {
3313                let _ = common
3314                    .send(
3315                        msg,
3316                        None,
3317                        false,
3318                        Duration::ZERO,
3319                        Some(Arc::clone(&conn_clone)),
3320                    )
3321                    .await;
3322            }
3323        });
3324    }
3325}
3326
3327#[async_trait]
3328impl WebsocketHandler for WebsocketStreams {
3329    /// Handles the WebSocket connection opening by processing any pending subscriptions.
3330    ///
3331    /// This method is called when a WebSocket connection is established. It retrieves
3332    /// any pending stream subscriptions from the connection state and sends them
3333    /// immediately using the `send_subscription_payload` method.
3334    ///
3335    /// # Arguments
3336    ///
3337    /// * `_url` - The URL of the WebSocket connection (unused)
3338    /// * `connection` - The WebSocket connection that has just been opened
3339    ///
3340    /// # Remarks
3341    ///
3342    /// If there are any pending subscriptions, they are sent as a batch subscription
3343    /// payload. The method uses a lock to safely access and clear the pending subscriptions
3344    /// from the connection state.
3345    async fn on_open(&self, _url: String, connection: Arc<WebsocketConnection>) {
3346        let mut pending_subs: Vec<String> = {
3347            let mut conn_state = connection.state.lock().await;
3348            take(&mut conn_state.pending_subscriptions)
3349                .into_iter()
3350                .collect()
3351        };
3352
3353        if self.is_signed_streams() {
3354            let assigned: Vec<String> = {
3355                let map = self.connection_streams.lock().await;
3356                map.iter()
3357                    .filter(|(_, conn)| Arc::ptr_eq(conn, &connection))
3358                    .map(|(key, _)| match key.split_once("::") {
3359                        Some((_prefix, rest)) => rest.to_string(),
3360                        None => key.clone(),
3361                    })
3362                    .collect()
3363            };
3364            for stream in assigned {
3365                if !pending_subs.contains(&stream) {
3366                    pending_subs.push(stream);
3367                }
3368            }
3369
3370            Self::spawn_signed_streams_ping(&connection);
3371        }
3372
3373        if !pending_subs.is_empty() {
3374            info!("Processing queued subscriptions for connection");
3375            self.send_subscription_payload(&connection, &pending_subs, None);
3376        }
3377    }
3378
3379    /// Handles incoming WebSocket stream messages by parsing the JSON payload and invoking registered stream callbacks.
3380    ///
3381    /// This method processes WebSocket messages with a specific structure, extracting the stream name and data.
3382    /// It retrieves and executes any registered callbacks associated with the stream name.
3383    ///
3384    /// # Arguments
3385    ///
3386    /// * `data` - The raw WebSocket message as a JSON-formatted string
3387    /// * `connection` - The WebSocket connection through which the message was received
3388    ///
3389    /// # Behavior
3390    ///
3391    /// - Parses the JSON message
3392    /// - Extracts the stream name and data payload
3393    /// - Looks up and invokes any registered callbacks for the stream
3394    /// - Silently returns if message parsing or stream extraction fails
3395    async fn on_message(&self, data: String, connection: Arc<WebsocketConnection>) {
3396        let msg: Value = match serde_json::from_str(&data) {
3397            Ok(v) => v,
3398            Err(err) => {
3399                error!(
3400                    "Failed to parse WebSocket stream message {} – {}",
3401                    data, err
3402                );
3403                return;
3404            }
3405        };
3406
3407        let signed = self.is_signed_streams();
3408
3409        if signed && msg.get("type").and_then(Value::as_str) == Some("COMMAND") {
3410            let sub_type = msg.get("subType").and_then(Value::as_str).unwrap_or("");
3411            if msg.get("data").and_then(Value::as_str) == Some("SUCCESS") {
3412                debug!("{} command succeeded: {}", sub_type, data);
3413            } else {
3414                warn!("{} command failed: {}", sub_type, data);
3415            }
3416            return;
3417        }
3418
3419        let stream_field = msg.get("stream").and_then(Value::as_str).or_else(|| {
3420            if signed {
3421                msg.get("topic").and_then(Value::as_str)
3422            } else {
3423                None
3424            }
3425        });
3426
3427        let (stream_name, payload) = match (stream_field, msg.get("data").cloned()) {
3428            (Some(name), Some(data)) => (
3429                name.to_string(),
3430                if signed {
3431                    Self::parse_signed_stream_data(data)
3432                } else {
3433                    data
3434                },
3435            ),
3436            _ => return,
3437        };
3438
3439        let callbacks = {
3440            let conn_state = connection.state.lock().await;
3441            let key = self.stream_key(&stream_name, conn_state.url_path.as_deref());
3442            conn_state
3443                .stream_callbacks
3444                .get(&key)
3445                .cloned()
3446                .unwrap_or_else(Vec::new)
3447        };
3448
3449        for callback in callbacks {
3450            callback(&payload);
3451        }
3452    }
3453
3454    /// Retrieves the reconnection URL for a specific WebSocket connection by identifying all streams associated with that connection.
3455    ///
3456    /// # Arguments
3457    ///
3458    /// * `_default_url` - A default URL that can be used if no specific reconnection URL is determined
3459    /// * `connection` - The WebSocket connection for which to generate a reconnection URL
3460    ///
3461    /// # Returns
3462    ///
3463    /// A URL string that can be used to reconnect to the WebSocket, based on the streams associated with the given connection
3464    async fn get_reconnect_url(
3465        &self,
3466        _default_url: String,
3467        connection: Arc<WebsocketConnection>,
3468    ) -> String {
3469        let connection_streams = self.connection_streams.lock().await;
3470        let reconnect_streams = connection_streams
3471            .iter()
3472            .filter_map(|(key, conn_arc)| {
3473                if Arc::ptr_eq(conn_arc, &connection) {
3474                    let stream = match key.split_once("::") {
3475                        Some((_prefix, rest)) => rest.to_string(),
3476                        None => key.clone(),
3477                    };
3478                    Some(stream)
3479                } else {
3480                    None
3481                }
3482            })
3483            .collect::<Vec<_>>();
3484
3485        let url_path = {
3486            let st = connection.state.lock().await;
3487            st.url_path.as_deref().map(std::string::ToString::to_string)
3488        };
3489
3490        self.prepare_url(&reconnect_streams, url_path.as_deref())
3491    }
3492}
3493
3494pub struct WebsocketStream<T> {
3495    websocket_base: WebsocketBase,
3496    stream_or_id: String,
3497    url_path: Option<String>,
3498    callback: Mutex<Option<Arc<dyn Fn(&Value) + Send + Sync>>>,
3499    pub id: Option<StreamId>,
3500    _phantom: PhantomData<T>,
3501}
3502
3503impl<T> WebsocketStream<T>
3504where
3505    T: DeserializeOwned + Send + 'static,
3506{
3507    /// Registers a callback function for a specific event on the WebSocket stream.
3508    ///
3509    /// This method currently only supports the "message" event. When a message is received,
3510    /// the provided callback function will be invoked with the deserialized payload.
3511    ///
3512    /// # Arguments
3513    ///
3514    /// * `event` - The event type to listen for (currently only "message" is supported)
3515    /// * `callback_fn` - A function that will be called with the deserialized message payload
3516    ///
3517    /// # Errors
3518    ///
3519    /// Logs an error if the payload cannot be deserialized into the expected type
3520    ///
3521    /// # Examples
3522    ///
3523    ///
3524    /// stream.on("message", |data: `MyType`| {
3525    ///     // Handle the deserialized message
3526    /// });
3527    async fn on<F>(&self, event: &str, callback_fn: F)
3528    where
3529        F: Fn(T) + Send + Sync + 'static,
3530    {
3531        if event != "message" {
3532            return;
3533        }
3534
3535        let cb_wrapper: Arc<dyn Fn(&Value) + Send + Sync> =
3536            Arc::new(
3537                move |v: &Value| match serde_json::from_value::<T>(v.clone()) {
3538                    Ok(data) => callback_fn(data),
3539                    Err(e) => error!("Failed to deserialize stream payload: {:?}", e),
3540                },
3541            );
3542
3543        {
3544            let mut guard = self.callback.lock().await;
3545            *guard = Some(cb_wrapper.clone());
3546        }
3547
3548        match &self.websocket_base {
3549            WebsocketBase::WebsocketStreams(ws_streams) => {
3550                let key = ws_streams.stream_key(&self.stream_or_id, self.url_path.as_deref());
3551                let conn = {
3552                    let map = ws_streams.connection_streams.lock().await;
3553                    map.get(&key).cloned().expect("stream must be subscribed")
3554                };
3555
3556                {
3557                    let mut conn_state = conn.state.lock().await;
3558                    let entry = conn_state.stream_callbacks.entry(key).or_default();
3559
3560                    if !entry
3561                        .iter()
3562                        .any(|existing| Arc::ptr_eq(existing, &cb_wrapper))
3563                    {
3564                        entry.push(cb_wrapper);
3565                    }
3566                }
3567            }
3568            WebsocketBase::WebsocketApi(ws_api) => {
3569                let mut stream_callbacks = ws_api.stream_callbacks.lock().await;
3570                let entry = stream_callbacks
3571                    .entry(self.stream_or_id.clone())
3572                    .or_default();
3573
3574                if !entry
3575                    .iter()
3576                    .any(|existing| Arc::ptr_eq(existing, &cb_wrapper))
3577                {
3578                    entry.push(cb_wrapper);
3579                }
3580            }
3581        }
3582    }
3583
3584    /// Synchronously sets a message callback for the WebSocket stream on the current thread.
3585    ///
3586    /// # Arguments
3587    ///
3588    /// * `callback_fn` - A function that will be called with the deserialized message payload
3589    ///
3590    /// # Panics
3591    ///
3592    /// Panics if the thread runtime fails to be created or if the thread join fails
3593    ///
3594    /// # Examples
3595    ///
3596    ///
3597    /// let stream = `Arc::new(WebsocketStream::new())`;
3598    /// `stream.on_message(|data`: `MyType`| {
3599    ///     // Handle the deserialized message
3600    /// });
3601    ///
3602    pub fn on_message<F>(self: &Arc<Self>, callback_fn: F)
3603    where
3604        T: Send + Sync,
3605        F: Fn(T) + Send + Sync + 'static,
3606    {
3607        let handler: Arc<Self> = Arc::clone(self);
3608
3609        std::thread::spawn(move || {
3610            let rt = tokio::runtime::Builder::new_current_thread()
3611                .enable_all()
3612                .build()
3613                .expect("failed to build Tokio runtime");
3614
3615            rt.block_on(handler.on("message", callback_fn));
3616        })
3617        .join()
3618        .expect("on_message thread panicked");
3619    }
3620
3621    /// Unsubscribes from the current WebSocket stream and removes the associated callback.
3622    ///
3623    /// This method performs the following actions:
3624    /// - Removes the current callback associated with the stream
3625    /// - Removes the callback from the connection's stream callbacks
3626    /// - Asynchronously unsubscribes from the stream using the WebSocket streams base
3627    ///
3628    /// # Panics
3629    ///
3630    /// Panics if the stream is not subscribed to
3631    ///
3632    /// # Notes
3633    /// - If no callback is present, no action is taken
3634    /// - Spawns an asynchronous task to handle the unsubscription process
3635    pub async fn unsubscribe(&self) {
3636        let maybe_cb = {
3637            let mut guard = self.callback.lock().await;
3638            guard.take()
3639        };
3640
3641        if let Some(cb) = maybe_cb {
3642            match &self.websocket_base {
3643                WebsocketBase::WebsocketStreams(ws_streams) => {
3644                    let key = ws_streams.stream_key(&self.stream_or_id, self.url_path.as_deref());
3645                    let conn = {
3646                        let map = ws_streams.connection_streams.lock().await;
3647                        map.get(&key)
3648                            .cloned()
3649                            .expect("stream must have been subscribed")
3650                    };
3651
3652                    {
3653                        let mut conn_state = conn.state.lock().await;
3654                        if let Some(list) = conn_state.stream_callbacks.get_mut(&key) {
3655                            list.retain(|existing| !Arc::ptr_eq(existing, &cb));
3656                        }
3657                    }
3658
3659                    let stream = self.stream_or_id.clone();
3660                    let id = self.id.clone();
3661                    let url_path = self.url_path.clone();
3662                    let websocket_streams_base = Arc::clone(ws_streams);
3663                    spawn(async move {
3664                        websocket_streams_base
3665                            .unsubscribe(vec![stream], id, url_path.as_deref())
3666                            .await;
3667                    });
3668                }
3669                WebsocketBase::WebsocketApi(ws_api) => {
3670                    let mut stream_callbacks = ws_api.stream_callbacks.lock().await;
3671                    if let Some(list) = stream_callbacks.get_mut(&self.stream_or_id) {
3672                        list.retain(|existing| !Arc::ptr_eq(existing, &cb));
3673                    }
3674                }
3675            }
3676        }
3677    }
3678}
3679
3680/// Creates a new WebSocket stream handler for the specified stream or ID.
3681/// This function subscribes to the stream if the WebSocket base is of type `WebsocketStreams`.
3682///
3683/// # Arguments
3684///
3685/// * `websocket_base` - The base WebSocket instance (either `WebsocketStreams` or `WebsocketApi`)
3686/// * `stream_or_id` - The stream name or identifier to subscribe to
3687/// * `id` - An optional request identifier for the subscription
3688/// * `url_path` - An optional URL path for the subscription
3689///
3690/// # Returns
3691///
3692/// A new `WebsocketStream` instance.
3693///
3694pub async fn create_stream_handler<T>(
3695    websocket_base: WebsocketBase,
3696    stream_or_id: String,
3697    id: Option<StreamId>,
3698    url_path: Option<String>,
3699) -> Arc<WebsocketStream<T>>
3700where
3701    T: DeserializeOwned + Send + 'static,
3702{
3703    match &websocket_base {
3704        WebsocketBase::WebsocketStreams(ws_streams) => {
3705            ws_streams
3706                .clone()
3707                .subscribe(vec![stream_or_id.clone()], id.clone(), url_path.as_deref())
3708                .await;
3709        }
3710        WebsocketBase::WebsocketApi(_) => {}
3711    }
3712
3713    Arc::new(WebsocketStream {
3714        websocket_base,
3715        stream_or_id,
3716        url_path,
3717        id,
3718        callback: Mutex::new(None),
3719        _phantom: PhantomData,
3720    })
3721}
3722
3723#[cfg(test)]
3724mod tests {
3725    use crate::TOKIO_SHARED_RT;
3726    use crate::common::utils::{SignatureGenerator, build_user_agent};
3727    use crate::common::websocket::{
3728        PendingRequest, RECONNECT_RETRY_BASE_DELAY, RECONNECT_RETRY_MAX_DELAY,
3729        ReconnectDisposition, ReconnectEntry, SendWebsocketMessageResult, WebsocketApi,
3730        WebsocketBase, WebsocketCommon, WebsocketConnection, WebsocketConnectionState,
3731        WebsocketEvent, WebsocketEventEmitter, WebsocketHandler, WebsocketMessageSendOptions,
3732        WebsocketMode, WebsocketSessionLogonReq, WebsocketStream, WebsocketStreams,
3733        create_stream_handler,
3734    };
3735    use crate::config::{
3736        ConfigurationWebsocketApi, ConfigurationWebsocketStreams, PrivateKey, WebsocketHandshakeFn,
3737    };
3738    use crate::errors::{WebsocketConnectionFailureReason, WebsocketError};
3739    use crate::models::{StreamId, TimeUnit};
3740    use async_trait::async_trait;
3741    use futures::{SinkExt, StreamExt};
3742    use http::header::USER_AGENT;
3743    use regex::Regex;
3744    use serde_json::{Value, json};
3745    use std::collections::{BTreeMap, HashSet};
3746    use std::marker::PhantomData;
3747    use std::net::SocketAddr;
3748    use std::sync::{
3749        Arc,
3750        atomic::{AtomicBool, AtomicUsize, Ordering},
3751    };
3752    use tokio::net::TcpListener;
3753    use tokio::sync::{
3754        Mutex,
3755        mpsc::{Receiver, UnboundedSender, unbounded_channel},
3756        oneshot,
3757    };
3758    use tokio::time::{Duration, advance, pause, resume, sleep, timeout};
3759    use tokio_tungstenite::{
3760        accept_async, accept_hdr_async, tungstenite,
3761        tungstenite::{
3762            Message,
3763            protocol::{CloseFrame, frame::coding::CloseCode},
3764        },
3765    };
3766    use tungstenite::handshake::server::Request;
3767
3768    /// RAII guard that aborts a spawned task when dropped.
3769    ///
3770    /// Used by tests to bound the lifetime of mock listener / spawned helper tasks
3771    /// to the scope of the test, so they don't leak into `TOKIO_SHARED_RT` and
3772    /// add nondeterministic background load to subsequent tests.
3773    struct AbortOnDrop(tokio::task::JoinHandle<()>);
3774
3775    impl Drop for AbortOnDrop {
3776        fn drop(&mut self) {
3777            self.0.abort();
3778        }
3779    }
3780
3781    /// Spawn a mock WebSocket listener that accepts a single connection and
3782    /// holds it open until the client disconnects, then exits. The returned
3783    /// guard aborts the task when dropped.
3784    fn spawn_mock_ws_listener(listener: TcpListener) -> AbortOnDrop {
3785        AbortOnDrop(tokio::spawn(async move {
3786            if let Ok((stream, _)) = listener.accept().await {
3787                let Ok(mut ws) = accept_async(stream).await else {
3788                    return;
3789                };
3790                while ws.next().await.is_some() {}
3791            }
3792        }))
3793    }
3794
3795    fn subscribe_events(common: &WebsocketCommon) -> Arc<Mutex<Vec<WebsocketEvent>>> {
3796        let events = Arc::new(Mutex::new(Vec::new()));
3797        let events_clone = events.clone();
3798        common.events.subscribe(move |event| {
3799            let events_clone = events_clone.clone();
3800            tokio::spawn(async move {
3801                events_clone.lock().await.push(event);
3802            });
3803        });
3804        events
3805    }
3806
3807    async fn create_connection(
3808        id: &str,
3809        has_writer: bool,
3810        reconnection_pending: bool,
3811        renewal_pending: bool,
3812        close_initiated: bool,
3813    ) -> Arc<WebsocketConnection> {
3814        let conn = WebsocketConnection::new(id);
3815        let mut st = conn.state.lock().await;
3816        st.reconnection_pending = reconnection_pending;
3817        st.renewal_pending = renewal_pending;
3818        st.close_initiated = close_initiated;
3819        if has_writer {
3820            let (tx, _) = unbounded_channel::<Message>();
3821            st.ws_write_tx = Some(tx);
3822        } else {
3823            st.ws_write_tx = None;
3824        }
3825        drop(st);
3826        conn
3827    }
3828
3829    fn create_websocket_api(
3830        time_unit: Option<TimeUnit>,
3831        mode: Option<WebsocketMode>,
3832        auto_session_relogon: Option<bool>,
3833    ) -> Arc<WebsocketApi> {
3834        let mode = mode.unwrap_or(WebsocketMode::Single);
3835        let auto_session_relogon = auto_session_relogon.unwrap_or(true);
3836        let sig_gen = SignatureGenerator::new(
3837            Some("api_secret".into()),
3838            None::<PrivateKey>,
3839            None::<String>,
3840        );
3841        let config = ConfigurationWebsocketApi {
3842            api_key: Some("api_key".into()),
3843            api_secret: Some("api_secret".into()),
3844            private_key: None,
3845            private_key_passphrase: None,
3846            ws_url: Some("wss://example.com".into()),
3847            mode,
3848            reconnect_delay: 1000,
3849            signature_gen: sig_gen,
3850            timeout: 500,
3851            time_unit,
3852            auto_session_relogon,
3853            agent: None,
3854            handshake: None,
3855            user_agent: build_user_agent("product"),
3856            timestamp_provider: None,
3857        };
3858        let conn1 = WebsocketConnection::new("c1");
3859        let conn2 = WebsocketConnection::new("c2");
3860        WebsocketApi::new(config, vec![conn1, conn2])
3861    }
3862
3863    fn create_websocket_streams(
3864        ws_url: Option<&str>,
3865        conns: Option<Vec<Arc<WebsocketConnection>>>,
3866        url_paths: Option<Vec<String>>,
3867    ) -> Arc<WebsocketStreams> {
3868        let mut connections: Vec<Arc<WebsocketConnection>> = vec![];
3869        let url_paths = url_paths.unwrap_or_default();
3870        if conns.is_none() {
3871            connections.push(WebsocketConnection::new("c1"));
3872            connections.push(WebsocketConnection::new("c2"));
3873        } else {
3874            connections = conns.expect("Expected connections to be set");
3875        }
3876        let config = ConfigurationWebsocketStreams {
3877            api_key: None,
3878            api_secret: None,
3879            private_key: None,
3880            private_key_passphrase: None,
3881            ws_url: Some(ws_url.unwrap_or("example.com").to_string()),
3882            mode: WebsocketMode::Single,
3883            reconnect_delay: 500,
3884            time_unit: None,
3885            agent: None,
3886            handshake: None,
3887            user_agent: build_user_agent("product"),
3888            timestamp_provider: None,
3889        };
3890        WebsocketStreams::new(config, connections, url_paths)
3891    }
3892
3893    fn subscribe_to_emitter(emitter: &WebsocketEventEmitter) -> Receiver<WebsocketEvent> {
3894        let (test_tx, test_rx) = tokio::sync::mpsc::channel(16);
3895        let _sub = emitter.subscribe(move |evt| {
3896            let _ = test_tx.try_send(evt);
3897        });
3898        test_rx
3899    }
3900
3901    async fn expect_websocket_event(rx: &mut Receiver<WebsocketEvent>) -> WebsocketEvent {
3902        timeout(Duration::from_millis(200), rx.recv())
3903            .await
3904            .expect("timed out waiting for event")
3905            .expect("subscriber channel closed")
3906    }
3907
3908    async fn eventually_async<F, Fut>(max_wait: Duration, mut f: F) -> bool
3909    where
3910        F: FnMut() -> Fut,
3911        Fut: std::future::Future<Output = bool>,
3912    {
3913        let start = tokio::time::Instant::now();
3914        while start.elapsed() < max_wait {
3915            if f().await {
3916                return true;
3917            }
3918            sleep(Duration::from_millis(20)).await;
3919        }
3920        false
3921    }
3922
3923    mod event_emitter {
3924        use super::*;
3925
3926        #[test]
3927        fn event_emitter_subscribe_and_emit() {
3928            TOKIO_SHARED_RT.block_on(async {
3929                let emitter = WebsocketEventEmitter::new();
3930                let (tx, rx) = oneshot::channel();
3931                let tx = Arc::new(std::sync::Mutex::new(Some(tx)));
3932                let tx_clone = tx.clone();
3933                let _sub = emitter.subscribe(move |event| {
3934                    if let Some(sender) = tx_clone.lock().unwrap().take() {
3935                        let _ = sender.send(event);
3936                    }
3937                });
3938                emitter.emit(&WebsocketEvent::Open);
3939                let received = timeout(Duration::from_millis(100), rx)
3940                    .await
3941                    .expect("timed out");
3942                assert_eq!(received, Ok(WebsocketEvent::Open));
3943            });
3944        }
3945
3946        #[test]
3947        fn single_subscriber_gets_event() {
3948            TOKIO_SHARED_RT.block_on(async {
3949                let emitter = WebsocketEventEmitter::new();
3950                let mut rx = subscribe_to_emitter(&emitter);
3951
3952                let e1 = WebsocketEvent::Open;
3953                emitter.emit(&e1);
3954
3955                let got = expect_websocket_event(&mut rx).await;
3956                assert_eq!(got, e1);
3957            });
3958        }
3959
3960        #[test]
3961        fn multiple_subscribers_get_event() {
3962            TOKIO_SHARED_RT.block_on(async {
3963                let emitter = WebsocketEventEmitter::new();
3964                let mut rx1 = subscribe_to_emitter(&emitter);
3965                let mut rx2 = subscribe_to_emitter(&emitter);
3966
3967                let e = WebsocketEvent::Message("hello".into());
3968                emitter.emit(&e);
3969
3970                assert_eq!(expect_websocket_event(&mut rx1).await, e.clone());
3971                assert_eq!(expect_websocket_event(&mut rx2).await, e);
3972            });
3973        }
3974
3975        #[test]
3976        fn closed_subscribers_are_pruned() {
3977            TOKIO_SHARED_RT.block_on(async {
3978                let emitter = WebsocketEventEmitter::new();
3979                let rx1 = subscribe_to_emitter(&emitter);
3980                let mut rx2 = subscribe_to_emitter(&emitter);
3981                drop(rx1);
3982
3983                let e = WebsocketEvent::Pong;
3984                emitter.emit(&e);
3985
3986                assert_eq!(expect_websocket_event(&mut rx2).await, e);
3987            });
3988        }
3989
3990        #[test]
3991        fn prune_on_error_does_not_hang() {
3992            TOKIO_SHARED_RT.block_on(async {
3993                let emitter = WebsocketEventEmitter::new();
3994                let rx = subscribe_to_emitter(&emitter);
3995                drop(rx);
3996
3997                let e = WebsocketEvent::Close(1000, "bye".into());
3998                emitter.emit(&e);
3999            });
4000        }
4001    }
4002
4003    mod websocket_common {
4004        use super::*;
4005
4006        mod initialisation {
4007            use super::*;
4008
4009            #[test]
4010            fn single_mode() {
4011                TOKIO_SHARED_RT.block_on(async {
4012                    let common = WebsocketCommon::new(vec![], WebsocketMode::Single, 0, None, None);
4013                    assert_eq!(common.connection_pool.len(), 1);
4014                });
4015            }
4016
4017            #[test]
4018            fn pool_mode() {
4019                TOKIO_SHARED_RT.block_on(async {
4020                    let common =
4021                        WebsocketCommon::new(vec![], WebsocketMode::Pool(3), 0, None, None);
4022                    assert_eq!(common.connection_pool.len(), 3);
4023                });
4024            }
4025        }
4026
4027        mod spawn_reconnect_loop {
4028            use super::*;
4029
4030            #[test]
4031            fn reconnects_of_the_same_connection_are_serialized() {
4032                TOKIO_SHARED_RT.block_on(async {
4033                    let active = Arc::new(AtomicUsize::new(0));
4034                    let max_active = Arc::new(AtomicUsize::new(0));
4035                    let calls = Arc::new(AtomicUsize::new(0));
4036                    let (a, m, c) = (active.clone(), max_active.clone(), calls.clone());
4037                    let handshake: WebsocketHandshakeFn = Arc::new(move |_, _, _, _| {
4038                        let (a, m, c) = (a.clone(), m.clone(), c.clone());
4039                        Box::pin(async move {
4040                            let now = a.fetch_add(1, Ordering::SeqCst) + 1;
4041                            m.fetch_max(now, Ordering::SeqCst);
4042                            c.fetch_add(1, Ordering::SeqCst);
4043                            sleep(Duration::from_millis(100)).await;
4044                            a.fetch_sub(1, Ordering::SeqCst);
4045                            Err(tungstenite::Error::ConnectionClosed)
4046                        })
4047                    });
4048
4049                    let conn = WebsocketConnection::new("same");
4050                    let common = WebsocketCommon::new_with_handshake(
4051                        vec![conn.clone()],
4052                        WebsocketMode::Single,
4053                        0,
4054                        None,
4055                        None,
4056                        Some(handshake),
4057                    );
4058                    // Renewal entries for an idle connection are never retried, so
4059                    // exactly two handshakes are expected.
4060                    for _ in 0..2 {
4061                        common
4062                            .reconnect_tx
4063                            .send(ReconnectEntry {
4064                                connection_id: "same".into(),
4065                                url: "ws://pool.test/same".into(),
4066                                is_renewal: true,
4067                                attempt: 0,
4068                                expected_writer: None,
4069                            })
4070                            .await
4071                            .unwrap();
4072                    }
4073
4074                    let done = timeout(Duration::from_secs(5), async {
4075                        while calls.load(Ordering::SeqCst) < 2 {
4076                            sleep(Duration::from_millis(20)).await;
4077                        }
4078                    })
4079                    .await;
4080                    assert!(done.is_ok(), "both entries should be handled");
4081                    assert_eq!(
4082                        max_active.load(Ordering::SeqCst),
4083                        1,
4084                        "handshakes of the same connection overlapped"
4085                    );
4086                });
4087            }
4088
4089            #[test]
4090            fn pool_reconnects_start_independent_handshakes_before_prior_handshake_completes() {
4091                TOKIO_SHARED_RT.block_on(async {
4092                    let (a_started_tx, mut a_started_rx) = unbounded_channel();
4093                    let (b_started_tx, mut b_started_rx) = unbounded_channel();
4094                    let (a_release_tx, a_release_rx) = oneshot::channel();
4095                    let a_release_rx = Arc::new(Mutex::new(Some(a_release_rx)));
4096
4097                    let handshake: WebsocketHandshakeFn = Arc::new(move |request, _, _, _| {
4098                        let path = request.uri().path().to_owned();
4099                        let a_started_tx = a_started_tx.clone();
4100                        let b_started_tx = b_started_tx.clone();
4101                        let a_release_rx = Arc::clone(&a_release_rx);
4102                        Box::pin(async move {
4103                            match path.as_str() {
4104                                "/a" => {
4105                                    a_started_tx.send(()).unwrap();
4106                                    a_release_rx
4107                                        .lock()
4108                                        .await
4109                                        .take()
4110                                        .expect("A handshake must run once")
4111                                        .await
4112                                        .expect("test must release A handshake");
4113                                }
4114                                "/b" => {
4115                                    b_started_tx.send(()).unwrap();
4116                                }
4117                                unexpected => panic!("unexpected reconnect path: {unexpected}"),
4118                            }
4119                            Err(tungstenite::Error::ConnectionClosed)
4120                        })
4121                    });
4122
4123                    let common = WebsocketCommon::new_with_handshake(
4124                        vec![WebsocketConnection::new("a"), WebsocketConnection::new("b")],
4125                        WebsocketMode::Pool(2),
4126                        0,
4127                        None,
4128                        None,
4129                        Some(handshake),
4130                    );
4131                    common
4132                        .reconnect_tx
4133                        .send(ReconnectEntry {
4134                            connection_id: "a".into(),
4135                            url: "ws://pool.test/a".into(),
4136                            is_renewal: false,
4137                            attempt: 0,
4138                            expected_writer: None,
4139                        })
4140                        .await
4141                        .unwrap();
4142                    timeout(Duration::from_secs(1), a_started_rx.recv())
4143                        .await
4144                        .expect("A handshake did not start")
4145                        .expect("A handshake notification channel closed");
4146
4147                    common
4148                        .reconnect_tx
4149                        .send(ReconnectEntry {
4150                            connection_id: "b".into(),
4151                            url: "ws://pool.test/b".into(),
4152                            is_renewal: false,
4153                            attempt: 0,
4154                            expected_writer: None,
4155                        })
4156                        .await
4157                        .unwrap();
4158
4159                    let b_started_before_a_release =
4160                        timeout(Duration::from_millis(150), b_started_rx.recv())
4161                            .await
4162                            .is_ok();
4163
4164                    a_release_tx.send(()).unwrap();
4165                    drop(common);
4166
4167                    assert!(
4168                        b_started_before_a_release,
4169                        "pool connection B's handshake did not start while connection A was gated"
4170                    );
4171                });
4172            }
4173
4174            #[test]
4175            fn successful_reconnect_entry_triggers_init_connect() {
4176                TOKIO_SHARED_RT.block_on(async {
4177                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4178                    let addr = listener.local_addr().unwrap();
4179                    tokio::spawn(async move {
4180                        if let Ok((stream, _)) = listener.accept().await {
4181                            let mut ws = accept_async(stream).await.unwrap();
4182                            sleep(Duration::from_secs(5)).await;
4183                            let _ = ws.close(None).await;
4184                        }
4185                    });
4186
4187                    let conn = WebsocketConnection::new("c1");
4188                    let common = WebsocketCommon::new(
4189                        vec![conn.clone()],
4190                        WebsocketMode::Single,
4191                        10,
4192                        None,
4193                        None,
4194                    );
4195                    let url = format!("ws://{addr}");
4196                    common
4197                        .reconnect_tx
4198                        .send(ReconnectEntry {
4199                            connection_id: "c1".into(),
4200                            url: url.clone(),
4201                            is_renewal: false,
4202                            attempt: 0,
4203                            expected_writer: None,
4204                        })
4205                        .await
4206                        .unwrap();
4207
4208                    let mut ok = false;
4209                    for _ in 0..100 {
4210                        if conn.state.lock().await.ws_write_tx.is_some() {
4211                            ok = true;
4212                            break;
4213                        }
4214                        sleep(Duration::from_millis(50)).await;
4215                    }
4216                    assert!(ok, "expected ws_write_tx to be Some after reconnect");
4217                });
4218            }
4219
4220            #[test]
4221            fn reconnect_entry_with_unknown_id_is_ignored() {
4222                TOKIO_SHARED_RT.block_on(async {
4223                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4224                    let addr = listener.local_addr().unwrap();
4225                    tokio::spawn(async move {
4226                        if let Ok((stream, _)) = listener.accept().await {
4227                            let mut ws = accept_async(stream).await.unwrap();
4228                            let _ = ws.close(None).await;
4229                        }
4230                    });
4231
4232                    let conn = WebsocketConnection::new("c1");
4233                    let common = WebsocketCommon::new(
4234                        vec![conn.clone()],
4235                        WebsocketMode::Single,
4236                        5,
4237                        None,
4238                        None,
4239                    );
4240                    let url = format!("ws://{addr}");
4241                    common
4242                        .reconnect_tx
4243                        .send(ReconnectEntry {
4244                            connection_id: "other".into(),
4245                            url,
4246                            is_renewal: false,
4247                            attempt: 0,
4248                            expected_writer: None,
4249                        })
4250                        .await
4251                        .unwrap();
4252
4253                    sleep(Duration::from_secs(1)).await;
4254
4255                    let st = conn.state.lock().await;
4256                    assert!(st.ws_write_tx.is_none());
4257                });
4258            }
4259
4260            #[test]
4261            fn renewal_entries_bypass_initial_delay() {
4262                TOKIO_SHARED_RT.block_on(async {
4263                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4264                    let addr = listener.local_addr().unwrap();
4265                    tokio::spawn(async move {
4266                        if let Ok((stream, _)) = listener.accept().await {
4267                            let mut ws = accept_async(stream).await.unwrap();
4268                            let _ = ws.close(None).await;
4269                        }
4270                    });
4271
4272                    let conn = WebsocketConnection::new("renew");
4273                    let common = WebsocketCommon::new(
4274                        vec![conn.clone()],
4275                        WebsocketMode::Single,
4276                        200,
4277                        None,
4278                        None,
4279                    );
4280                    let url = format!("ws://{addr}");
4281                    common
4282                        .reconnect_tx
4283                        .send(ReconnectEntry {
4284                            connection_id: "renew".into(),
4285                            url: url.clone(),
4286                            is_renewal: true,
4287                            attempt: 0,
4288                            expected_writer: None,
4289                        })
4290                        .await
4291                        .unwrap();
4292
4293                    sleep(Duration::from_secs(2)).await;
4294
4295                    let st = conn.state.lock().await;
4296
4297                    assert!(st.ws_write_tx.is_some());
4298                });
4299            }
4300
4301            #[test]
4302            fn non_renewal_entries_respect_initial_delay() {
4303                TOKIO_SHARED_RT.block_on(async {
4304                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4305                    let addr = listener.local_addr().unwrap();
4306                    let _listener_guard = spawn_mock_ws_listener(listener);
4307
4308                    let reconnect_delay_ms = 1500;
4309                    let conn = WebsocketConnection::new("nonrenew");
4310                    let common = WebsocketCommon::new(
4311                        vec![conn.clone()],
4312                        WebsocketMode::Single,
4313                        reconnect_delay_ms,
4314                        None,
4315                        None,
4316                    );
4317                    let url = format!("ws://{addr}");
4318                    let queued_at = tokio::time::Instant::now();
4319                    common
4320                        .reconnect_tx
4321                        .send(ReconnectEntry {
4322                            connection_id: "nonrenew".into(),
4323                            url: url.clone(),
4324                            is_renewal: false,
4325                            attempt: 0,
4326                            expected_writer: None,
4327                        })
4328                        .await
4329                        .unwrap();
4330
4331                    sleep(Duration::from_millis(100)).await;
4332                    if queued_at.elapsed()
4333                        < Duration::from_millis(reconnect_delay_ms as u64)
4334                            .saturating_sub(Duration::from_millis(200))
4335                    {
4336                        assert!(conn.state.lock().await.ws_write_tx.is_none());
4337                    }
4338
4339                    let mut ok = false;
4340                    for _ in 0..200 {
4341                        if conn.state.lock().await.ws_write_tx.is_some() {
4342                            ok = true;
4343                            break;
4344                        }
4345                        sleep(Duration::from_millis(50)).await;
4346                    }
4347                    assert!(
4348                        ok,
4349                        "expected ws_write_tx to be Some after reconnect delay elapsed"
4350                    );
4351                });
4352            }
4353        }
4354
4355        mod spawn_renewal_loop {
4356            use super::*;
4357
4358            #[tokio::test]
4359            async fn scheduling_renewal_does_not_panic_for_known_connection() {
4360                pause();
4361
4362                let conn = WebsocketConnection::new("known");
4363                let common =
4364                    WebsocketCommon::new(vec![conn.clone()], WebsocketMode::Single, 0, None, None);
4365                let url = "wss://example".to_string();
4366                common
4367                    .renewal_tx
4368                    .send((conn.id.clone(), url))
4369                    .await
4370                    .unwrap();
4371                advance(Duration::from_secs(23 * 60 * 60 + 1)).await;
4372                resume();
4373            }
4374
4375            #[tokio::test]
4376            async fn scheduling_renewal_ignored_for_unknown_connection() {
4377                pause();
4378
4379                let conn = WebsocketConnection::new("c1");
4380                let common =
4381                    WebsocketCommon::new(vec![conn.clone()], WebsocketMode::Single, 0, None, None);
4382                common
4383                    .renewal_tx
4384                    .send(("other".into(), "u".into()))
4385                    .await
4386                    .unwrap();
4387                advance(Duration::from_secs(23 * 60 * 60 + 1)).await;
4388
4389                resume();
4390            }
4391        }
4392
4393        mod lifetime {
4394            use super::*;
4395            use std::sync::Weak;
4396
4397            /// Polls until `weak` can no longer be upgraded, or gives up after ~5s.
4398            async fn wait_freed<T: ?Sized>(weak: &Weak<T>) -> bool {
4399                for _ in 0..100 {
4400                    if weak.upgrade().is_none() {
4401                        return true;
4402                    }
4403                    sleep(Duration::from_millis(50)).await;
4404                }
4405                false
4406            }
4407
4408            #[tokio::test]
4409            async fn dropped_common_is_freed() {
4410                let conn = WebsocketConnection::new("c1");
4411                let common = WebsocketCommon::new(vec![conn], WebsocketMode::Single, 0, None, None);
4412                let weak = Arc::downgrade(&common);
4413                drop(common);
4414                assert!(
4415                    wait_freed(&weak).await,
4416                    "reconnect/renewal loops kept WebsocketCommon alive"
4417                );
4418            }
4419
4420            #[tokio::test]
4421            async fn dropped_common_with_pending_renewal_is_freed() {
4422                let conn = WebsocketConnection::new("c1");
4423                let common =
4424                    WebsocketCommon::new(vec![conn.clone()], WebsocketMode::Single, 0, None, None);
4425                common
4426                    .renewal_tx
4427                    .send((conn.id.clone(), "wss://example".into()))
4428                    .await
4429                    .unwrap();
4430                let weak = Arc::downgrade(&common);
4431                drop(common);
4432                assert!(
4433                    wait_freed(&weak).await,
4434                    "renewal loop with a queued renewal kept WebsocketCommon alive"
4435                );
4436            }
4437
4438            #[tokio::test]
4439            async fn disconnected_and_dropped_api_is_freed() {
4440                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4441                let addr = listener.local_addr().unwrap();
4442                let _listener_guard = spawn_mock_ws_listener(listener);
4443
4444                let mut config = ConfigurationWebsocketApi::builder()
4445                    .api_key("key")
4446                    .api_secret("secret")
4447                    .ws_url(format!("ws://{addr}"))
4448                    .build()
4449                    .unwrap();
4450                config.user_agent = build_user_agent("product");
4451                let api = WebsocketApi::new(config, vec![]);
4452                api.clone().connect().await.unwrap();
4453
4454                let probe = Arc::new(());
4455                let probe_weak = Arc::downgrade(&probe);
4456                let _subscription = api.common.events.subscribe(move |_| {
4457                    let _ = &probe;
4458                });
4459
4460                let api_weak = Arc::downgrade(&api);
4461                let common_weak = Arc::downgrade(&api.common);
4462                api.disconnect().await.unwrap();
4463                drop(api);
4464
4465                assert!(wait_freed(&api_weak).await, "WebsocketApi was not freed");
4466                assert!(
4467                    wait_freed(&common_weak).await,
4468                    "WebsocketCommon was not freed"
4469                );
4470                assert!(
4471                    wait_freed(&probe_weak).await,
4472                    "event subscriber callback was not freed"
4473                );
4474            }
4475
4476            /// Mock server that reports when the client's socket is closed.
4477            async fn closing_server() -> (String, oneshot::Receiver<()>) {
4478                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4479                let url = format!("ws://{}", listener.local_addr().unwrap());
4480                let (closed_tx, closed_rx) = oneshot::channel();
4481                tokio::spawn(async move {
4482                    if let Ok((stream, _)) = listener.accept().await {
4483                        if let Ok(mut ws) = accept_async(stream).await {
4484                            while let Some(Ok(_)) = ws.next().await {}
4485                        }
4486                        let _ = closed_tx.send(());
4487                    }
4488                });
4489                (url, closed_rx)
4490            }
4491
4492            #[tokio::test]
4493            async fn dropped_api_without_disconnect_closes_socket_and_is_freed() {
4494                let (url, closed_rx) = closing_server().await;
4495                let mut config = ConfigurationWebsocketApi::builder()
4496                    .api_key("key")
4497                    .api_secret("secret")
4498                    .ws_url(url)
4499                    .build()
4500                    .unwrap();
4501                config.user_agent = build_user_agent("product");
4502                let api = WebsocketApi::new(config, vec![]);
4503                api.clone().connect().await.unwrap();
4504                assert!(api.is_connected().await);
4505
4506                let common_weak = Arc::downgrade(&api.common);
4507                drop(api);
4508
4509                assert!(
4510                    timeout(Duration::from_secs(5), closed_rx).await.is_ok(),
4511                    "socket was not closed when the client was dropped"
4512                );
4513                assert!(
4514                    wait_freed(&common_weak).await,
4515                    "WebsocketCommon was not freed"
4516                );
4517            }
4518
4519            #[tokio::test]
4520            async fn dropped_streams_without_disconnect_closes_socket_and_is_freed() {
4521                let (url, closed_rx) = closing_server().await;
4522                let ws = create_websocket_streams(
4523                    Some(&url),
4524                    Some(vec![WebsocketConnection::new("c1")]),
4525                    None,
4526                );
4527                ws.clone().connect(vec![]).await.unwrap();
4528
4529                let common_weak = Arc::downgrade(&ws.common);
4530                drop(ws);
4531
4532                assert!(
4533                    timeout(Duration::from_secs(5), closed_rx).await.is_ok(),
4534                    "socket was not closed when the client was dropped"
4535                );
4536                assert!(
4537                    wait_freed(&common_weak).await,
4538                    "WebsocketCommon was not freed"
4539                );
4540            }
4541
4542            #[tokio::test]
4543            async fn drop_does_not_reconnect() {
4544                let (url, closed_rx) = closing_server().await;
4545                let conn = WebsocketConnection::new("c1");
4546                let ws = create_websocket_streams(Some(&url), Some(vec![conn.clone()]), None);
4547                ws.clone().connect(vec![]).await.unwrap();
4548                drop(ws);
4549
4550                timeout(Duration::from_secs(5), closed_rx)
4551                    .await
4552                    .unwrap()
4553                    .unwrap();
4554                let st = conn.state.lock().await;
4555                assert!(st.close_initiated, "drop must mark the connection closed");
4556                assert!(
4557                    !st.reconnection_pending,
4558                    "drop must not schedule a reconnect"
4559                );
4560                assert!(st.ws_write_tx.is_none());
4561            }
4562
4563            #[tokio::test]
4564            async fn disconnected_and_dropped_streams_is_freed() {
4565                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4566                let addr = listener.local_addr().unwrap();
4567                let _listener_guard = spawn_mock_ws_listener(listener);
4568
4569                let ws = create_websocket_streams(
4570                    Some(&format!("ws://{addr}")),
4571                    Some(vec![WebsocketConnection::new("c1")]),
4572                    None,
4573                );
4574                ws.clone().connect(vec![]).await.unwrap();
4575
4576                let ws_weak = Arc::downgrade(&ws);
4577                let common_weak = Arc::downgrade(&ws.common);
4578                ws.disconnect().await.unwrap();
4579                drop(ws);
4580
4581                assert!(wait_freed(&ws_weak).await, "WebsocketStreams was not freed");
4582                assert!(
4583                    wait_freed(&common_weak).await,
4584                    "WebsocketCommon was not freed"
4585                );
4586            }
4587        }
4588
4589        mod reconnect_regressions {
4590            use super::*;
4591
4592            #[test]
4593            fn init_connect_is_not_skipped_when_reconnection_pending() {
4594                TOKIO_SHARED_RT.block_on(async {
4595                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4596                    let addr = listener.local_addr().unwrap();
4597
4598                    tokio::spawn(async move {
4599                        if let Ok((stream, _)) = listener.accept().await {
4600                            tokio::spawn(async move {
4601                                let mut ws = accept_async(stream).await.unwrap();
4602                                sleep(Duration::from_millis(500)).await;
4603                                let _ = ws.close(None).await;
4604                            });
4605                        }
4606                    });
4607
4608                    let conn = WebsocketConnection::new("c-reconnect");
4609                    {
4610                        let mut st = conn.state.lock().await;
4611                        let (tx, _) = unbounded_channel::<Message>();
4612                        st.ws_write_tx = Some(tx);
4613                        st.reconnection_pending = true;
4614                    }
4615
4616                    let common = WebsocketCommon::new(
4617                        vec![conn.clone()],
4618                        WebsocketMode::Single,
4619                        0,
4620                        None,
4621                        None,
4622                    );
4623
4624                    let url = format!("ws://{addr}");
4625                    common
4626                        .clone()
4627                        .init_connect(&url, false, Some(conn.clone()))
4628                        .await
4629                        .unwrap();
4630
4631                    let ok = eventually_async(Duration::from_secs(2), || {
4632                        let conn = conn.clone();
4633                        async move {
4634                            let st = conn.state.lock().await;
4635                            st.ws_write_tx.is_some() && !st.reconnection_pending
4636                        }
4637                    })
4638                    .await;
4639
4640                    assert!(
4641                        ok,
4642                        "expected writer installed and reconnection_pending cleared"
4643                    );
4644                });
4645            }
4646
4647            #[test]
4648            fn pending_request_is_resolved_on_socket_drop() {
4649                TOKIO_SHARED_RT.block_on(async {
4650                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4651                    let addr = listener.local_addr().unwrap();
4652
4653                    tokio::spawn(async move {
4654                        if let Ok((stream, _)) = listener.accept().await {
4655                            let mut ws = accept_async(stream).await.unwrap();
4656                            let _ = ws.next().await;
4657                            let _ = ws.close(None).await;
4658                        }
4659                    });
4660
4661                    let conn = WebsocketConnection::new("c1");
4662                    let common = WebsocketCommon::new(
4663                        vec![conn.clone()],
4664                        WebsocketMode::Single,
4665                        0,
4666                        None,
4667                        None,
4668                    );
4669
4670                    let url = format!("ws://{addr}");
4671                    common
4672                        .clone()
4673                        .init_connect(&url, false, Some(conn.clone()))
4674                        .await
4675                        .unwrap();
4676
4677                    let rx = common
4678                        .send(
4679                            "{\"id\":\"req-1\",\"method\":\"PING\"}".to_string(),
4680                            Some("req-1".to_string()),
4681                            true,
4682                            Duration::from_millis(150),
4683                            Some(conn.clone()),
4684                        )
4685                        .await
4686                        .unwrap()
4687                        .expect("expected oneshot receiver");
4688
4689                    let res = timeout(Duration::from_secs(2), rx)
4690                        .await
4691                        .expect("did not resolve pending request")
4692                        .expect("oneshot cancelled");
4693
4694                    assert!(matches!(res, Err(WebsocketError::Timeout)));
4695
4696                    let ok = eventually_async(Duration::from_secs(1), || {
4697                        let conn = conn.clone();
4698                        async move { conn.state.lock().await.pending_requests.is_empty() }
4699                    })
4700                    .await;
4701
4702                    assert!(ok, "pending_requests should be drained");
4703                });
4704            }
4705        }
4706
4707        mod is_connection_ready {
4708            use super::*;
4709
4710            #[test]
4711            fn is_connection_ready() {
4712                TOKIO_SHARED_RT.block_on(async {
4713                    let conn = WebsocketConnection::new("c1");
4714                    let common = WebsocketCommon::new(
4715                        vec![conn.clone()],
4716                        WebsocketMode::Single,
4717                        0,
4718                        None,
4719                        None,
4720                    );
4721                    assert!(!common.is_connection_ready(&conn, false).await);
4722                    assert!(common.is_connection_ready(&conn, true).await);
4723                });
4724            }
4725
4726            #[test]
4727            fn connection_ready_basic() {
4728                TOKIO_SHARED_RT.block_on(async {
4729                    let conn = create_connection("c1", true, false, false, false).await;
4730                    let common = WebsocketCommon::new(
4731                        vec![conn.clone()],
4732                        WebsocketMode::Single,
4733                        0,
4734                        None,
4735                        None,
4736                    );
4737                    assert!(common.is_connection_ready(&conn, false).await);
4738                });
4739            }
4740
4741            #[test]
4742            fn connection_not_ready_without_writer() {
4743                TOKIO_SHARED_RT.block_on(async {
4744                    let conn = create_connection("c1", false, false, false, false).await;
4745                    let common = WebsocketCommon::new(
4746                        vec![conn.clone()],
4747                        WebsocketMode::Single,
4748                        0,
4749                        None,
4750                        None,
4751                    );
4752                    assert!(!common.is_connection_ready(&conn, false).await);
4753                    assert!(common.is_connection_ready(&conn, true).await);
4754                });
4755            }
4756
4757            #[test]
4758            fn connection_not_ready_when_flagged() {
4759                TOKIO_SHARED_RT.block_on(async {
4760                    let conn1 = create_connection("c1", true, true, false, false).await;
4761                    let conn2 = create_connection("c2", true, false, true, false).await;
4762                    let conn3 = create_connection("c3", true, false, false, true).await;
4763
4764                    let common = WebsocketCommon::new(
4765                        vec![conn1.clone(), conn2.clone(), conn3.clone()],
4766                        WebsocketMode::Pool(3),
4767                        0,
4768                        None,
4769                        None,
4770                    );
4771
4772                    assert!(!common.is_connection_ready(&conn1, false).await);
4773                    assert!(common.is_connection_ready(&conn2, false).await);
4774                    assert!(!common.is_connection_ready(&conn3, false).await);
4775                });
4776            }
4777        }
4778
4779        mod is_connected {
4780            use super::*;
4781
4782            #[test]
4783            fn with_pool_various_connections() {
4784                TOKIO_SHARED_RT.block_on(async {
4785                    let conn_a = create_connection("a", true, false, false, false).await;
4786                    let conn_b = create_connection("b", false, false, false, false).await;
4787                    let conn_c = create_connection("c", true, true, false, false).await;
4788                    let pool = vec![conn_a.clone(), conn_b.clone(), conn_c.clone()];
4789                    let common = WebsocketCommon::new(pool, WebsocketMode::Pool(3), 0, None, None);
4790
4791                    assert!(common.is_connected(None).await);
4792                    assert!(common.is_connected(Some(&conn_a)).await);
4793                    assert!(!common.is_connected(Some(&conn_b)).await);
4794                    assert!(!common.is_connected(Some(&conn_c)).await);
4795                });
4796            }
4797
4798            #[test]
4799            fn with_pool_all_bad_connections() {
4800                TOKIO_SHARED_RT.block_on(async {
4801                    let bad1 = create_connection("c1", false, false, false, false).await;
4802                    let bad2 = create_connection("c2", true, true, false, false).await;
4803                    let bad3 = create_connection("c3", true, false, false, true).await;
4804                    let common = WebsocketCommon::new(
4805                        vec![bad1, bad2, bad3],
4806                        WebsocketMode::Pool(3),
4807                        0,
4808                        None,
4809                        None,
4810                    );
4811
4812                    assert!(!common.is_connected(None).await);
4813                });
4814            }
4815
4816            #[test]
4817            fn with_pool_ignore_close_initiated() {
4818                TOKIO_SHARED_RT.block_on(async {
4819                    let good = create_connection("c1", true, false, false, false).await;
4820                    let closed = create_connection("c2", true, false, false, true).await;
4821                    let bad = create_connection("c3", false, false, false, false).await;
4822                    let common = WebsocketCommon::new(
4823                        vec![closed.clone(), good.clone(), bad.clone()],
4824                        WebsocketMode::Pool(3),
4825                        0,
4826                        None,
4827                        None,
4828                    );
4829
4830                    assert!(common.is_connected(None).await);
4831                    assert!(!common.is_connected(Some(&closed)).await);
4832                });
4833            }
4834        }
4835
4836        mod get_available_connections {
4837            use super::*;
4838
4839            #[test]
4840            fn single_mode() {
4841                TOKIO_SHARED_RT.block_on(async {
4842                    let common = WebsocketCommon::new(vec![], WebsocketMode::Single, 0, None, None);
4843                    let connections = common.get_available_connections(false, None).await;
4844                    assert_eq!(connections[0].id, common.connection_pool[0].id);
4845                });
4846            }
4847
4848            #[test]
4849            fn single_mode_with_url_path_does_not_force_first_connection() {
4850                TOKIO_SHARED_RT.block_on(async {
4851                    let conn1 = WebsocketConnection::new("c1");
4852                    let conn2 = WebsocketConnection::new("c2");
4853
4854                    let (tx2, _rx2) = unbounded_channel();
4855                    {
4856                        let mut s2 = conn2.state.lock().await;
4857                        s2.ws_write_tx = Some(tx2);
4858                        s2.url_path = Some("path1".to_string());
4859                    }
4860
4861                    {
4862                        let mut s1 = conn1.state.lock().await;
4863                        s1.url_path = Some("path1".to_string());
4864                    }
4865
4866                    let pool = vec![conn1.clone(), conn2.clone()];
4867                    let common = WebsocketCommon::new(pool, WebsocketMode::Single, 0, None, None);
4868
4869                    let connections = common.get_available_connections(false, Some("path1")).await;
4870
4871                    assert_eq!(connections.len(), 1);
4872                    assert_eq!(connections[0].id, "c2");
4873                });
4874            }
4875
4876            #[test]
4877            fn pool_mode_not_ready() {
4878                TOKIO_SHARED_RT.block_on(async {
4879                    let common =
4880                        WebsocketCommon::new(vec![], WebsocketMode::Pool(2), 0, None, None);
4881                    let connections = common.get_available_connections(false, None).await;
4882                    assert!(connections.is_empty());
4883                });
4884            }
4885
4886            #[test]
4887            fn pool_mode_with_ready() {
4888                TOKIO_SHARED_RT.block_on(async {
4889                    let conn1 = WebsocketConnection::new("c1");
4890                    let conn2 = WebsocketConnection::new("c2");
4891                    let (tx1, _rx1) = unbounded_channel();
4892                    {
4893                        let mut s1 = conn1.state.lock().await;
4894                        s1.ws_write_tx = Some(tx1);
4895                    }
4896                    let pool = vec![conn1.clone(), conn2.clone()];
4897                    let common = WebsocketCommon::new(pool, WebsocketMode::Pool(2), 0, None, None);
4898                    let connections = common.get_available_connections(false, None).await;
4899                    assert!(connections.len() == 1);
4900                });
4901            }
4902        }
4903
4904        mod get_connection {
4905            use super::*;
4906
4907            #[test]
4908            fn single_mode() {
4909                TOKIO_SHARED_RT.block_on(async {
4910                    let common = WebsocketCommon::new(vec![], WebsocketMode::Single, 0, None, None);
4911                    let conn = common
4912                        .get_connection(false, None)
4913                        .await
4914                        .expect("should get connection");
4915                    assert_eq!(conn.id, common.connection_pool[0].id);
4916                });
4917            }
4918
4919            #[test]
4920            fn single_mode_with_url_path_selects_matching_ready_connection() {
4921                TOKIO_SHARED_RT.block_on(async {
4922                    let conn1 = WebsocketConnection::new("c1");
4923                    let conn2 = WebsocketConnection::new("c2");
4924
4925                    let (tx1, _rx1) = unbounded_channel();
4926                    {
4927                        let mut s1 = conn1.state.lock().await;
4928                        s1.ws_write_tx = Some(tx1);
4929                        s1.url_path = Some("path2".to_string());
4930                    }
4931
4932                    let (tx2, _rx2) = unbounded_channel();
4933                    {
4934                        let mut s2 = conn2.state.lock().await;
4935                        s2.ws_write_tx = Some(tx2);
4936                        s2.url_path = Some("path1".to_string());
4937                    }
4938
4939                    let pool = vec![conn1.clone(), conn2.clone()];
4940                    let common = WebsocketCommon::new(pool, WebsocketMode::Single, 0, None, None);
4941
4942                    let chosen = common
4943                        .get_connection(false, Some("path1"))
4944                        .await
4945                        .expect("should get connection");
4946
4947                    assert_eq!(chosen.id, "c2");
4948                });
4949            }
4950
4951            #[test]
4952            fn pool_mode_not_ready() {
4953                TOKIO_SHARED_RT.block_on(async {
4954                    let common =
4955                        WebsocketCommon::new(vec![], WebsocketMode::Pool(2), 0, None, None);
4956                    let result = common.get_connection(false, None).await;
4957                    assert!(matches!(
4958                        result,
4959                        Err(crate::errors::WebsocketError::NotConnected)
4960                    ));
4961                });
4962            }
4963
4964            #[test]
4965            fn pool_mode_with_ready() {
4966                TOKIO_SHARED_RT.block_on(async {
4967                    let conn1 = WebsocketConnection::new("c1");
4968                    let conn2 = WebsocketConnection::new("c2");
4969                    let (tx1, _rx1) = unbounded_channel();
4970                    {
4971                        let mut s1 = conn1.state.lock().await;
4972                        s1.ws_write_tx = Some(tx1);
4973                    }
4974                    let pool = vec![conn1.clone(), conn2.clone()];
4975                    let common = WebsocketCommon::new(pool, WebsocketMode::Pool(2), 0, None, None);
4976                    let result = common.get_connection(false, None).await;
4977                    assert!(result.is_ok());
4978                    let chosen = result.unwrap();
4979                    assert_eq!(chosen.id, conn1.id);
4980                });
4981            }
4982
4983            #[test]
4984            fn pool_mode_with_url_path_filters_connections() {
4985                TOKIO_SHARED_RT.block_on(async {
4986                    let conn1 = WebsocketConnection::new("c1");
4987                    let conn2 = WebsocketConnection::new("c2");
4988
4989                    let (tx1, _rx1) = unbounded_channel();
4990                    {
4991                        let mut s1 = conn1.state.lock().await;
4992                        s1.ws_write_tx = Some(tx1);
4993                        s1.url_path = Some("path1".to_string());
4994                    }
4995
4996                    let (tx2, _rx2) = unbounded_channel();
4997                    {
4998                        let mut s2 = conn2.state.lock().await;
4999                        s2.ws_write_tx = Some(tx2);
5000                        s2.url_path = Some("path2".to_string());
5001                    }
5002
5003                    let pool = vec![conn1.clone(), conn2.clone()];
5004                    let common = WebsocketCommon::new(pool, WebsocketMode::Pool(2), 0, None, None);
5005
5006                    let chosen = common
5007                        .get_connection(false, Some("path2"))
5008                        .await
5009                        .expect("should pick ready connection for path2");
5010
5011                    assert_eq!(chosen.id, "c2");
5012                });
5013            }
5014
5015            #[test]
5016            fn url_path_no_match_returns_not_connected() {
5017                TOKIO_SHARED_RT.block_on(async {
5018                    let conn1 = WebsocketConnection::new("c1");
5019                    let (tx1, _rx1) = unbounded_channel();
5020                    {
5021                        let mut s1 = conn1.state.lock().await;
5022                        s1.ws_write_tx = Some(tx1);
5023                        s1.url_path = Some("path1".to_string());
5024                    }
5025
5026                    let pool = vec![conn1.clone()];
5027                    let common = WebsocketCommon::new(pool, WebsocketMode::Pool(1), 0, None, None);
5028
5029                    let result = common.get_connection(false, Some("path2")).await;
5030                    assert!(matches!(
5031                        result,
5032                        Err(crate::errors::WebsocketError::NotConnected)
5033                    ));
5034                });
5035            }
5036        }
5037
5038        mod close_connection_gracefully {
5039            use super::*;
5040
5041            #[tokio::test]
5042            async fn waits_for_pending_requests_then_closes() {
5043                pause();
5044
5045                let conn = WebsocketConnection::new("c1");
5046                let (tx, mut rx) = unbounded_channel::<Message>();
5047                let (req_tx, _req_rx) = oneshot::channel();
5048                {
5049                    let mut st = conn.state.lock().await;
5050                    st.pending_requests
5051                        .insert("r".to_string(), PendingRequest { completion: req_tx });
5052                }
5053                let common =
5054                    WebsocketCommon::new(vec![conn.clone()], WebsocketMode::Single, 0, None, None);
5055                let close_fut = common.close_connection_gracefully(tx.clone(), conn.clone());
5056                advance(Duration::from_secs(1)).await;
5057                {
5058                    let mut st = conn.state.lock().await;
5059                    st.pending_requests.clear();
5060                }
5061                conn.drain_notify.notify_waiters();
5062                advance(Duration::from_secs(1)).await;
5063                close_fut.await.unwrap();
5064                match rx.try_recv() {
5065                    Ok(Message::Close(_)) => {}
5066                    other => panic!("expected Close, got {other:?}"),
5067                }
5068
5069                resume();
5070            }
5071
5072            #[tokio::test]
5073            async fn force_closes_after_timeout() {
5074                pause();
5075
5076                let conn = WebsocketConnection::new("c2");
5077                let (tx, mut rx) = unbounded_channel::<Message>();
5078                let (req_tx, _req_rx) = oneshot::channel();
5079                {
5080                    let mut st = conn.state.lock().await;
5081                    st.pending_requests.insert(
5082                        "request_id".to_string(),
5083                        PendingRequest { completion: req_tx },
5084                    );
5085                }
5086                let common =
5087                    WebsocketCommon::new(vec![conn.clone()], WebsocketMode::Single, 0, None, None);
5088                let close_fut = common.close_connection_gracefully(tx.clone(), conn.clone());
5089                advance(Duration::from_secs(30)).await;
5090                close_fut.await.unwrap();
5091                match rx.try_recv() {
5092                    Ok(Message::Close(_)) => {}
5093                    other => panic!("expected Close on timeout, got {other:?}"),
5094                }
5095
5096                resume();
5097            }
5098        }
5099
5100        mod get_reconnect_url {
5101            use super::*;
5102
5103            struct DummyHandler {
5104                url: String,
5105            }
5106
5107            #[async_trait::async_trait]
5108            impl WebsocketHandler for DummyHandler {
5109                async fn on_open(&self, _url: String, _connection: Arc<WebsocketConnection>) {}
5110                async fn on_message(&self, _data: String, _connection: Arc<WebsocketConnection>) {}
5111                async fn get_reconnect_url(
5112                    &self,
5113                    _default_url: String,
5114                    _connection: Arc<WebsocketConnection>,
5115                ) -> String {
5116                    self.url.clone()
5117                }
5118            }
5119
5120            #[test]
5121            fn returns_default_when_no_handler() {
5122                TOKIO_SHARED_RT.block_on(async {
5123                    let conn = WebsocketConnection::new("c1");
5124                    let common = WebsocketCommon::new(
5125                        vec![conn.clone()],
5126                        WebsocketMode::Single,
5127                        0,
5128                        None,
5129                        None,
5130                    );
5131                    let default = "wss://default".to_string();
5132                    let result = common.get_reconnect_url(&default, conn.clone()).await;
5133                    assert_eq!(result, default);
5134                });
5135            }
5136
5137            #[test]
5138            fn returns_handler_url_when_set() {
5139                TOKIO_SHARED_RT.block_on(async {
5140                    let conn = WebsocketConnection::new("c2");
5141                    let handler = Arc::new(DummyHandler {
5142                        url: "wss://custom".into(),
5143                    });
5144                    conn.set_handler(handler).await;
5145                    let common = WebsocketCommon::new(
5146                        vec![conn.clone()],
5147                        WebsocketMode::Single,
5148                        0,
5149                        None,
5150                        None,
5151                    );
5152                    let default = "wss://default".to_string();
5153                    let result = common.get_reconnect_url(&default, conn.clone()).await;
5154                    assert_eq!(result, "wss://custom");
5155                });
5156            }
5157        }
5158
5159        mod on_open {
5160            use super::*;
5161
5162            struct DummyHandler {
5163                called: Arc<Mutex<bool>>,
5164                opened_url: Arc<Mutex<Option<String>>>,
5165            }
5166
5167            #[async_trait]
5168            impl WebsocketHandler for DummyHandler {
5169                async fn on_open(&self, url: String, _connection: Arc<WebsocketConnection>) {
5170                    let mut flag = self.called.lock().await;
5171                    *flag = true;
5172                    let mut store = self.opened_url.lock().await;
5173                    *store = Some(url);
5174                }
5175                async fn on_message(&self, _data: String, _connection: Arc<WebsocketConnection>) {}
5176                async fn get_reconnect_url(
5177                    &self,
5178                    default_url: String,
5179                    _connection: Arc<WebsocketConnection>,
5180                ) -> String {
5181                    default_url
5182                }
5183            }
5184
5185            #[test]
5186            fn emits_open_and_calls_handler() {
5187                TOKIO_SHARED_RT.block_on(async {
5188                    let conn = WebsocketConnection::new("c1");
5189                    let called = Arc::new(Mutex::new(false));
5190                    let opened_url = Arc::new(Mutex::new(None));
5191                    let handler = Arc::new(DummyHandler {
5192                        called: called.clone(),
5193                        opened_url: opened_url.clone(),
5194                    });
5195
5196                    conn.set_handler(handler.clone()).await;
5197                    let common = WebsocketCommon::new(
5198                        vec![conn.clone()],
5199                        WebsocketMode::Single,
5200                        0,
5201                        None,
5202                        None,
5203                    );
5204                    let events = subscribe_events(&common);
5205                    common
5206                        .on_open("wss://example.com".into(), conn.clone(), None)
5207                        .await;
5208
5209                    sleep(std::time::Duration::from_millis(10)).await;
5210
5211                    let evs = events.lock().await;
5212                    assert!(evs.iter().any(|e| matches!(e, WebsocketEvent::Open)));
5213                    assert!(*called.lock().await);
5214                    assert_eq!(
5215                        opened_url.lock().await.as_deref(),
5216                        Some("wss://example.com")
5217                    );
5218                });
5219            }
5220
5221            #[test]
5222            fn handles_renewal_pending_and_closes_old_writer() {
5223                TOKIO_SHARED_RT.block_on(async {
5224                    let conn = WebsocketConnection::new("c2");
5225                    let (old_tx, mut old_rx) = unbounded_channel::<Message>();
5226                    {
5227                        let mut st = conn.state.lock().await;
5228                        st.renewal_pending = true;
5229                    }
5230                    let common = WebsocketCommon::new(
5231                        vec![conn.clone()],
5232                        WebsocketMode::Single,
5233                        0,
5234                        None,
5235                        None,
5236                    );
5237                    common
5238                        .on_open("url".into(), conn.clone(), Some(old_tx.clone()))
5239                        .await;
5240                    assert!(!conn.state.lock().await.renewal_pending);
5241                    match old_rx.try_recv() {
5242                        Ok(Message::Close(_)) => {}
5243                        other => panic!("expected Close, got {other:?}"),
5244                    }
5245                });
5246            }
5247        }
5248
5249        mod on_message {
5250            use super::*;
5251
5252            struct DummyHandler {
5253                called_with: Arc<Mutex<Vec<String>>>,
5254            }
5255
5256            #[async_trait]
5257            impl WebsocketHandler for DummyHandler {
5258                async fn on_open(&self, _url: String, _connection: Arc<WebsocketConnection>) {}
5259                async fn on_message(&self, data: String, _connection: Arc<WebsocketConnection>) {
5260                    self.called_with.lock().await.push(data);
5261                }
5262                async fn get_reconnect_url(
5263                    &self,
5264                    default_url: String,
5265                    _connection: Arc<WebsocketConnection>,
5266                ) -> String {
5267                    default_url
5268                }
5269            }
5270
5271            #[test]
5272            fn emits_message_event_without_handler() {
5273                TOKIO_SHARED_RT.block_on(async {
5274                    let conn = WebsocketConnection::new("c1");
5275                    let common = WebsocketCommon::new(
5276                        vec![conn.clone()],
5277                        WebsocketMode::Single,
5278                        0,
5279                        None,
5280                        None,
5281                    );
5282                    let events = subscribe_events(&common);
5283                    common.on_message("msg".into(), conn.clone()).await;
5284
5285                    sleep(Duration::from_millis(10)).await;
5286
5287                    let locked = events.lock().await;
5288                    assert!(
5289                        locked
5290                            .iter()
5291                            .any(|e| matches!(e, WebsocketEvent::Message(m) if m == "msg"))
5292                    );
5293                });
5294            }
5295
5296            #[test]
5297            fn calls_handler_and_emits_message() {
5298                TOKIO_SHARED_RT.block_on(async {
5299                    let conn = WebsocketConnection::new("c2");
5300                    let called = Arc::new(Mutex::new(Vec::new()));
5301                    let handler = Arc::new(DummyHandler {
5302                        called_with: called.clone(),
5303                    });
5304                    conn.set_handler(handler.clone()).await;
5305
5306                    let common = WebsocketCommon::new(
5307                        vec![conn.clone()],
5308                        WebsocketMode::Single,
5309                        0,
5310                        None,
5311                        None,
5312                    );
5313                    let events = subscribe_events(&common);
5314                    common.on_message("msg".into(), conn.clone()).await;
5315
5316                    sleep(Duration::from_millis(10)).await;
5317
5318                    let evs = events.lock().await;
5319                    assert!(
5320                        evs.iter()
5321                            .any(|e| matches!(e, WebsocketEvent::Message(m) if m == "msg"))
5322                    );
5323                    let msgs = called.lock().await;
5324                    assert_eq!(msgs.as_slice(), &["msg".to_string()]);
5325                });
5326            }
5327
5328            #[test]
5329            fn preserves_message_order() {
5330                TOKIO_SHARED_RT.block_on(async {
5331                    let conn = WebsocketConnection::new("c3");
5332                    let called = Arc::new(Mutex::new(Vec::new()));
5333                    let handler = Arc::new(DummyHandler {
5334                        called_with: called.clone(),
5335                    });
5336                    conn.set_handler(handler.clone()).await;
5337
5338                    let common = WebsocketCommon::new(
5339                        vec![conn.clone()],
5340                        WebsocketMode::Single,
5341                        0,
5342                        None,
5343                        None,
5344                    );
5345
5346                    for i in 0..20 {
5347                        common.on_message(format!("msg_{i}"), conn.clone()).await;
5348                    }
5349
5350                    let msgs = called.lock().await;
5351                    let expected: Vec<String> = (0..20).map(|i| format!("msg_{i}")).collect();
5352                    assert_eq!(msgs.as_slice(), expected.as_slice());
5353                });
5354            }
5355
5356            #[test]
5357            fn preserves_order_with_slow_handler() {
5358                use std::sync::atomic::{AtomicU32, Ordering};
5359
5360                struct SlowHandler {
5361                    received: Arc<Mutex<Vec<String>>>,
5362                    concurrent_count: Arc<AtomicU32>,
5363                    max_concurrent: Arc<AtomicU32>,
5364                }
5365
5366                #[async_trait]
5367                impl WebsocketHandler for SlowHandler {
5368                    async fn on_open(&self, _url: String, _connection: Arc<WebsocketConnection>) {}
5369                    async fn on_message(
5370                        &self,
5371                        data: String,
5372                        _connection: Arc<WebsocketConnection>,
5373                    ) {
5374                        let prev = self.concurrent_count.fetch_add(1, Ordering::SeqCst);
5375                        self.max_concurrent.fetch_max(prev + 1, Ordering::SeqCst);
5376                        sleep(Duration::from_millis(1)).await;
5377                        self.received.lock().await.push(data);
5378                        self.concurrent_count.fetch_sub(1, Ordering::SeqCst);
5379                    }
5380                    async fn get_reconnect_url(
5381                        &self,
5382                        default_url: String,
5383                        _connection: Arc<WebsocketConnection>,
5384                    ) -> String {
5385                        default_url
5386                    }
5387                }
5388
5389                TOKIO_SHARED_RT.block_on(async {
5390                    let conn = WebsocketConnection::new("c4");
5391                    let received = Arc::new(Mutex::new(Vec::new()));
5392                    let concurrent_count = Arc::new(AtomicU32::new(0));
5393                    let max_concurrent = Arc::new(AtomicU32::new(0));
5394                    let handler = Arc::new(SlowHandler {
5395                        received: received.clone(),
5396                        concurrent_count: concurrent_count.clone(),
5397                        max_concurrent: max_concurrent.clone(),
5398                    });
5399                    conn.set_handler(handler.clone()).await;
5400
5401                    let common = WebsocketCommon::new(
5402                        vec![conn.clone()],
5403                        WebsocketMode::Single,
5404                        0,
5405                        None,
5406                        None,
5407                    );
5408
5409                    for i in 0..10 {
5410                        common.on_message(format!("msg_{i}"), conn.clone()).await;
5411                    }
5412
5413                    let msgs = received.lock().await;
5414                    let expected: Vec<String> = (0..10).map(|i| format!("msg_{i}")).collect();
5415                    assert_eq!(msgs.as_slice(), expected.as_slice());
5416                    assert_eq!(
5417                        max_concurrent.load(Ordering::SeqCst),
5418                        1,
5419                        "messages must be processed sequentially, not concurrently"
5420                    );
5421                });
5422            }
5423        }
5424
5425        mod create_websocket {
5426            use super::*;
5427
5428            #[test]
5429            fn configuration_builders_accept_custom_handshakes() {
5430                let handshake: WebsocketHandshakeFn = Arc::new(|_, _, _, _| {
5431                    Box::pin(async { Err(tungstenite::Error::ConnectionClosed) })
5432                });
5433
5434                let api = ConfigurationWebsocketApi::builder()
5435                    .handshake(handshake.clone())
5436                    .build()
5437                    .unwrap();
5438                let streams = ConfigurationWebsocketStreams::builder()
5439                    .handshake(handshake)
5440                    .build()
5441                    .unwrap();
5442
5443                assert!(api.handshake.is_some());
5444                assert!(streams.handshake.is_some());
5445                assert!(format!("{streams:?}").contains("<custom handshake fn>"));
5446            }
5447
5448            #[test]
5449            fn custom_handshake_routes_connection_through_custom_transport() {
5450                use tokio::io::{AsyncReadExt, AsyncWriteExt};
5451
5452                TOKIO_SHARED_RT.block_on(async {
5453                    // 1. Real echo WebSocket server (the "origin").
5454                    let origin_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5455                    let origin_addr = origin_listener.local_addr().unwrap();
5456                    let _origin_guard = AbortOnDrop(tokio::spawn(async move {
5457                        if let Ok((stream, _)) = origin_listener.accept().await {
5458                            let Ok(mut ws) = accept_async(stream).await else {
5459                                return;
5460                            };
5461                            // Echo one message back, prefixed so we can prove
5462                            // the round-trip actually reached the origin.
5463                            while let Some(Ok(msg)) = ws.next().await {
5464                                if let Message::Text(text) = msg {
5465                                    let _ =
5466                                        ws.send(Message::Text(format!("echo:{text}").into())).await;
5467                                }
5468                            }
5469                        }
5470                    }));
5471
5472                    // 2. A separate TCP relay standing in for a custom transport
5473                    //    (e.g. a SOCKS5 tunnel). It blindly pipes bytes between
5474                    //    the client and the origin, and counts the bytes it
5475                    //    carried so the test can assert traffic really flowed
5476                    //    through it rather than directly to the origin.
5477                    let relay_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5478                    let relay_addr = relay_listener.local_addr().unwrap();
5479                    let relay_bytes = Arc::new(AtomicUsize::new(0));
5480                    let relay_bytes_srv = Arc::clone(&relay_bytes);
5481                    let _relay_guard = AbortOnDrop(tokio::spawn(async move {
5482                        if let Ok((mut inbound, _)) = relay_listener.accept().await {
5483                            let Ok(mut outbound) =
5484                                tokio::net::TcpStream::connect(origin_addr).await
5485                            else {
5486                                return;
5487                            };
5488                            let (mut ri, mut wi) = inbound.split();
5489                            let (mut ro, mut wo) = outbound.split();
5490                            // Client -> origin: count bytes as they flow so the
5491                            // test can observe traffic live, not just at close.
5492                            let up = async {
5493                                let mut buf = [0u8; 4096];
5494                                loop {
5495                                    match ri.read(&mut buf).await {
5496                                        Ok(0) | Err(_) => break,
5497                                        Ok(n) => {
5498                                            relay_bytes_srv.fetch_add(n, Ordering::SeqCst);
5499                                            if wo.write_all(&buf[..n]).await.is_err() {
5500                                                break;
5501                                            }
5502                                        }
5503                                    }
5504                                }
5505                            };
5506                            let down = tokio::io::copy(&mut ro, &mut wi);
5507                            let _ = tokio::join!(up, down);
5508                        }
5509                    }));
5510
5511                    // 3. Custom handshake: rewrite the request URI so the
5512                    //    transport connects to the relay instead of the origin.
5513                    //    This is the essence of the feature — the caller decides
5514                    //    how/where the underlying connection is opened.
5515                    let handshake_calls = Arc::new(AtomicUsize::new(0));
5516                    let handshake_calls_clone = Arc::clone(&handshake_calls);
5517                    let handshake: WebsocketHandshakeFn =
5518                        Arc::new(move |mut request, ws_config, disable_nagle, connector| {
5519                            let handshake_calls = Arc::clone(&handshake_calls_clone);
5520                            Box::pin(async move {
5521                                handshake_calls.fetch_add(1, Ordering::SeqCst);
5522                                // Redirect the transport target to the relay.
5523                                *request.uri_mut() = format!("ws://{relay_addr}/").parse().unwrap();
5524                                tokio_tungstenite::connect_async_tls_with_config(
5525                                    request,
5526                                    ws_config,
5527                                    disable_nagle,
5528                                    connector,
5529                                )
5530                                .await
5531                            })
5532                        });
5533
5534                    // 4. Drive the SDK's real create_websocket with the custom
5535                    //    handshake, pointing at the origin address (which the
5536                    //    handshake will override to the relay).
5537                    let ws = WebsocketCommon::create_websocket(
5538                        &format!("ws://{origin_addr}"),
5539                        None,
5540                        None,
5541                        Some(handshake),
5542                    )
5543                    .await
5544                    .expect("custom-handshake connection should succeed");
5545
5546                    // 5. Exchange a real message round-trip over the tunneled
5547                    //    connection and verify it reached the echo origin.
5548                    let (mut write, mut read) = ws.split();
5549                    write
5550                        .send(Message::Text("through-tunnel".into()))
5551                        .await
5552                        .unwrap();
5553                    let reply = timeout(Duration::from_secs(5), read.next())
5554                        .await
5555                        .expect("timed out awaiting echo")
5556                        .expect("stream ended")
5557                        .expect("read error");
5558
5559                    assert_eq!(
5560                        reply,
5561                        Message::Text("echo:through-tunnel".into()),
5562                        "message did not round-trip through the origin"
5563                    );
5564                    assert_eq!(
5565                        handshake_calls.load(Ordering::SeqCst),
5566                        1,
5567                        "custom handshake should have been invoked exactly once"
5568                    );
5569                    assert!(
5570                        relay_bytes.load(Ordering::SeqCst) > 0,
5571                        "no bytes flowed through the custom transport relay"
5572                    );
5573                });
5574            }
5575
5576            #[test]
5577            fn custom_handshake_is_used_for_initial_connection() {
5578                TOKIO_SHARED_RT.block_on(async {
5579                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5580                    let addr = listener.local_addr().unwrap();
5581                    let _listener_guard = spawn_mock_ws_listener(listener);
5582                    let handshake_calls = Arc::new(AtomicUsize::new(0));
5583                    let handshake_calls_clone = Arc::clone(&handshake_calls);
5584                    let handshake: WebsocketHandshakeFn =
5585                        Arc::new(move |request, config, disable_nagle, connector| {
5586                            let handshake_calls = Arc::clone(&handshake_calls_clone);
5587                            Box::pin(async move {
5588                                handshake_calls.fetch_add(1, Ordering::SeqCst);
5589                                assert!(config.is_none());
5590                                assert!(!disable_nagle);
5591                                assert!(connector.is_none());
5592                                tokio_tungstenite::connect_async_tls_with_config(
5593                                    request,
5594                                    config,
5595                                    disable_nagle,
5596                                    connector,
5597                                )
5598                                .await
5599                            })
5600                        });
5601
5602                    let result = WebsocketCommon::create_websocket(
5603                        &format!("ws://{addr}"),
5604                        None,
5605                        None,
5606                        Some(handshake),
5607                    )
5608                    .await;
5609
5610                    assert!(result.is_ok(), "custom handshake failed: {result:?}");
5611                    assert_eq!(handshake_calls.load(Ordering::SeqCst), 1);
5612                });
5613            }
5614
5615            #[test]
5616            fn custom_handshake_error_maps_to_handshake_error() {
5617                TOKIO_SHARED_RT.block_on(async {
5618                    let handshake_calls = Arc::new(AtomicUsize::new(0));
5619                    let handshake_calls_clone = Arc::clone(&handshake_calls);
5620                    let handshake: WebsocketHandshakeFn = Arc::new(move |_, _, _, _| {
5621                        let handshake_calls = Arc::clone(&handshake_calls_clone);
5622                        Box::pin(async move {
5623                            handshake_calls.fetch_add(1, Ordering::SeqCst);
5624                            Err(tungstenite::Error::ConnectionClosed)
5625                        })
5626                    });
5627
5628                    let result = WebsocketCommon::create_websocket(
5629                        "ws://127.0.0.1:1",
5630                        None,
5631                        None,
5632                        Some(handshake),
5633                    )
5634                    .await;
5635
5636                    assert!(matches!(result, Err(WebsocketError::Handshake(_))));
5637                    assert_eq!(handshake_calls.load(Ordering::SeqCst), 1);
5638                });
5639            }
5640
5641            #[test]
5642            fn successful_connection() {
5643                TOKIO_SHARED_RT.block_on(async {
5644                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5645                    let addr: SocketAddr = listener.local_addr().unwrap();
5646
5647                    let expected_ua = build_user_agent("product");
5648                    let expected_ua_clone = expected_ua.clone();
5649
5650                    tokio::spawn(async move {
5651                        if let Ok((stream, _)) = listener.accept().await {
5652                            let callback = |req: &Request, resp| {
5653                                let got = req
5654                                    .headers()
5655                                    .get(USER_AGENT)
5656                                    .expect("no USER_AGENT header in WS handshake")
5657                                    .to_str()
5658                                    .expect("invalid USER_AGENT header");
5659                                assert_eq!(got, expected_ua_clone, "User-Agent mismatch");
5660                                Ok(resp)
5661                            };
5662                            let _ = accept_hdr_async(stream, callback).await.unwrap();
5663                        }
5664                    });
5665
5666                    let url = format!("ws://{addr}");
5667                    let res =
5668                        WebsocketCommon::create_websocket(&url, None, Some(expected_ua), None)
5669                            .await;
5670                    assert!(res.is_ok(), "handshake failed: {res:?}");
5671                });
5672            }
5673
5674            #[test]
5675            fn invalid_url_returns_handshake_error() {
5676                TOKIO_SHARED_RT.block_on(async {
5677                    let res =
5678                        WebsocketCommon::create_websocket("not-a-valid-url", None, None, None)
5679                            .await;
5680                    assert!(matches!(res, Err(WebsocketError::Handshake(_))));
5681                });
5682            }
5683
5684            #[test]
5685            fn unreachable_host_returns_handshake_error() {
5686                TOKIO_SHARED_RT.block_on(async {
5687                    let res =
5688                        WebsocketCommon::create_websocket("ws://127.0.0.1:1", None, None, None)
5689                            .await;
5690                    assert!(matches!(res, Err(WebsocketError::Handshake(_))));
5691                });
5692            }
5693        }
5694
5695        mod connect_pool {
5696            use super::*;
5697
5698            #[test]
5699            fn connects_all_in_pool() {
5700                TOKIO_SHARED_RT.block_on(async {
5701                    let pool_size = 3;
5702                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5703                    let addr = listener.local_addr().unwrap();
5704                    tokio::spawn(async move {
5705                        for _ in 0..pool_size {
5706                            if let Ok((stream, _)) = listener.accept().await {
5707                                tokio::spawn(async move {
5708                                    let mut ws = accept_async(stream).await.unwrap();
5709                                    sleep(Duration::from_millis(500)).await;
5710                                    let _ = ws.close(None).await;
5711                                });
5712                            }
5713                        }
5714                    });
5715                    let conns: Vec<Arc<WebsocketConnection>> = (0..pool_size)
5716                        .map(|i| WebsocketConnection::new(format!("c{i}")))
5717                        .collect();
5718                    let common = WebsocketCommon::new(
5719                        conns.clone(),
5720                        WebsocketMode::Pool(pool_size),
5721                        0,
5722                        None,
5723                        None,
5724                    );
5725                    let url = format!("ws://{addr}");
5726                    common.clone().connect_pool(&url, None).await.unwrap();
5727                    for conn in conns {
5728                        let mut ok = false;
5729                        for _ in 0..100 {
5730                            if conn.state.lock().await.ws_write_tx.is_some() {
5731                                ok = true;
5732                                break;
5733                            }
5734                            sleep(Duration::from_millis(50)).await;
5735                        }
5736                        assert!(ok, "expected ws_write_tx Some after connect");
5737                    }
5738                });
5739            }
5740
5741            #[test]
5742            fn fails_if_any_refused() {
5743                TOKIO_SHARED_RT.block_on(async {
5744                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5745                    let addr = listener.local_addr().unwrap();
5746                    let pool_size = 3;
5747                    tokio::spawn(async move {
5748                        for _ in 0..2 {
5749                            if let Ok((stream, _)) = listener.accept().await {
5750                                let mut ws = accept_async(stream).await.unwrap();
5751                                let _ = ws.close(None).await;
5752                            }
5753                        }
5754                    });
5755                    let mut conns = Vec::new();
5756                    let valid_url = format!("ws://{addr}");
5757                    for i in 0..2 {
5758                        conns.push(WebsocketConnection::new(format!("c{i}")));
5759                    }
5760                    conns.push(WebsocketConnection::new("bad"));
5761                    let common = WebsocketCommon::new(
5762                        conns.clone(),
5763                        WebsocketMode::Pool(pool_size),
5764                        0,
5765                        None,
5766                        None,
5767                    );
5768                    let res = common.clone().connect_pool(&valid_url, None).await;
5769                    assert!(matches!(res, Err(WebsocketError::Handshake(_))));
5770                });
5771            }
5772
5773            #[test]
5774            fn fails_on_invalid_url() {
5775                TOKIO_SHARED_RT.block_on(async {
5776                    let conns = vec![WebsocketConnection::new("c1")];
5777                    let common = WebsocketCommon::new(conns, WebsocketMode::Pool(1), 0, None, None);
5778                    let res = common.connect_pool("not-a-url", None).await;
5779                    assert!(matches!(res, Err(WebsocketError::Handshake(_))));
5780                });
5781            }
5782
5783            #[test]
5784            fn fails_if_mixed_success_and_invalid_url() {
5785                TOKIO_SHARED_RT.block_on(async {
5786                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5787                    let addr = listener.local_addr().unwrap();
5788                    tokio::spawn(async move {
5789                        if let Ok((stream, _)) = listener.accept().await {
5790                            let mut ws = accept_async(stream).await.unwrap();
5791                            let _ = ws.close(None).await;
5792                        }
5793                    });
5794                    let good = WebsocketConnection::new("good");
5795                    let bad = WebsocketConnection::new("bad");
5796                    let common = WebsocketCommon::new(
5797                        vec![good, bad],
5798                        WebsocketMode::Pool(2),
5799                        0,
5800                        None,
5801                        None,
5802                    );
5803                    let url = format!("ws://{addr}");
5804                    let res = common.connect_pool(&url, None).await;
5805                    assert!(matches!(res, Err(WebsocketError::Handshake(_))));
5806                });
5807            }
5808
5809            #[test]
5810            fn init_connect_invoked_for_each() {
5811                TOKIO_SHARED_RT.block_on(async {
5812                    let pool_size = 2;
5813                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5814                    let addr = listener.local_addr().unwrap();
5815                    tokio::spawn(async move {
5816                        for _ in 0..pool_size {
5817                            if let Ok((stream, _)) = listener.accept().await {
5818                                tokio::spawn(async move {
5819                                    let mut ws = accept_async(stream).await.unwrap();
5820                                    sleep(Duration::from_millis(500)).await;
5821                                    let _ = ws.close(None).await;
5822                                });
5823                            }
5824                        }
5825                    });
5826                    let conns: Vec<Arc<WebsocketConnection>> = (0..pool_size)
5827                        .map(|i| WebsocketConnection::new(format!("c{i}")))
5828                        .collect();
5829                    let common = WebsocketCommon::new(
5830                        conns.clone(),
5831                        WebsocketMode::Pool(pool_size),
5832                        0,
5833                        None,
5834                        None,
5835                    );
5836                    let url = format!("ws://{addr}");
5837                    common.clone().connect_pool(&url, None).await.unwrap();
5838                    for conn in conns {
5839                        let mut ok = false;
5840                        for _ in 0..100 {
5841                            if conn.state.lock().await.ws_write_tx.is_some() {
5842                                ok = true;
5843                                break;
5844                            }
5845                            sleep(Duration::from_millis(25)).await;
5846                        }
5847                        assert!(ok, "expected ws_write_tx Some for {}", conn.id);
5848                    }
5849                });
5850            }
5851
5852            #[test]
5853            fn single_mode_uses_first_connection() {
5854                TOKIO_SHARED_RT.block_on(async {
5855                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5856                    let addr = listener.local_addr().unwrap();
5857                    let _listener_guard = spawn_mock_ws_listener(listener);
5858                    let conn = WebsocketConnection::new("c1");
5859                    let common = WebsocketCommon::new(
5860                        vec![conn.clone()],
5861                        WebsocketMode::Single,
5862                        0,
5863                        None,
5864                        None,
5865                    );
5866                    let url = format!("ws://{addr}");
5867                    common.connect_pool(&url, None).await.unwrap();
5868                    let ok = eventually_async(Duration::from_secs(5), || {
5869                        let conn = conn.clone();
5870                        async move { conn.state.lock().await.ws_write_tx.is_some() }
5871                    })
5872                    .await;
5873
5874                    assert!(ok, "single mode did not select first connection");
5875                });
5876            }
5877
5878            #[test]
5879            fn empty_subset_is_ok_and_connects_none() {
5880                TOKIO_SHARED_RT.block_on(async {
5881                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5882                    let addr = listener.local_addr().unwrap();
5883
5884                    tokio::spawn(async move {
5885                        let _ = addr;
5886                    });
5887
5888                    let c1 = WebsocketConnection::new("c1");
5889                    let c2 = WebsocketConnection::new("c2");
5890
5891                    let common = WebsocketCommon::new(
5892                        vec![c1.clone(), c2.clone()],
5893                        WebsocketMode::Pool(2),
5894                        0,
5895                        None,
5896                        None,
5897                    );
5898
5899                    let url = format!("ws://{addr}");
5900                    common
5901                        .clone()
5902                        .connect_pool(&url, Some(vec![]))
5903                        .await
5904                        .unwrap();
5905
5906                    assert!(c1.state.lock().await.ws_write_tx.is_none());
5907                    assert!(c2.state.lock().await.ws_write_tx.is_none());
5908                });
5909            }
5910        }
5911
5912        mod connection_recovery {
5913            use super::*;
5914
5915            #[test]
5916            fn pool_reconnects_when_every_member_is_down() {
5917                TOKIO_SHARED_RT.block_on(async {
5918                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5919                    let addr = listener.local_addr().unwrap();
5920                    let _server = spawn_mock_ws_listener(listener);
5921
5922                    let a = WebsocketConnection::new("a");
5923                    let b = WebsocketConnection::new("b");
5924                    let common = WebsocketCommon::new(
5925                        vec![a.clone(), b.clone()],
5926                        WebsocketMode::Pool(2),
5927                        0,
5928                        None,
5929                        None,
5930                    );
5931                    // Both pool members lost their socket and await recovery.
5932                    for conn in [&a, &b] {
5933                        conn.state.lock().await.reconnection_pending = true;
5934                    }
5935
5936                    common
5937                        .clone()
5938                        .init_connect(&format!("ws://{addr}"), false, Some(a.clone()))
5939                        .await
5940                        .expect("an explicit connection must not require a ready pool member");
5941                    let st = a.state.lock().await;
5942                    assert!(st.ws_write_tx.is_some());
5943                    assert!(!st.reconnection_pending);
5944                });
5945            }
5946
5947            #[test]
5948            fn stale_renewal_entry_already_queued_does_not_replace_newer_socket() {
5949                TOKIO_SHARED_RT.block_on(async {
5950                    let calls = Arc::new(AtomicUsize::new(0));
5951                    let conn = WebsocketConnection::new("c1");
5952                    let common = WebsocketCommon::new_with_handshake(
5953                        vec![conn.clone()],
5954                        WebsocketMode::Single,
5955                        0,
5956                        None,
5957                        None,
5958                        Some(counting_handshake(calls.clone(), vec![])),
5959                    );
5960                    let (old_tx, _old_rx) = unbounded_channel::<Message>();
5961                    let (newer_tx, _newer_rx) = unbounded_channel::<Message>();
5962                    conn.state.lock().await.ws_write_tx = Some(newer_tx.clone());
5963
5964                    // A renewal retry for the old socket that was enqueued just before
5965                    // the connection moved on to `newer_tx`.
5966                    common
5967                        .reconnect_tx
5968                        .send(ReconnectEntry {
5969                            connection_id: "c1".into(),
5970                            url: "ws://unused".into(),
5971                            is_renewal: true,
5972                            attempt: 1,
5973                            expected_writer: Some(old_tx.downgrade()),
5974                        })
5975                        .await
5976                        .unwrap();
5977
5978                    sleep(Duration::from_millis(300)).await;
5979                    assert_eq!(calls.load(Ordering::SeqCst), 0);
5980                    let st = conn.state.lock().await;
5981                    assert!(st.ws_write_tx.as_ref().unwrap().same_channel(&newer_tx));
5982                    assert!(!st.renewal_pending);
5983                });
5984            }
5985
5986            #[tokio::test(start_paused = true)]
5987            async fn renewal_retry_fires_when_socket_is_unchanged() {
5988                let calls = Arc::new(AtomicUsize::new(0));
5989                let conn = WebsocketConnection::new("c1");
5990                let common = WebsocketCommon::new_with_handshake(
5991                    vec![conn.clone()],
5992                    WebsocketMode::Single,
5993                    0,
5994                    None,
5995                    None,
5996                    Some(counting_handshake(calls.clone(), (1..100).collect())),
5997                );
5998                let (old_tx, _old_rx) = unbounded_channel::<Message>();
5999                conn.state.lock().await.ws_write_tx = Some(old_tx);
6000                common
6001                    .schedule_reconnect_retry(
6002                        &conn,
6003                        ReconnectEntry {
6004                            connection_id: "c1".into(),
6005                            url: "ws://unused".into(),
6006                            is_renewal: true,
6007                            attempt: 0,
6008                            expected_writer: None,
6009                        },
6010                    )
6011                    .await;
6012                tokio::task::yield_now().await;
6013                advance(Duration::from_secs(1)).await;
6014                for _ in 0..64 {
6015                    if calls.load(Ordering::SeqCst) >= 1 {
6016                        break;
6017                    }
6018                    tokio::task::yield_now().await;
6019                }
6020                assert_eq!(calls.load(Ordering::SeqCst), 1, "renewal retry should run");
6021            }
6022
6023            #[test]
6024            fn reconnect_entry_staleness_rules() {
6025                let (current, _rx) = unbounded_channel::<Message>();
6026                let (other, _rx2) = unbounded_channel::<Message>();
6027                let entry =
6028                    |is_renewal, expected: Option<&UnboundedSender<Message>>| ReconnectEntry {
6029                        connection_id: "c".into(),
6030                        url: "u".into(),
6031                        is_renewal,
6032                        attempt: 0,
6033                        expected_writer: expected.map(UnboundedSender::downgrade),
6034                    };
6035                let mut st = WebsocketConnectionState::new();
6036                st.ws_write_tx = Some(current.clone());
6037
6038                assert!(entry(true, None).is_still_needed(&st));
6039                assert!(entry(true, Some(&current)).is_still_needed(&st));
6040                assert!(!entry(true, Some(&other)).is_still_needed(&st));
6041                assert!(!entry(false, None).is_still_needed(&st));
6042                st.reconnection_pending = true;
6043                assert!(entry(false, None).is_still_needed(&st));
6044                st.close_initiated = true;
6045                assert!(!entry(false, None).is_still_needed(&st));
6046                assert!(!entry(true, Some(&current)).is_still_needed(&st));
6047            }
6048
6049            #[test]
6050            fn reconnect_claim_cancels_a_writer_blocked_in_send() {
6051                TOKIO_SHARED_RT.block_on(async {
6052                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6053                    let addr = listener.local_addr().unwrap();
6054                    // Server completes the handshake, then never reads again, so the
6055                    // client's socket buffers fill up and its writer blocks in `send`.
6056                    let _server = AbortOnDrop(tokio::spawn(async move {
6057                        let (stream, _) = listener.accept().await.unwrap();
6058                        let _ws = accept_async(stream).await.unwrap();
6059                        sleep(Duration::from_secs(30)).await;
6060                    }));
6061
6062                    let conn = WebsocketConnection::new("stalled");
6063                    let common = WebsocketCommon::new(
6064                        vec![conn.clone()],
6065                        WebsocketMode::Single,
6066                        60_000,
6067                        None,
6068                        None,
6069                    );
6070                    common
6071                        .clone()
6072                        .init_connect(&format!("ws://{addr}"), false, Some(conn.clone()))
6073                        .await
6074                        .unwrap();
6075                    let writer = conn.state.lock().await.ws_write_tx.clone().unwrap();
6076
6077                    let chunk = "x".repeat(1 << 20);
6078                    for _ in 0..64 {
6079                        writer.send(Message::Text(chunk.clone().into())).unwrap();
6080                    }
6081                    // Give the writer time to fill the socket buffers and block.
6082                    sleep(Duration::from_millis(300)).await;
6083                    assert!(
6084                        !writer.is_closed(),
6085                        "writer should still be blocked sending"
6086                    );
6087
6088                    assert_eq!(
6089                        conn.state.lock().await.begin_reconnect(
6090                            &writer,
6091                            WebsocketConnectionFailureReason::UnexpectedClose,
6092                        ),
6093                        ReconnectDisposition::Started,
6094                    );
6095
6096                    // The writer task exits (dropping its receiver) despite the stall.
6097                    let exited = wait_until(Duration::from_secs(2), || {
6098                        let writer = writer.clone();
6099                        async move { writer.is_closed() }
6100                    })
6101                    .await;
6102                    assert!(
6103                        exited,
6104                        "writer stayed blocked in send after the reconnect claim"
6105                    );
6106                });
6107            }
6108
6109            #[tokio::test(start_paused = true)]
6110            async fn stale_renewal_retry_is_ignored_after_newer_writer_is_installed() {
6111                let calls = Arc::new(AtomicUsize::new(0));
6112                let conn = WebsocketConnection::new("c1");
6113                let common = WebsocketCommon::new_with_handshake(
6114                    vec![conn.clone()],
6115                    WebsocketMode::Single,
6116                    0,
6117                    None,
6118                    None,
6119                    Some(counting_handshake(calls.clone(), vec![1])),
6120                );
6121
6122                let (old_tx, _old_rx) = unbounded_channel::<Message>();
6123                conn.state.lock().await.ws_write_tx = Some(old_tx);
6124                common
6125                    .schedule_reconnect_retry(
6126                        &conn,
6127                        ReconnectEntry {
6128                            connection_id: "c1".into(),
6129                            url: "ws://unused".into(),
6130                            is_renewal: true,
6131                            attempt: 0,
6132                            expected_writer: None,
6133                        },
6134                    )
6135                    .await;
6136
6137                tokio::task::yield_now().await;
6138
6139                let (newer_writer, _newer_rx) = unbounded_channel::<Message>();
6140                {
6141                    let mut state = conn.state.lock().await;
6142                    state.ws_write_tx = Some(newer_writer);
6143                    state.reconnection_pending = false;
6144                }
6145
6146                advance(Duration::from_secs(1)).await;
6147                for _ in 0..64 {
6148                    if calls.load(Ordering::SeqCst) >= 1 {
6149                        break;
6150                    }
6151                    tokio::task::yield_now().await;
6152                }
6153                assert_eq!(
6154                    calls.load(Ordering::SeqCst),
6155                    0,
6156                    "stale renewal retry performed a handshake after a newer writer was installed"
6157                );
6158            }
6159            use tokio::io::{AsyncReadExt, AsyncWriteExt};
6160
6161            fn counting_handshake(
6162                calls: Arc<AtomicUsize>,
6163                fail_on: Vec<usize>,
6164            ) -> WebsocketHandshakeFn {
6165                Arc::new(move |request, config, disable_nagle, connector| {
6166                    let attempt = calls.fetch_add(1, Ordering::SeqCst) + 1;
6167                    let fail = fail_on.contains(&attempt);
6168                    Box::pin(async move {
6169                        if fail {
6170                            return Err(tungstenite::Error::Io(std::io::Error::new(
6171                                std::io::ErrorKind::ConnectionRefused,
6172                                "injected handshake failure",
6173                            )));
6174                        }
6175                        tokio_tungstenite::connect_async_tls_with_config(
6176                            request,
6177                            config,
6178                            disable_nagle,
6179                            connector,
6180                        )
6181                        .await
6182                    })
6183                })
6184            }
6185
6186            async fn wait_until<F, Fut>(max: Duration, mut check: F) -> bool
6187            where
6188                F: FnMut() -> Fut,
6189                Fut: std::future::Future<Output = bool>,
6190            {
6191                let deadline = tokio::time::Instant::now() + max;
6192                while tokio::time::Instant::now() < deadline {
6193                    if check().await {
6194                        return true;
6195                    }
6196                    sleep(Duration::from_millis(20)).await;
6197                }
6198                check().await
6199            }
6200
6201            #[test]
6202            fn retry_delay_is_exponential_jittered_and_capped() {
6203                for attempt in 0..40 {
6204                    let exp = RECONNECT_RETRY_BASE_DELAY
6205                        .saturating_mul(2u32.saturating_pow(attempt))
6206                        .min(RECONNECT_RETRY_MAX_DELAY);
6207                    for _ in 0..20 {
6208                        let d = WebsocketCommon::reconnect_retry_delay(attempt);
6209                        assert!(d >= exp / 2, "attempt {attempt}: {d:?} < {:?}", exp / 2);
6210                        assert!(d <= exp, "attempt {attempt}: {d:?} > {exp:?}");
6211                    }
6212                }
6213                assert!(
6214                    WebsocketCommon::reconnect_retry_delay(u32::MAX) <= RECONNECT_RETRY_MAX_DELAY
6215                );
6216            }
6217
6218            #[test]
6219            fn writer_releases_socket_after_peer_eof() {
6220                TOKIO_SHARED_RT.block_on(async {
6221                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6222                    let addr = listener.local_addr().unwrap();
6223                    let (result_tx, result_rx) = oneshot::channel();
6224                    let _server = AbortOnDrop(tokio::spawn(async move {
6225                        let (tcp, _) = listener.accept().await.unwrap();
6226                        let mut ws = accept_async(tcp).await.unwrap();
6227                        // Half-close without a Close frame, keep reading.
6228                        ws.get_mut().shutdown().await.unwrap();
6229                        let released = timeout(Duration::from_secs(2), async {
6230                            let mut buf = [0u8; 256];
6231                            loop {
6232                                match ws.get_mut().read(&mut buf).await {
6233                                    Ok(0) | Err(_) => return true,
6234                                    Ok(_) => {}
6235                                }
6236                            }
6237                        })
6238                        .await
6239                        .unwrap_or(false);
6240                        let _ = result_tx.send(released);
6241                    }));
6242
6243                    let conn = WebsocketConnection::new("c1");
6244                    let common = WebsocketCommon::new(
6245                        vec![conn.clone()],
6246                        WebsocketMode::Single,
6247                        60_000,
6248                        None,
6249                        None,
6250                    );
6251                    common
6252                        .clone()
6253                        .init_connect(&format!("ws://{addr}"), false, Some(conn.clone()))
6254                        .await
6255                        .unwrap();
6256
6257                    assert!(
6258                        result_rx.await.unwrap(),
6259                        "client kept the old socket open after peer EOF"
6260                    );
6261                    let st = conn.state.lock().await;
6262                    assert!(
6263                        st.reconnection_pending,
6264                        "peer EOF should still schedule a reconnect"
6265                    );
6266                    assert!(st.ws_write_tx.is_none());
6267                });
6268            }
6269
6270            #[test]
6271            fn failed_reconnect_handshake_is_retried_until_it_succeeds() {
6272                TOKIO_SHARED_RT.block_on(async {
6273                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6274                    let addr = listener.local_addr().unwrap();
6275                    let _server = spawn_mock_ws_listener(listener);
6276
6277                    let calls = Arc::new(AtomicUsize::new(0));
6278                    let conn = WebsocketConnection::new("c1");
6279                    let common = WebsocketCommon::new_with_handshake(
6280                        vec![conn.clone()],
6281                        WebsocketMode::Single,
6282                        10,
6283                        None,
6284                        None,
6285                        Some(counting_handshake(calls.clone(), vec![1, 2])),
6286                    );
6287                    conn.state.lock().await.reconnection_pending = true;
6288                    common
6289                        .reconnect_tx
6290                        .send(ReconnectEntry {
6291                            connection_id: "c1".into(),
6292                            url: format!("ws://{addr}"),
6293                            is_renewal: false,
6294                            attempt: 0,
6295                            expected_writer: None,
6296                        })
6297                        .await
6298                        .unwrap();
6299
6300                    let reconnected = wait_until(Duration::from_secs(8), || {
6301                        let conn = conn.clone();
6302                        async move {
6303                            let st = conn.state.lock().await;
6304                            st.ws_write_tx.is_some() && !st.reconnection_pending
6305                        }
6306                    })
6307                    .await;
6308                    assert!(
6309                        reconnected,
6310                        "expected reconnect to succeed after retries; handshake calls: {}",
6311                        calls.load(Ordering::SeqCst)
6312                    );
6313                    assert_eq!(calls.load(Ordering::SeqCst), 3);
6314                });
6315            }
6316
6317            #[test]
6318            fn failed_reconnect_is_not_retried_after_close_initiated() {
6319                TOKIO_SHARED_RT.block_on(async {
6320                    let calls = Arc::new(AtomicUsize::new(0));
6321                    let conn = WebsocketConnection::new("c1");
6322                    let common = WebsocketCommon::new_with_handshake(
6323                        vec![conn.clone()],
6324                        WebsocketMode::Single,
6325                        10,
6326                        None,
6327                        None,
6328                        Some(counting_handshake(calls.clone(), (1..100).collect())),
6329                    );
6330                    conn.state.lock().await.reconnection_pending = true;
6331                    common
6332                        .reconnect_tx
6333                        .send(ReconnectEntry {
6334                            connection_id: "c1".into(),
6335                            url: "ws://127.0.0.1:1".into(),
6336                            is_renewal: false,
6337                            attempt: 0,
6338                            expected_writer: None,
6339                        })
6340                        .await
6341                        .unwrap();
6342
6343                    assert!(
6344                        wait_until(Duration::from_secs(2), || {
6345                            let calls = calls.clone();
6346                            async move { calls.load(Ordering::SeqCst) >= 1 }
6347                        })
6348                        .await
6349                    );
6350                    conn.state.lock().await.close_initiated = true;
6351
6352                    sleep(Duration::from_millis(2500)).await;
6353                    assert_eq!(calls.load(Ordering::SeqCst), 1, "no retry after close");
6354                });
6355            }
6356
6357            #[test]
6358            fn retry_is_skipped_when_reconnect_no_longer_pending() {
6359                TOKIO_SHARED_RT.block_on(async {
6360                    let calls = Arc::new(AtomicUsize::new(0));
6361                    let conn = WebsocketConnection::new("c1");
6362                    let common = WebsocketCommon::new_with_handshake(
6363                        vec![conn.clone()],
6364                        WebsocketMode::Single,
6365                        10,
6366                        None,
6367                        None,
6368                        Some(counting_handshake(calls.clone(), (1..100).collect())),
6369                    );
6370
6371                    common
6372                        .schedule_reconnect_retry(
6373                            &conn,
6374                            ReconnectEntry {
6375                                connection_id: "c1".into(),
6376                                url: "ws://127.0.0.1:1".into(),
6377                                is_renewal: false,
6378                                attempt: 0,
6379                                expected_writer: None,
6380                            },
6381                        )
6382                        .await;
6383                    sleep(Duration::from_millis(1500)).await;
6384                    assert_eq!(calls.load(Ordering::SeqCst), 0);
6385                });
6386            }
6387
6388            #[test]
6389            fn failed_renewal_resets_renewal_pending_and_is_retried() {
6390                TOKIO_SHARED_RT.block_on(async {
6391                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6392                    let addr = listener.local_addr().unwrap();
6393                    let _server = spawn_mock_ws_listener(listener);
6394
6395                    let calls = Arc::new(AtomicUsize::new(0));
6396                    let conn = WebsocketConnection::new("c1");
6397                    let common = WebsocketCommon::new_with_handshake(
6398                        vec![conn.clone()],
6399                        WebsocketMode::Single,
6400                        10,
6401                        None,
6402                        None,
6403                        Some(counting_handshake(calls.clone(), vec![1])),
6404                    );
6405
6406                    let (old_tx, _old_rx) = unbounded_channel::<Message>();
6407                    conn.state.lock().await.ws_write_tx = Some(old_tx.clone());
6408
6409                    common
6410                        .reconnect_tx
6411                        .send(ReconnectEntry {
6412                            connection_id: "c1".into(),
6413                            url: format!("ws://{addr}"),
6414                            is_renewal: true,
6415                            attempt: 0,
6416                            expected_writer: None,
6417                        })
6418                        .await
6419                        .unwrap();
6420
6421                    let renewed = wait_until(Duration::from_secs(5), || {
6422                        let conn = conn.clone();
6423                        let old_tx = old_tx.clone();
6424                        async move {
6425                            let st = conn.state.lock().await;
6426                            !st.renewal_pending
6427                                && st
6428                                    .ws_write_tx
6429                                    .as_ref()
6430                                    .is_some_and(|tx| !tx.same_channel(&old_tx))
6431                        }
6432                    })
6433                    .await;
6434                    assert!(
6435                        renewed,
6436                        "expected renewal retry to install a new writer; handshake calls: {}",
6437                        calls.load(Ordering::SeqCst)
6438                    );
6439                    assert_eq!(calls.load(Ordering::SeqCst), 2);
6440                });
6441            }
6442        }
6443
6444        mod init_connect {
6445            use super::*;
6446
6447            #[test]
6448            fn reconnect_transition_must_preempt_a_stale_writer_frame() {
6449                TOKIO_SHARED_RT.block_on(async {
6450                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6451                    let addr = listener.local_addr().unwrap();
6452                    let (server_ready_tx, server_ready_rx) = oneshot::channel();
6453                    let (post_reconnect_tx, post_reconnect_rx) = oneshot::channel();
6454
6455                    let _server = AbortOnDrop(tokio::spawn(async move {
6456                        let (stream, _) = listener.accept().await.unwrap();
6457                        let mut ws = accept_async(stream).await.unwrap();
6458                        server_ready_tx.send(()).unwrap();
6459                        let frame = ws.next().await;
6460                        let _ = post_reconnect_tx.send(frame);
6461                    }));
6462
6463                    let conn = WebsocketConnection::new("writer-cancel");
6464                    let common = WebsocketCommon::new(
6465                        vec![conn.clone()],
6466                        WebsocketMode::Single,
6467                        5_000,
6468                        None,
6469                        None,
6470                    );
6471                    let url = format!("ws://{addr}");
6472                    common
6473                        .clone()
6474                        .init_connect(&url, false, Some(conn.clone()))
6475                        .await
6476                        .unwrap();
6477                    server_ready_rx.await.unwrap();
6478
6479                    let writer = conn
6480                        .state
6481                        .lock()
6482                        .await
6483                        .ws_write_tx
6484                        .clone()
6485                        .expect("connection did not install a writer");
6486
6487                    assert_eq!(
6488                        conn.state.lock().await.begin_reconnect(
6489                            &writer,
6490                            WebsocketConnectionFailureReason::UnexpectedClose,
6491                        ),
6492                        ReconnectDisposition::Started,
6493                    );
6494
6495                    writer.send(Message::Text("after-reconnect".into())).unwrap();
6496                    let post_reconnect = timeout(Duration::from_secs(1), post_reconnect_rx)
6497                        .await
6498                        .expect("server did not finish observing the connection")
6499                        .unwrap();
6500
6501                    // The abandoned socket may still be closed gracefully (Close frame),
6502                    // but no data frame queued after the reconnect claim may reach it.
6503                    assert!(
6504                        matches!(post_reconnect, None | Some(Ok(Message::Close(_)))),
6505                        "server received a frame after the reader started reconnecting: {post_reconnect:?}"
6506                    );
6507                });
6508            }
6509            use std::panic;
6510            use tokio::sync::mpsc::{channel, error::TryRecvError};
6511
6512            struct GatedHandler {
6513                gate: Mutex<Option<oneshot::Receiver<()>>>,
6514            }
6515
6516            #[async_trait]
6517            impl WebsocketHandler for GatedHandler {
6518                async fn on_open(&self, _url: String, _connection: Arc<WebsocketConnection>) {
6519                    if let Some(rx) = self.gate.lock().await.take() {
6520                        let _ = rx.await;
6521                    }
6522                }
6523
6524                async fn on_message(&self, _data: String, _connection: Arc<WebsocketConnection>) {}
6525
6526                async fn get_reconnect_url(
6527                    &self,
6528                    url: String,
6529                    _connection: Arc<WebsocketConnection>,
6530                ) -> String {
6531                    url
6532                }
6533            }
6534
6535            #[test]
6536            fn reconnect_state_requires_current_writer_and_open_lifecycle() {
6537                let (current_writer, _) = unbounded_channel::<Message>();
6538                let (replaced_writer, _) = unbounded_channel::<Message>();
6539                let mut state = WebsocketConnectionState::new();
6540                state.ws_write_tx = Some(current_writer.clone());
6541
6542                assert_eq!(
6543                    state.begin_reconnect(
6544                        &replaced_writer,
6545                        WebsocketConnectionFailureReason::UnexpectedClose,
6546                    ),
6547                    ReconnectDisposition::StaleWriter
6548                );
6549                assert!(!state.reconnection_pending);
6550                assert!(
6551                    state
6552                        .ws_write_tx
6553                        .as_ref()
6554                        .is_some_and(|writer| writer.same_channel(&current_writer))
6555                );
6556
6557                state.close_initiated = true;
6558                assert_eq!(
6559                    state.begin_reconnect(
6560                        &current_writer,
6561                        WebsocketConnectionFailureReason::UnexpectedClose,
6562                    ),
6563                    ReconnectDisposition::Rejected
6564                );
6565                state.close_initiated = false;
6566
6567                assert_eq!(
6568                    state.begin_reconnect(
6569                        &current_writer,
6570                        WebsocketConnectionFailureReason::UnexpectedClose,
6571                    ),
6572                    ReconnectDisposition::Started
6573                );
6574                assert!(state.reconnection_pending);
6575                assert!(state.ws_write_tx.is_none());
6576            }
6577
6578            #[test]
6579            fn pool_mode_none_connection_uses_first() {
6580                TOKIO_SHARED_RT.block_on(async {
6581                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6582                    let addr = listener.local_addr().unwrap();
6583                    tokio::spawn(async move {
6584                        for _ in 0..2 {
6585                            if let Ok((stream, _)) = listener.accept().await {
6586                                let mut ws = accept_async(stream).await.unwrap();
6587                                ws.close(None).await.ok();
6588                            }
6589                        }
6590                    });
6591
6592                    let c1 = WebsocketConnection::new("c1");
6593                    let c2 = WebsocketConnection::new("c2");
6594                    let common = WebsocketCommon::new(
6595                        vec![c1.clone(), c2.clone()],
6596                        WebsocketMode::Pool(2),
6597                        0,
6598                        None,
6599                        None,
6600                    );
6601                    let url = format!("ws://{addr}");
6602
6603                    common
6604                        .clone()
6605                        .init_connect(&url, false, None)
6606                        .await
6607                        .unwrap();
6608
6609                    let ok = eventually_async(Duration::from_secs(5), || {
6610                        let conn1 = c1.clone();
6611                        async move { conn1.state.lock().await.ws_write_tx.is_some() }
6612                    })
6613                    .await;
6614
6615                    assert!(ok, "first connection was never selected");
6616                    assert!(c2.state.lock().await.ws_write_tx.is_none());
6617                });
6618            }
6619
6620            #[test]
6621            fn writer_channel_can_send_text() {
6622                TOKIO_SHARED_RT.block_on(async {
6623                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6624                    let addr = listener.local_addr().unwrap();
6625                    let received = Arc::new(Mutex::new(None::<String>));
6626                    let received_clone = received.clone();
6627
6628                    tokio::spawn(async move {
6629                        if let Ok((stream, _)) = listener.accept().await {
6630                            let mut ws = accept_async(stream).await.unwrap();
6631                            if let Some(Ok(Message::Text(txt))) = ws.next().await {
6632                                *received_clone.lock().await = Some(txt.to_string());
6633                            }
6634                            ws.close(None).await.ok();
6635                        }
6636                    });
6637
6638                    let conn = WebsocketConnection::new("cw");
6639                    let common = WebsocketCommon::new(
6640                        vec![conn.clone()],
6641                        WebsocketMode::Single,
6642                        0,
6643                        None,
6644                        None,
6645                    );
6646                    let url = format!("ws://{addr}");
6647                    common
6648                        .clone()
6649                        .init_connect(&url, false, Some(conn.clone()))
6650                        .await
6651                        .unwrap();
6652
6653                    let tx = conn.state.lock().await.ws_write_tx.clone().unwrap();
6654                    tx.send(Message::Text("ping".into())).unwrap();
6655
6656                    sleep(Duration::from_millis(50)).await;
6657
6658                    let lock = received.lock().await;
6659                    assert_eq!(lock.as_deref(), Some("ping"));
6660                });
6661            }
6662
6663            #[test]
6664            fn does_not_skip_when_reconnection_pending_even_if_writer_exists() {
6665                TOKIO_SHARED_RT.block_on(async {
6666                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6667                    let addr = listener.local_addr().unwrap();
6668                    let _listener_guard = spawn_mock_ws_listener(listener);
6669
6670                    let conn = WebsocketConnection::new("c-reconnect");
6671                    {
6672                        let mut st = conn.state.lock().await;
6673                        let (tx, _) = unbounded_channel::<Message>();
6674                        st.ws_write_tx = Some(tx);
6675                        st.reconnection_pending = true;
6676                    }
6677
6678                    let common = WebsocketCommon::new(
6679                        vec![conn.clone()],
6680                        WebsocketMode::Single,
6681                        0,
6682                        None,
6683                        None,
6684                    );
6685
6686                    let url = format!("ws://{addr}");
6687                    common
6688                        .clone()
6689                        .init_connect(&url, false, Some(conn.clone()))
6690                        .await
6691                        .unwrap();
6692
6693                    let st = conn.state.lock().await;
6694                    assert!(
6695                        st.ws_write_tx.is_some(),
6696                        "writer should be set after connect"
6697                    );
6698                    assert!(
6699                        !st.reconnection_pending,
6700                        "reconnection_pending should be cleared after successful connect"
6701                    );
6702                });
6703            }
6704
6705            #[test]
6706            fn responds_to_ping_with_pong() {
6707                TOKIO_SHARED_RT.block_on(async {
6708                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6709                    let addr = listener.local_addr().unwrap();
6710
6711                    let saw_pong = Arc::new(Mutex::new(false));
6712                    let saw_pong2 = saw_pong.clone();
6713
6714                    tokio::spawn(async move {
6715                        if let Ok((stream, _)) = listener.accept().await {
6716                            let mut ws = accept_async(stream).await.unwrap();
6717                            ws.send(Message::Ping(vec![1, 2, 3].into())).await.unwrap();
6718                            if let Some(Ok(Message::Pong(payload))) = ws.next().await {
6719                                if payload[..] == [1, 2, 3] {
6720                                    *saw_pong2.lock().await = true;
6721                                }
6722                            }
6723                            let _ = ws.close(None).await;
6724                        }
6725                    });
6726
6727                    let conn = WebsocketConnection::new("c-ping");
6728                    let common = WebsocketCommon::new(
6729                        vec![conn.clone()],
6730                        WebsocketMode::Single,
6731                        0,
6732                        None,
6733                        None,
6734                    );
6735                    let url = format!("ws://{addr}");
6736                    common
6737                        .clone()
6738                        .init_connect(&url, false, Some(conn))
6739                        .await
6740                        .unwrap();
6741
6742                    sleep(Duration::from_millis(50)).await;
6743
6744                    assert!(*saw_pong.lock().await, "server should have seen a Pong");
6745                });
6746            }
6747
6748            #[test]
6749            fn handshake_error_on_invalid_url() {
6750                TOKIO_SHARED_RT.block_on(async {
6751                    let conn = WebsocketConnection::new("c-invalid");
6752                    let common = WebsocketCommon::new(
6753                        vec![conn.clone()],
6754                        WebsocketMode::Single,
6755                        0,
6756                        None,
6757                        None,
6758                    );
6759                    let res = common
6760                        .clone()
6761                        .init_connect("not-a-url", false, Some(conn.clone()))
6762                        .await;
6763                    assert!(matches!(res, Err(WebsocketError::Handshake(_))));
6764                });
6765            }
6766
6767            #[test]
6768            fn skip_if_writer_exists_and_not_renewal() {
6769                TOKIO_SHARED_RT.block_on(async {
6770                    let conn = WebsocketConnection::new("c-writer");
6771                    let (tx, mut rx) = unbounded_channel::<Message>();
6772                    {
6773                        let mut st = conn.state.lock().await;
6774                        st.ws_write_tx = Some(tx.clone());
6775                    }
6776                    let common = WebsocketCommon::new(
6777                        vec![conn.clone()],
6778                        WebsocketMode::Single,
6779                        0,
6780                        None,
6781                        None,
6782                    );
6783                    let res = common
6784                        .clone()
6785                        .init_connect("ws://127.0.0.1:1", false, Some(conn.clone()))
6786                        .await;
6787
6788                    assert!(res.is_ok());
6789                    assert!(rx.try_recv().is_err());
6790                });
6791            }
6792
6793            #[test]
6794            fn short_circuit_on_already_renewing() {
6795                TOKIO_SHARED_RT.block_on(async {
6796                    let conn = WebsocketConnection::new("c-renew");
6797                    {
6798                        let mut st = conn.state.lock().await;
6799                        st.renewal_pending = true;
6800                    }
6801                    let common = WebsocketCommon::new(
6802                        vec![conn.clone()],
6803                        WebsocketMode::Single,
6804                        0,
6805                        None,
6806                        None,
6807                    );
6808                    let res = common
6809                        .clone()
6810                        .init_connect("ws://127.0.0.1:1", true, Some(conn.clone()))
6811                        .await;
6812
6813                    assert!(res.is_ok());
6814                    assert!(conn.state.lock().await.ws_write_tx.is_none());
6815                });
6816            }
6817
6818            #[test]
6819            fn renewal_skipped_when_close_initiated() {
6820                TOKIO_SHARED_RT.block_on(async {
6821                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6822                    let addr = listener.local_addr().unwrap();
6823                    let _listener_guard = spawn_mock_ws_listener(listener);
6824
6825                    let conn = WebsocketConnection::new("c-closed-renew");
6826                    {
6827                        let mut st = conn.state.lock().await;
6828                        st.close_initiated = true;
6829                    }
6830                    let common = WebsocketCommon::new(
6831                        vec![conn.clone()],
6832                        WebsocketMode::Single,
6833                        0,
6834                        None,
6835                        None,
6836                    );
6837                    let url = format!("ws://{addr}");
6838                    let res = common
6839                        .clone()
6840                        .init_connect(&url, true, Some(conn.clone()))
6841                        .await;
6842
6843                    assert!(res.is_ok());
6844                    let st = conn.state.lock().await;
6845                    assert!(
6846                        st.ws_write_tx.is_none(),
6847                        "closed connection must not be renewed"
6848                    );
6849                    assert!(!st.renewal_pending);
6850                });
6851            }
6852
6853            #[test]
6854            fn reconnect_skipped_when_close_initiated() {
6855                TOKIO_SHARED_RT.block_on(async {
6856                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6857                    let addr = listener.local_addr().unwrap();
6858                    let _listener_guard = spawn_mock_ws_listener(listener);
6859
6860                    let conn = WebsocketConnection::new("c-closed-reconnect");
6861                    {
6862                        let mut st = conn.state.lock().await;
6863                        st.close_initiated = true;
6864                        st.reconnection_pending = true;
6865                    }
6866                    let common = WebsocketCommon::new(
6867                        vec![conn.clone()],
6868                        WebsocketMode::Single,
6869                        0,
6870                        None,
6871                        None,
6872                    );
6873                    let url = format!("ws://{addr}");
6874                    let res = common
6875                        .clone()
6876                        .init_connect(&url, false, Some(conn.clone()))
6877                        .await;
6878
6879                    assert!(res.is_ok());
6880                    let st = conn.state.lock().await;
6881                    assert!(
6882                        st.ws_write_tx.is_none(),
6883                        "closed connection must not reconnect"
6884                    );
6885                    assert!(!st.reconnection_pending);
6886                });
6887            }
6888
6889            #[test]
6890            fn disconnect_during_reconnect_marks_close_initiated() {
6891                TOKIO_SHARED_RT.block_on(async {
6892                    let conn = WebsocketConnection::new("c-reconnecting");
6893                    {
6894                        let mut st = conn.state.lock().await;
6895                        st.reconnection_pending = true;
6896                    }
6897                    let common = WebsocketCommon::new(
6898                        vec![conn.clone()],
6899                        WebsocketMode::Single,
6900                        0,
6901                        None,
6902                        None,
6903                    );
6904                    common.disconnect().await.unwrap();
6905                    assert!(conn.state.lock().await.close_initiated);
6906                });
6907            }
6908
6909            #[test]
6910            fn is_renewal_true_sets_and_clears_flag() {
6911                TOKIO_SHARED_RT.block_on(async {
6912                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6913                    let addr = listener.local_addr().unwrap();
6914                    let _listener_guard = spawn_mock_ws_listener(listener);
6915
6916                    let conn = WebsocketConnection::new("c-new-renew");
6917                    let (gate_tx, gate_rx) = oneshot::channel();
6918                    conn.set_handler(Arc::new(GatedHandler {
6919                        gate: Mutex::new(Some(gate_rx)),
6920                    }))
6921                    .await;
6922
6923                    let common = WebsocketCommon::new(
6924                        vec![conn.clone()],
6925                        WebsocketMode::Single,
6926                        0,
6927                        None,
6928                        None,
6929                    );
6930                    let url = format!("ws://{addr}");
6931                    let res = common
6932                        .clone()
6933                        .init_connect(&url, true, Some(conn.clone()))
6934                        .await;
6935
6936                    assert!(res.is_ok());
6937
6938                    {
6939                        let st = conn.state.lock().await;
6940                        assert!(st.ws_write_tx.is_some(), "writer should be set");
6941                        assert!(
6942                            st.renewal_pending,
6943                            "renewal_pending must be true until on_open"
6944                        );
6945                    }
6946
6947                    let _ = gate_tx.send(());
6948
6949                    let ok = eventually_async(Duration::from_secs(2), || {
6950                        let conn = conn.clone();
6951                        async move { !conn.state.lock().await.renewal_pending }
6952                    })
6953                    .await;
6954                    assert!(ok, "renewal_pending should be cleared in on_open");
6955                    let st = conn.state.lock().await;
6956                    assert!(
6957                        !st.renewal_pending,
6958                        "renewal_pending should be cleared in on_open"
6959                    );
6960                });
6961            }
6962
6963            #[test]
6964            fn successful_renewal_does_not_schedule_reconnect() {
6965                TOKIO_SHARED_RT.block_on(async {
6966                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6967                    let addr = listener.local_addr().unwrap();
6968                    let server_handle = tokio::spawn(async move {
6969                        let (initial_stream, _) = listener.accept().await.unwrap();
6970                        let _initial_ws = accept_async(initial_stream).await.unwrap();
6971                        let (renewed_stream, _) = listener.accept().await.unwrap();
6972                        let _renewed_ws = accept_async(renewed_stream).await.unwrap();
6973                        sleep(Duration::from_secs(10)).await;
6974                    });
6975
6976                    let conn = WebsocketConnection::new("c-needs-renew");
6977                    let (reconnect_tx, mut reconnect_rx) = channel::<ReconnectEntry>(1);
6978                    let (renewal_tx, _renewal_rx) = channel::<(String, String)>(1);
6979                    let common = Arc::new(WebsocketCommon {
6980                        events: WebsocketEventEmitter::new(),
6981                        mode: WebsocketMode::Single,
6982                        round_robin_index: AtomicUsize::new(0),
6983                        connection_pool: vec![conn.clone()],
6984                        reconnect_tx,
6985                        renewal_tx,
6986                        reconnect_delay: 0,
6987                        agent: None,
6988                        handshake: None,
6989                        user_agent: None,
6990                        extra_headers: std::sync::RwLock::new(Vec::new()),
6991                    });
6992                    let url = format!("ws://{addr}");
6993                    let res = common
6994                        .clone()
6995                        .init_connect(&url, false, Some(conn.clone()))
6996                        .await;
6997
6998                    assert!(res.is_ok());
6999
7000                    {
7001                        let st = conn.state.lock().await;
7002                        assert!(st.ws_write_tx.is_some(), "writer should be set");
7003                    }
7004
7005                    common
7006                        .clone()
7007                        .init_connect(&url, true, Some(conn.clone()))
7008                        .await
7009                        .expect("Renewal init_connect should succeed");
7010
7011                    sleep(Duration::from_millis(1000)).await;
7012
7013                    match reconnect_rx.try_recv() {
7014                        Err(TryRecvError::Empty) => {}
7015                        Ok(_) => panic!("Received reconnection request on renewal"),
7016                        Err(TryRecvError::Disconnected) => {
7017                            panic!("Sender for reconnection_rx disconnected")
7018                        }
7019                    }
7020                    server_handle.abort();
7021                });
7022            }
7023
7024            #[test]
7025            fn replaced_connection_close_does_not_schedule_reconnect() {
7026                TOKIO_SHARED_RT.block_on(async {
7027                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7028                    let addr = listener.local_addr().unwrap();
7029                    let (close_initial_tx, close_initial_rx) = oneshot::channel();
7030                    let _server_guard = AbortOnDrop(tokio::spawn(async move {
7031                        let (initial_stream, _) = listener.accept().await.unwrap();
7032                        let mut initial_ws = accept_async(initial_stream).await.unwrap();
7033                        let (renewed_stream, _) = listener.accept().await.unwrap();
7034                        let _renewed_ws = accept_async(renewed_stream).await.unwrap();
7035                        let _ = close_initial_rx.await;
7036                        initial_ws
7037                            .close(Some(CloseFrame {
7038                                code: CloseCode::Away,
7039                                reason: "replaced connection test".into(),
7040                            }))
7041                            .await
7042                            .unwrap();
7043                        sleep(Duration::from_secs(10)).await;
7044                    }));
7045
7046                    let conn = WebsocketConnection::new("c-replaced-close");
7047                    let (reconnect_tx, mut reconnect_rx) = channel::<ReconnectEntry>(1);
7048                    let (renewal_tx, _renewal_rx) = channel::<(String, String)>(1);
7049                    let common = Arc::new(WebsocketCommon {
7050                        events: WebsocketEventEmitter::new(),
7051                        mode: WebsocketMode::Single,
7052                        round_robin_index: AtomicUsize::new(0),
7053                        connection_pool: vec![conn.clone()],
7054                        reconnect_tx,
7055                        renewal_tx,
7056                        reconnect_delay: 0,
7057                        agent: None,
7058                        handshake: None,
7059                        user_agent: None,
7060                        extra_headers: std::sync::RwLock::new(Vec::new()),
7061                    });
7062                    let events = subscribe_events(&common);
7063                    let url = format!("ws://{addr}");
7064                    common
7065                        .clone()
7066                        .init_connect(&url, false, Some(conn.clone()))
7067                        .await
7068                        .unwrap();
7069                    let initial_opened = eventually_async(Duration::from_secs(2), || {
7070                        let events = events.clone();
7071                        async move {
7072                            events
7073                                .lock()
7074                                .await
7075                                .iter()
7076                                .any(|event| matches!(event, WebsocketEvent::Open))
7077                        }
7078                    })
7079                    .await;
7080                    assert!(initial_opened, "initial connection did not open");
7081
7082                    let (gate_tx, gate_rx) = oneshot::channel();
7083                    conn.set_handler(Arc::new(GatedHandler {
7084                        gate: Mutex::new(Some(gate_rx)),
7085                    }))
7086                    .await;
7087                    common
7088                        .clone()
7089                        .init_connect(&url, true, Some(conn.clone()))
7090                        .await
7091                        .unwrap();
7092                    close_initial_tx.send(()).unwrap();
7093
7094                    let old_connection_closed = eventually_async(Duration::from_secs(2), || {
7095                        let events = events.clone();
7096                        async move {
7097                            events.lock().await.iter().any(|event| {
7098                                matches!(event, WebsocketEvent::Close(code, _) if *code == 1001)
7099                            })
7100                        }
7101                    })
7102                    .await;
7103                    assert!(old_connection_closed, "replaced connection did not close");
7104                    assert!(matches!(reconnect_rx.try_recv(), Err(TryRecvError::Empty)));
7105                    sleep(Duration::from_millis(50)).await;
7106                    assert!(
7107                        events
7108                            .lock()
7109                            .await
7110                            .iter()
7111                            .all(|event| !matches!(event, WebsocketEvent::Error(_))),
7112                        "replaced connection emitted an error event"
7113                    );
7114                    gate_tx.send(()).unwrap();
7115                });
7116            }
7117
7118            #[test]
7119            fn schedules_one_reconnect_when_renewed_connection_closes() {
7120                TOKIO_SHARED_RT.block_on(async {
7121                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7122                    let addr = listener.local_addr().unwrap();
7123                    let (close_renewed_tx, close_renewed_rx) = oneshot::channel();
7124                    let _server_guard = AbortOnDrop(tokio::spawn(async move {
7125                        let (initial_stream, _) = listener.accept().await.unwrap();
7126                        let mut initial_ws = accept_async(initial_stream).await.unwrap();
7127                        let (renewed_stream, _) = listener.accept().await.unwrap();
7128                        let mut renewed_ws = accept_async(renewed_stream).await.unwrap();
7129                        let _initial_guard = AbortOnDrop(tokio::spawn(async move {
7130                            while initial_ws.next().await.is_some() {}
7131                        }));
7132                        let _ = close_renewed_rx.await;
7133                        renewed_ws
7134                            .close(Some(CloseFrame {
7135                                code: CloseCode::Away,
7136                                reason: "renewed connection test".into(),
7137                            }))
7138                            .await
7139                            .unwrap();
7140                    }));
7141
7142                    let conn = WebsocketConnection::new("c-renewed-failure");
7143                    let (reconnect_tx, mut reconnect_rx) = channel::<ReconnectEntry>(1);
7144                    let (renewal_tx, _renewal_rx) = channel::<(String, String)>(1);
7145                    let common = Arc::new(WebsocketCommon {
7146                        events: WebsocketEventEmitter::new(),
7147                        mode: WebsocketMode::Single,
7148                        round_robin_index: AtomicUsize::new(0),
7149                        connection_pool: vec![conn.clone()],
7150                        reconnect_tx,
7151                        renewal_tx,
7152                        reconnect_delay: 0,
7153                        agent: None,
7154                        handshake: None,
7155                        user_agent: None,
7156                        extra_headers: std::sync::RwLock::new(Vec::new()),
7157                    });
7158                    let url = format!("ws://{addr}");
7159                    common
7160                        .clone()
7161                        .init_connect(&url, false, Some(conn.clone()))
7162                        .await
7163                        .unwrap();
7164                    common
7165                        .clone()
7166                        .init_connect(&url, true, Some(conn.clone()))
7167                        .await
7168                        .unwrap();
7169
7170                    let renewal_completed = eventually_async(Duration::from_secs(2), || {
7171                        let conn = conn.clone();
7172                        async move { !conn.state.lock().await.renewal_pending }
7173                    })
7174                    .await;
7175                    assert!(renewal_completed, "renewal did not complete");
7176                    close_renewed_tx.send(()).unwrap();
7177
7178                    let entry = timeout(Duration::from_secs(2), reconnect_rx.recv())
7179                        .await
7180                        .expect("renewed connection failure did not schedule reconnect")
7181                        .expect("reconnect queue closed");
7182                    assert_eq!(entry.connection_id, conn.id);
7183                    assert!(!entry.is_renewal);
7184                    sleep(Duration::from_millis(50)).await;
7185                    assert!(
7186                        matches!(reconnect_rx.try_recv(), Err(TryRecvError::Empty)),
7187                        "renewed connection failure scheduled duplicate reconnects"
7188                    );
7189                });
7190            }
7191
7192            #[test]
7193            fn default_connection_selected_when_none_passed() {
7194                TOKIO_SHARED_RT.block_on(async {
7195                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7196                    let addr = listener.local_addr().unwrap();
7197                    let _listener_guard = spawn_mock_ws_listener(listener);
7198                    let conn = WebsocketConnection::new("c-default");
7199                    let common = WebsocketCommon::new(
7200                        vec![conn.clone()],
7201                        WebsocketMode::Single,
7202                        0,
7203                        None,
7204                        None,
7205                    );
7206                    let url = format!("ws://{addr}");
7207                    let res = common.clone().init_connect(&url, false, None).await;
7208
7209                    assert!(res.is_ok());
7210                    let ok = eventually_async(Duration::from_secs(5), || {
7211                        let conn = conn.clone();
7212                        async move { conn.state.lock().await.ws_write_tx.is_some() }
7213                    })
7214                    .await;
7215
7216                    assert!(ok, "default connection was never selected");
7217                });
7218            }
7219
7220            #[test]
7221            fn schedules_reconnect_on_abnormal_close() {
7222                TOKIO_SHARED_RT.block_on(async {
7223                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7224                    let addr = listener.local_addr().unwrap();
7225                    tokio::spawn(async move {
7226                        if let Ok((stream, _)) = listener.accept().await {
7227                            let ws = accept_async(stream).await.unwrap();
7228
7229                            // Explicitly sending an AbnormalClose (1006) is not allowed by the websocket
7230                            // protocol and will be converted into Protocol (1002), which the client receives.
7231                            //
7232                            // Abruptly dropping the connection simulates a real abnormal close, which is
7233                            // handled as an error in the websocket protocol
7234                            drop(ws);
7235                        }
7236                    });
7237                    let conn = WebsocketConnection::new("c-close");
7238                    let common = WebsocketCommon::new(
7239                        vec![conn.clone()],
7240                        WebsocketMode::Single,
7241                        5_000,
7242                        None,
7243                        None,
7244                    );
7245                    let url = format!("ws://{addr}");
7246                    common
7247                        .clone()
7248                        .init_connect(&url, false, Some(conn.clone()))
7249                        .await
7250                        .unwrap();
7251
7252                    sleep(Duration::from_millis(50)).await;
7253
7254                    let st = conn.state.lock().await;
7255                    assert!(
7256                        st.reconnection_pending,
7257                        "expected reconnection_pending to be true after abnormal close"
7258                    );
7259                    assert!(
7260                        st.ws_write_tx.is_none(),
7261                        "ws_write_tx should be cleared when scheduling a reconnect"
7262                    );
7263                });
7264            }
7265        }
7266
7267        mod disconnect {
7268            use super::*;
7269
7270            #[test]
7271            fn returns_ok_when_no_connections_are_ready() {
7272                TOKIO_SHARED_RT.block_on(async {
7273                    let conn = WebsocketConnection::new("c1");
7274                    let common = WebsocketCommon::new(
7275                        vec![conn.clone()],
7276                        WebsocketMode::Single,
7277                        0,
7278                        None,
7279                        None,
7280                    );
7281                    let res = common.disconnect().await;
7282
7283                    assert!(res.is_ok());
7284                    assert!(!conn.state.lock().await.close_initiated);
7285                });
7286            }
7287
7288            #[test]
7289            fn closes_all_ready_connections() {
7290                TOKIO_SHARED_RT.block_on(async {
7291                    let conn1 = WebsocketConnection::new("c1");
7292                    let conn2 = WebsocketConnection::new("c2");
7293                    let (tx1, mut rx1) = unbounded_channel::<Message>();
7294                    let (tx2, mut rx2) = unbounded_channel::<Message>();
7295                    {
7296                        let mut s1 = conn1.state.lock().await;
7297                        s1.ws_write_tx = Some(tx1);
7298                    }
7299                    {
7300                        let mut s2 = conn2.state.lock().await;
7301                        s2.ws_write_tx = Some(tx2);
7302                    }
7303                    let common = WebsocketCommon::new(
7304                        vec![conn1.clone(), conn2.clone()],
7305                        WebsocketMode::Pool(2),
7306                        0,
7307                        None,
7308                        None,
7309                    );
7310                    let fut = common.disconnect();
7311
7312                    sleep(Duration::from_millis(50)).await;
7313
7314                    fut.await.unwrap();
7315
7316                    assert!(conn1.state.lock().await.close_initiated);
7317                    assert!(conn2.state.lock().await.close_initiated);
7318
7319                    {
7320                        let st = conn1.state.lock().await;
7321                        assert!(!st.is_session_logged_on, "conn1 should be logged out");
7322                        assert!(st.session_logon_req.is_none(), "conn1 req cleared");
7323                    }
7324                    {
7325                        let st = conn2.state.lock().await;
7326                        assert!(!st.is_session_logged_on, "conn2 should be logged out");
7327                        assert!(st.session_logon_req.is_none(), "conn2 req cleared");
7328                    }
7329
7330                    match (rx1.try_recv(), rx2.try_recv()) {
7331                        (Ok(Message::Close(_)), Ok(Message::Close(_))) => {}
7332                        other => panic!("expected two Close frames, got {other:?}"),
7333                    }
7334                });
7335            }
7336
7337            #[test]
7338            fn does_not_mark_close_initiated_if_no_writer() {
7339                TOKIO_SHARED_RT.block_on(async {
7340                    let conn = WebsocketConnection::new("c-new");
7341                    let common = WebsocketCommon::new(
7342                        vec![conn.clone()],
7343                        WebsocketMode::Single,
7344                        0,
7345                        None,
7346                        None,
7347                    );
7348                    common.disconnect().await.unwrap();
7349
7350                    assert!(!conn.state.lock().await.close_initiated);
7351                });
7352            }
7353
7354            #[test]
7355            fn mixed_pool_marks_all_and_closes_only_writers() {
7356                TOKIO_SHARED_RT.block_on(async {
7357                    let conn_w = WebsocketConnection::new("with");
7358                    let conn_wo = WebsocketConnection::new("without");
7359                    let (tx, mut rx) = unbounded_channel::<Message>();
7360                    {
7361                        let mut st = conn_w.state.lock().await;
7362                        st.ws_write_tx = Some(tx);
7363                    }
7364                    let common = WebsocketCommon::new(
7365                        vec![conn_w.clone(), conn_wo.clone()],
7366                        WebsocketMode::Pool(2),
7367                        0,
7368                        None,
7369                        None,
7370                    );
7371                    let fut = common.disconnect();
7372
7373                    sleep(Duration::from_millis(50)).await;
7374
7375                    fut.await.unwrap();
7376
7377                    assert!(conn_w.state.lock().await.close_initiated);
7378                    assert!(conn_wo.state.lock().await.close_initiated);
7379                    assert!(matches!(rx.try_recv(), Ok(Message::Close(_))));
7380                });
7381            }
7382
7383            #[test]
7384            fn after_disconnect_not_connected() {
7385                TOKIO_SHARED_RT.block_on(async {
7386                    let conn = WebsocketConnection::new("c1");
7387                    let (tx, mut _rx) = unbounded_channel::<Message>();
7388                    {
7389                        let mut st = conn.state.lock().await;
7390                        st.ws_write_tx = Some(tx);
7391                    }
7392                    let common = WebsocketCommon::new(
7393                        vec![conn.clone()],
7394                        WebsocketMode::Single,
7395                        0,
7396                        None,
7397                        None,
7398                    );
7399                    common.disconnect().await.unwrap();
7400                    assert!(!common.is_connected(Some(&conn)).await);
7401                });
7402            }
7403        }
7404
7405        mod ping_server {
7406            use super::*;
7407
7408            #[test]
7409            fn sends_ping_to_all_ready_connections() {
7410                TOKIO_SHARED_RT.block_on(async {
7411                    let mut conns = Vec::new();
7412                    for i in 0..3 {
7413                        let conn = WebsocketConnection::new(format!("c{i}"));
7414                        let (tx, rx) = unbounded_channel::<Message>();
7415                        {
7416                            let mut st = conn.state.lock().await;
7417                            st.ws_write_tx = Some(tx);
7418                        }
7419                        conns.push((conn, rx));
7420                    }
7421                    let common = WebsocketCommon::new(
7422                        conns.iter().map(|(c, _)| c.clone()).collect(),
7423                        WebsocketMode::Pool(3),
7424                        0,
7425                        None,
7426                        None,
7427                    );
7428                    common.ping_server().await;
7429                    for (_, mut rx) in conns {
7430                        match rx.try_recv() {
7431                            Ok(Message::Ping(payload)) if payload.is_empty() => {}
7432                            other => panic!("expected empty-payload Ping, got {other:?}"),
7433                        }
7434                    }
7435                });
7436            }
7437
7438            #[test]
7439            fn skips_not_ready_and_partial() {
7440                TOKIO_SHARED_RT.block_on(async {
7441                    let ready = WebsocketConnection::new("ready");
7442                    let not_ready = WebsocketConnection::new("not-ready");
7443                    let (tx_r, mut rx_r) = unbounded_channel::<Message>();
7444                    {
7445                        let mut st = ready.state.lock().await;
7446                        st.ws_write_tx = Some(tx_r);
7447                    }
7448                    {
7449                        let mut st = not_ready.state.lock().await;
7450                        st.ws_write_tx = None;
7451                    }
7452                    let common = WebsocketCommon::new(
7453                        vec![ready.clone(), not_ready.clone()],
7454                        WebsocketMode::Pool(2),
7455                        0,
7456                        None,
7457                        None,
7458                    );
7459                    common.ping_server().await;
7460                    match rx_r.try_recv() {
7461                        Ok(Message::Ping(payload)) if payload.is_empty() => {}
7462                        other => panic!("expected Ping on ready, got {other:?}"),
7463                    }
7464                });
7465            }
7466
7467            #[test]
7468            fn no_ping_when_flags_block() {
7469                TOKIO_SHARED_RT.block_on(async {
7470                    let conn = WebsocketConnection::new("c1");
7471                    let (tx, mut rx) = unbounded_channel::<Message>();
7472                    {
7473                        let mut st = conn.state.lock().await;
7474                        st.ws_write_tx = Some(tx);
7475                        st.reconnection_pending = true;
7476                    }
7477                    let common = WebsocketCommon::new(
7478                        vec![conn.clone()],
7479                        WebsocketMode::Single,
7480                        0,
7481                        None,
7482                        None,
7483                    );
7484                    common.ping_server().await;
7485                    assert!(rx.try_recv().is_err());
7486                });
7487            }
7488        }
7489
7490        mod send {
7491            use super::*;
7492
7493            #[test]
7494            fn round_robin_send_without_specific() {
7495                TOKIO_SHARED_RT.block_on(async {
7496                    let conn1 = WebsocketConnection::new("c1");
7497                    let conn2 = WebsocketConnection::new("c2");
7498                    let (tx1, mut rx1) = unbounded_channel::<Message>();
7499                    let (tx2, mut rx2) = unbounded_channel::<Message>();
7500                    {
7501                        let mut s1 = conn1.state.lock().await;
7502                        s1.ws_write_tx = Some(tx1);
7503                    }
7504                    {
7505                        let mut s2 = conn2.state.lock().await;
7506                        s2.ws_write_tx = Some(tx2);
7507                    }
7508                    let common = WebsocketCommon::new(
7509                        vec![conn1.clone(), conn2.clone()],
7510                        WebsocketMode::Pool(2),
7511                        0,
7512                        None,
7513                        None,
7514                    );
7515
7516                    let res1 = common
7517                        .send("a".into(), None, false, Duration::from_secs(1), None)
7518                        .await
7519                        .unwrap();
7520                    assert!(res1.is_none());
7521
7522                    let res2 = common
7523                        .send("b".into(), None, false, Duration::from_secs(1), None)
7524                        .await
7525                        .unwrap();
7526                    assert!(res2.is_none());
7527
7528                    assert_eq!(
7529                        if let Message::Text(t) = rx1.try_recv().unwrap() {
7530                            t
7531                        } else {
7532                            panic!()
7533                        },
7534                        "a"
7535                    );
7536                    assert_eq!(
7537                        if let Message::Text(t) = rx2.try_recv().unwrap() {
7538                            t
7539                        } else {
7540                            panic!()
7541                        },
7542                        "b"
7543                    );
7544                });
7545            }
7546
7547            #[test]
7548            fn round_robin_skips_not_ready() {
7549                TOKIO_SHARED_RT.block_on(async {
7550                    let conn1 = WebsocketConnection::new("c1");
7551                    let conn2 = WebsocketConnection::new("c2");
7552                    let (tx2, mut rx2) = unbounded_channel::<Message>();
7553                    {
7554                        let mut s1 = conn1.state.lock().await;
7555                        s1.ws_write_tx = None;
7556                    }
7557                    {
7558                        let mut s2 = conn2.state.lock().await;
7559                        s2.ws_write_tx = Some(tx2);
7560                    }
7561                    let common = WebsocketCommon::new(
7562                        vec![conn1.clone(), conn2.clone()],
7563                        WebsocketMode::Pool(2),
7564                        0,
7565                        None,
7566                        None,
7567                    );
7568                    let res = common
7569                        .send("bar".into(), None, false, Duration::from_secs(1), None)
7570                        .await
7571                        .unwrap();
7572                    assert!(res.is_none());
7573                    match rx2.try_recv().unwrap() {
7574                        Message::Text(t) => assert_eq!(t, "bar"),
7575                        other => panic!("unexpected {other:?}"),
7576                    }
7577                });
7578            }
7579
7580            #[test]
7581            fn sync_send_on_specific_connection() {
7582                TOKIO_SHARED_RT.block_on(async {
7583                    let conn1 = WebsocketConnection::new("c1");
7584                    let conn2 = WebsocketConnection::new("c2");
7585                    let (tx2, mut rx2) = unbounded_channel::<Message>();
7586                    {
7587                        let mut st = conn2.state.lock().await;
7588                        st.ws_write_tx = Some(tx2);
7589                    }
7590                    let common = WebsocketCommon::new(
7591                        vec![conn1.clone(), conn2.clone()],
7592                        WebsocketMode::Pool(2),
7593                        0,
7594                        None,
7595                        None,
7596                    );
7597                    let res = common
7598                        .send(
7599                            "payload".into(),
7600                            Some("id".into()),
7601                            false,
7602                            Duration::from_secs(1),
7603                            Some(conn2.clone()),
7604                        )
7605                        .await
7606                        .unwrap();
7607                    assert!(res.is_none());
7608                    match rx2.try_recv() {
7609                        Ok(Message::Text(t)) => assert_eq!(t, "payload"),
7610                        other => panic!("expected Text, got {other:?}"),
7611                    }
7612                });
7613            }
7614
7615            #[test]
7616            fn sync_send_with_id_does_not_insert_pending() {
7617                TOKIO_SHARED_RT.block_on(async {
7618                    let conn = WebsocketConnection::new("c1");
7619                    let (tx, mut rx) = unbounded_channel::<Message>();
7620                    {
7621                        let mut st = conn.state.lock().await;
7622                        st.ws_write_tx = Some(tx);
7623                    }
7624                    let common = WebsocketCommon::new(
7625                        vec![conn.clone()],
7626                        WebsocketMode::Single,
7627                        0,
7628                        None,
7629                        None,
7630                    );
7631                    let res = common
7632                        .send(
7633                            "msg".into(),
7634                            Some("id".into()),
7635                            false,
7636                            Duration::from_secs(1),
7637                            Some(conn.clone()),
7638                        )
7639                        .await
7640                        .unwrap();
7641                    assert!(res.is_none());
7642                    assert!(conn.state.lock().await.pending_requests.is_empty());
7643                    match rx.try_recv().unwrap() {
7644                        Message::Text(t) => assert_eq!(t, "msg"),
7645                        other => panic!("unexpected {other:?}"),
7646                    }
7647                });
7648            }
7649
7650            #[test]
7651            fn sync_send_error_if_not_ready() {
7652                TOKIO_SHARED_RT.block_on(async {
7653                    let conn = WebsocketConnection::new("c1");
7654                    let common = WebsocketCommon::new(
7655                        vec![conn.clone()],
7656                        WebsocketMode::Single,
7657                        0,
7658                        None,
7659                        None,
7660                    );
7661                    let err = common
7662                        .send(
7663                            "msg".into(),
7664                            Some("id".into()),
7665                            false,
7666                            Duration::from_secs(1),
7667                            Some(conn.clone()),
7668                        )
7669                        .await
7670                        .unwrap_err();
7671                    assert!(matches!(err, WebsocketError::NotConnected));
7672                });
7673            }
7674
7675            #[test]
7676            fn sync_send_error_when_no_ready() {
7677                TOKIO_SHARED_RT.block_on(async {
7678                    let conn = WebsocketConnection::new("c1");
7679                    let common = WebsocketCommon::new(
7680                        vec![conn.clone()],
7681                        WebsocketMode::Single,
7682                        0,
7683                        None,
7684                        None,
7685                    );
7686                    let err = common
7687                        .send("msg".into(), None, false, Duration::from_secs(1), None)
7688                        .await
7689                        .unwrap_err();
7690                    assert!(matches!(err, WebsocketError::NotConnected));
7691                });
7692            }
7693
7694            #[test]
7695            fn async_send_and_receive() {
7696                TOKIO_SHARED_RT.block_on(async {
7697                    let conn = WebsocketConnection::new("c1");
7698                    let (tx, mut rx) = unbounded_channel::<Message>();
7699                    {
7700                        let mut st = conn.state.lock().await;
7701                        st.ws_write_tx = Some(tx);
7702                    }
7703                    let common = WebsocketCommon::new(
7704                        vec![conn.clone()],
7705                        WebsocketMode::Single,
7706                        0,
7707                        None,
7708                        None,
7709                    );
7710                    let fut = common
7711                        .send(
7712                            "hello".into(),
7713                            Some("id".into()),
7714                            true,
7715                            Duration::from_secs(5),
7716                            Some(conn.clone()),
7717                        )
7718                        .await
7719                        .unwrap()
7720                        .unwrap();
7721                    match rx.try_recv() {
7722                        Ok(Message::Text(t)) => assert_eq!(t, "hello"),
7723                        other => panic!("expected Text, got {other:?}"),
7724                    }
7725                    {
7726                        let mut st = conn.state.lock().await;
7727                        let pr = st.pending_requests.remove("id").unwrap();
7728                        pr.completion.send(Ok(serde_json::json!("ok"))).unwrap();
7729                    }
7730                    let resp = fut.await.unwrap().unwrap();
7731                    assert_eq!(resp, serde_json::json!("ok"));
7732                });
7733            }
7734
7735            #[test]
7736            fn async_send_default_connection() {
7737                TOKIO_SHARED_RT.block_on(async {
7738                    let conn = WebsocketConnection::new("c1");
7739                    let (tx, mut rx) = unbounded_channel::<Message>();
7740                    {
7741                        let mut st = conn.state.lock().await;
7742                        st.ws_write_tx = Some(tx);
7743                    }
7744                    let common = WebsocketCommon::new(
7745                        vec![conn.clone()],
7746                        WebsocketMode::Single,
7747                        0,
7748                        None,
7749                        None,
7750                    );
7751                    let fut = common
7752                        .send(
7753                            "msg".into(),
7754                            Some("id".into()),
7755                            true,
7756                            Duration::from_secs(5),
7757                            None,
7758                        )
7759                        .await
7760                        .unwrap()
7761                        .unwrap();
7762                    match rx.try_recv() {
7763                        Ok(Message::Text(t)) => assert_eq!(t, "msg"),
7764                        _ => panic!("no text"),
7765                    }
7766                    {
7767                        let mut st = conn.state.lock().await;
7768                        let pr = st.pending_requests.remove("id").unwrap();
7769                        pr.completion.send(Ok(serde_json::json!(123))).unwrap();
7770                    }
7771                    let resp = fut.await.unwrap().unwrap();
7772                    assert_eq!(resp, serde_json::json!(123));
7773                });
7774            }
7775
7776            #[test]
7777            fn async_send_failure_removes_pending_entry() {
7778                // Regression test: when wait_for_reply=true, send() registers a
7779                // pending_requests entry BEFORE writing to ws_write_tx so that a
7780                // racing response can never be lost. If the write itself fails
7781                // afterwards, the pending entry must be removed so it doesn't
7782                // leak (otherwise close_connection_gracefully would block on it
7783                // until the 30s drain timeout, and on_message could match a
7784                // future request that reuses the id).
7785                TOKIO_SHARED_RT.block_on(async {
7786                    let conn = WebsocketConnection::new("c1");
7787                    let (tx, rx) = unbounded_channel::<Message>();
7788                    drop(rx);
7789                    {
7790                        let mut st = conn.state.lock().await;
7791                        st.ws_write_tx = Some(tx);
7792                    }
7793                    let common = WebsocketCommon::new(
7794                        vec![conn.clone()],
7795                        WebsocketMode::Single,
7796                        0,
7797                        None,
7798                        None,
7799                    );
7800                    let err = common
7801                        .send(
7802                            "msg".into(),
7803                            Some("id".into()),
7804                            true,
7805                            Duration::from_secs(1),
7806                            Some(conn.clone()),
7807                        )
7808                        .await
7809                        .unwrap_err();
7810                    assert!(matches!(err, WebsocketError::NotConnected));
7811                    assert!(
7812                        conn.state.lock().await.pending_requests.is_empty(),
7813                        "pending_requests must be empty after a failed send"
7814                    );
7815                });
7816            }
7817
7818            #[test]
7819            fn async_send_error_if_no_id() {
7820                TOKIO_SHARED_RT.block_on(async {
7821                    let conn = WebsocketConnection::new("c§");
7822                    let (tx, _rx) = unbounded_channel::<Message>();
7823                    {
7824                        let mut st = conn.state.lock().await;
7825                        st.ws_write_tx = Some(tx);
7826                    }
7827                    let common = WebsocketCommon::new(
7828                        vec![conn.clone()],
7829                        WebsocketMode::Single,
7830                        0,
7831                        None,
7832                        None,
7833                    );
7834                    let err = common
7835                        .send(
7836                            "msg".into(),
7837                            None,
7838                            true,
7839                            Duration::from_secs(1),
7840                            Some(conn.clone()),
7841                        )
7842                        .await
7843                        .unwrap_err();
7844                    assert!(matches!(err, WebsocketError::NotConnected));
7845                });
7846            }
7847
7848            #[test]
7849            fn timeout_rejects_async() {
7850                TOKIO_SHARED_RT.block_on(async {
7851                    pause();
7852                    let conn = WebsocketConnection::new("c1");
7853                    let (tx, _rx) = unbounded_channel::<Message>();
7854                    {
7855                        let mut st = conn.state.lock().await;
7856                        st.ws_write_tx = Some(tx);
7857                    }
7858                    let common = WebsocketCommon::new(
7859                        vec![conn.clone()],
7860                        WebsocketMode::Single,
7861                        0,
7862                        None,
7863                        None,
7864                    );
7865                    let fut = common
7866                        .send(
7867                            "msg".into(),
7868                            Some("id".into()),
7869                            true,
7870                            Duration::from_secs(1),
7871                            Some(conn.clone()),
7872                        )
7873                        .await
7874                        .unwrap()
7875                        .unwrap();
7876                    advance(Duration::from_secs(1)).await;
7877                    let res = fut.await.unwrap();
7878                    assert!(res.is_err(), "expected timeout error");
7879                    assert!(!conn.state.lock().await.pending_requests.contains_key("id"));
7880                });
7881            }
7882
7883            #[test]
7884            fn async_send_errors_if_no_connection_ready() {
7885                TOKIO_SHARED_RT.block_on(async {
7886                    let conn = WebsocketConnection::new("c1");
7887                    let common = WebsocketCommon::new(
7888                        vec![conn.clone()],
7889                        WebsocketMode::Single,
7890                        0,
7891                        None,
7892                        None,
7893                    );
7894                    let err = common
7895                        .send(
7896                            "msg".into(),
7897                            Some("id".into()),
7898                            true,
7899                            Duration::from_secs(1),
7900                            None,
7901                        )
7902                        .await
7903                        .unwrap_err();
7904                    assert!(matches!(err, WebsocketError::NotConnected));
7905                });
7906            }
7907        }
7908    }
7909
7910    mod websocket_api {
7911        use super::*;
7912
7913        mod initialisation {
7914            use super::*;
7915
7916            #[test]
7917            fn new_initializes_common() {
7918                TOKIO_SHARED_RT.block_on(async {
7919                    let conn = WebsocketConnection::new("id");
7920                    let pool = vec![conn.clone()];
7921
7922                    let sig_gen = SignatureGenerator::new(
7923                        Some("api_secret".to_string()),
7924                        None::<PrivateKey>,
7925                        None::<String>,
7926                    );
7927
7928                    let config = ConfigurationWebsocketApi {
7929                        api_key: Some("api_key".to_string()),
7930                        api_secret: Some("api_secret".to_string()),
7931                        private_key: None,
7932                        private_key_passphrase: None,
7933                        ws_url: Some("wss://example".to_string()),
7934                        mode: WebsocketMode::Single,
7935                        reconnect_delay: 1000,
7936                        signature_gen: sig_gen,
7937                        timeout: 500,
7938                        time_unit: None,
7939                        auto_session_relogon: false,
7940                        agent: None,
7941                        handshake: None,
7942                        user_agent: build_user_agent("product"),
7943                        timestamp_provider: None,
7944                    };
7945
7946                    let api = WebsocketApi::new(config, pool.clone());
7947
7948                    assert_eq!(api.common.connection_pool.len(), 1);
7949                    assert_eq!(api.common.mode, WebsocketMode::Single);
7950
7951                    let flag = *api.is_connecting.lock().await;
7952                    assert!(!flag);
7953                });
7954            }
7955        }
7956
7957        mod connect {
7958            use super::*;
7959
7960            #[test]
7961            fn connect_when_not_connected_establishes() {
7962                TOKIO_SHARED_RT.block_on(async {
7963                    let conn = WebsocketConnection::new("id");
7964                    {
7965                        let mut st = conn.state.lock().await;
7966                        st.ws_write_tx = None;
7967                    }
7968                    let sig = SignatureGenerator::new(
7969                        Some("api_secret".into()),
7970                        None::<PrivateKey>,
7971                        None::<String>,
7972                    );
7973                    let cfg = ConfigurationWebsocketApi {
7974                        api_key: Some("api_key".into()),
7975                        api_secret: Some("api_secret".to_string()),
7976                        private_key: None,
7977                        private_key_passphrase: None,
7978                        ws_url: Some("ws://doesnotexist:1".to_string()),
7979                        mode: WebsocketMode::Single,
7980                        reconnect_delay: 0,
7981                        signature_gen: sig,
7982                        timeout: 10,
7983                        time_unit: None,
7984                        auto_session_relogon: false,
7985                        agent: None,
7986                        handshake: None,
7987                        user_agent: build_user_agent("product"),
7988                        timestamp_provider: None,
7989                    };
7990                    let api = WebsocketApi::new(cfg, vec![conn.clone()]);
7991                    let res = api.clone().connect().await;
7992                    assert!(!matches!(res, Err(WebsocketError::Timeout)));
7993                });
7994            }
7995
7996            #[test]
7997            fn already_connected_returns_ok() {
7998                TOKIO_SHARED_RT.block_on(async {
7999                    let conn = WebsocketConnection::new("id2");
8000                    let (tx, _) = unbounded_channel();
8001                    {
8002                        let mut st = conn.state.lock().await;
8003                        st.ws_write_tx = Some(tx);
8004                    }
8005                    let sig = SignatureGenerator::new(
8006                        Some("api_secret".to_string()),
8007                        None::<PrivateKey>,
8008                        None::<String>,
8009                    );
8010                    let cfg = ConfigurationWebsocketApi {
8011                        api_key: Some("api_key".to_string()),
8012                        api_secret: Some("api_secret".to_string()),
8013                        private_key: None,
8014                        private_key_passphrase: None,
8015                        ws_url: Some("ws://example.com".to_string()),
8016                        mode: WebsocketMode::Single,
8017                        reconnect_delay: 0,
8018                        signature_gen: sig,
8019                        timeout: 10,
8020                        time_unit: None,
8021                        auto_session_relogon: false,
8022                        agent: None,
8023                        handshake: None,
8024                        user_agent: build_user_agent("product"),
8025                        timestamp_provider: None,
8026                    };
8027                    let api = WebsocketApi::new(cfg, vec![conn.clone()]);
8028                    let res = api.connect().await;
8029                    assert!(res.is_ok());
8030                });
8031            }
8032
8033            #[test]
8034            fn not_connected_returns_error() {
8035                TOKIO_SHARED_RT.block_on(async {
8036                    let conn = WebsocketConnection::new("id1");
8037                    let sig = SignatureGenerator::new(
8038                        Some("api_secret".to_string()),
8039                        None::<PrivateKey>,
8040                        None::<String>,
8041                    );
8042                    let cfg = ConfigurationWebsocketApi {
8043                        api_key: Some("api_key".to_string()),
8044                        api_secret: Some("api_secret".to_string()),
8045                        private_key: None,
8046                        private_key_passphrase: None,
8047                        ws_url: Some("ws://127.0.0.1:9".to_string()),
8048                        mode: WebsocketMode::Single,
8049                        reconnect_delay: 0,
8050                        signature_gen: sig,
8051                        timeout: 10,
8052                        time_unit: None,
8053                        auto_session_relogon: false,
8054                        agent: None,
8055                        handshake: None,
8056                        user_agent: build_user_agent("product"),
8057                        timestamp_provider: None,
8058                    };
8059                    let api = WebsocketApi::new(cfg, vec![conn.clone()]);
8060                    let res = api.connect().await;
8061                    assert!(res.is_err());
8062                });
8063            }
8064
8065            #[test]
8066            fn concurrent_calls_both_error_or_ok() {
8067                TOKIO_SHARED_RT.block_on(async {
8068                    let conn = WebsocketConnection::new("id3");
8069                    let sig = SignatureGenerator::new(
8070                        Some("api_secret".to_string()),
8071                        None::<PrivateKey>,
8072                        None::<String>,
8073                    );
8074                    let cfg = ConfigurationWebsocketApi {
8075                        api_key: Some("api_key".to_string()),
8076                        api_secret: Some("api_secret".to_string()),
8077                        private_key: None,
8078                        private_key_passphrase: None,
8079                        ws_url: Some("wss://invalid-domain".to_string()),
8080                        mode: WebsocketMode::Single,
8081                        reconnect_delay: 0,
8082                        signature_gen: sig,
8083                        timeout: 10,
8084                        time_unit: None,
8085                        auto_session_relogon: false,
8086                        agent: None,
8087                        handshake: None,
8088                        user_agent: build_user_agent("product"),
8089                        timestamp_provider: None,
8090                    };
8091                    let api = WebsocketApi::new(cfg, vec![conn.clone()]);
8092                    let fut1 = tokio::spawn(api.clone().connect());
8093                    let fut2 = tokio::spawn(api.clone().connect());
8094                    let r1 = fut1.await.unwrap();
8095                    let r2 = fut2.await.unwrap();
8096
8097                    assert!(r1.is_err());
8098                    assert!(r2.is_err() || r2.is_ok());
8099                });
8100            }
8101
8102            #[test]
8103            fn pool_failure_is_propagated() {
8104                TOKIO_SHARED_RT.block_on(async {
8105                    let conn = WebsocketConnection::new("w");
8106                    let sig = SignatureGenerator::new(
8107                        Some("api_secret".to_string()),
8108                        None::<PrivateKey>,
8109                        None::<String>,
8110                    );
8111                    let cfg = ConfigurationWebsocketApi {
8112                        api_key: Some("api_key".into()),
8113                        api_secret: Some("api_secret".to_string()),
8114                        private_key: None,
8115                        private_key_passphrase: None,
8116                        ws_url: Some("ws://doesnotexist:1".to_string()),
8117                        mode: WebsocketMode::Single,
8118                        reconnect_delay: 0,
8119                        signature_gen: sig,
8120                        timeout: 10,
8121                        time_unit: None,
8122                        auto_session_relogon: false,
8123                        agent: None,
8124                        handshake: None,
8125                        user_agent: build_user_agent("product"),
8126                        timestamp_provider: None,
8127                    };
8128                    let api = WebsocketApi::new(cfg, vec![conn.clone()]);
8129                    let res = api.clone().connect().await;
8130                    match res {
8131                        Err(WebsocketError::Handshake(_) | WebsocketError::Timeout) => {}
8132                        _ => panic!("expected handshake or timeout error"),
8133                    }
8134                });
8135            }
8136        }
8137
8138        mod send_message {
8139            use super::*;
8140
8141            #[test]
8142            fn unsigned_message() {
8143                TOKIO_SHARED_RT.block_on(async {
8144                    let api = create_websocket_api(None, None, None);
8145                    let conn = &api.common.connection_pool[0];
8146                    let (tx, mut rx) = unbounded_channel::<Message>();
8147                    {
8148                        let mut st = conn.state.lock().await;
8149                        st.ws_write_tx = Some(tx);
8150                    }
8151
8152                    let fut = tokio::spawn({
8153                        let api = api.clone();
8154                        async move {
8155                            let mut params = BTreeMap::new();
8156                            params.insert("foo".into(), Value::String("bar".into()));
8157                            let send_res = api
8158                                .send_message::<Value>(
8159                                    "method",
8160                                    params,
8161                                    WebsocketMessageSendOptions {
8162                                        with_api_key: false,
8163                                        is_signed: false,
8164                                        ..Default::default()
8165                                    },
8166                                )
8167                                .await
8168                                .unwrap();
8169
8170                            match send_res {
8171                                SendWebsocketMessageResult::Single(resp) => resp,
8172                                SendWebsocketMessageResult::Multiple(_) => {
8173                                    panic!("expected single response")
8174                                }
8175                            }
8176                        }
8177                    });
8178
8179                    let Message::Text(txt) = rx.recv().await.unwrap() else {
8180                        panic!()
8181                    };
8182                    let req: Value = serde_json::from_str(&txt).unwrap();
8183                    assert_eq!(req["method"], "method");
8184                    assert_eq!(req["params"]["foo"], "bar");
8185                    assert!(req["params"].get("apiKey").is_none());
8186                    assert!(req["params"].get("timestamp").is_none());
8187                    assert!(req["params"].get("signature").is_none());
8188
8189                    let id = req["id"].as_str().unwrap().to_string();
8190                    let mut st = conn.state.lock().await;
8191                    let pending = st.pending_requests.remove(&id).unwrap();
8192                    let reply = json!({
8193                        "id": id,
8194                        "result": { "x": 42 },
8195                        "rateLimits": [{ "limit": 7 }]
8196                    });
8197                    pending.completion.send(Ok(reply)).unwrap();
8198
8199                    let resp = fut.await.unwrap();
8200                    let rate_limits = resp.rate_limits.unwrap_or_default();
8201
8202                    assert!(rate_limits.is_empty());
8203                    assert_eq!(resp.raw, json!({"x": 42}));
8204                });
8205            }
8206
8207            #[test]
8208            fn with_api_key_only() {
8209                TOKIO_SHARED_RT.block_on(async {
8210                    let api = create_websocket_api(None, None, None);
8211                    let conn = &api.common.connection_pool[0];
8212                    let (tx, mut rx) = unbounded_channel::<Message>();
8213                    {
8214                        let mut st = conn.state.lock().await;
8215                        st.ws_write_tx = Some(tx);
8216                    }
8217
8218                    let fut = tokio::spawn({
8219                        let api = api.clone();
8220                        async move {
8221                            let params = BTreeMap::new();
8222                            let send_res = api
8223                                .send_message::<Value>(
8224                                    "method",
8225                                    params,
8226                                    WebsocketMessageSendOptions {
8227                                        with_api_key: true,
8228                                        is_signed: false,
8229                                        ..Default::default()
8230                                    },
8231                                )
8232                                .await
8233                                .unwrap();
8234
8235                            match send_res {
8236                                SendWebsocketMessageResult::Single(resp) => resp,
8237                                SendWebsocketMessageResult::Multiple(_) => {
8238                                    panic!("expected single response")
8239                                }
8240                            }
8241                        }
8242                    });
8243
8244                    let Message::Text(txt) = rx.recv().await.unwrap() else {
8245                        panic!()
8246                    };
8247                    let req: Value = serde_json::from_str(&txt).unwrap();
8248                    assert_eq!(req["params"]["apiKey"], "api_key");
8249
8250                    let id = req["id"].as_str().unwrap().to_string();
8251                    let mut st = conn.state.lock().await;
8252                    let pending = st.pending_requests.remove(&id).unwrap();
8253                    pending
8254                        .completion
8255                        .send(Ok(json!({
8256                            "id": id,
8257                            "result": {},
8258                            "rateLimits": []
8259                        })))
8260                        .unwrap();
8261
8262                    let resp = fut.await.unwrap();
8263
8264                    assert_eq!(resp.raw, json!({}));
8265                    assert!(st.pending_requests.is_empty());
8266                });
8267            }
8268
8269            #[test]
8270            fn signed_message_has_timestamp_and_signature() {
8271                TOKIO_SHARED_RT.block_on(async {
8272                    let api = create_websocket_api(None, None, None);
8273                    let conn = &api.common.connection_pool[0];
8274                    let (tx, mut rx) = unbounded_channel::<Message>();
8275                    {
8276                        let mut st = conn.state.lock().await;
8277                        st.ws_write_tx = Some(tx);
8278                    }
8279
8280                    let fut = tokio::spawn({
8281                        let api = api.clone();
8282                        async move {
8283                            let mut params = BTreeMap::new();
8284                            params.insert("foo".into(), Value::String("bar".into()));
8285                            let send_res = api
8286                                .send_message::<Value>(
8287                                    "method",
8288                                    params,
8289                                    WebsocketMessageSendOptions {
8290                                        with_api_key: true,
8291                                        is_signed: true,
8292                                        ..Default::default()
8293                                    },
8294                                )
8295                                .await
8296                                .unwrap();
8297
8298                            match send_res {
8299                                SendWebsocketMessageResult::Single(resp) => resp,
8300                                SendWebsocketMessageResult::Multiple(_) => {
8301                                    panic!("expected single response")
8302                                }
8303                            }
8304                        }
8305                    });
8306
8307                    let Message::Text(txt) = rx.recv().await.unwrap() else {
8308                        panic!()
8309                    };
8310                    let req: Value = serde_json::from_str(&txt).unwrap();
8311                    let p = &req["params"];
8312                    assert_eq!(p["apiKey"], "api_key");
8313                    assert!(p["timestamp"].is_number());
8314                    assert!(p["signature"].is_string());
8315
8316                    let id = req["id"].as_str().unwrap().to_string();
8317                    let mut st = conn.state.lock().await;
8318                    let pending = st.pending_requests.remove(&id).unwrap();
8319                    pending
8320                        .completion
8321                        .send(Ok(json!({
8322                            "id": id,
8323                            "result": { "ok": true },
8324                            "rateLimits": []
8325                        })))
8326                        .unwrap();
8327
8328                    let resp = fut.await.unwrap();
8329                    assert_eq!(resp.raw, json!({ "ok": true }));
8330                });
8331            }
8332
8333            #[test]
8334            fn multi_session_logon() {
8335                TOKIO_SHARED_RT.block_on(async {
8336                    let api = create_websocket_api(None, Some(WebsocketMode::Pool(2)), None);
8337                    let conn0 = &api.common.connection_pool[0];
8338                    let conn1 = &api.common.connection_pool[1];
8339
8340                    let (tx0, mut rx0) = unbounded_channel::<Message>();
8341                    let (tx1, mut rx1) = unbounded_channel::<Message>();
8342                    {
8343                        let mut st0 = conn0.state.lock().await;
8344                        st0.ws_write_tx = Some(tx0);
8345                    }
8346                    {
8347                        let mut st1 = conn1.state.lock().await;
8348                        st1.ws_write_tx = Some(tx1);
8349                    }
8350
8351                    let fut = tokio::spawn({
8352                        let api = api.clone();
8353                        async move {
8354                            let params = BTreeMap::new();
8355                            let send_res = api
8356                                .send_message::<Value>(
8357                                    "method",
8358                                    params,
8359                                    WebsocketMessageSendOptions {
8360                                        is_session_logon: Some(true),
8361                                        ..Default::default()
8362                                    },
8363                                )
8364                                .await
8365                                .unwrap();
8366
8367                            match send_res {
8368                                SendWebsocketMessageResult::Multiple(v) => v,
8369                                SendWebsocketMessageResult::Single(_) => {
8370                                    panic!("expected multiple responses")
8371                                }
8372                            }
8373                        }
8374                    });
8375
8376                    let Message::Text(txt0) = rx0.recv().await.unwrap() else {
8377                        panic!()
8378                    };
8379                    let Message::Text(txt1) = rx1.recv().await.unwrap() else {
8380                        panic!()
8381                    };
8382                    let req0: Value = serde_json::from_str(&txt0).unwrap();
8383                    let req1: Value = serde_json::from_str(&txt1).unwrap();
8384                    assert_eq!(req0["method"], "method");
8385                    assert_eq!(req1["method"], "method");
8386                    let id = req0["id"].as_str().unwrap().to_string();
8387                    assert_eq!(req1["id"].as_str().unwrap(), &id);
8388
8389                    {
8390                        let mut st0 = conn0.state.lock().await;
8391                        let pending0 = st0.pending_requests.remove(&id).unwrap();
8392                        pending0
8393                            .completion
8394                            .send(Ok(json!({
8395                                "id": id,
8396                                "result": { "ok": true },
8397                                "rateLimits": []
8398                            })))
8399                            .unwrap();
8400                    }
8401                    {
8402                        let mut st1 = conn1.state.lock().await;
8403                        let pending1 = st1.pending_requests.remove(&id).unwrap();
8404                        pending1
8405                            .completion
8406                            .send(Ok(json!({
8407                                "id": id,
8408                                "result": { "ok": true },
8409                                "rateLimits": []
8410                            })))
8411                            .unwrap();
8412                    }
8413
8414                    let results = fut.await.unwrap();
8415                    assert_eq!(results.len(), 2);
8416
8417                    for conn in &api.common.connection_pool {
8418                        let st = conn.state.lock().await;
8419                        assert!(st.is_session_logged_on, "should be logged out");
8420                        assert!(st.session_logon_req.is_some(), "req cleared");
8421
8422                        // let req = st
8423                        //     .session_logon_req
8424                        //     .as_ref()
8425                        //     .expect("session_logon_req should be Some(_)");
8426                        // assert_eq!(req.method, "method");
8427                        // let mut expected = BTreeMap::new();
8428                        // expected.insert("ok".to_string(), Value::Bool(true));
8429                        // assert_eq!(
8430                        //     req.payload, expected,
8431                        //     "stored payload should be {{ \"ok\": true }}"
8432                        // );
8433                        // assert!(
8434                        //     req.options.is_session_logon.unwrap_or(false),
8435                        //     "expected options.is_session_logon = true"
8436                        // );
8437                    }
8438                });
8439            }
8440
8441            #[test]
8442            fn multi_session_logout() {
8443                TOKIO_SHARED_RT.block_on(async {
8444                    let api = create_websocket_api(None, Some(WebsocketMode::Pool(2)), None);
8445
8446                    for conn in &api.common.connection_pool {
8447                        let (tx, _rx) = unbounded_channel::<Message>();
8448                        let mut st = conn.state.lock().await;
8449                        st.ws_write_tx = Some(tx);
8450                        st.is_session_logged_on = true;
8451                        st.session_logon_req = Some(WebsocketSessionLogonReq {
8452                            method: "method".into(),
8453                            payload: BTreeMap::new(),
8454                            options: WebsocketMessageSendOptions::default(),
8455                        });
8456                    }
8457
8458                    let mut rxs = Vec::new();
8459                    for conn in &api.common.connection_pool {
8460                        let rx = {
8461                            let (tx, rx) = unbounded_channel::<Message>();
8462                            conn.state.lock().await.ws_write_tx = Some(tx);
8463                            rx
8464                        };
8465                        rxs.push(rx);
8466                    }
8467
8468                    let fut = tokio::spawn({
8469                        let api = api.clone();
8470                        async move {
8471                            let send_res = api
8472                                .send_message::<Value>(
8473                                    "method",
8474                                    BTreeMap::new(),
8475                                    WebsocketMessageSendOptions {
8476                                        is_signed: false,
8477                                        with_api_key: false,
8478                                        is_session_logout: Some(true),
8479                                        ..Default::default()
8480                                    },
8481                                )
8482                                .await
8483                                .unwrap();
8484
8485                            match send_res {
8486                                SendWebsocketMessageResult::Multiple(v) => v,
8487                                SendWebsocketMessageResult::Single(_) => panic!("expected multi"),
8488                            }
8489                        }
8490                    });
8491
8492                    let mut ids = Vec::new();
8493                    for mut rx in rxs {
8494                        let Message::Text(txt) = rx.recv().await.unwrap() else {
8495                            panic!()
8496                        };
8497                        let req: Value = serde_json::from_str(&txt).unwrap();
8498                        assert_eq!(req["method"], "method");
8499                        ids.push(req["id"].as_str().unwrap().to_string());
8500                    }
8501
8502                    assert_eq!(ids[0], ids[1]);
8503
8504                    for conn in &api.common.connection_pool {
8505                        let id = &ids[0];
8506                        let mut st = conn.state.lock().await;
8507                        let pending = st.pending_requests.remove(id).unwrap();
8508                        pending
8509                            .completion
8510                            .send(Ok(json!({
8511                                "id": id,
8512                                "result": {},
8513                                "rateLimits": []
8514                            })))
8515                            .unwrap();
8516                    }
8517
8518                    let results = fut.await.unwrap();
8519                    assert_eq!(results.len(), 2);
8520
8521                    for conn in &api.common.connection_pool {
8522                        let st = conn.state.lock().await;
8523                        assert!(!st.is_session_logged_on, "should be logged out");
8524                        assert!(st.session_logon_req.is_none(), "req cleared");
8525                    }
8526                });
8527            }
8528
8529            #[test]
8530            fn skip_signature_when_logged_on_and_auto_relogon() {
8531                TOKIO_SHARED_RT.block_on(async {
8532                    let api = create_websocket_api(None, Some(WebsocketMode::Single), None);
8533                    let conn = &api.common.connection_pool[0];
8534                    {
8535                        let mut st = conn.state.lock().await;
8536                        st.ws_write_tx = Some(unbounded_channel::<Message>().0);
8537                        st.is_session_logged_on = true;
8538                    }
8539
8540                    let mut rx;
8541                    {
8542                        let mut st = conn.state.lock().await;
8543                        let (tx, new_rx) = unbounded_channel::<Message>();
8544                        st.ws_write_tx = Some(tx);
8545                        rx = new_rx;
8546                    }
8547
8548                    let fut = tokio::spawn({
8549                        let api = api.clone();
8550                        async move {
8551                            let send_res = api
8552                                .send_message::<Value>(
8553                                    "method",
8554                                    BTreeMap::new(),
8555                                    WebsocketMessageSendOptions {
8556                                        is_signed: true,
8557                                        ..Default::default()
8558                                    },
8559                                )
8560                                .await
8561                                .unwrap();
8562
8563                            match send_res {
8564                                SendWebsocketMessageResult::Single(resp) => resp,
8565                                SendWebsocketMessageResult::Multiple(_) => {
8566                                    panic!("expected single")
8567                                }
8568                            }
8569                        }
8570                    });
8571
8572                    let Message::Text(txt) = rx.recv().await.unwrap() else {
8573                        panic!()
8574                    };
8575                    let req: Value = serde_json::from_str(&txt).unwrap();
8576                    let p = &req["params"];
8577                    assert!(p.get("timestamp").is_some());
8578                    assert!(p.get("signature").is_none());
8579
8580                    let id = req["id"].as_str().unwrap();
8581                    let mut st = conn.state.lock().await;
8582                    let pending = st.pending_requests.remove(id).unwrap();
8583                    pending
8584                        .completion
8585                        .send(Ok(json!({
8586                            "id": id,
8587                            "result": {},
8588                            "rateLimits": []
8589                        })))
8590                        .unwrap();
8591
8592                    let resp = fut.await.unwrap();
8593                    assert_eq!(resp.raw, json!({}));
8594                });
8595            }
8596
8597            #[test]
8598            fn include_signature_when_logged_on_and_no_auto_relogon() {
8599                TOKIO_SHARED_RT.block_on(async {
8600                    let api = create_websocket_api(None, Some(WebsocketMode::Single), Some(false));
8601                    let conn = &api.common.connection_pool[0];
8602                    {
8603                        let mut st = conn.state.lock().await;
8604                        st.ws_write_tx = Some(unbounded_channel::<Message>().0);
8605                        st.is_session_logged_on = true;
8606                    }
8607
8608                    let mut rx;
8609                    {
8610                        let mut st = conn.state.lock().await;
8611                        let (tx, new_rx) = unbounded_channel::<Message>();
8612                        st.ws_write_tx = Some(tx);
8613                        rx = new_rx;
8614                    }
8615
8616                    let fut = tokio::spawn({
8617                        let api = api.clone();
8618                        async move {
8619                            let send_res = api
8620                                .send_message::<Value>(
8621                                    "method",
8622                                    BTreeMap::new(),
8623                                    WebsocketMessageSendOptions {
8624                                        is_signed: true,
8625                                        ..Default::default()
8626                                    },
8627                                )
8628                                .await
8629                                .unwrap();
8630
8631                            match send_res {
8632                                SendWebsocketMessageResult::Single(resp) => resp,
8633                                SendWebsocketMessageResult::Multiple(_) => {
8634                                    panic!("expected single")
8635                                }
8636                            }
8637                        }
8638                    });
8639
8640                    let Message::Text(txt) = rx.recv().await.unwrap() else {
8641                        panic!()
8642                    };
8643                    let req: Value = serde_json::from_str(&txt).unwrap();
8644                    let p = &req["params"];
8645                    assert!(p.get("timestamp").is_some());
8646                    assert!(p.get("signature").is_some());
8647
8648                    let id = req["id"].as_str().unwrap();
8649                    let mut st = conn.state.lock().await;
8650                    let pending = st.pending_requests.remove(id).unwrap();
8651                    pending
8652                        .completion
8653                        .send(Ok(json!({
8654                            "id": id,
8655                            "result": {},
8656                            "rateLimits": []
8657                        })))
8658                        .unwrap();
8659
8660                    let resp = fut.await.unwrap();
8661                    assert_eq!(resp.raw, json!({}));
8662                });
8663            }
8664
8665            #[test]
8666            fn error_if_not_connected() {
8667                TOKIO_SHARED_RT.block_on(async {
8668                    let api = create_websocket_api(None, None, None);
8669                    let conn = &api.common.connection_pool[0];
8670                    {
8671                        let mut st = conn.state.lock().await;
8672                        st.ws_write_tx = None;
8673                    }
8674                    let params = BTreeMap::new();
8675                    let err = api
8676                        .send_message::<Value>(
8677                            "method",
8678                            params,
8679                            WebsocketMessageSendOptions {
8680                                with_api_key: false,
8681                                is_signed: false,
8682                                ..Default::default()
8683                            },
8684                        )
8685                        .await
8686                        .unwrap_err();
8687                    matches!(err, WebsocketError::NotConnected);
8688                });
8689            }
8690        }
8691
8692        mod prepare_url {
8693            use super::*;
8694
8695            #[test]
8696            fn no_time_unit() {
8697                TOKIO_SHARED_RT.block_on(async {
8698                    let api = create_websocket_api(None, None, None);
8699                    let url = "wss://example.com/ws".to_string();
8700                    assert_eq!(api.prepare_url(&url), url);
8701                });
8702            }
8703
8704            #[test]
8705            fn appends_time_unit() {
8706                TOKIO_SHARED_RT.block_on(async {
8707                    let api = create_websocket_api(Some(TimeUnit::Millisecond), None, None);
8708                    let base = "wss://example.com/ws".to_string();
8709                    let got = api.prepare_url(&base);
8710                    assert_eq!(got, format!("{base}?timeUnit=millisecond"));
8711                });
8712            }
8713
8714            #[test]
8715            fn handles_existing_query() {
8716                TOKIO_SHARED_RT.block_on(async {
8717                    let api = create_websocket_api(Some(TimeUnit::Microsecond), None, None);
8718                    let base = "wss://example.com/ws?foo=bar".to_string();
8719                    let got = api.prepare_url(&base);
8720                    assert_eq!(got, format!("{base}&timeUnit=microsecond"));
8721                });
8722            }
8723        }
8724
8725        mod on_open {
8726            use super::*;
8727
8728            fn create_websocket_api_and_conn() -> (Arc<WebsocketApi>, Arc<WebsocketConnection>) {
8729                let sig_gen = SignatureGenerator::new(
8730                    Some("api_secret".to_string()),
8731                    None::<_>,
8732                    None::<String>,
8733                );
8734                let config = ConfigurationWebsocketApi {
8735                    api_key: Some("api_key".to_string()),
8736                    api_secret: Some("api_secret".to_string()),
8737                    private_key: None,
8738                    private_key_passphrase: None,
8739                    ws_url: Some("wss://example".to_string()),
8740                    mode: WebsocketMode::Single,
8741                    reconnect_delay: 0,
8742                    signature_gen: sig_gen,
8743                    timeout: 1000,
8744                    time_unit: None,
8745                    auto_session_relogon: true,
8746                    agent: None,
8747                    handshake: None,
8748                    user_agent: build_user_agent("product"),
8749                    timestamp_provider: None,
8750                };
8751                let conn = WebsocketConnection::new("test-conn");
8752                let api = WebsocketApi::new(config, vec![conn.clone()]);
8753                (api, conn)
8754            }
8755
8756            #[test]
8757            fn session_relogon_on_open() {
8758                TOKIO_SHARED_RT.block_on(async {
8759                    let (api, conn) = create_websocket_api_and_conn();
8760
8761                    let req = WebsocketSessionLogonReq {
8762                        method: "method".into(),
8763                        payload: {
8764                            let mut m = BTreeMap::new();
8765                            m.insert("foo".into(), Value::String("bar".into()));
8766                            m
8767                        },
8768                        options: WebsocketMessageSendOptions {
8769                            with_api_key: true,
8770                            is_signed: true,
8771                            is_session_logon: Some(true),
8772                            ..Default::default()
8773                        },
8774                    };
8775
8776                    let (tx, mut rx) = unbounded_channel::<Message>();
8777                    {
8778                        let mut st = conn.state.lock().await;
8779                        st.session_logon_req = Some(req.clone());
8780                        st.is_session_logged_on = false;
8781                        st.ws_write_tx = Some(tx);
8782                    }
8783
8784                    api.on_open("wss://example".to_string(), conn.clone()).await;
8785
8786                    let Message::Text(raw) = rx.recv().await.unwrap() else {
8787                        panic!("expected a Text message");
8788                    };
8789                    let msg: Value = serde_json::from_str(&raw).unwrap();
8790                    assert_eq!(msg["method"], "method");
8791                    assert_eq!(msg["params"]["foo"], "bar");
8792
8793                    let id = msg["id"].as_str().unwrap().to_string();
8794                    {
8795                        let mut st = conn.state.lock().await;
8796                        let pending = st.pending_requests.remove(&id).expect("pending request");
8797                        pending
8798                            .completion
8799                            .send(Ok(json!({
8800                                "id": id,
8801                                "result": {},
8802                                "rateLimits": []
8803                            })))
8804                            .unwrap();
8805                    }
8806
8807                    sleep(Duration::from_millis(10)).await;
8808
8809                    let st = conn.state.lock().await;
8810                    assert!(st.is_session_logged_on, "should now be logged on");
8811                });
8812            }
8813
8814            #[test]
8815            fn no_relogon_if_already_logged_on() {
8816                TOKIO_SHARED_RT.block_on(async {
8817                    let (api, conn) = create_websocket_api_and_conn();
8818
8819                    let req = WebsocketSessionLogonReq {
8820                        method: "method".into(),
8821                        payload: BTreeMap::new(),
8822                        options: WebsocketMessageSendOptions {
8823                            is_session_logon: Some(true),
8824                            ..Default::default()
8825                        },
8826                    };
8827
8828                    let (tx, mut rx) = unbounded_channel::<Message>();
8829                    {
8830                        let mut st = conn.state.lock().await;
8831                        st.session_logon_req = Some(req);
8832                        st.is_session_logged_on = true;
8833                        st.ws_write_tx = Some(tx);
8834                    }
8835
8836                    api.on_open("wss://example".to_string(), conn.clone()).await;
8837
8838                    assert!(rx.try_recv().is_err(), "no re‐logon when already on");
8839
8840                    let st = conn.state.lock().await;
8841                    assert!(st.is_session_logged_on);
8842                });
8843            }
8844
8845            #[test]
8846            fn session_relogon_fails_gracefully() {
8847                TOKIO_SHARED_RT.block_on(async {
8848                    let (api, conn) = create_websocket_api_and_conn();
8849
8850                    let req = WebsocketSessionLogonReq {
8851                        method: "method".into(),
8852                        payload: {
8853                            let mut m = BTreeMap::new();
8854                            m.insert("x".into(), Value::Number(1.into()));
8855                            m
8856                        },
8857                        options: WebsocketMessageSendOptions {
8858                            is_session_logon: Some(true),
8859                            ..Default::default()
8860                        },
8861                    };
8862                    {
8863                        let mut st = conn.state.lock().await;
8864                        st.session_logon_req = Some(req);
8865                        st.is_session_logged_on = false;
8866                        st.ws_write_tx = None;
8867                    }
8868
8869                    api.on_open("wss://example".into(), conn.clone()).await;
8870
8871                    let st = conn.state.lock().await;
8872                    assert!(
8873                        !st.is_session_logged_on,
8874                        "should remain logged‐off on failure"
8875                    );
8876                });
8877            }
8878
8879            #[test]
8880            fn session_relogon_noop_when_no_req() {
8881                TOKIO_SHARED_RT.block_on(async {
8882                    let (api, conn) = create_websocket_api_and_conn();
8883
8884                    {
8885                        let mut st = conn.state.lock().await;
8886                        st.session_logon_req = None;
8887                        st.is_session_logged_on = false;
8888                        st.ws_write_tx = Some(unbounded_channel::<Message>().0);
8889                    }
8890
8891                    api.on_open("wss://example".into(), conn.clone()).await;
8892
8893                    let st = conn.state.lock().await;
8894                    assert!(!st.is_session_logged_on, "still logged‐off");
8895                });
8896            }
8897        }
8898
8899        mod on_message {
8900            use super::*;
8901
8902            fn create_websocket_api_and_conn() -> (Arc<WebsocketApi>, Arc<WebsocketConnection>) {
8903                let sig_gen = SignatureGenerator::new(
8904                    Some("api_secret".to_string()),
8905                    None::<_>,
8906                    None::<String>,
8907                );
8908                let config = ConfigurationWebsocketApi {
8909                    api_key: Some("api_key".to_string()),
8910                    api_secret: Some("api_secret".to_string()),
8911                    private_key: None,
8912                    private_key_passphrase: None,
8913                    ws_url: Some("wss://example".to_string()),
8914                    mode: WebsocketMode::Single,
8915                    reconnect_delay: 0,
8916                    signature_gen: sig_gen,
8917                    timeout: 1000,
8918                    time_unit: None,
8919                    auto_session_relogon: false,
8920                    agent: None,
8921                    handshake: None,
8922                    user_agent: build_user_agent("product"),
8923                    timestamp_provider: None,
8924                };
8925                let conn = WebsocketConnection::new("test");
8926                let api = WebsocketApi::new(config, vec![conn.clone()]);
8927                (api, conn)
8928            }
8929
8930            #[test]
8931            fn resolves_pending_and_removes_request() {
8932                TOKIO_SHARED_RT.block_on(async {
8933                    let (api, conn) = create_websocket_api_and_conn();
8934                    let (tx, rx) = oneshot::channel();
8935                    {
8936                        let mut st = conn.state.lock().await;
8937                        st.pending_requests
8938                            .insert("id1".to_string(), PendingRequest { completion: tx });
8939                    }
8940                    let msg = json!({"id":"id1","status":200,"foo":"bar"});
8941                    api.on_message(msg.to_string(), conn.clone()).await;
8942                    let got = rx.await.unwrap().unwrap();
8943                    assert_eq!(got, msg);
8944                    let st = conn.state.lock().await;
8945                    assert!(!st.pending_requests.contains_key("id1"));
8946                });
8947            }
8948
8949            #[test]
8950            fn uses_result_when_present() {
8951                TOKIO_SHARED_RT.block_on(async {
8952                    let (api, conn) = create_websocket_api_and_conn();
8953                    let (tx, rx) = oneshot::channel();
8954                    {
8955                        let mut st = conn.state.lock().await;
8956                        st.pending_requests
8957                            .insert("id1".to_string(), PendingRequest { completion: tx });
8958                    }
8959                    let msg = json!({
8960                        "id": "id1",
8961                        "status": 200,
8962                        "response": [1,2],
8963                        "result": {"a":1}
8964                    });
8965                    api.on_message(msg.to_string(), conn.clone()).await;
8966                    let got = rx.await.unwrap().unwrap();
8967                    assert_eq!(got.get("result").unwrap(), &json!({"a":1}));
8968                });
8969            }
8970
8971            #[test]
8972            fn uses_response_when_no_result() {
8973                TOKIO_SHARED_RT.block_on(async {
8974                    let (api, conn) = create_websocket_api_and_conn();
8975                    let (tx, rx) = oneshot::channel();
8976                    {
8977                        let mut st = conn.state.lock().await;
8978                        st.pending_requests
8979                            .insert("id1".to_string(), PendingRequest { completion: tx });
8980                    }
8981                    let msg = json!({
8982                        "id": "id1",
8983                        "status": 200,
8984                        "response": ["ok"]
8985                    });
8986                    api.on_message(msg.to_string(), conn.clone()).await;
8987                    let got = rx.await.unwrap().unwrap();
8988                    assert_eq!(got.get("response").unwrap(), &json!(["ok"]));
8989                });
8990            }
8991
8992            #[test]
8993            fn errors_for_status_ge_400() {
8994                TOKIO_SHARED_RT.block_on(async {
8995                    let (api, conn) = create_websocket_api_and_conn();
8996                    let (tx, rx) = oneshot::channel();
8997                    {
8998                        let mut st = conn.state.lock().await;
8999                        st.pending_requests
9000                            .insert("bad".to_string(), PendingRequest { completion: tx });
9001                    }
9002                    let err_obj = json!({"code":123,"msg":"oops"});
9003                    let msg = json!({"id":"bad","status":500,"error":err_obj});
9004                    api.on_message(msg.to_string(), conn.clone()).await;
9005                    match rx.await.unwrap() {
9006                        Err(WebsocketError::ResponseError {
9007                            code,
9008                            message,
9009                            retry_after,
9010                        }) => {
9011                            assert_eq!(code, 123);
9012                            assert_eq!(message, "oops");
9013                            assert_eq!(retry_after, None);
9014                        }
9015                        other => panic!("expected ResponseError, got {other:?}"),
9016                    }
9017                    let st = conn.state.lock().await;
9018                    assert!(!st.pending_requests.contains_key("bad"));
9019                });
9020            }
9021
9022            #[test]
9023            fn ignores_unknown_id() {
9024                TOKIO_SHARED_RT.block_on(async {
9025                    let (api, conn) = create_websocket_api_and_conn();
9026                    let msg = json!({"id":"nope","status":200});
9027                    api.on_message(msg.to_string(), conn.clone()).await;
9028                    let st = conn.state.lock().await;
9029                    assert!(st.pending_requests.is_empty());
9030                });
9031            }
9032
9033            #[test]
9034            fn parse_error_ignored() {
9035                TOKIO_SHARED_RT.block_on(async {
9036                    let (api, conn) = create_websocket_api_and_conn();
9037                    api.on_message("not json".to_string(), conn.clone()).await;
9038                    let st = conn.state.lock().await;
9039                    assert!(st.pending_requests.is_empty());
9040                });
9041            }
9042
9043            #[test]
9044            fn error_status_sends_error() {
9045                TOKIO_SHARED_RT.block_on(async {
9046                    let (api, conn) = create_websocket_api_and_conn();
9047                    let (tx, rx) = oneshot::channel();
9048                    {
9049                        let mut st = conn.state.lock().await;
9050                        st.pending_requests
9051                            .insert("err".to_string(), PendingRequest { completion: tx });
9052                    }
9053                    let msg = json!({
9054                        "id": "err",
9055                        "status": 500,
9056                        "error": { "code": 42, "msg": "Bad!" }
9057                    });
9058                    api.on_message(msg.to_string(), conn.clone()).await;
9059                    match rx.await.unwrap() {
9060                        Err(WebsocketError::ResponseError {
9061                            code,
9062                            message,
9063                            retry_after,
9064                        }) => {
9065                            assert_eq!(code, 42);
9066                            assert_eq!(message, "Bad!");
9067                            assert_eq!(retry_after, None);
9068                        }
9069                        other => panic!("expected ResponseError, got {other:?}"),
9070                    }
9071                });
9072            }
9073
9074            #[test]
9075            fn rate_limit_error_exposes_retry_after_from_error_data() {
9076                TOKIO_SHARED_RT.block_on(async {
9077                    let (api, conn) = create_websocket_api_and_conn();
9078                    let (tx, rx) = oneshot::channel();
9079                    {
9080                        let mut st = conn.state.lock().await;
9081                        st.pending_requests
9082                            .insert("ban".to_string(), PendingRequest { completion: tx });
9083                    }
9084                    let msg = json!({
9085                        "id": "ban",
9086                        "status": 418,
9087                        "error": {
9088                            "code": -1003,
9089                            "msg": "Way too much request weight used; IP banned until 1659146400000.",
9090                            "data": { "serverTime": 1659142907531_i64, "retryAfter": 1659146400000_i64 }
9091                        }
9092                    });
9093                    api.on_message(msg.to_string(), conn.clone()).await;
9094                    match rx.await.unwrap() {
9095                        Err(WebsocketError::ResponseError {
9096                            code, retry_after, ..
9097                        }) => {
9098                            assert_eq!(code, -1003);
9099                            assert_eq!(retry_after, Some(1_659_146_400_000));
9100                        }
9101                        other => panic!("expected ResponseError, got {other:?}"),
9102                    }
9103                });
9104            }
9105
9106            #[test]
9107            fn unknown_id_logs_warning_and_leaves_pending() {
9108                TOKIO_SHARED_RT.block_on(async {
9109                    let (api, conn) = create_websocket_api_and_conn();
9110                    {
9111                        let mut st = conn.state.lock().await;
9112                        st.pending_requests.insert(
9113                            "keep".to_string(),
9114                            PendingRequest {
9115                                completion: oneshot::channel().0,
9116                            },
9117                        );
9118                    }
9119                    api.on_message(
9120                        json!({ "id": "foo", "status": 200, "result": 1 }).to_string(),
9121                        conn.clone(),
9122                    )
9123                    .await;
9124                    let st = conn.state.lock().await;
9125                    assert!(st.pending_requests.contains_key("keep"));
9126                });
9127            }
9128
9129            #[test]
9130            fn server_shutdown_enqueues_reconnect() {
9131                TOKIO_SHARED_RT.block_on(async {
9132                    let (api, conn) = create_websocket_api_and_conn();
9133
9134                    let msg = json!({
9135                        "event": { "e": "serverShutdown" }
9136                    });
9137
9138                    api.on_message(msg.to_string(), conn.clone()).await;
9139
9140                    let st = conn.state.lock().await;
9141                    assert!(st.renewal_pending);
9142                });
9143            }
9144
9145            #[test]
9146            fn server_shutdown_ignored_if_renewal_pending() {
9147                TOKIO_SHARED_RT.block_on(async {
9148                    let (api, conn) = create_websocket_api_and_conn();
9149
9150                    {
9151                        let mut st = conn.state.lock().await;
9152                        st.renewal_pending = true;
9153                    }
9154
9155                    let msg = json!({
9156                        "event": { "e": "serverShutdown" }
9157                    });
9158
9159                    api.on_message(msg.to_string(), conn.clone()).await;
9160
9161                    let st = conn.state.lock().await;
9162
9163                    assert!(st.renewal_pending);
9164                });
9165            }
9166
9167            #[test]
9168            fn server_shutdown_ignored_if_close_initiated() {
9169                TOKIO_SHARED_RT.block_on(async {
9170                    let (api, conn) = create_websocket_api_and_conn();
9171
9172                    {
9173                        let mut st = conn.state.lock().await;
9174                        st.close_initiated = true;
9175                    }
9176
9177                    let msg = json!({
9178                        "event": { "e": "serverShutdown" }
9179                    });
9180
9181                    api.on_message(msg.to_string(), conn.clone()).await;
9182
9183                    let st = conn.state.lock().await;
9184
9185                    assert!(!st.renewal_pending);
9186                });
9187            }
9188
9189            #[test]
9190            fn server_shutdown_does_not_touch_pending_requests() {
9191                TOKIO_SHARED_RT.block_on(async {
9192                    let (api, conn) = create_websocket_api_and_conn();
9193
9194                    {
9195                        let mut st = conn.state.lock().await;
9196                        st.pending_requests.insert(
9197                            "keep".to_string(),
9198                            PendingRequest {
9199                                completion: oneshot::channel().0,
9200                            },
9201                        );
9202                    }
9203
9204                    let msg = json!({
9205                        "event": { "e": "serverShutdown" }
9206                    });
9207
9208                    api.on_message(msg.to_string(), conn.clone()).await;
9209
9210                    let st = conn.state.lock().await;
9211
9212                    assert!(st.pending_requests.contains_key("keep"));
9213                    assert!(st.renewal_pending);
9214                });
9215            }
9216        }
9217    }
9218
9219    mod websocket_streams {
9220        use super::*;
9221
9222        mod initialisation {
9223            use super::*;
9224
9225            #[test]
9226            fn new_initializes_fields() {
9227                TOKIO_SHARED_RT.block_on(async {
9228                    let config = ConfigurationWebsocketStreams {
9229                        api_key: None,
9230                        api_secret: None,
9231                        private_key: None,
9232                        private_key_passphrase: None,
9233                        ws_url: Some("wss://example".to_string()),
9234                        mode: WebsocketMode::Pool(2),
9235                        reconnect_delay: 500,
9236                        time_unit: None,
9237                        agent: None,
9238                        handshake: None,
9239                        user_agent: build_user_agent("product"),
9240                        timestamp_provider: None,
9241                    };
9242                    let conn1 = WebsocketConnection::new("c1");
9243                    let conn2 = WebsocketConnection::new("c2");
9244                    let api = WebsocketStreams::new(
9245                        config.clone(),
9246                        vec![conn1.clone(), conn2.clone()],
9247                        vec![],
9248                    );
9249
9250                    assert_eq!(api.common.connection_pool.len(), 2);
9251                    assert!(Arc::ptr_eq(&api.common.connection_pool[0], &conn1));
9252                    assert!(Arc::ptr_eq(&api.common.connection_pool[1], &conn2));
9253                    assert_eq!(api.configuration.ws_url, Some("wss://example".to_string()));
9254                    let flag = api.is_connecting.lock().await;
9255                    assert!(!*flag);
9256                });
9257            }
9258
9259            #[test]
9260            fn new_expands_pool_when_url_paths_present() {
9261                TOKIO_SHARED_RT.block_on(async {
9262                    let config = ConfigurationWebsocketStreams {
9263                        api_key: None,
9264                        api_secret: None,
9265                        private_key: None,
9266                        private_key_passphrase: None,
9267                        ws_url: Some("wss://example".to_string()),
9268                        mode: WebsocketMode::Pool(2),
9269                        reconnect_delay: 500,
9270                        time_unit: None,
9271                        agent: None,
9272                        handshake: None,
9273                        user_agent: build_user_agent("product"),
9274                        timestamp_provider: None,
9275                    };
9276
9277                    let conn1 = WebsocketConnection::new("c1");
9278                    let conn2 = WebsocketConnection::new("c2");
9279
9280                    let api = WebsocketStreams::new(
9281                        config,
9282                        vec![conn1.clone(), conn2.clone()],
9283                        vec!["path1".to_string(), "path2".to_string()],
9284                    );
9285
9286                    assert_eq!(api.common.connection_pool.len(), 4);
9287                    assert!(Arc::ptr_eq(&api.common.connection_pool[0], &conn1));
9288                    assert!(Arc::ptr_eq(&api.common.connection_pool[1], &conn2));
9289                });
9290            }
9291
9292            #[test]
9293            fn new_does_not_expand_pool_when_already_sized_for_url_paths() {
9294                TOKIO_SHARED_RT.block_on(async {
9295                    let config = ConfigurationWebsocketStreams {
9296                        api_key: None,
9297                        api_secret: None,
9298                        private_key: None,
9299                        private_key_passphrase: None,
9300                        ws_url: Some("wss://example".to_string()),
9301                        mode: WebsocketMode::Pool(2),
9302                        reconnect_delay: 500,
9303                        time_unit: None,
9304                        agent: None,
9305                        handshake: None,
9306                        user_agent: build_user_agent("product"),
9307                        timestamp_provider: None,
9308                    };
9309
9310                    let conns = vec![
9311                        WebsocketConnection::new("c1"),
9312                        WebsocketConnection::new("c2"),
9313                        WebsocketConnection::new("c3"),
9314                        WebsocketConnection::new("c4"),
9315                    ];
9316
9317                    let api = WebsocketStreams::new(
9318                        config,
9319                        conns.clone(),
9320                        vec!["path1".to_string(), "path2".to_string()],
9321                    );
9322
9323                    assert_eq!(api.common.connection_pool.len(), 4);
9324                    for (i, c) in conns.iter().enumerate() {
9325                        assert!(Arc::ptr_eq(&api.common.connection_pool[i], c));
9326                    }
9327                });
9328            }
9329        }
9330
9331        mod connect {
9332            use super::*;
9333
9334            #[test]
9335            fn establishes_successfully() {
9336                TOKIO_SHARED_RT.block_on(async {
9337                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9338                    let port = listener.local_addr().unwrap().port();
9339
9340                    tokio::spawn(async move {
9341                        for _ in 0..2 {
9342                            if let Ok((stream, _)) = listener.accept().await {
9343                                let mut ws = accept_async(stream).await.unwrap();
9344                                ws.close(None).await.ok();
9345                            }
9346                        }
9347                    });
9348
9349                    let create_websocket_streams = |ws_url: &str| {
9350                        let c1 = WebsocketConnection::new("c1");
9351                        let c2 = WebsocketConnection::new("c2");
9352                        let config = ConfigurationWebsocketStreams {
9353                            api_key: None,
9354                            api_secret: None,
9355                            private_key: None,
9356                            private_key_passphrase: None,
9357                            ws_url: Some(ws_url.to_string()),
9358                            mode: WebsocketMode::Pool(2),
9359                            reconnect_delay: 500,
9360                            time_unit: None,
9361                            agent: None,
9362                            handshake: None,
9363                            user_agent: build_user_agent("product"),
9364                            timestamp_provider: None,
9365                        };
9366                        WebsocketStreams::new(config, vec![c1, c2], vec![])
9367                    };
9368
9369                    let url = format!("ws://127.0.0.1:{port}");
9370                    let ws = create_websocket_streams(&url);
9371
9372                    let res = ws.connect(vec!["stream1".into()]).await;
9373                    assert!(res.is_ok());
9374                });
9375            }
9376
9377            #[test]
9378            fn establishes_successfully_with_url_paths() {
9379                TOKIO_SHARED_RT.block_on(async {
9380                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9381                    let addr = listener.local_addr().unwrap();
9382
9383                    tokio::spawn(async move {
9384                        for _ in 0..4 {
9385                            if let Ok((stream, _)) = listener.accept().await {
9386                                let mut ws = accept_async(stream).await.unwrap();
9387                                ws.close(None).await.ok();
9388                            }
9389                        }
9390                    });
9391
9392                    let config = ConfigurationWebsocketStreams {
9393                        api_key: None,
9394                        api_secret: None,
9395                        private_key: None,
9396                        private_key_passphrase: None,
9397                        ws_url: Some(format!("ws://{}", addr)),
9398                        mode: WebsocketMode::Pool(2),
9399                        reconnect_delay: 500,
9400                        time_unit: None,
9401                        agent: None,
9402                        handshake: None,
9403                        user_agent: build_user_agent("product"),
9404                        timestamp_provider: None,
9405                    };
9406
9407                    let ws = WebsocketStreams::new(
9408                        config,
9409                        vec![],
9410                        vec!["path1".to_string(), "path2".to_string()],
9411                    );
9412
9413                    let res = ws.clone().connect(vec!["stream1".into()]).await;
9414                    assert!(res.is_ok());
9415                    assert_eq!(ws.common.connection_pool.len(), 4);
9416                });
9417            }
9418
9419            #[test]
9420            fn connect_sets_url_path_on_connections_when_url_paths_present() {
9421                TOKIO_SHARED_RT.block_on(async {
9422                    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9423                    let addr = listener.local_addr().unwrap();
9424
9425                    tokio::spawn(async move {
9426                        for _ in 0..4 {
9427                            if let Ok((stream, _)) = listener.accept().await {
9428                                let mut ws = accept_async(stream).await.unwrap();
9429                                ws.close(None).await.ok();
9430                            }
9431                        }
9432                    });
9433
9434                    let config = ConfigurationWebsocketStreams {
9435                        api_key: None,
9436                        api_secret: None,
9437                        private_key: None,
9438                        private_key_passphrase: None,
9439                        ws_url: Some(format!("ws://{}", addr)),
9440                        mode: WebsocketMode::Pool(2),
9441                        reconnect_delay: 500,
9442                        time_unit: None,
9443                        agent: None,
9444                        handshake: None,
9445                        user_agent: build_user_agent("product"),
9446                        timestamp_provider: None,
9447                    };
9448
9449                    let ws = WebsocketStreams::new(
9450                        config,
9451                        vec![],
9452                        vec!["path1".to_string(), "path2".to_string()],
9453                    );
9454
9455                    ws.clone().connect(vec!["stream1".into()]).await.unwrap();
9456
9457                    let pool_size = ws.configuration.mode.pool_size();
9458
9459                    for (i, conn) in ws.common.connection_pool.iter().enumerate() {
9460                        let expected = if i < pool_size { "path1" } else { "path2" };
9461                        let st = conn.state.lock().await;
9462                        assert_eq!(st.url_path.as_deref(), Some(expected));
9463                    }
9464                });
9465            }
9466
9467            #[test]
9468            fn refused_returns_error() {
9469                TOKIO_SHARED_RT.block_on(async {
9470                    let ws = create_websocket_streams(Some("ws://127.0.0.1:9"), None, None);
9471                    let res = ws.connect(vec!["stream1".into()]).await;
9472                    assert!(res.is_err());
9473                });
9474            }
9475
9476            #[test]
9477            fn invalid_url_returns_error() {
9478                TOKIO_SHARED_RT.block_on(async {
9479                    let ws = create_websocket_streams(Some("not-a-url"), None, None);
9480                    let res = ws.connect(vec!["s".into()]).await;
9481                    assert!(res.is_err());
9482                });
9483            }
9484        }
9485
9486        mod disconnect {
9487            use super::*;
9488
9489            #[test]
9490            fn disconnect_clears_state_and_streams() {
9491                TOKIO_SHARED_RT.block_on(async {
9492                    let ws = create_websocket_streams(None, None, None);
9493                    let conn = &ws.common.connection_pool[0];
9494                    {
9495                        let mut state = conn.state.lock().await;
9496                        state.stream_callbacks.insert("s1".to_string(), Vec::new());
9497                        state.pending_subscriptions.push_back("s2".to_string());
9498                    }
9499                    {
9500                        let mut map = ws.connection_streams.lock().await;
9501                        map.insert("s3".to_string(), Arc::clone(conn));
9502                    }
9503
9504                    let res = ws.disconnect().await;
9505                    assert!(res.is_ok());
9506
9507                    let state = conn.state.lock().await;
9508                    assert!(state.stream_callbacks.is_empty());
9509                    assert!(state.pending_subscriptions.is_empty());
9510
9511                    let map = ws.connection_streams.lock().await;
9512                    assert!(map.is_empty());
9513                });
9514            }
9515        }
9516
9517        mod subscribe {
9518            use super::*;
9519
9520            #[test]
9521            fn empty_list_does_nothing() {
9522                TOKIO_SHARED_RT.block_on(async {
9523                    let ws = create_websocket_streams(None, None, None);
9524                    ws.clone().subscribe(Vec::new(), None, None).await;
9525                    let map = ws.connection_streams.lock().await;
9526                    assert!(map.is_empty());
9527                });
9528            }
9529
9530            #[test]
9531            fn queue_when_not_ready() {
9532                TOKIO_SHARED_RT.block_on(async {
9533                    let ws = create_websocket_streams(None, None, None);
9534                    let conn = ws.common.connection_pool[0].clone();
9535                    ws.clone().subscribe(vec!["s1".into()], None, None).await;
9536                    let state = conn.state.lock().await;
9537                    let pending: Vec<String> =
9538                        state.pending_subscriptions.iter().cloned().collect();
9539                    assert_eq!(pending, vec!["s1".to_string()]);
9540                });
9541            }
9542
9543            #[test]
9544            fn only_one_subscription_per_stream() {
9545                TOKIO_SHARED_RT.block_on(async {
9546                    let ws = create_websocket_streams(None, None, None);
9547                    let conn = ws.common.connection_pool[0].clone();
9548                    ws.clone().subscribe(vec!["s1".into()], None, None).await;
9549                    ws.clone().subscribe(vec!["s1".into()], None, None).await;
9550                    let state = conn.state.lock().await;
9551                    let pending: Vec<String> =
9552                        state.pending_subscriptions.iter().cloned().collect();
9553                    assert_eq!(pending, vec!["s1".to_string()]);
9554                });
9555            }
9556
9557            #[test]
9558            fn multiple_streams_assigned() {
9559                TOKIO_SHARED_RT.block_on(async {
9560                    let ws = create_websocket_streams(None, None, None);
9561                    ws.clone()
9562                        .subscribe(vec!["s1".into(), "s2".into()], None, None)
9563                        .await;
9564                    let map = ws.connection_streams.lock().await;
9565                    assert!(map.contains_key("s1"));
9566                    assert!(map.contains_key("s2"));
9567                });
9568            }
9569
9570            #[test]
9571            fn existing_stream_not_reassigned() {
9572                TOKIO_SHARED_RT.block_on(async {
9573                    let ws = create_websocket_streams(None, None, None);
9574                    ws.clone().subscribe(vec!["s1".into()], None, None).await;
9575                    let first_id = {
9576                        let map = ws.connection_streams.lock().await;
9577                        map.get("s1").unwrap().id.clone()
9578                    };
9579                    ws.clone()
9580                        .subscribe(vec!["s1".into(), "s2".into()], None, None)
9581                        .await;
9582                    let map = ws.connection_streams.lock().await;
9583                    let second_id = map.get("s1").unwrap().id.clone();
9584                    assert_eq!(first_id, second_id);
9585                    assert!(map.contains_key("s2"));
9586                });
9587            }
9588
9589            #[test]
9590            fn queue_when_not_ready_with_url_path() {
9591                TOKIO_SHARED_RT.block_on(async {
9592                    let ws = create_websocket_streams(None, None, None);
9593
9594                    let conn = ws.common.connection_pool[0].clone();
9595                    {
9596                        let mut st = conn.state.lock().await;
9597                        st.ws_write_tx = None;
9598                        st.url_path = Some("path1".to_string());
9599                        st.reconnection_pending = false;
9600                        st.close_initiated = false;
9601                    }
9602
9603                    ws.clone()
9604                        .subscribe(vec!["s1".into()], None, Some("path1"))
9605                        .await;
9606
9607                    let state = conn.state.lock().await;
9608                    let pending: Vec<String> =
9609                        state.pending_subscriptions.iter().cloned().collect();
9610                    assert_eq!(pending, vec!["s1".to_string()]);
9611                });
9612            }
9613
9614            #[test]
9615            fn only_one_subscription_per_stream_per_url_path() {
9616                TOKIO_SHARED_RT.block_on(async {
9617                    let ws = create_websocket_streams(None, None, None);
9618
9619                    let conn = ws.common.connection_pool[0].clone();
9620                    {
9621                        let mut st = conn.state.lock().await;
9622                        st.ws_write_tx = None;
9623                        st.url_path = Some("path1".to_string());
9624                        st.reconnection_pending = false;
9625                        st.close_initiated = false;
9626                    }
9627
9628                    ws.clone()
9629                        .subscribe(vec!["s1".into()], None, Some("path1"))
9630                        .await;
9631                    ws.clone()
9632                        .subscribe(vec!["s1".into()], None, Some("path1"))
9633                        .await;
9634
9635                    let state = conn.state.lock().await;
9636                    let pending: Vec<String> =
9637                        state.pending_subscriptions.iter().cloned().collect();
9638                    assert_eq!(pending, vec!["s1".to_string()]);
9639                });
9640            }
9641
9642            #[test]
9643            fn same_stream_can_be_subscribed_on_different_url_paths() {
9644                TOKIO_SHARED_RT.block_on(async {
9645                    let ws = create_websocket_streams(None, None, None);
9646
9647                    let conn1 = ws.common.connection_pool[0].clone();
9648                    let conn2 = ws.common.connection_pool[1].clone();
9649
9650                    {
9651                        let mut st1 = conn1.state.lock().await;
9652                        st1.ws_write_tx = None;
9653                        st1.url_path = Some("path1".to_string());
9654                        st1.reconnection_pending = false;
9655                        st1.close_initiated = false;
9656                    }
9657                    {
9658                        let mut st2 = conn2.state.lock().await;
9659                        st2.ws_write_tx = None;
9660                        st2.url_path = Some("path2".to_string());
9661                        st2.reconnection_pending = false;
9662                        st2.close_initiated = false;
9663                    }
9664
9665                    ws.clone()
9666                        .subscribe(vec!["s1".into()], None, Some("path1"))
9667                        .await;
9668                    ws.clone()
9669                        .subscribe(vec!["s1".into()], None, Some("path2"))
9670                        .await;
9671
9672                    let map = ws.connection_streams.lock().await;
9673                    assert!(map.contains_key("path1::s1"));
9674                    assert!(map.contains_key("path2::s1"));
9675                });
9676            }
9677        }
9678
9679        mod unsubscribe {
9680            use super::*;
9681
9682            #[test]
9683            fn removes_stream_with_no_callbacks() {
9684                TOKIO_SHARED_RT.block_on(async {
9685                    let ws = create_websocket_streams(None, None, None);
9686                    let conn = ws.common.connection_pool[0].clone();
9687
9688                    {
9689                        let (tx, _rx) = unbounded_channel::<Message>();
9690                        let mut st = conn.state.lock().await;
9691                        st.ws_write_tx = Some(tx);
9692                    }
9693
9694                    {
9695                        let mut map = ws.connection_streams.lock().await;
9696                        map.insert("s1".to_string(), conn.clone());
9697                    }
9698                    {
9699                        let mut st = conn.state.lock().await;
9700                        st.stream_callbacks.insert("s1".to_string(), Vec::new());
9701                    }
9702
9703                    ws.unsubscribe(vec!["s1".to_string()], None, None).await;
9704
9705                    assert!(!ws.connection_streams.lock().await.contains_key("s1"));
9706                    assert!(!conn.state.lock().await.stream_callbacks.contains_key("s1"));
9707                });
9708            }
9709
9710            #[test]
9711            fn preserves_stream_with_callbacks() {
9712                TOKIO_SHARED_RT.block_on(async {
9713                    let ws = create_websocket_streams(None, None, None);
9714                    let conn = ws.common.connection_pool[1].clone();
9715
9716                    {
9717                        let mut map = ws.connection_streams.lock().await;
9718                        map.insert("s2".to_string(), conn.clone());
9719                    }
9720                    {
9721                        let mut state = conn.state.lock().await;
9722                        state
9723                            .stream_callbacks
9724                            .insert("s2".to_string(), vec![Arc::new(|_: &Value| {})]);
9725                    }
9726
9727                    ws.unsubscribe(vec!["s2".to_string()], None, None).await;
9728
9729                    assert!(ws.connection_streams.lock().await.contains_key("s2"));
9730                    assert!(conn.state.lock().await.stream_callbacks.contains_key("s2"));
9731                });
9732            }
9733
9734            #[test]
9735            fn does_not_send_if_callbacks_exist() {
9736                TOKIO_SHARED_RT.block_on(async {
9737                    let ws = create_websocket_streams(None, None, None);
9738                    let conn = ws.common.connection_pool[0].clone();
9739                    {
9740                        let mut map = ws.connection_streams.lock().await;
9741                        map.insert("s1".to_string(), conn.clone());
9742                    }
9743                    {
9744                        let mut state = conn.state.lock().await;
9745                        state.stream_callbacks.insert(
9746                            "s1".to_string(),
9747                            vec![Arc::new(|_: &Value| {}), Arc::new(|_: &Value| {})],
9748                        );
9749                    }
9750                    ws.unsubscribe(vec!["s1".into()], None, None).await;
9751                    assert!(ws.connection_streams.lock().await.contains_key("s1"));
9752                    assert!(conn.state.lock().await.stream_callbacks.contains_key("s1"));
9753                });
9754            }
9755
9756            #[test]
9757            fn warns_if_not_associated() {
9758                TOKIO_SHARED_RT.block_on(async {
9759                    let ws = create_websocket_streams(None, None, None);
9760                    ws.unsubscribe(vec!["nope".into()], None, None).await;
9761                });
9762            }
9763
9764            #[test]
9765            fn empty_list_does_nothing() {
9766                TOKIO_SHARED_RT.block_on(async {
9767                    let ws = create_websocket_streams(None, None, None);
9768                    let before = ws.connection_streams.lock().await.len();
9769                    ws.unsubscribe(Vec::<String>::new(), None, None).await;
9770                    let after = ws.connection_streams.lock().await.len();
9771                    assert_eq!(before, after);
9772                });
9773            }
9774
9775            #[test]
9776            fn invalid_custom_id_falls_back() {
9777                TOKIO_SHARED_RT.block_on(async {
9778                    let ws = create_websocket_streams(None, None, None);
9779                    let conn = ws.common.connection_pool[0].clone();
9780                    {
9781                        let mut map = ws.connection_streams.lock().await;
9782                        map.insert("foo".to_string(), conn.clone());
9783                    }
9784                    {
9785                        let mut state = conn.state.lock().await;
9786                        let (tx, _rx) = unbounded_channel();
9787                        state.ws_write_tx = Some(tx);
9788                        state.stream_callbacks.insert("foo".to_string(), Vec::new());
9789                    }
9790                    ws.unsubscribe(
9791                        vec!["foo".into()],
9792                        Some(StreamId::Str("bad-id".into())),
9793                        None,
9794                    )
9795                    .await;
9796                    assert!(!ws.connection_streams.lock().await.contains_key("foo"));
9797                });
9798            }
9799
9800            #[test]
9801            fn removes_even_without_write_channel() {
9802                TOKIO_SHARED_RT.block_on(async {
9803                    let ws = create_websocket_streams(None, None, None);
9804                    let conn = ws.common.connection_pool[0].clone();
9805                    {
9806                        let mut map = ws.connection_streams.lock().await;
9807                        map.insert("x".to_string(), conn.clone());
9808                    }
9809                    {
9810                        let mut state = conn.state.lock().await;
9811                        let (tx, _rx) = unbounded_channel();
9812                        state.ws_write_tx = Some(tx);
9813                        state.stream_callbacks.insert("x".to_string(), Vec::new());
9814                    }
9815                    ws.unsubscribe(vec!["x".into()], None, None).await;
9816                    assert!(!ws.connection_streams.lock().await.contains_key("x"));
9817                });
9818            }
9819
9820            #[test]
9821            fn removes_stream_with_no_callbacks_with_url_path() {
9822                TOKIO_SHARED_RT.block_on(async {
9823                    let ws = create_websocket_streams(None, None, None);
9824                    let conn = ws.common.connection_pool[0].clone();
9825
9826                    {
9827                        let (tx, _rx) = unbounded_channel::<Message>();
9828                        let mut st = conn.state.lock().await;
9829                        st.ws_write_tx = Some(tx);
9830                        st.url_path = Some("path1".to_string());
9831                    }
9832
9833                    {
9834                        let mut map = ws.connection_streams.lock().await;
9835                        map.insert("path1::s1".to_string(), conn.clone());
9836                    }
9837                    {
9838                        let mut st = conn.state.lock().await;
9839                        st.stream_callbacks
9840                            .insert("path1::s1".to_string(), Vec::new());
9841                    }
9842
9843                    ws.unsubscribe(vec!["s1".to_string()], None, Some("path1"))
9844                        .await;
9845
9846                    assert!(!ws.connection_streams.lock().await.contains_key("path1::s1"));
9847                    assert!(
9848                        !conn
9849                            .state
9850                            .lock()
9851                            .await
9852                            .stream_callbacks
9853                            .contains_key("path1::s1")
9854                    );
9855                });
9856            }
9857
9858            #[test]
9859            fn preserves_stream_with_callbacks_with_url_path() {
9860                TOKIO_SHARED_RT.block_on(async {
9861                    let ws = create_websocket_streams(None, None, None);
9862                    let conn = ws.common.connection_pool[0].clone();
9863
9864                    {
9865                        let (tx, _rx) = unbounded_channel::<Message>();
9866                        let mut st = conn.state.lock().await;
9867                        st.ws_write_tx = Some(tx);
9868                        st.url_path = Some("path1".to_string());
9869                    }
9870
9871                    {
9872                        let mut map = ws.connection_streams.lock().await;
9873                        map.insert("path1::s2".to_string(), conn.clone());
9874                    }
9875                    {
9876                        let mut state = conn.state.lock().await;
9877                        state
9878                            .stream_callbacks
9879                            .insert("path1::s2".to_string(), vec![Arc::new(|_: &Value| {})]);
9880                    }
9881
9882                    ws.unsubscribe(vec!["s2".to_string()], None, Some("path1"))
9883                        .await;
9884
9885                    assert!(ws.connection_streams.lock().await.contains_key("path1::s2"));
9886                    assert!(
9887                        conn.state
9888                            .lock()
9889                            .await
9890                            .stream_callbacks
9891                            .contains_key("path1::s2")
9892                    );
9893                });
9894            }
9895
9896            #[test]
9897            fn url_path_mismatch_does_not_remove_other_path_subscription() {
9898                TOKIO_SHARED_RT.block_on(async {
9899                    let ws = create_websocket_streams(None, None, None);
9900                    let conn = ws.common.connection_pool[0].clone();
9901
9902                    {
9903                        let (tx, _rx) = unbounded_channel::<Message>();
9904                        let mut st = conn.state.lock().await;
9905                        st.ws_write_tx = Some(tx);
9906                        st.url_path = Some("path1".to_string());
9907                    }
9908
9909                    {
9910                        let mut map = ws.connection_streams.lock().await;
9911                        map.insert("path1::s1".to_string(), conn.clone());
9912                    }
9913                    {
9914                        let mut st = conn.state.lock().await;
9915                        st.stream_callbacks
9916                            .insert("path1::s1".to_string(), Vec::new());
9917                    }
9918
9919                    ws.unsubscribe(vec!["s1".to_string()], None, Some("path2"))
9920                        .await;
9921
9922                    assert!(ws.connection_streams.lock().await.contains_key("path1::s1"));
9923                    assert!(
9924                        conn.state
9925                            .lock()
9926                            .await
9927                            .stream_callbacks
9928                            .contains_key("path1::s1")
9929                    );
9930                });
9931            }
9932        }
9933
9934        mod is_subscribed {
9935            use super::*;
9936
9937            #[test]
9938            fn returns_false_when_not_subscribed() {
9939                TOKIO_SHARED_RT.block_on(async {
9940                    let ws = create_websocket_streams(None, None, None);
9941                    assert!(!ws.is_subscribed("unknown").await);
9942                });
9943            }
9944
9945            #[test]
9946            fn returns_true_when_subscribed() {
9947                TOKIO_SHARED_RT.block_on(async {
9948                    let ws = create_websocket_streams(None, None, None);
9949                    let conn = ws.common.connection_pool[0].clone();
9950                    {
9951                        let mut map = ws.connection_streams.lock().await;
9952                        map.insert("stream1".to_string(), conn);
9953                    }
9954                    assert!(ws.is_subscribed("stream1").await);
9955                });
9956            }
9957
9958            #[test]
9959            fn returns_true_when_subscribed_with_url_path_key() {
9960                TOKIO_SHARED_RT.block_on(async {
9961                    let ws = create_websocket_streams(None, None, None);
9962                    let conn = ws.common.connection_pool[0].clone();
9963                    {
9964                        let mut map = ws.connection_streams.lock().await;
9965                        map.insert("path1::stream1".to_string(), conn);
9966                    }
9967                    assert!(ws.is_subscribed("stream1").await);
9968                });
9969            }
9970
9971            #[test]
9972            fn returns_true_when_same_stream_subscribed_on_multiple_paths() {
9973                TOKIO_SHARED_RT.block_on(async {
9974                    let ws = create_websocket_streams(None, None, None);
9975                    let conn1 = ws.common.connection_pool[0].clone();
9976                    let conn2 = ws.common.connection_pool[1].clone();
9977                    {
9978                        let mut map = ws.connection_streams.lock().await;
9979                        map.insert("path1::stream1".to_string(), conn1);
9980                        map.insert("path2::stream1".to_string(), conn2);
9981                    }
9982                    assert!(ws.is_subscribed("stream1").await);
9983                });
9984            }
9985
9986            #[test]
9987            fn returns_false_when_only_similar_suffix_exists() {
9988                TOKIO_SHARED_RT.block_on(async {
9989                    let ws = create_websocket_streams(None, None, None);
9990                    let conn = ws.common.connection_pool[0].clone();
9991                    {
9992                        let mut map = ws.connection_streams.lock().await;
9993                        map.insert("path1::stream10".to_string(), conn);
9994                    }
9995                    assert!(!ws.is_subscribed("stream1").await);
9996                });
9997            }
9998        }
9999
10000        mod stream_key {
10001            use super::*;
10002
10003            #[test]
10004            fn stream_key_without_url_path_returns_stream() {
10005                TOKIO_SHARED_RT.block_on(async {
10006                    let ws = create_websocket_streams(None, None, None);
10007                    assert_eq!(ws.stream_key("s1", None), "s1");
10008                });
10009            }
10010
10011            #[test]
10012            fn stream_key_with_empty_url_path_returns_stream() {
10013                TOKIO_SHARED_RT.block_on(async {
10014                    let ws = create_websocket_streams(None, None, None);
10015                    assert_eq!(ws.stream_key("s1", Some("")), "s1");
10016                });
10017            }
10018
10019            #[test]
10020            fn stream_key_with_url_path_prefixes_stream() {
10021                TOKIO_SHARED_RT.block_on(async {
10022                    let ws = create_websocket_streams(None, None, None);
10023                    assert_eq!(ws.stream_key("s1", Some("path1")), "path1::s1");
10024                });
10025            }
10026
10027            #[test]
10028            fn stream_key_distinguishes_paths() {
10029                TOKIO_SHARED_RT.block_on(async {
10030                    let ws = create_websocket_streams(None, None, None);
10031                    assert_eq!(ws.stream_key("s1", Some("path1")), "path1::s1");
10032                    assert_eq!(ws.stream_key("s1", Some("path2")), "path2::s1");
10033                });
10034            }
10035        }
10036
10037        mod prepare_url {
10038            use super::*;
10039
10040            #[test]
10041            fn without_time_unit_returns_base_url() {
10042                TOKIO_SHARED_RT.block_on(async {
10043                    let conns = vec![
10044                        WebsocketConnection::new("c1"),
10045                        WebsocketConnection::new("c2"),
10046                    ];
10047                    let config = ConfigurationWebsocketStreams {
10048                        api_key: None,
10049                        api_secret: None,
10050                        private_key: None,
10051                        private_key_passphrase: None,
10052                        ws_url: Some("wss://example".to_string()),
10053                        mode: WebsocketMode::Single,
10054                        reconnect_delay: 100,
10055                        time_unit: None,
10056                        agent: None,
10057                        handshake: None,
10058                        user_agent: build_user_agent("product"),
10059                        timestamp_provider: None,
10060                    };
10061                    let ws = WebsocketStreams::new(config, conns, vec![]);
10062                    let url = ws.prepare_url(&["s1".into(), "s2".into()], None);
10063                    assert_eq!(url, "wss://example/stream?streams=s1/s2");
10064                });
10065            }
10066
10067            #[test]
10068            fn with_time_unit_appends_parameter() {
10069                TOKIO_SHARED_RT.block_on(async {
10070                    let conns = vec![WebsocketConnection::new("c1")];
10071                    let config = ConfigurationWebsocketStreams {
10072                        api_key: None,
10073                        api_secret: None,
10074                        private_key: None,
10075                        private_key_passphrase: None,
10076                        ws_url: Some("wss://example".to_string()),
10077                        mode: WebsocketMode::Single,
10078                        reconnect_delay: 100,
10079                        time_unit: Some(TimeUnit::Millisecond),
10080                        agent: None,
10081                        handshake: None,
10082                        user_agent: build_user_agent("product"),
10083                        timestamp_provider: None,
10084                    };
10085                    let ws = WebsocketStreams::new(config, conns, vec![]);
10086                    let url = ws.prepare_url(&["a".into()], None);
10087                    assert_eq!(url, "wss://example/stream?streams=a&timeUnit=millisecond");
10088                });
10089            }
10090
10091            #[test]
10092            fn multiple_streams_and_time_unit() {
10093                TOKIO_SHARED_RT.block_on(async {
10094                    let conns = vec![WebsocketConnection::new("c1")];
10095                    let config = ConfigurationWebsocketStreams {
10096                        api_key: None,
10097                        api_secret: None,
10098                        private_key: None,
10099                        private_key_passphrase: None,
10100                        ws_url: Some("wss://example".to_string()),
10101                        mode: WebsocketMode::Single,
10102                        reconnect_delay: 100,
10103                        time_unit: Some(TimeUnit::Microsecond),
10104                        agent: None,
10105                        handshake: None,
10106                        user_agent: build_user_agent("product"),
10107                        timestamp_provider: None,
10108                    };
10109                    let ws = WebsocketStreams::new(config, conns, vec![]);
10110                    let url = ws.prepare_url(&["x".into(), "y".into(), "z".into()], None);
10111                    assert_eq!(
10112                        url,
10113                        "wss://example/stream?streams=x/y/z&timeUnit=microsecond"
10114                    );
10115                });
10116            }
10117
10118            #[test]
10119            fn with_url_path_prefixes_base_url() {
10120                TOKIO_SHARED_RT.block_on(async {
10121                    let conns = vec![WebsocketConnection::new("c1")];
10122                    let config = ConfigurationWebsocketStreams {
10123                        api_key: None,
10124                        api_secret: None,
10125                        private_key: None,
10126                        private_key_passphrase: None,
10127                        ws_url: Some("wss://example".to_string()),
10128                        mode: WebsocketMode::Single,
10129                        reconnect_delay: 100,
10130                        time_unit: None,
10131                        agent: None,
10132                        handshake: None,
10133                        user_agent: build_user_agent("product"),
10134                        timestamp_provider: None,
10135                    };
10136                    let ws = WebsocketStreams::new(config, conns, vec![]);
10137                    let url = ws.prepare_url(["s1".into()].as_ref(), Some("path1"));
10138                    assert_eq!(url, "wss://example/path1/stream?streams=s1");
10139                });
10140            }
10141
10142            #[test]
10143            fn with_url_path_and_time_unit_appends_parameter() {
10144                TOKIO_SHARED_RT.block_on(async {
10145                    let conns = vec![WebsocketConnection::new("c1")];
10146                    let config = ConfigurationWebsocketStreams {
10147                        api_key: None,
10148                        api_secret: None,
10149                        private_key: None,
10150                        private_key_passphrase: None,
10151                        ws_url: Some("wss://example".to_string()),
10152                        mode: WebsocketMode::Single,
10153                        reconnect_delay: 100,
10154                        time_unit: Some(TimeUnit::Millisecond),
10155                        agent: None,
10156                        handshake: None,
10157                        user_agent: build_user_agent("product"),
10158                        timestamp_provider: None,
10159                    };
10160                    let ws = WebsocketStreams::new(config, conns, vec![]);
10161                    let url = ws.prepare_url(["a".into()].as_ref(), Some("path1"));
10162                    assert_eq!(
10163                        url,
10164                        "wss://example/path1/stream?streams=a&timeUnit=millisecond"
10165                    );
10166                });
10167            }
10168
10169            #[test]
10170            fn url_path_distinguishes_urls_for_same_streams() {
10171                TOKIO_SHARED_RT.block_on(async {
10172                    let conns = vec![WebsocketConnection::new("c1")];
10173                    let config = ConfigurationWebsocketStreams {
10174                        api_key: None,
10175                        api_secret: None,
10176                        private_key: None,
10177                        private_key_passphrase: None,
10178                        ws_url: Some("wss://example".to_string()),
10179                        mode: WebsocketMode::Single,
10180                        reconnect_delay: 100,
10181                        time_unit: None,
10182                        agent: None,
10183                        handshake: None,
10184                        user_agent: build_user_agent("product"),
10185                        timestamp_provider: None,
10186                    };
10187                    let ws = WebsocketStreams::new(config, conns, vec![]);
10188                    let u1 = ws.prepare_url(["s1".into()].as_ref(), Some("path1"));
10189                    let u2 = ws.prepare_url(["s1".into()].as_ref(), Some("path2"));
10190                    assert_eq!(u1, "wss://example/path1/stream?streams=s1");
10191                    assert_eq!(u2, "wss://example/path2/stream?streams=s1");
10192                });
10193            }
10194        }
10195
10196        mod handle_stream_assignment {
10197            use super::*;
10198
10199            #[test]
10200            fn assigns_new_streams_to_connections() {
10201                TOKIO_SHARED_RT.block_on(async {
10202                    let ws = create_websocket_streams(None, None, None);
10203                    let groups = ws
10204                        .clone()
10205                        .handle_stream_assignment(vec!["s1".into(), "s2".into()], None)
10206                        .await;
10207                    let mut seen_streams = HashSet::new();
10208                    for (_conn, streams) in &groups {
10209                        for s in streams {
10210                            seen_streams.insert(s);
10211                        }
10212                    }
10213                    assert_eq!(
10214                        seen_streams,
10215                        ["s1".to_string(), "s2".to_string()].iter().collect()
10216                    );
10217                    assert_eq!(groups.len(), 1);
10218                });
10219            }
10220
10221            #[test]
10222            fn reuses_existing_connection_for_duplicate_stream() {
10223                TOKIO_SHARED_RT.block_on(async {
10224                    let ws = create_websocket_streams(None, None, None);
10225                    let _ = ws
10226                        .clone()
10227                        .handle_stream_assignment(vec!["s1".into()], None)
10228                        .await;
10229                    let groups = ws
10230                        .clone()
10231                        .handle_stream_assignment(vec!["s1".into(), "s3".into()], None)
10232                        .await;
10233                    let mut all_streams = Vec::new();
10234                    for (_conn, streams) in groups {
10235                        all_streams.extend(streams);
10236                    }
10237                    all_streams.sort();
10238                    assert_eq!(all_streams, vec!["s1".to_string(), "s3".to_string()]);
10239                });
10240            }
10241
10242            #[test]
10243            fn empty_stream_list_returns_empty() {
10244                TOKIO_SHARED_RT.block_on(async {
10245                    let ws = create_websocket_streams(None, None, None);
10246                    let groups = ws.clone().handle_stream_assignment(vec![], None).await;
10247                    assert!(groups.is_empty());
10248                });
10249            }
10250
10251            #[test]
10252            fn closed_or_reconnecting_forces_reassignment_of_stream() {
10253                TOKIO_SHARED_RT.block_on(async {
10254                    let ws = create_websocket_streams(None, None, None);
10255                    let mut groups = ws
10256                        .clone()
10257                        .handle_stream_assignment(vec!["s1".into()], None)
10258                        .await;
10259                    let (conn, _) = groups.pop().unwrap();
10260                    {
10261                        let mut st = conn.state.lock().await;
10262                        st.close_initiated = true;
10263                    }
10264                    let groups2 = ws
10265                        .clone()
10266                        .handle_stream_assignment(vec!["s2".into()], None)
10267                        .await;
10268                    assert_eq!(groups2.len(), 1);
10269                    let (_new_conn, streams) = &groups2[0];
10270                    assert_eq!(streams, &vec!["s2".to_string()]);
10271                });
10272            }
10273
10274            #[test]
10275            fn no_available_connections_falls_back_to_one() {
10276                TOKIO_SHARED_RT.block_on(async {
10277                    let ws = create_websocket_streams(None, Some(vec![]), None);
10278                    let assigned = ws.handle_stream_assignment(vec!["foo".into()], None).await;
10279                    assert_eq!(assigned.len(), 1);
10280                    let (_conn, streams) = &assigned[0];
10281                    assert_eq!(streams.as_slice(), &["foo".to_string()]);
10282                });
10283            }
10284
10285            #[test]
10286            fn single_connection_groups_multiple_streams() {
10287                TOKIO_SHARED_RT.block_on(async {
10288                    let conn = WebsocketConnection::new("c1");
10289                    let ws = create_websocket_streams(None, Some(vec![conn.clone()]), None);
10290                    let assigned = ws
10291                        .handle_stream_assignment(vec!["s1".into(), "s2".into()], None)
10292                        .await;
10293                    assert_eq!(assigned.len(), 1);
10294                    let (assigned_conn, streams) = &assigned[0];
10295                    assert!(Arc::ptr_eq(assigned_conn, &conn));
10296                    assert_eq!(streams.len(), 2);
10297                    assert!(streams.contains(&"s1".to_string()));
10298                    assert!(streams.contains(&"s2".to_string()));
10299                });
10300            }
10301
10302            #[test]
10303            fn reuse_existing_healthy_connection() {
10304                TOKIO_SHARED_RT.block_on(async {
10305                    let conn = WebsocketConnection::new("c");
10306                    let ws = create_websocket_streams(None, Some(vec![conn.clone()]), None);
10307                    let _ = ws.handle_stream_assignment(vec!["s1".into()], None).await;
10308                    let second = ws.handle_stream_assignment(vec!["s1".into()], None).await;
10309                    assert_eq!(second.len(), 1);
10310                    let (assigned_conn, streams) = &second[0];
10311                    assert!(Arc::ptr_eq(assigned_conn, &conn));
10312                    assert_eq!(streams.as_slice(), &["s1".to_string()]);
10313                });
10314            }
10315
10316            #[test]
10317            fn mix_new_and_assigned_streams() {
10318                TOKIO_SHARED_RT.block_on(async {
10319                    let conn = WebsocketConnection::new("c");
10320                    let ws = create_websocket_streams(None, Some(vec![conn.clone()]), None);
10321                    let _ = ws
10322                        .handle_stream_assignment(vec!["s1".into(), "s2".into()], None)
10323                        .await;
10324                    let mixed = ws
10325                        .handle_stream_assignment(vec!["s2".into(), "s3".into()], None)
10326                        .await;
10327                    assert_eq!(mixed.len(), 1);
10328                    let (assigned_conn, streams) = &mixed[0];
10329                    assert!(Arc::ptr_eq(assigned_conn, &conn));
10330                    let mut got = streams.clone();
10331                    got.sort();
10332                    assert_eq!(got, vec!["s2".to_string(), "s3".to_string()]);
10333                });
10334            }
10335
10336            #[test]
10337            fn assigns_streams_with_url_path_keys() {
10338                TOKIO_SHARED_RT.block_on(async {
10339                    let ws = create_websocket_streams(None, None, None);
10340
10341                    let conn = ws.common.connection_pool[0].clone();
10342                    {
10343                        let mut st = conn.state.lock().await;
10344                        st.url_path = Some("path1".to_string());
10345                        st.ws_write_tx = None;
10346                        st.reconnection_pending = false;
10347                        st.close_initiated = false;
10348                    }
10349
10350                    let groups = ws
10351                        .handle_stream_assignment(vec!["s1".into(), "s2".into()], Some("path1"))
10352                        .await;
10353
10354                    let map = ws.connection_streams.lock().await;
10355                    assert!(map.contains_key("path1::s1"));
10356                    assert!(map.contains_key("path1::s2"));
10357                    assert_eq!(groups.len(), 1);
10358
10359                    let (_assigned_conn, streams) = &groups[0];
10360                    let mut got = streams.clone();
10361                    got.sort();
10362                    assert_eq!(got, vec!["s1".to_string(), "s2".to_string()]);
10363                });
10364            }
10365
10366            #[test]
10367            fn same_stream_on_different_paths_creates_distinct_keys() {
10368                TOKIO_SHARED_RT.block_on(async {
10369                    let ws = create_websocket_streams(None, None, None);
10370
10371                    let conn1 = ws.common.connection_pool[0].clone();
10372                    let conn2 = ws.common.connection_pool[1].clone();
10373
10374                    {
10375                        let mut st = conn1.state.lock().await;
10376                        st.url_path = Some("path1".to_string());
10377                        st.ws_write_tx = None;
10378                        st.reconnection_pending = false;
10379                        st.close_initiated = false;
10380                    }
10381                    {
10382                        let mut st = conn2.state.lock().await;
10383                        st.url_path = Some("path2".to_string());
10384                        st.ws_write_tx = None;
10385                        st.reconnection_pending = false;
10386                        st.close_initiated = false;
10387                    }
10388
10389                    let g1 = ws
10390                        .handle_stream_assignment(vec!["s1".into()], Some("path1"))
10391                        .await;
10392                    let g2 = ws
10393                        .handle_stream_assignment(vec!["s1".into()], Some("path2"))
10394                        .await;
10395
10396                    assert_eq!(g1.len(), 1);
10397                    assert_eq!(g2.len(), 1);
10398
10399                    let map = ws.connection_streams.lock().await;
10400                    assert!(map.contains_key("path1::s1"));
10401                    assert!(map.contains_key("path2::s1"));
10402                });
10403            }
10404
10405            #[test]
10406            fn reuses_existing_connection_for_same_path_and_stream() {
10407                TOKIO_SHARED_RT.block_on(async {
10408                    let ws = create_websocket_streams(None, None, None);
10409
10410                    let conn = ws.common.connection_pool[0].clone();
10411                    {
10412                        let mut st = conn.state.lock().await;
10413                        st.url_path = Some("path1".to_string());
10414                        st.ws_write_tx = None;
10415                        st.reconnection_pending = false;
10416                        st.close_initiated = false;
10417                    }
10418
10419                    let first = ws
10420                        .handle_stream_assignment(vec!["s1".into()], Some("path1"))
10421                        .await;
10422                    let second = ws
10423                        .handle_stream_assignment(vec!["s1".into(), "s2".into()], Some("path1"))
10424                        .await;
10425
10426                    assert_eq!(first.len(), 1);
10427                    assert_eq!(second.len(), 1);
10428
10429                    let map = ws.connection_streams.lock().await;
10430                    let c1 = map.get("path1::s1").unwrap().clone();
10431                    let c2 = map.get("path1::s2").unwrap().clone();
10432                    assert!(Arc::ptr_eq(&c1, &c2));
10433                });
10434            }
10435
10436            #[test]
10437            fn closed_or_reconnecting_forces_reassignment_with_url_path() {
10438                TOKIO_SHARED_RT.block_on(async {
10439                    let ws = create_websocket_streams(None, None, None);
10440
10441                    let conn1 = ws.common.connection_pool[0].clone();
10442                    let conn2 = ws.common.connection_pool[1].clone();
10443
10444                    {
10445                        let mut st = conn1.state.lock().await;
10446                        st.url_path = Some("path1".to_string());
10447                        st.ws_write_tx = None;
10448                        st.reconnection_pending = false;
10449                        st.close_initiated = false;
10450                    }
10451                    {
10452                        let mut st = conn2.state.lock().await;
10453                        st.url_path = Some("path1".to_string());
10454                        st.ws_write_tx = None;
10455                        st.reconnection_pending = false;
10456                        st.close_initiated = false;
10457                    }
10458
10459                    let _ = ws
10460                        .handle_stream_assignment(vec!["s1".into()], Some("path1"))
10461                        .await;
10462
10463                    {
10464                        let mut st = conn1.state.lock().await;
10465                        st.close_initiated = true;
10466                    }
10467
10468                    let _ = ws
10469                        .handle_stream_assignment(vec!["s1".into()], Some("path1"))
10470                        .await;
10471
10472                    let map = ws.connection_streams.lock().await;
10473                    let assigned = map.get("path1::s1").unwrap().clone();
10474                    assert!(!Arc::ptr_eq(&assigned, &conn1));
10475                });
10476            }
10477        }
10478
10479        mod send_subscription_payload {
10480            use super::*;
10481
10482            #[test]
10483            fn subscribe_payload_with_custom_id_fallbacks_if_invalid() {
10484                TOKIO_SHARED_RT.block_on(async {
10485                    let ws: Arc<WebsocketStreams> =
10486                        create_websocket_streams(Some("ws://example.com"), None, None);
10487                    let conn = &ws.common.connection_pool[0];
10488                    let (tx, mut rx) = unbounded_channel();
10489                    {
10490                        let mut st = conn.state.lock().await;
10491                        st.ws_write_tx = Some(tx);
10492                    }
10493                    let id = Some("badid".to_string());
10494                    ws.send_subscription_payload(
10495                        conn,
10496                        &vec!["s1".to_string()],
10497                        id.map(StreamId::from),
10498                    );
10499                    let msg = rx.recv().await.expect("no message sent");
10500                    if let Message::Text(txt) = msg {
10501                        let v: serde_json::Value = serde_json::from_str(&txt).unwrap();
10502                        assert_eq!(v["method"], "SUBSCRIBE");
10503                        let id = v["id"].as_str().unwrap();
10504                        assert_ne!(id, "badid");
10505                        assert!(Regex::new(r"^[0-9a-fA-F]{32}$").unwrap().is_match(id));
10506                    } else {
10507                        panic!("unexpected message: {msg:?}");
10508                    }
10509                });
10510            }
10511
10512            #[test]
10513            fn subscribe_payload_with_and_without_custom_string_id() {
10514                TOKIO_SHARED_RT.block_on(async {
10515                    let ws: Arc<WebsocketStreams> =
10516                        create_websocket_streams(Some("ws://unused"), None, None);
10517                    let conn = &ws.common.connection_pool[0];
10518                    let (tx, mut rx) = unbounded_channel();
10519                    {
10520                        let mut st = conn.state.lock().await;
10521                        st.ws_write_tx = Some(tx);
10522                    }
10523                    let id = Some("deadbeefdeadbeefdeadbeefdeadbeef".to_string());
10524                    ws.send_subscription_payload(
10525                        conn,
10526                        &vec!["a".to_string(), "b".to_string()],
10527                        id.map(StreamId::from),
10528                    );
10529                    let msg1 = rx.recv().await.unwrap();
10530                    ws.send_subscription_payload(conn, &vec!["x".to_string()], None);
10531                    let msg2 = rx.recv().await.unwrap();
10532
10533                    if let Message::Text(txt1) = msg1 {
10534                        let v1: serde_json::Value = serde_json::from_str(&txt1).unwrap();
10535                        assert_eq!(v1["id"], "deadbeefdeadbeefdeadbeefdeadbeef");
10536                        assert_eq!(
10537                            v1["params"].as_array().unwrap(),
10538                            &vec![serde_json::json!("a"), serde_json::json!("b")]
10539                        );
10540                    } else {
10541                        panic!()
10542                    }
10543
10544                    if let Message::Text(txt2) = msg2 {
10545                        let v2: serde_json::Value = serde_json::from_str(&txt2).unwrap();
10546                        assert_eq!(v2["method"], "SUBSCRIBE");
10547                        let params = v2["params"].as_array().unwrap();
10548                        assert_eq!(params.len(), 1);
10549                        assert_eq!(params[0], "x");
10550                        let id2 = v2["id"].as_str().unwrap();
10551                        assert!(Regex::new(r"^[0-9a-fA-F]{32}$").unwrap().is_match(id2));
10552                    } else {
10553                        panic!()
10554                    }
10555                });
10556            }
10557
10558            #[test]
10559            fn subscribe_payload_with_and_without_custom_integer_id() {
10560                TOKIO_SHARED_RT.block_on(async {
10561                    let ws: Arc<WebsocketStreams> =
10562                        create_websocket_streams(Some("ws://unused"), None, None);
10563                    ws.stream_id_is_strictly_number
10564                        .store(true, Ordering::Relaxed);
10565                    let conn = &ws.common.connection_pool[0];
10566                    let (tx, mut rx) = unbounded_channel();
10567                    {
10568                        let mut st = conn.state.lock().await;
10569                        st.ws_write_tx = Some(tx);
10570                    }
10571
10572                    let id = Some(123u32);
10573
10574                    ws.send_subscription_payload(
10575                        conn,
10576                        &vec!["a".to_string(), "b".to_string()],
10577                        id.map(StreamId::from),
10578                    );
10579                    let msg1 = rx.recv().await.unwrap();
10580
10581                    ws.send_subscription_payload(conn, &vec!["x".to_string()], None);
10582                    let msg2 = rx.recv().await.unwrap();
10583
10584                    if let Message::Text(txt1) = msg1 {
10585                        let v1: serde_json::Value = serde_json::from_str(&txt1).unwrap();
10586                        assert_eq!(v1["method"], "SUBSCRIBE");
10587                        assert_eq!(v1["id"].as_u64(), Some(123));
10588                        assert_eq!(
10589                            v1["params"].as_array().unwrap(),
10590                            &vec![serde_json::json!("a"), serde_json::json!("b")]
10591                        );
10592                    } else {
10593                        panic!("Expected Message::Text for msg1");
10594                    }
10595
10596                    if let Message::Text(txt2) = msg2 {
10597                        let v2: serde_json::Value = serde_json::from_str(&txt2).unwrap();
10598                        assert_eq!(v2["method"], "SUBSCRIBE");
10599
10600                        let params = v2["params"].as_array().unwrap();
10601                        assert_eq!(params.len(), 1);
10602                        assert_eq!(params[0], "x");
10603
10604                        let id2 = v2.get("id").expect("payload should contain id");
10605                        assert!(
10606                            id2.is_number(),
10607                            "expected numeric id in strict-number mode, got: {id2:?}"
10608                        );
10609                        let n = id2.as_u64().unwrap();
10610                        assert!(u32::try_from(n).is_ok(), "id should fit u32, got {n}");
10611                    } else {
10612                        panic!("Expected Message::Text for msg2");
10613                    }
10614                });
10615            }
10616        }
10617
10618        mod on_open {
10619            use super::*;
10620
10621            #[test]
10622            fn sends_pending_subscriptions() {
10623                TOKIO_SHARED_RT.block_on(async {
10624                    let ws: Arc<WebsocketStreams> =
10625                        create_websocket_streams(Some("ws://example.com"), None, None);
10626                    let conn = &ws.common.connection_pool[0];
10627                    let (tx, mut rx) = unbounded_channel();
10628                    {
10629                        let mut st = conn.state.lock().await;
10630                        st.ws_write_tx = Some(tx);
10631                        st.pending_subscriptions.push_back("foo".to_string());
10632                        st.pending_subscriptions.push_back("bar".to_string());
10633                    }
10634                    ws.on_open("ws://example.com".to_string(), conn.clone())
10635                        .await;
10636                    let msg = rx.recv().await.expect("no subscription sent");
10637                    if let Message::Text(txt) = msg {
10638                        let v: Value = serde_json::from_str(&txt).unwrap();
10639                        assert_eq!(v["method"], "SUBSCRIBE");
10640                        let params = v["params"].as_array().unwrap();
10641                        assert_eq!(
10642                            params,
10643                            &vec![Value::String("foo".into()), Value::String("bar".into())]
10644                        );
10645                    } else {
10646                        panic!("unexpected message: {msg:?}");
10647                    }
10648                    let st_after = conn.state.lock().await;
10649                    assert!(st_after.pending_subscriptions.is_empty());
10650                });
10651            }
10652
10653            #[test]
10654            fn with_no_pending_subscriptions_sends_nothing() {
10655                TOKIO_SHARED_RT.block_on(async {
10656                    let ws: Arc<WebsocketStreams> =
10657                        create_websocket_streams(Some("ws://example.com"), None, None);
10658                    let conn = &ws.common.connection_pool[0];
10659                    let (tx, mut rx) = unbounded_channel();
10660                    {
10661                        let mut st = conn.state.lock().await;
10662                        st.ws_write_tx = Some(tx);
10663                    }
10664                    ws.on_open("ws://example.com".to_string(), conn.clone())
10665                        .await;
10666                    assert!(rx.try_recv().is_err(), "unexpected message sent");
10667                });
10668            }
10669
10670            #[test]
10671            fn clears_pending_without_write_channel() {
10672                TOKIO_SHARED_RT.block_on(async {
10673                    let ws: Arc<WebsocketStreams> =
10674                        create_websocket_streams(Some("ws://example.com"), None, None);
10675                    let conn = &ws.common.connection_pool[0];
10676                    {
10677                        let mut st = conn.state.lock().await;
10678                        st.pending_subscriptions.push_back("solo".to_string());
10679                    }
10680                    ws.on_open("ws://example.com".to_string(), conn.clone())
10681                        .await;
10682                    let st_after = conn.state.lock().await;
10683                    assert!(st_after.pending_subscriptions.is_empty());
10684                });
10685            }
10686        }
10687
10688        mod on_message {
10689            use super::*;
10690
10691            #[test]
10692            fn invokes_registered_callback() {
10693                TOKIO_SHARED_RT.block_on(async {
10694                    let ws: Arc<WebsocketStreams> =
10695                        create_websocket_streams(Some("ws://example.com"), None, None);
10696                    let conn = &ws.common.connection_pool[0];
10697                    let called = Arc::new(AtomicBool::new(false));
10698                    let called_clone = called.clone();
10699
10700                    {
10701                        let mut st = conn.state.lock().await;
10702                        st.stream_callbacks
10703                            .entry("stream1".to_string())
10704                            .or_default()
10705                            .push(
10706                                (Box::new(move |_: &Value| {
10707                                    called_clone.store(true, Ordering::SeqCst);
10708                                })
10709                                    as Box<dyn Fn(&Value) + Send + Sync>)
10710                                    .into(),
10711                            );
10712                    }
10713
10714                    let msg = json!({
10715                        "stream": "stream1",
10716                        "data": { "key": "value" }
10717                    })
10718                    .to_string();
10719
10720                    ws.on_message(msg, conn.clone()).await;
10721
10722                    assert!(called.load(Ordering::SeqCst));
10723                });
10724            }
10725
10726            #[test]
10727            fn invokes_all_registered_callbacks() {
10728                TOKIO_SHARED_RT.block_on(async {
10729                    let ws: Arc<WebsocketStreams> =
10730                        create_websocket_streams(Some("ws://example.com"), None, None);
10731                    let conn = &ws.common.connection_pool[0];
10732                    let counter = Arc::new(AtomicUsize::new(0));
10733
10734                    {
10735                        let mut st = conn.state.lock().await;
10736                        let entry = st.stream_callbacks.entry("s".into()).or_default();
10737                        let c1 = counter.clone();
10738                        entry.push(
10739                            (Box::new(move |_: &Value| {
10740                                c1.fetch_add(1, Ordering::SeqCst);
10741                            }) as Box<dyn Fn(&Value) + Send + Sync>)
10742                                .into(),
10743                        );
10744                        let c2 = counter.clone();
10745                        entry.push(
10746                            (Box::new(move |_: &Value| {
10747                                c2.fetch_add(1, Ordering::SeqCst);
10748                            }) as Box<dyn Fn(&Value) + Send + Sync>)
10749                                .into(),
10750                        );
10751                    }
10752
10753                    let msg = json!({"stream":"s","data":42}).to_string();
10754                    ws.on_message(msg, conn.clone()).await;
10755
10756                    assert_eq!(counter.load(Ordering::SeqCst), 2);
10757                });
10758            }
10759
10760            #[test]
10761            fn handles_null_data_field() {
10762                TOKIO_SHARED_RT.block_on(async {
10763                    let ws: Arc<WebsocketStreams> =
10764                        create_websocket_streams(Some("ws://example.com"), None, None);
10765                    let conn = &ws.common.connection_pool[0];
10766                    let called = Arc::new(AtomicUsize::new(0));
10767                    {
10768                        let mut st = conn.state.lock().await;
10769                        st.stream_callbacks.entry("n".into()).or_default().push(
10770                            (Box::new({
10771                                let c = called.clone();
10772                                move |data: &Value| {
10773                                    if data.is_null() {
10774                                        c.fetch_add(1, Ordering::SeqCst);
10775                                    }
10776                                }
10777                            }) as Box<dyn Fn(&Value) + Send + Sync>)
10778                                .into(),
10779                        );
10780                    }
10781                    let msg = json!({"stream":"n","data":null}).to_string();
10782                    ws.on_message(msg, conn.clone()).await;
10783                    assert_eq!(called.load(Ordering::SeqCst), 1);
10784                });
10785            }
10786
10787            #[test]
10788            fn with_invalid_json_does_not_panic() {
10789                TOKIO_SHARED_RT.block_on(async {
10790                    let ws: Arc<WebsocketStreams> =
10791                        create_websocket_streams(Some("ws://example.com"), None, None);
10792                    let conn = &ws.common.connection_pool[0];
10793                    let bad = "not a json";
10794                    ws.on_message(bad.to_string(), conn.clone()).await;
10795                });
10796            }
10797
10798            #[test]
10799            fn without_stream_field_does_nothing() {
10800                TOKIO_SHARED_RT.block_on(async {
10801                    let ws: Arc<WebsocketStreams> =
10802                        create_websocket_streams(Some("ws://example.com"), None, None);
10803                    let conn = &ws.common.connection_pool[0];
10804                    let msg = json!({ "data": { "foo": 1 } }).to_string();
10805                    ws.on_message(msg, conn.clone()).await;
10806                });
10807            }
10808
10809            #[test]
10810            fn with_unregistered_stream_does_not_panic() {
10811                TOKIO_SHARED_RT.block_on(async {
10812                    let ws: Arc<WebsocketStreams> =
10813                        create_websocket_streams(Some("ws://example.com"), None, None);
10814                    let conn = &ws.common.connection_pool[0];
10815                    let msg = json!({
10816                        "stream": "nope",
10817                        "data": { "foo": 1 }
10818                    })
10819                    .to_string();
10820                    ws.on_message(msg, conn.clone()).await;
10821                });
10822            }
10823
10824            #[test]
10825            fn invokes_registered_callback_with_url_path_key() {
10826                TOKIO_SHARED_RT.block_on(async {
10827                    let ws: Arc<WebsocketStreams> =
10828                        create_websocket_streams(Some("ws://example.com"), None, None);
10829                    let conn = &ws.common.connection_pool[0];
10830
10831                    {
10832                        let mut st = conn.state.lock().await;
10833                        st.url_path = Some("path1".to_string());
10834                    }
10835
10836                    let called = Arc::new(AtomicBool::new(false));
10837                    let called_clone = called.clone();
10838
10839                    {
10840                        let mut st = conn.state.lock().await;
10841                        st.stream_callbacks
10842                            .entry("path1::stream1".to_string())
10843                            .or_default()
10844                            .push(
10845                                (Box::new(move |_: &Value| {
10846                                    called_clone.store(true, Ordering::SeqCst);
10847                                })
10848                                    as Box<dyn Fn(&Value) + Send + Sync>)
10849                                    .into(),
10850                            );
10851                    }
10852
10853                    let msg = json!({
10854                        "stream": "stream1",
10855                        "data": { "key": "value" }
10856                    })
10857                    .to_string();
10858
10859                    ws.on_message(msg, conn.clone()).await;
10860
10861                    assert!(called.load(Ordering::SeqCst));
10862                });
10863            }
10864
10865            #[test]
10866            fn does_not_invoke_callback_when_url_path_mismatch() {
10867                TOKIO_SHARED_RT.block_on(async {
10868                    let ws: Arc<WebsocketStreams> =
10869                        create_websocket_streams(Some("ws://example.com"), None, None);
10870                    let conn = &ws.common.connection_pool[0];
10871
10872                    {
10873                        let mut st = conn.state.lock().await;
10874                        st.url_path = Some("path2".to_string());
10875                    }
10876
10877                    let called = Arc::new(AtomicBool::new(false));
10878                    let called_clone = called.clone();
10879
10880                    {
10881                        let mut st = conn.state.lock().await;
10882                        st.stream_callbacks
10883                            .entry("path1::stream1".to_string())
10884                            .or_default()
10885                            .push(
10886                                (Box::new(move |_: &Value| {
10887                                    called_clone.store(true, Ordering::SeqCst);
10888                                })
10889                                    as Box<dyn Fn(&Value) + Send + Sync>)
10890                                    .into(),
10891                            );
10892                    }
10893
10894                    let msg = json!({
10895                        "stream": "stream1",
10896                        "data": { "key": "value" }
10897                    })
10898                    .to_string();
10899
10900                    ws.on_message(msg, conn.clone()).await;
10901
10902                    assert!(!called.load(Ordering::SeqCst));
10903                });
10904            }
10905
10906            #[test]
10907            fn invokes_only_callbacks_for_current_url_path_when_both_exist() {
10908                TOKIO_SHARED_RT.block_on(async {
10909                    let ws: Arc<WebsocketStreams> =
10910                        create_websocket_streams(Some("ws://example.com"), None, None);
10911                    let conn = &ws.common.connection_pool[0];
10912
10913                    {
10914                        let mut st = conn.state.lock().await;
10915                        st.url_path = Some("path1".to_string());
10916                    }
10917
10918                    let c1 = Arc::new(AtomicUsize::new(0));
10919                    let c2 = Arc::new(AtomicUsize::new(0));
10920
10921                    {
10922                        let mut st = conn.state.lock().await;
10923
10924                        let a = c1.clone();
10925                        st.stream_callbacks
10926                            .entry("path1::s".to_string())
10927                            .or_default()
10928                            .push(
10929                                (Box::new(move |_: &Value| {
10930                                    a.fetch_add(1, Ordering::SeqCst);
10931                                })
10932                                    as Box<dyn Fn(&Value) + Send + Sync>)
10933                                    .into(),
10934                            );
10935
10936                        let b = c2.clone();
10937                        st.stream_callbacks
10938                            .entry("path2::s".to_string())
10939                            .or_default()
10940                            .push(
10941                                (Box::new(move |_: &Value| {
10942                                    b.fetch_add(1, Ordering::SeqCst);
10943                                })
10944                                    as Box<dyn Fn(&Value) + Send + Sync>)
10945                                    .into(),
10946                            );
10947                    }
10948
10949                    let msg = json!({"stream":"s","data":42}).to_string();
10950                    ws.on_message(msg, conn.clone()).await;
10951
10952                    assert_eq!(c1.load(Ordering::SeqCst), 1);
10953                    assert_eq!(c2.load(Ordering::SeqCst), 0);
10954                });
10955            }
10956        }
10957
10958        mod get_reconnect_url {
10959            use super::*;
10960
10961            #[test]
10962            fn single_stream_reconnect_url() {
10963                TOKIO_SHARED_RT.block_on(async {
10964                    let ws: Arc<WebsocketStreams> =
10965                        create_websocket_streams(Some("ws://example.com"), None, None);
10966                    let c0 = ws.common.connection_pool[0].clone();
10967                    {
10968                        let mut map = ws.connection_streams.lock().await;
10969                        map.insert("s1".to_string(), c0.clone());
10970                    }
10971                    let url = ws.get_reconnect_url("default_url".into(), c0).await;
10972                    assert_eq!(url, "ws://example.com/stream?streams=s1");
10973                });
10974            }
10975
10976            #[test]
10977            fn multiple_streams_same_connection() {
10978                TOKIO_SHARED_RT.block_on(async {
10979                    let ws: Arc<WebsocketStreams> =
10980                        create_websocket_streams(Some("ws://example.com"), None, None);
10981                    let c0 = ws.common.connection_pool[0].clone();
10982                    {
10983                        let mut map = ws.connection_streams.lock().await;
10984                        map.insert("a".to_string(), c0.clone());
10985                        map.insert("b".to_string(), c0.clone());
10986                    }
10987                    let url = ws.get_reconnect_url("default_url".into(), c0).await;
10988                    let suffix = url
10989                        .strip_prefix("ws://example.com/stream?streams=")
10990                        .unwrap();
10991                    let parts: Vec<_> = suffix.split('&').next().unwrap().split('/').collect();
10992                    let set = parts.into_iter().collect::<std::collections::HashSet<_>>();
10993                    assert_eq!(set, ["a", "b"].iter().copied().collect());
10994                });
10995            }
10996
10997            #[test]
10998            fn reconnect_url_with_time_unit() {
10999                TOKIO_SHARED_RT.block_on(async {
11000                    let mut ws: Arc<WebsocketStreams> =
11001                        create_websocket_streams(Some("ws://example.com"), None, None);
11002                    Arc::get_mut(&mut ws).unwrap().configuration.time_unit =
11003                        Some(TimeUnit::Microsecond);
11004                    let c0 = ws.common.connection_pool[0].clone();
11005                    {
11006                        let mut map = ws.connection_streams.lock().await;
11007                        map.insert("x".to_string(), c0.clone());
11008                    }
11009                    let url = ws.get_reconnect_url("default_url".into(), c0).await;
11010                    assert_eq!(
11011                        url,
11012                        "ws://example.com/stream?streams=x&timeUnit=microsecond"
11013                    );
11014                });
11015            }
11016
11017            #[test]
11018            fn reconnect_url_uses_url_path_from_connection_state() {
11019                TOKIO_SHARED_RT.block_on(async {
11020                    let ws: Arc<WebsocketStreams> =
11021                        create_websocket_streams(Some("ws://example.com"), None, None);
11022                    let c0 = ws.common.connection_pool[0].clone();
11023
11024                    {
11025                        let mut st = c0.state.lock().await;
11026                        st.url_path = Some("path1".to_string());
11027                    }
11028
11029                    {
11030                        let mut map = ws.connection_streams.lock().await;
11031                        map.insert("path1::s1".to_string(), c0.clone());
11032                    }
11033
11034                    let url = ws.get_reconnect_url("default_url".into(), c0).await;
11035                    assert_eq!(url, "ws://example.com/path1/stream?streams=s1");
11036                });
11037            }
11038
11039            #[test]
11040            fn reconnect_url_strips_prefix_from_multiple_keys_with_url_path() {
11041                TOKIO_SHARED_RT.block_on(async {
11042                    let ws: Arc<WebsocketStreams> =
11043                        create_websocket_streams(Some("ws://example.com"), None, None);
11044                    let c0 = ws.common.connection_pool[0].clone();
11045
11046                    {
11047                        let mut st = c0.state.lock().await;
11048                        st.url_path = Some("path1".to_string());
11049                    }
11050
11051                    {
11052                        let mut map = ws.connection_streams.lock().await;
11053                        map.insert("path1::a".to_string(), c0.clone());
11054                        map.insert("path1::b".to_string(), c0.clone());
11055                    }
11056
11057                    let url = ws.get_reconnect_url("default_url".into(), c0).await;
11058
11059                    let suffix = url
11060                        .strip_prefix("ws://example.com/path1/stream?streams=")
11061                        .unwrap();
11062                    let parts: Vec<_> = suffix.split('&').next().unwrap().split('/').collect();
11063                    let set = parts.into_iter().collect::<std::collections::HashSet<_>>();
11064                    assert_eq!(set, ["a", "b"].iter().copied().collect());
11065                });
11066            }
11067
11068            #[test]
11069            fn reconnect_url_with_url_path_and_time_unit() {
11070                TOKIO_SHARED_RT.block_on(async {
11071                    let mut ws: Arc<WebsocketStreams> =
11072                        create_websocket_streams(Some("ws://example.com"), None, None);
11073                    Arc::get_mut(&mut ws).unwrap().configuration.time_unit =
11074                        Some(TimeUnit::Microsecond);
11075
11076                    let c0 = ws.common.connection_pool[0].clone();
11077
11078                    {
11079                        let mut st = c0.state.lock().await;
11080                        st.url_path = Some("path1".to_string());
11081                    }
11082
11083                    {
11084                        let mut map = ws.connection_streams.lock().await;
11085                        map.insert("path1::x".to_string(), c0.clone());
11086                    }
11087
11088                    let url = ws.get_reconnect_url("default_url".into(), c0).await;
11089                    assert_eq!(
11090                        url,
11091                        "ws://example.com/path1/stream?streams=x&timeUnit=microsecond"
11092                    );
11093                });
11094            }
11095
11096            #[test]
11097            fn reconnect_url_ignores_streams_from_other_connections_even_if_same_path_prefix() {
11098                TOKIO_SHARED_RT.block_on(async {
11099                    let ws: Arc<WebsocketStreams> =
11100                        create_websocket_streams(Some("ws://example.com"), None, None);
11101                    let c0 = ws.common.connection_pool[0].clone();
11102                    let c1 = ws.common.connection_pool[1].clone();
11103
11104                    {
11105                        let mut st = c0.state.lock().await;
11106                        st.url_path = Some("path1".to_string());
11107                    }
11108                    {
11109                        let mut st = c1.state.lock().await;
11110                        st.url_path = Some("path1".to_string());
11111                    }
11112
11113                    {
11114                        let mut map = ws.connection_streams.lock().await;
11115                        map.insert("path1::a".to_string(), c0.clone());
11116                        map.insert("path1::b".to_string(), c1.clone());
11117                    }
11118
11119                    let url = ws.get_reconnect_url("default_url".into(), c0).await;
11120
11121                    let suffix = url
11122                        .strip_prefix("ws://example.com/path1/stream?streams=")
11123                        .unwrap();
11124                    let parts: Vec<_> = suffix.split('&').next().unwrap().split('/').collect();
11125                    let set = parts.into_iter().collect::<std::collections::HashSet<_>>();
11126                    assert_eq!(set, ["a"].iter().copied().collect());
11127                });
11128            }
11129        }
11130    }
11131
11132    mod websocket_streams_signed {
11133        use super::*;
11134        use hmac::{Hmac, Mac};
11135        use sha2::Sha256;
11136        use std::collections::HashMap;
11137        use tokio::sync::mpsc::unbounded_channel;
11138
11139        const API_KEY: &str = "test-api-key";
11140        const API_SECRET: &str = "test-api-secret";
11141
11142        fn signed_config(ws_url: &str) -> ConfigurationWebsocketStreams {
11143            let mut cfg = ConfigurationWebsocketStreams::builder()
11144                .ws_url(ws_url.to_string())
11145                .api_key(API_KEY)
11146                .api_secret(API_SECRET)
11147                .build()
11148                .expect("Failed to build configuration");
11149            cfg.user_agent = build_user_agent("product");
11150            cfg
11151        }
11152
11153        fn create_signed_streams(
11154            ws_url: &str,
11155            conns: Vec<Arc<WebsocketConnection>>,
11156        ) -> Arc<WebsocketStreams> {
11157            let ws = WebsocketStreams::new(signed_config(ws_url), conns, vec![]);
11158            ws.signed_streams.store(true, Ordering::Relaxed);
11159            ws
11160        }
11161
11162        fn hmac_hex(payload: &str) -> String {
11163            let mut mac = Hmac::<Sha256>::new_from_slice(API_SECRET.as_bytes()).unwrap();
11164            mac.update(payload.as_bytes());
11165            hex::encode(mac.finalize().into_bytes())
11166        }
11167
11168        #[test]
11169        fn prepare_url_uses_configured_timestamp_provider() {
11170            TOKIO_SHARED_RT.block_on(async {
11171                let mut cfg = signed_config("wss://api.binance.com/sapi/wss");
11172                cfg.timestamp_provider = Some(Arc::new(|| 1_700_000_000_123));
11173                let ws = WebsocketStreams::new(cfg, vec![WebsocketConnection::new("c1")], vec![]);
11174                ws.signed_streams.store(true, Ordering::Relaxed);
11175
11176                let url = ws.prepare_url(&[], None);
11177                let q: HashMap<_, _> = url::Url::parse(&url)
11178                    .unwrap()
11179                    .query_pairs()
11180                    .into_owned()
11181                    .collect();
11182                assert_eq!(q["timestamp"], "1700000000123");
11183                let payload = format!(
11184                    "random={}&recvWindow=30000&timestamp=1700000000123",
11185                    q["random"]
11186                );
11187                assert_eq!(q["signature"], hmac_hex(&payload));
11188            });
11189        }
11190
11191        #[test]
11192        fn prepare_url_builds_signed_url() {
11193            TOKIO_SHARED_RT.block_on(async {
11194                let ws = create_signed_streams(
11195                    "wss://api.binance.com/sapi/wss",
11196                    vec![WebsocketConnection::new("c1")],
11197                );
11198                let url = ws.prepare_url(&["ignored".to_string()], None);
11199
11200                let parsed = url::Url::parse(&url).unwrap();
11201                assert_eq!(
11202                    format!(
11203                        "{}://{}{}",
11204                        parsed.scheme(),
11205                        parsed.host_str().unwrap(),
11206                        parsed.path()
11207                    ),
11208                    "wss://api.binance.com/sapi/wss"
11209                );
11210                let q: HashMap<_, _> = parsed.query_pairs().into_owned().collect();
11211                assert!(!q.contains_key("streams"));
11212                assert_eq!(q["recvWindow"], "30000");
11213                assert!(q["timestamp"].parse::<u128>().is_ok());
11214                assert!(
11215                    Regex::new(r"^[0-9a-f]{32}$")
11216                        .unwrap()
11217                        .is_match(&q["random"])
11218                );
11219
11220                let payload = format!(
11221                    "random={}&recvWindow=30000&timestamp={}",
11222                    q["random"], q["timestamp"]
11223                );
11224                assert_eq!(q["signature"], hmac_hex(&payload));
11225            });
11226        }
11227
11228        #[test]
11229        fn connect_fails_without_credentials() {
11230            TOKIO_SHARED_RT.block_on(async {
11231                let mut cfg = signed_config("ws://127.0.0.1:1");
11232                cfg.api_key = None;
11233                let ws = WebsocketStreams::new(cfg, vec![WebsocketConnection::new("c1")], vec![]);
11234                ws.signed_streams.store(true, Ordering::Relaxed);
11235                let err = ws.clone().connect(vec![]).await.unwrap_err();
11236                assert!(matches!(err, WebsocketError::Handshake(ref m) if m.contains("api_key")));
11237
11238                let mut cfg = signed_config("ws://127.0.0.1:1");
11239                cfg.api_secret = None;
11240                let ws = WebsocketStreams::new(cfg, vec![WebsocketConnection::new("c1")], vec![]);
11241                ws.signed_streams.store(true, Ordering::Relaxed);
11242                let err = ws.clone().connect(vec![]).await.unwrap_err();
11243                assert!(
11244                    matches!(err, WebsocketError::Handshake(ref m) if m.contains("api_secret"))
11245                );
11246            });
11247        }
11248
11249        #[test]
11250        fn subscription_and_unsubscription_use_command_value_payloads() {
11251            TOKIO_SHARED_RT.block_on(async {
11252                let conn = WebsocketConnection::new("c1");
11253                let ws = create_signed_streams("ws://example.com", vec![conn.clone()]);
11254                let (tx, mut rx) = unbounded_channel();
11255                conn.state.lock().await.ws_write_tx = Some(tx);
11256
11257                ws.send_subscription_payload(
11258                    &conn,
11259                    &vec!["a_topic".to_string(), "b_topic".to_string()],
11260                    None,
11261                );
11262
11263                let Message::Text(txt) = rx.recv().await.expect("no message sent") else {
11264                    panic!("expected text message");
11265                };
11266                let v: Value = serde_json::from_str(&txt).unwrap();
11267                assert_eq!(
11268                    v,
11269                    json!({ "command": "SUBSCRIBE", "value": "a_topic|b_topic" })
11270                );
11271
11272                {
11273                    let mut map = ws.connection_streams.lock().await;
11274                    map.insert("a_topic".to_string(), conn.clone());
11275                    map.insert("b_topic".to_string(), conn.clone());
11276                }
11277                ws.unsubscribe(
11278                    vec!["a_topic".to_string(), "b_topic".to_string()],
11279                    None,
11280                    None,
11281                )
11282                .await;
11283
11284                let Message::Text(txt) = rx.recv().await.expect("no message sent") else {
11285                    panic!("expected text message");
11286                };
11287                let v: Value = serde_json::from_str(&txt).unwrap();
11288                assert_eq!(
11289                    v,
11290                    json!({ "command": "UNSUBSCRIBE", "value": "a_topic|b_topic" })
11291                );
11292                assert!(
11293                    rx.try_recv().is_err(),
11294                    "only one UNSUBSCRIBE message expected"
11295                );
11296            });
11297        }
11298
11299        #[test]
11300        fn on_message_routes_by_topic_and_parses_string_data() {
11301            TOKIO_SHARED_RT.block_on(async {
11302                let conn = WebsocketConnection::new("c1");
11303                let ws = create_signed_streams("ws://example.com", vec![conn.clone()]);
11304
11305                let received = Arc::new(std::sync::Mutex::new(Vec::<Value>::new()));
11306                let received_clone = Arc::clone(&received);
11307                conn.state.lock().await.stream_callbacks.insert(
11308                    "com_announcement_en".to_string(),
11309                    vec![Arc::new(move |v: &Value| {
11310                        received_clone.lock().unwrap().push(v.clone());
11311                    })],
11312                );
11313
11314                let data = json!({ "catalogId": 161, "title": "Notice of..." });
11315                let msg = json!({
11316                    "type": "DATA",
11317                    "topic": "com_announcement_en",
11318                    "data": data.to_string(),
11319                });
11320                ws.on_message(msg.to_string(), conn.clone()).await;
11321
11322                let cmd = json!({
11323                    "type": "COMMAND",
11324                    "data": "SUCCESS",
11325                    "subType": "SUBSCRIBE",
11326                    "code": "00000000",
11327                });
11328                ws.on_message(cmd.to_string(), conn.clone()).await;
11329
11330                let got = received.lock().unwrap().clone();
11331                assert_eq!(got, vec![data]);
11332            });
11333        }
11334
11335        #[test]
11336        fn on_open_resubscribes_assigned_streams() {
11337            TOKIO_SHARED_RT.block_on(async {
11338                let conn = WebsocketConnection::new("c1");
11339                let ws = create_signed_streams("ws://example.com", vec![conn.clone()]);
11340                let (tx, mut rx) = unbounded_channel();
11341                {
11342                    let mut st = conn.state.lock().await;
11343                    st.ws_write_tx = Some(tx);
11344                    st.pending_subscriptions
11345                        .push_back("pending_topic".to_string());
11346                }
11347                ws.connection_streams
11348                    .lock()
11349                    .await
11350                    .insert("assigned_topic".to_string(), conn.clone());
11351
11352                ws.on_open("ws://example.com".to_string(), conn.clone())
11353                    .await;
11354
11355                let Message::Text(txt) = rx.recv().await.expect("no message sent") else {
11356                    panic!("expected text message");
11357                };
11358                let v: Value = serde_json::from_str(&txt).unwrap();
11359                assert_eq!(v["command"], "SUBSCRIBE");
11360                let values: HashSet<String> = v["value"]
11361                    .as_str()
11362                    .unwrap()
11363                    .split('|')
11364                    .map(str::to_string)
11365                    .collect();
11366                assert_eq!(
11367                    values,
11368                    ["pending_topic", "assigned_topic"]
11369                        .iter()
11370                        .map(|s| (*s).to_string())
11371                        .collect()
11372                );
11373                assert!(conn.state.lock().await.pending_subscriptions.is_empty());
11374            });
11375        }
11376
11377        #[test]
11378        fn connect_sends_api_key_header_signed_url_and_subscribes() {
11379            TOKIO_SHARED_RT.block_on(async {
11380                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
11381                let addr: SocketAddr = listener.local_addr().unwrap();
11382                let (seen_tx, mut seen_rx) = unbounded_channel::<(String, Option<String>)>();
11383                let (msg_tx, mut msg_rx) = unbounded_channel::<String>();
11384
11385                let _server = AbortOnDrop(tokio::spawn(async move {
11386                    if let Ok((stream, _)) = listener.accept().await {
11387                        let callback = |req: &Request, resp| {
11388                            let api_key = req
11389                                .headers()
11390                                .get("X-MBX-APIKEY")
11391                                .map(|v| v.to_str().unwrap().to_string());
11392                            let _ = seen_tx.send((req.uri().to_string(), api_key));
11393                            Ok(resp)
11394                        };
11395                        let mut ws = accept_hdr_async(stream, callback).await.unwrap();
11396                        while let Some(Ok(msg)) = ws.next().await {
11397                            if let Message::Text(txt) = msg {
11398                                let _ = msg_tx.send(txt.to_string());
11399                                let push = json!({
11400                                    "type": "DATA",
11401                                    "topic": "com_announcement_en",
11402                                    "data": json!({ "catalogId": 1 }).to_string(),
11403                                });
11404                                let _ = ws.send(Message::Text(push.to_string().into())).await;
11405                            }
11406                        }
11407                    }
11408                }));
11409
11410                let conn = WebsocketConnection::new("c1");
11411                let ws = create_signed_streams(&format!("ws://{addr}/sapi/wss"), vec![conn]);
11412                ws.clone()
11413                    .connect(vec!["com_announcement_en".to_string()])
11414                    .await
11415                    .expect("connect should succeed");
11416
11417                let (uri, api_key) = timeout(Duration::from_secs(5), seen_rx.recv())
11418                    .await
11419                    .unwrap()
11420                    .unwrap();
11421                assert_eq!(api_key.as_deref(), Some(API_KEY));
11422                assert!(uri.starts_with("/sapi/wss?random="), "unexpected uri {uri}");
11423                assert!(uri.contains("&recvWindow=30000&timestamp="));
11424                assert!(uri.contains("&signature="));
11425                assert!(!uri.contains("/stream"));
11426
11427                let sub = timeout(Duration::from_secs(5), msg_rx.recv())
11428                    .await
11429                    .unwrap()
11430                    .unwrap();
11431                let v: Value = serde_json::from_str(&sub).unwrap();
11432                assert_eq!(
11433                    v,
11434                    json!({ "command": "SUBSCRIBE", "value": "com_announcement_en" })
11435                );
11436                assert!(ws.is_subscribed("com_announcement_en").await);
11437
11438                let _ = ws.disconnect().await;
11439            });
11440        }
11441    }
11442
11443    mod websocket_stream {
11444        use super::*;
11445
11446        mod on {
11447            use super::*;
11448
11449            #[test]
11450            fn registers_callback_and_stream_callback_for_websocket_streams() {
11451                TOKIO_SHARED_RT.block_on(async {
11452                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11453                    let stream_name = "s1".to_string();
11454                    let conn = ws_base.common.connection_pool[0].clone();
11455
11456                    let key = ws_base.stream_key(&stream_name, None);
11457
11458                    {
11459                        let mut map = ws_base.connection_streams.lock().await;
11460                        map.insert(key.clone(), conn.clone());
11461                    }
11462                    {
11463                        let mut state = conn.state.lock().await;
11464                        state.stream_callbacks.insert(key.clone(), Vec::new());
11465                    }
11466
11467                    let stream = Arc::new(WebsocketStream::<Value> {
11468                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11469                        stream_or_id: stream_name.clone(),
11470                        callback: Mutex::new(None),
11471                        url_path: None,
11472                        id: None,
11473                        _phantom: PhantomData,
11474                    });
11475
11476                    stream.on("message", |_| {}).await;
11477
11478                    let cb_guard = stream.callback.lock().await;
11479                    assert!(cb_guard.is_some());
11480
11481                    let cbs = {
11482                        let state = conn.state.lock().await;
11483                        state.stream_callbacks.get(&key).unwrap().clone()
11484                    };
11485                    assert_eq!(cbs.len(), 1);
11486                });
11487            }
11488
11489            #[test]
11490            fn message_twice_registers_two_wrappers_for_websocket_streams() {
11491                TOKIO_SHARED_RT.block_on(async {
11492                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11493                    let stream_name = "s2".to_string();
11494                    let conn = ws_base.common.connection_pool[0].clone();
11495
11496                    let key = ws_base.stream_key(&stream_name, None);
11497
11498                    {
11499                        let mut map = ws_base.connection_streams.lock().await;
11500                        map.insert(key.clone(), conn.clone());
11501                    }
11502                    {
11503                        let mut state = conn.state.lock().await;
11504                        state.stream_callbacks.insert(key.clone(), Vec::new());
11505                    }
11506
11507                    let stream = Arc::new(WebsocketStream::<Value> {
11508                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11509                        stream_or_id: stream_name.clone(),
11510                        url_path: None,
11511                        callback: Mutex::new(None),
11512                        id: None,
11513                        _phantom: PhantomData,
11514                    });
11515
11516                    stream.on("message", |_| {}).await;
11517                    stream.on("message", |_| {}).await;
11518
11519                    let state = conn.state.lock().await;
11520                    let callbacks = state.stream_callbacks.get(&key).unwrap();
11521                    assert_eq!(callbacks.len(), 2);
11522                });
11523            }
11524
11525            #[test]
11526            fn ignores_non_message_event_for_websocket_streams() {
11527                TOKIO_SHARED_RT.block_on(async {
11528                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11529                    let stream = Arc::new(WebsocketStream::<Value> {
11530                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11531                        stream_or_id: "s".into(),
11532                        url_path: None,
11533                        callback: Mutex::new(None),
11534                        id: None,
11535                        _phantom: PhantomData,
11536                    });
11537                    stream.on("open", |_| {}).await;
11538                    let guard = stream.callback.lock().await;
11539                    assert!(guard.is_none());
11540                });
11541            }
11542
11543            #[test]
11544            fn registers_callback_and_stream_callback_for_websocket_api() {
11545                TOKIO_SHARED_RT.block_on(async {
11546                    let ws_base = create_websocket_api(None, None, None);
11547
11548                    {
11549                        let mut stream_callbacks = ws_base.stream_callbacks.lock().await;
11550                        stream_callbacks.insert("id1".to_string(), Vec::new());
11551                    }
11552
11553                    let stream = Arc::new(WebsocketStream::<Value> {
11554                        websocket_base: WebsocketBase::WebsocketApi(ws_base.clone()),
11555                        stream_or_id: "id1".to_string(),
11556                        url_path: None,
11557                        callback: Mutex::new(None),
11558                        id: None,
11559                        _phantom: PhantomData,
11560                    });
11561
11562                    let called = Arc::new(Mutex::new(false));
11563                    let called_clone = called.clone();
11564                    stream
11565                        .on("message", move |v: Value| {
11566                            let mut lock = called_clone.blocking_lock();
11567                            *lock = v == Value::String("x".into());
11568                        })
11569                        .await;
11570
11571                    let cb_guard = stream.callback.lock().await;
11572                    assert!(cb_guard.is_some());
11573
11574                    let stream_callbacks = ws_base.stream_callbacks.lock().await;
11575                    let callbacks = stream_callbacks.get("id1").unwrap();
11576                    assert_eq!(callbacks.len(), 1);
11577                });
11578            }
11579
11580            #[test]
11581            fn message_twice_registers_two_wrappers_for_websocket_api() {
11582                TOKIO_SHARED_RT.block_on(async {
11583                    let ws_base = create_websocket_api(None, None, None);
11584
11585                    let stream = Arc::new(WebsocketStream::<Value> {
11586                        websocket_base: WebsocketBase::WebsocketApi(ws_base.clone()),
11587                        stream_or_id: "id2".to_string(),
11588                        url_path: None,
11589                        callback: Mutex::new(None),
11590                        id: None,
11591                        _phantom: PhantomData,
11592                    });
11593
11594                    stream.on("message", |_| {}).await;
11595                    stream.on("message", |_| {}).await;
11596
11597                    let stream_callbacks = ws_base.stream_callbacks.lock().await;
11598                    let callbacks = stream_callbacks.get("id2").unwrap();
11599                    assert_eq!(callbacks.len(), 2);
11600                });
11601            }
11602
11603            #[test]
11604            fn ignores_non_message_event_for_websocket_api() {
11605                TOKIO_SHARED_RT.block_on(async {
11606                    let ws_base = create_websocket_api(None, None, None);
11607
11608                    let stream = Arc::new(WebsocketStream::<Value> {
11609                        websocket_base: WebsocketBase::WebsocketApi(ws_base.clone()),
11610                        stream_or_id: "id3".into(),
11611                        url_path: None,
11612                        callback: Mutex::new(None),
11613                        id: None,
11614                        _phantom: PhantomData,
11615                    });
11616
11617                    stream.on("open", |_| {}).await;
11618
11619                    let guard = stream.callback.lock().await;
11620                    assert!(guard.is_none());
11621
11622                    let stream_callbacks = ws_base.stream_callbacks.lock().await;
11623                    assert!(stream_callbacks.get("id3").is_none());
11624                    assert!(stream_callbacks.is_empty());
11625                });
11626            }
11627
11628            #[test]
11629            fn registers_callback_for_websocket_streams_with_url_path() {
11630                TOKIO_SHARED_RT.block_on(async {
11631                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11632                    let stream_name = "s1".to_string();
11633                    let conn = ws_base.common.connection_pool[0].clone();
11634
11635                    let key = ws_base.stream_key(&stream_name, Some("path1"));
11636
11637                    {
11638                        let mut map = ws_base.connection_streams.lock().await;
11639                        map.insert(key.clone(), conn.clone());
11640                    }
11641                    {
11642                        let mut state = conn.state.lock().await;
11643                        state.stream_callbacks.insert(key.clone(), Vec::new());
11644                    }
11645
11646                    let stream = Arc::new(WebsocketStream::<Value> {
11647                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11648                        stream_or_id: stream_name.clone(),
11649                        url_path: Some("path1".to_string()),
11650                        callback: Mutex::new(None),
11651                        id: None,
11652                        _phantom: PhantomData,
11653                    });
11654
11655                    stream.on("message", |_| {}).await;
11656
11657                    let cb_guard = stream.callback.lock().await;
11658                    assert!(cb_guard.is_some());
11659
11660                    let callbacks = {
11661                        let state = conn.state.lock().await;
11662                        state.stream_callbacks.get(&key).unwrap().clone()
11663                    };
11664                    assert_eq!(callbacks.len(), 1);
11665                });
11666            }
11667
11668            #[test]
11669            fn url_path_routes_callback_to_correct_key_when_same_stream_name_used() {
11670                TOKIO_SHARED_RT.block_on(async {
11671                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11672                    let stream_name = "s1".to_string();
11673                    let conn = ws_base.common.connection_pool[0].clone();
11674
11675                    let key1 = ws_base.stream_key(&stream_name, Some("path1"));
11676                    let key2 = ws_base.stream_key(&stream_name, Some("path2"));
11677
11678                    {
11679                        let mut map = ws_base.connection_streams.lock().await;
11680                        map.insert(key1.clone(), conn.clone());
11681                        map.insert(key2.clone(), conn.clone());
11682                    }
11683                    {
11684                        let mut state = conn.state.lock().await;
11685                        state.stream_callbacks.insert(key1.clone(), Vec::new());
11686                        state.stream_callbacks.insert(key2.clone(), Vec::new());
11687                    }
11688
11689                    let stream_path1 = Arc::new(WebsocketStream::<Value> {
11690                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11691                        stream_or_id: stream_name.clone(),
11692                        url_path: Some("path1".to_string()),
11693                        callback: Mutex::new(None),
11694                        id: None,
11695                        _phantom: PhantomData,
11696                    });
11697
11698                    let stream_path2 = Arc::new(WebsocketStream::<Value> {
11699                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11700                        stream_or_id: stream_name.clone(),
11701                        url_path: Some("path2".to_string()),
11702                        callback: Mutex::new(None),
11703                        id: None,
11704                        _phantom: PhantomData,
11705                    });
11706
11707                    stream_path1.on("message", |_| {}).await;
11708                    stream_path2.on("message", |_| {}).await;
11709
11710                    let state = conn.state.lock().await;
11711                    assert_eq!(state.stream_callbacks.get(&key1).unwrap().len(), 1);
11712                    assert_eq!(state.stream_callbacks.get(&key2).unwrap().len(), 1);
11713                });
11714            }
11715        }
11716
11717        mod on_message {
11718            use super::*;
11719
11720            #[test]
11721            fn on_message_registers_callback_for_websocket_streams() {
11722                TOKIO_SHARED_RT.block_on(async {
11723                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11724                    let stream_name = "s".to_string();
11725                    let conn = ws_base.common.connection_pool[0].clone();
11726                    {
11727                        let mut map = ws_base.connection_streams.lock().await;
11728                        map.insert(stream_name.clone(), conn.clone());
11729                    }
11730                    {
11731                        let mut state = conn.state.lock().await;
11732                        state
11733                            .stream_callbacks
11734                            .insert(stream_name.clone(), Vec::new());
11735                    }
11736                    let stream = Arc::new(WebsocketStream::<Value> {
11737                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11738                        stream_or_id: stream_name.clone(),
11739                        url_path: None,
11740                        callback: Mutex::new(None),
11741                        id: None,
11742                        _phantom: PhantomData,
11743                    });
11744                    stream.on_message(|_v| {});
11745                    let callbacks = &conn.state.lock().await.stream_callbacks[&stream_name];
11746                    assert_eq!(callbacks.len(), 1);
11747                });
11748            }
11749
11750            #[test]
11751            fn on_message_twice_registers_two_callbacks_for_websocket_streams() {
11752                TOKIO_SHARED_RT.block_on(async {
11753                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11754                    let stream_name = "s".to_string();
11755                    let conn = ws_base.common.connection_pool[0].clone();
11756                    {
11757                        let mut map = ws_base.connection_streams.lock().await;
11758                        map.insert(stream_name.clone(), conn.clone());
11759                    }
11760                    {
11761                        let mut state = conn.state.lock().await;
11762                        state
11763                            .stream_callbacks
11764                            .insert(stream_name.clone(), Vec::new());
11765                    }
11766                    let stream = Arc::new(WebsocketStream::<Value> {
11767                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11768                        stream_or_id: stream_name.clone(),
11769                        url_path: None,
11770                        callback: Mutex::new(None),
11771                        id: None,
11772                        _phantom: PhantomData,
11773                    });
11774                    stream.on_message(|_v| {});
11775                    stream.on_message(|_v| {});
11776                    let callbacks = &conn.state.lock().await.stream_callbacks[&stream_name];
11777                    assert_eq!(callbacks.len(), 2);
11778                });
11779            }
11780
11781            #[test]
11782            fn on_message_registers_callback_for_websocket_api() {
11783                TOKIO_SHARED_RT.block_on(async {
11784                    let ws_base = create_websocket_api(None, None, None);
11785                    let identifier = "id1".to_string();
11786
11787                    let stream = Arc::new(WebsocketStream::<Value> {
11788                        websocket_base: WebsocketBase::WebsocketApi(ws_base.clone()),
11789                        stream_or_id: identifier.clone(),
11790                        url_path: None,
11791                        callback: Mutex::new(None),
11792                        id: None,
11793                        _phantom: PhantomData,
11794                    });
11795
11796                    stream.on_message(|_v: Value| {});
11797
11798                    let stream_callbacks = ws_base.stream_callbacks.lock().await;
11799                    let callbacks = stream_callbacks.get(&identifier).unwrap();
11800                    assert_eq!(callbacks.len(), 1);
11801                });
11802            }
11803
11804            #[test]
11805            fn on_message_twice_registers_two_callbacks_for_websocket_api() {
11806                TOKIO_SHARED_RT.block_on(async {
11807                    let ws_base = create_websocket_api(None, None, None);
11808                    let identifier = "id2".to_string();
11809
11810                    let stream = Arc::new(WebsocketStream::<Value> {
11811                        websocket_base: WebsocketBase::WebsocketApi(ws_base.clone()),
11812                        stream_or_id: identifier.clone(),
11813                        url_path: None,
11814                        callback: Mutex::new(None),
11815                        id: None,
11816                        _phantom: PhantomData,
11817                    });
11818
11819                    stream.on_message(|_v: Value| {});
11820                    stream.on_message(|_v: Value| {});
11821
11822                    let stream_callbacks = ws_base.stream_callbacks.lock().await;
11823                    let callbacks = stream_callbacks.get(&identifier).unwrap();
11824                    assert_eq!(callbacks.len(), 2);
11825                });
11826            }
11827        }
11828
11829        mod unsubscribe {
11830            use super::*;
11831
11832            #[test]
11833            fn without_callback_does_nothing() {
11834                TOKIO_SHARED_RT.block_on(async {
11835                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11836                    let stream_name = "s1".to_string();
11837                    let conn = ws_base.common.connection_pool[0].clone();
11838                    {
11839                        let mut map = ws_base.connection_streams.lock().await;
11840                        map.insert(stream_name.clone(), conn.clone());
11841                    }
11842                    let mut state = conn.state.lock().await;
11843                    state.stream_callbacks.insert(stream_name.clone(), vec![]);
11844                    drop(state);
11845                    let stream = Arc::new(WebsocketStream::<Value> {
11846                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11847                        stream_or_id: stream_name.clone(),
11848                        url_path: None,
11849                        callback: Mutex::new(None),
11850                        id: None,
11851                        _phantom: PhantomData,
11852                    });
11853                    stream.unsubscribe().await;
11854                    let state = conn.state.lock().await;
11855                    assert!(state.stream_callbacks.contains_key(&stream_name));
11856                });
11857            }
11858
11859            #[test]
11860            fn removes_registered_callback_and_clears_state() {
11861                TOKIO_SHARED_RT.block_on(async {
11862                    let ws_base = create_websocket_streams(Some("example.com"), None, None);
11863                    let stream_name = "s2".to_string();
11864                    let conn = ws_base.common.connection_pool[0].clone();
11865                    {
11866                        let mut map = ws_base.connection_streams.lock().await;
11867                        map.insert(stream_name.clone(), conn.clone());
11868                    }
11869                    {
11870                        let mut state = conn.state.lock().await;
11871                        state
11872                            .stream_callbacks
11873                            .insert(stream_name.clone(), Vec::new());
11874                    }
11875                    let stream = Arc::new(WebsocketStream::<Value> {
11876                        websocket_base: WebsocketBase::WebsocketStreams(ws_base.clone()),
11877                        stream_or_id: stream_name.clone(),
11878                        url_path: None,
11879                        callback: Mutex::new(None),
11880                        id: None,
11881                        _phantom: PhantomData,
11882                    });
11883                    stream.on("message", |_| {}).await;
11884                    {
11885                        let guard = stream.callback.lock().await;
11886                        assert!(guard.is_some());
11887                    }
11888                    stream.unsubscribe().await;
11889                    sleep(Duration::from_millis(10)).await;
11890                    let guard = stream.callback.lock().await;
11891                    assert!(guard.is_none());
11892                    let state = conn.state.lock().await;
11893                    assert!(
11894                        state
11895                            .stream_callbacks
11896                            .get(&stream_name)
11897                            .is_none_or(std::vec::Vec::is_empty)
11898                    );
11899                });
11900            }
11901
11902            #[test]
11903            fn without_callback_does_nothing_for_websocket_api() {
11904                TOKIO_SHARED_RT.block_on(async {
11905                    let ws_base = create_websocket_api(None, None, None);
11906                    let identifier = "id1".to_string();
11907
11908                    {
11909                        let mut stream_callbacks = ws_base.stream_callbacks.lock().await;
11910                        stream_callbacks.insert(identifier.clone(), Vec::new());
11911                    }
11912
11913                    let stream = Arc::new(WebsocketStream::<Value> {
11914                        websocket_base: WebsocketBase::WebsocketApi(ws_base.clone()),
11915                        stream_or_id: identifier.clone(),
11916                        url_path: None,
11917                        callback: Mutex::new(None),
11918                        id: None,
11919                        _phantom: PhantomData,
11920                    });
11921
11922                    stream.unsubscribe().await;
11923
11924                    let stream_callbacks = ws_base.stream_callbacks.lock().await;
11925                    assert!(stream_callbacks.contains_key(&identifier));
11926                    let callbacks = stream_callbacks.get(&identifier).unwrap();
11927                    assert!(callbacks.is_empty());
11928                });
11929            }
11930
11931            #[test]
11932            fn removes_registered_callback_and_clears_state_for_websocket_api() {
11933                TOKIO_SHARED_RT.block_on(async {
11934                    let ws_base = create_websocket_api(None, None, None);
11935                    let identifier = "id2".to_string();
11936
11937                    {
11938                        let mut stream_callbacks = ws_base.stream_callbacks.lock().await;
11939                        stream_callbacks.insert(identifier.clone(), Vec::new());
11940                    }
11941
11942                    let stream = Arc::new(WebsocketStream::<Value> {
11943                        websocket_base: WebsocketBase::WebsocketApi(ws_base.clone()),
11944                        stream_or_id: identifier.clone(),
11945                        url_path: None,
11946                        callback: Mutex::new(None),
11947                        id: None,
11948                        _phantom: PhantomData,
11949                    });
11950
11951                    stream.on("message", |_| {}).await;
11952
11953                    {
11954                        let stream_callbacks = ws_base.stream_callbacks.lock().await;
11955                        let callbacks = stream_callbacks
11956                            .get(&identifier)
11957                            .expect("Entry for 'id2' should exist");
11958                        assert_eq!(callbacks.len(), 1);
11959                    }
11960
11961                    stream.unsubscribe().await;
11962
11963                    {
11964                        let guard = stream.callback.lock().await;
11965                        assert!(guard.is_none());
11966                    }
11967
11968                    {
11969                        let stream_callbacks = ws_base.stream_callbacks.lock().await;
11970                        let callbacks = stream_callbacks
11971                            .get(&identifier)
11972                            .expect("Entry for 'id2' should still exist");
11973                        assert!(callbacks.is_empty());
11974                    }
11975                });
11976            }
11977        }
11978    }
11979
11980    mod create_stream_handler {
11981        use super::*;
11982
11983        #[test]
11984        fn create_stream_handler_without_id_registers_stream() {
11985            TOKIO_SHARED_RT.block_on(async {
11986                let ws = create_websocket_streams(Some("ws://example.com"), None, None);
11987                let stream_name = "foo".to_string();
11988                let handler = create_stream_handler::<serde_json::Value>(
11989                    WebsocketBase::WebsocketStreams(ws.clone()),
11990                    stream_name.clone(),
11991                    None,
11992                    None,
11993                )
11994                .await;
11995                assert_eq!(handler.stream_or_id, stream_name);
11996                assert!(handler.id.is_none());
11997                let map = ws.connection_streams.lock().await;
11998                assert!(map.contains_key(&stream_name));
11999            });
12000        }
12001
12002        #[test]
12003        fn create_stream_handler_with_custom_string_id_registers_stream_and_id() {
12004            TOKIO_SHARED_RT.block_on(async {
12005                let ws = create_websocket_streams(Some("ws://example.com"), None, None);
12006                let stream_name = "bar".to_string();
12007                let custom_id = StreamId::from("my-custom-id".to_string());
12008                let handler = create_stream_handler::<serde_json::Value>(
12009                    WebsocketBase::WebsocketStreams(ws.clone()),
12010                    stream_name.clone(),
12011                    Some(custom_id.clone()),
12012                    None,
12013                )
12014                .await;
12015                assert_eq!(handler.stream_or_id, stream_name);
12016                assert_eq!(handler.id, Some(custom_id));
12017                let map = ws.connection_streams.lock().await;
12018                assert!(map.contains_key(&stream_name));
12019            });
12020        }
12021
12022        #[test]
12023        fn create_stream_handler_with_custom_integer_id_registers_stream_and_id() {
12024            TOKIO_SHARED_RT.block_on(async {
12025                let ws = create_websocket_streams(Some("ws://example.com"), None, None);
12026                let stream_name = "bar".to_string();
12027                let custom_id = StreamId::from(123u32);
12028                let handler = create_stream_handler::<serde_json::Value>(
12029                    WebsocketBase::WebsocketStreams(ws.clone()),
12030                    stream_name.clone(),
12031                    Some(custom_id.clone()),
12032                    None,
12033                )
12034                .await;
12035                assert_eq!(handler.stream_or_id, stream_name);
12036                assert_eq!(handler.id, Some(custom_id));
12037                let map = ws.connection_streams.lock().await;
12038                assert!(map.contains_key(&stream_name));
12039            });
12040        }
12041
12042        #[test]
12043        fn create_stream_handler_without_id_registers_api_stream() {
12044            TOKIO_SHARED_RT.block_on(async {
12045                let ws_base = create_websocket_api(None, None, None);
12046                let identifier = "foo-api".to_string();
12047
12048                let handler = create_stream_handler::<Value>(
12049                    WebsocketBase::WebsocketApi(ws_base.clone()),
12050                    identifier.clone(),
12051                    None,
12052                    None,
12053                )
12054                .await;
12055
12056                assert_eq!(handler.stream_or_id, identifier);
12057                assert!(handler.id.is_none());
12058            });
12059        }
12060
12061        #[test]
12062        fn create_stream_handler_with_custom_string_id_registers_api_stream_and_id() {
12063            TOKIO_SHARED_RT.block_on(async {
12064                let ws_base = create_websocket_api(None, None, None);
12065                let identifier = "bar-api".to_string();
12066                let custom_id = StreamId::from("custom-123".to_string());
12067
12068                let handler = create_stream_handler::<Value>(
12069                    WebsocketBase::WebsocketApi(ws_base.clone()),
12070                    identifier.clone(),
12071                    Some(custom_id.clone()),
12072                    None,
12073                )
12074                .await;
12075
12076                assert_eq!(handler.stream_or_id, identifier);
12077                assert_eq!(handler.id, Some(custom_id));
12078            });
12079        }
12080
12081        #[test]
12082        fn create_stream_handler_with_custom_integer_id_registers_api_stream_and_id() {
12083            TOKIO_SHARED_RT.block_on(async {
12084                let ws_base = create_websocket_api(None, None, None);
12085                let identifier = "bar-api".to_string();
12086                let custom_id = StreamId::from(123u32);
12087
12088                let handler = create_stream_handler::<Value>(
12089                    WebsocketBase::WebsocketApi(ws_base.clone()),
12090                    identifier.clone(),
12091                    Some(custom_id.clone()),
12092                    None,
12093                )
12094                .await;
12095
12096                assert_eq!(handler.stream_or_id, identifier);
12097                assert_eq!(handler.id, Some(custom_id));
12098            });
12099        }
12100
12101        #[test]
12102        fn websocket_streams_without_url_path_registers_stream_key() {
12103            TOKIO_SHARED_RT.block_on(async {
12104                let ws = create_websocket_streams(Some("ws://example.com"), None, None);
12105                let stream_name = "foo".to_string();
12106
12107                let handler = create_stream_handler::<Value>(
12108                    WebsocketBase::WebsocketStreams(ws.clone()),
12109                    stream_name.clone(),
12110                    None,
12111                    None,
12112                )
12113                .await;
12114
12115                assert_eq!(handler.stream_or_id, stream_name);
12116                assert!(handler.id.is_none());
12117
12118                let map = ws.connection_streams.lock().await;
12119                assert!(map.contains_key("foo"));
12120            });
12121        }
12122
12123        #[test]
12124        fn websocket_streams_with_url_path_registers_prefixed_stream_key() {
12125            TOKIO_SHARED_RT.block_on(async {
12126                let ws = create_websocket_streams(Some("ws://example.com"), None, None);
12127
12128                {
12129                    let conn = ws.common.connection_pool[0].clone();
12130                    let mut st = conn.state.lock().await;
12131                    st.url_path = Some("path1".to_string());
12132                }
12133
12134                let stream_name = "foo".to_string();
12135
12136                let handler = create_stream_handler::<Value>(
12137                    WebsocketBase::WebsocketStreams(ws.clone()),
12138                    stream_name.clone(),
12139                    None,
12140                    Some("path1".to_string()),
12141                )
12142                .await;
12143
12144                assert_eq!(handler.stream_or_id, stream_name);
12145                assert!(handler.id.is_none());
12146
12147                let map = ws.connection_streams.lock().await;
12148                assert!(map.contains_key("path1::foo"));
12149            });
12150        }
12151
12152        #[test]
12153        fn websocket_streams_with_custom_id_preserves_id_and_registers_prefixed_key() {
12154            TOKIO_SHARED_RT.block_on(async {
12155                let ws = create_websocket_streams(Some("ws://example.com"), None, None);
12156
12157                {
12158                    let conn = ws.common.connection_pool[0].clone();
12159                    let mut st = conn.state.lock().await;
12160                    st.url_path = Some("path1".to_string());
12161                }
12162
12163                let stream_name = "bar".to_string();
12164                let custom_id = StreamId::from("my-custom-id".to_string());
12165
12166                let handler = create_stream_handler::<Value>(
12167                    WebsocketBase::WebsocketStreams(ws.clone()),
12168                    stream_name.clone(),
12169                    Some(custom_id.clone()),
12170                    Some("path1".to_string()),
12171                )
12172                .await;
12173
12174                assert_eq!(handler.stream_or_id, stream_name);
12175                assert_eq!(handler.id, Some(custom_id));
12176
12177                let map = ws.connection_streams.lock().await;
12178                assert!(map.contains_key("path1::bar"));
12179            });
12180        }
12181
12182        #[test]
12183        fn websocket_api_does_not_register_stream_in_connection_map() {
12184            TOKIO_SHARED_RT.block_on(async {
12185                let ws_base = create_websocket_api(None, None, None);
12186                let identifier = "foo-api".to_string();
12187
12188                let handler = create_stream_handler::<Value>(
12189                    WebsocketBase::WebsocketApi(ws_base.clone()),
12190                    identifier.clone(),
12191                    None,
12192                    Some("path1".to_string()),
12193                )
12194                .await;
12195
12196                assert_eq!(handler.stream_or_id, identifier);
12197                assert!(handler.id.is_none());
12198            });
12199        }
12200    }
12201
12202    mod websocket_connection_failure_reason {
12203        use super::*;
12204        use std::io::{Error as IoError, ErrorKind};
12205        use tokio_tungstenite::tungstenite::Error as TungsteniteError;
12206
12207        #[test]
12208        fn from_tungstenite_error_classifies_connection_closed() {
12209            let error = TungsteniteError::ConnectionClosed;
12210            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12211            assert!(matches!(
12212                reason,
12213                WebsocketConnectionFailureReason::ConnectionReset
12214            ));
12215            assert!(reason.should_reconnect());
12216        }
12217
12218        #[test]
12219        fn from_tungstenite_error_classifies_already_closed() {
12220            let error = TungsteniteError::AlreadyClosed;
12221            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12222            assert!(matches!(
12223                reason,
12224                WebsocketConnectionFailureReason::ConnectionReset
12225            ));
12226            assert!(reason.should_reconnect());
12227        }
12228
12229        #[test]
12230        fn from_tungstenite_error_classifies_io_errors() {
12231            // Test ConnectionReset
12232            let io_error = IoError::new(ErrorKind::ConnectionReset, "connection reset");
12233            let error = TungsteniteError::Io(io_error);
12234            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12235            assert!(matches!(
12236                reason,
12237                WebsocketConnectionFailureReason::ConnectionReset
12238            ));
12239            assert!(reason.should_reconnect());
12240
12241            // Test ConnectionAborted
12242            let io_error = IoError::new(ErrorKind::ConnectionAborted, "connection aborted");
12243            let error = TungsteniteError::Io(io_error);
12244            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12245            assert!(matches!(
12246                reason,
12247                WebsocketConnectionFailureReason::ConnectionReset
12248            ));
12249            assert!(reason.should_reconnect());
12250
12251            // Test TimedOut
12252            let io_error = IoError::new(ErrorKind::TimedOut, "timed out");
12253            let error = TungsteniteError::Io(io_error);
12254            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12255            assert!(matches!(
12256                reason,
12257                WebsocketConnectionFailureReason::NetworkInterruption
12258            ));
12259            assert!(reason.should_reconnect());
12260
12261            // Test UnexpectedEof
12262            let io_error = IoError::new(ErrorKind::UnexpectedEof, "unexpected eof");
12263            let error = TungsteniteError::Io(io_error);
12264            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12265            assert!(matches!(
12266                reason,
12267                WebsocketConnectionFailureReason::StreamEnded
12268            ));
12269            assert!(reason.should_reconnect());
12270
12271            // Test PermissionDenied
12272            let io_error = IoError::new(ErrorKind::PermissionDenied, "permission denied");
12273            let error = TungsteniteError::Io(io_error);
12274            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12275            assert!(matches!(
12276                reason,
12277                WebsocketConnectionFailureReason::AuthenticationFailure
12278            ));
12279            assert!(!reason.should_reconnect());
12280
12281            // Test other IO errors default to NetworkInterruption
12282            let io_error = IoError::other("other error");
12283            let error = TungsteniteError::Io(io_error);
12284            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12285            assert!(matches!(
12286                reason,
12287                WebsocketConnectionFailureReason::NetworkInterruption
12288            ));
12289            assert!(reason.should_reconnect());
12290        }
12291
12292        #[test]
12293        fn from_tungstenite_error_classifies_protocol_errors() {
12294            // Protocol error -> ProtocolViolation
12295            use tokio_tungstenite::tungstenite::error::ProtocolError;
12296            let protocol_error = ProtocolError::ResetWithoutClosingHandshake;
12297            let error = TungsteniteError::Protocol(protocol_error);
12298            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12299            assert!(matches!(
12300                reason,
12301                WebsocketConnectionFailureReason::UnexpectedClose
12302            ));
12303            assert!(reason.should_reconnect());
12304
12305            // UTF8 error -> ProtocolViolation
12306            let error = TungsteniteError::Utf8;
12307            let reason = WebsocketConnectionFailureReason::from_tungstenite_error(&error);
12308            assert!(matches!(
12309                reason,
12310                WebsocketConnectionFailureReason::ProtocolViolation
12311            ));
12312            assert!(!reason.should_reconnect());
12313        }
12314
12315        #[test]
12316        fn from_close_code_classifies_standard_codes() {
12317            // Normal closure
12318            let reason = WebsocketConnectionFailureReason::from_close_code(1000, false);
12319            assert!(matches!(
12320                reason,
12321                WebsocketConnectionFailureReason::NormalClose
12322            ));
12323            assert!(!reason.should_reconnect());
12324
12325            // Going away (server restart) -> ServerTemporaryError
12326            let reason = WebsocketConnectionFailureReason::from_close_code(1001, false);
12327            assert!(matches!(
12328                reason,
12329                WebsocketConnectionFailureReason::ServerTemporaryError
12330            ));
12331            assert!(reason.should_reconnect());
12332
12333            // Protocol error -> ProtocolViolation
12334            let reason = WebsocketConnectionFailureReason::from_close_code(1002, false);
12335            assert!(matches!(
12336                reason,
12337                WebsocketConnectionFailureReason::ProtocolViolation
12338            ));
12339            assert!(!reason.should_reconnect());
12340
12341            // Abnormal closure -> UnexpectedClose
12342            let reason = WebsocketConnectionFailureReason::from_close_code(1006, false);
12343            assert!(matches!(
12344                reason,
12345                WebsocketConnectionFailureReason::UnexpectedClose
12346            ));
12347            assert!(reason.should_reconnect());
12348
12349            // Policy violation -> PermanentServerError
12350            let reason = WebsocketConnectionFailureReason::from_close_code(1008, false);
12351            assert!(matches!(
12352                reason,
12353                WebsocketConnectionFailureReason::PermanentServerError
12354            ));
12355            assert!(!reason.should_reconnect());
12356
12357            // Server error -> ServerTemporaryError
12358            let reason = WebsocketConnectionFailureReason::from_close_code(1011, false);
12359            assert!(matches!(
12360                reason,
12361                WebsocketConnectionFailureReason::ServerTemporaryError
12362            ));
12363            assert!(reason.should_reconnect());
12364
12365            // TLS handshake failure -> ConfigurationError
12366            let reason = WebsocketConnectionFailureReason::from_close_code(1015, false);
12367            assert!(matches!(
12368                reason,
12369                WebsocketConnectionFailureReason::ConfigurationError
12370            ));
12371            assert!(!reason.should_reconnect());
12372
12373            // Business/application errors -> PermanentServerError
12374            let reason = WebsocketConnectionFailureReason::from_close_code(4000, false);
12375            assert!(matches!(
12376                reason,
12377                WebsocketConnectionFailureReason::PermanentServerError
12378            ));
12379            assert!(!reason.should_reconnect());
12380
12381            let reason = WebsocketConnectionFailureReason::from_close_code(4999, false);
12382            assert!(matches!(
12383                reason,
12384                WebsocketConnectionFailureReason::PermanentServerError
12385            ));
12386            assert!(!reason.should_reconnect());
12387
12388            // Unknown codes default to UnexpectedClose
12389            let reason = WebsocketConnectionFailureReason::from_close_code(9999, false);
12390            assert!(matches!(
12391                reason,
12392                WebsocketConnectionFailureReason::UnexpectedClose
12393            ));
12394            assert!(reason.should_reconnect());
12395        }
12396
12397        #[test]
12398        fn from_close_code_handles_user_initiated() {
12399            // Any code with user_initiated=true should return UserInitiatedClose
12400            let reason = WebsocketConnectionFailureReason::from_close_code(1000, true);
12401            assert!(matches!(
12402                reason,
12403                WebsocketConnectionFailureReason::UserInitiatedClose
12404            ));
12405            assert!(!reason.should_reconnect());
12406
12407            let reason = WebsocketConnectionFailureReason::from_close_code(1006, true);
12408            assert!(matches!(
12409                reason,
12410                WebsocketConnectionFailureReason::UserInitiatedClose
12411            ));
12412            assert!(!reason.should_reconnect());
12413
12414            let reason = WebsocketConnectionFailureReason::from_close_code(4000, true);
12415            assert!(matches!(
12416                reason,
12417                WebsocketConnectionFailureReason::UserInitiatedClose
12418            ));
12419            assert!(!reason.should_reconnect());
12420        }
12421
12422        #[test]
12423        fn should_reconnect_logic() {
12424            // Reconnectable failures
12425            assert!(WebsocketConnectionFailureReason::NetworkInterruption.should_reconnect());
12426            assert!(WebsocketConnectionFailureReason::ConnectionReset.should_reconnect());
12427            assert!(WebsocketConnectionFailureReason::ServerTemporaryError.should_reconnect());
12428            assert!(WebsocketConnectionFailureReason::UnexpectedClose.should_reconnect());
12429            assert!(WebsocketConnectionFailureReason::StreamEnded.should_reconnect());
12430
12431            // Non-reconnectable failures
12432            assert!(!WebsocketConnectionFailureReason::AuthenticationFailure.should_reconnect());
12433            assert!(!WebsocketConnectionFailureReason::ProtocolViolation.should_reconnect());
12434            assert!(!WebsocketConnectionFailureReason::ConfigurationError.should_reconnect());
12435            assert!(!WebsocketConnectionFailureReason::UserInitiatedClose.should_reconnect());
12436            assert!(!WebsocketConnectionFailureReason::PermanentServerError.should_reconnect());
12437            assert!(!WebsocketConnectionFailureReason::NormalClose.should_reconnect());
12438        }
12439
12440        #[test]
12441        fn debug_and_clone_work() {
12442            let reason = WebsocketConnectionFailureReason::NetworkInterruption;
12443            let cloned = reason;
12444            let debug_str = format!("{:?}", reason);
12445
12446            assert!(matches!(
12447                cloned,
12448                WebsocketConnectionFailureReason::NetworkInterruption
12449            ));
12450            assert!(debug_str.contains("NetworkInterruption"));
12451        }
12452    }
12453}