use std::sync::Arc;
use std::time::Duration;
use dashmap::DashMap;
use iroh::PublicKey;
use tokio::time::{Instant, sleep};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PeerInfo {
pub node_id: PublicKey,
pub last_seen: Instant,
pub name: Option<String>,
pub connected: bool,
}
impl PeerInfo {
#[must_use]
pub fn new(node_id: PublicKey) -> Self {
Self {
node_id,
last_seen: Instant::now(),
name: None,
connected: true,
}
}
pub fn touch(&mut self) {
self.last_seen = Instant::now();
}
}
#[derive(Debug, Clone)]
pub struct NeighborMap {
peers: Arc<DashMap<PublicKey, PeerInfo>>,
expiration_timeout: Duration,
}
impl NeighborMap {
#[must_use]
pub fn new(expiration_timeout: Duration) -> Self {
let map = Self::new_without_cleanup(expiration_timeout);
map.spawn_cleanup_task();
map
}
#[must_use]
pub fn new_without_cleanup(expiration_timeout: Duration) -> Self {
Self {
peers: Arc::new(DashMap::new()),
expiration_timeout,
}
}
pub fn touch(&self, node_id: PublicKey, name: Option<&str>) {
let mut entry = self.peers.entry(node_id).or_insert_with(|| PeerInfo::new(node_id));
entry.touch();
if let Some(n) = name {
entry.name = Some(n.to_string());
}
entry.connected = true;
}
pub fn mark_disconnected(&self, node_id: &PublicKey) {
if let Some(mut entry) = self.peers.get_mut(node_id) {
entry.connected = false;
}
}
#[must_use]
pub fn remove(&self, node_id: &PublicKey) -> Option<PeerInfo> {
self.peers.remove(node_id).map(|(_, info)| info)
}
#[must_use]
pub fn list(&self) -> Vec<PeerInfo> {
let now = Instant::now();
self.peers
.iter()
.filter(|entry| now.duration_since(entry.last_seen) <= self.expiration_timeout)
.map(|entry| entry.value().clone())
.collect()
}
#[must_use]
pub fn list_connected(&self) -> Vec<PeerInfo> {
let now = Instant::now();
self.peers
.iter()
.filter(|entry| entry.connected && now.duration_since(entry.last_seen) <= self.expiration_timeout)
.map(|entry| entry.value().clone())
.collect()
}
#[must_use]
pub const fn inner(&self) -> &Arc<DashMap<PublicKey, PeerInfo>> {
&self.peers
}
#[must_use]
pub fn len(&self) -> usize {
self.peers.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.peers.is_empty()
}
#[must_use]
pub fn cleanup(&self) -> usize {
let now = Instant::now();
let expired: Vec<PublicKey> = self
.peers
.iter()
.filter_map(|entry| (now.duration_since(entry.last_seen) > self.expiration_timeout).then(|| *entry.key()))
.collect();
let count = expired.len();
for key in expired {
self.peers.remove(&key);
}
count
}
fn spawn_cleanup_task(&self) {
let peers = Arc::clone(&self.peers);
let expiration_timeout = self.expiration_timeout;
let cleanup_interval = Duration::from_secs(expiration_timeout.as_secs().saturating_div(3));
tokio::spawn(async move {
loop {
sleep(cleanup_interval).await;
let now = Instant::now();
let expired: Vec<PublicKey> = peers
.iter()
.filter_map(|entry| {
(now.duration_since(entry.last_seen) > expiration_timeout).then(|| *entry.key())
})
.collect();
for key in expired {
peers.remove(&key);
}
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn test_key(id: u8) -> PublicKey {
let mut seed = [0_u8; 32];
seed[0] = id;
let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
let verifying_key = signing_key.verifying_key();
PublicKey::from_bytes(&verifying_key.to_bytes()).unwrap()
}
#[test]
fn test_insert_and_list() {
let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
let id = test_key(1);
map.touch(id, Some("alice"));
assert_eq!(map.len(), 1);
assert!(!map.is_empty());
let peers = map.list();
assert_eq!(peers.len(), 1);
assert_eq!(peers.first().unwrap().name.as_deref(), Some("alice"));
}
#[test]
fn test_touch_updates_last_seen() {
let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
let id = test_key(1);
map.touch(id, None);
let first_seen = map.list().first().unwrap().last_seen;
std::thread::sleep(Duration::from_millis(10));
map.touch(id, None);
let second_seen = map.list().first().unwrap().last_seen;
assert!(second_seen > first_seen);
}
#[test]
fn test_remove_explicit() {
let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
let id = test_key(1);
map.touch(id, None);
let info = map.remove(&id);
assert!(info.is_some());
assert!(map.is_empty());
}
#[test]
fn test_cleanup_removes_expired() {
let map = NeighborMap::new_without_cleanup(Duration::from_millis(50));
let id = test_key(1);
map.touch(id, None);
std::thread::sleep(Duration::from_millis(100));
let removed = map.cleanup();
assert_eq!(removed, 1);
assert!(map.is_empty());
}
#[test]
fn test_cleanup_preserves_fresh() {
let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
let alice = test_key(1);
let bob = test_key(2);
map.touch(alice, None);
map.touch(bob, None);
let removed = map.cleanup();
assert_eq!(removed, 0);
assert_eq!(map.len(), 2);
}
#[test]
fn test_mark_disconnected() {
let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
let id = test_key(1);
map.touch(id, None);
assert!(map.list_connected().len() == 1);
map.mark_disconnected(&id);
assert!(map.list_connected().is_empty());
assert_eq!(map.len(), 1); }
#[test]
fn test_empty_map() {
let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
assert!(map.list().is_empty());
assert!(map.is_empty());
assert_eq!(map.cleanup(), 0);
}
#[test]
fn test_double_touch_resets_timer() {
let map = NeighborMap::new_without_cleanup(Duration::from_millis(500));
let id = test_key(1);
map.touch(id, None);
std::thread::sleep(Duration::from_millis(250));
map.touch(id, None);
std::thread::sleep(Duration::from_millis(200));
assert_eq!(map.cleanup(), 0);
assert_eq!(map.len(), 1);
}
#[test]
fn test_multiple_peers() {
let map = NeighborMap::new_without_cleanup(Duration::from_mins(1));
let ids: Vec<PublicKey> = (0..5).map(test_key).collect();
for id in &ids {
map.touch(*id, None);
}
assert_eq!(map.len(), 5);
assert_eq!(map.list().len(), 5);
let _ = map.remove(ids.first().unwrap());
assert_eq!(map.len(), 4);
let _ = map.cleanup();
assert_eq!(map.len(), 4);
}
}