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 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 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 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#[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
209struct 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 let is_close = matches!(msg, Message::Close(_));
1336 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 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 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 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 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 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 info!("WebSocket stream ended for connection {}", reader_conn.id);
1590
1591 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 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 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 let conn = Arc::clone(conn);
1733 rt.spawn(async move {
1734 close(&mut *conn.state.lock().await);
1735 });
1736 }
1737 }
1738 }
1739
1740 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 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 fn drop(&mut self) {
1952 self.common.close_on_drop();
1953 }
1954}
1955
1956impl WebsocketApi {
1957 #[must_use]
1958 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 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 pub async fn disconnect(&self) -> Result<(), WebsocketError> {
2072 self.common.disconnect().await
2073 }
2074
2075 pub async fn is_connected(&self) -> bool {
2081 self.common.is_connected(None).await
2082 }
2083
2084 pub async fn ping_server(&self) {
2089 self.common.ping_server().await;
2090 }
2091
2092 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 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 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 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 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 fn drop(&mut self) {
2565 self.common.close_on_drop();
2566 }
2567}
2568
2569impl WebsocketStreams {
2570 #[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 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 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 fn is_signed_streams(&self) -> bool {
2740 self.signed_streams.load(Ordering::Relaxed)
2741 }
2742
2743 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 fn prepare_signed_url(&self) -> String {
2776 let base = self.configuration.ws_url.as_deref().unwrap_or("");
2777 let query = format!(
2778 "random={}&recvWindow={}×tamp={}",
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 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 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 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 pub async fn is_connected(&self) -> bool {
2872 self.common.is_connected(None).await
2873 }
2874
2875 pub async fn ping_server(&self) {
2884 self.common.ping_server().await;
2885 }
2886
2887 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
3680pub 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 struct AbortOnDrop(tokio::task::JoinHandle<()>);
3774
3775 impl Drop for AbortOnDrop {
3776 fn drop(&mut self) {
3777 self.0.abort();
3778 }
3779 }
3780
3781 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 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 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 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 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 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 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 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 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 *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 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 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 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 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(¤t)).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(¤t)).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 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 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 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 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 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(¤t_writer))
6555 );
6556
6557 state.close_initiated = true;
6558 assert_eq!(
6559 state.begin_reconnect(
6560 ¤t_writer,
6561 WebsocketConnectionFailureReason::UnexpectedClose,
6562 ),
6563 ReconnectDisposition::Rejected
6564 );
6565 state.close_initiated = false;
6566
6567 assert_eq!(
6568 state.begin_reconnect(
6569 ¤t_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 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 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 }
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×tamp=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×tamp={}",
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×tamp="));
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 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 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 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 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 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 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 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 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 let reason = WebsocketConnectionFailureReason::from_close_code(1000, false);
12319 assert!(matches!(
12320 reason,
12321 WebsocketConnectionFailureReason::NormalClose
12322 ));
12323 assert!(!reason.should_reconnect());
12324
12325 let reason = WebsocketConnectionFailureReason::from_close_code(1001, false);
12327 assert!(matches!(
12328 reason,
12329 WebsocketConnectionFailureReason::ServerTemporaryError
12330 ));
12331 assert!(reason.should_reconnect());
12332
12333 let reason = WebsocketConnectionFailureReason::from_close_code(1002, false);
12335 assert!(matches!(
12336 reason,
12337 WebsocketConnectionFailureReason::ProtocolViolation
12338 ));
12339 assert!(!reason.should_reconnect());
12340
12341 let reason = WebsocketConnectionFailureReason::from_close_code(1006, false);
12343 assert!(matches!(
12344 reason,
12345 WebsocketConnectionFailureReason::UnexpectedClose
12346 ));
12347 assert!(reason.should_reconnect());
12348
12349 let reason = WebsocketConnectionFailureReason::from_close_code(1008, false);
12351 assert!(matches!(
12352 reason,
12353 WebsocketConnectionFailureReason::PermanentServerError
12354 ));
12355 assert!(!reason.should_reconnect());
12356
12357 let reason = WebsocketConnectionFailureReason::from_close_code(1011, false);
12359 assert!(matches!(
12360 reason,
12361 WebsocketConnectionFailureReason::ServerTemporaryError
12362 ));
12363 assert!(reason.should_reconnect());
12364
12365 let reason = WebsocketConnectionFailureReason::from_close_code(1015, false);
12367 assert!(matches!(
12368 reason,
12369 WebsocketConnectionFailureReason::ConfigurationError
12370 ));
12371 assert!(!reason.should_reconnect());
12372
12373 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 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 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 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 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}