Skip to main content

musli_web/
web03.rs

1//! Client side implementation for [`web-sys`] `0.3.x`.
2//!
3//! [`web-sys`]: <https://docs.rs/web-sys/0.3>
4//!
5//! # Examples
6//!
7//! This example uses [`web03`]:
8//!
9//! [`web03`]: crate::web03
10//!
11//! ```no_run
12//! use musli_web::web03::prelude::*;
13//!
14//! mod api {
15//!     use musli::{Decode, Encode};
16//!     use musli_web::api;
17//!
18//!     #[derive(Encode, Decode)]
19//!     pub struct HelloRequest<'de> {
20//!         pub message: &'de str,
21//!     }
22//!
23//!     #[derive(Encode, Decode)]
24//!     pub struct HelloResponse<'de> {
25//!         pub message: &'de str,
26//!     }
27//!
28//!     api::define! {
29//!         pub type Hello;
30//!
31//!         impl Endpoint for Hello {
32//!             impl<'de> Request for HelloRequest<'de>;
33//!             type Response<'de> = HelloResponse<'de>;
34//!         }
35//!     }
36//! }
37//!
38//! let service = ws::connect(ws::Connect::location("ws"))
39//!     .on_error(|error| {
40//!         tracing::error!("WebSocket error: {error}");
41//!     })
42//!     .build();
43//!
44//! service.connect();
45//!
46//! let request = service
47//!     .handle()
48//!     .request()
49//!     .body(api::HelloRequest {
50//!         message: "Hello!",
51//!     })
52//!     .on_raw_packet(move |packet: Result<ws::RawPacket, ws::Error>| {
53//!         match packet {
54//!             Ok(packet) => {
55//!                 if let Ok(response) = packet.decode::<api::HelloResponse>() {
56//!                     tracing::info!("Response: {}", response.message);
57//!                 }
58//!             }
59//!             Err(error) => {
60//!                 tracing::error!("Request error: {error}");
61//!             }
62//!         }
63//!     })
64//!     .send();
65//! ```
66
67use alloc::rc::Rc;
68use alloc::rc::Weak;
69use alloc::string::String;
70
71use wasm_bindgen02::JsCast;
72use wasm_bindgen02::closure::Closure;
73use web_sys03::js_sys::{ArrayBuffer, Math, Uint8Array};
74use web_sys03::{BinaryType, CloseEvent, ErrorEvent, MessageEvent, WebSocket, Window, window};
75
76use crate::api::Mode;
77use crate::web::{
78    Connect, EmptyCallback, Error, Location, ServiceBuilder, Shared, SocketImpl, WebImpl,
79    WindowImpl,
80};
81
82pub mod prelude {
83    //! The public facing API for use with yew `0.2.1` and web-sys `0.3.x`.
84
85    pub mod ws {
86        //! Organization module prefixing all exported items with `ws` for
87        //! convenient namespacing.
88
89        pub use crate::api::ChannelId;
90        pub use crate::web::{
91            Call, ChannelEvent, Connect, EmptyCallback, Error, KeyedRequests, Listener, Packet,
92            RawPacket, Request, Requests, State, StateListener,
93        };
94
95        use crate::web03::Web03Impl;
96
97        /// Implementation alias for [`connect`].
98        ///
99        /// [`connect`]: crate::web03::connect
100        pub fn connect(connect: Connect) -> ServiceBuilder<EmptyCallback> {
101            crate::web03::connect(connect)
102        }
103
104        /// Implementation alias for [`Service`].
105        ///
106        /// [`Service`]: crate::web::Service
107        pub type Service = crate::web::Service<Web03Impl>;
108
109        /// Implementation alias for [`Handle`].
110        ///
111        /// [`Handle`]: crate::web::Handle
112        pub type Handle = crate::web::Handle<Web03Impl>;
113
114        /// Implementation alias for [`Channel`].
115        ///
116        /// [`Channel`]: crate::web::Channel
117        pub type Channel = crate::web::Channel<Web03Impl>;
118
119        /// Implementation alias for [`RequestBuilder`].
120        ///
121        /// [`RequestBuilder`]: crate::web::RequestBuilder
122        pub type RequestBuilder<'a, B, C> = crate::web::RequestBuilder<'a, Web03Impl, B, C>;
123
124        /// Implementation alias for [`ServiceBuilder`].
125        ///
126        /// [`ServiceBuilder`]: crate::web::ServiceBuilder
127        pub type ServiceBuilder<C> = crate::web::ServiceBuilder<Web03Impl, C>;
128    }
129}
130
131/// Handles for websocket implementation.
132#[doc(hidden)]
133pub struct Handles {
134    close: Closure<dyn Fn(CloseEvent)>,
135    message: Closure<dyn Fn(MessageEvent)>,
136    error: Closure<dyn Fn(ErrorEvent)>,
137}
138
139/// WebSocket implementation for web-sys `0.3.x`.
140///
141/// See [`connect()`].
142#[derive(Clone, Copy)]
143pub enum Web03Impl {}
144
145impl crate::web::sealed_socket::Sealed for WebSocket {}
146
147impl SocketImpl for WebSocket {
148    type Handles = Handles;
149
150    #[inline]
151    fn new(url: &str, handles: &Self::Handles) -> Result<Self, Error> {
152        let this = WebSocket::new(url)?;
153        this.set_binary_type(BinaryType::Arraybuffer);
154        this.set_onclose(Some(handles.close.as_ref().unchecked_ref()));
155        this.set_onmessage(Some(handles.message.as_ref().unchecked_ref()));
156        this.set_onerror(Some(handles.error.as_ref().unchecked_ref()));
157        Ok(this)
158    }
159
160    #[inline]
161    fn send(&self, mode: Mode, data: &[u8]) -> Result<(), Error> {
162        match mode {
163            Mode::Binary => {
164                self.send_with_u8_array(data)?;
165            }
166            Mode::Text => {
167                // NB: A text frame is only ever written in a mode which
168                // guarantees that everything in it is valid UTF-8, so the lossy
169                // path is never taken.
170                self.send_with_str(&String::from_utf8_lossy(data))?;
171            }
172        }
173
174        Ok(())
175    }
176
177    #[inline]
178    fn close(self) -> Result<(), Error> {
179        // Clear event listeners to ensure they don't fire when we dispose of
180        // the socket.
181        WebSocket::set_onclose(&self, None);
182        WebSocket::set_onmessage(&self, None);
183        WebSocket::set_onerror(&self, None);
184        WebSocket::close(&self)?;
185        Ok(())
186    }
187}
188
189impl crate::web::sealed_window::Sealed for Window {}
190
191impl WindowImpl for Window {
192    type Timeout = Timeout;
193    type OnBeforeUnload = Event;
194
195    #[inline]
196    fn new() -> Result<Self, Error> {
197        let Some(window) = window() else {
198            return Err(Error::message("No window in web-sys 0.3.x context"));
199        };
200
201        Ok(window)
202    }
203
204    #[inline]
205    fn location(&self) -> Result<Location, Error> {
206        let location = Window::location(self);
207
208        Ok(Location {
209            protocol: location.protocol()?,
210            host: location.hostname()?,
211            port: location.port()?,
212        })
213    }
214
215    #[inline]
216    fn onbeforeunload(&self, callback: impl Fn() + 'static) -> Result<Self::OnBeforeUnload, Error> {
217        let closure = Closure::new(callback);
218        window()
219            .unwrap()
220            .set_onbeforeunload(Some(closure.as_ref().unchecked_ref()));
221        Ok(Event { closure })
222    }
223
224    #[inline]
225    fn set_timeout(
226        &self,
227        millis: u32,
228        callback: impl Fn() + 'static,
229    ) -> Result<Self::Timeout, Error> {
230        let closure = Closure::new(callback);
231
232        let id = self.set_timeout_with_callback_and_timeout_and_arguments_0(
233            closure.as_ref().unchecked_ref(),
234            millis as i32,
235        )?;
236
237        Ok(Timeout {
238            window: self.clone(),
239            id: Some(id),
240            closure: Some(closure),
241        })
242    }
243}
244
245pub struct Timeout {
246    window: Window,
247    id: Option<i32>,
248    #[allow(dead_code)]
249    closure: Option<Closure<dyn FnMut()>>,
250}
251
252impl Drop for Timeout {
253    /// Disposes of the timeout, dually cancelling this timeout by calling
254    /// `clearTimeout` directly.
255    fn drop(&mut self) {
256        if let Some(id) = self.id.take() {
257            self.window.clear_timeout_with_handle(id);
258        }
259    }
260}
261
262pub struct Event {
263    #[allow(dead_code)]
264    closure: Closure<dyn FnMut()>,
265}
266
267impl crate::web::sealed_web::Sealed for Web03Impl {}
268
269impl WebImpl for Web03Impl {
270    type Window = Window;
271    type Handles = Handles;
272    type Socket = WebSocket;
273
274    #[inline]
275    fn random(range: u32) -> u32 {
276        ((Math::random() * range as f64).round() as u32).min(range)
277    }
278
279    #[inline]
280    #[allow(private_interfaces)]
281    fn handles(shared: &Weak<Shared<Self>>) -> Self::Handles {
282        let close = {
283            let shared = shared.clone();
284
285            Closure::new(move |e: CloseEvent| {
286                if let Some(shared) = shared.upgrade() {
287                    shared.web03_close(e);
288                }
289            })
290        };
291
292        let message = {
293            let shared = shared.clone();
294
295            Closure::new(move |e: MessageEvent| {
296                if let Some(shared) = shared.upgrade() {
297                    shared.web03_message(e);
298                }
299            })
300        };
301
302        let error = {
303            let shared = shared.clone();
304
305            Closure::new(move |e: ErrorEvent| {
306                if let Some(shared) = shared.upgrade() {
307                    shared.web03_error(e);
308                }
309            })
310        };
311
312        Self::Handles {
313            close,
314            message,
315            error,
316        }
317    }
318}
319
320/// Construct a new [`ServiceBuilder`] associated with the given [`Connect`]
321/// strategy.
322#[inline]
323pub fn connect(connect: Connect) -> ServiceBuilder<Web03Impl, EmptyCallback> {
324    crate::web::connect(connect)
325}
326
327impl Shared<Web03Impl> {
328    fn web03_close(self: &Rc<Self>, e: CloseEvent) {
329        tracing::debug!(code = e.code(), reason = e.reason(), "Close event");
330
331        if let Err(e) = self.close_and_reconnect() {
332            self.on_error.call(e);
333        }
334    }
335
336    fn web03_message(self: &Rc<Shared<Web03Impl>>, e: MessageEvent) {
337        tracing::debug!("Message event");
338
339        let data = e.data();
340
341        // NB: The browser hands a text frame over as a string and a binary one
342        // as an `ArrayBuffer`, which is what tells the two modes apart.
343        let result = if let Some(text) = data.as_string() {
344            self.text_message(&text)
345        } else {
346            let Ok(array_buffer) = data.dyn_into::<ArrayBuffer>() else {
347                self.on_error
348                    .call(Error::message("Expected message as ArrayBuffer or string"));
349                return;
350            };
351
352            let array = Uint8Array::new(&array_buffer);
353            let needed = array.length() as usize;
354
355            let mut buf = self.next_buffer(needed);
356
357            // SAFETY: We've sized the buffer appropriately above, and recycled
358            // buffers are empty so nothing is left uninitialized.
359            unsafe {
360                array.raw_copy_to_ptr(buf.data.as_mut_ptr());
361                buf.data.set_len(needed);
362            }
363
364            self.message(Mode::Binary, buf)
365        };
366
367        if let Err(e) = result {
368            self.on_error.call(e);
369        }
370    }
371
372    fn web03_error(self: &Rc<Self>, e: ErrorEvent) {
373        tracing::debug!(message = e.message(), "Error event");
374
375        if let Err(e) = self.close_and_reconnect() {
376            self.on_error.call(e);
377        }
378    }
379}