maia_wasm/
websocket.rs

1//! WebSocket client for waterfall data.
2
3use std::cell::RefCell;
4use std::rc::Rc;
5use wasm_bindgen::JsCast;
6use wasm_bindgen::prelude::*;
7use web_sys::{CloseEvent, MessageEvent, WebSocket, Window};
8
9use crate::waterfall::Waterfall;
10
11/// WebSocket client for waterfall data.
12///
13/// Implements a WebSocket client that receives messages containing waterfall
14/// data and submits the data to the waterfall by calling
15/// [Waterfall::put_waterfall_spectrum].
16pub struct WebSocketClient {}
17
18struct WebSocketData {
19    url: String,
20    // Closure that handles onmessage
21    onmessage: JsValue,
22    // Closure that handles onclose. It is inside a RefCell<Option<>> because
23    // the closure is self-referential, in the sense that to try a reconnection,
24    // the onclose closure needs access to the onclose closure, in order to
25    // assign it to the onclose of the new websocket.
26    onclose: RefCell<Option<JsValue>>,
27}
28
29impl WebSocketClient {
30    /// Starts the WebSocket client.
31    ///
32    /// The client is given shared mutable access to the [`Waterfall`].
33    ///
34    /// This function creates and registers the appropriate on-message handler
35    /// for the WebSocket client. No further interaction with the
36    /// `WebSocketClient` returned by this function is needed and it can be
37    /// dropped immediately.
38    pub fn start(window: &Window, waterfall: Rc<RefCell<Waterfall>>) -> Result<(), JsValue> {
39        let location = window.location();
40        let protocol = if location.protocol()? == "https:" {
41            "wss"
42        } else {
43            "ws"
44        };
45        let hostname = location.hostname()?;
46        let port = location.port()?;
47        let data = Rc::new(WebSocketData {
48            url: format!("{protocol}://{hostname}:{port}/waterfall"),
49            onmessage: onmessage(waterfall).into_js_value(),
50            onclose: RefCell::new(None),
51        });
52        data.setup_onclose();
53        // initiate first connection
54        data.connect()?;
55        Ok(())
56    }
57}
58
59fn onmessage(waterfall: Rc<RefCell<Waterfall>>) -> Closure<dyn Fn(MessageEvent)> {
60    Closure::new(move |event: MessageEvent| {
61        let data = match event.data().dyn_into::<js_sys::ArrayBuffer>() {
62            Ok(x) => x,
63            Err(e) => {
64                web_sys::console::error_1(&e);
65                return;
66            }
67        };
68        waterfall
69            .borrow_mut()
70            .put_waterfall_spectrum(&js_sys::Float32Array::new(&data));
71    })
72}
73
74impl WebSocketData {
75    fn connect(&self) -> Result<(), JsValue> {
76        let ws = WebSocket::new(&self.url)?;
77        ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
78        ws.set_onmessage(Some(self.onmessage.unchecked_ref()));
79        // by this point onclose shouldn't be None
80        ws.set_onclose(Some(
81            self.onclose.borrow().as_ref().unwrap().unchecked_ref(),
82        ));
83        Ok(())
84    }
85
86    fn setup_onclose(self: &Rc<Self>) {
87        let data = Rc::clone(self);
88        let closure = Closure::<dyn Fn(CloseEvent)>::new(move |_: CloseEvent| {
89            data.connect().unwrap();
90        });
91        *self.onclose.borrow_mut() = Some(closure.into_js_value());
92    }
93}