Skip to main content

wsio_client/
session.rs

1use std::sync::{
2    Arc,
3    LazyLock,
4};
5
6use anyhow::{
7    Result,
8    bail,
9};
10use arc_swap::ArcSwap;
11use kikiutils::atomic::enum_cell::AtomicEnumCell;
12use num_enum::{
13    IntoPrimitive,
14    TryFromPrimitive,
15};
16use tokio::{
17    spawn,
18    sync::{
19        Mutex,
20        mpsc::{
21            Receiver,
22            Sender,
23            channel,
24        },
25    },
26    task::JoinHandle,
27    time::{
28        sleep,
29        timeout,
30    },
31};
32use tokio_tungstenite::tungstenite::Message;
33use tokio_util::sync::CancellationToken;
34
35use crate::{
36    WsIoClient,
37    core::{
38        channel_capacity_from_websocket_config,
39        packet::{
40            WsIoPacket,
41            WsIoPacketType,
42        },
43        traits::task::spawner::TaskSpawner,
44        utils::task::abort_locked_task,
45    },
46    runtime::WsIoClientRuntime,
47};
48
49// Enums
50#[repr(u8)]
51#[derive(Debug, Eq, IntoPrimitive, PartialEq, TryFromPrimitive)]
52enum SessionState {
53    AwaitingInit,
54    AwaitingReady,
55    Closed,
56    Closing,
57    Created,
58    Initiating,
59    Ready,
60    Readying,
61}
62
63// Structs
64#[derive(Debug)]
65pub struct WsIoClientSession {
66    cancel_token: ArcSwap<CancellationToken>,
67    init_timeout_task: Mutex<Option<JoinHandle<()>>>,
68    message_tx: Sender<Arc<Message>>,
69    ping_task: Mutex<Option<JoinHandle<()>>>,
70    ready_timeout_task: Mutex<Option<JoinHandle<()>>>,
71    runtime: Arc<WsIoClientRuntime>,
72    state: AtomicEnumCell<SessionState>,
73}
74
75impl TaskSpawner for WsIoClientSession {
76    #[inline]
77    fn cancel_token(&self) -> Arc<CancellationToken> {
78        self.cancel_token.load_full()
79    }
80}
81
82impl WsIoClientSession {
83    #[inline]
84    pub(crate) fn new(runtime: Arc<WsIoClientRuntime>) -> (Arc<Self>, Receiver<Arc<Message>>) {
85        let channel_capacity = channel_capacity_from_websocket_config(&runtime.config.websocket_config);
86        let (message_tx, message_rx) = channel(channel_capacity);
87        (
88            Arc::new(Self {
89                cancel_token: ArcSwap::new(Arc::new(CancellationToken::new())),
90                init_timeout_task: Mutex::new(None),
91                message_tx,
92                ping_task: Mutex::new(None),
93                ready_timeout_task: Mutex::new(None),
94                runtime,
95                state: AtomicEnumCell::new(SessionState::Created),
96            }),
97            message_rx,
98        )
99    }
100
101    // Private methods
102    #[inline]
103    fn handle_disconnect_packet(&self) -> Result<()> {
104        let runtime = self.runtime.clone();
105        spawn(async move { runtime.disconnect().await });
106        Ok(())
107    }
108
109    #[inline]
110    fn handle_event_packet(self: &Arc<Self>, event: &str, packet_data: Option<Vec<u8>>) -> Result<()> {
111        self.runtime.event_registry.dispatch_event_packet(
112            self.clone(),
113            event,
114            &self.runtime.config.packet_codec,
115            packet_data,
116            &self.runtime,
117        );
118
119        Ok(())
120    }
121
122    async fn handle_init_packet(self: &Arc<Self>, packet_data: Option<&[u8]>) -> Result<()> {
123        // Verify current state; only valid from AwaitingInit → Initiating
124        let state = self.state.get();
125        match state {
126            SessionState::AwaitingInit => self.state.try_transition(state, SessionState::Initiating)?,
127            _ => bail!("Received init packet in invalid state: {state:?}"),
128        }
129
130        // Abort init-timeout task
131        abort_locked_task(&self.init_timeout_task).await;
132
133        // Invoke init_handler with timeout protection if configured
134        let response_data = if let Some(init_handler) = &self.runtime.config.init_handler {
135            timeout(
136                self.runtime.config.init_handler_timeout,
137                init_handler(self.clone(), packet_data, &self.runtime.config.packet_codec),
138            )
139            .await??
140        } else {
141            None
142        };
143
144        // Transition state to AwaitingReady
145        self.state
146            .try_transition(SessionState::Initiating, SessionState::AwaitingReady)?;
147
148        // Spawn ready-timeout watchdog to close session if Ready is not received in time
149        let session = self.clone();
150        *self.ready_timeout_task.lock().await = Some(spawn(async move {
151            sleep(session.runtime.config.ready_packet_timeout).await;
152            if session.state.is(SessionState::AwaitingReady) {
153                session.close();
154            }
155        }));
156
157        // Send init packet
158        self.send_packet(&WsIoPacket::new_init(response_data)).await
159    }
160
161    async fn handle_ready_packet(self: &Arc<Self>) -> Result<()> {
162        // Verify current state; only valid from AwaitingReady → Ready
163        let state = self.state.get();
164        match state {
165            SessionState::AwaitingReady => self.state.try_transition(state, SessionState::Ready)?,
166            _ => bail!("Received ready packet in invalid state: {state:?}"),
167        }
168
169        // Abort ready-timeout task
170        abort_locked_task(&self.ready_timeout_task).await;
171
172        // Wake send event message task
173        self.runtime.wake_send_event_message_task_notify.notify_waiters();
174
175        // Invoke on_session_ready_handler if configured
176        if let Some(on_session_ready_handler) = self.runtime.config.on_session_ready_handler.clone() {
177            // Run handler asynchronously in a detached task
178            self.spawn_task(on_session_ready_handler(self.clone()));
179        }
180
181        Ok(())
182    }
183
184    async fn send_message(&self, message: Arc<Message>) -> Result<()> {
185        Ok(self.message_tx.send(message).await?)
186    }
187
188    async fn send_packet(&self, packet: &WsIoPacket) -> Result<()> {
189        self.send_message(self.runtime.encode_packet_to_message(packet)?).await
190    }
191
192    // Protected methods
193    pub(crate) async fn cleanup(self: &Arc<Self>) {
194        // Set state to Closing
195        self.state.store(SessionState::Closing);
196
197        // Abort tasks
198        abort_locked_task(&self.init_timeout_task).await;
199        abort_locked_task(&self.ping_task).await;
200        abort_locked_task(&self.ready_timeout_task).await;
201
202        // Cancel all ongoing operations via cancel token
203        self.cancel_token.load().cancel();
204
205        // Invoke on_session_close_handler with timeout protection if configured
206        if let Some(on_session_close_handler) = &self.runtime.config.on_session_close_handler {
207            let _ = timeout(
208                self.runtime.config.on_session_close_handler_timeout,
209                on_session_close_handler(self.clone()),
210            )
211            .await;
212        }
213
214        // Set state to Closed
215        self.state.store(SessionState::Closed);
216    }
217
218    #[inline]
219    pub(crate) fn close(&self) {
220        // Skip if session is already Closing or Closed, otherwise set state to Closing
221        match self.state.get() {
222            SessionState::Closed | SessionState::Closing => return,
223            _ => self.state.store(SessionState::Closing),
224        }
225
226        // Send websocket close frame to initiate graceful shutdown
227        let _ = self.message_tx.try_send(Arc::new(Message::Close(None)));
228    }
229
230    pub(crate) async fn emit_event_message(&self, message: Arc<Message>) -> Result<()> {
231        self.state.ensure(SessionState::Ready, |state| {
232            format!("Cannot emit event message in invalid state: {state:?}")
233        })?;
234
235        self.send_message(message).await
236    }
237
238    pub(crate) async fn handle_incoming_packet(self: &Arc<Self>, encoded_packet: &[u8]) -> Result<()> {
239        // TODO: lazy load
240        let packet = self.runtime.config.packet_codec.decode(encoded_packet)?;
241        match packet.r#type {
242            WsIoPacketType::Disconnect => self.handle_disconnect_packet(),
243            WsIoPacketType::Event => {
244                if self.is_ready() {
245                    if let Some(event) = packet.key.as_deref() {
246                        return self.handle_event_packet(event, packet.data);
247                    } else {
248                        bail!("Event packet missing key");
249                    }
250                }
251
252                Ok(())
253            },
254            WsIoPacketType::Init => self.handle_init_packet(packet.data.as_deref()).await,
255            WsIoPacketType::Ready => self.handle_ready_packet().await,
256        }
257    }
258
259    pub(crate) async fn init(self: &Arc<Self>) {
260        self.state.store(SessionState::AwaitingInit);
261        let session = self.clone();
262
263        // Create init-timeout watchdog to close session if init not received in time
264        *self.init_timeout_task.lock().await = Some(spawn(async move {
265            sleep(session.runtime.config.init_packet_timeout).await;
266            if session.state.is(SessionState::AwaitingInit) {
267                session.close();
268            }
269        }));
270
271        // Create ping task to send 1-byte heartbeat frame to keep the connection alive
272        let session = self.clone();
273        *self.ping_task.lock().await = Some(spawn(async move {
274            loop {
275                sleep(session.runtime.config.ping_interval).await;
276                if session.send_message(PING_MESSAGE.clone()).await.is_err() {
277                    session.close();
278                }
279            }
280        }));
281    }
282
283    // Public methods
284    #[inline]
285    pub fn client(&self) -> WsIoClient {
286        WsIoClient(self.runtime.clone())
287    }
288
289    #[inline]
290    pub fn is_ready(&self) -> bool {
291        self.state.is(SessionState::Ready)
292    }
293}
294
295// Constants/Statics
296static PING_MESSAGE: LazyLock<Arc<Message>> = LazyLock::new(|| Arc::new(Message::Binary(vec![0x01].into())));