rmux_core/events/
cursor.rs1use super::ring::{OutputEvent, RecentOutputSnapshot};
2use std::ops::Range;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct OutputCursor {
7 next_sequence: u64,
8 missed_events: u64,
9}
10
11impl OutputCursor {
12 #[must_use]
14 pub const fn new(next_sequence: u64) -> Self {
15 Self {
16 next_sequence,
17 missed_events: 0,
18 }
19 }
20
21 #[must_use]
23 pub const fn next_sequence(&self) -> u64 {
24 self.next_sequence
25 }
26
27 #[must_use]
29 pub const fn missed_events(&self) -> u64 {
30 self.missed_events
31 }
32
33 pub(super) fn advance_to(&mut self, next_sequence: u64) {
34 self.next_sequence = next_sequence;
35 }
36
37 pub fn advance_past_sequence(&mut self, sequence: u64) -> bool {
42 if self.next_sequence != sequence {
43 return false;
44 }
45 self.next_sequence = sequence.wrapping_add(1);
46 true
47 }
48
49 pub(super) fn record_gap(&mut self, missed: u64, resume_sequence: u64) {
50 self.missed_events = self.missed_events.saturating_add(missed);
51 self.next_sequence = resume_sequence;
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum OutputCursorItem {
58 Event(OutputEvent),
60 Gap(Box<OutputGap>),
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct OutputGap {
67 expected_sequence: u64,
68 resume_sequence: u64,
69 missed_events: u64,
70 newest_sequence: u64,
71 recent_snapshot: RecentOutputSnapshot,
72}
73
74impl OutputGap {
75 pub(super) const fn new(
76 expected_sequence: u64,
77 resume_sequence: u64,
78 missed_events: u64,
79 newest_sequence: u64,
80 recent_snapshot: RecentOutputSnapshot,
81 ) -> Self {
82 Self {
83 expected_sequence,
84 resume_sequence,
85 missed_events,
86 newest_sequence,
87 recent_snapshot,
88 }
89 }
90
91 #[must_use]
93 pub const fn expected_sequence(&self) -> u64 {
94 self.expected_sequence
95 }
96
97 #[must_use]
99 pub const fn resume_sequence(&self) -> u64 {
100 self.resume_sequence
101 }
102
103 #[must_use]
105 pub const fn missed_events(&self) -> u64 {
106 self.missed_events
107 }
108
109 #[must_use]
111 pub fn missed_range(&self) -> Range<u64> {
112 self.expected_sequence..self.resume_sequence
113 }
114
115 #[must_use]
117 pub const fn newest_sequence(&self) -> u64 {
118 self.newest_sequence
119 }
120
121 #[must_use]
123 pub const fn recent_snapshot(&self) -> &RecentOutputSnapshot {
124 &self.recent_snapshot
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::{OutputCursor, OutputCursorItem};
131 use crate::events::OutputRing;
132
133 #[test]
134 fn output_cursor_item_size_stays_bounded() {
135 assert!(
136 std::mem::size_of::<OutputCursorItem>() <= 48,
137 "OutputCursorItem should stay compact for batched cursor vectors, got {}",
138 std::mem::size_of::<OutputCursorItem>()
139 );
140 }
141
142 #[test]
143 fn cursor_advances_independently_through_retained_events() {
144 let mut ring = OutputRing::new(8, 64);
145 ring.push(b"one".to_vec());
146 ring.push(b"two".to_vec());
147 let mut first = ring.cursor_from_oldest();
148 let mut second = ring.cursor_from_oldest();
149
150 assert_eq!(
151 ring.poll_cursor(&mut first),
152 Some(OutputCursorItem::Event(ring.retained_events()[0].clone()))
153 );
154 assert_eq!(first.next_sequence(), 1);
155 assert_eq!(second.next_sequence(), 0);
156
157 assert_eq!(
158 ring.poll_cursor(&mut first),
159 Some(OutputCursorItem::Event(ring.retained_events()[1].clone()))
160 );
161 assert_eq!(ring.poll_cursor(&mut first), None);
162 assert_eq!(first.next_sequence(), ring.next_sequence());
163
164 assert_eq!(
165 ring.poll_cursor(&mut second),
166 Some(OutputCursorItem::Event(ring.retained_events()[0].clone()))
167 );
168 assert_eq!(second.next_sequence(), 1);
169 }
170
171 #[test]
172 fn lagged_cursor_reports_explicit_gap_and_resumes_at_oldest_event() {
173 let mut ring = OutputRing::new(2, 64);
174 let mut cursor = OutputCursor::new(0);
175 for bytes in [b"zero".as_slice(), b"one".as_slice(), b"two".as_slice()] {
176 ring.push(bytes.to_vec());
177 }
178
179 let Some(OutputCursorItem::Gap(gap)) = ring.poll_cursor(&mut cursor) else {
180 panic!("cursor should report lag");
181 };
182 assert_eq!(gap.expected_sequence(), 0);
183 assert_eq!(gap.resume_sequence(), 1);
184 assert_eq!(gap.missed_events(), 1);
185 assert_eq!(gap.missed_range(), 0..1);
186 assert_eq!(gap.newest_sequence(), 2);
187 assert_eq!(gap.recent_snapshot().oldest_sequence(), Some(0));
188 assert_eq!(gap.recent_snapshot().newest_sequence(), Some(2));
189 assert_eq!(cursor.missed_events(), 1);
190 assert_eq!(cursor.next_sequence(), 1);
191
192 let Some(OutputCursorItem::Event(event)) = ring.poll_cursor(&mut cursor) else {
193 panic!("cursor should resume with oldest retained event");
194 };
195 assert_eq!(event.sequence(), 1);
196 assert_eq!(event.bytes(), b"one");
197 }
198}