use crate::PeerId;
use crate::adaptive::trust::TrustRecord;
use crate::address::MultiAddr;
use serde::{Deserialize, Serialize};
use std::io::Write as _;
use std::path::Path;
const CACHE_FILENAME: &str = "close_group_cache.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedCloseGroupPeer {
pub peer_id: PeerId,
pub addresses: Vec<MultiAddr>,
pub trust: TrustRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloseGroupCache {
pub peers: Vec<CachedCloseGroupPeer>,
pub saved_at_epoch_secs: u64,
}
impl CloseGroupCache {
pub async fn save_to_dir(&self, dir: &Path) -> anyhow::Result<()> {
tokio::fs::create_dir_all(dir).await.map_err(|e| {
anyhow::anyhow!(
"failed to create close group cache directory {}: {e}",
dir.display()
)
})?;
let path = dir.join(CACHE_FILENAME);
let json = serde_json::to_string_pretty(self)
.map_err(|e| anyhow::anyhow!("failed to serialize close group cache: {e}"))?;
let dir_owned = dir.to_path_buf();
tokio::task::spawn_blocking(move || {
let mut tmp = tempfile::NamedTempFile::new_in(&dir_owned).map_err(|e| {
anyhow::anyhow!("failed to create temp file in {}: {e}", dir_owned.display())
})?;
tmp.write_all(json.as_bytes())
.map_err(|e| anyhow::anyhow!("failed to write close group cache: {e}"))?;
tmp.persist(&path).map_err(|e| {
anyhow::anyhow!(
"failed to persist close group cache to {}: {e}",
path.display()
)
})?;
Ok(())
})
.await
.map_err(|e| anyhow::anyhow!("close group cache save task panicked: {e}"))?
}
pub async fn load_from_dir(dir: &Path) -> anyhow::Result<Option<Self>> {
let path = dir.join(CACHE_FILENAME);
match tokio::fs::read_to_string(&path).await {
Ok(json) => {
let cache: Self = serde_json::from_str(&json)
.map_err(|e| anyhow::anyhow!("failed to deserialize close group cache: {e}"))?;
Ok(Some(cache))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(anyhow::anyhow!(
"failed to read close group cache from {}: {e}",
path.display()
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adaptive::trust::TrustRecord;
#[tokio::test]
async fn test_save_load_roundtrip() {
let cache = CloseGroupCache {
peers: vec![
CachedCloseGroupPeer {
peer_id: PeerId::random(),
addresses: vec!["/ip4/10.0.1.1/udp/9000/quic".parse().unwrap()],
trust: TrustRecord {
score: 0.8,
last_updated_epoch_secs: 1_234_567_890,
},
},
CachedCloseGroupPeer {
peer_id: PeerId::random(),
addresses: vec!["/ip4/10.0.2.1/udp/9000/quic".parse().unwrap()],
trust: TrustRecord {
score: 0.6,
last_updated_epoch_secs: 1_234_567_890,
},
},
],
saved_at_epoch_secs: 1_234_567_890,
};
let dir = tempfile::tempdir().unwrap();
cache.save_to_dir(dir.path()).await.unwrap();
let loaded = CloseGroupCache::load_from_dir(dir.path())
.await
.unwrap()
.unwrap();
assert_eq!(loaded.peers.len(), 2);
assert_eq!(loaded.peers[0].peer_id, cache.peers[0].peer_id);
assert!((loaded.peers[0].trust.score - 0.8).abs() < f64::EPSILON);
assert_eq!(loaded.saved_at_epoch_secs, 1_234_567_890);
}
#[tokio::test]
async fn test_load_nonexistent_returns_none() {
let dir = tempfile::tempdir().unwrap();
let result = CloseGroupCache::load_from_dir(dir.path()).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_empty_cache() {
let cache = CloseGroupCache {
peers: vec![],
saved_at_epoch_secs: 0,
};
let dir = tempfile::tempdir().unwrap();
cache.save_to_dir(dir.path()).await.unwrap();
let loaded = CloseGroupCache::load_from_dir(dir.path())
.await
.unwrap()
.unwrap();
assert!(loaded.peers.is_empty());
}
}