use std::collections::HashMap;
use web_time::{Duration, Instant};
pub struct Dup {
entries: HashMap<String, DupEntry>,
max: usize,
age: Duration,
last_drop: Instant,
}
struct DupEntry {
was: Instant,
}
impl Dup {
pub fn new(max: usize, age_secs: u64) -> Self {
Self {
entries: HashMap::with_capacity(max),
max,
age: Duration::from_secs(age_secs),
last_drop: Instant::now(),
}
}
pub fn default_gun() -> Self {
Self::new(999, 9)
}
pub fn check(&mut self, id: &str) -> bool {
if let Some(entry) = self.entries.get(id) {
if entry.was.elapsed() < self.age {
return true;
}
self.entries.remove(id);
}
false
}
pub fn track(&mut self, id: &str) {
self.entries.insert(
id.to_string(),
DupEntry {
was: Instant::now(),
},
);
if self.entries.len() > self.max {
self.drop_oldest(self.max / 3);
}
if self.last_drop.elapsed() > self.age / 2 {
self.drop(None);
}
}
pub fn drop(&mut self, force_age: Option<Duration>) {
let cutoff = force_age.unwrap_or(self.age);
let now = Instant::now();
let expired: Vec<String> = self
.entries
.iter()
.filter(|(_, entry)| now.duration_since(entry.was) > cutoff)
.map(|(k, _)| k.clone())
.collect();
for k in expired {
self.entries.remove(&k);
}
self.last_drop = Instant::now();
}
fn drop_oldest(&mut self, n: usize) {
let n = n.min(self.entries.len());
if n == 0 {
return;
}
let mut pairs: Vec<(String, Instant)> = self
.entries
.iter()
.map(|(k, v)| (k.clone(), v.was))
.collect();
pairs.sort_by_key(|a| a.1);
for (k, _) in pairs.into_iter().take(n) {
self.entries.remove(&k);
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn max(&self) -> usize {
self.max
}
pub fn age(&self) -> Duration {
self.age
}
pub fn clear(&mut self) {
self.entries.clear();
self.last_drop = Instant::now();
}
}
impl Default for Dup {
fn default() -> Self {
Self::default_gun()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dup_basic() {
let mut dup = Dup::new(10, 5);
assert!(!dup.check("msg-1"));
dup.track("msg-1");
assert!(dup.check("msg-1"));
}
#[test]
fn test_dup_expiration() {
let mut dup = Dup::new(10, 1);
dup.track("msg-1");
assert!(dup.check("msg-1"));
std::thread::sleep(Duration::from_secs(2));
assert!(!dup.check("msg-1"));
}
#[test]
fn test_dup_max_eviction() {
let mut dup = Dup::new(3, 60);
dup.track("a");
dup.track("b");
dup.track("c");
assert_eq!(dup.len(), 3);
dup.track("d");
assert_eq!(dup.len(), 3);
assert!(!dup.check("a")); }
#[test]
fn test_gun_default() {
let dup = Dup::default_gun();
assert_eq!(dup.max(), 999);
assert_eq!(dup.age(), Duration::from_secs(9));
}
#[test]
fn test_dup_default_trait() {
let dup = Dup::default();
assert_eq!(dup.max(), 999);
assert_eq!(dup.age(), Duration::from_secs(9));
}
#[test]
fn test_dup_is_empty() {
let mut dup = Dup::new(10, 5);
assert!(dup.is_empty());
dup.track("x");
assert!(!dup.is_empty());
}
#[test]
fn test_dup_clear() {
let mut dup = Dup::new(10, 60);
dup.track("a");
dup.track("b");
assert_eq!(dup.len(), 2);
dup.clear();
assert_eq!(dup.len(), 0);
assert!(dup.is_empty());
}
#[test]
fn test_dup_drop_with_force_age() {
let mut dup = Dup::new(100, 60); dup.track("a");
dup.track("b");
assert_eq!(dup.len(), 2);
dup.drop(Some(Duration::from_secs(0)));
assert_eq!(dup.len(), 0);
}
#[test]
fn test_dup_retrack_updates_timestamp() {
let mut dup = Dup::new(10, 1);
dup.track("msg-1");
std::thread::sleep(Duration::from_millis(500));
dup.track("msg-1"); std::thread::sleep(Duration::from_millis(600));
assert!(dup.check("msg-1")); }
#[test]
fn test_dup_different_ids_independent() {
let mut dup = Dup::new(10, 60);
dup.track("msg-1");
assert!(dup.check("msg-1"));
assert!(!dup.check("msg-2"));
dup.track("msg-2");
assert!(dup.check("msg-2"));
assert!(dup.check("msg-1"));
}
#[test]
fn test_dup_expired_entry_removed_on_check() {
let mut dup = Dup::new(10, 1);
dup.track("msg-1");
assert_eq!(dup.len(), 1);
std::thread::sleep(Duration::from_secs(2));
assert!(!dup.check("msg-1"));
assert_eq!(dup.len(), 0); }
}