Skip to main content

workflow_websocket/client/
mod.rs

1//!
2//! async WebSocket client functionality (requires a browser (WASM) or tokio (native) executors)
3//!
4//! # TLS crypto provider (native)
5//!
6//! Native secure (`wss://`) connections use `tungstenite` with
7//! [`rustls`](https://docs.rs/rustls). Since rustls 0.23, a process-level crypto
8//! provider must be installed before the first secure connection — typically by
9//! the application or a higher-level SDK at startup:
10//!
11//! ```ignore
12//! rustls::crypto::ring::default_provider().install_default().unwrap();
13//! ```
14//!
15//! If exactly one rustls provider is compiled into the binary, it is selected
16//! automatically and no explicit install is needed; an install is only required
17//! when none — or more than one — provider is present. With more than one and no
18//! installed default, opening a `wss://` connection fails with *"Could not
19//! automatically determine the process-level CryptoProvider"*. In the
20//! browser/WASM environment TLS is handled by the host `WebSocket`, so no
21//! provider is required.
22//!
23
24use cfg_if::cfg_if;
25
26mod wasm;
27pub use wasm::WebSocketInterface as _WSI;
28
29cfg_if! {
30    if #[cfg(target_arch = "wasm32")] {
31        use wasm::WebSocketInterface;
32    } else {
33        mod native;
34        use native::WebSocketInterface;
35    }
36}
37
38/// Low-level bindings to the underlying browser/Node.js WebSocket interface.
39pub mod bindings;
40pub mod config;
41/// WebSocket client error type and its conversions.
42pub mod error;
43/// WebSocket client message types exchanged with the server.
44pub mod message;
45/// Connection options controlling connect and reconnect behavior.
46pub mod options;
47/// Result type alias used throughout the WebSocket client.
48pub mod result;
49
50pub use config::WebSocketConfig;
51pub use error::Error;
52use futures::Future;
53pub use message::*;
54pub use options::{ConnectOptions, ConnectStrategy};
55pub use result::Result;
56
57use async_trait::async_trait;
58use std::pin::Pin;
59use std::sync::Arc;
60use workflow_core::channel::{Channel, Receiver, Sender, oneshot};
61/// Result of a connect attempt. On success yields `Some(receiver)` when the
62/// caller did not block on the connection (the receiver fires once connected),
63/// or `None` when the connect call blocked until the connection was established.
64pub type ConnectResult<E> = std::result::Result<Option<Receiver<Result<()>>>, E>;
65
66/// Shared closure invoked to perform a custom handshake negotiation using the
67/// supplied send/receive channels before the connection is considered ready.
68pub type HandshakeFn = Arc<
69    Box<dyn Send + Sync + Fn(&Sender<Message>, &Receiver<Message>) -> HandshakeFnReturn + 'static>,
70>;
71/// The boxed future returned by a [`HandshakeFn`], resolving once the
72/// handshake completes (or fails).
73pub type HandshakeFnReturn = Pin<Box<dyn Send + Sync + 'static + Future<Output = Result<()>>>>;
74
75/// Trait implemented by custom handshake handlers that negotiate with the
76/// server immediately after the socket opens and before it is marked connected.
77#[async_trait]
78pub trait Handshake: Send + Sync + 'static {
79    /// Perform the handshake using the given send and receive channels,
80    /// returning once negotiation has succeeded or failed.
81    async fn handshake(&self, sender: &Sender<Message>, receiver: &Receiver<Message>)
82    -> Result<()>;
83}
84
85/// Trait implemented by URL resolvers that supply the destination URL
86/// dynamically when no explicit URL has been configured.
87#[async_trait]
88pub trait Resolver: Send + Sync + 'static {
89    /// Resolve and return the WebSocket URL to connect to.
90    async fn resolve_url(&self) -> ResolverResult;
91}
92/// Result of a [`Resolver::resolve_url`] call, yielding the destination URL.
93pub type ResolverResult = Result<String>;
94/// Alias for the WebSocket client [`Error`] type.
95pub type WebSocketError = Error;
96
97struct Inner {
98    client: Arc<WebSocketInterface>,
99    sender_channel: Channel<(Message, Ack)>,
100    receiver_channel: Channel<Message>,
101}
102
103impl Inner {
104    pub fn new(
105        client: Arc<WebSocketInterface>,
106        sender_channel: Channel<(Message, Ack)>,
107        receiver_channel: Channel<Message>,
108    ) -> Self {
109        Self {
110            client,
111            sender_channel,
112            receiver_channel,
113        }
114    }
115}
116
117/// An async WebSocket implementation capable of operating
118/// uniformly under a browser-backed executor in WASM and under
119/// native tokio-runtime.
120#[derive(Clone)]
121pub struct WebSocket {
122    inner: Arc<Inner>,
123}
124
125impl WebSocket {
126    /// Create a new WebSocket instance connecting to the given URL.
127    pub fn new(url: Option<&str>, config: Option<WebSocketConfig>) -> Result<WebSocket> {
128        if let Some(url) = url
129            && !url.starts_with("ws://")
130            && !url.starts_with("wss://")
131        {
132            return Err(Error::AddressSchema(url.to_string()));
133        }
134
135        let config = config.unwrap_or_default();
136
137        let receiver_channel = if let Some(cap) = config.receiver_channel_cap {
138            Channel::bounded(cap)
139        } else {
140            Channel::<Message>::unbounded()
141        };
142
143        let sender_channel = if let Some(cap) = config.sender_channel_cap {
144            Channel::bounded(cap)
145        } else {
146            Channel::<(Message, Ack)>::unbounded()
147        };
148
149        let client = Arc::new(WebSocketInterface::new(
150            url,
151            Some(config),
152            sender_channel.clone(),
153            receiver_channel.clone(),
154        )?);
155
156        let websocket = WebSocket {
157            inner: Arc::new(Inner::new(client, sender_channel, receiver_channel)),
158        };
159
160        Ok(websocket)
161    }
162
163    /// Get current websocket connection URL
164    pub fn url(&self) -> Option<String> {
165        self.inner.client.current_url()
166    }
167
168    /// Changes WebSocket connection URL.
169    /// Following this call, you must invoke
170    /// `WebSocket::reconnect().await` manually
171    pub fn set_url(&self, url: &str) {
172        self.inner.client.set_default_url(url);
173    }
174
175    /// Configure WebSocket connection settings
176    /// Can be supplied after the WebSocket has been
177    /// has been created to alter the configuration
178    /// for the next connection.
179    pub fn configure(&self, config: WebSocketConfig) {
180        self.inner.client.configure(config);
181    }
182
183    /// Returns the reference to the Sender channel
184    pub fn sender_tx(&self) -> &Sender<(Message, Ack)> {
185        &self.inner.sender_channel.sender
186    }
187
188    /// Returns the reference to the Receiver channel
189    pub fn receiver_rx(&self) -> &Receiver<Message> {
190        &self.inner.receiver_channel.receiver
191    }
192
193    /// Returns true if websocket is connected, false otherwise
194    pub fn is_connected(&self) -> bool {
195        self.inner.client.is_connected()
196    }
197
198    /// Connects the websocket to the destination URL.
199    /// Optionally accepts `block_until_connected` argument
200    /// that will block the async execution until the websocket
201    /// is connected.
202    ///
203    /// Once invoked, connection task will run in the background
204    /// and will attempt to repeatedly reconnect if the websocket
205    /// connection is closed.
206    ///
207    /// To suspend reconnection, you have to call `disconnect()`
208    /// method explicitly.
209    ///
210    pub async fn connect(&self, options: ConnectOptions) -> ConnectResult<Error> {
211        self.inner.client.connect(options).await
212    }
213
214    /// Disconnects the websocket from the destination server.
215    pub async fn disconnect(&self) -> Result<()> {
216        self.inner.client.disconnect().await
217    }
218
219    /// Trigger WebSocket to reconnect.  This method
220    /// closes the underlying WebSocket connection
221    /// causing the WebSocket implementation to
222    /// re-initiate connection.
223    pub async fn reconnect(&self) -> Result<()> {
224        self.inner.client.close().await
225    }
226
227    /// Sends a message to the destination server. This function
228    /// will queue the message on the relay channel and return
229    /// successfully if the message has been queued.
230    /// This function enforces async yield in order to prevent
231    /// potential blockage of the executor if it is being executed
232    /// in tight loops.
233    pub async fn post(&self, message: Message) -> Result<&Self> {
234        if !self.inner.client.is_connected() {
235            return Err(Error::NotConnected);
236        }
237
238        let result = Ok(self
239            .inner
240            .sender_channel
241            .sender
242            .send((message, None))
243            .await?);
244        workflow_core::task::yield_now().await;
245        result.map(|_| self)
246    }
247
248    /// Sends a message to the destination server. This function
249    /// will block until until the message was relayed to the
250    /// underlying websocket implementation.
251    pub async fn send(&self, message: Message) -> std::result::Result<&Self, Arc<Error>> {
252        if !self.inner.client.is_connected() {
253            return Err(Arc::new(Error::NotConnected));
254        }
255
256        let (ack_sender, ack_receiver) = oneshot();
257        self.inner
258            .sender_channel
259            .send((message, Some(ack_sender)))
260            .await
261            .map_err(|err| Arc::new(err.into()))?;
262
263        ack_receiver
264            .recv()
265            .await
266            .map_err(|_| Arc::new(Error::DispatchChannelAck))?
267            .map(|_| self)
268    }
269
270    /// Receives message from the websocket. Blocks until a message is
271    /// received from the underlying websocket connection.
272    pub async fn recv(&self) -> Result<Message> {
273        Ok(self.inner.receiver_channel.receiver.recv().await?)
274    }
275
276    /// Triggers a disconnection on the underlying WebSocket.
277    /// This is intended for debug purposes only.
278    /// Can be used to test application reconnection logic.
279    pub fn trigger_abort(&self) -> Result<()> {
280        self.inner.client.trigger_abort()
281    }
282}