hive_btle/
persistence.rs

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