use std::collections::HashMap;
#[derive(Debug, Default, Clone)]
pub struct SequenceDedup {
checkpoints: HashMap<String, u64>,
}
impl SequenceDedup {
pub fn new(checkpoints: HashMap<String, u64>) -> Self {
Self { checkpoints }
}
pub fn should_skip(&self, source_id: &str, sequence: Option<u64>) -> bool {
match sequence {
Some(seq) => self.checkpoints.get(source_id).is_some_and(|&cp| seq <= cp),
None => false,
}
}
pub fn advance(&mut self, source_id: &str, sequence: u64) {
if let Some(entry) = self.checkpoints.get_mut(source_id) {
if sequence > *entry {
*entry = sequence;
}
} else {
self.checkpoints.insert(source_id.to_string(), sequence);
}
}
pub fn checkpoint_for(&self, source_id: &str) -> Option<u64> {
self.checkpoints.get(source_id).copied()
}
pub fn all_checkpoints(&self) -> &HashMap<String, u64> {
&self.checkpoints
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_dedup_passes_everything() {
let d = SequenceDedup::default();
assert!(!d.should_skip("s1", Some(1)));
assert!(!d.should_skip("s1", Some(u64::MAX)));
assert!(!d.should_skip("s1", None));
}
#[test]
fn skips_events_at_or_below_checkpoint() {
let mut checkpoints = HashMap::new();
checkpoints.insert("s1".to_string(), 100u64);
let d = SequenceDedup::new(checkpoints);
assert!(d.should_skip("s1", Some(1)));
assert!(d.should_skip("s1", Some(99)));
assert!(d.should_skip("s1", Some(100))); }
#[test]
fn passes_events_above_checkpoint() {
let mut checkpoints = HashMap::new();
checkpoints.insert("s1".to_string(), 100u64);
let d = SequenceDedup::new(checkpoints);
assert!(!d.should_skip("s1", Some(101)));
assert!(!d.should_skip("s1", Some(u64::MAX)));
}
#[test]
fn none_sequence_always_passes() {
let mut checkpoints = HashMap::new();
checkpoints.insert("s1".to_string(), 100u64);
let d = SequenceDedup::new(checkpoints);
assert!(!d.should_skip("s1", None));
}
#[test]
fn unknown_source_passes() {
let mut checkpoints = HashMap::new();
checkpoints.insert("s1".to_string(), 100u64);
let d = SequenceDedup::new(checkpoints);
assert!(!d.should_skip("other", Some(1)));
assert!(!d.should_skip("other", Some(200)));
}
#[test]
fn advance_inserts_new_source() {
let mut d = SequenceDedup::default();
d.advance("s1", 42);
assert_eq!(d.checkpoint_for("s1"), Some(42));
}
#[test]
fn advance_updates_checkpoint() {
let mut d = SequenceDedup::default();
d.advance("s1", 42);
d.advance("s1", 100);
assert_eq!(d.checkpoint_for("s1"), Some(100));
}
#[test]
fn advance_is_monotonic() {
let mut d = SequenceDedup::default();
d.advance("s1", 100);
d.advance("s1", 42); assert_eq!(d.checkpoint_for("s1"), Some(100));
}
#[test]
fn advance_equal_is_noop() {
let mut d = SequenceDedup::default();
d.advance("s1", 100);
d.advance("s1", 100);
assert_eq!(d.checkpoint_for("s1"), Some(100));
}
#[test]
fn per_source_isolation() {
let mut d = SequenceDedup::default();
d.advance("s1", 100);
d.advance("s2", 50);
assert_eq!(d.checkpoint_for("s1"), Some(100));
assert_eq!(d.checkpoint_for("s2"), Some(50));
assert!(d.should_skip("s1", Some(50)));
assert!(!d.should_skip("s2", Some(75)));
}
#[test]
fn all_checkpoints_returns_full_map() {
let mut d = SequenceDedup::default();
d.advance("a", 1);
d.advance("b", 2);
d.advance("c", 3);
let map = d.all_checkpoints();
assert_eq!(map.len(), 3);
assert_eq!(map.get("a"), Some(&1));
assert_eq!(map.get("b"), Some(&2));
assert_eq!(map.get("c"), Some(&3));
}
#[test]
fn checkpoint_for_unknown_returns_none() {
let d = SequenceDedup::default();
assert_eq!(d.checkpoint_for("missing"), None);
}
}