Skip to main content

wsio_server/connection/
mod.rs

1use std::{
2    fmt::{
3        Debug as FmtDebug,
4        Formatter,
5        Result as FmtResult,
6    },
7    sync::{
8        Arc,
9        LazyLock,
10        atomic::{
11            AtomicU64,
12            Ordering,
13        },
14    },
15};
16
17use anyhow::{
18    Result,
19    bail,
20};
21use arc_swap::ArcSwap;
22use http::{
23    HeaderMap,
24    Uri,
25};
26use kikiutils::{
27    atomic::enum_cell::AtomicEnumCell,
28    types::fx_collections::FxDashSet,
29};
30use num_enum::{
31    IntoPrimitive,
32    TryFromPrimitive,
33};
34use serde::{
35    Serialize,
36    de::DeserializeOwned,
37};
38use tokio::{
39    spawn,
40    sync::{
41        Mutex,
42        mpsc::{
43            Receiver,
44            Sender,
45            channel,
46        },
47    },
48    task::JoinHandle,
49    time::{
50        sleep,
51        timeout,
52    },
53};
54use tokio_tungstenite::tungstenite::Message;
55use tokio_util::sync::CancellationToken;
56
57#[cfg(feature = "connection-extensions")]
58mod extensions;
59
60#[cfg(feature = "connection-extensions")]
61use self::extensions::ConnectionExtensions;
62use crate::{
63    WsIoServer,
64    core::{
65        channel_capacity_from_websocket_config,
66        event::registry::WsIoEventRegistry,
67        packet::{
68            WsIoPacket,
69            WsIoPacketType,
70        },
71        traits::task::spawner::TaskSpawner,
72        types::BoxAsyncUnaryResultHandler,
73        utils::task::abort_locked_task,
74    },
75    namespace::{
76        WsIoServerNamespace,
77        operators::broadcast::WsIoServerNamespaceBroadcastOperator,
78    },
79};
80
81// Enums
82#[repr(u8)]
83#[derive(Debug, Eq, IntoPrimitive, PartialEq, TryFromPrimitive)]
84enum ConnectionState {
85    Activating,
86    AwaitingInit,
87    Closed,
88    Closing,
89    Created,
90    Initiating,
91    Ready,
92}
93
94// Structs
95pub struct WsIoServerConnection {
96    cancel_token: ArcSwap<CancellationToken>,
97    event_registry: WsIoEventRegistry<WsIoServerConnection, WsIoServerConnection>,
98    #[cfg(feature = "connection-extensions")]
99    extensions: ConnectionExtensions,
100    headers: HeaderMap,
101    id: u64,
102    init_timeout_task: Mutex<Option<JoinHandle<()>>>,
103    joined_rooms: FxDashSet<String>,
104    message_tx: Sender<Arc<Message>>,
105    namespace: Arc<WsIoServerNamespace>,
106    on_close_handler: Mutex<Option<BoxAsyncUnaryResultHandler<Self>>>,
107    request_uri: Uri,
108    state: AtomicEnumCell<ConnectionState>,
109}
110
111impl FmtDebug for WsIoServerConnection {
112    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
113        let init_timeout_task = match self.init_timeout_task.try_lock() {
114            Ok(task) => {
115                if task.is_some() {
116                    "<task>"
117                } else {
118                    "<none>"
119                }
120            },
121            Err(_) => "<locked>",
122        };
123
124        let on_close_handler = match self.on_close_handler.try_lock() {
125            Ok(handler) => {
126                if handler.is_some() {
127                    "<handler>"
128                } else {
129                    "<none>"
130                }
131            },
132            Err(_) => "<locked>",
133        };
134
135        let mut debug = f.debug_struct("WsIoServerConnection");
136        debug
137            .field("id", &self.id)
138            .field("state", &self.state)
139            .field("request_uri", &self.request_uri)
140            .field("headers", &self.headers)
141            .field("joined_rooms_len", &self.joined_rooms.len())
142            .field("message_tx", &self.message_tx)
143            .field("cancel_token", &"<cancel_token>")
144            .field("namespace", &"<namespace>")
145            .field("event_registry", &self.event_registry)
146            .field("init_timeout_task", &init_timeout_task)
147            .field("on_close_handler", &on_close_handler);
148
149        #[cfg(feature = "connection-extensions")]
150        debug.field("extensions", &self.extensions);
151
152        debug.finish()
153    }
154}
155
156impl TaskSpawner for WsIoServerConnection {
157    #[inline]
158    fn cancel_token(&self) -> Arc<CancellationToken> {
159        self.cancel_token.load_full()
160    }
161}
162
163impl WsIoServerConnection {
164    #[inline]
165    pub(crate) fn new(
166        headers: HeaderMap,
167        namespace: Arc<WsIoServerNamespace>,
168        request_uri: Uri,
169    ) -> (Arc<Self>, Receiver<Arc<Message>>) {
170        let channel_capacity = channel_capacity_from_websocket_config(&namespace.config.websocket_config);
171        let (message_tx, message_rx) = channel(channel_capacity);
172        (
173            Arc::new(Self {
174                cancel_token: ArcSwap::new(Arc::new(CancellationToken::new())),
175                event_registry: WsIoEventRegistry::new(),
176                #[cfg(feature = "connection-extensions")]
177                extensions: ConnectionExtensions::new(),
178                headers,
179                id: NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed),
180                init_timeout_task: Mutex::new(None),
181                joined_rooms: FxDashSet::default(),
182                message_tx,
183                namespace,
184                on_close_handler: Mutex::new(None),
185                request_uri,
186                state: AtomicEnumCell::new(ConnectionState::Created),
187            }),
188            message_rx,
189        )
190    }
191
192    // Private methods
193    #[inline]
194    fn handle_event_packet(self: &Arc<Self>, event: &str, packet_data: Option<Vec<u8>>) -> Result<()> {
195        self.event_registry.dispatch_event_packet(
196            self.clone(),
197            event,
198            &self.namespace.config.packet_codec,
199            packet_data,
200            self,
201        );
202
203        Ok(())
204    }
205
206    async fn handle_init_packet(self: &Arc<Self>, packet_data: Option<&[u8]>) -> Result<()> {
207        // Verify current state; only valid from AwaitingInit → Initiating
208        let state = self.state.get();
209        match state {
210            ConnectionState::AwaitingInit => self.state.try_transition(state, ConnectionState::Initiating)?,
211            _ => bail!("Received init packet in invalid state: {state:?}"),
212        }
213
214        // Abort init-timeout task
215        abort_locked_task(&self.init_timeout_task).await;
216
217        // Invoke init_response_handler with timeout protection if configured
218        if let Some(init_response_handler) = &self.namespace.config.init_response_handler {
219            timeout(
220                self.namespace.config.init_response_handler_timeout,
221                init_response_handler(self.clone(), packet_data, &self.namespace.config.packet_codec),
222            )
223            .await??
224        }
225
226        // Activate connection
227        self.state
228            .try_transition(ConnectionState::Initiating, ConnectionState::Activating)?;
229
230        // Invoke middleware with timeout protection if configured
231        if let Some(middleware) = &self.namespace.config.middleware {
232            timeout(
233                self.namespace.config.middleware_execution_timeout,
234                middleware(self.clone()),
235            )
236            .await??;
237
238            // Ensure connection is still in Activating state
239            self.state.ensure(ConnectionState::Activating, |state| {
240                format!("Cannot activate connection in invalid state: {state:?}")
241            })?;
242        }
243
244        // Invoke on_connect_handler with timeout protection if configured
245        if let Some(on_connect_handler) = &self.namespace.config.on_connect_handler {
246            timeout(
247                self.namespace.config.on_connect_handler_timeout,
248                on_connect_handler(self.clone()),
249            )
250            .await??;
251        }
252
253        // Transition state to Ready
254        self.state
255            .try_transition(ConnectionState::Activating, ConnectionState::Ready)?;
256
257        // Insert connection into namespace
258        self.namespace.insert_connection(self.clone());
259
260        // Send ready packet
261        self.send_packet(&WsIoPacket::new_ready()).await?;
262
263        // Invoke on_ready_handler if configured
264        if let Some(on_ready_handler) = self.namespace.config.on_ready_handler.clone() {
265            // Run handler asynchronously in a detached task
266            self.spawn_task(on_ready_handler(self.clone()));
267        }
268
269        Ok(())
270    }
271
272    async fn send_packet(&self, packet: &WsIoPacket) -> Result<()> {
273        self.send_message(self.namespace.encode_packet_to_message(packet)?)
274            .await
275    }
276
277    // Protected methods
278    pub(crate) async fn cleanup(self: &Arc<Self>) {
279        // Set connection state to Closing
280        self.state.store(ConnectionState::Closing);
281
282        // Remove connection from namespace
283        self.namespace.remove_connection(self.id);
284
285        // Leave all joined rooms
286        let joined_rooms = self.joined_rooms.iter().map(|entry| entry.clone()).collect::<Vec<_>>();
287        for room_name in &joined_rooms {
288            self.namespace.remove_connection_id_from_room(room_name, self.id);
289        }
290
291        self.joined_rooms.clear();
292
293        // Abort init-timeout task
294        abort_locked_task(&self.init_timeout_task).await;
295
296        // Cancel all ongoing operations via cancel token
297        self.cancel_token.load().cancel();
298
299        // Invoke on_close_handler with timeout protection if configured
300        if let Some(on_close_handler) = self.on_close_handler.lock().await.take() {
301            let _ = timeout(
302                self.namespace.config.on_close_handler_timeout,
303                on_close_handler(self.clone()),
304            )
305            .await;
306        }
307
308        // Set connection state to Closed
309        self.state.store(ConnectionState::Closed);
310    }
311
312    #[inline]
313    pub(crate) fn close(&self) {
314        // Skip if connection is already Closing or Closed, otherwise set connection state to Closing
315        match self.state.get() {
316            ConnectionState::Closed | ConnectionState::Closing => return,
317            _ => self.state.store(ConnectionState::Closing),
318        }
319
320        // Send websocket close frame to initiate graceful shutdown
321        let _ = self.message_tx.try_send(Arc::new(Message::Close(None)));
322    }
323
324    pub(crate) async fn emit_event_message(&self, message: Arc<Message>) -> Result<()> {
325        self.state.ensure(ConnectionState::Ready, |state| {
326            format!("Cannot emit in invalid state: {state:?}")
327        })?;
328
329        self.send_message(message).await
330    }
331
332    pub(crate) async fn handle_incoming_packet(self: &Arc<Self>, encoded_packet: &[u8]) -> Result<()> {
333        // TODO: lazy load
334        let packet = self.namespace.config.packet_codec.decode(encoded_packet)?;
335        match packet.r#type {
336            WsIoPacketType::Event => {
337                if self.is_ready() {
338                    if let Some(event) = packet.key.as_deref() {
339                        return self.handle_event_packet(event, packet.data);
340                    } else {
341                        bail!("Event packet missing key");
342                    }
343                }
344
345                Ok(())
346            },
347            WsIoPacketType::Init => self.handle_init_packet(packet.data.as_deref()).await,
348            _ => Ok(()),
349        }
350    }
351
352    pub(crate) async fn init(self: &Arc<Self>) -> Result<()> {
353        // Verify current state; only valid Created
354        self.state.ensure(ConnectionState::Created, |state| {
355            format!("Cannot init connection in invalid state: {state:?}")
356        })?;
357
358        // Generate init request data if init request handler is configured
359        let init_request_data = if let Some(init_request_handler) = &self.namespace.config.init_request_handler {
360            timeout(
361                self.namespace.config.init_request_handler_timeout,
362                init_request_handler(self.clone(), &self.namespace.config.packet_codec),
363            )
364            .await??
365        } else {
366            None
367        };
368
369        // Transition state to AwaitingInit
370        self.state
371            .try_transition(ConnectionState::Created, ConnectionState::AwaitingInit)?;
372
373        // Spawn init-response-timeout watchdog to close connection if init not received in time
374        let connection = self.clone();
375        *self.init_timeout_task.lock().await = Some(spawn(async move {
376            sleep(connection.namespace.config.init_response_timeout).await;
377            if connection.state.is(ConnectionState::AwaitingInit) {
378                connection.close();
379            }
380        }));
381
382        // Send init packet
383        self.send_packet(&WsIoPacket::new_init(init_request_data)).await
384    }
385
386    pub(crate) async fn send_message(&self, message: Arc<Message>) -> Result<()> {
387        Ok(self.message_tx.send(message).await?)
388    }
389
390    // Public methods
391    pub async fn disconnect(&self) {
392        let _ = self.send_packet(&WsIoPacket::new_disconnect()).await;
393        self.close()
394    }
395
396    pub async fn emit<D: Serialize>(&self, event: impl AsRef<str>, data: Option<&D>) -> Result<()> {
397        self.emit_event_message(
398            self.namespace.encode_packet_to_message(&WsIoPacket::new_event(
399                event.as_ref(),
400                data.map(|data| self.namespace.config.packet_codec.encode_data(data))
401                    .transpose()?,
402            ))?,
403        )
404        .await
405    }
406
407    #[inline]
408    pub fn except(
409        self: &Arc<Self>,
410        room_names: impl IntoIterator<Item = impl Into<String>>,
411    ) -> WsIoServerNamespaceBroadcastOperator {
412        self.namespace.except(room_names).except_connection_ids([self.id])
413    }
414
415    #[cfg(feature = "connection-extensions")]
416    #[inline]
417    pub fn extensions(&self) -> &ConnectionExtensions {
418        &self.extensions
419    }
420
421    #[inline]
422    pub fn headers(&self) -> &HeaderMap {
423        &self.headers
424    }
425
426    #[inline]
427    pub fn id(&self) -> u64 {
428        self.id
429    }
430
431    #[inline]
432    pub fn is_ready(&self) -> bool {
433        self.state.is(ConnectionState::Ready)
434    }
435
436    #[inline]
437    pub fn join(self: &Arc<Self>, room_names: impl IntoIterator<Item = impl Into<String>>) {
438        for room_name in room_names {
439            let room_name = room_name.into();
440            self.namespace.add_connection_id_to_room(&room_name, self.id);
441            self.joined_rooms.insert(room_name);
442        }
443    }
444
445    #[inline]
446    pub fn leave(self: &Arc<Self>, room_names: impl IntoIterator<Item = impl Into<String>>) {
447        for room_name in room_names {
448            let room_name = &room_name.into();
449            self.namespace.remove_connection_id_from_room(room_name, self.id);
450
451            self.joined_rooms.remove(room_name);
452        }
453    }
454
455    #[inline]
456    pub fn namespace(&self) -> Arc<WsIoServerNamespace> {
457        self.namespace.clone()
458    }
459
460    #[inline]
461    pub fn off(&self, event: impl AsRef<str>) {
462        self.event_registry.off(event.as_ref());
463    }
464
465    #[inline]
466    pub fn off_by_handler_id(&self, event: impl AsRef<str>, handler_id: u32) {
467        self.event_registry.off_by_handler_id(event.as_ref(), handler_id);
468    }
469
470    #[inline]
471    pub fn on<H, Fut, D>(&self, event: impl AsRef<str>, handler: H) -> u32
472    where
473        H: Fn(Arc<WsIoServerConnection>, Arc<D>) -> Fut + Send + Sync + 'static,
474        Fut: Future<Output = Result<()>> + Send + 'static,
475        D: DeserializeOwned + Send + Sync + 'static,
476    {
477        self.event_registry.on(event.as_ref(), handler)
478    }
479
480    pub async fn on_close<H, Fut>(&self, handler: H)
481    where
482        H: Fn(Arc<WsIoServerConnection>) -> Fut + Send + Sync + 'static,
483        Fut: Future<Output = Result<()>> + Send + 'static,
484    {
485        *self.on_close_handler.lock().await = Some(Box::new(move |connection| Box::pin(handler(connection))));
486    }
487
488    #[inline]
489    pub fn request_uri(&self) -> &Uri {
490        &self.request_uri
491    }
492
493    #[inline]
494    pub fn server(&self) -> WsIoServer {
495        self.namespace.server()
496    }
497
498    #[inline]
499    pub fn to(
500        self: &Arc<Self>,
501        room_names: impl IntoIterator<Item = impl Into<String>>,
502    ) -> WsIoServerNamespaceBroadcastOperator {
503        self.namespace.to(room_names).except_connection_ids([self.id])
504    }
505}
506
507// Constants/Statics
508static NEXT_CONNECTION_ID: LazyLock<AtomicU64> = LazyLock::new(|| AtomicU64::new(0));
509
510#[cfg(test)]
511mod tests {
512    use http::{
513        HeaderMap,
514        Uri,
515    };
516
517    use super::*;
518
519    fn create_test_connection() -> Arc<WsIoServerConnection> {
520        let server = Arc::new(WsIoServer::builder().build());
521        let namespace = server.new_namespace_builder("/socket").register().unwrap();
522        let (connection, _rx) =
523            WsIoServerConnection::new(HeaderMap::new(), namespace, Uri::from_static("http://localhost"));
524
525        connection
526    }
527
528    #[tokio::test]
529    async fn test_handle_incoming_packet_decode_error() {
530        let connection = create_test_connection();
531        let garbage_data = b"obviously not valid json or messagepack";
532        // Should seamlessly return a Result::Err, not panic
533        let result = connection.handle_incoming_packet(garbage_data).await;
534        assert!(result.is_err(), "Decoding garbage payload should trigger an error");
535    }
536
537    #[tokio::test]
538    async fn test_handle_init_packet_in_invalid_state() {
539        let connection = create_test_connection();
540        assert_eq!(connection.state.get(), ConnectionState::Created);
541
542        // Sending an init packet when the connection is merely `Created` (not yet `AwaitingInit`) should throw an error
543        // Init packet JSON encoded (type: 2 = Init) -> serialized as tuple array
544        let encoded = b"[2,null,null]";
545
546        // This simulates a manual client Init push before server starts the handshake buffer
547        let result = connection.handle_incoming_packet(encoded).await;
548        assert!(
549            result.is_err(),
550            "Should error because state is Created, not AwaitingInit"
551        );
552
553        assert!(result.unwrap_err().to_string().contains("invalid state"));
554    }
555
556    #[tokio::test]
557    async fn test_handle_event_packet_missing_key() {
558        let connection = create_test_connection();
559
560        // Force the connection into the Ready state so it accepts Event packets
561        connection.state.store(ConnectionState::Ready);
562
563        // Manufacture an Event packet manually without a key (type: 1 = Event) -> serialized as tuple array
564        let encoded = b"[1,null,null]";
565
566        let result = connection.handle_incoming_packet(encoded).await;
567        assert!(result.is_err(), "Should bail on missing event key");
568        assert_eq!(result.unwrap_err().to_string(), "Event packet missing key");
569    }
570
571    #[tokio::test]
572    async fn test_connection_close_state_transitions() {
573        let connection = create_test_connection();
574        assert_eq!(connection.state.get(), ConnectionState::Created);
575
576        connection.close();
577        assert_eq!(connection.state.get(), ConnectionState::Closing);
578
579        // Calling close again when Closing shouldn't alter anything
580        connection.close();
581        assert_eq!(connection.state.get(), ConnectionState::Closing);
582    }
583
584    #[tokio::test]
585    async fn test_connection_cleanup() {
586        let connection = create_test_connection();
587        let namespace = connection.namespace();
588
589        // Insert connection manually for test
590        namespace.insert_connection(connection.clone());
591        assert_eq!(namespace.connection_count(), 1);
592
593        connection.join(["room_a", "room_b"]);
594        assert!(connection.joined_rooms.contains("room_a"));
595
596        connection.cleanup().await;
597
598        assert_eq!(connection.state.get(), ConnectionState::Closed);
599        assert!(connection.joined_rooms.is_empty());
600        assert_eq!(namespace.connection_count(), 0);
601    }
602}