wasm_sockets/
lib.rs

1//! This crate offers 2 (wasm-only) websocket clients.
2//! The first client offered is the [`EventClient`]. This client is event based and gives you the most control.
3//! ```
4//! use console_error_panic_hook;
5//! use console_log;
6//! use log::{error, info, Level};
7//! use std::panic;
8//! use wasm_sockets::{self, WebSocketError};
9//!
10//! fn main() -> Result<(), WebSocketError> {
11//!     panic::set_hook(Box::new(console_error_panic_hook::hook));
12//!     // console_log and log macros are used instead of println!
13//!     // so that messages can be seen in the browser console
14//!     console_log::init_with_level(Level::Trace).expect("Failed to enable logging");
15//!     info!("Creating connection");
16//!
17//!     let mut client = wasm_sockets::EventClient::new("wss://ws.ifelse.io")?;
18//!     client.set_on_error(Some(Box::new(|error| {
19//!         error!("{:#?}", error);
20//!     })));
21//!     client.set_on_connection(Some(Box::new(|client: &wasm_sockets::EventClient| {
22//!         info!("{:#?}", client.status);
23//!         info!("Sending message...");
24//!         client.send_string("Hello, World!").unwrap();
25//!         client.send_binary(vec![20]).unwrap();
26//!     })));
27//!     client.set_on_close(Some(Box::new(|_evt| {
28//!         info!("Connection closed");
29//!     })));
30//!     client.set_on_message(Some(Box::new(
31//!         |client: &wasm_sockets::EventClient, message: wasm_sockets::Message| {
32//!             info!("New Message: {:#?}", message);
33//!         },
34//!     )));
35//!
36//!     info!("Connection successfully created");
37//!     Ok(())
38//! }
39//! ```
40//! The second client offered is the [`PollingClient`]. This client is ideal for games, because it is designed to be used with a loop.
41//! This client is also much simpler than the [`EventClient`]. However, you can access the main [`EventClient`] that it is using
42//! if you want access to lower level control.
43//! ```
44//! use console_error_panic_hook;
45//! use log::{info, Level};
46//! use std::cell::RefCell;
47//! use std::panic;
48//! use std::rc::Rc;
49//! #[cfg(target_arch = "wasm32")]
50//! use wasm_bindgen::prelude::*;
51//! use wasm_sockets::{self, ConnectionStatus, WebSocketError};
52//!
53//! fn main() -> Result<(), WebSocketError> {
54//!     panic::set_hook(Box::new(console_error_panic_hook::hook));
55//!     // console_log and log macros are used instead of println!
56//!     // so that messages can be seen in the browser console
57//!     console_log::init_with_level(Level::Trace).expect("Failed to enable logging");
58//!     info!("Creating connection");
59//!
60//!     // Client is wrapped in an Rc<RefCell<>> so it can be used within setInterval
61//!     // This isn't required when being used within a game engine
62//!     let client = Rc::new(RefCell::new(wasm_sockets::PollingClient::new(
63//!         "wss://ws.ifelse.io",
64//!     )?));
65//!
66//!     let f = Closure::wrap(Box::new(move || {
67//!         if client.borrow().status() == ConnectionStatus::Connected {
68//!             info!("Sending message");
69//!             client.borrow().send_string("Hello, World!").unwrap();
70//!         }
71//!         // receive() gives you all new websocket messages since receive() was last called
72//!         info!("New messages: {:#?}", client.borrow_mut().receive());
73//!     }) as Box<dyn Fn()>);
74//!
75//!     // Start non-blocking game loop
76//!     setInterval(&f, 100);
77//!     f.forget();
78//!
79//!     Ok(())
80//! }
81//! // Bind setInterval to make a basic game loop
82//! #[wasm_bindgen]
83//! extern "C" {
84//!     fn setInterval(closure: &Closure<dyn Fn()>, time: u32) -> i32;
85//! }
86//! ```
87#[cfg(test)]
88mod tests;
89use log::{error, trace};
90use std::cell::RefCell;
91use std::rc::Rc;
92use thiserror::Error;
93#[cfg(target_arch = "wasm32")]
94use wasm_bindgen::prelude::*;
95#[cfg(target_arch = "wasm32")]
96use wasm_bindgen::JsCast;
97use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket};
98
99#[cfg(not(target_arch = "wasm32"))]
100compile_error!("wasm-sockets can only compile to WASM targets");
101
102#[derive(Debug, Clone, PartialEq)]
103pub enum ConnectionStatus {
104    /// Connecting to a server
105    Connecting,
106    /// Connected to a server
107    Connected,
108    /// Disconnected from a server due to an error
109    Error,
110    /// Disconnected from a server without an error
111    Disconnected,
112}
113
114/// Message is a representation of a websocket message that can be sent or recieved
115#[derive(Debug, Clone)]
116pub enum Message {
117    /// A text message
118    Text(String),
119    /// A binary message
120    Binary(Vec<u8>),
121}
122#[cfg(target_arch = "wasm32")]
123pub struct PollingClient {
124    /// The URL this client is connected to
125    pub url: String,
126    /// The core [`EventClient`] this client is using
127    pub event_client: EventClient,
128    /// The current connection status
129    pub status: Rc<RefCell<ConnectionStatus>>,
130    data: Rc<RefCell<Vec<Message>>>,
131}
132#[cfg(target_arch = "wasm32")]
133// TODO: Replace unwraps and JsValue with custom error type
134impl PollingClient {
135    /// Create a new PollingClient and connect to a WebSocket URL
136    ///
137    /// Note: An Ok() from this function does not mean the connection has succeeded.
138    /// ```
139    /// PollingClient::new("wss://ws.ifelse.io")?;
140    /// ```
141    pub fn new(url: &str) -> Result<Self, WebSocketError> {
142        // Create connection
143        let mut client = EventClient::new(url)?;
144        let data = Rc::new(RefCell::new(vec![]));
145        let data_ref = data.clone();
146        let status = Rc::new(RefCell::new(ConnectionStatus::Connecting));
147        let status_ref = status.clone();
148
149        client.set_on_connection(Some(Box::new(move |_client| {
150            *status_ref.borrow_mut() = ConnectionStatus::Connected;
151        })));
152
153        let status_ref = status.clone();
154
155        client.set_on_error(Some(Box::new(move |_e| {
156            *status_ref.borrow_mut() = ConnectionStatus::Error;
157        })));
158
159        let status_ref = status.clone();
160
161        client.set_on_close(Some(Box::new(move |_evt| {
162            *status_ref.borrow_mut() = ConnectionStatus::Disconnected;
163        })));
164
165        client.set_on_message(Some(Box::new(move |_client: &EventClient, m: Message| {
166            data_ref.borrow_mut().push(m);
167        })));
168
169        Ok(Self {
170            url: url.to_string(),
171            event_client: client,
172            status,
173            data,
174        })
175    }
176    /// Get all new WebSocket messages that were received since this function was last called
177    /// ```
178    /// println!("New messages: {:#?}", client.receive());
179    /// ```
180    pub fn receive(&mut self) -> Vec<Message> {
181        let data = (*self.data.borrow()).clone();
182        (*self.data.borrow_mut()).clear();
183        data
184    }
185    /// Get the client's current connection status
186    /// ```
187    /// println!("Current status: {:#?}", client.status());
188    /// ```
189    pub fn status(&self) -> ConnectionStatus {
190        self.status.borrow().clone()
191    }
192    /// Send a text message to the server
193    /// ```
194    /// client.send_string("Hello server!")?;
195    /// ```
196    pub fn send_string(&self, message: &str) -> Result<(), JsValue> {
197        self.event_client.send_string(message)
198    }
199    /// Send a binary message to the server
200    /// ```
201    /// client.send_binary(vec![0x2, 0xF])?;
202    /// ```
203    pub fn send_binary(&self, message: Vec<u8>) -> Result<(), JsValue> {
204        self.event_client.send_binary(message)
205    }
206
207    /// Close the connection
208    /// ```
209    /// client.close()?;
210    /// ```
211    pub fn close(&self) -> Result<(), JsValue> {
212        self.event_client.close()
213    }
214    /// Close the connection with a custom close code and, optionally, a reason string
215    ///
216    /// The reason string must be at most 123 bytes long.
217    ///
218    /// ```
219    /// client.close_with(1001, Some("going away"))?;
220    /// ```
221    pub fn close_with(&self, code: u16, reason: Option<&str>) -> Result<(), JsValue> {
222        self.event_client.close_with(code, reason)
223    }
224}
225
226#[derive(Debug, Clone, Error)]
227pub enum WebSocketError {
228    #[error("Failed to create websocket connection: {0}")]
229    ConnectionCreationError(String),
230}
231
232#[cfg(target_arch = "wasm32")]
233pub struct EventClient {
234    /// The URL this client is connected to
235    pub url: Rc<RefCell<String>>,
236    /// The raw web_sys WebSocket object this client is using.
237    /// Be careful when using this field, as it will be a different type depending on the compilation target.
238    connection: Rc<RefCell<web_sys::WebSocket>>,
239    /// The current connection status
240    pub status: Rc<RefCell<ConnectionStatus>>,
241    /// The function bound to the on_error event
242    pub on_error: Rc<RefCell<Option<Box<dyn Fn(ErrorEvent)>>>>,
243    /// The function bound to the on_connection event
244    pub on_connection: Rc<RefCell<Option<Box<dyn Fn(&EventClient)>>>>,
245    /// The function bound to the on_message event
246    pub on_message: Rc<RefCell<Option<Box<dyn Fn(&EventClient, Message)>>>>,
247    /// The function bound to the on_close event
248    pub on_close: Rc<RefCell<Option<Box<dyn Fn(CloseEvent)>>>>,
249}
250
251#[cfg(target_arch = "wasm32")]
252impl EventClient {
253    /// Create a new EventClient and connect to a WebSocket URL
254    ///
255    /// Note: An Ok() from this function does not mean the connection has succeeded.
256    /// ```
257    /// EventClient::new("wss://ws.ifelse.io")?;
258    /// ```
259    pub fn new(url: &str) -> Result<Self, WebSocketError> {
260        // Create connection
261        let ws: web_sys::WebSocket = match WebSocket::new(url) {
262            Ok(ws) => ws,
263            Err(_e) => Err(WebSocketError::ConnectionCreationError(
264                "Failed to connect".into(),
265            ))?,
266        };
267        // For small binary messages, like CBOR, Arraybuffer is more efficient than Blob handling
268        ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
269
270        let status = Rc::new(RefCell::new(ConnectionStatus::Connecting));
271        let ref_status = status.clone();
272
273        let on_error: Rc<RefCell<Option<Box<dyn Fn(ErrorEvent)>>>> = Rc::new(RefCell::new(None));
274        let on_error_ref = on_error.clone();
275
276        let onerror_callback = Closure::wrap(Box::new(move |e: ErrorEvent| {
277            *ref_status.borrow_mut() = ConnectionStatus::Error;
278            if let Some(f) = &*on_error_ref.borrow() {
279                f.as_ref()(e);
280            }
281        }) as Box<dyn Fn(ErrorEvent)>);
282        ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
283        onerror_callback.forget();
284
285        let on_close: Rc<RefCell<Option<Box<dyn Fn(CloseEvent)>>>> = Rc::new(RefCell::new(None));
286        let on_close_ref = on_close.clone();
287        let ref_status = status.clone();
288
289        let onclose_callback = Closure::wrap(Box::new(move |e: CloseEvent| {
290            *ref_status.borrow_mut() = ConnectionStatus::Disconnected;
291            if let Some(f) = &*on_close_ref.borrow() {
292                f.as_ref()(e);
293            }
294        }) as Box<dyn Fn(CloseEvent)>);
295        ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
296        onclose_callback.forget();
297
298        let on_connection: Rc<RefCell<Option<Box<dyn Fn(&EventClient)>>>> =
299            Rc::new(RefCell::new(None));
300        let on_connection_ref = on_connection.clone();
301
302        let on_message: Rc<RefCell<Option<Box<dyn Fn(&EventClient, Message)>>>> =
303            Rc::new(RefCell::new(None));
304        let on_message_ref = on_message.clone();
305
306        let ref_status = status.clone();
307
308        let connection = Rc::new(RefCell::new(ws));
309
310        let client = Rc::new(RefCell::new(Self {
311            url: Rc::new(RefCell::new(url.to_string())),
312            connection: connection.clone(),
313            on_error: on_error.clone(),
314            on_connection: on_connection.clone(),
315            status: status.clone(),
316            on_message: on_message.clone(),
317            on_close: on_close.clone(),
318        }));
319        let client_ref = client.clone();
320
321        let onopen_callback = Closure::wrap(Box::new(move |_| {
322            *ref_status.borrow_mut() = ConnectionStatus::Connected;
323            if let Some(f) = &*on_connection_ref.borrow() {
324                f.as_ref()(&*client_ref.clone().borrow());
325            }
326        }) as Box<dyn Fn(JsValue)>);
327        connection
328            .borrow_mut()
329            .set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
330        onopen_callback.forget();
331
332        let client_ref = client;
333
334        let onmessage_callback = Closure::wrap(Box::new(move |e: MessageEvent| {
335            // Process different types of message data
336            if let Ok(abuf) = e.data().dyn_into::<js_sys::ArrayBuffer>() {
337                // Received arraybuffer
338                trace!("message event, received arraybuffer: {:?}", abuf);
339                // Convert arraybuffer to vec
340                let array = js_sys::Uint8Array::new(&abuf).to_vec();
341                if let Some(f) = &*on_message_ref.borrow() {
342                    f.as_ref()(&*client_ref.clone().borrow(), Message::Binary(array));
343                }
344            } else if let Ok(blob) = e.data().dyn_into::<web_sys::Blob>() {
345                // Received blob data
346                trace!("message event, received blob: {:?}", blob);
347                let fr = web_sys::FileReader::new().unwrap();
348                let fr_c = fr.clone();
349                // create onLoadEnd callback
350                let cbref = on_message_ref.clone();
351                let cbfref = client_ref.clone();
352                let onloadend_cb = Closure::wrap(Box::new(move |_e: web_sys::ProgressEvent| {
353                    let array = js_sys::Uint8Array::new(&fr_c.result().unwrap()).to_vec();
354                    if let Some(f) = &*cbref.borrow() {
355                        f.as_ref()(&*cbfref.clone().borrow(), Message::Binary(array));
356                    }
357                })
358                    as Box<dyn Fn(web_sys::ProgressEvent)>);
359                fr.set_onloadend(Some(onloadend_cb.as_ref().unchecked_ref()));
360                fr.read_as_array_buffer(&blob).expect("blob not readable");
361                onloadend_cb.forget();
362            } else if let Ok(txt) = e.data().dyn_into::<js_sys::JsString>() {
363                if let Some(f) = &*on_message_ref.borrow() {
364                    f.as_ref()(&*client_ref.clone().borrow(), Message::Text(txt.into()));
365                }
366            } else {
367                // Got unknown data
368                panic!("Unknown data: {:#?}", e.data());
369            }
370        }) as Box<dyn Fn(MessageEvent)>);
371        // set message event handler on WebSocket
372        connection
373            .borrow()
374            .set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
375        // forget the callback to keep it alive
376        onmessage_callback.forget();
377
378        Ok(Self {
379            url: Rc::new(RefCell::new(url.to_string())),
380            connection,
381            on_error,
382            on_connection,
383            on_message,
384            on_close,
385            status,
386        })
387    }
388    /// Set an on_error event handler.
389    /// This handler will be run when the client disconnects from the server due to an error.
390    /// This will overwrite the previous handler.
391    /// You can set [None](std::option) to disable the on_error handler.
392    /// ```
393    /// client.set_on_error(Some(Box::new(|error| {
394    ///    panic!("Error: {:#?}", error);
395    /// })));
396    /// ```
397    pub fn set_on_error(&mut self, f: Option<Box<dyn Fn(ErrorEvent)>>) {
398        *self.on_error.borrow_mut() = f;
399    }
400    /// Set an on_connection event handler.
401    /// This handler will be run when the client successfully connects to a server.
402    /// This will overwrite the previous handler.
403    /// You can set [None](std::option) to disable the on_connection handler.
404    /// ```
405    /// client.set_on_connection(Some(Box::new(|client| {
406    ///     info!("Connected");
407    /// })));
408    /// ```
409    pub fn set_on_connection(&mut self, f: Option<Box<dyn Fn(&EventClient)>>) {
410        *self.on_connection.borrow_mut() = f;
411    }
412    /// Set an on_message event handler.
413    /// This handler will be run when the client receives a message from a server.
414    /// This will overwrite the previous handler.
415    /// You can set [None](std::option) to disable the on_message handler.
416    /// ```
417    /// client.set_on_message(Some(Box::new(
418    ///     |c, m| {
419    ///         info!("New Message: {:#?}", m);
420    ///     },
421    ///  )));
422    /// ```
423    pub fn set_on_message(&mut self, f: Option<Box<dyn Fn(&EventClient, Message)>>) {
424        *self.on_message.borrow_mut() = f;
425    }
426    /// Set an on_close event handler.
427    /// This handler will be run when the client disconnects from a server without an error.
428    /// This will overwrite the previous handler.
429    /// You can set [None](std::option) to disable the on_close handler.
430    /// ```
431    /// client.set_on_close(Some(Box::new(|_evt| {
432    ///     info!("Closed");
433    /// })));
434    /// ```
435    pub fn set_on_close(&mut self, f: Option<Box<dyn Fn(CloseEvent)>>) {
436        *self.on_close.borrow_mut() = f;
437    }
438
439    /// Send a text message to the server
440    /// ```
441    /// client.send_string("Hello server!")?;
442    /// ```
443    pub fn send_string(&self, message: &str) -> Result<(), JsValue> {
444        self.connection.borrow().send_with_str(message)
445    }
446    /// Send a binary message to the server
447    /// ```
448    /// client.send_binary(vec![0x2, 0xF])?;
449    /// ```
450    pub fn send_binary(&self, message: Vec<u8>) -> Result<(), JsValue> {
451        self.connection
452            .borrow()
453            .send_with_u8_array(message.as_slice())
454    }
455
456    /// Close the connection
457    /// ```
458    /// client.close()?;
459    /// ```
460    pub fn close(&self) -> Result<(), JsValue> {
461        self.connection.borrow().close()
462    }
463    /// Close the connection with a custom close code and, optionally, a reason string
464    ///
465    /// The reason string must be at most 123 bytes long.
466    ///
467    /// ```
468    /// client.close_with(1001, Some("going away"))?;
469    /// ```
470    pub fn close_with(&self, code: u16, reason: Option<&str>) -> Result<(), JsValue> {
471        match reason {
472            Some(reason) => self
473                .connection
474                .borrow()
475                .close_with_code_and_reason(code, reason),
476            None => self.connection.borrow().close_with_code(code),
477        }
478    }
479}