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        let id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed);
173
174        #[cfg(feature = "tracing")]
175        tracing::debug!(
176            connection_id = id,
177            namespace = %namespace.path(),
178            request_path = request_uri.path(),
179            "creating server connection"
180        );
181
182        (
183            Arc::new(Self {
184                cancel_token: ArcSwap::new(Arc::new(CancellationToken::new())),
185                event_registry: WsIoEventRegistry::new(),
186                #[cfg(feature = "connection-extensions")]
187                extensions: ConnectionExtensions::new(),
188                headers,
189                id,
190                init_timeout_task: Mutex::new(None),
191                joined_rooms: FxDashSet::default(),
192                message_tx,
193                namespace,
194                on_close_handler: Mutex::new(None),
195                request_uri,
196                state: AtomicEnumCell::new(ConnectionState::Created),
197            }),
198            message_rx,
199        )
200    }
201
202    // Private methods
203    #[inline]
204    fn handle_event_packet(self: &Arc<Self>, event: &str, packet_data: Option<Vec<u8>>) -> Result<()> {
205        #[cfg(feature = "tracing")]
206        tracing::trace!(
207            connection_id = self.id,
208            event,
209            has_data = packet_data.is_some(),
210            "received client event packet"
211        );
212
213        self.event_registry.dispatch_event_packet(
214            self.clone(),
215            event,
216            &self.namespace.config.packet_codec,
217            packet_data,
218            self,
219        );
220
221        Ok(())
222    }
223
224    async fn handle_init_packet(self: &Arc<Self>, packet_data: Option<&[u8]>) -> Result<()> {
225        // Verify current state; only valid from AwaitingInit → Initiating
226        let state = self.state.get();
227        match state {
228            ConnectionState::AwaitingInit => self.state.try_transition(state, ConnectionState::Initiating)?,
229            _ => {
230                #[cfg(feature = "tracing")]
231                tracing::debug!(
232                    connection_id = self.id,
233                    ?state,
234                    "received init packet in invalid server connection state"
235                );
236
237                bail!("Received init packet in invalid state: {state:?}");
238            },
239        }
240
241        #[cfg(feature = "tracing")]
242        tracing::debug!(connection_id = self.id, "received client init packet");
243
244        // Abort init-timeout task
245        abort_locked_task(&self.init_timeout_task).await;
246
247        // Invoke init_response_handler with timeout protection if configured
248        if let Some(init_response_handler) = &self.namespace.config.init_response_handler {
249            match timeout(
250                self.namespace.config.init_response_handler_timeout,
251                init_response_handler(self.clone(), packet_data, &self.namespace.config.packet_codec),
252            )
253            .await
254            {
255                Ok(result) => result?,
256                Err(err) => {
257                    #[cfg(feature = "tracing")]
258                    tracing::warn!(
259                        connection_id = self.id,
260                        error = %err,
261                        "server init response handler timed out"
262                    );
263
264                    return Err(err.into());
265                },
266            }
267        }
268
269        // Activate connection
270        self.state
271            .try_transition(ConnectionState::Initiating, ConnectionState::Activating)?;
272
273        // Invoke middleware with timeout protection if configured
274        if let Some(middleware) = &self.namespace.config.middleware {
275            match timeout(
276                self.namespace.config.middleware_execution_timeout,
277                middleware(self.clone()),
278            )
279            .await
280            {
281                Ok(result) => result?,
282                Err(err) => {
283                    #[cfg(feature = "tracing")]
284                    tracing::warn!(connection_id = self.id, error = %err, "server middleware timed out");
285                    return Err(err.into());
286                },
287            }
288
289            // Ensure connection is still in Activating state
290            self.state.ensure(ConnectionState::Activating, |state| {
291                format!("Cannot activate connection in invalid state: {state:?}")
292            })?;
293        }
294
295        // Invoke on_connect_handler with timeout protection if configured
296        if let Some(on_connect_handler) = &self.namespace.config.on_connect_handler {
297            match timeout(
298                self.namespace.config.on_connect_handler_timeout,
299                on_connect_handler(self.clone()),
300            )
301            .await
302            {
303                Ok(result) => result?,
304                Err(err) => {
305                    #[cfg(feature = "tracing")]
306                    tracing::warn!(connection_id = self.id, error = %err, "server on-connect handler timed out");
307                    return Err(err.into());
308                },
309            }
310        }
311
312        // Transition state to Ready
313        self.state
314            .try_transition(ConnectionState::Activating, ConnectionState::Ready)?;
315
316        // Insert connection into namespace
317        self.namespace.insert_connection(self.clone());
318
319        #[cfg(feature = "tracing")]
320        tracing::debug!(connection_id = self.id, "server connection is ready");
321
322        // Send ready packet
323        self.send_packet(&WsIoPacket::new_ready()).await?;
324
325        // Invoke on_ready_handler if configured
326        if let Some(on_ready_handler) = self.namespace.config.on_ready_handler.clone() {
327            // Run handler asynchronously in a detached task
328            self.spawn_task(on_ready_handler(self.clone()));
329        }
330
331        Ok(())
332    }
333
334    async fn send_packet(&self, packet: &WsIoPacket) -> Result<()> {
335        self.send_message(self.namespace.encode_packet_to_message(packet)?)
336            .await
337    }
338
339    // Protected methods
340    pub(crate) async fn cleanup(self: &Arc<Self>) {
341        #[cfg(feature = "tracing")]
342        tracing::debug!(connection_id = self.id, "cleaning up server connection");
343        // Set connection state to Closing
344        self.state.store(ConnectionState::Closing);
345
346        // Remove connection from namespace
347        self.namespace.remove_connection(self.id);
348
349        // Leave all joined rooms
350        let joined_rooms = self.joined_rooms.iter().map(|entry| entry.clone()).collect::<Vec<_>>();
351        for room_name in &joined_rooms {
352            self.namespace.remove_connection_id_from_room(room_name, self.id);
353        }
354
355        self.joined_rooms.clear();
356
357        // Abort init-timeout task
358        abort_locked_task(&self.init_timeout_task).await;
359
360        // Cancel all ongoing operations via cancel token
361        self.cancel_token.load().cancel();
362
363        // Invoke on_close_handler with timeout protection if configured
364        if let Some(on_close_handler) = self.on_close_handler.lock().await.take()
365            && let Err(_err) = timeout(
366                self.namespace.config.on_close_handler_timeout,
367                on_close_handler(self.clone()),
368            )
369            .await
370        {
371            #[cfg(feature = "tracing")]
372            tracing::warn!(connection_id = self.id, error = %_err, "server close handler timed out");
373        }
374
375        // Set connection state to Closed
376        self.state.store(ConnectionState::Closed);
377
378        #[cfg(feature = "tracing")]
379        tracing::debug!(connection_id = self.id, "server connection closed");
380    }
381
382    #[inline]
383    pub(crate) fn close(&self) {
384        // Skip if connection is already Closing or Closed, otherwise set connection state to Closing
385        match self.state.get() {
386            ConnectionState::Closed | ConnectionState::Closing => return,
387            _state => {
388                #[cfg(feature = "tracing")]
389                tracing::debug!(connection_id = self.id, state = ?_state, "closing server connection");
390                self.state.store(ConnectionState::Closing)
391            },
392        }
393
394        // Send websocket close frame to initiate graceful shutdown
395        let _ = self.message_tx.try_send(Arc::new(Message::Close(None)));
396    }
397
398    pub(crate) async fn emit_event_message(&self, message: Arc<Message>) -> Result<()> {
399        self.state.ensure(ConnectionState::Ready, |state| {
400            format!("Cannot emit in invalid state: {state:?}")
401        })?;
402
403        self.send_message(message).await
404    }
405
406    pub(crate) async fn handle_incoming_packet(self: &Arc<Self>, encoded_packet: &[u8]) -> Result<()> {
407        // TODO: lazy load
408        let packet = match self.namespace.config.packet_codec.decode(encoded_packet) {
409            Ok(packet) => packet,
410            Err(err) => {
411                #[cfg(feature = "tracing")]
412                tracing::debug!(connection_id = self.id, error = %err, "failed to decode client packet");
413                return Err(err);
414            },
415        };
416
417        match packet.r#type {
418            WsIoPacketType::Event => {
419                if self.is_ready() {
420                    if let Some(event) = packet.key.as_deref() {
421                        return self.handle_event_packet(event, packet.data);
422                    } else {
423                        bail!("Event packet missing key");
424                    }
425                }
426
427                Ok(())
428            },
429            WsIoPacketType::Init => self.handle_init_packet(packet.data.as_deref()).await,
430            _ => Ok(()),
431        }
432    }
433
434    pub(crate) async fn init(self: &Arc<Self>) -> Result<()> {
435        // Verify current state; only valid Created
436        self.state.ensure(ConnectionState::Created, |state| {
437            format!("Cannot init connection in invalid state: {state:?}")
438        })?;
439
440        #[cfg(feature = "tracing")]
441        tracing::debug!(connection_id = self.id, "initializing server connection");
442
443        // Generate init request data if init request handler is configured
444        let init_request_data = if let Some(init_request_handler) = &self.namespace.config.init_request_handler {
445            match timeout(
446                self.namespace.config.init_request_handler_timeout,
447                init_request_handler(self.clone(), &self.namespace.config.packet_codec),
448            )
449            .await
450            {
451                Ok(result) => result?,
452                Err(err) => {
453                    #[cfg(feature = "tracing")]
454                    tracing::warn!(
455                        connection_id = self.id,
456                        error = %err,
457                        "server init request handler timed out"
458                    );
459
460                    return Err(err.into());
461                },
462            }
463        } else {
464            None
465        };
466
467        // Transition state to AwaitingInit
468        self.state
469            .try_transition(ConnectionState::Created, ConnectionState::AwaitingInit)?;
470
471        // Spawn init-response-timeout watchdog to close connection if init not received in time
472        let connection = self.clone();
473        *self.init_timeout_task.lock().await = Some(spawn(async move {
474            sleep(connection.namespace.config.init_response_timeout).await;
475            if connection.state.is(ConnectionState::AwaitingInit) {
476                #[cfg(feature = "tracing")]
477                tracing::warn!(
478                    connection_id = connection.id,
479                    "timed out waiting for client init response packet"
480                );
481
482                connection.close();
483            }
484        }));
485
486        // Send init packet
487        self.send_packet(&WsIoPacket::new_init(init_request_data)).await
488    }
489
490    pub(crate) async fn send_message(&self, message: Arc<Message>) -> Result<()> {
491        Ok(self.message_tx.send(message).await?)
492    }
493
494    // Public methods
495    pub async fn disconnect(&self) {
496        #[cfg(feature = "tracing")]
497        tracing::debug!(connection_id = self.id, "disconnecting server connection");
498        let _ = self.send_packet(&WsIoPacket::new_disconnect()).await;
499        self.close()
500    }
501
502    pub async fn emit<D: Serialize>(&self, event: impl AsRef<str>, data: Option<&D>) -> Result<()> {
503        self.emit_event_message(
504            self.namespace.encode_packet_to_message(&WsIoPacket::new_event(
505                event.as_ref(),
506                data.map(|data| self.namespace.config.packet_codec.encode_data(data))
507                    .transpose()?,
508            ))?,
509        )
510        .await
511    }
512
513    #[inline]
514    pub fn except(
515        self: &Arc<Self>,
516        room_names: impl IntoIterator<Item = impl Into<String>>,
517    ) -> WsIoServerNamespaceBroadcastOperator {
518        self.namespace.except(room_names).except_connection_ids([self.id])
519    }
520
521    #[cfg(feature = "connection-extensions")]
522    #[inline]
523    pub fn extensions(&self) -> &ConnectionExtensions {
524        &self.extensions
525    }
526
527    #[inline]
528    pub fn headers(&self) -> &HeaderMap {
529        &self.headers
530    }
531
532    #[inline]
533    pub fn id(&self) -> u64 {
534        self.id
535    }
536
537    #[inline]
538    pub fn is_ready(&self) -> bool {
539        self.state.is(ConnectionState::Ready)
540    }
541
542    #[inline]
543    pub fn join(self: &Arc<Self>, room_names: impl IntoIterator<Item = impl Into<String>>) {
544        for room_name in room_names {
545            let room_name = room_name.into();
546            self.namespace.add_connection_id_to_room(&room_name, self.id);
547
548            #[cfg(feature = "tracing")]
549            tracing::trace!(connection_id = self.id, room = %room_name, "connection joined room");
550            self.joined_rooms.insert(room_name);
551        }
552    }
553
554    #[inline]
555    pub fn leave(self: &Arc<Self>, room_names: impl IntoIterator<Item = impl Into<String>>) {
556        for room_name in room_names {
557            let room_name = room_name.into();
558            self.namespace.remove_connection_id_from_room(&room_name, self.id);
559
560            self.joined_rooms.remove(&room_name);
561
562            #[cfg(feature = "tracing")]
563            tracing::trace!(connection_id = self.id, room = %room_name, "connection left room");
564        }
565    }
566
567    #[inline]
568    pub fn namespace(&self) -> Arc<WsIoServerNamespace> {
569        self.namespace.clone()
570    }
571
572    #[inline]
573    pub fn off(&self, event: impl AsRef<str>) {
574        self.event_registry.off(event.as_ref());
575    }
576
577    #[inline]
578    pub fn off_by_handler_id(&self, event: impl AsRef<str>, handler_id: u32) {
579        self.event_registry.off_by_handler_id(event.as_ref(), handler_id);
580    }
581
582    #[inline]
583    pub fn on<H, Fut, D>(&self, event: impl AsRef<str>, handler: H) -> u32
584    where
585        H: Fn(Arc<WsIoServerConnection>, Arc<D>) -> Fut + Send + Sync + 'static,
586        Fut: Future<Output = Result<()>> + Send + 'static,
587        D: DeserializeOwned + Send + Sync + 'static,
588    {
589        self.event_registry.on(event.as_ref(), handler)
590    }
591
592    pub async fn on_close<H, Fut>(&self, handler: H)
593    where
594        H: Fn(Arc<WsIoServerConnection>) -> Fut + Send + Sync + 'static,
595        Fut: Future<Output = Result<()>> + Send + 'static,
596    {
597        *self.on_close_handler.lock().await = Some(Box::new(move |connection| Box::pin(handler(connection))));
598    }
599
600    #[inline]
601    pub fn request_uri(&self) -> &Uri {
602        &self.request_uri
603    }
604
605    #[inline]
606    pub fn server(&self) -> WsIoServer {
607        self.namespace.server()
608    }
609
610    #[inline]
611    pub fn to(
612        self: &Arc<Self>,
613        room_names: impl IntoIterator<Item = impl Into<String>>,
614    ) -> WsIoServerNamespaceBroadcastOperator {
615        self.namespace.to(room_names).except_connection_ids([self.id])
616    }
617}
618
619// Constants/Statics
620static NEXT_CONNECTION_ID: LazyLock<AtomicU64> = LazyLock::new(|| AtomicU64::new(0));
621
622#[cfg(test)]
623mod tests {
624    use http::{
625        HeaderMap,
626        Uri,
627    };
628
629    use super::*;
630
631    fn create_test_connection() -> Arc<WsIoServerConnection> {
632        let server = Arc::new(WsIoServer::builder().build());
633        let namespace = server.new_namespace_builder("/socket").register().unwrap();
634        let (connection, _rx) =
635            WsIoServerConnection::new(HeaderMap::new(), namespace, Uri::from_static("http://localhost"));
636
637        connection
638    }
639
640    #[tokio::test]
641    async fn test_handle_incoming_packet_decode_error() {
642        let connection = create_test_connection();
643        let garbage_data = b"obviously not valid json or messagepack";
644        // Should seamlessly return a Result::Err, not panic
645        let result = connection.handle_incoming_packet(garbage_data).await;
646        assert!(result.is_err(), "Decoding garbage payload should trigger an error");
647    }
648
649    #[tokio::test]
650    async fn test_handle_init_packet_in_invalid_state() {
651        let connection = create_test_connection();
652        assert_eq!(connection.state.get(), ConnectionState::Created);
653
654        // Sending an init packet when the connection is merely `Created` (not yet `AwaitingInit`) should throw an error
655        // Init packet JSON encoded (type: 2 = Init) -> serialized as tuple array
656        let encoded = b"[2,null,null]";
657
658        // This simulates a manual client Init push before server starts the handshake buffer
659        let result = connection.handle_incoming_packet(encoded).await;
660        assert!(
661            result.is_err(),
662            "Should error because state is Created, not AwaitingInit"
663        );
664
665        assert!(result.unwrap_err().to_string().contains("invalid state"));
666    }
667
668    #[tokio::test]
669    async fn test_handle_event_packet_missing_key() {
670        let connection = create_test_connection();
671
672        // Force the connection into the Ready state so it accepts Event packets
673        connection.state.store(ConnectionState::Ready);
674
675        // Manufacture an Event packet manually without a key (type: 1 = Event) -> serialized as tuple array
676        let encoded = b"[1,null,null]";
677
678        let result = connection.handle_incoming_packet(encoded).await;
679        assert!(result.is_err(), "Should bail on missing event key");
680        assert_eq!(result.unwrap_err().to_string(), "Event packet missing key");
681    }
682
683    #[tokio::test]
684    async fn test_connection_close_state_transitions() {
685        let connection = create_test_connection();
686        assert_eq!(connection.state.get(), ConnectionState::Created);
687
688        connection.close();
689        assert_eq!(connection.state.get(), ConnectionState::Closing);
690
691        // Calling close again when Closing shouldn't alter anything
692        connection.close();
693        assert_eq!(connection.state.get(), ConnectionState::Closing);
694    }
695
696    #[tokio::test]
697    async fn test_connection_cleanup() {
698        let connection = create_test_connection();
699        let namespace = connection.namespace();
700
701        // Insert connection manually for test
702        namespace.insert_connection(connection.clone());
703        assert_eq!(namespace.connection_count(), 1);
704
705        connection.join(["room_a", "room_b"]);
706        assert!(connection.joined_rooms.contains("room_a"));
707
708        connection.cleanup().await;
709
710        assert_eq!(connection.state.get(), ConnectionState::Closed);
711        assert!(connection.joined_rooms.is_empty());
712        assert_eq!(namespace.connection_count(), 0);
713    }
714}