helia_ipns/
local_store.rs

1//! Local storage for IPNS records with caching and metadata
2
3use crate::errors::IpnsError;
4use crate::record::IpnsRecord;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10/// Metadata associated with a stored IPNS record
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RecordMetadata {
13    /// The key name used to publish this record
14    pub key_name: String,
15
16    /// Lifetime of the record in milliseconds
17    pub lifetime: u64,
18
19    /// When the record was created/stored (Unix timestamp in milliseconds)
20    pub created: u64,
21}
22
23impl RecordMetadata {
24    /// Create new metadata
25    pub fn new(key_name: String, lifetime: u64) -> Self {
26        let created = SystemTime::now()
27            .duration_since(UNIX_EPOCH)
28            .unwrap()
29            .as_millis() as u64;
30
31        Self {
32            key_name,
33            lifetime,
34            created,
35        }
36    }
37
38    /// Get created time as SystemTime
39    pub fn created_time(&self) -> SystemTime {
40        UNIX_EPOCH + std::time::Duration::from_millis(self.created)
41    }
42
43    /// Check if the record should be republished based on DHT expiry or record expiry
44    pub fn should_republish(&self, dht_expiry_ms: u64, republish_threshold_ms: u64) -> bool {
45        let now = SystemTime::now()
46            .duration_since(UNIX_EPOCH)
47            .unwrap()
48            .as_millis() as u64;
49
50        let dht_expiry = self.created + dht_expiry_ms;
51        let record_expiry = self.created + self.lifetime;
52
53        // If DHT expiry is within threshold, republish
54        if dht_expiry.saturating_sub(now) <= republish_threshold_ms {
55            return true;
56        }
57
58        // If record expiry is within threshold, republish
59        if record_expiry.saturating_sub(now) <= republish_threshold_ms {
60            return true;
61        }
62
63        false
64    }
65}
66
67/// Stored record with metadata
68#[derive(Debug, Clone)]
69pub struct StoredRecord {
70    /// The marshaled IPNS record
71    pub record: Vec<u8>,
72
73    /// Metadata about the record
74    pub metadata: Option<RecordMetadata>,
75
76    /// When this record was stored locally (Unix timestamp in milliseconds)
77    pub created: u64,
78}
79
80/// Local store for IPNS records
81///
82/// Provides caching with TTL tracking and metadata storage
83#[derive(Debug, Clone)]
84pub struct LocalStore {
85    records: Arc<RwLock<HashMap<Vec<u8>, StoredRecord>>>,
86}
87
88impl LocalStore {
89    /// Create a new local store
90    pub fn new() -> Self {
91        Self {
92            records: Arc::new(RwLock::new(HashMap::new())),
93        }
94    }
95
96    /// Store an IPNS record
97    pub fn put(
98        &self,
99        routing_key: &[u8],
100        record: Vec<u8>,
101        metadata: Option<RecordMetadata>,
102    ) -> Result<(), IpnsError> {
103        let created = SystemTime::now()
104            .duration_since(UNIX_EPOCH)
105            .unwrap()
106            .as_millis() as u64;
107
108        let stored = StoredRecord {
109            record,
110            metadata,
111            created,
112        };
113
114        let mut records = self.records.write().unwrap();
115        records.insert(routing_key.to_vec(), stored);
116
117        tracing::debug!(
118            "Stored IPNS record for routing key: {}",
119            bs58::encode(routing_key).into_string()
120        );
121
122        Ok(())
123    }
124
125    /// Get an IPNS record
126    pub fn get(&self, routing_key: &[u8]) -> Result<StoredRecord, IpnsError> {
127        let records = self.records.read().unwrap();
128
129        records.get(routing_key).cloned().ok_or_else(|| {
130            IpnsError::NotFound(format!(
131                "No record found for routing key: {}",
132                bs58::encode(routing_key).into_string()
133            ))
134        })
135    }
136
137    /// Check if a record exists
138    pub fn has(&self, routing_key: &[u8]) -> bool {
139        let records = self.records.read().unwrap();
140        records.contains_key(routing_key)
141    }
142
143    /// Delete a record
144    pub fn delete(&self, routing_key: &[u8]) -> Result<(), IpnsError> {
145        let mut records = self.records.write().unwrap();
146
147        if records.remove(routing_key).is_some() {
148            tracing::debug!(
149                "Deleted IPNS record for routing key: {}",
150                bs58::encode(routing_key).into_string()
151            );
152            Ok(())
153        } else {
154            Err(IpnsError::NotFound(format!(
155                "No record found for routing key: {}",
156                bs58::encode(routing_key).into_string()
157            )))
158        }
159    }
160
161    /// List all stored records (for republishing)
162    pub fn list(&self) -> Vec<(Vec<u8>, StoredRecord)> {
163        let records = self.records.read().unwrap();
164        records
165            .iter()
166            .map(|(k, v)| (k.clone(), v.clone()))
167            .collect()
168    }
169
170    /// Clear all records
171    pub fn clear(&self) {
172        let mut records = self.records.write().unwrap();
173        records.clear();
174        tracing::debug!("Cleared all IPNS records from local store");
175    }
176
177    /// Get the number of stored records
178    pub fn len(&self) -> usize {
179        let records = self.records.read().unwrap();
180        records.len()
181    }
182
183    /// Check if the store is empty
184    pub fn is_empty(&self) -> bool {
185        self.len() == 0
186    }
187}
188
189impl Default for LocalStore {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_local_store_operations() {
201        let store = LocalStore::new();
202        let routing_key = b"test-key";
203        let record = b"test-record".to_vec();
204
205        // Initially empty
206        assert!(store.is_empty());
207        assert!(!store.has(routing_key));
208
209        // Put a record
210        let metadata = RecordMetadata::new("my-key".to_string(), 48 * 60 * 60 * 1000);
211        store
212            .put(routing_key, record.clone(), Some(metadata.clone()))
213            .unwrap();
214
215        // Should now have the record
216        assert!(!store.is_empty());
217        assert!(store.has(routing_key));
218        assert_eq!(store.len(), 1);
219
220        // Get the record
221        let stored = store.get(routing_key).unwrap();
222        assert_eq!(stored.record, record);
223        assert!(stored.metadata.is_some());
224        assert_eq!(stored.metadata.unwrap().key_name, "my-key");
225
226        // Delete the record
227        store.delete(routing_key).unwrap();
228        assert!(store.is_empty());
229        assert!(!store.has(routing_key));
230    }
231
232    #[test]
233    fn test_should_republish() {
234        let metadata = RecordMetadata {
235            key_name: "test".to_string(),
236            lifetime: 48 * 60 * 60 * 1000, // 48 hours
237            created: SystemTime::now()
238                .duration_since(UNIX_EPOCH)
239                .unwrap()
240                .as_millis() as u64
241                - (20 * 60 * 60 * 1000), // Created 20 hours ago
242        };
243
244        let dht_expiry_ms = 24 * 60 * 60 * 1000; // 24 hours
245        let threshold_ms = 4 * 60 * 60 * 1000; // 4 hours
246
247        // Should need republishing (DHT will expire in 4 hours)
248        assert!(metadata.should_republish(dht_expiry_ms, threshold_ms));
249    }
250}