Skip to main content

kevy_replicate/
replica_error.rs

1//! Error surface of [`crate::replica::ReplicaClient`] — split from
2//! `replica.rs` so each file stays under the 500-LOC house rule.
3//! Re-exported from [`crate::replica`], so caller paths are
4//! unchanged.
5
6use crate::wire::WireError;
7use std::io;
8
9/// Errors a replica client can surface to its driver loop.
10#[derive(Debug)]
11pub enum ReplicaError {
12    /// Primary closed the connection or never replied during the
13    /// handshake / `+ACK` exchange.
14    HandshakeRejected,
15    /// `+ACK` line was malformed (didn't start with `+ACK `, didn't
16    /// parse the offset).
17    AckMalformed,
18    /// Peer closed the connection mid-frame; reconnect to resume.
19    Truncated,
20    /// Wire-level decode error (envelope shape wrong, payload
21    /// malformed, etc.).
22    Frame(WireError),
23    /// Frame arrived with an offset other than the expected next.
24    /// Caller should trigger a full snapshot resync.
25    OffsetGap {
26        /// The offset the client expected next (= `last_seen + 1`).
27        expected: u64,
28        /// The offset the primary actually sent.
29        got: u64,
30    },
31    /// While streaming a snapshot, the primary sent bytes that were
32    /// neither a snapshot chunk nor `+SNAPSHOT_END`. Interleaving live
33    /// frames inside a snapshot is forbidden (see `docs/snapshot.md`).
34    UnexpectedInSnapshot,
35    /// `next_frame` was called but the next event is a snapshot
36    /// marker / chunk. Callers that want the snapshot-aware surface
37    /// must use [`ReplicaClient::next_event`].
38    SnapshotInProgress,
39    /// Underlying socket I/O failure.
40    Io(io::Error),
41}
42
43impl std::fmt::Display for ReplicaError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::HandshakeRejected => write!(f, "primary rejected replication handshake"),
47            Self::AckMalformed => write!(f, "primary sent malformed +ACK"),
48            Self::Truncated => write!(f, "replication stream truncated by peer"),
49            Self::Frame(e) => write!(f, "replication frame decode error: {e}"),
50            Self::OffsetGap { expected, got } => {
51                write!(f, "replication offset gap: expected {expected}, got {got}")
52            }
53            Self::UnexpectedInSnapshot => {
54                write!(f, "primary sent non-chunk bytes mid-snapshot")
55            }
56            Self::SnapshotInProgress => {
57                write!(f, "snapshot in progress; use next_event() to consume")
58            }
59            Self::Io(e) => write!(f, "replication socket I/O error: {e}"),
60        }
61    }
62}
63
64impl std::error::Error for ReplicaError {}
65
66impl From<io::Error> for ReplicaError {
67    fn from(e: io::Error) -> Self {
68        ReplicaError::Io(e)
69    }
70}
71
72impl From<WireError> for ReplicaError {
73    fn from(e: WireError) -> Self {
74        match e {
75            WireError::Truncated => ReplicaError::Truncated,
76            other => ReplicaError::Frame(other),
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn from_io_error_wraps_into_io_variant() {
87        let e: ReplicaError = io::Error::new(io::ErrorKind::ConnectionRefused, "x").into();
88        assert!(matches!(e, ReplicaError::Io(_)));
89    }
90
91    #[test]
92    fn from_wire_error_truncated_maps_to_truncated() {
93        let e: ReplicaError = WireError::Truncated.into();
94        assert!(matches!(e, ReplicaError::Truncated));
95    }
96
97    #[test]
98    fn from_wire_error_other_maps_to_frame() {
99        let e: ReplicaError = WireError::BadEnvelope.into();
100        assert!(matches!(e, ReplicaError::Frame(_)));
101    }
102}