aboutsummaryrefslogtreecommitdiff
path: root/src/api/event_hub.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/event_hub.rs')
-rw-r--r--src/api/event_hub.rs81
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)
+ );
+ }
+}