#![allow(clippy::module_name_repetitions)]
use crate::app::Orders;
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
use crate::browser::json;
use gloo_file::FileReadError;
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
use serde::Serialize;
use wasm_bindgen::{JsCast, JsValue};
mod builder;
mod message;
pub use builder::Builder;
use builder::Callbacks;
pub use message::WebSocketMessage;
pub type Result<T> = std::result::Result<T, WebSocketError>;
pub type BinaryType = web_sys::BinaryType;
pub type Blob = gloo_file::Blob;
pub type State = web_sys::TcpReadyState;
pub type CloseEvent = web_sys::CloseEvent;
#[allow(clippy::module_name_repetitions)]
#[derive(Debug)]
pub enum WebSocketError {
TextError(&'static str),
SendError(JsValue),
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
JsonError(json::Error),
PromiseError(JsValue),
FileReaderError(FileReadError),
OpenError(JsValue),
CloseError(JsValue),
}
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
impl From<json::Error> for WebSocketError {
fn from(v: json::Error) -> Self {
Self::JsonError(v)
}
}
#[derive(Debug)]
#[must_use = "WebSocket is closed on drop"]
pub struct WebSocket {
ws: web_sys::WebSocket,
#[allow(dead_code)]
callbacks: Callbacks,
}
impl WebSocket {
pub fn builder<U: AsRef<str>, Ms: 'static, O: Orders<Ms>>(
url: U,
orders: &O,
) -> Builder<U, Ms, O> {
Builder::new(url, orders)
}
pub fn send_text<S>(&self, message: S) -> Result<()>
where
S: AsRef<str>,
{
self.ws
.send_with_str(message.as_ref())
.map_err(WebSocketError::SendError)
}
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
pub fn send_json<T: Serialize + ?Sized>(&self, data: &T) -> Result<()> {
let data: String = json::to_string(data)?;
self.send_text(data)
}
pub fn send_bytes(&self, message: &[u8]) -> Result<()> {
self.ws
.send_with_u8_array(message)
.map_err(WebSocketError::SendError)
}
pub fn buffered_amount(&self) -> u32 {
self.ws.buffered_amount()
}
pub fn protocol(&self) -> String {
self.ws.protocol()
}
pub fn extensions(&self) -> String {
self.ws.extensions()
}
pub fn close(&self, code: Option<u16>, reason: Option<&str>) -> Result<()> {
self.ws
.close_with_code_and_reason(code.unwrap_or(1000), reason.unwrap_or_default())
.map_err(WebSocketError::CloseError)
}
#[allow(clippy::missing_panics_doc)]
pub fn state(&self) -> State {
match self.ws.ready_state() {
0 => State::Connecting,
1 => State::Open,
2 => State::Closing,
3 => State::Closed,
state_id => panic!("unknown WebSocket State id: {}", state_id),
}
}
pub const fn raw_web_socket(&self) -> &web_sys::WebSocket {
&self.ws
}
fn new(
url: &str,
callbacks: Callbacks,
protocols: &[&str],
binary_type: Option<BinaryType>,
) -> Result<Self> {
let ws = {
if protocols.is_empty() {
web_sys::WebSocket::new(url).map_err(WebSocketError::OpenError)?
} else {
let protocol_array = protocols
.iter()
.map(|protocol| JsValue::from(*protocol))
.collect::<js_sys::Array>();
web_sys::WebSocket::new_with_str_sequence(url, &JsValue::from(&protocol_array))
.map_err(WebSocketError::OpenError)?
}
};
if let Some(binary_type) = binary_type {
ws.set_binary_type(binary_type);
}
if let Some(on_open) = &callbacks.on_open {
ws.set_onopen(Some(on_open.as_ref().unchecked_ref()));
}
if let Some(on_close) = &callbacks.on_close {
ws.set_onclose(Some(on_close.as_ref().unchecked_ref()));
}
if let Some(on_error) = &callbacks.on_error {
ws.set_onerror(Some(on_error.as_ref().unchecked_ref()));
}
if let Some(on_message) = &callbacks.on_message {
ws.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
}
Ok(Self { ws, callbacks })
}
}
impl Drop for WebSocket {
fn drop(&mut self) {
if matches!(self.state(), State::Connecting | State::Open) {
self.ws.close().expect("close WebSocket connection");
}
self.ws.set_onopen(None);
self.ws.set_onclose(None);
self.ws.set_onerror(None);
self.ws.set_onmessage(None);
}
}