Skip to main content

imessage_webhooks/
event_cache.rs

1/// Event deduplication cache with 1-hour TTL.
2///
3/// Prevents duplicate webhook deliveries when the same event is detected
4/// multiple times (e.g., during DB polling overlap).
5use std::collections::HashMap;
6use std::time::{Duration, Instant};
7
8const TTL: Duration = Duration::from_secs(3600); // 1 hour
9
10pub struct EventCache {
11    entries: HashMap<String, Instant>,
12}
13
14impl EventCache {
15    pub fn new() -> Self {
16        Self {
17            entries: HashMap::new(),
18        }
19    }
20
21    /// Check if an event key has been seen recently.
22    /// Returns true if the event is a duplicate (should be skipped).
23    pub fn is_duplicate(&mut self, key: &str) -> bool {
24        self.cleanup();
25
26        if self.entries.contains_key(key) {
27            return true;
28        }
29
30        self.entries.insert(key.to_string(), Instant::now());
31        false
32    }
33
34    /// Remove expired entries.
35    fn cleanup(&mut self) {
36        let now = Instant::now();
37        self.entries.retain(|_, ts| now.duration_since(*ts) < TTL);
38    }
39
40    /// Number of cached entries (for testing).
41    pub fn len(&self) -> usize {
42        self.entries.len()
43    }
44
45    pub fn is_empty(&self) -> bool {
46        self.entries.is_empty()
47    }
48}
49
50impl Default for EventCache {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn new_events_are_not_duplicates() {
62        let mut cache = EventCache::new();
63        assert!(!cache.is_duplicate("event-1"));
64        assert!(!cache.is_duplicate("event-2"));
65        assert_eq!(cache.len(), 2);
66    }
67
68    #[test]
69    fn repeated_events_are_duplicates() {
70        let mut cache = EventCache::new();
71        assert!(!cache.is_duplicate("event-1"));
72        assert!(cache.is_duplicate("event-1"));
73        assert!(cache.is_duplicate("event-1"));
74    }
75
76    #[test]
77    fn different_events_are_independent() {
78        let mut cache = EventCache::new();
79        assert!(!cache.is_duplicate("event-1"));
80        assert!(!cache.is_duplicate("event-2"));
81        assert!(cache.is_duplicate("event-1"));
82        assert!(cache.is_duplicate("event-2"));
83    }
84}