Skip to main content

workflow_websocket/client/
options.rs

1use super::error::Error;
2use super::result::Result;
3use cfg_if::cfg_if;
4use std::fmt::Display;
5use std::str::FromStr;
6use wasm_bindgen::convert::TryFromJsValue;
7use wasm_bindgen::prelude::*;
8use workflow_core::time::Duration;
9
10/// `ConnectionStrategy` specifies how the WebSocket `async fn connect()`
11/// function should behave during the first-time connectivity phase.
12/// @category WebSocket
13#[wasm_bindgen]
14#[derive(Default, Clone, Copy, Debug, Eq, PartialEq)]
15pub enum ConnectStrategy {
16    /// Continuously attempt to connect to the server. This behavior will
17    /// block `connect()` function until the connection is established.
18    #[default]
19    Retry,
20    /// Causes `connect()` to return immediately if the first-time connection
21    /// has failed.
22    Fallback,
23}
24
25impl FromStr for ConnectStrategy {
26    type Err = Error;
27    fn from_str(s: &str) -> Result<Self> {
28        match s {
29            "retry" => Ok(ConnectStrategy::Retry),
30            "fallback" => Ok(ConnectStrategy::Fallback),
31            _ => Err(Error::InvalidConnectStrategyArg(s.to_string())),
32        }
33    }
34}
35
36impl ConnectStrategy {
37    /// Creates a strategy from a boolean: `true` yields [`ConnectStrategy::Retry`],
38    /// `false` yields [`ConnectStrategy::Fallback`].
39    pub fn new(retry: bool) -> Self {
40        if retry {
41            ConnectStrategy::Retry
42        } else {
43            ConnectStrategy::Fallback
44        }
45    }
46
47    /// Returns `true` if this strategy is [`ConnectStrategy::Fallback`].
48    pub fn is_fallback(&self) -> bool {
49        matches!(self, ConnectStrategy::Fallback)
50    }
51}
52
53impl TryFrom<JsValue> for ConnectStrategy {
54    type Error = Error;
55    fn try_from(value: JsValue) -> Result<Self> {
56        if value.is_undefined() || value.is_null() {
57            Ok(ConnectStrategy::default())
58        } else if let Some(string) = value.as_string() {
59            Ok(string.parse()?)
60        } else {
61            Ok(ConnectStrategy::try_from_js_value(value)?)
62        }
63    }
64}
65
66///
67/// `ConnectOptions` is used to configure the `WebSocket` connectivity behavior.
68///
69/// @category WebSocket
70#[derive(Clone, Debug)]
71pub struct ConnectOptions {
72    /// Indicates if the `async fn connect()` method should return immediately
73    /// or block until the connection is established.
74    pub block_async_connect: bool,
75    /// [`ConnectStrategy`] used to configure the retry or fallback behavior.
76    pub strategy: ConnectStrategy,
77    /// Optional `url` that will change the current URL of the WebSocket.
78    /// Note that the URL overrides the use of resolver.
79    pub url: Option<String>,
80    /// Optional `timeout` that will change the timeout of the WebSocket connection process.
81    /// `Timeout` is the period after which the async connection attempt is aborted. `Timeout`
82    /// is followed by the retry delay if the `ConnectionStrategy` is set to `Retry`.
83    pub connect_timeout: Option<Duration>,
84    /// Retry interval denotes the time to wait before attempting to reconnect.
85    pub retry_interval: Option<Duration>,
86}
87
88/// Default connection timeout in milliseconds used when none is specified.
89pub const DEFAULT_CONNECT_TIMEOUT_MILLIS: u64 = 5_000;
90/// Default retry interval in milliseconds used when none is specified.
91pub const DEFAULT_CONNECT_RETRY_MILLIS: u64 = 5_000;
92
93impl Default for ConnectOptions {
94    fn default() -> Self {
95        Self {
96            block_async_connect: true,
97            strategy: ConnectStrategy::Retry,
98            url: None,
99            connect_timeout: None,
100            retry_interval: None,
101        }
102    }
103}
104
105impl ConnectOptions {
106    /// Options that block `connect()` until the first connection attempt
107    /// completes, returning on failure using [`ConnectStrategy::Fallback`].
108    pub fn blocking_fallback() -> Self {
109        Self {
110            block_async_connect: true,
111            strategy: ConnectStrategy::Fallback,
112            url: None,
113            connect_timeout: None,
114            retry_interval: None,
115        }
116    }
117    /// Options that block `connect()` until the connection is established,
118    /// continuously retrying using [`ConnectStrategy::Retry`].
119    pub fn blocking_retry() -> Self {
120        Self {
121            block_async_connect: true,
122            strategy: ConnectStrategy::Retry,
123            url: None,
124            connect_timeout: None,
125            retry_interval: None,
126        }
127    }
128
129    /// Options that return from `connect()` immediately while retrying
130    /// the connection in the background using [`ConnectStrategy::Retry`].
131    pub fn non_blocking_retry() -> Self {
132        Self {
133            block_async_connect: false,
134            strategy: ConnectStrategy::Retry,
135            url: None,
136            connect_timeout: None,
137            retry_interval: None,
138        }
139    }
140
141    /// Sets a custom URL that overrides the current WebSocket URL (and the resolver).
142    pub fn with_url<S: Display>(self, url: S) -> Self {
143        Self {
144            url: Some(url.to_string()),
145            ..self
146        }
147    }
148
149    /// Sets the connection timeout after which a connection attempt is aborted.
150    pub fn with_connect_timeout(self, timeout: Duration) -> Self {
151        Self {
152            connect_timeout: Some(timeout),
153            ..self
154        }
155    }
156
157    /// Sets the retry interval to wait before attempting to reconnect.
158    pub fn with_retry_interval(self, interval: Duration) -> Self {
159        Self {
160            retry_interval: Some(interval),
161            ..self
162        }
163    }
164
165    /// Returns the configured connection timeout, or the default
166    /// ([`DEFAULT_CONNECT_TIMEOUT_MILLIS`]) if none was set.
167    pub fn connect_timeout(&self) -> Duration {
168        self.connect_timeout
169            .unwrap_or(Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MILLIS))
170    }
171
172    /// Returns the configured retry interval, or the default
173    /// ([`DEFAULT_CONNECT_RETRY_MILLIS`]) if none was set.
174    pub fn retry_interval(&self) -> Duration {
175        self.retry_interval
176            .unwrap_or(Duration::from_millis(DEFAULT_CONNECT_RETRY_MILLIS))
177    }
178}
179
180cfg_if! {
181    if #[cfg(feature = "wasm32-sdk")] {
182        use js_sys::Object;
183        use wasm_bindgen::JsCast;
184        use workflow_wasm::extensions::object::*;
185
186        #[wasm_bindgen(typescript_custom_section)]
187        const TS_CONNECT_OPTIONS: &'static str = r#"
188
189        /**
190         * `ConnectOptions` is used to configure the `WebSocket` connectivity behavior.
191         * 
192         * @category WebSocket
193         */
194        export interface IConnectOptions {
195            /**
196             * Indicates if the `async fn connect()` method should return immediately
197             * or wait for connection to occur or fail before returning.
198             * (default is `true`)
199             */
200            blockAsyncConnect? : boolean,
201            /**
202             * ConnectStrategy used to configure the retry or fallback behavior.
203             * In retry mode, the WebSocket will continuously attempt to connect to the server.
204             * (default is {link ConnectStrategy.Retry}).
205             */
206            strategy?: ConnectStrategy | string,
207            /** 
208             * A custom URL that will change the current URL of the WebSocket.
209             * If supplied, the URL will override the use of resolver.
210             */
211            url?: string,
212            /**
213             * A custom connection timeout in milliseconds.
214             */
215            timeoutDuration?: number,
216            /** 
217             * A custom retry interval in milliseconds.
218             */
219            retryInterval?: number,
220        }
221        "#;
222
223        #[wasm_bindgen]
224        extern "C" {
225            #[wasm_bindgen(typescript_type = "IConnectOptions | undefined")]
226            pub type IConnectOptions;
227        }
228
229        impl TryFrom<IConnectOptions> for ConnectOptions {
230            type Error = Error;
231            fn try_from(args: IConnectOptions) -> Result<Self> {
232                Self::try_from(&args)
233            }
234        }
235
236        impl TryFrom<&IConnectOptions> for ConnectOptions {
237            type Error = Error;
238            fn try_from(args: &IConnectOptions) -> Result<Self> {
239                let options = if let Some(args) = args.dyn_ref::<Object>() {
240                    let url = args.get_value("url")?.as_string();
241                    let block_async_connect = args
242                        .get_value("blockAsyncConnect")?
243                        .as_bool()
244                        .unwrap_or(true);
245                    let strategy = ConnectStrategy::try_from(args.get_value("strategy")?)?;
246                    let timeout = args
247                        .get_value("timeoutDuration")?
248                        .as_f64()
249                        .map(|f| Duration::from_millis(f as u64));
250                    let retry_interval = args
251                        .get_value("retryInterval")?
252                        .as_f64()
253                        .map(|f| Duration::from_millis(f as u64));
254
255                    ConnectOptions {
256                        block_async_connect,
257                        strategy,
258                        url,
259                        connect_timeout: timeout,
260                        retry_interval,
261                        ..Default::default()
262                    }
263                } else if let Some(retry) = args.as_bool() {
264                    ConnectOptions {
265                        block_async_connect: true,
266                        strategy: ConnectStrategy::new(retry),
267                        url: None,
268                        connect_timeout: None,
269                        retry_interval: None,
270                        ..Default::default()
271                    }
272                } else {
273                    ConnectOptions::default()
274                };
275
276                Ok(options)
277            }
278        }
279    }
280}