Skip to main content

kevy_replicate/
replica_event.rs

1//! The events a [`crate::replica::ReplicaClient`] yields — 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 unchanged.
4
5use crate::feed::FeedPosition;
6use crate::replica::DecodedFrame;
7
8/// Event yielded by [`ReplicaClient::next_event`](crate::replica::ReplicaClient::next_event). A driver loop
9/// pattern-matches and applies each:
10/// - [`Self::Frame`] → run through the local dispatcher.
11/// - [`Self::SnapshotBegin`] → caller should reset / prepare the
12///   local store for a fresh-from-snapshot fill.
13/// - [`Self::SnapshotChunk`] → append the bytes to the caller's
14///   accumulating snapshot buffer.
15/// - [`Self::SnapshotEnd`] → caller hands the accumulated buffer to
16///   `kevy_persist::load_snapshot`; [`ReplicaClient`](crate::replica::ReplicaClient) has already
17///   advanced `expected_offset` to `ack_offset`, so the next
18///   [`Self::Frame`] arrives at `ack_offset` with no gap.
19///
20/// A replica joining through a snapshot sees the whole sequence:
21///
22/// ```
23/// # mod doc { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/support/doc_primary.rs")); } use doc::*;
24/// use kevy_replicate::replica::{ReplicaClient, ReplicaEvent};
25/// use kevy_replicate::wire;
26///
27/// # let mut stream = wire::encode_snapshot_begin();
28/// # stream.extend(wire::encode_snapshot_chunk(b"dump"));
29/// # stream.extend(wire::encode_snapshot_end(9));
30/// # stream.extend(wire::encode_frame(9, &argv(&["SET", "k", "v"])));
31/// # let (addr, _primary) = fake_primary(b"+ACK 1 0\r\n", stream);
32/// let mut client = ReplicaClient::connect(addr, "replica-a", 0)?;
33/// let mut snapshot = Vec::new();
34/// while let Some(event) = client.next_event() {
35///     match event? {
36///         ReplicaEvent::SnapshotBegin => snapshot.clear(),
37///         ReplicaEvent::SnapshotChunk(bytes) => snapshot.extend(bytes),
38///         ReplicaEvent::SnapshotEnd { ack_offset } => assert_eq!(ack_offset, 9),
39///         ReplicaEvent::Frame(frame) => assert_eq!(frame.offset, 9),
40///         _ => {}
41///     }
42/// }
43/// assert_eq!(snapshot, b"dump");
44/// # Ok::<(), Box<dyn std::error::Error>>(())
45/// ```
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47#[non_exhaustive]
48pub enum ReplicaEvent {
49    /// A live mutation frame.
50    ///
51    /// ```
52    /// # mod doc { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/support/doc_primary.rs")); } use doc::*;
53    /// use kevy_replicate::replica::{DecodedFrame, ReplicaClient, ReplicaEvent};
54    ///
55    /// # let stream = kevy_replicate::wire::encode_frame(0, &argv(&["SET", "k", "v"]));
56    /// # let (addr, _primary) = fake_primary(b"+ACK 1 0\r\n", stream);
57    /// let mut client = ReplicaClient::connect(addr, "replica-a", 0)?;
58    /// let expected = DecodedFrame::new(0, argv(&["SET", "k", "v"]));
59    /// assert_eq!(client.next_event().transpose()?, Some(ReplicaEvent::Frame(expected)));
60    /// # Ok::<(), Box<dyn std::error::Error>>(())
61    /// ```
62    Frame(DecodedFrame),
63    /// In-stream heartbeat: the primary's tail at send time — its feed
64    /// generation (the REPL.TOKEN / REPL.WAIT gen truth; `0` = the
65    /// primary spoke the legacy one-number heartbeat, "unknown") and its
66    /// `next_offset`. Lets the replica compute lag (applied vs primary)
67    /// and judge link liveness. Occupies no offset space.
68    ///
69    /// ```
70    /// # mod doc { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/support/doc_primary.rs")); } use doc::*;
71    /// use kevy_replicate::feed::FeedPosition;
72    /// use kevy_replicate::replica::{ReplicaClient, ReplicaEvent};
73    ///
74    /// # let stream = kevy_replicate::wire::encode_ping(FeedPosition::new(1, 120));
75    /// # let (addr, _primary) = fake_primary(b"+ACK 1 100\r\n", stream);
76    /// let mut client = ReplicaClient::connect(addr, "replica-a", 100)?;
77    /// let Some(ReplicaEvent::Ping(tail)) = client.next_event().transpose()? else { panic!() };
78    /// assert_eq!(tail.offset - client.expected_offset(), 20); // frames behind the primary
79    /// # Ok::<(), Box<dyn std::error::Error>>(())
80    /// ```
81    Ping(FeedPosition),
82    /// Snapshot ship begin marker (`+SNAPSHOT\r\n`).
83    ///
84    /// ```
85    /// # mod doc { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/support/doc_primary.rs")); } use doc::*;
86    /// use kevy_replicate::replica::{ReplicaClient, ReplicaEvent};
87    ///
88    /// # let (addr, _primary) = fake_primary(b"+ACK 1 0\r\n", kevy_replicate::wire::encode_snapshot_begin());
89    /// let mut client = ReplicaClient::connect(addr, "replica-a", 0)?;
90    /// assert_eq!(client.next_event().transpose()?, Some(ReplicaEvent::SnapshotBegin));
91    /// # Ok::<(), Box<dyn std::error::Error>>(())
92    /// ```
93    SnapshotBegin,
94    /// One snapshot chunk's payload bytes (RESP bulk string body).
95    ///
96    /// ```
97    /// # mod doc { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/support/doc_primary.rs")); } use doc::*;
98    /// use kevy_replicate::replica::{ReplicaClient, ReplicaEvent};
99    /// use kevy_replicate::wire;
100    ///
101    /// # let mut stream = wire::encode_snapshot_begin();
102    /// # stream.extend(wire::encode_snapshot_chunk(b"part-1"));
103    /// # let (addr, _primary) = fake_primary(b"+ACK 1 0\r\n", stream);
104    /// let mut client = ReplicaClient::connect(addr, "replica-a", 0)?;
105    /// client.next_event(); // SnapshotBegin
106    /// let chunk = ReplicaEvent::SnapshotChunk(b"part-1".to_vec());
107    /// assert_eq!(client.next_event().transpose()?, Some(chunk));
108    /// # Ok::<(), Box<dyn std::error::Error>>(())
109    /// ```
110    SnapshotChunk(Vec<u8>),
111    /// Snapshot ship end marker carrying the offset the next live
112    /// frame will have.
113    ///
114    /// ```
115    /// # mod doc { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/support/doc_primary.rs")); } use doc::*;
116    /// use kevy_replicate::replica::{ReplicaClient, ReplicaEvent};
117    /// use kevy_replicate::wire;
118    ///
119    /// # let mut stream = wire::encode_snapshot_begin();
120    /// # stream.extend(wire::encode_snapshot_end(57));
121    /// # let (addr, _primary) = fake_primary(b"+ACK 1 0\r\n", stream);
122    /// let mut client = ReplicaClient::connect(addr, "replica-a", 0)?;
123    /// client.next_event(); // SnapshotBegin
124    /// let end = client.next_event().transpose()?;
125    /// assert_eq!(end, Some(ReplicaEvent::SnapshotEnd { ack_offset: 57 }));
126    /// # Ok::<(), Box<dyn std::error::Error>>(())
127    /// ```
128    SnapshotEnd {
129        /// The offset the primary's `next_offset` was at when the
130        /// snapshot started. After this event, [`ReplicaClient::expected_offset`](crate::replica::ReplicaClient::expected_offset)
131        /// equals this value.
132        ///
133        /// ```
134        /// # mod doc { include!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/support/doc_primary.rs")); } use doc::*;
135        /// use kevy_replicate::replica::{ReplicaClient, ReplicaEvent};
136        /// use kevy_replicate::wire;
137        ///
138        /// # let mut stream = wire::encode_snapshot_begin();
139        /// # stream.extend(wire::encode_snapshot_end(57));
140        /// # let (addr, _primary) = fake_primary(b"+ACK 1 0\r\n", stream);
141        /// let mut client = ReplicaClient::connect(addr, "replica-a", 0)?;
142        /// client.next_event(); // SnapshotBegin
143        /// let Some(ReplicaEvent::SnapshotEnd { ack_offset }) = client.next_event().transpose()? else {
144        ///     panic!("expected the end marker")
145        /// };
146        /// assert_eq!(client.expected_offset(), ack_offset); // the next live frame is 57
147        /// # Ok::<(), Box<dyn std::error::Error>>(())
148        /// ```
149        ack_offset: u64,
150    },
151}