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}