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}