use std::time::Duration;
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
#[error("Connection failed: {0}")]
Connect(#[from] ConnectError),
#[error("Handshake failed: {0}")]
Handshake(#[from] HandshakeError),
#[error("Send failed: {0}")]
Send(#[from] SendError),
#[error("Receive failed: {0}")]
Receive(#[from] ReceiveError),
#[error("Supervisor error: {0}")]
Supervisor(#[from] SupervisorError),
#[error("Extension error: {0}")]
Extension(#[from] ExtensionError),
#[error("Invalid configuration: {0}")]
Config(String),
#[error("Client is already running")]
AlreadyRunning,
#[error("Graceful shutdown timed out after {0:?}")]
ShutdownTimeout(Duration),
#[error("{context}: {source}")]
Context {
context: String,
#[source]
source: Box<ClientError>,
},
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum ConnectError {
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("Invalid URI: {0}")]
InvalidUri(String),
#[error("DNS resolution failed: {0}")]
DnsResolution(String),
#[error("TCP connection failed: {0}")]
TcpConnect(String),
#[error("TCP failed: {0}")]
TcpFailed(String),
#[error("TLS handshake failed: {0}")]
TlsHandshake(String),
#[error("TLS configuration error: {0}")]
Tls(String),
#[error("WebSocket upgrade failed: {0}")]
WebSocketUpgrade(String),
#[error("WebSocket connection failed: {0}")]
WebSocketFailed(String),
#[error("Handshake failed: {0}")]
HandshakeFailed(String),
#[error("Connection timeout after {0:?}")]
Timeout(Duration),
#[error("Connection refused")]
Refused,
#[error("IO error: {0}")]
Io(String),
}
impl ConnectError {
#[must_use]
pub const fn is_retryable(&self) -> bool {
match self {
Self::InvalidUrl(_) | Self::InvalidUri(_) | Self::TlsHandshake(_) | Self::Tls(_) => {
false
}
Self::DnsResolution(_)
| Self::TcpConnect(_)
| Self::TcpFailed(_)
| Self::WebSocketUpgrade(_)
| Self::WebSocketFailed(_)
| Self::HandshakeFailed(_)
| Self::Timeout(_)
| Self::Refused
| Self::Io(_) => true,
}
}
#[must_use]
pub const fn suggested_delay(&self) -> Option<Duration> {
match self {
Self::DnsResolution(_) => Some(Duration::from_millis(500)),
Self::Timeout(_) => Some(Duration::from_secs(5)),
Self::Refused => Some(Duration::from_secs(2)),
_ => None, }
}
}
impl From<std::io::Error> for ConnectError {
fn from(e: std::io::Error) -> Self {
Self::Io(e.to_string())
}
}
impl From<tungstenite::Error> for ConnectError {
fn from(e: tungstenite::Error) -> Self {
Self::WebSocketUpgrade(e.to_string())
}
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum HandshakeError {
#[error("Handshake failed: {0}")]
Failed(String),
#[error("Authentication failed: {0}")]
AuthFailed(String),
#[error("Handshake timeout after {0:?}")]
Timeout(Duration),
#[error("Protocol error: {0}")]
Protocol(String),
#[error("WebSocket error: {0}")]
WebSocket(String),
}
impl HandshakeError {
#[must_use]
pub const fn is_retryable(&self) -> bool {
match self {
Self::AuthFailed(_) | Self::Protocol(_) => false,
Self::Failed(_) | Self::Timeout(_) | Self::WebSocket(_) => true,
}
}
}
impl From<tungstenite::Error> for HandshakeError {
fn from(e: tungstenite::Error) -> Self {
Self::WebSocket(e.to_string())
}
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum SendError {
#[error("Not connected")]
NotConnected,
#[error("Channel closed")]
ChannelClosed,
#[error("Send buffer is full")]
ChannelFull,
#[error("Send timed out after {0:?}")]
Timeout(Duration),
#[error("Message too large: {size} bytes (max: {max})")]
MessageTooLarge { size: usize, max: usize },
#[error("WebSocket error: {0}")]
WebSocket(String),
}
impl From<tungstenite::Error> for SendError {
fn from(e: tungstenite::Error) -> Self {
Self::WebSocket(e.to_string())
}
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum ReceiveError {
#[error("Stream closed")]
StreamClosed,
#[error("Receive timeout after {0:?}")]
Timeout(Duration),
#[error("WebSocket error: {0}")]
WebSocket(String),
}
impl From<tungstenite::Error> for ReceiveError {
fn from(e: tungstenite::Error) -> Self {
Self::WebSocket(e.to_string())
}
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum SupervisorError {
#[error("Max retries exceeded after {attempts} attempts")]
MaxRetriesExceeded { attempts: u32 },
#[error("Shutdown requested")]
Shutdown,
#[error("Fatal error: {0}")]
Fatal(String),
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum ExtensionError {
#[error("Extension '{name}' failed: {message}")]
Failed { name: String, message: String },
#[error("Extension '{name}' initialization failed: {message}")]
InitFailed { name: String, message: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisconnectReason {
Normal,
Error(String),
Timeout,
Shutdown,
ServerClosed {
code: Option<u16>,
reason: Option<String>,
},
}
impl std::fmt::Display for DisconnectReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Normal => write!(f, "normal closure"),
Self::Error(e) => write!(f, "error: {e}"),
Self::Timeout => write!(f, "timeout"),
Self::Shutdown => write!(f, "shutdown"),
Self::ServerClosed { code, reason } => {
write!(f, "server closed")?;
if let Some(c) = code {
write!(f, " (code: {c})")?;
}
if let Some(r) = reason {
write!(f, ": {r}")?;
}
Ok(())
}
}
}
}
pub type ClientResult<T> = Result<T, ClientError>;
pub trait ErrorContext<T> {
fn with_context(self, context: impl Into<String>) -> Result<T, ClientError>;
}
impl<T, E: Into<ClientError>> ErrorContext<T> for Result<T, E> {
fn with_context(self, context: impl Into<String>) -> Result<T, ClientError> {
self.map_err(|e| ClientError::Context {
context: context.into(),
source: Box::new(e.into()),
})
}
}