use crate::http_client::{Error, NoBody, Request, Result};
use crate::wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync};
use bytes::Bytes;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Frame {
Text(String),
Binary(Bytes),
Ping(Bytes),
Pong(Bytes),
Close(Option<CloseFrame>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CloseFrame {
pub code: u16,
pub reason: String,
}
#[derive(Clone, Debug, Default)]
pub struct ConnectOptions {
pub timeout: Option<Duration>,
}
impl ConnectOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
self.timeout = timeout;
self
}
}
pub trait WebSocketClientExt: Clone + WasmCompatSend + WasmCompatSync + 'static {
fn connect(
&self,
request: Request<NoBody>,
options: ConnectOptions,
) -> impl Future<Output = Result<BoxedWebSocketConnection>> + WasmCompatSend;
}
pub trait WebSocketConnection: WasmCompatSend + WasmCompatSync {
fn send(&mut self, frame: Frame) -> WasmBoxedFuture<'_, Result<()>>;
fn recv(&mut self) -> WasmBoxedFuture<'_, Result<Option<Frame>>>;
fn close(&mut self, frame: Option<CloseFrame>) -> WasmBoxedFuture<'_, Result<()>>;
}
pub type BoxedWebSocketConnection = Box<dyn WebSocketConnection>;
impl WebSocketConnection for BoxedWebSocketConnection {
fn send(&mut self, frame: Frame) -> WasmBoxedFuture<'_, Result<()>> {
(**self).send(frame)
}
fn recv(&mut self) -> WasmBoxedFuture<'_, Result<Option<Frame>>> {
(**self).recv()
}
fn close(&mut self, frame: Option<CloseFrame>) -> WasmBoxedFuture<'_, Result<()>> {
(**self).close(frame)
}
}
#[derive(Debug, thiserror::Error)]
#[error("invalid websocket base URL: {0}")]
pub struct InvalidWebSocketUrl(String);
pub fn websocket_url(base_url: &str, path: &str) -> Result<String> {
fn invalid(message: impl Into<String>) -> Error {
Error::instance(InvalidWebSocketUrl(message.into()))
}
let mut url =
url::Url::parse(base_url).map_err(|error| invalid(format!("{base_url}: {error}")))?;
let scheme = match url.scheme() {
"https" | "wss" => "wss",
"http" | "ws" => "ws",
other => {
return Err(invalid(format!(
"unsupported base URL scheme for websocket mode: {other}"
)));
}
};
url.set_scheme(scheme)
.map_err(|()| invalid(format!("failed to convert {base_url} to a websocket URL")))?;
let path = format!(
"{}/{}",
url.path().trim_end_matches('/'),
path.trim_matches('/')
);
url.set_path(&path);
Ok(url.to_string())
}
#[cfg(test)]
mod tests;