gun/
storage.rs

1//! Pluggable storage backends for persistent data
2//!
3//! This module provides storage abstractions for Gun, allowing data to be persisted
4//! to disk or other backends. Multiple storage implementations are provided:
5//!
6//! - **MemoryStorage**: In-memory only (no persistence)
7//! - **LocalStorage**: File-based storage (similar to browser localStorage)
8//! - **SledStorage**: High-performance embedded database
9//!
10//! Based on Gun.js storage adapters (localStorage, RAD, S3, etc.). All storage
11//! backends implement the [`Storage`](Storage) trait for a uniform interface.
12
13use crate::error::{GunError, GunResult};
14use crate::state::Node;
15use async_trait::async_trait;
16use parking_lot::RwLock;
17use std::collections::{HashMap, HashSet};
18use std::fs;
19use std::io::{Read, Write};
20use std::path::PathBuf;
21
22/// Storage backend trait for persistent data storage
23///
24/// All storage backends in Gun implement this trait. It provides a simple interface
25/// for storing and retrieving nodes by their soul (unique identifier).
26///
27/// Based on Gun.js storage adapters. The trait is async to support I/O operations
28/// and is `Send + Sync` to work across threads.
29///
30/// # Example
31///
32/// ```rust,no_run
33/// use gun::storage::{Storage, LocalStorage};
34/// use gun::state::Node;
35/// use std::sync::Arc;
36///
37/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
38/// let storage: Arc<dyn Storage> = Arc::new(LocalStorage::new("./gun_data")?);
39///
40/// let node = Node::with_soul("user_123".to_string());
41/// storage.put("user_123", &node).await?;
42///
43/// if let Some(loaded_node) = storage.get("user_123").await? {
44///     println!("Loaded node: {:?}", loaded_node);
45/// }
46/// # Ok(())
47/// # }
48/// ```
49#[async_trait]
50pub trait Storage: Send + Sync {
51    /// Retrieve a node by its soul (unique identifier)
52    ///
53    /// # Arguments
54    /// * `soul` - The unique identifier of the node to retrieve
55    ///
56    /// # Returns
57    /// `Ok(Some(node))` if found, `Ok(None)` if not found, or `GunError` on failure.
58    async fn get(&self, soul: &str) -> GunResult<Option<Node>>;
59
60    /// Store a node by its soul (unique identifier)
61    ///
62    /// # Arguments
63    /// * `soul` - The unique identifier for the node
64    /// * `node` - The node to store
65    ///
66    /// # Returns
67    /// `Ok(())` on success, or `GunError` on failure.
68    async fn put(&self, soul: &str, node: &Node) -> GunResult<()>;
69
70    /// Check if a node exists in storage
71    ///
72    /// # Arguments
73    /// * `soul` - The unique identifier to check
74    ///
75    /// # Returns
76    /// `Ok(true)` if the node exists, `Ok(false)` if not, or `GunError` on failure.
77    async fn has(&self, soul: &str) -> GunResult<bool>;
78}
79
80/// In-memory storage backend (no persistence)
81///
82/// Stores data in a `HashMap` in memory. Data is lost when the instance is dropped.
83/// This is useful for:
84/// - Testing
85/// - Temporary data
86/// - Performance-critical scenarios where persistence isn't needed
87///
88/// # Thread Safety
89///
90/// `MemoryStorage` is thread-safe and can be shared across threads using `Arc<MemoryStorage>`.
91///
92/// # Example
93///
94/// ```rust,no_run
95/// use gun::storage::{Storage, MemoryStorage};
96/// use gun::state::Node;
97/// use std::sync::Arc;
98///
99/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
100/// let storage = Arc::new(MemoryStorage::new());
101/// let node = Node::with_soul("user_123".to_string());
102/// storage.put("user_123", &node).await?;
103/// # Ok(())
104/// # }
105/// ```
106pub struct MemoryStorage {
107    data: RwLock<HashMap<String, Node>>,
108}
109
110impl MemoryStorage {
111    pub fn new() -> Self {
112        Self {
113            data: RwLock::new(HashMap::new()),
114        }
115    }
116}
117
118#[async_trait]
119impl Storage for MemoryStorage {
120    async fn get(&self, soul: &str) -> GunResult<Option<Node>> {
121        let data = self.data.read();
122        Ok(data.get(soul).cloned())
123    }
124
125    async fn put(&self, soul: &str, node: &Node) -> GunResult<()> {
126        let mut data = self.data.write();
127        data.insert(soul.to_string(), node.clone());
128        Ok(())
129    }
130
131    async fn has(&self, soul: &str) -> GunResult<bool> {
132        let data = self.data.read();
133        Ok(data.contains_key(soul))
134    }
135}
136
137impl Default for MemoryStorage {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143/// Sled-based persistent storage backend
144///
145/// Uses the [sled](https://docs.rs/sled) embedded database for high-performance,
146/// persistent storage. This is recommended for:
147/// - Large datasets
148/// - High write throughput
149/// - Production applications
150///
151/// Sled provides:
152/// - ACID transactions
153/// - High performance
154/// - Automatic crash recovery
155/// - Efficient storage format
156///
157/// # Thread Safety
158///
159/// `SledStorage` is thread-safe and can be shared across threads using `Arc<SledStorage>`.
160///
161/// # Example
162///
163/// ```rust,no_run
164/// use gun::storage::{Storage, SledStorage};
165/// use gun::state::Node;
166/// use std::sync::Arc;
167///
168/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
169/// let storage = Arc::new(SledStorage::new("./gun_data")?);
170/// let node = Node::with_soul("user_123".to_string());
171/// storage.put("user_123", &node).await?;
172/// # Ok(())
173/// # }
174/// ```
175pub struct SledStorage {
176    db: sled::Db,
177}
178
179impl SledStorage {
180    /// Create a new SledStorage instance
181    ///
182    /// # Arguments
183    /// * `path` - Directory path where the sled database will be stored
184    ///
185    /// # Returns
186    /// `Ok(SledStorage)` if initialization succeeds, or `GunError` on failure.
187    ///
188    /// # Errors
189    /// Returns `GunError::Storage` if the sled database cannot be opened or created.
190    pub fn new(path: &str) -> GunResult<Self> {
191        let db = sled::open(path)?;
192        Ok(Self { db })
193    }
194}
195
196#[async_trait]
197impl Storage for SledStorage {
198    async fn get(&self, soul: &str) -> GunResult<Option<Node>> {
199        match self.db.get(soul)? {
200            Some(ivec) => {
201                let json_str = String::from_utf8(ivec.to_vec())
202                    .map_err(|e| GunError::InvalidData(format!("Invalid UTF-8: {}", e)))?;
203                let node: Node = serde_json::from_str(&json_str)?;
204                Ok(Some(node))
205            }
206            None => Ok(None),
207        }
208    }
209
210    async fn put(&self, soul: &str, node: &Node) -> GunResult<()> {
211        let json_str = serde_json::to_string(node)?;
212        self.db.insert(soul, json_str.as_bytes())?;
213        self.db.flush_async().await?;
214        Ok(())
215    }
216
217    async fn has(&self, soul: &str) -> GunResult<bool> {
218        Ok(self.db.contains_key(soul)?)
219    }
220}
221
222/// LocalStorage-equivalent storage for Rust
223/// Provides a simple, persistent key-value store similar to browser localStorage
224/// Stores data in JSON files on disk in a single directory
225///
226/// This is similar to browser localStorage in that it:
227/// - Persists data to disk
228/// - Provides simple get/put/has operations
229/// - Stores data in a user-accessible location
230/// - Is simpler than a full database (like Sled)
231pub struct LocalStorage {
232    data_dir: PathBuf,
233    cache: RwLock<HashMap<String, Node>>, // In-memory cache for performance
234    dirty: RwLock<HashSet<String>>,       // Track which keys need to be written to disk
235}
236
237impl LocalStorage {
238    /// Create a new LocalStorage instance
239    ///
240    /// # Arguments
241    /// * `data_dir` - Directory path where data will be stored (e.g., "./gun_data")
242    ///
243    /// Creates the directory if it doesn't exist
244    pub fn new(data_dir: &str) -> GunResult<Self> {
245        let path = PathBuf::from(data_dir);
246
247        // Create directory if it doesn't exist
248        fs::create_dir_all(&path).map_err(|e| {
249            GunError::Io(std::io::Error::other(format!(
250                "Failed to create storage directory: {}",
251                e
252            )))
253        })?;
254
255        // Load existing data into cache
256        let cache = Self::load_all(&path)?;
257
258        Ok(Self {
259            data_dir: path,
260            cache: RwLock::new(cache),
261            dirty: RwLock::new(HashSet::new()),
262        })
263    }
264
265    /// Load all data from disk into memory cache
266    fn load_all(path: &PathBuf) -> GunResult<HashMap<String, Node>> {
267        let mut data = HashMap::new();
268
269        // Read all files in the directory
270        if let Ok(entries) = fs::read_dir(path) {
271            for entry in entries.flatten() {
272                let file_path = entry.path();
273                if file_path.is_file() {
274                    if let Some(file_name) = file_path.file_name() {
275                        if let Some(soul) = file_name.to_str() {
276                            // Try to decode the filename (may be URL-encoded)
277                            let soul = urlencoding::decode(soul)
278                                .unwrap_or(std::borrow::Cow::Borrowed(soul))
279                                .into_owned();
280
281                            if let Ok(node) = Self::load_file(&file_path) {
282                                data.insert(soul, node);
283                            }
284                        }
285                    }
286                }
287            }
288        }
289
290        Ok(data)
291    }
292
293    /// Load a single file from disk
294    fn load_file(path: &PathBuf) -> GunResult<Node> {
295        let mut file = fs::File::open(path)?;
296        let mut contents = String::new();
297        file.read_to_string(&mut contents)?;
298        let node: Node = serde_json::from_str(&contents)?;
299        Ok(node)
300    }
301
302    /// Save a node to disk
303    fn save_file(&self, soul: &str, node: &Node) -> GunResult<()> {
304        // Encode soul as filename-safe (URL encoding)
305        let encoded_soul = urlencoding::encode(soul);
306        let file_path = self.data_dir.join(encoded_soul.as_ref());
307
308        let json_str = serde_json::to_string_pretty(node).map_err(GunError::Serialization)?;
309
310        // Write atomically: write to temp file, then rename
311        let temp_path = file_path.with_extension("tmp");
312        let mut file = fs::File::create(&temp_path)?;
313        file.write_all(json_str.as_bytes())?;
314        file.sync_all()?;
315        drop(file);
316
317        // Atomic rename
318        fs::rename(&temp_path, &file_path)?;
319
320        Ok(())
321    }
322
323    /// Flush dirty entries to disk
324    pub async fn flush(&self) -> GunResult<()> {
325        let dirty_keys: Vec<String> = {
326            let dirty = self.dirty.read();
327            dirty.iter().cloned().collect()
328        };
329
330        let cache = self.cache.read();
331        for soul in dirty_keys {
332            if let Some(node) = cache.get(&soul) {
333                if let Err(e) = self.save_file(&soul, node) {
334                    eprintln!("Error saving {} to disk: {}", soul, e);
335                }
336            }
337        }
338
339        // Clear dirty set
340        let mut dirty = self.dirty.write();
341        dirty.clear();
342
343        Ok(())
344    }
345}
346
347#[async_trait]
348impl Storage for LocalStorage {
349    async fn get(&self, soul: &str) -> GunResult<Option<Node>> {
350        // Check cache first
351        let cache = self.cache.read();
352        Ok(cache.get(soul).cloned())
353    }
354
355    async fn put(&self, soul: &str, node: &Node) -> GunResult<()> {
356        // Update cache
357        {
358            let mut cache = self.cache.write();
359            cache.insert(soul.to_string(), node.clone());
360        }
361
362        // Mark as dirty for disk write
363        {
364            let mut dirty = self.dirty.write();
365            dirty.insert(soul.to_string());
366        }
367
368        // Write to disk immediately (localStorage behavior)
369        // Could be optimized to batch writes, but for now we match localStorage's synchronous behavior
370        self.save_file(soul, node)?;
371
372        // Remove from dirty set since we just wrote it
373        let mut dirty = self.dirty.write();
374        dirty.remove(soul);
375
376        Ok(())
377    }
378
379    async fn has(&self, soul: &str) -> GunResult<bool> {
380        let cache = self.cache.read();
381        Ok(cache.contains_key(soul))
382    }
383}
384
385// Implement Drop to flush on cleanup
386impl Drop for LocalStorage {
387    fn drop(&mut self) {
388        // Flush any remaining dirty entries
389        let dirty_keys: Vec<String> = {
390            let dirty = self.dirty.read();
391            dirty.iter().cloned().collect()
392        };
393
394        if !dirty_keys.is_empty() {
395            let cache = self.cache.read();
396            for soul in dirty_keys {
397                if let Some(node) = cache.get(&soul) {
398                    let _ = self.save_file(&soul, node);
399                }
400            }
401        }
402    }
403}