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