1use std::collections::HashMap;
34use web_time::{Duration, Instant};
35
36pub struct Dup {
42 entries: HashMap<String, DupEntry>,
43 max: usize,
45 age: Duration,
47 last_drop: Instant,
49}
50
51struct DupEntry {
52 was: Instant,
53}
54
55impl Dup {
56 pub fn new(max: usize, age_secs: u64) -> Self {
63 Self {
64 entries: HashMap::with_capacity(max),
65 max,
66 age: Duration::from_secs(age_secs),
67 last_drop: Instant::now(),
68 }
69 }
70
71 pub fn default_gun() -> Self {
73 Self::new(100_000, 9)
74 }
75
76 pub fn check(&mut self, id: &str) -> bool {
81 if let Some(entry) = self.entries.get(id) {
82 if entry.was.elapsed() < self.age {
83 return true;
84 }
85 self.entries.remove(id);
87 }
88 false
89 }
90
91 pub fn track(&mut self, id: &str) {
97 self.entries.insert(
98 id.to_string(),
99 DupEntry {
100 was: Instant::now(),
101 },
102 );
103 if self.entries.len() > self.max {
104 self.drop_oldest(self.max / 3);
105 }
106 if self.last_drop.elapsed() > self.age / 2 {
108 self.drop(None);
109 }
110 }
111
112 pub fn drop(&mut self, force_age: Option<Duration>) {
117 let cutoff = force_age.unwrap_or(self.age);
118 let now = Instant::now();
119 let expired: Vec<String> = self
120 .entries
121 .iter()
122 .filter(|(_, entry)| now.duration_since(entry.was) > cutoff)
123 .map(|(k, _)| k.clone())
124 .collect();
125 for k in expired {
126 self.entries.remove(&k);
127 }
128 self.last_drop = Instant::now();
129 }
130
131 fn drop_oldest(&mut self, n: usize) {
132 let n = n.min(self.entries.len());
133 if n == 0 {
134 return;
135 }
136 let mut pairs: Vec<(String, Instant)> = self
138 .entries
139 .iter()
140 .map(|(k, v)| (k.clone(), v.was))
141 .collect();
142 pairs.sort_by_key(|a| a.1);
143 for (k, _) in pairs.into_iter().take(n) {
144 self.entries.remove(&k);
145 }
146 }
147
148 pub fn len(&self) -> usize {
150 self.entries.len()
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.entries.is_empty()
156 }
157
158 pub fn max(&self) -> usize {
160 self.max
161 }
162
163 pub fn age(&self) -> Duration {
165 self.age
166 }
167
168 pub fn clear(&mut self) {
170 self.entries.clear();
171 self.last_drop = Instant::now();
172 }
173}
174
175impl Default for Dup {
176 fn default() -> Self {
177 Self::default_gun()
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_dup_basic() {
187 let mut dup = Dup::new(10, 5);
188 assert!(!dup.check("msg-1"));
189 dup.track("msg-1");
190 assert!(dup.check("msg-1"));
191 }
192
193 #[test]
194 fn test_dup_expiration() {
195 let mut dup = Dup::new(10, 1);
196 dup.track("msg-1");
197 assert!(dup.check("msg-1"));
198 std::thread::sleep(Duration::from_secs(2));
199 assert!(!dup.check("msg-1"));
200 }
201
202 #[test]
203 fn test_dup_max_eviction() {
204 let mut dup = Dup::new(3, 60);
205 dup.track("a");
206 dup.track("b");
207 dup.track("c");
208 assert_eq!(dup.len(), 3);
209 dup.track("d");
210 assert_eq!(dup.len(), 3);
211 assert!(!dup.check("a")); }
213
214 #[test]
215 fn test_gun_default() {
216 let dup = Dup::default_gun();
217 assert_eq!(dup.max(), 100_000);
218 assert_eq!(dup.age(), Duration::from_secs(9));
219 }
220
221 #[test]
222 fn test_dup_default_trait() {
223 let dup = Dup::default();
224 assert_eq!(dup.max(), 100_000);
225 assert_eq!(dup.age(), Duration::from_secs(9));
226 }
227
228 #[test]
229 fn test_dup_is_empty() {
230 let mut dup = Dup::new(10, 5);
231 assert!(dup.is_empty());
232 dup.track("x");
233 assert!(!dup.is_empty());
234 }
235
236 #[test]
237 fn test_dup_clear() {
238 let mut dup = Dup::new(10, 60);
239 dup.track("a");
240 dup.track("b");
241 assert_eq!(dup.len(), 2);
242 dup.clear();
243 assert_eq!(dup.len(), 0);
244 assert!(dup.is_empty());
245 }
246
247 #[test]
248 fn test_dup_drop_with_force_age() {
249 let mut dup = Dup::new(100, 60); dup.track("a");
251 dup.track("b");
252 assert_eq!(dup.len(), 2);
253 dup.drop(Some(Duration::from_secs(0)));
255 assert_eq!(dup.len(), 0);
256 }
257
258 #[test]
259 fn test_dup_retrack_updates_timestamp() {
260 let mut dup = Dup::new(10, 1);
261 dup.track("msg-1");
262 std::thread::sleep(Duration::from_millis(500));
263 dup.track("msg-1"); std::thread::sleep(Duration::from_millis(600));
265 assert!(dup.check("msg-1")); }
268
269 #[test]
270 fn test_dup_different_ids_independent() {
271 let mut dup = Dup::new(10, 60);
272 dup.track("msg-1");
273 assert!(dup.check("msg-1"));
274 assert!(!dup.check("msg-2"));
275 dup.track("msg-2");
276 assert!(dup.check("msg-2"));
277 assert!(dup.check("msg-1"));
278 }
279
280 #[test]
281 fn test_dup_expired_entry_removed_on_check() {
282 let mut dup = Dup::new(10, 1);
283 dup.track("msg-1");
284 assert_eq!(dup.len(), 1);
285 std::thread::sleep(Duration::from_secs(2));
286 assert!(!dup.check("msg-1"));
287 assert_eq!(dup.len(), 0); }
289}