1use 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#[derive(Debug, ThisError)]
43pub enum FormatError {
44 #[error("received text for a binary format")]
47 ReceivedTextForBinary,
48 #[error("received binary for a text format")]
51 ReceivedBinaryForText,
52 #[error("trying to encode a binary format as Text")]
55 CantEncodeBinaryAsText,
56}
57
58pub type Text = Result<String, Error>;
63
64pub type Binary = Result<Vec<u8>, Error>;
66
67#[derive(Clone, Debug, PartialEq)]
69pub enum WebSocketStatus {
70 Opened,
72 Closed,
74 Error,
76}
77
78#[derive(Clone, Debug, PartialEq, thiserror::Error)]
79pub enum WebSocketError {
81 #[error("{0}")]
82 CreationError(String),
84}
85
86#[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#[derive(Default, Debug)]
119pub struct WebSocketService {}
120
121impl WebSocketService {
122 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, ¬ification)?;
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 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, ¬ification)?;
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 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, ¬ification)?;
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 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 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}