1use std::time::Duration;
4
5use thiserror::Error;
6
7use super::peer_types::Address;
8use crate::{auth::Permission, entry::ID};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum TimeoutPhase {
17 Connect,
19 Request,
22}
23
24impl TimeoutPhase {
25 pub fn peer_reachable(&self) -> bool {
27 matches!(self, TimeoutPhase::Request)
28 }
29}
30
31impl std::fmt::Display for TimeoutPhase {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 TimeoutPhase::Connect => write!(f, "connecting"),
35 TimeoutPhase::Request => write!(f, "awaiting a response"),
36 }
37 }
38}
39
40#[derive(Debug, Error)]
42#[non_exhaustive]
43pub enum SyncError {
44 #[error("No transport enabled. Call enable_http_transport() first")]
46 NoTransportEnabled,
47
48 #[error("Sync is not enabled on this Instance. Call Instance::enable_sync() first")]
50 SyncNotEnabled,
51
52 #[error("Server already running on {address}")]
54 ServerAlreadyRunning { address: String },
55
56 #[error("Server not running")]
58 ServerNotRunning,
59
60 #[error("Unexpected response type: expected {expected}, got {actual}")]
62 UnexpectedResponse {
63 expected: &'static str,
64 actual: String,
65 },
66
67 #[error("Network error: {0}")]
69 Network(String),
70
71 #[error("Failed to send command to background sync: {0}")]
73 CommandSendError(String),
74
75 #[error("Failed to initialize transport: {0}")]
77 TransportInit(String),
78
79 #[error("Failed to create async runtime: {0}")]
81 RuntimeCreation(String),
82
83 #[error("Failed to bind server to {address}: {reason}")]
85 ServerBind { address: String, reason: String },
86
87 #[error("Failed to connect to {address}: {reason}")]
89 ConnectionFailed { address: String, reason: String },
90
91 #[error("Request to {address} timed out after {elapsed:?} while {phase}")]
99 Timeout {
100 address: String,
101 phase: TimeoutPhase,
102 elapsed: Duration,
103 },
104
105 #[error("Device key '{key_name}' not found in backend storage")]
107 DeviceKeyNotFound { key_name: String },
108
109 #[error("Transport type '{transport_type}' not supported")]
111 UnsupportedTransport { transport_type: String },
112
113 #[error("Invalid address: {0}")]
115 InvalidAddress(String),
116
117 #[error("Peer not found: {0}")]
119 PeerNotFound(String),
120
121 #[error("Peer already exists: {0}")]
123 PeerAlreadyExists(String),
124
125 #[error("Serialization error: {0}")]
127 SerializationError(String),
128
129 #[error("Protocol version mismatch: expected {expected}, received {received}")]
131 ProtocolMismatch { expected: u32, received: u32 },
132
133 #[error("Handshake failed: {0}")]
135 HandshakeFailed(String),
136
137 #[error("Entry not found: {0}")]
139 EntryNotFound(ID),
140
141 #[error("Invalid entry: {0}")]
143 InvalidEntry(String),
144
145 #[error("Sync protocol error: {0}")]
147 SyncProtocolError(String),
148
149 #[error("Backend error: {0}")]
151 BackendError(String),
152
153 #[error("Bootstrap request not found: {0}")]
155 RequestNotFound(String),
156
157 #[error("Bootstrap request already exists: {0}")]
159 RequestAlreadyExists(String),
160
161 #[error(
163 "Invalid request state for '{request_id}': expected {expected_status}, found {current_status}"
164 )]
165 InvalidRequestState {
166 request_id: String,
167 current_status: String,
168 expected_status: String,
169 },
170
171 #[error("Invalid data: {0}")]
173 InvalidData(String),
174
175 #[error(
177 "Insufficient permission for request '{request_id}': required {required_permission}, but key has {actual_permission:?}"
178 )]
179 InsufficientPermission {
180 request_id: String,
181 required_permission: String,
182 actual_permission: Permission,
183 },
184
185 #[error("Authentication required to read database '{0}'")]
187 AuthenticationRequired(String),
188
189 #[error("Authentication failed: {0}")]
191 AuthenticationFailed(String),
192
193 #[error("Permission denied: {0}")]
195 PermissionDenied(String),
196
197 #[error("Invalid public key: {reason}")]
199 InvalidPublicKey { reason: String },
200
201 #[error("Invalid key name: {reason}")]
203 InvalidKeyName { reason: String },
204
205 #[error("Instance has been dropped")]
207 InstanceDropped,
208
209 #[error("Bootstrap request pending approval (request_id: {request_id}): {message}")]
211 BootstrapPending { request_id: String, message: String },
212
213 #[error("Bootstrap request rejected (request_id: {request_id}): {message}")]
215 BootstrapRejected { request_id: String, message: String },
216
217 #[error("Transport config type mismatch for '{name}': expected '{expected}', found '{found}'")]
219 TransportTypeMismatch {
220 name: String,
221 expected: String,
222 found: String,
223 },
224
225 #[error("Transport not found: {name}")]
227 TransportNotFound { name: String },
228
229 #[error("No transport can handle address: {address:?}")]
231 NoTransportForAddress { address: Address },
232
233 #[error("Multiple transport errors: {}", errors.join(", "))]
235 MultipleTransportErrors { errors: Vec<String> },
236}
237
238impl SyncError {
239 pub fn is_configuration_error(&self) -> bool {
242 matches!(
243 self,
244 SyncError::NoTransportEnabled | SyncError::SyncNotEnabled
245 )
246 }
247
248 pub fn is_server_error(&self) -> bool {
250 matches!(
251 self,
252 SyncError::ServerAlreadyRunning { .. }
253 | SyncError::ServerNotRunning
254 | SyncError::ServerBind { .. }
255 )
256 }
257
258 pub fn is_network_error(&self) -> bool {
264 matches!(
265 self,
266 SyncError::Network(_) | SyncError::ConnectionFailed { .. } | SyncError::Timeout { .. }
267 )
268 }
269
270 pub fn timeout_phase(&self) -> Option<TimeoutPhase> {
275 match self {
276 SyncError::Timeout { phase, .. } => Some(*phase),
277 _ => None,
278 }
279 }
280
281 pub fn is_timeout(&self) -> bool {
283 self.timeout_phase().is_some()
284 }
285
286 pub fn is_protocol_error(&self) -> bool {
288 matches!(self, SyncError::UnexpectedResponse { .. })
289 }
290
291 pub fn is_not_found(&self) -> bool {
293 matches!(
294 self,
295 SyncError::PeerNotFound(_) | SyncError::EntryNotFound(_)
296 )
297 }
298
299 pub fn is_validation_error(&self) -> bool {
301 matches!(
302 self,
303 SyncError::InvalidEntry(_)
304 | SyncError::InvalidPublicKey { .. }
305 | SyncError::InvalidKeyName { .. }
306 )
307 }
308
309 pub fn is_backend_error(&self) -> bool {
311 matches!(self, SyncError::BackendError(_))
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 fn timeout(phase: TimeoutPhase) -> SyncError {
320 SyncError::Timeout {
321 address: "127.0.0.1:8080".to_string(),
322 phase,
323 elapsed: Duration::from_secs(30),
324 }
325 }
326
327 #[test]
330 fn a_timeout_is_distinguishable_from_a_refusal() {
331 let refused = SyncError::ConnectionFailed {
332 address: "127.0.0.1:8080".to_string(),
333 reason: "connection refused".to_string(),
334 };
335
336 assert!(timeout(TimeoutPhase::Request).is_timeout());
337 assert!(!refused.is_timeout());
338 assert!(!SyncError::Network("read failed".to_string()).is_timeout());
339 }
340
341 #[test]
344 fn a_timeout_is_still_a_network_error() {
345 assert!(timeout(TimeoutPhase::Connect).is_network_error());
346 assert!(timeout(TimeoutPhase::Request).is_network_error());
347 }
348
349 #[test]
352 fn the_phase_records_whether_the_peer_answered_at_all() {
353 assert_eq!(
354 timeout(TimeoutPhase::Connect).timeout_phase(),
355 Some(TimeoutPhase::Connect)
356 );
357 assert!(!TimeoutPhase::Connect.peer_reachable());
358 assert!(TimeoutPhase::Request.peer_reachable());
359
360 assert_eq!(SyncError::ServerNotRunning.timeout_phase(), None);
361 }
362
363 #[test]
366 fn the_message_names_the_peer_and_the_deadline() {
367 let rendered = timeout(TimeoutPhase::Request).to_string();
368 assert!(rendered.contains("127.0.0.1:8080"), "{rendered}");
369 assert!(rendered.contains("30s"), "{rendered}");
370 assert!(rendered.contains("awaiting a response"), "{rendered}");
371 }
372}