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}