1use crate::http_client::{Error, NoBody, Request, Result};
13use crate::wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync};
14use bytes::Bytes;
15use std::time::Duration;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum Frame {
20 Text(String),
22 Binary(Bytes),
24 Ping(Bytes),
26 Pong(Bytes),
28 Close(Option<CloseFrame>),
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct CloseFrame {
35 pub code: u16,
37 pub reason: String,
39}
40
41#[derive(Clone, Debug, Default)]
43pub struct ConnectOptions {
44 pub timeout: Option<Duration>,
47}
48
49impl ConnectOptions {
50 #[must_use]
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 #[must_use]
58 pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
59 self.timeout = timeout;
60 self
61 }
62}
63
64pub trait WebSocketClientExt: Clone + WasmCompatSend + WasmCompatSync + 'static {
68 fn connect(
71 &self,
72 request: Request<NoBody>,
73 options: ConnectOptions,
74 ) -> impl Future<Output = Result<BoxedWebSocketConnection>> + WasmCompatSend;
75}
76
77pub trait WebSocketConnection: WasmCompatSend + WasmCompatSync {
81 fn send(&mut self, frame: Frame) -> WasmBoxedFuture<'_, Result<()>>;
83
84 fn recv(&mut self) -> WasmBoxedFuture<'_, Result<Option<Frame>>>;
86
87 fn close(&mut self, frame: Option<CloseFrame>) -> WasmBoxedFuture<'_, Result<()>>;
90}
91
92pub type BoxedWebSocketConnection = Box<dyn WebSocketConnection>;
94
95impl WebSocketConnection for BoxedWebSocketConnection {
96 fn send(&mut self, frame: Frame) -> WasmBoxedFuture<'_, Result<()>> {
97 (**self).send(frame)
98 }
99
100 fn recv(&mut self) -> WasmBoxedFuture<'_, Result<Option<Frame>>> {
101 (**self).recv()
102 }
103
104 fn close(&mut self, frame: Option<CloseFrame>) -> WasmBoxedFuture<'_, Result<()>> {
105 (**self).close(frame)
106 }
107}
108
109#[derive(Debug, thiserror::Error)]
111#[error("invalid websocket base URL: {0}")]
112pub struct InvalidWebSocketUrl(String);
113
114pub fn websocket_url(base_url: &str, path: &str) -> Result<String> {
118 fn invalid(message: impl Into<String>) -> Error {
119 Error::instance(InvalidWebSocketUrl(message.into()))
120 }
121
122 let mut url =
123 url::Url::parse(base_url).map_err(|error| invalid(format!("{base_url}: {error}")))?;
124
125 let scheme = match url.scheme() {
126 "https" | "wss" => "wss",
127 "http" | "ws" => "ws",
128 other => {
129 return Err(invalid(format!(
130 "unsupported base URL scheme for websocket mode: {other}"
131 )));
132 }
133 };
134 url.set_scheme(scheme)
135 .map_err(|()| invalid(format!("failed to convert {base_url} to a websocket URL")))?;
136
137 let path = format!(
138 "{}/{}",
139 url.path().trim_end_matches('/'),
140 path.trim_matches('/')
141 );
142 url.set_path(&path);
143 Ok(url.to_string())
144}
145
146#[cfg(test)]
147mod tests;