leptos_query_rs/persistence/
mod.rs

1use crate::retry::QueryError;
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::Arc;
6use std::time::Instant;
7
8#[cfg(target_arch = "wasm32")]
9use web_sys::Storage;
10
11/// Trait for storage backends
12#[async_trait]
13pub trait StorageBackend: Send + Sync {
14    /// Store data with a key
15    async fn store(&self, key: &str, data: &[u8]) -> Result<(), QueryError>;
16    
17    /// Retrieve data by key
18    async fn retrieve(&self, key: &str) -> Result<Option<Vec<u8>>, QueryError>;
19    
20    /// Remove data by key
21    async fn remove(&self, key: &str) -> Result<(), QueryError>;
22    
23    /// List all keys
24    async fn list_keys(&self) -> Result<Vec<String>, QueryError>;
25    
26    /// Clear all data
27    async fn clear(&self) -> Result<(), QueryError>;
28    
29    /// Get total size of stored data
30    async fn size(&self) -> Result<usize, QueryError>;
31}
32
33/// In-memory storage backend for testing and fallback
34pub struct MemoryBackend {
35    data: Arc<parking_lot::RwLock<HashMap<String, Vec<u8>>>>,
36}
37
38impl Default for MemoryBackend {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl MemoryBackend {
45    pub fn new() -> Self {
46        Self {
47            data: Arc::new(parking_lot::RwLock::new(HashMap::new())),
48        }
49    }
50}
51
52#[async_trait]
53impl StorageBackend for MemoryBackend {
54    async fn store(&self, key: &str, data: &[u8]) -> Result<(), QueryError> {
55        let mut map = self.data.write();
56        map.insert(key.to_string(), data.to_vec());
57        Ok(())
58    }
59    
60    async fn retrieve(&self, key: &str) -> Result<Option<Vec<u8>>, QueryError> {
61        let map = self.data.read();
62        Ok(map.get(key).cloned())
63    }
64    
65    async fn remove(&self, key: &str) -> Result<(), QueryError> {
66        let mut map = self.data.write();
67        map.remove(key);
68        Ok(())
69    }
70    
71    async fn list_keys(&self) -> Result<Vec<String>, QueryError> {
72        let map = self.data.read();
73        Ok(map.keys().cloned().collect())
74    }
75    
76    async fn clear(&self) -> Result<(), QueryError> {
77        let mut map = self.data.write();
78        map.clear();
79        Ok(())
80    }
81    
82    async fn size(&self) -> Result<usize, QueryError> {
83        let map = self.data.read();
84        Ok(map.len())
85    }
86}
87
88/// Web localStorage backend with synchronous API for testing
89#[cfg(feature = "persistence")]
90pub struct LocalStorageBackend {
91    prefix: String,
92    // For non-WASM targets, we'll use in-memory storage for testing
93    #[cfg(not(target_arch = "wasm32"))]
94    data: std::cell::RefCell<std::collections::HashMap<String, Vec<u8>>>,
95}
96
97#[cfg(feature = "persistence")]
98impl LocalStorageBackend {
99    pub fn new(prefix: String) -> Self {
100        Self { 
101            prefix,
102            #[cfg(not(target_arch = "wasm32"))]
103            data: std::cell::RefCell::new(std::collections::HashMap::new()),
104        }
105    }
106    
107    pub fn prefix(&self) -> &str {
108        &self.prefix
109    }
110    
111    fn make_key(&self, key: &crate::types::QueryKey) -> String {
112        format!("{}_{}", self.prefix, key.to_string())
113    }
114    
115    pub fn store<T: Serialize>(&self, key: &crate::types::QueryKey, data: &T) -> Result<(), QueryError> {
116        let serialized = bincode::serialize(data)
117            .map_err(|e| QueryError::SerializationError(e.to_string()))?;
118        
119        #[cfg(target_arch = "wasm32")]
120        {
121            let window = web_sys::window().ok_or_else(|| {
122                QueryError::StorageError("window not available".to_string())
123            })?;
124            
125            let storage = window.local_storage().map_err(|_| {
126                QueryError::StorageError("localStorage not available".to_string())
127            })?.ok_or_else(|| {
128                QueryError::StorageError("localStorage not available".to_string())
129            })?;
130            
131            let encoded = base64::encode(&serialized);
132            let full_key = self.make_key(key);
133            storage.set_item(&full_key, &encoded).map_err(|_| {
134                QueryError::StorageError("Failed to store data".to_string())
135            })?;
136        }
137        
138        #[cfg(not(target_arch = "wasm32"))]
139        {
140            // For non-WASM targets, use in-memory storage for testing
141            let full_key = self.make_key(key);
142            self.data.borrow_mut().insert(full_key, serialized);
143        }
144        
145        Ok(())
146    }
147    
148    pub fn retrieve<T: serde::de::DeserializeOwned>(&self, key: &crate::types::QueryKey) -> Result<Option<T>, QueryError> {
149        #[cfg(target_arch = "wasm32")]
150        {
151            let window = web_sys::window().ok_or_else(|| {
152                QueryError::StorageError("window not available".to_string())
153            })?;
154            
155            let storage = window.local_storage().map_err(|_| {
156                QueryError::StorageError("localStorage not available".to_string())
157            })?.ok_or_else(|| {
158                QueryError::StorageError("localStorage not available".to_string())
159            })?;
160            
161            let full_key = self.make_key(key);
162            let encoded = storage.get_item(&full_key).map_err(|_| {
163                QueryError::StorageError("Failed to retrieve data".to_string())
164            })?;
165            
166            match encoded {
167                Some(encoded) => {
168                    let data = base64::decode(&encoded).map_err(|_| {
169                        QueryError::StorageError("Failed to decode data".to_string())
170                    })?;
171                    let deserialized: T = bincode::deserialize(&data)
172                        .map_err(|e| QueryError::DeserializationError(e.to_string()))?;
173                    Ok(Some(deserialized))
174                }
175                None => Ok(None),
176            }
177        }
178        
179        #[cfg(not(target_arch = "wasm32"))]
180        {
181            // For non-WASM targets, use in-memory storage for testing
182            let full_key = self.make_key(key);
183            if let Some(data) = self.data.borrow().get(&full_key) {
184                let deserialized: T = bincode::deserialize(data)
185                    .map_err(|e| QueryError::DeserializationError(e.to_string()))?;
186                Ok(Some(deserialized))
187            } else {
188                Ok(None)
189            }
190        }
191    }
192    
193    pub fn remove(&self, key: &crate::types::QueryKey) -> Result<(), QueryError> {
194        #[cfg(target_arch = "wasm32")]
195        {
196            let window = web_sys::window().ok_or_else(|| {
197                QueryError::StorageError("window not available".to_string())
198            })?;
199            
200            let storage = window.local_storage().map_err(|_| {
201                QueryError::StorageError("localStorage not available".to_string())
202            })?.ok_or_else(|| {
203                QueryError::StorageError("localStorage not available".to_string())
204            })?;
205            
206            let full_key = self.make_key(key);
207            storage.remove_item(&full_key).map_err(|_| {
208                QueryError::StorageError("Failed to remove data".to_string())
209            })?;
210        }
211        
212        #[cfg(not(target_arch = "wasm32"))]
213        {
214            // For non-WASM targets, use in-memory storage for testing
215            let full_key = self.make_key(key);
216            self.data.borrow_mut().remove(&full_key);
217        }
218        
219        Ok(())
220    }
221    
222    pub fn clear(&self) -> Result<(), QueryError> {
223        #[cfg(target_arch = "wasm32")]
224        {
225            let window = web_sys::window().ok_or_else(|| {
226                QueryError::StorageError("window not available".to_string())
227            })?;
228            
229            let storage = window.local_storage().map_err(|_| {
230                QueryError::StorageError("localStorage not available".to_string())
231            })?.ok_or_else(|| {
232                QueryError::StorageError("localStorage not available".to_string())
233            })?;
234            
235            // Clear all items with our prefix
236            let length = storage.length().map_err(|_| {
237                QueryError::StorageError("Failed to get storage length".to_string())
238            })?;
239            
240            for i in 0..length {
241                if let Ok(Some(key)) = storage.key(i) {
242                    if key.starts_with(&self.prefix) {
243                        storage.remove_item(&key).map_err(|_| {
244                            QueryError::StorageError("Failed to remove item".to_string())
245                        })?;
246                    }
247                }
248            }
249        }
250        
251        #[cfg(not(target_arch = "wasm32"))]
252        {
253            // For non-WASM targets, use in-memory storage for testing
254            self.data.borrow_mut().clear();
255        }
256        
257        Ok(())
258    }
259}
260
261/// IndexedDB backend with synchronous API for testing
262#[cfg(feature = "persistence")]
263pub struct IndexedDBBackend {
264    db_name: String,
265    store_name: String,
266    // For testing, we'll use in-memory storage
267    data: std::cell::RefCell<std::collections::HashMap<String, Vec<u8>>>,
268}
269
270#[cfg(feature = "persistence")]
271impl IndexedDBBackend {
272    pub fn new(db_name: String, store_name: String) -> Self {
273        Self { 
274            db_name, 
275            store_name,
276            data: std::cell::RefCell::new(std::collections::HashMap::new()),
277        }
278    }
279    
280    pub fn db_name(&self) -> &str {
281        &self.db_name
282    }
283    
284    pub fn store_name(&self) -> &str {
285        &self.store_name
286    }
287    
288    pub fn store<T: Serialize>(&self, key: &crate::types::QueryKey, data: &T) -> Result<(), QueryError> {
289        // For testing, use in-memory storage
290        // In a real implementation, this would use IndexedDB
291        let serialized = bincode::serialize(data)
292            .map_err(|e| QueryError::SerializationError(e.to_string()))?;
293        
294        let key_str = key.to_string();
295        self.data.borrow_mut().insert(key_str, serialized);
296        Ok(())
297    }
298    
299    pub fn retrieve<T: serde::de::DeserializeOwned>(&self, key: &crate::types::QueryKey) -> Result<Option<T>, QueryError> {
300        // For testing, use in-memory storage
301        // In a real implementation, this would use IndexedDB
302        let key_str = key.to_string();
303        
304        if let Some(data) = self.data.borrow().get(&key_str) {
305            let deserialized: T = bincode::deserialize(data)
306                .map_err(|e| QueryError::DeserializationError(e.to_string()))?;
307            Ok(Some(deserialized))
308        } else {
309            Ok(None)
310        }
311    }
312    
313    pub fn remove(&self, key: &crate::types::QueryKey) -> Result<(), QueryError> {
314        // For testing, use in-memory storage
315        // In a real implementation, this would use IndexedDB
316        let key_str = key.to_string();
317        
318        self.data.borrow_mut().remove(&key_str);
319        Ok(())
320    }
321    
322    pub fn clear(&self) -> Result<(), QueryError> {
323        // For testing, use in-memory storage
324        // In a real implementation, this would use IndexedDB
325        
326        self.data.borrow_mut().clear();
327        Ok(())
328    }
329}
330
331// The old async implementation has been replaced with the new synchronous API above
332
333/// Configuration for persistence
334#[derive(Clone, Debug, Serialize, Deserialize)]
335pub struct PersistenceConfig {
336    /// Whether persistence is enabled
337    pub enabled: bool,
338    /// Storage backend type
339    pub backend: PersistenceBackend,
340    /// Maximum size of cache in bytes
341    pub max_size: Option<usize>,
342    /// Whether to compress data
343    pub compress: bool,
344    /// Encryption key (optional)
345    pub encryption_key: Option<String>,
346    /// Whether to persist offline queue
347    pub persist_offline_queue: bool,
348}
349
350impl Default for PersistenceConfig {
351    fn default() -> Self {
352        Self {
353            enabled: true,
354            backend: PersistenceBackend::Memory,
355            max_size: Some(10 * 1024 * 1024), // 10MB
356            compress: false,
357            encryption_key: None,
358            persist_offline_queue: true,
359        }
360    }
361}
362
363/// Available storage backends
364#[derive(Clone, Debug, Serialize, Deserialize)]
365pub enum PersistenceBackend {
366    /// In-memory storage (for testing)
367    Memory,
368    /// Web localStorage
369    LocalStorage,
370    /// IndexedDB (future)
371    IndexedDB,
372}
373
374/// Persistence manager for cache and offline queue
375pub struct PersistenceManager {
376    #[allow(dead_code)]
377    config: PersistenceConfig,
378    backend: Box<dyn StorageBackend + Send + Sync>,
379}
380
381impl PersistenceManager {
382    /// Create a new persistence manager
383    pub async fn new(config: PersistenceConfig) -> Result<Self, QueryError> {
384        let backend = Self::create_backend(&config).await?;
385        
386        Ok(Self {
387            config,
388            backend,
389        })
390    }
391    
392    /// Create a storage backend based on configuration
393    async fn create_backend(config: &PersistenceConfig) -> Result<Box<dyn StorageBackend + Send + Sync>, QueryError> {
394        match &config.backend {
395            PersistenceBackend::Memory => {
396                Ok(Box::new(MemoryBackend::new()))
397            }
398            PersistenceBackend::LocalStorage => {
399                #[cfg(target_arch = "wasm32")]
400                {
401                    LocalStorageBackend::new().map(|b| Box::new(b) as Box<dyn StorageBackend + Send + Sync>)
402                }
403                #[cfg(not(target_arch = "wasm32"))]
404                {
405                    Err(QueryError::StorageError("localStorage not available on this platform".to_string()))
406                }
407            }
408            PersistenceBackend::IndexedDB => {
409                Err(QueryError::StorageError("IndexedDB backend not yet implemented".to_string()))
410            }
411        }
412    }
413    
414    /// Store a cache entry
415    pub async fn store_cache_entry(&self, key: &crate::types::QueryKey, entry: &crate::client::CacheEntry) -> Result<(), QueryError> {
416        let data = bincode::serialize(entry)
417            .map_err(|e| QueryError::StorageError(format!("Serialization failed: {}", e)))?;
418        
419        let key_str = key.to_string();
420        self.backend.store(&key_str, &data).await
421    }
422    
423    /// Retrieve a cache entry
424    pub async fn retrieve_cache_entry(&self, key: &crate::types::QueryKey) -> Result<Option<crate::client::CacheEntry>, QueryError> {
425        let key_str = key.to_string();
426        if let Some(data) = self.backend.retrieve(&key_str).await? {
427            let entry: crate::client::CacheEntry = bincode::deserialize(&data)
428                .map_err(|e| QueryError::StorageError(format!("Deserialization failed: {}", e)))?;
429            Ok(Some(entry))
430        } else {
431            Ok(None)
432        }
433    }
434    
435    /// Remove a cache entry
436    pub async fn remove_cache_entry(&self, key: &crate::types::QueryKey) -> Result<(), QueryError> {
437        let key_str = key.to_string();
438        self.backend.remove(&key_str).await
439    }
440    
441    /// List all cached keys
442    pub async fn list_cached_keys(&self) -> Result<Vec<crate::types::QueryKey>, QueryError> {
443        let keys = self.backend.list_keys().await?;
444        let mut query_keys = Vec::new();
445        
446        for key_str in keys {
447            // Try to parse as QueryKey
448            if let Ok(key) = serde_json::from_str(&key_str) {
449                query_keys.push(key);
450            }
451        }
452        
453        Ok(query_keys)
454    }
455    
456    /// Clear all cache data
457    pub async fn clear_cache(&self) -> Result<(), QueryError> {
458        self.backend.clear().await
459    }
460    
461    /// Get storage statistics
462    pub async fn get_stats(&self) -> Result<StorageStats, QueryError> {
463        let size = self.backend.size().await?;
464        Ok(StorageStats {
465            total_entries: size,
466            total_size_bytes: 0, // Would need to calculate this
467        })
468    }
469    
470    /// Add a request to the offline queue
471    pub async fn add_to_offline_queue(&self, request: OfflineRequest) -> Result<(), QueryError> {
472        let data = bincode::serialize(&request)
473            .map_err(|e| QueryError::StorageError(format!("Serialization failed: {}", e)))?;
474        
475        let key = format!("offline_queue_{}", request.timestamp.elapsed().as_millis());
476        self.backend.store(&key, &data).await
477    }
478    
479    /// Process the offline queue
480    pub async fn process_offline_queue(&self) -> Result<Vec<OfflineRequest>, QueryError> {
481        let keys = self.backend.list_keys().await?;
482        let mut requests = Vec::new();
483        
484        for key in keys {
485            if key.starts_with("offline_queue_") {
486                if let Some(data) = self.backend.retrieve(&key).await? {
487                    if let Ok(request) = bincode::deserialize::<OfflineRequest>(&data) {
488                        requests.push(request);
489                    }
490                }
491                // Remove the processed request
492                let _ = self.backend.remove(&key).await;
493            }
494        }
495        
496        Ok(requests)
497    }
498
499    /// Get the offline queue
500    pub fn get_offline_queue(&self) -> Vec<OfflineRequest> {
501        // This is a simplified implementation
502        // In a real implementation, this would read from storage
503        Vec::new()
504    }
505
506    /// Check if cache is persisted
507    pub fn is_cache_persisted(&self) -> bool {
508        // For now, return true if we have any persistence backend
509        // In a real implementation, this would check actual persistence status
510        true
511    }
512}
513
514/// Storage statistics
515#[derive(Clone, Debug, Serialize, Deserialize)]
516pub struct StorageStats {
517    /// Total number of entries
518    pub total_entries: usize,
519    /// Total size in bytes
520    pub total_size_bytes: usize,
521}
522
523/// Offline request for queueing
524#[derive(Clone, Debug, Serialize, Deserialize)]
525pub struct OfflineRequest {
526    /// Type of request
527    pub request_type: OfflineRequestType,
528    /// Request data (serialized)
529    pub data: Vec<u8>,
530    /// Timestamp when request was queued
531    #[serde(with = "instant_serde")]
532    pub timestamp: Instant,
533    /// Retry count
534    pub retry_count: u32,
535}
536
537/// Types of offline requests
538#[derive(Clone, Debug, Serialize, Deserialize)]
539pub enum OfflineRequestType {
540    /// Query request
541    Query,
542    /// Mutation request
543    Mutation,
544    /// Cache invalidation
545    Invalidate,
546    /// Cache removal
547    Remove,
548}
549
550/// Serialization helpers for Instant
551mod instant_serde {
552    use serde::{Deserialize, Deserializer, Serialize, Serializer};
553    use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
554
555    pub fn serialize<S>(instant: &Instant, serializer: S) -> Result<S::Ok, S::Error>
556    where
557        S: Serializer,
558    {
559        // Convert Instant to SystemTime for serialization
560        let system_time = SystemTime::now() - instant.elapsed();
561        let duration = system_time.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO);
562        duration.serialize(serializer)
563    }
564
565    pub fn deserialize<'de, D>(deserializer: D) -> Result<Instant, D::Error>
566    where
567        D: Deserializer<'de>,
568    {
569        let duration = Duration::deserialize(deserializer)?;
570        let system_time = UNIX_EPOCH + duration;
571        let now = SystemTime::now();
572        let elapsed = now.duration_since(system_time).unwrap_or(Duration::ZERO);
573        Ok(Instant::now() - elapsed)
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    
581    #[tokio::test]
582    async fn test_memory_backend() {
583        let backend = MemoryBackend::new();
584        
585        // Test store and retrieve
586        backend.store("test_key", b"test_data").await.unwrap();
587        let data = backend.retrieve("test_key").await.unwrap();
588        assert_eq!(data, Some(b"test_data".to_vec()));
589        
590        // Test remove
591        backend.remove("test_key").await.unwrap();
592        let data = backend.retrieve("test_key").await.unwrap();
593        assert_eq!(data, None);
594        
595        // Test list keys
596        backend.store("key1", b"data1").await.unwrap();
597        backend.store("key2", b"data2").await.unwrap();
598        let keys = backend.list_keys().await.unwrap();
599        assert_eq!(keys.len(), 2);
600        assert!(keys.contains(&"key1".to_string()));
601        assert!(keys.contains(&"key2".to_string()));
602        
603        // Test clear
604        backend.clear().await.unwrap();
605        let keys = backend.list_keys().await.unwrap();
606        assert_eq!(keys.len(), 0);
607    }
608    
609    #[tokio::test]
610    async fn test_persistence_manager() {
611        let config = PersistenceConfig::default();
612        let manager = PersistenceManager::new(config).await.unwrap();
613        
614        // Test stats
615        let stats = manager.get_stats().await.unwrap();
616        assert_eq!(stats.total_entries, 0);
617    }
618    
619    #[tokio::test]
620    async fn test_offline_queue() {
621        let config = PersistenceConfig::default();
622        let manager = PersistenceManager::new(config).await.unwrap();
623        
624        let request = OfflineRequest {
625            request_type: OfflineRequestType::Query,
626            data: b"test_data".to_vec(),
627            timestamp: Instant::now(),
628            retry_count: 0,
629        };
630        
631        manager.add_to_offline_queue(request.clone()).await.unwrap();
632        let requests = manager.process_offline_queue().await.unwrap();
633        assert_eq!(requests.len(), 1);
634    }
635}