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;
69
70use wasm_bindgen02::JsCast;
71use wasm_bindgen02::closure::Closure;
72use web_sys03::js_sys::{ArrayBuffer, Math, Uint8Array};
73use web_sys03::{BinaryType, CloseEvent, ErrorEvent, MessageEvent, WebSocket, Window, window};
74
75use crate::web::{
76    Connect, EmptyCallback, Error, Location, ServiceBuilder, Shared, SocketImpl, WebImpl,
77    WindowImpl,
78};
79
80pub mod prelude {
81    //! The public facing API for use with yew `0.2.1` and web-sys `0.3.x`.
82
83    pub mod ws {
84        //! Organization module prefixing all exported items with `ws` for
85        //! convenient namespacing.
86
87        pub use crate::api::ChannelId;
88        pub use crate::web::{
89            Connect, EmptyCallback, Error, Listener, Packet, RawPacket, Request, State,
90            StateListener,
91        };
92
93        use crate::web03::Web03Impl;
94
95        /// Implementation alias for [`connect`].
96        ///
97        /// [`connect`]: crate::web03::connect
98        pub fn connect(connect: Connect) -> ServiceBuilder<EmptyCallback> {
99            crate::web03::connect(connect)
100        }
101
102        /// Implementation alias for [`Service`].
103        ///
104        /// [`Service`]: crate::web::Service
105        pub type Service = crate::web::Service<Web03Impl>;
106
107        /// Implementation alias for [`Handle`].
108        ///
109        /// [`Handle`]: crate::web::Handle
110        pub type Handle = crate::web::Handle<Web03Impl>;
111
112        /// Implementation alias for [`Channel`].
113        ///
114        /// [`Channel`]: crate::web::Channel
115        pub type Channel = crate::web::Channel<Web03Impl>;
116
117        /// Implementation alias for [`RequestBuilder`].
118        ///
119        /// [`RequestBuilder`]: crate::web::RequestBuilder
120        pub type RequestBuilder<'a, B, C> = crate::web::RequestBuilder<'a, Web03Impl, B, C>;
121
122        /// Implementation alias for [`ServiceBuilder`].
123        ///
124        /// [`ServiceBuilder`]: crate::web::ServiceBuilder
125        pub type ServiceBuilder<C> = crate::web::ServiceBuilder<Web03Impl, C>;
126    }
127}
128
129/// Handles for websocket implementation.
130#[doc(hidden)]
131pub struct Handles {
132    close: Closure<dyn Fn(CloseEvent)>,
133    message: Closure<dyn Fn(MessageEvent)>,
134    error: Closure<dyn Fn(ErrorEvent)>,
135}
136
137/// WebSocket implementation for web-sys `0.3.x`.
138///
139/// See [`connect()`].
140#[derive(Clone, Copy)]
141pub enum Web03Impl {}
142
143impl crate::web::sealed_socket::Sealed for WebSocket {}
144
145impl SocketImpl for WebSocket {
146    type Handles = Handles;
147
148    #[inline]
149    fn new(url: &str, handles: &Self::Handles) -> Result<Self, Error> {
150        let this = WebSocket::new(url)?;
151        this.set_binary_type(BinaryType::Arraybuffer);
152        this.set_onclose(Some(handles.close.as_ref().unchecked_ref()));
153        this.set_onmessage(Some(handles.message.as_ref().unchecked_ref()));
154        this.set_onerror(Some(handles.error.as_ref().unchecked_ref()));
155        Ok(this)
156    }
157
158    #[inline]
159    fn send(&self, data: &[u8]) -> Result<(), Error> {
160        self.send_with_u8_array(data)?;
161        Ok(())
162    }
163
164    #[inline]
165    fn close(self) -> Result<(), Error> {
166        // Clear event listeners to ensure they don't fire when we dispose of
167        // the socket.
168        WebSocket::set_onclose(&self, None);
169        WebSocket::set_onmessage(&self, None);
170        WebSocket::set_onerror(&self, None);
171        WebSocket::close(&self)?;
172        Ok(())
173    }
174}
175
176impl crate::web::sealed_window::Sealed for Window {}
177
178impl WindowImpl for Window {
179    type Timeout = Timeout;
180    type OnBeforeUnload = Event;
181
182    #[inline]
183    fn new() -> Result<Self, Error> {
184        let Some(window) = window() else {
185            return Err(Error::message("No window in web-sys 0.3.x context"));
186        };
187
188        Ok(window)
189    }
190
191    #[inline]
192    fn location(&self) -> Result<Location, Error> {
193        let location = Window::location(self);
194
195        Ok(Location {
196            protocol: location.protocol()?,
197            host: location.hostname()?,
198            port: location.port()?,
199        })
200    }
201
202    #[inline]
203    fn onbeforeunload(&self, callback: impl Fn() + 'static) -> Result<Self::OnBeforeUnload, Error> {
204        let closure = Closure::new(callback);
205        window()
206            .unwrap()
207            .set_onbeforeunload(Some(closure.as_ref().unchecked_ref()));
208        Ok(Event { closure })
209    }
210
211    #[inline]
212    fn set_timeout(
213        &self,
214        millis: u32,
215        callback: impl Fn() + 'static,
216    ) -> Result<Self::Timeout, Error> {
217        let closure = Closure::new(callback);
218
219        let id = self.set_timeout_with_callback_and_timeout_and_arguments_0(
220            closure.as_ref().unchecked_ref(),
221            millis as i32,
222        )?;
223
224        Ok(Timeout {
225            window: self.clone(),
226            id: Some(id),
227            closure: Some(closure),
228        })
229    }
230}
231
232pub struct Timeout {
233    window: Window,
234    id: Option<i32>,
235    #[allow(dead_code)]
236    closure: Option<Closure<dyn FnMut()>>,
237}
238
239impl Drop for Timeout {
240    /// Disposes of the timeout, dually cancelling this timeout by calling
241    /// `clearTimeout` directly.
242    fn drop(&mut self) {
243        if let Some(id) = self.id.take() {
244            self.window.clear_timeout_with_handle(id);
245        }
246    }
247}
248
249pub struct Event {
250    #[allow(dead_code)]
251    closure: Closure<dyn FnMut()>,
252}
253
254impl crate::web::sealed_web::Sealed for Web03Impl {}
255
256impl WebImpl for Web03Impl {
257    type Window = Window;
258    type Handles = Handles;
259    type Socket = WebSocket;
260
261    #[inline]
262    fn random(range: u32) -> u32 {
263        ((Math::random() * range as f64).round() as u32).min(range)
264    }
265
266    #[inline]
267    #[allow(private_interfaces)]
268    fn handles(shared: &Weak<Shared<Self>>) -> Self::Handles {
269        let close = {
270            let shared = shared.clone();
271
272            Closure::new(move |e: CloseEvent| {
273                if let Some(shared) = shared.upgrade() {
274                    shared.web03_close(e);
275                }
276            })
277        };
278
279        let message = {
280            let shared = shared.clone();
281
282            Closure::new(move |e: MessageEvent| {
283                if let Some(shared) = shared.upgrade() {
284                    shared.web03_message(e);
285                }
286            })
287        };
288
289        let error = {
290            let shared = shared.clone();
291
292            Closure::new(move |e: ErrorEvent| {
293                if let Some(shared) = shared.upgrade() {
294                    shared.web03_error(e);
295                }
296            })
297        };
298
299        Self::Handles {
300            close,
301            message,
302            error,
303        }
304    }
305}
306
307/// Construct a new [`ServiceBuilder`] associated with the given [`Connect`]
308/// strategy.
309#[inline]
310pub fn connect(connect: Connect) -> ServiceBuilder<Web03Impl, EmptyCallback> {
311    crate::web::connect(connect)
312}
313
314impl Shared<Web03Impl> {
315    fn web03_close(self: &Rc<Self>, e: CloseEvent) {
316        tracing::debug!(code = e.code(), reason = e.reason(), "Close event");
317
318        if let Err(e) = self.close_and_reconnect() {
319            self.on_error.call(e);
320        }
321    }
322
323    fn web03_message(self: &Rc<Shared<Web03Impl>>, e: MessageEvent) {
324        tracing::debug!("Message event");
325
326        let Ok(array_buffer) = e.data().dyn_into::<ArrayBuffer>() else {
327            self.on_error
328                .call(Error::message("Expected message as ArrayBuffer"));
329            return;
330        };
331
332        let array = Uint8Array::new(&array_buffer);
333        let needed = array.length() as usize;
334
335        let mut buf = self.next_buffer(needed);
336
337        // SAFETY: We've sized the buffer appropriately above.
338        unsafe {
339            array.raw_copy_to_ptr(buf.data.as_mut_ptr());
340            buf.data.set_len(needed);
341        }
342
343        if let Err(e) = self.message(buf) {
344            self.on_error.call(e);
345        }
346    }
347
348    fn web03_error(self: &Rc<Self>, e: ErrorEvent) {
349        tracing::debug!(message = e.message(), "Error event");
350
351        if let Err(e) = self.close_and_reconnect() {
352            self.on_error.call(e);
353        }
354    }
355}