Skip to main content

arch_sdk/client/
websocket.rs

1use crate::types::{
2    Event, EventFilter, EventTopic, SubscriptionErrorResponse, SubscriptionResponse,
3    SubscriptionStatus, UnsubscribeResponse, WebSocketRequest,
4};
5use crate::{SubscriptionRequest, UnsubscribeRequest};
6use futures::future::{BoxFuture, FutureExt};
7use futures::{SinkExt, StreamExt};
8use serde::Deserialize;
9use std::collections::HashMap;
10use std::future::Future;
11use std::panic::{self, AssertUnwindSafe};
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::net::TcpStream;
15use tokio::sync::{mpsc, Mutex, RwLock};
16use tokio_tungstenite::tungstenite::Bytes;
17use tokio_tungstenite::{connect_async, tungstenite::protocol::Message, WebSocketStream};
18use tracing::{debug, error, info, warn};
19
20/// Error types for the WebSocket client
21#[derive(Debug, Clone, thiserror::Error)]
22pub enum WebSocketError {
23    /// Failed to connect to the server
24    #[error("Failed to connect to the server: {0}")]
25    ConnectionFailed(String),
26    /// Failed to send a message
27    #[error("Failed to send a message: {0}")]
28    SendFailed(String),
29    /// Failed to parse a response
30    #[error("Failed to parse a response: {0}")]
31    ParseError(String),
32    /// Subscription failed
33    #[error("Failed to subscribe: {0}")]
34    SubscriptionFailed(String),
35    /// Unsubscription failed
36    #[error("Failed to unsubscribe: {0}")]
37    UnsubscriptionFailed(String),
38    /// Failed to read from the WebSocket
39    #[error("Failed to read from the WebSocket: {0}")]
40    ReadFailed(String),
41    /// General error
42    #[error("Other error: {0}")]
43    Other(String),
44}
45
46/// Event handler that receives events from subscriptions
47pub type EventCallback = Box<dyn Fn(Event) + Send + Sync + 'static>;
48
49/// Connection status change handler
50pub type ConnectionCallback = Box<dyn Fn(bool) + Send + Sync + 'static>;
51
52/// Asynchronous event handler that can perform async operations
53pub type AsyncEventCallback = Box<dyn Fn(Event) -> BoxFuture<'static, ()> + Send + Sync + 'static>;
54
55/// A handler for a subscription
56struct SubscriptionHandler {
57    /// The topic that was subscribed to
58    topic: EventTopic,
59    /// The filter used for subscription
60    filter: EventFilter,
61    /// Whether this subscription is pending server confirmation
62    pending: bool,
63}
64
65/// Represents all possible types of WebSocket messages that can be received
66#[derive(Debug, Deserialize)]
67#[serde(untagged)]
68pub enum WebSocketMessage {
69    /// An event from the server
70    Event(Event),
71    /// Response to a subscription request
72    SubscriptionResponse(SubscriptionResponse),
73    /// Response to an unsubscribe request
74    UnsubscribeResponse(UnsubscribeResponse),
75    /// Error response
76    ErrorResponse(SubscriptionErrorResponse),
77}
78
79/// Backoff strategy for reconnection attempts
80#[derive(Clone, Debug)]
81pub enum BackoffStrategy {
82    /// Constant delay between reconnection attempts
83    Constant(Duration),
84
85    /// Linear increase in delay (initial + attempt * step)
86    Linear { initial: Duration, step: Duration },
87
88    /// Exponential increase in delay with jitter
89    /// delay = min(max_delay, initial * (factor ^ attempt) * (1 ± jitter))
90    Exponential {
91        initial: Duration,
92        factor: f32,
93        max_delay: Duration,
94        jitter: f32, // Random factor to avoid thundering herd (0.0 - 1.0)
95    },
96}
97
98impl BackoffStrategy {
99    /// Create a default exponential backoff strategy
100    pub fn default_exponential() -> Self {
101        Self::Exponential {
102            initial: Duration::from_secs(1),
103            factor: 2.0,
104            max_delay: Duration::from_secs(5),
105            jitter: 0.1,
106        }
107    }
108
109    /// Calculate the next delay based on attempt number
110    pub fn next_delay(&self, attempt: usize) -> Duration {
111        match self {
112            Self::Constant(duration) => *duration,
113
114            Self::Linear { initial, step } => *initial + (*step * attempt as u32),
115
116            Self::Exponential {
117                initial,
118                factor,
119                max_delay,
120                jitter,
121            } => {
122                // Calculate base exponential delay
123                let base_ms = initial.as_millis() as f32 * factor.powi(attempt as i32);
124
125                // Apply jitter to avoid thundering herd
126                let jitter_factor = 1.0 - jitter + rand::random::<f32>() * jitter * 2.0;
127                let jittered_ms = base_ms * jitter_factor;
128
129                // Ensure we don't exceed max delay
130                let capped_ms = jittered_ms.min(max_delay.as_millis() as f32);
131
132                Duration::from_millis(capped_ms as u64)
133            }
134        }
135    }
136}
137
138/// A client for the WebSocket API
139pub struct WebSocketClient {
140    /// Channel for sending messages to the WebSocket
141    sender: mpsc::Sender<Message>,
142    /// Active subscriptions
143    subscriptions: Arc<RwLock<HashMap<String, SubscriptionHandler>>>,
144    /// Event callbacks by topic
145    event_callbacks: Arc<RwLock<HashMap<EventTopic, Vec<EventCallback>>>>,
146    /// Connection status callbacks
147    connection_callbacks: Arc<RwLock<Vec<ConnectionCallback>>>,
148    /// Connection status
149    connected: Arc<Mutex<bool>>,
150    /// Server URL
151    server_url: Arc<String>,
152    /// Auto-reconnect settings
153    auto_reconnect: Arc<Mutex<bool>>,
154    /// Backoff_strategy
155    backoff_strategy: Arc<Mutex<BackoffStrategy>>,
156    /// Maximum reconnect attempts (0 = infinite)
157    max_reconnect_attempts: Arc<Mutex<usize>>,
158    /// Whether the client should keep running
159    running: Arc<Mutex<bool>>,
160    /// Message processor cancellation channel
161    cancel_tx: Option<mpsc::Sender<()>>,
162    /// Async event callbacks by topic
163    async_event_callbacks: Arc<RwLock<HashMap<EventTopic, Vec<AsyncEventCallback>>>>,
164    /// Connection state change in progress
165    state_change_lock: Arc<Mutex<()>>,
166    /// Keep-alive interval if enabled
167    keep_alive_interval: Arc<Mutex<Option<Duration>>>,
168    /// Keep-alive task handle
169    keep_alive_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
170}
171
172impl WebSocketClient {
173    /// Create a new WebSocket client without connecting
174    pub fn new(url: &str) -> Self {
175        // Create shared state
176        let subscriptions = Arc::new(RwLock::new(HashMap::new()));
177        let event_callbacks = Arc::new(RwLock::new(HashMap::new()));
178        let connection_callbacks = Arc::new(RwLock::new(Vec::new()));
179        let connected = Arc::new(Mutex::new(false));
180        let server_url = Arc::new(url.to_string());
181        let auto_reconnect = Arc::new(Mutex::new(false));
182        let backoff_strategy = Arc::new(Mutex::new(BackoffStrategy::default_exponential()));
183        let max_reconnect_attempts = Arc::new(Mutex::new(0));
184        let running = Arc::new(Mutex::new(true));
185        let async_event_callbacks = Arc::new(RwLock::new(HashMap::new()));
186        let state_change_lock = Arc::new(Mutex::new(()));
187        let keep_alive_interval = Arc::new(Mutex::new(None));
188        let keep_alive_handle = Arc::new(Mutex::new(None));
189
190        // Create a dummy sender that will be replaced when connected
191        let (sender, _) = mpsc::channel::<Message>(100);
192
193        WebSocketClient {
194            sender,
195            subscriptions,
196            event_callbacks,
197            connection_callbacks,
198            connected,
199            server_url,
200            auto_reconnect,
201            backoff_strategy,
202            max_reconnect_attempts,
203            running,
204            cancel_tx: None,
205            async_event_callbacks,
206            state_change_lock,
207            keep_alive_interval,
208            keep_alive_handle,
209        }
210    }
211
212    /// Connect to the WebSocket server
213    pub async fn connect(&mut self) -> Result<(), WebSocketError> {
214        // Ensure we're not already in the process of connecting
215        let _connection_lock = self.state_change_lock.lock().await;
216
217        // If already connected, return early
218        if *self.connected.lock().await {
219            return Ok(());
220        }
221
222        // Cancel any existing processor task before starting a new one
223        if let Some(cancel_tx) = self.cancel_tx.take() {
224            // Send cancellation signal and wait a moment for it to be processed
225            let _ = cancel_tx.send(()).await;
226            tokio::time::sleep(Duration::from_millis(50)).await;
227        }
228
229        // Create a channel for cancellation
230        let (cancel_tx, cancel_rx) = mpsc::channel::<()>(1);
231        self.cancel_tx = Some(cancel_tx);
232
233        // Establish connection
234        let connect_result = Self::establish_new_connection(
235            &self.server_url,
236            &self.connected,
237            &self.connection_callbacks,
238            &self.keep_alive_handle,
239            &self.keep_alive_interval,
240            &self.running,
241        )
242        .await;
243
244        // Handle connection failure
245        if let Err(e) = &connect_result {
246            // Clear cancel_tx if connection fails
247            self.cancel_tx = None;
248            return Err(e.clone());
249        }
250
251        // Unwrap the successful connection result
252        let (read, sender) = connect_result.unwrap();
253
254        // Replace the dummy sender with the real one
255        self.sender = sender.clone();
256
257        // TODO: Store spawned task handle for cleanup
258        let _task_handle = tokio::spawn(Self::message_processor(
259            read,
260            sender,
261            self.subscriptions.clone(),
262            self.event_callbacks.clone(),
263            self.async_event_callbacks.clone(),
264            self.connection_callbacks.clone(),
265            self.connected.clone(),
266            self.keep_alive_handle.clone(),
267            self.keep_alive_interval.clone(),
268            self.running.clone(),
269            self.server_url.clone(),
270            self.auto_reconnect.clone(),
271            self.backoff_strategy.clone(),
272            self.max_reconnect_attempts.clone(),
273            cancel_rx,
274        ));
275
276        Ok(())
277    }
278
279    // Update the static connect method to use the new pattern
280    pub async fn connect_static(url: &str) -> Result<Self, WebSocketError> {
281        let mut client = Self::new(url);
282        client.connect().await?;
283        Ok(client)
284    }
285
286    /// Establish a new WebSocket connection
287    /// Used by both initial connection and reconnection
288    async fn establish_new_connection(
289        server_url: &Arc<String>,
290        connected: &Arc<Mutex<bool>>,
291        connection_callbacks: &Arc<RwLock<Vec<ConnectionCallback>>>,
292        keep_alive_handle: &Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
293        keep_alive_interval: &Arc<Mutex<Option<Duration>>>,
294        running: &Arc<Mutex<bool>>,
295    ) -> Result<
296        (
297            futures::stream::SplitStream<
298                WebSocketStream<tokio_tungstenite::MaybeTlsStream<TcpStream>>,
299            >,
300            mpsc::Sender<Message>,
301        ),
302        WebSocketError,
303    > {
304        // Connect to the server
305        let ws_stream = Self::establish_connection(server_url).await?;
306
307        // Split the WebSocket stream
308        let (write, read) = ws_stream.split();
309
310        // Set up the writer task
311        let sender = Self::spawn_writer_task(write);
312
313        // Keep connection alive with ping messages
314        if let Some(interval) = *keep_alive_interval.lock().await {
315            Self::restart_keep_alive(keep_alive_handle, &sender, running, connected, interval)
316                .await?;
317        }
318
319        // Mark as connected
320        Self::update_connection_status(connected, true, connection_callbacks).await;
321
322        Ok((read, sender))
323    }
324
325    /// Attempt to reconnect to the server
326    #[allow(clippy::too_many_arguments)]
327    async fn attempt_reconnection(
328        server_url: &Arc<String>,
329        connected: &Arc<Mutex<bool>>,
330        connection_callbacks: &Arc<RwLock<Vec<ConnectionCallback>>>,
331        subscriptions: &Arc<RwLock<HashMap<String, SubscriptionHandler>>>,
332        keep_alive_handle: &Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
333        keep_alive_interval: &Arc<Mutex<Option<Duration>>>,
334        running: &Arc<Mutex<bool>>,
335        backoff_strategy: &Arc<Mutex<BackoffStrategy>>,
336        max_reconnect_attempts: &Arc<Mutex<usize>>,
337        reconnect_attempts: &mut usize,
338        cancel_rx: &mut mpsc::Receiver<()>,
339    ) -> Result<
340        (
341            futures::stream::SplitStream<
342                WebSocketStream<tokio_tungstenite::MaybeTlsStream<TcpStream>>,
343            >,
344            mpsc::Sender<Message>,
345        ),
346        WebSocketError,
347    > {
348        loop {
349            // Check max reconnect attempts
350            let max_attempts = *max_reconnect_attempts.lock().await;
351            if max_attempts > 0 && *reconnect_attempts >= max_attempts {
352                error!("Maximum reconnection attempts reached ({})", max_attempts);
353                return Err(WebSocketError::ConnectionFailed(
354                    "Maximum reconnection attempts reached".to_string(),
355                ));
356            }
357
358            // Get the next delay from the backoff strategy
359            let delay = backoff_strategy
360                .lock()
361                .await
362                .next_delay(*reconnect_attempts);
363            tokio::time::sleep(delay).await;
364
365            *reconnect_attempts += 1;
366            info!(
367                "Attempting to reconnect (attempt {}, delay: {:?})...",
368                reconnect_attempts, delay
369            );
370
371            // Try to reconnect using our common connection logic
372            match Self::establish_new_connection(
373                server_url,
374                connected,
375                connection_callbacks,
376                keep_alive_handle,
377                keep_alive_interval,
378                running,
379            )
380            .await
381            {
382                Ok((read, sender)) => {
383                    // Re-subscribe to all topics
384                    if let Err(e) = Self::resubscribe_all(&sender, subscriptions).await {
385                        error!("Failed to re-subscribe: {}", e);
386                    }
387
388                    return Ok((read, sender));
389                }
390                Err(e) => {
391                    error!("Reconnection failed: {}", e);
392                    // Continue in the reconnection loop
393                }
394            }
395
396            // Check for cancellation during reconnection attempts
397            if let Ok(Some(())) =
398                tokio::time::timeout(tokio::time::Duration::from_millis(10), cancel_rx.recv()).await
399            {
400                return Err(WebSocketError::Other(
401                    "Cancelled during reconnection".to_string(),
402                ));
403            }
404        }
405    }
406
407    /// Notify connection status changes
408    async fn notify_connection_status(
409        connected: bool,
410        callbacks: &Arc<RwLock<Vec<ConnectionCallback>>>,
411    ) {
412        let callbacks_guard = callbacks.read().await;
413        for callback in callbacks_guard.iter() {
414            callback(connected);
415        }
416    }
417
418    /// Handle subscription responses consistently
419    async fn handle_subscription_response(
420        response: SubscriptionResponse,
421        subscriptions: &Arc<RwLock<HashMap<String, SubscriptionHandler>>>,
422    ) {
423        info!("Received subscription response: {:?}", response);
424
425        // Try to find subscription by request_id first, then fall back to topic
426        let mut subs = subscriptions.write().await;
427
428        // Determine which key to look for
429        let Some(lookup_key) = &response.request_id else {
430            warn!("Received subscription response for unknown subscription");
431            return;
432        };
433
434        if let Some(handler) = subs.remove(lookup_key) {
435            if matches!(response.status, SubscriptionStatus::Subscribed) {
436                // Store with subscription ID from server
437                subs.insert(
438                    response.subscription_id.clone(),
439                    SubscriptionHandler {
440                        topic: handler.topic,
441                        filter: handler.filter,
442                        pending: false,
443                    },
444                );
445            }
446        } else {
447            warn!(
448                "Received subscription response for unknown subscription: {:?}",
449                response
450            );
451        }
452    }
453
454    /// Re-subscribe to all active subscriptions after reconnection
455    async fn resubscribe_all(
456        sender: &mpsc::Sender<Message>,
457        subscriptions: &Arc<RwLock<HashMap<String, SubscriptionHandler>>>,
458    ) -> Result<(), WebSocketError> {
459        // Collect non-pending subscriptions and mark for replacement
460        let resubscribe_list = {
461            let mut subs = subscriptions.write().await;
462
463            // Identify subscriptions that need to be resubscribed
464            let to_resubscribe: Vec<_> = subs
465                .iter()
466                .map(|(id, handler)| (id.clone(), handler.topic.clone(), handler.filter.clone()))
467                .collect();
468
469            // Remove the old subscription entries from the map
470            subs.clear();
471
472            to_resubscribe
473        };
474
475        // Create new pending subscriptions
476        for (_, topic, filter) in resubscribe_list {
477            // Create a unique pending ID
478            let pending_id = format!("pending-{}-{}", topic, uuid::Uuid::new_v4());
479
480            // Add as pending subscription
481            {
482                let mut subs = subscriptions.write().await;
483                subs.insert(
484                    pending_id.clone(),
485                    SubscriptionHandler {
486                        topic: topic.clone(),
487                        filter: filter.clone(),
488                        pending: true,
489                    },
490                );
491            }
492
493            // Send subscription request
494            let request = WebSocketRequest::Subscribe(SubscriptionRequest {
495                topic,
496                filter,
497                request_id: Some(pending_id),
498            });
499
500            let message = serde_json::to_string(&request).map_err(|e| {
501                WebSocketError::Other(format!("Failed to serialize request: {}", e))
502            })?;
503
504            sender
505                .send(Message::Text(message.into()))
506                .await
507                .map_err(|e| WebSocketError::SendFailed(e.to_string()))?;
508        }
509
510        Ok(())
511    }
512
513    /// Process a single WebSocket message
514    async fn process_message(
515        message: Message,
516        sender: &mpsc::Sender<Message>,
517        subscriptions: &Arc<RwLock<HashMap<String, SubscriptionHandler>>>,
518        event_callbacks: &Arc<RwLock<HashMap<EventTopic, Vec<EventCallback>>>>,
519        async_event_callbacks: &Arc<RwLock<HashMap<EventTopic, Vec<AsyncEventCallback>>>>,
520    ) -> bool {
521        // Returns true if disconnected
522        match message {
523            Message::Text(text) => {
524                // Handle text message
525                match serde_json::from_str::<WebSocketMessage>(&text) {
526                    Ok(WebSocketMessage::Event(event)) => {
527                        Self::handle_event(event, event_callbacks, async_event_callbacks).await;
528                    }
529                    Ok(WebSocketMessage::SubscriptionResponse(response)) => {
530                        Self::handle_subscription_response(response, subscriptions).await;
531                    }
532                    Ok(WebSocketMessage::UnsubscribeResponse(response)) => {
533                        Self::handle_unsubscribe_response(response, subscriptions).await;
534                    }
535                    Ok(WebSocketMessage::ErrorResponse(error)) => {
536                        warn!("Subscription error: {}", error.error);
537                    }
538                    Err(e) => {
539                        error!("Failed to parse WebSocket message: {}", e);
540                        debug!("Message content: {}", text);
541                    }
542                }
543                false
544            }
545            Message::Binary(_) => {
546                debug!("Received binary message");
547                false
548            }
549            Message::Ping(data) => {
550                // Handle ping - automatically respond with pong
551                if let Err(e) = sender.send(Message::Pong(data)).await {
552                    warn!("Failed to send pong: {}", e);
553                }
554                false
555            }
556            Message::Pong(_) => false, // Handle pong message (keep-alive response)
557            Message::Frame(_) => false, // Handle raw frame
558            Message::Close(_) => true, // Connection closed
559        }
560    }
561
562    /// Handle an event message with support for both sync and async callbacks
563    async fn handle_event(
564        event: Event,
565        event_callbacks: &Arc<RwLock<HashMap<EventTopic, Vec<EventCallback>>>>,
566        async_event_callbacks: &Arc<RwLock<HashMap<EventTopic, Vec<AsyncEventCallback>>>>,
567    ) {
568        let topic = event.topic();
569
570        // Process synchronous callbacks
571        {
572            let callbacks = event_callbacks.read().await;
573            if let Some(handlers) = callbacks.get(&topic) {
574                for handler in handlers {
575                    // Catch panics from callback to prevent crashing the WebSocket loop
576                    match panic::catch_unwind(AssertUnwindSafe(|| {
577                        handler(event.clone());
578                    })) {
579                        Ok(_) => {}
580                        Err(e) => {
581                            // Log the panic but don't crash
582                            let panic_msg = if let Some(s) = e.downcast_ref::<&str>() {
583                                s
584                            } else if let Some(s) = e.downcast_ref::<String>() {
585                                s.as_str()
586                            } else {
587                                "Unknown panic"
588                            };
589                            error!("Event handler panicked: {}", panic_msg);
590                        }
591                    }
592                }
593            }
594        }
595
596        // Process asynchronous callbacks
597        {
598            let async_callbacks = async_event_callbacks.read().await;
599            if let Some(handlers) = async_callbacks.get(&topic) {
600                for handler in handlers {
601                    // Create a clone of the handler by calling it to get a future
602                    // This avoids borrowing issues by creating the future while we still have the lock
603                    let event_clone = event.clone();
604                    let future = match panic::catch_unwind(AssertUnwindSafe(|| {
605                        handler(event_clone.clone())
606                    })) {
607                        Ok(future) => future,
608                        Err(e) => {
609                            // Log panic but don't crash
610                            let panic_msg = if let Some(s) = e.downcast_ref::<&str>() {
611                                s
612                            } else if let Some(s) = e.downcast_ref::<String>() {
613                                s.as_str()
614                            } else {
615                                "Unknown panic"
616                            };
617                            error!("Async event handler panicked during setup: {}", panic_msg);
618                            continue;
619                        }
620                    };
621
622                    // Spawn a task with the future we already created
623                    tokio::spawn(async move {
624                        match panic::catch_unwind(AssertUnwindSafe(|| async {
625                            future.await;
626                        })) {
627                            Ok(f) => {
628                                f.await;
629                            }
630                            Err(e) => {
631                                // Log panic but don't crash
632                                let panic_msg = if let Some(s) = e.downcast_ref::<&str>() {
633                                    s
634                                } else if let Some(s) = e.downcast_ref::<String>() {
635                                    s.as_str()
636                                } else {
637                                    "Unknown panic"
638                                };
639                                error!(
640                                    "Async event handler panicked during execution: {}",
641                                    panic_msg
642                                );
643                            }
644                        }
645                    });
646                }
647            }
648        }
649    }
650
651    /// Handle an unsubscribe response
652    async fn handle_unsubscribe_response(
653        response: UnsubscribeResponse,
654        subscriptions: &Arc<RwLock<HashMap<String, SubscriptionHandler>>>,
655    ) {
656        if matches!(response.status, SubscriptionStatus::Unsubscribed) {
657            subscriptions
658                .write()
659                .await
660                .remove(&response.subscription_id);
661        }
662    }
663
664    /// Attempt to establish a new WebSocket connection
665    async fn establish_connection(
666        server_url: &str,
667    ) -> Result<WebSocketStream<tokio_tungstenite::MaybeTlsStream<TcpStream>>, WebSocketError> {
668        match connect_async(server_url).await {
669            Ok((ws_stream, _)) => Ok(ws_stream),
670            Err(e) => Err(WebSocketError::ConnectionFailed(e.to_string())),
671        }
672    }
673
674    /// Set up message writer task
675    fn spawn_writer_task(
676        write: futures::stream::SplitSink<
677            WebSocketStream<tokio_tungstenite::MaybeTlsStream<TcpStream>>,
678            Message,
679        >,
680    ) -> mpsc::Sender<Message> {
681        // Create a channel for the writer
682        let (sender, mut new_receiver) = mpsc::channel::<Message>(1000);
683
684        // Set up the writer task
685        tokio::spawn(async move {
686            let mut writer = write;
687            while let Some(msg) = new_receiver.recv().await {
688                if let Err(e) = writer.send(msg).await {
689                    error!("Failed to send message: {}", e);
690                    break;
691                }
692            }
693        });
694
695        sender
696    }
697
698    /// Main message processor with reconnection handling
699    #[allow(clippy::too_many_arguments)]
700    async fn message_processor(
701        initial_stream: futures::stream::SplitStream<
702            WebSocketStream<tokio_tungstenite::MaybeTlsStream<TcpStream>>,
703        >,
704        initial_sender: mpsc::Sender<Message>,
705        subscriptions: Arc<RwLock<HashMap<String, SubscriptionHandler>>>,
706        event_callbacks: Arc<RwLock<HashMap<EventTopic, Vec<EventCallback>>>>,
707        async_event_callbacks: Arc<RwLock<HashMap<EventTopic, Vec<AsyncEventCallback>>>>,
708        connection_callbacks: Arc<RwLock<Vec<ConnectionCallback>>>,
709        connected: Arc<Mutex<bool>>,
710        keep_alive_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
711        keep_alive_interval: Arc<Mutex<Option<Duration>>>,
712        running: Arc<Mutex<bool>>,
713        server_url: Arc<String>,
714        auto_reconnect: Arc<Mutex<bool>>,
715        backoff_strategy: Arc<Mutex<BackoffStrategy>>,
716        max_reconnect_attempts: Arc<Mutex<usize>>,
717        mut cancel_rx: mpsc::Receiver<()>,
718    ) {
719        let mut reconnect_attempts: usize = 0;
720        let mut read = initial_stream;
721        let mut sender = initial_sender;
722
723        // Main loop - runs as long as the client should be running
724        while *running.lock().await {
725            let disconnected = Self::process_messages(
726                &mut read,
727                &sender,
728                &subscriptions,
729                &event_callbacks,
730                &async_event_callbacks,
731                &connection_callbacks,
732                &connected,
733                &mut cancel_rx,
734            )
735            .await;
736
737            if disconnected {
738                // Handle reconnection if needed
739                if !*running.lock().await || !*auto_reconnect.lock().await {
740                    return; // Exit if client is shutting down or auto-reconnect is disabled
741                }
742
743                // Try reconnection
744                match Self::attempt_reconnection(
745                    &server_url,
746                    &connected,
747                    &connection_callbacks,
748                    &subscriptions,
749                    &keep_alive_handle,
750                    &keep_alive_interval,
751                    &running,
752                    &backoff_strategy,
753                    &max_reconnect_attempts,
754                    &mut reconnect_attempts,
755                    &mut cancel_rx,
756                )
757                .await
758                {
759                    Ok((new_read, new_sender)) => {
760                        // Update with new connection
761                        read = new_read;
762                        sender = new_sender;
763                        reconnect_attempts = 0; // Reset counter after successful reconnection
764                    }
765                    Err(_) => return, // Exit on reconnection failure or cancellation
766                }
767            }
768        }
769    }
770
771    /// Process messages until disconnection or cancellation
772    #[allow(clippy::too_many_arguments)]
773    async fn process_messages(
774        read: &mut futures::stream::SplitStream<
775            WebSocketStream<tokio_tungstenite::MaybeTlsStream<TcpStream>>,
776        >,
777        sender: &mpsc::Sender<Message>,
778        subscriptions: &Arc<RwLock<HashMap<String, SubscriptionHandler>>>,
779        event_callbacks: &Arc<RwLock<HashMap<EventTopic, Vec<EventCallback>>>>,
780        async_event_callbacks: &Arc<RwLock<HashMap<EventTopic, Vec<AsyncEventCallback>>>>,
781        connection_callbacks: &Arc<RwLock<Vec<ConnectionCallback>>>,
782        connected: &Arc<Mutex<bool>>,
783        cancel_rx: &mut mpsc::Receiver<()>,
784    ) -> bool {
785        loop {
786            tokio::select! {
787                // Check for cancellation
788                _ = cancel_rx.recv() => {
789                    return false; // Not disconnected, just cancelled
790                }
791
792                // Process WebSocket messages
793                message = read.next() => {
794                    match message {
795                        Some(Ok(msg)) => {
796                            if Self::process_message(msg, sender, subscriptions, event_callbacks, async_event_callbacks).await {
797                                // Connection closed
798                                Self::update_connection_status(connected, false, connection_callbacks).await;
799                                return true; // Disconnected
800                            }
801                        }
802                        Some(Err(e)) => {
803                            // Connection error with specific error message
804                            error!("WebSocket read error: {}", e);
805                            Self::update_connection_status(connected, false, connection_callbacks).await;
806                            return true; // Disconnected
807                        }
808                        None => {
809                            // Stream ended
810                            debug!("WebSocket stream ended");
811                            Self::update_connection_status(connected, false, connection_callbacks).await;
812                            return true; // Disconnected
813                        }
814                    }
815                }
816            }
817        }
818    }
819
820    /// Subscribe to a topic with a filter
821    pub async fn subscribe(
822        &self,
823        topic: EventTopic,
824        filter: EventFilter,
825    ) -> Result<(), WebSocketError> {
826        // Create a unique pending ID using a UUID
827        let pending_id = format!("pending-{}-{}", topic, uuid::Uuid::new_v4());
828
829        // First, check if we already have a matching subscription
830        let subs = self.subscriptions.read().await;
831        for (_, handler) in subs.iter() {
832            // If we have a non-pending subscription for this topic with the same filter
833            if !handler.pending && handler.topic == topic && handler.filter == filter {
834                return Ok(());
835            }
836        }
837        drop(subs);
838
839        // Add the subscription as pending
840        let mut subs = self.subscriptions.write().await;
841        subs.insert(
842            pending_id.clone(), // Use the pending ID as the key
843            SubscriptionHandler {
844                topic: topic.clone(),
845                filter: filter.clone(),
846                pending: true,
847            },
848        );
849        drop(subs);
850
851        // Send the subscription request
852        let request = WebSocketRequest::Subscribe(SubscriptionRequest {
853            topic: topic.clone(),
854            filter: filter.clone(),
855            request_id: Some(pending_id.clone()),
856        });
857
858        let message = serde_json::to_string(&request)
859            .map_err(|e| WebSocketError::Other(format!("Failed to serialize request: {}", e)))?;
860
861        self.sender
862            .send(Message::Text(message.into()))
863            .await
864            .map_err(|e| WebSocketError::SendFailed(e.to_string()))?;
865
866        Ok(())
867    }
868
869    /// Register a synchronous callback for a specific event topic
870    pub async fn on_event<F>(
871        &self,
872        topic: EventTopic,
873        filter: Option<EventFilter>,
874        callback: F,
875    ) -> Result<(), WebSocketError>
876    where
877        F: Fn(Event) + Send + Sync + 'static,
878    {
879        // First, make sure we have a subscription for this topic
880        let subs = self.subscriptions.read().await;
881        let has_topic_subscription = subs.values().any(|s| s.topic == topic);
882        drop(subs);
883
884        if !has_topic_subscription {
885            // Subscribe if needed
886            self.subscribe(topic.clone(), filter.unwrap_or_default())
887                .await?;
888        }
889
890        // Add the callback to the synchronous event callbacks collection
891        let mut callbacks = self.event_callbacks.write().await;
892        callbacks
893            .entry(topic)
894            .or_insert_with(Vec::new)
895            .push(Box::new(callback));
896
897        Ok(())
898    }
899
900    /// Register an async callback for a specific event topic
901    pub async fn on_event_async<F, Fut>(
902        &self,
903        topic: EventTopic,
904        filter: Option<EventFilter>,
905        callback: F,
906    ) -> Result<(), WebSocketError>
907    where
908        F: Fn(Event) -> Fut + Send + Sync + 'static,
909        Fut: Future<Output = ()> + Send + 'static,
910    {
911        // First, make sure we have a subscription for this topic
912        let subs = self.subscriptions.read().await;
913        let has_topic_subscription = subs.values().any(|s| s.topic == topic);
914        drop(subs);
915
916        if !has_topic_subscription {
917            // Subscribe if needed
918            self.subscribe(topic.clone(), filter.unwrap_or_default())
919                .await?;
920        }
921
922        // Convert the callback to use BoxFuture
923        let boxed_callback =
924            move |event: Event| -> BoxFuture<'static, ()> { callback(event).boxed() };
925
926        // Add the callback to the async event callbacks collection
927        let mut callbacks = self.async_event_callbacks.write().await;
928        callbacks
929            .entry(topic)
930            .or_insert_with(Vec::new)
931            .push(Box::new(boxed_callback));
932
933        Ok(())
934    }
935
936    /// Register a callback for connection status changes
937    pub async fn on_connection_change<F>(&self, callback: F)
938    where
939        F: Fn(bool) + Send + Sync + 'static,
940    {
941        self.connection_callbacks
942            .write()
943            .await
944            .push(Box::new(callback));
945    }
946
947    /// Unsubscribe from a topic
948    pub async fn unsubscribe(&self, subscription_id: &str) -> Result<(), WebSocketError> {
949        let topic = {
950            // Check if we have this subscription
951            let subs = self.subscriptions.read().await;
952            match subs.get(subscription_id) {
953                Some(sub) => sub.topic.clone(),
954                None => {
955                    return Err(WebSocketError::UnsubscriptionFailed(
956                        "Subscription not found".to_string(),
957                    ))
958                }
959            }
960        };
961
962        let request = WebSocketRequest::Unsubscribe(UnsubscribeRequest {
963            topic,
964            subscription_id: subscription_id.to_string(),
965        });
966
967        let request_json =
968            serde_json::to_string(&request).map_err(|e| WebSocketError::Other(e.to_string()))?;
969
970        // Send the request
971        self.sender
972            .send(Message::Text(request_json.into()))
973            .await
974            .map_err(|e| WebSocketError::SendFailed(e.to_string()))?;
975
976        // Remove the subscription (server response will also remove it)
977        self.subscriptions.write().await.remove(subscription_id);
978
979        Ok(())
980    }
981
982    /// Unsubscribe from all subscriptions for a topic
983    pub async fn unsubscribe_topic(&self, topic: &EventTopic) -> Result<(), WebSocketError> {
984        let subscription_ids: Vec<String> = {
985            let subs = self.subscriptions.read().await;
986            subs.iter()
987                .filter(|(_, handler)| handler.topic == *topic)
988                .map(|(id, _)| id.clone())
989                .collect()
990        };
991
992        let mut result = Ok(());
993        for id in subscription_ids {
994            if let Err(e) = self.unsubscribe(&id).await {
995                result = Err(e);
996            }
997        }
998
999        result
1000    }
1001
1002    /// Remove all event callbacks for a topic (both sync and async)
1003    pub async fn remove_event_listeners(&self, topic: &EventTopic) {
1004        // Remove synchronous callbacks
1005        let mut callbacks = self.event_callbacks.write().await;
1006        callbacks.remove(topic);
1007
1008        // Remove asynchronous callbacks
1009        let mut async_callbacks = self.async_event_callbacks.write().await;
1010        async_callbacks.remove(topic);
1011    }
1012
1013    /// Configure auto-reconnect settings
1014    pub async fn set_auto_reconnect(
1015        &self,
1016        enabled: bool,
1017        interval: std::time::Duration,
1018        max_attempts: usize,
1019    ) {
1020        *self.auto_reconnect.lock().await = enabled;
1021        *self.backoff_strategy.lock().await = BackoffStrategy::Constant(interval);
1022        *self.max_reconnect_attempts.lock().await = max_attempts;
1023    }
1024
1025    /// Check if the client is connected
1026    pub async fn is_connected(&self) -> bool {
1027        *self.connected.lock().await
1028    }
1029
1030    /// Close the connection and clean up resources
1031    ///
1032    /// This method should be called explicitly before the client is dropped
1033    /// to ensure proper cleanup of resources and graceful connection termination.
1034    pub async fn close(&self) -> Result<(), WebSocketError> {
1035        // Disable auto-reconnect first
1036        *self.auto_reconnect.lock().await = false;
1037
1038        // Signal the worker task to stop
1039        *self.running.lock().await = false;
1040
1041        // Cancel the message processor
1042        if let Some(cancel_tx) = &self.cancel_tx {
1043            let _ = cancel_tx.send(()).await;
1044        }
1045
1046        // Cancel any keep-alive task
1047        if let Some(handle) = self.keep_alive_handle.lock().await.take() {
1048            handle.abort();
1049        }
1050
1051        // Send close frame
1052        let _ = self.sender.send(Message::Close(None)).await;
1053
1054        // Update connection status
1055        Self::update_connection_status(&self.connected, false, &self.connection_callbacks).await;
1056
1057        Ok(())
1058    }
1059
1060    /// Configure reconnection settings
1061    pub async fn set_reconnect_options(
1062        &self,
1063        enabled: bool,
1064        strategy: BackoffStrategy,
1065        max_attempts: usize,
1066    ) {
1067        *self.auto_reconnect.lock().await = enabled;
1068        *self.backoff_strategy.lock().await = strategy;
1069        *self.max_reconnect_attempts.lock().await = max_attempts;
1070    }
1071
1072    /// Update connection status safely
1073    async fn update_connection_status(
1074        connected: &Arc<Mutex<bool>>,
1075        new_state: bool,
1076        connection_callbacks: &Arc<RwLock<Vec<ConnectionCallback>>>,
1077    ) -> bool {
1078        // Update the mutex-protected state
1079        let mut connected_guard = connected.lock().await;
1080        let changed = *connected_guard != new_state;
1081        *connected_guard = new_state;
1082        drop(connected_guard);
1083
1084        // Notify about the state change exactly once
1085        if changed {
1086            Self::notify_connection_status(new_state, connection_callbacks).await;
1087        }
1088
1089        changed // State changed
1090    }
1091
1092    /// Enable periodic ping messages to keep the connection alive
1093    pub async fn enable_keep_alive(&self, interval: Duration) -> Result<(), WebSocketError> {
1094        // Store the interval for reconnection handling
1095        *self.keep_alive_interval.lock().await = Some(interval);
1096
1097        // Start the keep-alive task
1098        Self::restart_keep_alive(
1099            &self.keep_alive_handle,
1100            &self.sender,
1101            &self.running,
1102            &self.connected,
1103            interval,
1104        )
1105        .await
1106    }
1107
1108    /// Restart the keep-alive task with a new sender
1109    async fn restart_keep_alive(
1110        keep_alive_handle: &Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
1111        sender: &mpsc::Sender<Message>,
1112        running: &Arc<Mutex<bool>>,
1113        connected: &Arc<Mutex<bool>>,
1114        interval: Duration,
1115    ) -> Result<(), WebSocketError> {
1116        // Cancel any existing keep-alive task
1117        if let Some(handle) = keep_alive_handle.lock().await.take() {
1118            handle.abort();
1119        }
1120
1121        let sender = sender.clone();
1122        let running = running.clone();
1123        let connected = connected.clone();
1124
1125        // Create a new keep-alive task
1126        let handle = tokio::spawn(async move {
1127            let mut interval_timer = tokio::time::interval(interval);
1128
1129            while *running.lock().await {
1130                interval_timer.tick().await;
1131
1132                // Only send pings if we're actually connected
1133                if *connected.lock().await {
1134                    if let Err(e) = sender.send(Message::Ping(Bytes::from_static(&[]))).await {
1135                        error!("Failed to send ping: {}", e);
1136                        // Don't break here, as we might reconnect and update the sender
1137                    }
1138                }
1139            }
1140        });
1141
1142        // Store the task handle
1143        *keep_alive_handle.lock().await = Some(handle);
1144
1145        Ok(())
1146    }
1147}
1148
1149impl Drop for WebSocketClient {
1150    fn drop(&mut self) {
1151        // Set running to false to stop background tasks
1152        if let Some(running) = Arc::get_mut(&mut self.running) {
1153            if let Ok(mut guard) = running.try_lock() {
1154                *guard = false;
1155            }
1156        }
1157
1158        // Try to cancel tasks without spawning new ones
1159        if let Some(cancel_tx) = self.cancel_tx.take() {
1160            // Try a non-blocking send (which will work in some cases)
1161            let _ = cancel_tx.try_send(());
1162        }
1163
1164        // Abort the keep-alive task if it exists
1165        if let Some(keep_alive_handle) = Arc::get_mut(&mut self.keep_alive_handle) {
1166            if let Ok(mut guard) = keep_alive_handle.try_lock() {
1167                if let Some(handle) = guard.take() {
1168                    handle.abort();
1169                }
1170            }
1171        }
1172    }
1173}