gbp_core/bounded.rs
1//! LRU-bounded set for per-epoch deduplication.
2//!
3//! Each sub-protocol client (GTP, GSP, GAP) maintains a deduplication set
4//! that MUST NOT grow unboundedly within a single epoch. This type provides
5//! a `HashSet` with insertion-order eviction: when capacity is exceeded the
6//! oldest entry is dropped.
7
8use std::collections::{HashSet, VecDeque};
9use std::hash::Hash;
10
11/// A set with a bounded capacity that evicts the oldest entry on overflow.
12///
13/// Insertion (`insert`) returns `true` when the item is new and was added,
14/// `false` when it was already present. When `len() == cap`, the least
15/// recently inserted item is removed before the new item is added.
16#[derive(Clone, Debug)]
17pub struct BoundedSeen<T> {
18 set: HashSet<T>,
19 order: VecDeque<T>,
20 cap: usize,
21}
22
23impl<T: Eq + Hash + Clone> BoundedSeen<T> {
24 /// Creates a new set with the given capacity limit.
25 ///
26 /// N=10000 is the recommended default per epoch (GTP §5, GSP §5).
27 pub fn new(cap: usize) -> Self {
28 Self { set: HashSet::with_capacity(cap.min(1024)), order: VecDeque::with_capacity(cap.min(1024)), cap }
29 }
30
31 /// Inserts an item. Returns `true` if the item is new and was added,
32 /// `false` if it was already present.
33 ///
34 /// When the set is at capacity, the oldest entry is evicted.
35 pub fn insert(&mut self, item: T) -> bool {
36 if !self.set.insert(item.clone()) {
37 return false;
38 }
39 self.order.push_back(item);
40 if self.order.len() > self.cap {
41 if let Some(old) = self.order.pop_front() {
42 self.set.remove(&old);
43 }
44 }
45 true
46 }
47
48 /// Returns the number of entries currently in the set.
49 pub fn len(&self) -> usize {
50 self.set.len()
51 }
52
53 /// Returns `true` if the set is empty.
54 pub fn is_empty(&self) -> bool {
55 self.set.is_empty()
56 }
57
58 /// Clears the set.
59 pub fn clear(&mut self) {
60 self.set.clear();
61 self.order.clear();
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn insert_new_returns_true() {
71 let mut s = BoundedSeen::new(10);
72 assert!(s.insert(1));
73 }
74
75 #[test]
76 fn insert_duplicate_returns_false() {
77 let mut s = BoundedSeen::new(10);
78 s.insert(1);
79 assert!(!s.insert(1));
80 }
81
82 #[test]
83 fn evicts_oldest_on_overflow() {
84 let mut s = BoundedSeen::new(3);
85 s.insert(1);
86 s.insert(2);
87 s.insert(3);
88 // At capacity; next insert evicts 1 (oldest).
89 s.insert(4);
90 // 1 was evicted, fresh again — but this evicts 2.
91 assert!(s.insert(1));
92 // 4 was second-newest, still present.
93 assert!(!s.insert(4));
94 assert_eq!(s.len(), 3);
95 }
96
97 #[test]
98 fn clear_resets() {
99 let mut s = BoundedSeen::new(100);
100 s.insert("a");
101 s.clear();
102 assert!(s.is_empty());
103 assert!(s.insert("a")); // fresh after clear
104 }
105}