yew_websocket/
websocket.rs

1//! A service to connect to a server through the
2//! [`WebSocket` Protocol](https://tools.ietf.org/html/rfc6455).
3
4/**
5 * Copyright (c) 2017 Denis Kolodin
6
7Permission is hereby granted, free of charge, to any
8person obtaining a copy of this software and associated
9documentation files (the "Software"), to deal in the
10Software without restriction, including without
11limitation the rights to use, copy, modify, merge,
12publish, distribute, sublicense, and/or sell copies of
13the Software, and to permit persons to whom the Software
14is furnished to do so, subject to the following
15conditions:
16
17The above copyright notice and this permission notice
18shall be included in all copies or substantial portions
19of the Software.
20
21THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
22ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
23TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
24PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
25SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
26CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
28IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
29DEALINGS IN THE SOFTWARE.
30 */
31use anyhow::Error;
32use std::fmt;
33use thiserror::Error as ThisError;
34use yew::callback::Callback;
35
36use gloo::events::EventListener;
37use js_sys::Uint8Array;
38use wasm_bindgen::JsCast;
39use web_sys::{BinaryType, Event, MessageEvent, WebSocket};
40
41/// Represents formatting errors.
42#[derive(Debug, ThisError)]
43pub enum FormatError {
44    /// Received text for a binary format, e.g. someone sending text
45    /// on a WebSocket that is using a binary serialization format, like Cbor.
46    #[error("received text for a binary format")]
47    ReceivedTextForBinary,
48    /// Received binary for a text format, e.g. someone sending binary
49    /// on a WebSocket that is using a text serialization format, like Json.
50    #[error("received binary for a text format")]
51    ReceivedBinaryForText,
52    /// Trying to encode a binary format as text", e.g., trying to
53    /// store a Cbor encoded value in a String.
54    #[error("trying to encode a binary format as Text")]
55    CantEncodeBinaryAsText,
56}
57
58/// A representation of a value which can be stored and restored as a text.
59///
60/// Some formats are binary only and can't be serialized to or deserialized
61/// from Text.  Attempting to do so will return an Err(FormatError).
62pub type Text = Result<String, Error>;
63
64/// A representation of a value which can be stored and restored as a binary.
65pub type Binary = Result<Vec<u8>, Error>;
66
67/// The status of a WebSocket connection. Used for status notifications.
68#[derive(Clone, Debug, PartialEq)]
69pub enum WebSocketStatus {
70    /// Fired when a WebSocket connection has opened.
71    Opened,
72    /// Fired when a WebSocket connection has closed.
73    Closed,
74    /// Fired when a WebSocket connection has failed.
75    Error,
76}
77
78#[derive(Clone, Debug, PartialEq, thiserror::Error)]
79/// An error encountered by a WebSocket.
80pub enum WebSocketError {
81    #[error("{0}")]
82    /// An error encountered when creating the WebSocket.
83    CreationError(String),
84}
85
86/// A handle to control the WebSocket connection. Implements `Task` and could be canceled.
87#[must_use = "the connection will be closed when the task is dropped"]
88pub struct WebSocketTask {
89    ws: WebSocket,
90    notification: Callback<WebSocketStatus>,
91    #[allow(dead_code)]
92    listeners: [EventListener; 4],
93}
94
95impl WebSocketTask {
96    fn new(
97        ws: WebSocket,
98        notification: Callback<WebSocketStatus>,
99        listener_0: EventListener,
100        listeners: [EventListener; 3],
101    ) -> WebSocketTask {
102        let [listener_1, listener_2, listener_3] = listeners;
103        WebSocketTask {
104            ws,
105            notification,
106            listeners: [listener_0, listener_1, listener_2, listener_3],
107        }
108    }
109}
110
111impl fmt::Debug for WebSocketTask {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.write_str("WebSocketTask")
114    }
115}
116
117/// A WebSocket service attached to a user context.
118#[derive(Default, Debug)]
119pub struct WebSocketService {}
120
121impl WebSocketService {
122    /// Connects to a server through a WebSocket connection. Needs two callbacks; one is passed
123    /// data, the other is passed updates about the WebSocket's status.
124    pub fn connect<OUT: 'static>(
125        url: &str,
126        callback: Callback<OUT>,
127        notification: Callback<WebSocketStatus>,
128    ) -> Result<WebSocketTask, WebSocketError>
129    where
130        OUT: From<Text> + From<Binary>,
131    {
132        let ConnectCommon(ws, listeners) = Self::connect_common(url, &notification)?;
133        let listener = EventListener::new(&ws, "message", move |event: &Event| {
134            let event = event.dyn_ref::<MessageEvent>().unwrap();
135            process_both(event, &callback);
136        });
137        Ok(WebSocketTask::new(ws, notification, listener, listeners))
138    }
139
140    /// Connects to a server through a WebSocket connection, like connect,
141    /// but only processes binary frames. Text frames are silently
142    /// ignored. Needs two functions to generate data and notification
143    /// messages.
144    pub fn connect_binary<OUT: 'static>(
145        url: &str,
146        callback: Callback<OUT>,
147        notification: Callback<WebSocketStatus>,
148    ) -> Result<WebSocketTask, WebSocketError>
149    where
150        OUT: From<Binary>,
151    {
152        let ConnectCommon(ws, listeners) = Self::connect_common(url, &notification)?;
153        let listener = EventListener::new(&ws, "message", move |event: &Event| {
154            let event = event.dyn_ref::<MessageEvent>().unwrap();
155            process_binary(event, &callback);
156        });
157        Ok(WebSocketTask::new(ws, notification, listener, listeners))
158    }
159
160    /// Connects to a server through a WebSocket connection, like connect,
161    /// but only processes text frames. Binary frames are silently
162    /// ignored. Needs two functions to generate data and notification
163    /// messages.
164    pub fn connect_text<OUT: 'static>(
165        url: &str,
166        callback: Callback<OUT>,
167        notification: Callback<WebSocketStatus>,
168    ) -> Result<WebSocketTask, WebSocketError>
169    where
170        OUT: From<Text>,
171    {
172        let ConnectCommon(ws, listeners) = Self::connect_common(url, &notification)?;
173        let listener = EventListener::new(&ws, "message", move |event: &Event| {
174            let event = event.dyn_ref::<MessageEvent>().unwrap();
175            process_text(event, &callback);
176        });
177        Ok(WebSocketTask::new(ws, notification, listener, listeners))
178    }
179
180    fn connect_common(
181        url: &str,
182        notification: &Callback<WebSocketStatus>,
183    ) -> Result<ConnectCommon, WebSocketError> {
184        let ws = WebSocket::new(url);
185
186        let ws = ws.map_err(|ws_error| {
187            WebSocketError::CreationError(
188                ws_error
189                    .unchecked_into::<js_sys::Error>()
190                    .to_string()
191                    .as_string()
192                    .unwrap(),
193            )
194        })?;
195
196        ws.set_binary_type(BinaryType::Arraybuffer);
197        let notify = notification.clone();
198        let listener_open = move |_: &Event| {
199            notify.emit(WebSocketStatus::Opened);
200        };
201        let notify = notification.clone();
202        let listener_close = move |_: &Event| {
203            notify.emit(WebSocketStatus::Closed);
204        };
205        let notify = notification.clone();
206        let listener_error = move |_: &Event| {
207            notify.emit(WebSocketStatus::Error);
208        };
209        {
210            let listeners = [
211                EventListener::new(&ws, "open", listener_open),
212                EventListener::new(&ws, "close", listener_close),
213                EventListener::new(&ws, "error", listener_error),
214            ];
215            Ok(ConnectCommon(ws, listeners))
216        }
217    }
218}
219
220struct ConnectCommon(WebSocket, [EventListener; 3]);
221
222fn process_binary<OUT: 'static>(event: &MessageEvent, callback: &Callback<OUT>)
223where
224    OUT: From<Binary>,
225{
226    let bytes = if !event.data().is_string() {
227        Some(event.data())
228    } else {
229        None
230    };
231
232    let data = if let Some(bytes) = bytes {
233        let bytes: Vec<u8> = Uint8Array::new(&bytes).to_vec();
234        Ok(bytes)
235    } else {
236        Err(FormatError::ReceivedTextForBinary.into())
237    };
238
239    let out = OUT::from(data);
240    callback.emit(out);
241}
242
243fn process_text<OUT: 'static>(event: &MessageEvent, callback: &Callback<OUT>)
244where
245    OUT: From<Text>,
246{
247    let text = event.data().as_string();
248
249    let data = if let Some(text) = text {
250        Ok(text)
251    } else {
252        Err(FormatError::ReceivedBinaryForText.into())
253    };
254
255    let out = OUT::from(data);
256    callback.emit(out);
257}
258
259fn process_both<OUT: 'static>(event: &MessageEvent, callback: &Callback<OUT>)
260where
261    OUT: From<Text> + From<Binary>,
262{
263    let is_text = event.data().is_string();
264    if is_text {
265        process_text(event, callback);
266    } else {
267        process_binary(event, callback);
268    }
269}
270
271impl WebSocketTask {
272    /// Sends data to a WebSocket connection.
273    pub fn send(&mut self, data: String) {
274        let result = self.ws.send_with_str(&data);
275
276        if result.is_err() {
277            self.notification.emit(WebSocketStatus::Error);
278        }
279    }
280
281    /// Sends binary data to a WebSocket connection.
282    pub fn send_binary(&self, data: Vec<u8>) {
283        let result = self.ws.send_with_u8_array(&data);
284
285        if result.is_err() {
286            self.notification.emit(WebSocketStatus::Error);
287        }
288    }
289}
290
291impl WebSocketTask {
292    fn is_active(&self) -> bool {
293        matches!(
294            self.ws.ready_state(),
295            WebSocket::CONNECTING | WebSocket::OPEN
296        )
297    }
298}
299
300impl Drop for WebSocketTask {
301    fn drop(&mut self) {
302        if self.is_active() {
303            self.ws.close().ok();
304        }
305    }
306}