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#[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#[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 #[inline]
103 fn handle_disconnect_packet(&self) -> Result<()> {
104 #[cfg(feature = "tracing")]
105 tracing::debug!("received server disconnect packet");
106 let runtime = self.runtime.clone();
107 spawn(async move { runtime.disconnect().await });
108 Ok(())
109 }
110
111 #[inline]
112 fn handle_event_packet(self: &Arc<Self>, event: &str, packet_data: Option<Vec<u8>>) -> Result<()> {
113 #[cfg(feature = "tracing")]
114 tracing::trace!(event, has_data = packet_data.is_some(), "received server event packet");
115 self.runtime.event_registry.dispatch_event_packet(
116 self.clone(),
117 event,
118 &self.runtime.config.packet_codec,
119 packet_data,
120 &self.runtime,
121 );
122
123 Ok(())
124 }
125
126 async fn handle_init_packet(self: &Arc<Self>, packet_data: Option<&[u8]>) -> Result<()> {
127 let state = self.state.get();
129 match state {
130 SessionState::AwaitingInit => self.state.try_transition(state, SessionState::Initiating)?,
131 _ => {
132 #[cfg(feature = "tracing")]
133 tracing::debug!(?state, "received init packet in invalid client session state");
134 bail!("Received init packet in invalid state: {state:?}");
135 },
136 }
137
138 #[cfg(feature = "tracing")]
139 tracing::debug!("received server init packet");
140
141 abort_locked_task(&self.init_timeout_task).await;
143
144 let response_data = if let Some(init_handler) = &self.runtime.config.init_handler {
146 match timeout(
147 self.runtime.config.init_handler_timeout,
148 init_handler(self.clone(), packet_data, &self.runtime.config.packet_codec),
149 )
150 .await
151 {
152 Ok(result) => result?,
153 Err(err) => {
154 #[cfg(feature = "tracing")]
155 tracing::warn!(error = %err, "client init handler timed out");
156 return Err(err.into());
157 },
158 }
159 } else {
160 None
161 };
162
163 self.state
165 .try_transition(SessionState::Initiating, SessionState::AwaitingReady)?;
166
167 let session = self.clone();
169 *self.ready_timeout_task.lock().await = Some(spawn(async move {
170 sleep(session.runtime.config.ready_packet_timeout).await;
171 if session.state.is(SessionState::AwaitingReady) {
172 #[cfg(feature = "tracing")]
173 tracing::warn!("timed out waiting for server ready packet");
174 session.close();
175 }
176 }));
177
178 self.send_packet(&WsIoPacket::new_init(response_data)).await
180 }
181
182 async fn handle_ready_packet(self: &Arc<Self>) -> Result<()> {
183 let state = self.state.get();
185 match state {
186 SessionState::AwaitingReady => self.state.try_transition(state, SessionState::Ready)?,
187 _ => {
188 #[cfg(feature = "tracing")]
189 tracing::debug!(?state, "received ready packet in invalid client session state");
190 bail!("Received ready packet in invalid state: {state:?}");
191 },
192 }
193
194 #[cfg(feature = "tracing")]
195 tracing::debug!("client session is ready");
196
197 abort_locked_task(&self.ready_timeout_task).await;
199
200 self.runtime.wake_send_event_message_task_notify.notify_waiters();
202
203 if let Some(on_session_ready_handler) = self.runtime.config.on_session_ready_handler.clone() {
205 self.spawn_task(on_session_ready_handler(self.clone()));
207 }
208
209 Ok(())
210 }
211
212 async fn send_message(&self, message: Arc<Message>) -> Result<()> {
213 Ok(self.message_tx.send(message).await?)
214 }
215
216 async fn send_packet(&self, packet: &WsIoPacket) -> Result<()> {
217 self.send_message(self.runtime.encode_packet_to_message(packet)?).await
218 }
219
220 pub(crate) async fn cleanup(self: &Arc<Self>) {
222 #[cfg(feature = "tracing")]
223 tracing::debug!("cleaning up client session");
224 self.state.store(SessionState::Closing);
226
227 abort_locked_task(&self.init_timeout_task).await;
229 abort_locked_task(&self.ping_task).await;
230 abort_locked_task(&self.ready_timeout_task).await;
231
232 self.cancel_token.load().cancel();
234
235 if let Some(on_session_close_handler) = &self.runtime.config.on_session_close_handler
237 && let Err(_err) = timeout(
238 self.runtime.config.on_session_close_handler_timeout,
239 on_session_close_handler(self.clone()),
240 )
241 .await
242 {
243 #[cfg(feature = "tracing")]
244 tracing::warn!(error = %_err, "client session close handler timed out");
245 }
246
247 self.state.store(SessionState::Closed);
249
250 #[cfg(feature = "tracing")]
251 tracing::debug!("client session closed");
252 }
253
254 #[inline]
255 pub(crate) fn close(&self) {
256 match self.state.get() {
258 SessionState::Closed | SessionState::Closing => return,
259 _state => {
260 #[cfg(feature = "tracing")]
261 tracing::debug!(state = ?_state, "closing client session");
262 self.state.store(SessionState::Closing)
263 },
264 }
265
266 let _ = self.message_tx.try_send(Arc::new(Message::Close(None)));
268 }
269
270 pub(crate) async fn emit_event_message(&self, message: Arc<Message>) -> Result<()> {
271 self.state.ensure(SessionState::Ready, |state| {
272 format!("Cannot emit event message in invalid state: {state:?}")
273 })?;
274
275 self.send_message(message).await
276 }
277
278 pub(crate) async fn handle_incoming_packet(self: &Arc<Self>, encoded_packet: &[u8]) -> Result<()> {
279 let packet = match self.runtime.config.packet_codec.decode(encoded_packet) {
281 Ok(packet) => packet,
282 Err(err) => {
283 #[cfg(feature = "tracing")]
284 tracing::debug!(error = %err, "failed to decode server packet");
285 return Err(err);
286 },
287 };
288 match packet.r#type {
289 WsIoPacketType::Disconnect => self.handle_disconnect_packet(),
290 WsIoPacketType::Event => {
291 if self.is_ready() {
292 if let Some(event) = packet.key.as_deref() {
293 return self.handle_event_packet(event, packet.data);
294 } else {
295 bail!("Event packet missing key");
296 }
297 }
298
299 Ok(())
300 },
301 WsIoPacketType::Init => self.handle_init_packet(packet.data.as_deref()).await,
302 WsIoPacketType::Ready => self.handle_ready_packet().await,
303 }
304 }
305
306 pub(crate) async fn init(self: &Arc<Self>) {
307 self.state.store(SessionState::AwaitingInit);
308 #[cfg(feature = "tracing")]
309 tracing::debug!("client session awaiting server init packet");
310 let session = self.clone();
311
312 *self.init_timeout_task.lock().await = Some(spawn(async move {
314 sleep(session.runtime.config.init_packet_timeout).await;
315 if session.state.is(SessionState::AwaitingInit) {
316 #[cfg(feature = "tracing")]
317 tracing::warn!("timed out waiting for server init packet");
318 session.close();
319 }
320 }));
321
322 let session = self.clone();
324 *self.ping_task.lock().await = Some(spawn(async move {
325 loop {
326 sleep(session.runtime.config.ping_interval).await;
327 if session.send_message(PING_MESSAGE.clone()).await.is_err() {
328 #[cfg(feature = "tracing")]
329 tracing::warn!("failed to send client heartbeat; closing session");
330 session.close();
331 }
332 }
333 }));
334 }
335
336 #[inline]
338 pub fn client(&self) -> WsIoClient {
339 WsIoClient(self.runtime.clone())
340 }
341
342 #[inline]
343 pub fn is_ready(&self) -> bool {
344 self.state.is(SessionState::Ready)
345 }
346}
347
348static PING_MESSAGE: LazyLock<Arc<Message>> = LazyLock::new(|| Arc::new(Message::Binary(vec![0x01].into())));