Skip to main content

workflow_websocket/client/
wasm.rs

1use super::{
2    ConnectOptions, ConnectResult, Handshake, Resolver, WebSocketConfig,
3    bindings::WebSocket as W3CWebSocket,
4    error::Error,
5    message::{Ack, Message},
6    result::Result,
7};
8use futures::{FutureExt, select, select_biased};
9use js_sys::{ArrayBuffer, Uint8Array};
10use std::ops::Deref;
11use std::sync::{
12    Arc, Mutex,
13    atomic::{AtomicBool, Ordering},
14};
15use wasm_bindgen::JsCast;
16use web_sys::{
17    CloseEvent as WsCloseEvent, ErrorEvent as WsErrorEvent, MessageEvent as WsMessageEvent,
18};
19use workflow_core::runtime::*;
20use workflow_core::{
21    channel::{Channel, DuplexChannel, Sender, oneshot, unbounded},
22    task::spawn,
23};
24use workflow_log::*;
25use workflow_wasm::callback::*;
26
27impl TryFrom<WsMessageEvent> for Message {
28    type Error = Error;
29
30    fn try_from(event: WsMessageEvent) -> std::result::Result<Self, Self::Error> {
31        match event.data() {
32            data if data.is_instance_of::<ArrayBuffer>() => {
33                let buffer = Uint8Array::new(data.unchecked_ref());
34                Ok(Message::Binary(buffer.to_vec()))
35            }
36            data if data.is_string() => match data.as_string() {
37                Some(text) => Ok(Message::Text(text)),
38                None => Err(Error::DataEncoding),
39            },
40            _ => Err(Error::DataType),
41        }
42    }
43}
44
45#[derive(Clone)]
46pub struct WebSocket(W3CWebSocket);
47unsafe impl Send for WebSocket {}
48unsafe impl Sync for WebSocket {}
49impl Deref for WebSocket {
50    type Target = W3CWebSocket;
51    fn deref(&self) -> &W3CWebSocket {
52        &self.0
53    }
54}
55
56impl WebSocket {
57    #[allow(dead_code)]
58    const CONNECTING: u16 = W3CWebSocket::CONNECTING;
59    #[allow(dead_code)]
60    const OPEN: u16 = W3CWebSocket::OPEN;
61    #[allow(dead_code)]
62    const CLOSING: u16 = W3CWebSocket::CLOSING;
63    #[allow(dead_code)]
64    const CLOSED: u16 = W3CWebSocket::CLOSED;
65
66    #[allow(dead_code)]
67    pub fn new(url: &str) -> Result<Self> {
68        Ok(WebSocket(W3CWebSocket::new(url)?))
69    }
70
71    pub fn new_with_config(url: &str, config: &WebSocketConfig) -> Result<Self> {
72        Ok(WebSocket(W3CWebSocket::new_with_config(url, config)?))
73    }
74
75    fn cleanup(&self) {
76        self.set_onopen(None);
77        self.set_onclose(None);
78        self.set_onerror(None);
79        self.set_onmessage(None);
80    }
81}
82
83impl From<W3CWebSocket> for WebSocket {
84    fn from(ws: W3CWebSocket) -> Self {
85        WebSocket(ws)
86    }
87}
88
89#[derive(Default)]
90struct Settings {
91    // default url WebSocket should connect to
92    default_url: Option<String>,
93    // URL WebSocket is currently connected to
94    current_url: Option<String>,
95}
96
97#[allow(dead_code)]
98struct Inner {
99    ws: WebSocket,
100    callbacks: CallbackMap,
101}
102
103unsafe impl Send for Inner {}
104unsafe impl Sync for Inner {}
105
106/// Platform-specific (WASM/browser) WebSocket interface backing the
107/// [`WebSocket`](super::WebSocket) client; owns the underlying socket,
108/// connection settings, channels and reconnection state.
109pub struct WebSocketInterface {
110    inner: Arc<Mutex<Option<Inner>>>,
111    settings: Arc<Mutex<Settings>>,
112    config: Mutex<WebSocketConfig>,
113    reconnect: AtomicBool,
114    is_connected: AtomicBool,
115    event_channel: Channel<Message>,
116    sender_channel: Channel<(Message, Ack)>,
117    receiver_channel: Channel<Message>,
118    dispatcher_shutdown: DuplexChannel,
119}
120
121impl WebSocketInterface {
122    /// Create a new interface for the optional default URL and configuration,
123    /// wired to the given sender and receiver channels.
124    pub fn new(
125        url: Option<&str>,
126        config: Option<WebSocketConfig>,
127        sender_channel: Channel<(Message, Ack)>,
128        receiver_channel: Channel<Message>,
129    ) -> Result<WebSocketInterface> {
130        sanity_checks()?;
131
132        let settings = Settings {
133            default_url: url.map(String::from),
134            ..Default::default()
135        };
136
137        let iface = WebSocketInterface {
138            inner: Arc::new(Mutex::new(None)),
139            settings: Arc::new(Mutex::new(settings)),
140            config: Mutex::new(config.unwrap_or_default()),
141            sender_channel,
142            receiver_channel,
143            event_channel: Channel::unbounded(),
144            reconnect: AtomicBool::new(true),
145            is_connected: AtomicBool::new(false),
146            dispatcher_shutdown: DuplexChannel::unbounded(),
147        };
148
149        Ok(iface)
150    }
151
152    /// Return the configured default URL used when no per-connect URL is given.
153    pub fn default_url(self: &Arc<Self>) -> Option<String> {
154        self.settings.lock().unwrap().default_url.clone()
155    }
156
157    /// Return the URL of the current (or most recent) connection.
158    pub fn current_url(self: &Arc<Self>) -> Option<String> {
159        self.settings.lock().unwrap().current_url.clone()
160    }
161
162    /// Set the default URL used for subsequent connection attempts.
163    pub fn set_default_url(self: &Arc<Self>, url: &str) {
164        self.settings
165            .lock()
166            .unwrap()
167            .default_url
168            .replace(url.to_string());
169    }
170
171    /// Record the URL of the connection currently being established.
172    pub fn set_current_url(self: &Arc<Self>, url: &str) {
173        self.settings
174            .lock()
175            .unwrap()
176            .current_url
177            .replace(url.to_string());
178    }
179
180    /// Return `true` if the socket is currently connected.
181    pub fn is_connected(self: &Arc<Self>) -> bool {
182        self.is_connected.load(Ordering::SeqCst)
183    }
184
185    fn resolver(&self) -> Option<Arc<dyn Resolver>> {
186        self.config.lock().unwrap().resolver.clone()
187    }
188
189    fn handshake(&self) -> Option<Arc<dyn Handshake>> {
190        self.config.lock().unwrap().handshake.clone()
191    }
192
193    /// Replace the configuration applied to the next connection attempt.
194    pub fn configure(&self, config: WebSocketConfig) {
195        *self.config.lock().unwrap() = config;
196    }
197
198    async fn resolve_url(self: &Arc<Self>, options: &ConnectOptions) -> Result<String> {
199        let url = if let Some(url) = options.url.as_ref().or(self.default_url().as_ref()) {
200            url.clone()
201        } else {
202            match self.resolver() {
203                Some(resolver) => resolver.resolve_url().await?,
204                _ => {
205                    return Err(Error::MissingUrl);
206                }
207            }
208        };
209        self.set_current_url(&url);
210        Ok(url)
211    }
212
213    /// Initiate the connection and start the background connect/reconnect task.
214    /// Depending on `options.block_async_connect`, either blocks until connected
215    /// or returns a receiver that fires once the connection is established.
216    pub async fn connect(self: &Arc<Self>, options: ConnectOptions) -> ConnectResult<Error> {
217        let (connect_trigger, connect_listener) = oneshot::<Result<()>>();
218
219        let connect_trigger = Arc::new(Mutex::new(Some(connect_trigger)));
220        self.connect_impl(options.clone(), connect_trigger).await?;
221
222        match options.block_async_connect {
223            true => match connect_listener.recv().await? {
224                Ok(_) => Ok(None),
225                Err(e) => Err(e),
226            },
227            false => Ok(Some(connect_listener)),
228        }
229    }
230
231    fn retry_connect_impl(
232        self: Arc<Self>,
233        options: ConnectOptions,
234        connect_trigger: Arc<Mutex<Option<Sender<Result<()>>>>>,
235    ) -> futures::future::BoxFuture<'static, Result<()>> {
236        Box::pin(async move { self.connect_impl(options, connect_trigger).await })
237            as futures::future::BoxFuture<'static, Result<()>>
238    }
239
240    async fn connect_impl(
241        self: &Arc<Self>,
242        options: ConnectOptions,
243        connect_trigger: Arc<Mutex<Option<Sender<Result<()>>>>>,
244    ) -> Result<()> {
245        if self.inner.lock().unwrap().is_some() {
246            log_warn!("WebSocket::connect() called while already initialized");
247
248            return Err(Error::AlreadyInitialized);
249        }
250
251        self.reconnect.store(true, Ordering::SeqCst);
252
253        let url = match self.resolve_url(&options).await {
254            Ok(url) => url,
255            Err(err) => {
256                log_trace!("WebSocket unable to resolve URL: {err}");
257                let self_ = self.clone();
258
259                if options.strategy.is_fallback() {
260                    self.reconnect.store(false, Ordering::SeqCst);
261
262                    // let connect_trigger = connect_trigger.lock().unwrap().take();
263                    // if let Some(connect_trigger) = connect_trigger {
264                    //     connect_trigger.send(Err(err)).await.ok();
265                    // }
266
267                    return Err(err);
268                }
269
270                let connect_trigger_ = connect_trigger.clone();
271                spawn(async move {
272                    // if reconnect is true, we sleep for reconnect interval and try to reconnect
273                    if self_.reconnect.load(Ordering::SeqCst) {
274                        workflow_core::task::sleep(
275                            options
276                                .retry_interval
277                                .unwrap_or(std::time::Duration::from_millis(1000)),
278                        )
279                        .await;
280                        // check again if reconnect may have been disabled during sleep
281                        if self_.reconnect.load(Ordering::SeqCst) {
282                            self_
283                                .retry_connect_impl(options, connect_trigger_)
284                                .await
285                                .ok();
286                        }
287                    }
288                });
289
290                return Ok(());
291            }
292        };
293
294        let mut inner = self.inner.lock().unwrap();
295
296        let ws = WebSocket::new_with_config(&url, &self.config.lock().unwrap())?;
297        ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
298
299        // - Message
300        let event_sender_ = self.event_channel.sender.clone();
301        let onmessage = callback!(move |event: WsMessageEvent| {
302            let msg: Message = event.try_into().expect("MessageEvent Error");
303            event_sender_.try_send(msg).unwrap_or_else(|err| {
304                log_trace!("WebSocket unable to try_send() `message` to event channel: `{err}`")
305            });
306        });
307        ws.set_onmessage(Some(onmessage.as_ref()));
308
309        // - Error
310        let onerror = callback!(move |_event: WsErrorEvent| {
311            // log_trace!("WS - error event: {:?}", _event);
312        });
313        ws.set_onerror(Some(onerror.as_ref()));
314
315        // - Open
316        let event_sender_ = self.event_channel.sender.clone();
317        let onopen = callback!(move || {
318            event_sender_.try_send(Message::Open).unwrap_or_else(|err| {
319                log_trace!("WebSocket unable to try_send() `open` to event channel: `{err}`")
320            });
321        });
322        ws.set_onopen(Some(onopen.as_ref()));
323
324        // - Close
325        let event_sender_ = self.event_channel.sender.clone();
326        let onclose = callback!(move |_event: WsCloseEvent| {
327            // log_trace!("WS - close event: {:?}", _event);
328            event_sender_
329                .try_send(Message::Close)
330                .unwrap_or_else(|err| {
331                    log_trace!("WebSocket unable to try_send() `close` to event channel: `{err}`")
332                });
333        });
334        ws.set_onclose(Some(onclose.as_ref()));
335
336        let callbacks = CallbackMap::new();
337        callbacks.retain(onmessage)?;
338        callbacks.retain(onerror)?;
339        callbacks.retain(onopen)?;
340        callbacks.retain(onclose)?;
341
342        *inner = Some(Inner {
343            ws: ws.clone(),
344            callbacks,
345        });
346
347        let self_ = self.clone();
348        spawn(async move {
349            self_
350                .dispatcher_task(&ws, options.clone(), connect_trigger.clone())
351                .await
352                .unwrap_or_else(|err| log_trace!("WebSocket error: {err}"));
353            // if reconnect is true, we sleep for reconnect interval and try to reconnect
354            if self_.reconnect.load(Ordering::SeqCst) {
355                workflow_core::task::sleep(
356                    options
357                        .retry_interval
358                        .unwrap_or(std::time::Duration::from_millis(1000)),
359                )
360                .await;
361                // check again if reconnect may have been disabled during sleep
362                if self_.reconnect.load(Ordering::SeqCst) {
363                    self_.reconnect(options, connect_trigger).await.ok();
364                }
365            }
366        });
367
368        Ok(())
369    }
370
371    fn ws(self: &Arc<Self>) -> Option<WebSocket> {
372        self.inner
373            .lock()
374            .expect("WebSocket:: inner lock failure")
375            .as_ref()
376            .map(|inner| inner.ws.clone())
377    }
378
379    /// Attempt to send a message directly over the underlying socket without
380    /// blocking, returning an error if the socket is not currently available.
381    #[allow(dead_code)]
382    pub fn try_send(self: &Arc<Self>, message: &Message) -> Result<()> {
383        match self.ws() {
384            Some(ws) => {
385                ws.try_send(message)?;
386                Ok(())
387            }
388            _ => Err(Error::NotConnected),
389        }
390    }
391
392    async fn handshake_impl(self: &Arc<Self>, ws: &WebSocket) -> Result<()> {
393        if let Some(handshake) = self.handshake() {
394            let (sender_tx, sender_rx) = unbounded();
395            let (receiver_tx, receiver_rx) = unbounded();
396            let (accept_tx, accept_rx) = oneshot();
397
398            spawn(async move {
399                accept_tx
400                    .send(handshake.handshake(&sender_tx, &receiver_rx).await)
401                    .await
402                    .unwrap_or_else(|err| {
403                        log_trace!("WebSocket handshake unable to send completion: `{}`", err)
404                    });
405            });
406
407            loop {
408                select_biased! {
409                    result = accept_rx.recv().fuse() => {
410                        return result?;
411                    },
412                    msg = sender_rx.recv().fuse() => {
413                        if let Ok(msg) = msg {
414                            ws.try_send(&msg)?;
415                        }
416                    },
417                    msg = self.event_channel.recv().fuse() => {
418                        if let Ok(msg) = msg {
419                            receiver_tx.send(msg).await?;
420                        }
421                    }
422                }
423            }
424        }
425
426        Ok(())
427    }
428
429    async fn dispatcher_task(
430        self: &Arc<Self>,
431        ws: &WebSocket,
432        options: ConnectOptions,
433        connect_trigger: Arc<Mutex<Option<Sender<Result<()>>>>>,
434    ) -> Result<()> {
435        'outer: loop {
436            select! {
437                _ = self.dispatcher_shutdown.request.receiver.recv().fuse() => {
438                    break 'outer;
439                },
440                msg = self.event_channel.recv().fuse() => {
441                    match msg {
442                        Ok(msg) => {
443                            match msg {
444                                Message::Binary(_) | Message::Text(_) => {
445                                    self.receiver_channel.sender.send(msg).await.unwrap();
446                                },
447                                Message::Open => {
448                                    // log_info!("WebSocket Message::Open");
449                                    // handle handshake failure
450                                    if let Err(err) = self.handshake_impl(ws).await {
451                                        log_info!("WebSocket handshake negotiation error: {err}");
452
453                                        if options.strategy.is_fallback() {
454                                            self.reconnect.store(false, Ordering::SeqCst);
455                                        }
456
457                                        let connect_trigger = connect_trigger.lock().unwrap().take();
458                                        if let Some(connect_trigger) = connect_trigger {
459                                            connect_trigger.send(Err(err)).await.ok();
460                                        }
461
462                                        return Err(Error::NegotiationFailure);
463                                    }
464
465                                    self.is_connected.store(true, Ordering::SeqCst);
466
467                                    let connect_trigger = connect_trigger.lock().unwrap().take();
468                                    if let Some(connect_trigger) = connect_trigger {
469                                        connect_trigger.send(Ok(())).await.ok();
470                                    }
471
472                                    self.receiver_channel.sender.send(msg).await.unwrap();
473                                },
474                                Message::Close => {
475                                    // log_info!("WebSocket Message::Close");
476
477                                    if let Some(inner) = self.inner.lock().unwrap().take() {
478                                        inner.ws.cleanup();
479                                    }
480
481                                    if self.is_connected.load(Ordering::SeqCst) {
482                                        self.is_connected.store(false, Ordering::SeqCst);
483                                        self.receiver_channel.sender.send(msg).await.unwrap();
484                                    } else if options.strategy.is_fallback() && options.block_async_connect {
485                                        // if we never connected and receiver Close while
486                                        // the strategy is Fallback, we disable reconnect
487                                        self.reconnect.store(false, Ordering::SeqCst);
488
489                                        let connect_trigger = connect_trigger.lock().unwrap().take();
490                                        if let Some(connect_trigger) = connect_trigger {
491                                            connect_trigger.send(Err(Error::Connect(self.current_url().unwrap()))).await.ok();
492                                        }
493                                    }
494
495                                    break 'outer;
496                                }
497                            }
498                        }
499                        Err(err) => {
500                            log_error!("WebSocket dispatcher channel error: {err}");
501                        }
502                    }
503                },
504                msg = self.sender_channel.receiver.recv().fuse() => {
505
506                    if let Ok((msg, ack)) = msg {
507
508                        // if ws.ready_state() != WebSocket::OPEN {
509                        //     return Err(Error::NotConnected);
510                        // }
511
512                        if let Some(ack) = ack {
513                            let result = ws
514                                .try_send(&msg)
515                                .map(Arc::new)
516                                .map_err(Arc::new);
517                            ack.send(result).await.unwrap_or_else(|err| {
518                                log_trace!("WebSocket error producing message ack {:?}", err)
519                            });
520                        } else {
521                            ws.try_send(&msg).unwrap_or_else(|err| {
522                                log_trace!("WebSocket unable to send `raw ws` message: `{err}`")
523                            });
524                        }
525                    }
526                }
527            }
528        }
529
530        Ok(())
531    }
532
533    async fn _shutdown(self: &Arc<Self>) -> Result<()> {
534        self.dispatcher_shutdown
535            .signal(())
536            .await
537            .map_err(|_| Error::DispatcherSignal)?;
538
539        Ok(())
540    }
541
542    /// Close the current connection, releasing its resources and emitting a
543    /// `Close` event if it was connected; reconnection (if enabled) may follow.
544    pub async fn close(self: &Arc<Self>) -> Result<()> {
545        if let Some(inner) = self.inner.lock().unwrap().take() {
546            inner.ws.cleanup();
547            inner.ws.close_if_open()?;
548        }
549
550        if self.is_connected.load(Ordering::SeqCst) {
551            self.event_channel.try_send(Message::Close)?;
552        }
553
554        Ok(())
555    }
556
557    async fn reconnect(
558        self: &Arc<Self>,
559        options: ConnectOptions,
560        connect_trigger: Arc<Mutex<Option<Sender<Result<()>>>>>,
561    ) -> Result<()> {
562        self.close().await?;
563
564        self.clone()
565            .retry_connect_impl(options, connect_trigger)
566            .await?;
567
568        Ok(())
569    }
570
571    /// Disable reconnection and close the connection, fully disconnecting.
572    pub async fn disconnect(self: &Arc<Self>) -> Result<()> {
573        self.reconnect.store(false, Ordering::SeqCst);
574        self.close().await.ok();
575        Ok(())
576    }
577
578    /// Force-close the underlying socket while connected to simulate a dropped
579    /// connection; intended for testing reconnection logic.
580    pub fn trigger_abort(self: &Arc<Self>) -> Result<()> {
581        if self.is_connected.load(Ordering::SeqCst)
582            && let Some(ws) = self.ws()
583        {
584            ws.close_if_open()?;
585        }
586        Ok(())
587    }
588}
589
590impl Drop for WebSocketInterface {
591    fn drop(&mut self) {}
592}
593
594trait TrySendMessage {
595    fn try_send(&self, message: &Message) -> Result<()>;
596}
597
598impl TrySendMessage for WebSocket {
599    fn try_send(&self, message: &Message) -> Result<()> {
600        match message {
601            Message::Binary(data) => {
602                if is_cross_origin_isolated() {
603                    // Create a non-shared ArrayBuffer for cross-origin isolated environments (Flutter).
604                    let array_buffer: ArrayBuffer = ArrayBuffer::new(data.len() as u32);
605                    let uint8_array = Uint8Array::new(&array_buffer);
606                    uint8_array.copy_from(&data[..]);
607                    self.send_with_array_buffer(&array_buffer)
608                        .map_err(|e| e.into())
609                } else {
610                    self.send_with_u8_array(data).map_err(|e| e.into())
611                }
612            }
613            Message::Text(text) => self.send_with_str(text).map_err(|e| e.into()),
614            _ => {
615                panic!("WebSocket trying to convert unsupported message type: `{message:?}`");
616            }
617        }
618    }
619}
620
621fn w3c_websocket_available() -> Result<bool> {
622    Ok(js_sys::Reflect::get(&js_sys::global(), &"WebSocket".into())
623        .map(|v| !v.is_falsy())
624        .unwrap_or(false))
625}
626
627fn sanity_checks() -> Result<()> {
628    if !w3c_websocket_available()? {
629        if is_node() {
630            log_info!("");
631            log_info!("+------------------------------------------------------------");
632            log_info!("|");
633            log_info!("| w3c websocket is not available");
634            log_info!("|");
635            log_info!("| Please include `WebSocket` module as a project dependency");
636            log_info!("| and add the following line to your Node.js script:");
637            log_info!("|");
638            log_info!("| `globalThis.WebSocket = require(\"websocket\").w3cwebsocket;`");
639            log_info!("|");
640            log_info!("| (or use any other w3c-compatible module)");
641            log_info!("|");
642            log_info!("+------------------------------------------------------------");
643            log_info!("");
644        } else {
645            log_info!("");
646            log_error!("w3c websocket is not available");
647            log_info!("");
648        }
649        panic!("w3c websocket is not available");
650    }
651    Ok(())
652}