1use std::{
3 collections::HashSet,
4 hash::{Hash, Hasher},
5 sync::{Arc, Mutex},
6 time::{Duration, Instant},
7};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct IdKey {
23 pub platform: String,
24 pub chat_id: String,
25 pub msg_id: String,
26}
27
28#[derive(Clone)]
30pub struct SeenSet {
31 inner: Arc<Mutex<Inner>>,
32 ttl: Duration,
33}
34
35struct Inner {
36 set: HashSet<u64>,
37 times: Vec<(u64, Instant)>,
38}
39
40impl SeenSet {
41 pub fn new(ttl: Duration) -> Self {
57 Self {
58 inner: Arc::new(Mutex::new(Inner {
59 set: HashSet::new(),
60 times: Vec::new(),
61 })),
62 ttl,
63 }
64 }
65
66 fn key_hash(k: &IdKey) -> u64 {
67 let mut hasher = std::collections::hash_map::DefaultHasher::new();
68 k.hash(&mut hasher);
69 hasher.finish()
70 }
71
72 pub fn seen_or_insert(&self, key: &IdKey) -> bool {
74 let now = Instant::now();
75 let mut g = self.inner.lock().unwrap();
76 let mut expired = Vec::new();
78 g.times.retain(|(h, t)| {
79 if now.duration_since(*t) > self.ttl {
80 expired.push(*h);
81 false
82 } else {
83 true
84 }
85 });
86 for h in expired {
87 g.set.remove(&h);
88 }
89 let h = Self::key_hash(key);
90 if g.set.contains(&h) {
91 return true;
92 }
93 g.set.insert(h);
94 g.times.push((h, now));
95 false
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use std::time::Duration;
103
104 #[test]
105 fn idempotency_basic() {
106 let s = SeenSet::new(Duration::from_millis(50));
107 let k = IdKey {
108 platform: "telegram".into(),
109 chat_id: "c1".into(),
110 msg_id: "m1".into(),
111 };
112 assert!(!s.seen_or_insert(&k));
113 assert!(s.seen_or_insert(&k));
114 }
115
116 #[test]
117 fn idempotency_entries_expire() {
118 let s = SeenSet::new(Duration::from_millis(10));
119 let k = IdKey {
120 platform: "telegram".into(),
121 chat_id: "c1".into(),
122 msg_id: "m1".into(),
123 };
124 assert!(!s.seen_or_insert(&k));
125 std::thread::sleep(Duration::from_millis(20));
126 assert!(!s.seen_or_insert(&k));
127 }
128}