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: u64) -> bool {
self.checkpoints
.get(source_id)
.is_some_and(|&cp| sequence <= cp)
}
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", 1));
assert!(!d.should_skip("s1", u64::MAX));
}
#[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", 1));
assert!(d.should_skip("s1", 99));
assert!(d.should_skip("s1", 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", 101));
assert!(!d.should_skip("s1", u64::MAX));
}
#[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", 1));
assert!(!d.should_skip("other", 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", 50));
assert!(!d.should_skip("s2", 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);
}
}