saorsa_core/bootstrap/
cache.rs1use crate::PeerId;
21use crate::adaptive::trust::TrustRecord;
22use crate::address::MultiAddr;
23use serde::{Deserialize, Serialize};
24use std::io::Write as _;
25use std::path::Path;
26
27const CACHE_FILENAME: &str = "close_group_cache.json";
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct CachedCloseGroupPeer {
33 pub peer_id: PeerId,
35 pub addresses: Vec<MultiAddr>,
37 pub trust: TrustRecord,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct CloseGroupCache {
48 pub peers: Vec<CachedCloseGroupPeer>,
50 pub saved_at_epoch_secs: u64,
52}
53
54impl CloseGroupCache {
55 pub async fn save_to_dir(&self, dir: &Path) -> anyhow::Result<()> {
61 tokio::fs::create_dir_all(dir).await.map_err(|e| {
63 anyhow::anyhow!(
64 "failed to create close group cache directory {}: {e}",
65 dir.display()
66 )
67 })?;
68
69 let path = dir.join(CACHE_FILENAME);
70 let json = serde_json::to_string_pretty(self)
71 .map_err(|e| anyhow::anyhow!("failed to serialize close group cache: {e}"))?;
72
73 let dir_owned = dir.to_path_buf();
75 tokio::task::spawn_blocking(move || {
76 let mut tmp = tempfile::NamedTempFile::new_in(&dir_owned).map_err(|e| {
77 anyhow::anyhow!("failed to create temp file in {}: {e}", dir_owned.display())
78 })?;
79 tmp.write_all(json.as_bytes())
80 .map_err(|e| anyhow::anyhow!("failed to write close group cache: {e}"))?;
81 tmp.persist(&path).map_err(|e| {
82 anyhow::anyhow!(
83 "failed to persist close group cache to {}: {e}",
84 path.display()
85 )
86 })?;
87 Ok(())
88 })
89 .await
90 .map_err(|e| anyhow::anyhow!("close group cache save task panicked: {e}"))?
91 }
92
93 pub async fn load_from_dir(dir: &Path) -> anyhow::Result<Option<Self>> {
97 let path = dir.join(CACHE_FILENAME);
98 match tokio::fs::read_to_string(&path).await {
99 Ok(json) => {
100 let cache: Self = serde_json::from_str(&json)
101 .map_err(|e| anyhow::anyhow!("failed to deserialize close group cache: {e}"))?;
102 Ok(Some(cache))
103 }
104 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
105 Err(e) => Err(anyhow::anyhow!(
106 "failed to read close group cache from {}: {e}",
107 path.display()
108 )),
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::adaptive::trust::TrustRecord;
117
118 #[tokio::test]
119 async fn test_save_load_roundtrip() {
120 let cache = CloseGroupCache {
121 peers: vec![
122 CachedCloseGroupPeer {
123 peer_id: PeerId::random(),
124 addresses: vec!["/ip4/10.0.1.1/udp/9000/quic".parse().unwrap()],
125 trust: TrustRecord {
126 score: 0.8,
127 last_updated_epoch_secs: 1_234_567_890,
128 },
129 },
130 CachedCloseGroupPeer {
131 peer_id: PeerId::random(),
132 addresses: vec!["/ip4/10.0.2.1/udp/9000/quic".parse().unwrap()],
133 trust: TrustRecord {
134 score: 0.6,
135 last_updated_epoch_secs: 1_234_567_890,
136 },
137 },
138 ],
139 saved_at_epoch_secs: 1_234_567_890,
140 };
141
142 let dir = tempfile::tempdir().unwrap();
143
144 cache.save_to_dir(dir.path()).await.unwrap();
145 let loaded = CloseGroupCache::load_from_dir(dir.path())
146 .await
147 .unwrap()
148 .unwrap();
149
150 assert_eq!(loaded.peers.len(), 2);
151 assert_eq!(loaded.peers[0].peer_id, cache.peers[0].peer_id);
152 assert!((loaded.peers[0].trust.score - 0.8).abs() < f64::EPSILON);
153 assert_eq!(loaded.saved_at_epoch_secs, 1_234_567_890);
154 }
155
156 #[tokio::test]
157 async fn test_load_nonexistent_returns_none() {
158 let dir = tempfile::tempdir().unwrap();
159 let result = CloseGroupCache::load_from_dir(dir.path()).await.unwrap();
160 assert!(result.is_none());
161 }
162
163 #[tokio::test]
164 async fn test_empty_cache() {
165 let cache = CloseGroupCache {
166 peers: vec![],
167 saved_at_epoch_secs: 0,
168 };
169
170 let dir = tempfile::tempdir().unwrap();
171
172 cache.save_to_dir(dir.path()).await.unwrap();
173 let loaded = CloseGroupCache::load_from_dir(dir.path())
174 .await
175 .unwrap()
176 .unwrap();
177 assert!(loaded.peers.is_empty());
178 }
179}