Skip to main content

bytesandbrains_core/
address.rs

1use std::fmt;
2use std::hash::{Hash, Hasher};
3use std::num::NonZeroUsize;
4use std::str::FromStr;
5
6use lru::LruCache;
7
8/// Trait for peer addresses — blanket implemented for any type that is
9/// `Hash + Eq + Clone + ToString + FromStr`.
10///
11/// This abstracts over `String`, `SocketAddr`, `Multiaddr`, or any custom address type.
12pub trait Address: Hash + Eq + Clone + ToString + FromStr + fmt::Debug {}
13
14impl<T: Hash + Eq + Clone + ToString + FromStr + fmt::Debug> Address for T {}
15
16/// A bounded, deduplicated collection of addresses for a peer.
17///
18/// Backed by an LRU cache: all operations (insert, lookup, promote, evict)
19/// are O(1). Most recently seen addresses are promoted automatically.
20/// When capacity is exceeded, the least recently seen entry is evicted.
21pub struct AddressBook<A: Address> {
22    cache: LruCache<A, ()>,
23    max_size: usize,
24}
25
26impl<A: Address> Clone for AddressBook<A> {
27    fn clone(&self) -> Self {
28        let mut cache = LruCache::new(NonZeroUsize::new(self.max_size).unwrap());
29        for (addr, _) in self.cache.iter().rev() {
30            cache.put(addr.clone(), ());
31        }
32        Self {
33            cache,
34            max_size: self.max_size,
35        }
36    }
37}
38
39impl<A: Address> PartialEq for AddressBook<A> {
40    fn eq(&self, other: &Self) -> bool {
41        if self.cache.len() != other.cache.len() {
42            return false;
43        }
44        self.cache
45            .iter()
46            .zip(other.cache.iter())
47            .all(|((a, _), (b, _))| a == b)
48    }
49}
50
51impl<A: Address> Eq for AddressBook<A> {}
52
53impl<A: Address> Hash for AddressBook<A> {
54    fn hash<H: Hasher>(&self, state: &mut H) {
55        for (addr, _) in self.cache.iter() {
56            addr.hash(state);
57        }
58    }
59}
60
61impl<A: Address> AddressBook<A> {
62    pub fn new(first_addr: A, max_size: usize) -> AddressBook<A> {
63        assert!(max_size > 0, "AddressBook max_size must be > 0");
64        let mut cache = LruCache::new(NonZeroUsize::new(max_size).unwrap());
65        cache.put(first_addr, ());
66        AddressBook { cache, max_size }
67    }
68
69    /// Record that we've seen this address. Returns true if it was newly added.
70    /// If already present, promotes it to most recent. If at capacity,
71    /// evicts the least recently seen entry.
72    pub fn seen(&mut self, addr: A) -> bool {
73        self.cache.put(addr, ()).is_none()
74    }
75
76    pub fn get(&self, index: usize) -> Option<&A> {
77        self.cache.iter().nth(index).map(|(addr, _)| addr)
78    }
79
80    /// Returns the most recently seen address.
81    pub fn first(&self) -> &A {
82        self.cache
83            .iter()
84            .next()
85            .map(|(addr, _)| addr)
86            .expect("AddressBook is never empty")
87    }
88
89    /// Iterates from most recently seen to least recently seen.
90    pub fn iter(&self) -> impl Iterator<Item = &A> {
91        self.cache.iter().map(|(addr, _)| addr)
92    }
93
94    pub fn len(&self) -> usize {
95        self.cache.len()
96    }
97
98    pub fn max_size(&self) -> usize {
99        self.max_size
100    }
101
102    /// Consumes the page and returns addresses in MRU-first order.
103    pub fn into_vec(self) -> Vec<A> {
104        self.cache.into_iter().map(|(addr, _)| addr).collect()
105    }
106
107    pub fn contains(&self, addr: &A) -> bool {
108        self.cache.contains(addr)
109    }
110
111    pub fn remove(&mut self, addr: &A) -> bool {
112        if self.cache.len() <= 1 {
113            return false;
114        }
115        self.cache.pop(addr).is_some()
116    }
117
118    pub fn is_empty(&self) -> bool {
119        self.cache.is_empty()
120    }
121
122    /// Merge addresses from another page into this one.
123    /// New addresses go in as least recent. Returns number added.
124    pub fn concat(&mut self, other: &AddressBook<A>) -> usize {
125        let mut added = 0;
126        // Iterate other in LRU-first order so we don't disturb our MRU ordering
127        for (addr, _) in other.cache.iter().rev() {
128            if !self.cache.contains(addr) && self.cache.len() < self.max_size {
129                // push without promoting existing — just insert at LRU end
130                self.cache.push(addr.clone(), ());
131                added += 1;
132            }
133        }
134        added
135    }
136}
137
138impl<A: Address + fmt::Debug> fmt::Debug for AddressBook<A> {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        let addrs: Vec<&A> = self.cache.iter().map(|(a, _)| a).collect();
141        f.debug_struct("AddressBook")
142            .field("addrs", &addrs)
143            .field("max_size", &self.max_size)
144            .finish()
145    }
146}
147
148impl<A: Address> fmt::Display for AddressBook<A> {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        let addrs: Vec<String> = self.iter().map(|a| a.to_string()).collect();
151        write!(f, "[{}] (max {})", addrs.join(", "), self.max_size)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_new() {
161        let page = AddressBook::new("addr1".to_string(), 5);
162        assert_eq!(page.len(), 1);
163        assert_eq!(page.first(), "addr1");
164        assert_eq!(page.max_size(), 5);
165    }
166
167    #[test]
168    fn test_seen_new_address() {
169        let mut page = AddressBook::new("addr1".to_string(), 5);
170        assert!(page.seen("addr2".to_string()));
171        assert_eq!(page.len(), 2);
172        assert_eq!(page.first(), "addr2");
173    }
174
175    #[test]
176    fn test_seen_existing_promotes() {
177        let mut page = AddressBook::new("addr1".to_string(), 5);
178        page.seen("addr2".to_string());
179        // addr2 is at front, addr1 at back
180        assert!(!page.seen("addr1".to_string()));
181        // addr1 promoted to front
182        assert_eq!(page.first(), "addr1");
183    }
184
185    #[test]
186    fn test_eviction_at_capacity() {
187        let mut page = AddressBook::new("addr1".to_string(), 2);
188        page.seen("addr2".to_string());
189        assert_eq!(page.len(), 2);
190        page.seen("addr3".to_string());
191        assert_eq!(page.len(), 2);
192        // addr1 should be evicted (least recently seen)
193        assert!(!page.contains(&"addr1".to_string()));
194        assert!(page.contains(&"addr3".to_string()));
195    }
196
197    #[test]
198    fn test_contains() {
199        let mut page = AddressBook::new("addr1".to_string(), 5);
200        assert!(page.contains(&"addr1".to_string()));
201        assert!(!page.contains(&"addr2".to_string()));
202        page.seen("addr2".to_string());
203        assert!(page.contains(&"addr2".to_string()));
204    }
205
206    #[test]
207    fn test_remove() {
208        let mut page = AddressBook::new("addr1".to_string(), 5);
209        page.seen("addr2".to_string());
210        assert!(page.remove(&"addr1".to_string()));
211        assert!(!page.contains(&"addr1".to_string()));
212        assert!(!page.remove(&"addr1".to_string()));
213    }
214
215    #[test]
216    fn test_remove_last_address_refused() {
217        let mut page = AddressBook::new("addr1".to_string(), 5);
218        assert!(!page.remove(&"addr1".to_string()));
219        assert_eq!(page.len(), 1);
220        assert!(page.contains(&"addr1".to_string()));
221    }
222
223    #[test]
224    fn test_concat() {
225        let mut page1 = AddressBook::new("a".to_string(), 5);
226        page1.seen("b".to_string());
227
228        let mut page2 = AddressBook::new("c".to_string(), 5);
229        page2.seen("d".to_string());
230
231        let added = page1.concat(&page2);
232        assert_eq!(added, 2);
233        assert_eq!(page1.len(), 4);
234    }
235
236    #[test]
237    fn test_concat_respects_capacity() {
238        let mut page1 = AddressBook::new("a".to_string(), 3);
239        page1.seen("b".to_string());
240
241        let mut page2 = AddressBook::new("c".to_string(), 5);
242        page2.seen("d".to_string());
243        page2.seen("e".to_string());
244
245        let added = page1.concat(&page2);
246        assert_eq!(added, 1); // Only room for 1 more
247        assert_eq!(page1.len(), 3);
248    }
249
250    #[test]
251    fn test_into_vec() {
252        let mut page = AddressBook::new("a".to_string(), 5);
253        page.seen("b".to_string());
254        let v = page.into_vec();
255        assert_eq!(v.len(), 2);
256    }
257
258    #[test]
259    fn test_equality() {
260        let mut page1 = AddressBook::new("a".to_string(), 5);
261        page1.seen("b".to_string());
262        let mut page2 = AddressBook::new("a".to_string(), 5);
263        page2.seen("b".to_string());
264        assert_eq!(page1, page2);
265    }
266
267    #[test]
268    fn test_socket_addr_as_address() {
269        use std::net::SocketAddr;
270        let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
271        let page = AddressBook::new(addr, 5);
272        assert_eq!(page.len(), 1);
273    }
274}