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#[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
94pub 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 #[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 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_locked_task(&self.init_timeout_task).await;
246
247 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 self.state
271 .try_transition(ConnectionState::Initiating, ConnectionState::Activating)?;
272
273 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 self.state.ensure(ConnectionState::Activating, |state| {
291 format!("Cannot activate connection in invalid state: {state:?}")
292 })?;
293 }
294
295 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 self.state
314 .try_transition(ConnectionState::Activating, ConnectionState::Ready)?;
315
316 self.namespace.insert_connection(self.clone());
318
319 #[cfg(feature = "tracing")]
320 tracing::debug!(connection_id = self.id, "server connection is ready");
321
322 self.send_packet(&WsIoPacket::new_ready()).await?;
324
325 if let Some(on_ready_handler) = self.namespace.config.on_ready_handler.clone() {
327 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 pub(crate) async fn cleanup(self: &Arc<Self>) {
341 #[cfg(feature = "tracing")]
342 tracing::debug!(connection_id = self.id, "cleaning up server connection");
343 self.state.store(ConnectionState::Closing);
345
346 self.namespace.remove_connection(self.id);
348
349 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_locked_task(&self.init_timeout_task).await;
359
360 self.cancel_token.load().cancel();
362
363 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 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 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 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 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 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 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 self.state
469 .try_transition(ConnectionState::Created, ConnectionState::AwaitingInit)?;
470
471 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 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 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
619static 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 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 let encoded = b"[2,null,null]";
657
658 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 connection.state.store(ConnectionState::Ready);
674
675 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 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 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}