Skip to main content

soothe_client/
errors.rs

1//! Client-facing error types.
2
3use thiserror::Error;
4
5/// Distinguishes clean vs unclean connection loss.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum DisconnectCause {
8    /// Abrupt loss: read/write error or missed pong.
9    Unclean = 0,
10    /// Graceful peer-initiated disconnect notification.
11    Clean = 1,
12}
13
14/// Human-readable cause name for logging.
15pub fn disconnect_cause_name(cause: DisconnectCause) -> &'static str {
16    match cause {
17        DisconnectCause::Clean => "clean",
18        DisconnectCause::Unclean => "unclean",
19    }
20}
21
22/// WebSocket connection failure.
23#[derive(Debug, Error)]
24#[error("connection error to {url} (attempt {attempt}): {source}")]
25pub struct ConnectionError {
26    /// Daemon WebSocket URL.
27    pub url: String,
28    /// Attempt number (1-based).
29    pub attempt: u32,
30    /// Underlying cause.
31    #[source]
32    pub source: Box<dyn std::error::Error + Send + Sync>,
33}
34
35impl ConnectionError {
36    /// Create a connection error.
37    pub fn new(
38        url: impl Into<String>,
39        attempt: u32,
40        source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
41    ) -> Self {
42        Self {
43            url: url.into(),
44            attempt,
45            source: source.into(),
46        }
47    }
48}
49
50/// Error reported by the daemon (protocol-1 structured error object).
51#[derive(Debug, Error, Clone)]
52#[error("daemon error [{code}]: {message}")]
53pub struct DaemonError {
54    /// Numeric error code.
55    pub code: i64,
56    /// Human-readable message.
57    pub message: String,
58    /// Optional structured data.
59    pub data: Option<serde_json::Value>,
60}
61
62impl DaemonError {
63    /// Create a daemon error.
64    pub fn new(code: i64, message: impl Into<String>) -> Self {
65        Self {
66            code,
67            message: message.into(),
68            data: None,
69        }
70    }
71
72    /// Attach optional data.
73    pub fn with_data(mut self, data: serde_json::Value) -> Self {
74        self.data = Some(data);
75        self
76    }
77}
78
79/// Timeout waiting for a daemon response.
80#[derive(Debug, Error)]
81#[error("timeout after {duration} waiting for {operation}")]
82pub struct TimeoutError {
83    /// Operation name.
84    pub operation: String,
85    /// Duration string.
86    pub duration: String,
87}
88
89impl TimeoutError {
90    /// Create a timeout error.
91    pub fn new(operation: impl Into<String>, duration: impl Into<String>) -> Self {
92        Self {
93            operation: operation.into(),
94            duration: duration.into(),
95        }
96    }
97}
98
99/// Bounded reconnect attempts exhausted.
100#[derive(Debug, Error)]
101#[error("reconnect to {url} failed after {attempts} attempts")]
102pub struct ReconnectError {
103    /// Daemon URL.
104    pub url: String,
105    /// Attempts made.
106    pub attempts: u32,
107    /// Last cause.
108    #[source]
109    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
110}
111
112impl ReconnectError {
113    /// Create a reconnect error.
114    pub fn new(
115        url: impl Into<String>,
116        attempts: u32,
117        source: Option<Box<dyn std::error::Error + Send + Sync>>,
118    ) -> Self {
119        Self {
120            url: url.into(),
121            attempts,
122            source,
123        }
124    }
125}
126
127/// Loop accepted reattach but failed the `loop_get` liveness probe.
128#[derive(Debug, Error)]
129#[error("stale loop {loop_id}: reattach accepted but liveness probe failed")]
130pub struct StaleLoopError {
131    /// Loop id that failed the probe.
132    pub loop_id: String,
133    /// Underlying cause.
134    #[source]
135    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
136}
137
138impl StaleLoopError {
139    /// Create a stale-loop error.
140    pub fn new(
141        loop_id: impl Into<String>,
142        source: Option<Box<dyn std::error::Error + Send + Sync>>,
143    ) -> Self {
144        Self {
145            loop_id: loop_id.into(),
146            source,
147        }
148    }
149}
150
151/// Heartbeat tracking failure (daemon not alive within timeout).
152#[derive(Debug, Error)]
153#[error("{message} (state={state}, last_heartbeat={last_heartbeat:?})")]
154pub struct HeartbeatError {
155    /// Instant of last received heartbeat (None if no heartbeat ever received).
156    pub last_heartbeat: Option<std::time::Instant>,
157    /// Daemon state at the time of the failure.
158    pub state: String,
159    /// Human-readable error message.
160    pub message: String,
161}
162
163impl HeartbeatError {
164    /// Create a heartbeat error.
165    pub fn new(
166        last_heartbeat: Option<std::time::Instant>,
167        state: impl Into<String>,
168        message: impl Into<String>,
169    ) -> Self {
170        Self {
171            last_heartbeat,
172            state: state.into(),
173            message: message.into(),
174        }
175    }
176}
177
178/// Unified client error.
179#[derive(Debug, Error)]
180pub enum Error {
181    /// Connection failure.
182    #[error(transparent)]
183    Connection(#[from] ConnectionError),
184    /// Daemon structured error.
185    #[error(transparent)]
186    Daemon(#[from] DaemonError),
187    /// Timeout.
188    #[error(transparent)]
189    Timeout(#[from] TimeoutError),
190    /// Reconnect exhausted.
191    #[error(transparent)]
192    Reconnect(#[from] ReconnectError),
193    /// Stale loop after reattach.
194    #[error(transparent)]
195    StaleLoop(#[from] StaleLoopError),
196    /// Heartbeat failure (daemon not alive within timeout).
197    #[error(transparent)]
198    Heartbeat(#[from] HeartbeatError),
199    /// Protocol / codec / transport failure.
200    #[error("{0}")]
201    Protocol(String),
202    /// I/O failure.
203    #[error(transparent)]
204    Io(#[from] std::io::Error),
205    /// JSON failure.
206    #[error(transparent)]
207    Json(#[from] serde_json::Error),
208    /// Generic message.
209    #[error("{0}")]
210    Message(String),
211}
212
213impl Error {
214    /// Protocol error helper.
215    pub fn protocol(msg: impl Into<String>) -> Self {
216        Self::Protocol(msg.into())
217    }
218
219    /// Message helper.
220    pub fn msg(msg: impl Into<String>) -> Self {
221        Self::Message(msg.into())
222    }
223}
224
225/// Result alias for this crate.
226pub type Result<T> = std::result::Result<T, Error>;