1use std::{
2 sync::Arc,
3 time::Duration,
4};
5
6use anyhow::{
7 Result,
8 bail,
9};
10use serde::{
11 Serialize,
12 de::DeserializeOwned,
13};
14use tokio_tungstenite::tungstenite::{
15 http::Request,
16 protocol::WebSocketConfig,
17};
18use url::Url;
19
20use crate::{
21 WsIoClient,
22 config::WsIoClientConfig,
23 core::packet::codecs::WsIoPacketCodec,
24 runtime::WsIoClientRuntime,
25 session::WsIoClientSession,
26};
27
28#[derive(Debug)]
35pub struct WsIoClientBuilder {
36 config: WsIoClientConfig,
37 connect_url: Url,
38}
39
40impl WsIoClientBuilder {
41 pub(crate) fn new(mut url: Url) -> Result<Self> {
42 if !matches!(url.scheme(), "ws" | "wss") {
43 bail!("Invalid URL scheme: {}", url.scheme());
44 }
45
46 let mut query_pairs = url.query_pairs().collect::<Vec<_>>();
47 query_pairs.retain(|(k, _)| k != "namespace");
48 query_pairs.push(("namespace".into(), Self::normalize_url_path(url.path()).into()));
49 let query = query_pairs
50 .iter()
51 .map(|(k, v)| format!("{k}={v}"))
52 .collect::<Vec<_>>()
53 .join("&");
54
55 url.set_query(Some(&query));
56 url.set_path("ws.io");
57 Ok(Self {
58 config: WsIoClientConfig {
59 disconnect_timeout: Duration::from_secs(5),
60 init_handler: None,
61 init_handler_timeout: Duration::from_secs(3),
62 init_packet_timeout: Duration::from_secs(5),
63 on_session_close_handler: None,
64 on_session_close_handler_timeout: Duration::from_secs(2),
65 on_session_ready_handler: None,
66 packet_codec: WsIoPacketCodec::SerdeJson,
67 ping_interval: Duration::from_secs(25),
68 ready_packet_timeout: Duration::from_secs(5),
69 reconnect_delay: Duration::from_secs(1),
70 request_modifier: None,
71 websocket_config: WebSocketConfig::default()
72 .max_frame_size(Some(8 * 1024 * 1024))
73 .max_message_size(Some(16 * 1024 * 1024))
74 .max_write_buffer_size(2 * 1024 * 1024)
75 .read_buffer_size(8 * 1024)
76 .write_buffer_size(8 * 1024),
77 },
78 connect_url: url,
79 })
80 }
81
82 fn normalize_url_path(path: &str) -> String {
84 format!(
85 "/{}",
86 path.split('/').filter(|s| !s.is_empty()).collect::<Vec<_>>().join("/")
87 )
88 }
89
90 pub fn build(self) -> WsIoClient {
94 WsIoClient(WsIoClientRuntime::new(self.config, self.connect_url))
95 }
96
97 pub fn disconnect_timeout(mut self, duration: Duration) -> Self {
100 self.config.disconnect_timeout = duration;
101 self
102 }
103
104 pub fn init_handler_timeout(mut self, duration: Duration) -> Self {
109 self.config.init_handler_timeout = duration;
110 self
111 }
112
113 pub fn init_packet_timeout(mut self, duration: Duration) -> Self {
119 self.config.init_packet_timeout = duration;
120 self
121 }
122
123 pub fn on_session_close<H, Fut>(mut self, handler: H) -> Self
128 where
129 H: Fn(Arc<WsIoClientSession>) -> Fut + Send + Sync + 'static,
130 Fut: Future<Output = Result<()>> + Send + 'static,
131 {
132 self.config.on_session_close_handler = Some(Box::new(move |session| Box::pin(handler(session))));
133 self
134 }
135
136 pub fn on_session_close_handler_timeout(mut self, duration: Duration) -> Self {
138 self.config.on_session_close_handler_timeout = duration;
139 self
140 }
141
142 pub fn on_session_ready<H, Fut>(mut self, handler: H) -> Self
147 where
148 H: Fn(Arc<WsIoClientSession>) -> Fut + Send + Sync + 'static,
149 Fut: Future<Output = Result<()>> + Send + 'static,
150 {
151 self.config.on_session_ready_handler = Some(Arc::new(move |session| Box::pin(handler(session))));
152 self
153 }
154
155 pub fn packet_codec(mut self, packet_codec: WsIoPacketCodec) -> Self {
159 self.config.packet_codec = packet_codec;
160 self
161 }
162
163 pub fn ping_interval(mut self, duration: Duration) -> Self {
169 self.config.ping_interval = duration;
170 self
171 }
172
173 pub fn ready_packet_timeout(mut self, duration: Duration) -> Self {
178 self.config.ready_packet_timeout = duration;
179 self
180 }
181
182 pub fn reconnect_delay(mut self, delay: Duration) -> Self {
187 self.config.reconnect_delay = delay;
188 self
189 }
190
191 pub fn request_modifier<M, Fut>(mut self, modifier: M) -> Self
196 where
197 M: Fn(Request<()>) -> Fut + Send + Sync + 'static,
198 Fut: Future<Output = Result<Request<()>>> + Send + 'static,
199 {
200 self.config.request_modifier = Some(Box::new(move |request| Box::pin(modifier(request))));
201 self
202 }
203
204 pub fn request_path(mut self, request_path: impl AsRef<str>) -> Self {
210 self.connect_url
211 .set_path(&Self::normalize_url_path(request_path.as_ref()));
212
213 self
214 }
215
216 pub fn websocket_config(mut self, websocket_config: WebSocketConfig) -> Self {
222 self.config.websocket_config = websocket_config;
223 self
224 }
225
226 pub fn websocket_config_mut<F: FnOnce(&mut WebSocketConfig)>(mut self, f: F) -> Self {
231 f(&mut self.config.websocket_config);
232 self
233 }
234
235 pub fn with_init_handler<H, Fut, D, R>(mut self, handler: H) -> WsIoClientBuilder
241 where
242 H: Fn(Arc<WsIoClientSession>, Option<D>) -> Fut + Send + Sync + 'static,
243 Fut: Future<Output = Result<Option<R>>> + Send + 'static,
244 D: DeserializeOwned + Send + 'static,
245 R: Serialize + Send + 'static,
246 {
247 let handler = Arc::new(handler);
248 self.config.init_handler = Some(Box::new(move |session, bytes, packet_codec| {
249 let handler = handler.clone();
250 Box::pin(async move {
251 handler(session, bytes.map(|bytes| packet_codec.decode_data(bytes)).transpose()?)
252 .await?
253 .map(|data| packet_codec.encode_data(&data))
254 .transpose()
255 })
256 }));
257
258 self
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use tokio_tungstenite::tungstenite::http::HeaderValue;
265
266 use super::*;
267
268 const TEST_URL: &str = "ws://localhost:8080/socket";
269
270 fn test_builder() -> WsIoClientBuilder {
271 WsIoClientBuilder::new(Url::parse(TEST_URL).unwrap()).unwrap()
272 }
273
274 #[test]
275 fn test_builder_new_valid_ws_url_sets_default_request_path_and_namespace_query() {
276 let builder = test_builder();
277
278 assert_eq!(builder.connect_url.path(), "/ws.io");
279 assert_eq!(
280 builder
281 .connect_url
282 .query_pairs()
283 .find(|(key, _)| key == "namespace")
284 .map(|(_, value)| value.into_owned()),
285 Some("/socket".into())
286 );
287 }
288
289 #[test]
290 fn test_builder_new_valid_wss_url() {
291 let result = WsIoClientBuilder::new(Url::parse("wss://localhost:8080/socket").unwrap());
292 assert!(result.is_ok());
293 }
294
295 #[test]
296 fn test_builder_new_invalid_scheme() {
297 let result = WsIoClientBuilder::new(Url::parse("http://localhost:8080/socket").unwrap());
298 assert!(result.is_err());
299 if let Err(e) = result {
300 let err_msg = format!("{e}");
301 assert!(err_msg.contains("Invalid URL scheme"));
302 }
303 }
304
305 #[test]
306 fn test_builder_configuration_chaining_updates_runtime_config() {
307 let builder = test_builder()
308 .disconnect_timeout(Duration::from_secs(20))
309 .init_handler_timeout(Duration::from_secs(10))
310 .init_packet_timeout(Duration::from_secs(15))
311 .on_session_close_handler_timeout(Duration::from_secs(5))
312 .packet_codec(WsIoPacketCodec::SerdeJson)
313 .ping_interval(Duration::from_secs(30))
314 .ready_packet_timeout(Duration::from_secs(10))
315 .reconnect_delay(Duration::from_secs(5))
316 .request_path("/custom/path");
317
318 assert_eq!(builder.connect_url.path(), "/custom/path");
319
320 let client = builder.build();
321
322 let config = &client.0.config;
323 assert_eq!(config.disconnect_timeout, Duration::from_secs(20));
324 assert_eq!(config.init_handler_timeout, Duration::from_secs(10));
325 assert_eq!(config.init_packet_timeout, Duration::from_secs(15));
326 assert_eq!(config.on_session_close_handler_timeout, Duration::from_secs(5));
327 assert!(matches!(config.packet_codec, WsIoPacketCodec::SerdeJson));
328 assert_eq!(config.ping_interval, Duration::from_secs(30));
329 assert_eq!(config.ready_packet_timeout, Duration::from_secs(10));
330 assert_eq!(config.reconnect_delay, Duration::from_secs(5));
331 }
332
333 #[test]
334 fn test_builder_request_path_normalizes() {
335 let builder = test_builder().request_path("/multiple//slashes///path/");
336
337 assert_eq!(builder.connect_url.path(), "/multiple/slashes/path");
338 }
339
340 #[test]
341 fn test_builder_websocket_config_override() {
342 let client = test_builder()
343 .websocket_config_mut(|config| {
344 *config = config.max_frame_size(Some(1024 * 1024));
345 })
346 .build();
347
348 assert_eq!(client.0.config.websocket_config.max_frame_size, Some(1024 * 1024));
349 }
350
351 #[test]
352 fn test_builder_websocket_config_replaces_defaults() {
353 let config = WebSocketConfig::default().max_frame_size(Some(42));
354 let client = test_builder().websocket_config(config).build();
355
356 assert_eq!(client.0.config.websocket_config.max_frame_size, Some(42));
357 }
358
359 #[test]
360 fn test_builder_with_init_and_session_handlers_registers_callbacks() {
361 let client = test_builder()
362 .with_init_handler(|_session, _data: Option<String>| async { Ok(Some("response".to_string())) })
363 .on_session_ready(|_session| async { Ok(()) })
364 .on_session_close(|_session| async { Ok(()) })
365 .build();
366
367 assert!(client.0.config.init_handler.is_some());
368 assert!(client.0.config.on_session_ready_handler.is_some());
369 assert!(client.0.config.on_session_close_handler.is_some());
370 }
371
372 #[test]
373 fn test_builder_request_modifier_registers_async_callback() {
374 let client = test_builder()
375 .request_modifier(|mut request| async move {
376 request
377 .headers_mut()
378 .insert("x-wsio-test", HeaderValue::from_static("enabled"));
379
380 Ok(request)
381 })
382 .build();
383
384 assert!(client.0.config.request_modifier.is_some());
385 }
386
387 #[test]
388 fn test_builder_all_timeout_configurations() {
389 let client = test_builder()
390 .disconnect_timeout(Duration::from_millis(500))
391 .init_handler_timeout(Duration::from_secs(1))
392 .init_packet_timeout(Duration::from_secs(2))
393 .on_session_close_handler_timeout(Duration::from_secs(3))
394 .ready_packet_timeout(Duration::from_secs(4))
395 .build();
396
397 assert_eq!(client.0.config.disconnect_timeout, Duration::from_millis(500));
398 assert_eq!(client.0.config.init_handler_timeout, Duration::from_secs(1));
399 assert_eq!(client.0.config.init_packet_timeout, Duration::from_secs(2));
400 assert_eq!(client.0.config.on_session_close_handler_timeout, Duration::from_secs(3));
401 assert_eq!(client.0.config.ready_packet_timeout, Duration::from_secs(4));
402 }
403
404 #[test]
405 fn test_builder_reconnect_delay_configuration() {
406 let client = test_builder().reconnect_delay(Duration::from_millis(500)).build();
407
408 assert_eq!(client.0.config.reconnect_delay, Duration::from_millis(500));
409 }
410}