use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PeerSource {
Manual,
Mdns,
RestApi,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredPeer {
pub addr: SocketAddr,
pub node_id: Option<String>,
pub last_connected_ms: u64,
pub source: PeerSource,
}
pub struct PeerStore {
path: PathBuf,
peers: Vec<StoredPeer>,
max_peers: usize,
}
impl PeerStore {
pub fn load(data_dir: &std::path::Path, max_peers: usize) -> Self {
let path = data_dir.join("known_peers.json");
let peers = if path.exists() {
match std::fs::read_to_string(&path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
Err(_) => Vec::new(),
}
} else {
Vec::new()
};
Self { path, peers, max_peers }
}
pub fn save(&self) -> Result<(), String> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create peer store dir: {}", e))?;
}
let json = serde_json::to_string_pretty(&self.peers)
.map_err(|e| format!("serialize peers: {}", e))?;
let mut file = std::fs::File::create(&self.path)
.map_err(|e| format!("create peer store: {}", e))?;
std::io::Write::write_all(&mut file, json.as_bytes())
.map_err(|e| format!("write peer store: {}", e))?;
file.sync_all()
.map_err(|e| format!("fsync peer store: {}", e))?;
Ok(())
}
pub fn add(&mut self, peer: StoredPeer) {
if self.peers.iter().any(|p| p.addr == peer.addr) {
return;
}
if self.peers.len() >= self.max_peers {
if let Some(oldest_idx) = self.peers.iter().enumerate()
.min_by_key(|(_, p)| p.last_connected_ms)
.map(|(i, _)| i)
{
self.peers.remove(oldest_idx);
}
}
self.peers.push(peer);
}
pub fn remove(&mut self, addr: &SocketAddr) {
self.peers.retain(|p| p.addr != *addr);
}
pub fn all(&self) -> &[StoredPeer] {
&self.peers
}
pub fn update_last_connected(&mut self, addr: &SocketAddr, ts_ms: u64) {
if let Some(peer) = self.peers.iter_mut().find(|p| p.addr == *addr) {
peer.last_connected_ms = ts_ms;
}
}
pub fn cleanup_stale(&mut self, max_age_ms: u64) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
self.peers.retain(|p| {
p.last_connected_ms == 0 || now_ms.saturating_sub(p.last_connected_ms) < max_age_ms
});
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_store(max_peers: usize) -> PeerStore {
let dir = tempfile::TempDir::new().unwrap();
PeerStore::load(dir.path(), max_peers)
}
fn addr(port: u16) -> SocketAddr {
format!("127.0.0.1:{}", port).parse().unwrap()
}
fn stored_peer(port: u16, ts: u64) -> StoredPeer {
StoredPeer {
addr: addr(port),
node_id: None,
last_connected_ms: ts,
source: PeerSource::Manual,
}
}
#[test]
fn peer_store_empty_on_first_load() {
let store = temp_store(100);
assert!(store.all().is_empty());
}
#[test]
fn peer_store_add_and_save() {
let dir = tempfile::TempDir::new().unwrap();
let mut store = PeerStore::load(dir.path(), 100);
store.add(stored_peer(9000, 1000));
assert_eq!(store.all().len(), 1);
assert!(store.save().is_ok());
assert!(dir.path().join("known_peers.json").exists());
}
#[test]
fn peer_store_load_persisted_data() {
let dir = tempfile::TempDir::new().unwrap();
{
let mut store = PeerStore::load(dir.path(), 100);
store.add(stored_peer(9000, 1000));
store.add(stored_peer(9001, 2000));
store.save().unwrap();
}
let store2 = PeerStore::load(dir.path(), 100);
assert_eq!(store2.all().len(), 2);
}
#[test]
fn peer_store_remove_existing() {
let mut store = temp_store(100);
store.add(stored_peer(9000, 1000));
store.add(stored_peer(9001, 2000));
store.remove(&addr(9000));
assert_eq!(store.all().len(), 1);
assert_eq!(store.all()[0].addr, addr(9001));
}
#[test]
fn peer_store_deduplicates_same_addr() {
let mut store = temp_store(100);
store.add(stored_peer(9000, 1000));
store.add(stored_peer(9000, 2000));
assert_eq!(store.all().len(), 1);
}
#[test]
fn peer_store_cleanup_stale() {
let mut store = temp_store(100);
store.add(stored_peer(9000, 0));
store.add(stored_peer(9001, 1));
store.cleanup_stale(1000); assert_eq!(store.all().len(), 1);
assert_eq!(store.all()[0].addr, addr(9000));
}
#[test]
fn peer_store_max_peers_enforced() {
let mut store = temp_store(2);
store.add(stored_peer(9000, 100));
store.add(stored_peer(9001, 200));
assert_eq!(store.all().len(), 2);
store.add(stored_peer(9002, 300));
assert_eq!(store.all().len(), 2);
assert!(!store.all().iter().any(|p| p.addr == addr(9000)));
}
#[test]
fn peer_store_update_last_connected() {
let mut store = temp_store(100);
store.add(stored_peer(9000, 1000));
store.update_last_connected(&addr(9000), 5000);
assert_eq!(store.all()[0].last_connected_ms, 5000);
}
}