Skip to main content

rmux_core/events/
cursor.rs

1use super::ring::{OutputEvent, RecentOutputSnapshot};
2use std::ops::Range;
3
4/// Independent read position for one pane-output subscriber.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct OutputCursor {
7    next_sequence: u64,
8    missed_events: u64,
9}
10
11impl OutputCursor {
12    /// Creates a cursor that will next read `next_sequence`.
13    #[must_use]
14    pub const fn new(next_sequence: u64) -> Self {
15        Self {
16            next_sequence,
17            missed_events: 0,
18        }
19    }
20
21    /// Returns the next sequence this cursor expects to read.
22    #[must_use]
23    pub const fn next_sequence(&self) -> u64 {
24        self.next_sequence
25    }
26
27    /// Returns the total number of events this cursor has explicitly missed.
28    #[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    /// Advances past `sequence` only when it is exactly the next retained event.
38    ///
39    /// This lets live-output fast paths consume an already identified event
40    /// without exposing arbitrary cursor rewinds to callers.
41    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/// One cursor poll result from an [`OutputRing`](super::ring::OutputRing).
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum OutputCursorItem {
58    /// A retained output event.
59    Event(OutputEvent),
60    /// The cursor fell behind the oldest retained event.
61    Gap(Box<OutputGap>),
62}
63
64/// Explicit report for output events that no longer fit in the ring.
65#[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    /// Returns the sequence the cursor expected before lag was detected.
92    #[must_use]
93    pub const fn expected_sequence(&self) -> u64 {
94        self.expected_sequence
95    }
96
97    /// Returns the oldest retained sequence the cursor can resume from.
98    #[must_use]
99    pub const fn resume_sequence(&self) -> u64 {
100        self.resume_sequence
101    }
102
103    /// Returns the number of events skipped by this gap.
104    #[must_use]
105    pub const fn missed_events(&self) -> u64 {
106        self.missed_events
107    }
108
109    /// Returns the half-open output sequence range skipped by this gap.
110    #[must_use]
111    pub fn missed_range(&self) -> Range<u64> {
112        self.expected_sequence..self.resume_sequence
113    }
114
115    /// Returns the newest appended sequence when the gap was reported.
116    #[must_use]
117    pub const fn newest_sequence(&self) -> u64 {
118        self.newest_sequence
119    }
120
121    /// Returns the bounded recent live bytes available at gap detection time.
122    #[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}