hive_btle/
persistence.rs

1//! Persistence abstraction for HIVE documents
2//!
3//! This module provides traits for persisting HIVE mesh state across restarts.
4//! Platform implementations can use platform-specific storage backends:
5//!
6//! - **ESP32**: NVS (Non-Volatile Storage)
7//! - **iOS/macOS**: Keychain or UserDefaults
8//! - **Android**: SharedPreferences or EncryptedSharedPreferences
9//! - **Linux**: File-based or SQLite
10//!
11//! ## Usage
12//!
13//! ```rust,no_run
14//! use hive_btle::persistence::{DocumentStore, MemoryStore};
15//! use hive_btle::document::HiveDocument;
16//! use hive_btle::NodeId;
17//!
18//! // Use the in-memory store for testing
19//! let mut store = MemoryStore::new();
20//!
21//! // Save a document
22//! let doc = HiveDocument::new(NodeId::new(0x12345678));
23//! store.save(&doc).unwrap();
24//!
25//! // Load it back
26//! let loaded = store.load().unwrap();
27//! assert!(loaded.is_some());
28//! ```
29
30use crate::document::HiveDocument;
31use crate::error::Result;
32
33#[cfg(feature = "std")]
34use std::sync::{Arc, RwLock};
35
36/// Trait for persisting HIVE documents
37///
38/// Implementations of this trait provide durable storage for mesh state,
39/// allowing nodes to recover their document after restarts.
40///
41/// ## Implementation Notes
42///
43/// - `save()` should be called after significant state changes (new peers, emergencies)
44/// - `load()` should be called during mesh initialization
45/// - Implementations should handle concurrent access safely
46/// - Consider encryption for sensitive deployment scenarios
47pub trait DocumentStore: Send + Sync {
48    /// Save the current document to persistent storage
49    ///
50    /// This should serialize the document and write it to durable storage.
51    /// Implementations should handle errors gracefully and return appropriate
52    /// error types.
53    fn save(&mut self, doc: &HiveDocument) -> Result<()>;
54
55    /// Load a previously saved document
56    ///
57    /// Returns `Ok(Some(doc))` if a document was found, `Ok(None)` if no
58    /// document exists (first run), or `Err` if loading failed.
59    fn load(&self) -> Result<Option<HiveDocument>>;
60
61    /// Clear any stored document
62    ///
63    /// Use this for factory reset or when leaving a mesh.
64    fn clear(&mut self) -> Result<()>;
65
66    /// Check if a document is stored
67    fn has_document(&self) -> bool {
68        self.load().ok().flatten().is_some()
69    }
70}
71
72/// In-memory document store for testing
73///
74/// This store keeps the document in memory only - it will be lost on restart.
75/// Useful for unit tests and development.
76#[cfg(feature = "std")]
77#[derive(Default)]
78pub struct MemoryStore {
79    document: RwLock<Option<HiveDocument>>,
80}
81
82#[cfg(feature = "std")]
83impl MemoryStore {
84    /// Create a new empty memory store
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Create a memory store pre-populated with a document
90    pub fn with_document(doc: HiveDocument) -> Self {
91        Self {
92            document: RwLock::new(Some(doc)),
93        }
94    }
95}
96
97#[cfg(feature = "std")]
98impl DocumentStore for MemoryStore {
99    fn save(&mut self, doc: &HiveDocument) -> Result<()> {
100        let mut stored = self.document.write().unwrap();
101        *stored = Some(doc.clone());
102        Ok(())
103    }
104
105    fn load(&self) -> Result<Option<HiveDocument>> {
106        let stored = self.document.read().unwrap();
107        Ok(stored.clone())
108    }
109
110    fn clear(&mut self) -> Result<()> {
111        let mut stored = self.document.write().unwrap();
112        *stored = None;
113        Ok(())
114    }
115}
116
117/// File-based document store
118///
119/// Stores the document as a binary file on the filesystem.
120/// Suitable for Linux desktop/server deployments.
121#[cfg(feature = "std")]
122pub struct FileStore {
123    path: std::path::PathBuf,
124}
125
126#[cfg(feature = "std")]
127impl FileStore {
128    /// Create a new file store at the given path
129    pub fn new<P: Into<std::path::PathBuf>>(path: P) -> Self {
130        Self { path: path.into() }
131    }
132
133    /// Get the storage path
134    pub fn path(&self) -> &std::path::Path {
135        &self.path
136    }
137}
138
139#[cfg(feature = "std")]
140impl DocumentStore for FileStore {
141    fn save(&mut self, doc: &HiveDocument) -> Result<()> {
142        let data = doc.encode();
143        std::fs::write(&self.path, data).map_err(|e| {
144            crate::error::BleError::NotSupported(format!("Failed to write document: {}", e))
145        })?;
146        Ok(())
147    }
148
149    fn load(&self) -> Result<Option<HiveDocument>> {
150        match std::fs::read(&self.path) {
151            Ok(data) => Ok(HiveDocument::decode(&data)),
152            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
153            Err(e) => Err(crate::error::BleError::NotSupported(format!(
154                "Failed to read document: {}",
155                e
156            ))),
157        }
158    }
159
160    fn clear(&mut self) -> Result<()> {
161        match std::fs::remove_file(&self.path) {
162            Ok(()) => Ok(()),
163            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
164            Err(e) => Err(crate::error::BleError::NotSupported(format!(
165                "Failed to clear document: {}",
166                e
167            ))),
168        }
169    }
170}
171
172/// Wrapper to make a DocumentStore shareable across threads
173#[cfg(feature = "std")]
174pub struct SharedStore<S: DocumentStore> {
175    inner: Arc<RwLock<S>>,
176}
177
178#[cfg(feature = "std")]
179impl<S: DocumentStore> SharedStore<S> {
180    /// Wrap a store for shared access
181    pub fn new(store: S) -> Self {
182        Self {
183            inner: Arc::new(RwLock::new(store)),
184        }
185    }
186}
187
188#[cfg(feature = "std")]
189impl<S: DocumentStore> Clone for SharedStore<S> {
190    fn clone(&self) -> Self {
191        Self {
192            inner: self.inner.clone(),
193        }
194    }
195}
196
197#[cfg(feature = "std")]
198impl<S: DocumentStore> DocumentStore for SharedStore<S> {
199    fn save(&mut self, doc: &HiveDocument) -> Result<()> {
200        let mut inner = self.inner.write().unwrap();
201        inner.save(doc)
202    }
203
204    fn load(&self) -> Result<Option<HiveDocument>> {
205        let inner = self.inner.read().unwrap();
206        inner.load()
207    }
208
209    fn clear(&mut self) -> Result<()> {
210        let mut inner = self.inner.write().unwrap();
211        inner.clear()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::NodeId;
219
220    #[test]
221    fn test_memory_store() {
222        let mut store = MemoryStore::new();
223
224        // Initially empty
225        assert!(store.load().unwrap().is_none());
226        assert!(!store.has_document());
227
228        // Save a document
229        let doc = HiveDocument::new(NodeId::new(0x12345678));
230        store.save(&doc).unwrap();
231
232        // Load it back
233        let loaded = store.load().unwrap().unwrap();
234        assert_eq!(loaded.node_id.as_u32(), 0x12345678);
235        assert!(store.has_document());
236
237        // Clear it
238        store.clear().unwrap();
239        assert!(store.load().unwrap().is_none());
240    }
241
242    #[test]
243    fn test_file_store() {
244        let temp_dir = std::env::temp_dir();
245        let path = temp_dir.join("hive_test_doc.bin");
246
247        // Clean up from any previous test
248        let _ = std::fs::remove_file(&path);
249
250        let mut store = FileStore::new(&path);
251
252        // Initially empty
253        assert!(store.load().unwrap().is_none());
254
255        // Save a document
256        let mut doc = HiveDocument::new(NodeId::new(0xAABBCCDD));
257        doc.increment_counter();
258        store.save(&doc).unwrap();
259
260        // Load it back
261        let loaded = store.load().unwrap().unwrap();
262        assert_eq!(loaded.node_id.as_u32(), 0xAABBCCDD);
263        assert_eq!(loaded.counter.value(), 1);
264
265        // Clear it
266        store.clear().unwrap();
267        assert!(store.load().unwrap().is_none());
268    }
269
270    #[test]
271    fn test_shared_store() {
272        let store = MemoryStore::new();
273        let mut shared = SharedStore::new(store);
274
275        let doc = HiveDocument::new(NodeId::new(0x11111111));
276        shared.save(&doc).unwrap();
277
278        // Clone and read from the clone
279        let shared2 = shared.clone();
280        let loaded = shared2.load().unwrap().unwrap();
281        assert_eq!(loaded.node_id.as_u32(), 0x11111111);
282    }
283}