diff options
Diffstat (limited to 'src/api/event_hub.rs')
| -rw-r--r-- | src/api/event_hub.rs | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/src/api/event_hub.rs b/src/api/event_hub.rs index 42cf47a2..29843313 100644 --- a/src/api/event_hub.rs +++ b/src/api/event_hub.rs @@ -9,6 +9,12 @@ struct EventHubState { events: Vec<(u64, crate::api::schema::EventEnvelope)>, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum EventHistoryError { + Lost, + Unavailable, +} + impl EventHub { const MAX_EVENTS: usize = 512; @@ -37,6 +43,29 @@ impl EventHub { .collect() } + pub(super) fn events_after_checked( + &self, + sequence: u64, + ) -> Result<Vec<(u64, crate::api::schema::EventEnvelope)>, EventHistoryError> { + let state = self + .inner + .lock() + .map_err(|_| EventHistoryError::Unavailable)?; + if state + .events + .first() + .is_some_and(|(first, _)| sequence < first.saturating_sub(1)) + { + return Err(EventHistoryError::Lost); + } + Ok(state + .events + .iter() + .filter(|(event_sequence, _)| *event_sequence > sequence) + .cloned() + .collect()) + } + pub fn current_sequence(&self) -> u64 { let Ok(state) = self.inner.lock() else { return 0; @@ -44,3 +73,55 @@ impl EventHub { state.next_sequence } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::schema::{EventData, EventEnvelope, EventKind}; + + fn event() -> EventEnvelope { + EventEnvelope { + event: EventKind::WorkspaceFocused, + data: EventData::WorkspaceFocused { + workspace_id: "workspace_1".into(), + }, + } + } + + #[test] + fn checked_history_distinguishes_retained_boundary_from_lost_events() { + let hub = EventHub::default(); + assert!(hub.events_after_checked(0).unwrap().is_empty()); + for _ in 0..EventHub::MAX_EVENTS { + hub.push(event()); + } + assert_eq!( + hub.events_after_checked(0).unwrap().len(), + EventHub::MAX_EVENTS + ); + hub.push(event()); + assert_eq!(hub.events_after_checked(0), Err(EventHistoryError::Lost)); + let retained = hub.events_after_checked(1).unwrap(); + assert_eq!(retained.len(), EventHub::MAX_EVENTS); + assert_eq!(retained.first().unwrap().0, 2); + assert_eq!(retained.last().unwrap().0, hub.current_sequence()); + assert!(hub + .events_after_checked(hub.current_sequence()) + .unwrap() + .is_empty()); + } + + #[test] + fn checked_history_reports_unavailable_instead_of_empty_after_poison() { + let hub = EventHub::default(); + assert!(std::panic::catch_unwind(|| { + let _guard = hub.inner.lock().unwrap(); + panic!("poison the test event history"); + }) + .is_err()); + assert_eq!( + hub.events_after_checked(0), + Err(EventHistoryError::Unavailable) + ); + } +} |
