syncweb_core/net/
neighbor_map.rs1use std::sync::Arc;
2use std::time::Duration;
3
4use dashmap::DashMap;
5use iroh::PublicKey;
6use tokio::time::{Instant, sleep};
7
8#[derive(Debug, Clone)]
10#[non_exhaustive]
11pub struct PeerInfo {
12 pub node_id: PublicKey,
13 pub last_seen: Instant,
14 pub name: Option<String>,
15 pub connected: bool,
16}
17
18impl PeerInfo {
19 #[must_use]
20 pub fn new(node_id: PublicKey) -> Self {
21 Self {
22 node_id,
23 last_seen: Instant::now(),
24 name: None,
25 connected: true,
26 }
27 }
28
29 pub fn touch(&mut self) {
30 self.last_seen = Instant::now();
31 }
32}
33
34#[derive(Debug, Clone)]
41pub struct NeighborMap {
42 peers: Arc<DashMap<PublicKey, PeerInfo>>,
43 expiration_timeout: Duration,
44}
45
46impl NeighborMap {
47 #[must_use]
52 pub fn new(expiration_timeout: Duration) -> Self {
53 let map = Self::new_without_cleanup(expiration_timeout);
54 map.spawn_cleanup_task();
55 map
56 }
57
58 #[must_use]
60 pub fn new_without_cleanup(expiration_timeout: Duration) -> Self {
61 Self {
62 peers: Arc::new(DashMap::new()),
63 expiration_timeout,
64 }
65 }
66
67 pub fn touch(&self, node_id: PublicKey, name: Option<&str>) {
69 let mut entry = self.peers.entry(node_id).or_insert_with(|| PeerInfo::new(node_id));
70 entry.touch();
71 if let Some(n) = name {
72 entry.name = Some(n.to_string());
73 }
74 entry.connected = true;
75 }
76
77 pub fn mark_disconnected(&self, node_id: &PublicKey) {
79 if let Some(mut entry) = self.peers.get_mut(node_id) {
80 entry.connected = false;
81 }
82 }
83
84 #[must_use]
86 pub fn remove(&self, node_id: &PublicKey) -> Option<PeerInfo> {
87 self.peers.remove(node_id).map(|(_, info)| info)
88 }
89
90 #[must_use]
92 pub fn list(&self) -> Vec<PeerInfo> {
93 let now = Instant::now();
94 self.peers
95 .iter()
96 .filter(|entry| now.duration_since(entry.last_seen) <= self.expiration_timeout)
97 .map(|entry| entry.value().clone())
98 .collect()
99 }
100
101 #[must_use]
103 pub fn list_connected(&self) -> Vec<PeerInfo> {
104 let now = Instant::now();
105 self.peers
106 .iter()
107 .filter(|entry| entry.connected && now.duration_since(entry.last_seen) <= self.expiration_timeout)
108 .map(|entry| entry.value().clone())
109 .collect()
110 }
111
112 #[must_use]
114 pub const fn inner(&self) -> &Arc<DashMap<PublicKey, PeerInfo>> {
115 &self.peers
116 }
117
118 #[must_use]
119 pub fn len(&self) -> usize {
120 self.peers.len()
121 }
122
123 #[must_use]
124 pub fn is_empty(&self) -> bool {
125 self.peers.is_empty()
126 }
127
128 #[must_use]
132 pub fn cleanup(&self) -> usize {
133 let now = Instant::now();
134 let expired: Vec<PublicKey> = self
135 .peers
136 .iter()
137 .filter_map(|entry| (now.duration_since(entry.last_seen) > self.expiration_timeout).then(|| *entry.key()))
138 .collect();
139
140 let count = expired.len();
141 for key in expired {
142 self.peers.remove(&key);
143 }
144 count
145 }
146
147 fn spawn_cleanup_task(&self) {
148 let peers = Arc::clone(&self.peers);
149 let expiration_timeout = self.expiration_timeout;
150 let cleanup_interval = Duration::from_secs(expiration_timeout.as_secs().saturating_div(3));
151
152 tokio::spawn(async move {
153 loop {
154 sleep(cleanup_interval).await;
155 let now = Instant::now();
156 let expired: Vec<PublicKey> = peers
157 .iter()
158 .filter_map(|entry| {
159 (now.duration_since(entry.last_seen) > expiration_timeout).then(|| *entry.key())
160 })
161 .collect();
162
163 for key in expired {
164 peers.remove(&key);
165 }
166 }
167 });
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use std::time::Duration;
175
176 fn test_key(id: u8) -> PublicKey {
177 let mut seed = [0_u8; 32];
178 seed[0] = id;
179 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
180 let verifying_key = signing_key.verifying_key();
181 PublicKey::from_bytes(&verifying_key.to_bytes()).unwrap()
182 }
183
184 #[test]
185 fn test_insert_and_list() {
186 let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
187 let id = test_key(1);
188
189 map.touch(id, Some("alice"));
190 assert_eq!(map.len(), 1);
191 assert!(!map.is_empty());
192
193 let peers = map.list();
194 assert_eq!(peers.len(), 1);
195 assert_eq!(peers.first().unwrap().name.as_deref(), Some("alice"));
196 }
197
198 #[test]
199 fn test_touch_updates_last_seen() {
200 let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
201 let id = test_key(1);
202
203 map.touch(id, None);
204 let first_seen = map.list().first().unwrap().last_seen;
205
206 std::thread::sleep(Duration::from_millis(10));
208 map.touch(id, None);
209 let second_seen = map.list().first().unwrap().last_seen;
210
211 assert!(second_seen > first_seen);
212 }
213
214 #[test]
215 fn test_remove_explicit() {
216 let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
217 let id = test_key(1);
218 map.touch(id, None);
219
220 let info = map.remove(&id);
221 assert!(info.is_some());
222 assert!(map.is_empty());
223 }
224
225 #[test]
226 fn test_cleanup_removes_expired() {
227 let map = NeighborMap::new_without_cleanup(Duration::from_millis(50));
228 let id = test_key(1);
229
230 map.touch(id, None);
231
232 std::thread::sleep(Duration::from_millis(100));
233
234 let removed = map.cleanup();
235 assert_eq!(removed, 1);
236 assert!(map.is_empty());
237 }
238
239 #[test]
240 fn test_cleanup_preserves_fresh() {
241 let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
242 let alice = test_key(1);
243 let bob = test_key(2);
244
245 map.touch(alice, None);
246 map.touch(bob, None);
247
248 let removed = map.cleanup();
251 assert_eq!(removed, 0);
252 assert_eq!(map.len(), 2);
253 }
254
255 #[test]
256 fn test_mark_disconnected() {
257 let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
258 let id = test_key(1);
259
260 map.touch(id, None);
261 assert!(map.list_connected().len() == 1);
262
263 map.mark_disconnected(&id);
264 assert!(map.list_connected().is_empty());
265 assert_eq!(map.len(), 1); }
267
268 #[test]
269 fn test_empty_map() {
270 let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
271 assert!(map.list().is_empty());
272 assert!(map.is_empty());
273 assert_eq!(map.cleanup(), 0);
274 }
275
276 #[test]
277 fn test_double_touch_resets_timer() {
278 let map = NeighborMap::new_without_cleanup(Duration::from_millis(500));
279 let id = test_key(1);
280
281 map.touch(id, None);
282
283 std::thread::sleep(Duration::from_millis(250));
285
286 map.touch(id, None);
288
289 std::thread::sleep(Duration::from_millis(200));
291
292 assert_eq!(map.cleanup(), 0);
294 assert_eq!(map.len(), 1);
295 }
296
297 #[test]
298 fn test_multiple_peers() {
299 let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
300 let ids: Vec<PublicKey> = (0..5).map(test_key).collect();
301
302 for id in &ids {
303 map.touch(*id, None);
304 }
305
306 assert_eq!(map.len(), 5);
307 assert_eq!(map.list().len(), 5);
308
309 let _ = map.remove(ids.first().unwrap());
310 assert_eq!(map.len(), 4);
311
312 let _ = map.cleanup();
313 assert_eq!(map.len(), 4);
314 }
315}