Skip to main content

oxigdal_pwa/cache/
storage.rs

1//! Cache storage management and quota handling.
2
3use crate::error::{PwaError, Result};
4use serde::{Deserialize, Serialize};
5use wasm_bindgen::JsCast;
6use wasm_bindgen_futures::JsFuture;
7use web_sys::StorageEstimate;
8
9/// Storage estimate information.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct StorageInfo {
12    /// Current usage in bytes
13    pub usage: Option<u64>,
14
15    /// Total quota in bytes
16    pub quota: Option<u64>,
17
18    /// Percentage used (0.0 to 1.0)
19    pub usage_percentage: Option<f64>,
20}
21
22impl StorageInfo {
23    /// Create storage info from web_sys::StorageEstimate.
24    pub fn from_estimate(estimate: &StorageEstimate) -> Self {
25        // Use js_sys::Reflect to get values from StorageEstimate
26        let obj: &js_sys::Object = estimate.as_ref();
27
28        let usage = js_sys::Reflect::get(obj, &"usage".into())
29            .ok()
30            .and_then(|v| v.as_f64())
31            .map(|v| v as u64);
32
33        let quota = js_sys::Reflect::get(obj, &"quota".into())
34            .ok()
35            .and_then(|v| v.as_f64())
36            .map(|v| v as u64);
37
38        let usage_percentage = if let (Some(u), Some(q)) = (usage, quota) {
39            if q > 0 {
40                Some(u as f64 / q as f64)
41            } else {
42                None
43            }
44        } else {
45            None
46        };
47
48        Self {
49            usage,
50            quota,
51            usage_percentage,
52        }
53    }
54
55    /// Get available space in bytes.
56    pub fn available(&self) -> Option<u64> {
57        if let (Some(quota), Some(usage)) = (self.quota, self.usage) {
58            Some(quota.saturating_sub(usage))
59        } else {
60            None
61        }
62    }
63
64    /// Check if storage is nearly full (>90%).
65    pub fn is_nearly_full(&self) -> bool {
66        self.usage_percentage.map(|p| p > 0.9).unwrap_or(false)
67    }
68
69    /// Check if storage has enough space for a given size.
70    pub fn has_space_for(&self, size: u64) -> bool {
71        self.available().map(|a| a >= size).unwrap_or(false)
72    }
73}
74
75/// Cache storage manager for managing storage quota and cleanup.
76pub struct CacheStorageManager;
77
78impl CacheStorageManager {
79    /// Estimate storage usage and quota.
80    pub async fn estimate() -> Result<StorageInfo> {
81        let navigator = Self::get_navigator()?;
82        let storage = navigator.storage();
83
84        let promise = storage.estimate().map_err(|e| {
85            PwaError::StorageEstimateFailed(format!("Estimate call failed: {:?}", e))
86        })?;
87
88        let result = JsFuture::from(promise)
89            .await
90            .map_err(|e| PwaError::StorageEstimateFailed(format!("Estimate failed: {:?}", e)))?;
91
92        let estimate = result
93            .dyn_into::<StorageEstimate>()
94            .map_err(|_| PwaError::StorageEstimateFailed("Invalid estimate object".to_string()))?;
95
96        Ok(StorageInfo::from_estimate(&estimate))
97    }
98
99    /// Check if persistent storage is available.
100    pub async fn is_persistent() -> Result<bool> {
101        let navigator = Self::get_navigator()?;
102        let storage = navigator.storage();
103
104        let promise = storage.persisted().map_err(|e| {
105            PwaError::StorageEstimateFailed(format!("Persisted call failed: {:?}", e))
106        })?;
107        let result = JsFuture::from(promise).await.map_err(|e| {
108            PwaError::StorageEstimateFailed(format!("Persisted check failed: {:?}", e))
109        })?;
110
111        result
112            .as_bool()
113            .ok_or_else(|| PwaError::StorageEstimateFailed("Invalid persisted result".to_string()))
114    }
115
116    /// Request persistent storage.
117    pub async fn request_persistent() -> Result<bool> {
118        let navigator = Self::get_navigator()?;
119        let storage = navigator.storage();
120
121        let promise = storage.persist().map_err(|e| {
122            PwaError::StorageEstimateFailed(format!("Persist call failed: {:?}", e))
123        })?;
124
125        let result = JsFuture::from(promise).await.map_err(|e| {
126            PwaError::StorageEstimateFailed(format!("Persist request failed: {:?}", e))
127        })?;
128
129        result
130            .as_bool()
131            .ok_or_else(|| PwaError::StorageEstimateFailed("Invalid persist result".to_string()))
132    }
133
134    /// Clean up old caches to free space.
135    pub async fn cleanup_old_caches(keep_names: &[String]) -> Result<u64> {
136        let cache_names = super::get_cache_names().await?;
137        let mut freed_bytes = 0u64;
138
139        for name in cache_names {
140            if !keep_names.contains(&name) {
141                // Estimate cache size before deleting
142                if let Ok(estimate) = Self::estimate_cache_size(&name).await {
143                    freed_bytes += estimate;
144                }
145
146                super::delete_cache(&name).await?;
147            }
148        }
149
150        Ok(freed_bytes)
151    }
152
153    /// Estimate the size of a specific cache.
154    pub async fn estimate_cache_size(cache_name: &str) -> Result<u64> {
155        let cache = super::open_cache(cache_name).await?;
156        let keys = cache.keys();
157
158        let requests = JsFuture::from(keys)
159            .await
160            .map_err(|e| PwaError::CacheOperation(format!("Keys promise failed: {:?}", e)))?;
161
162        let array = js_sys::Array::from(&requests);
163        let mut total_size = 0u64;
164
165        for i in 0..array.length() {
166            if let Ok(request) = array.get(i).dyn_into::<web_sys::Request>() {
167                // Match the request to get response
168                if let Ok(Some(response)) = Self::match_request_in_cache(&cache, &request).await {
169                    if let Ok(Some(size)) = Self::estimate_response_size(&response).await {
170                        total_size += size;
171                    }
172                }
173            }
174        }
175
176        Ok(total_size)
177    }
178
179    /// Match a request in a cache.
180    async fn match_request_in_cache(
181        cache: &web_sys::Cache,
182        request: &web_sys::Request,
183    ) -> Result<Option<web_sys::Response>> {
184        let promise = cache.match_with_request(request);
185
186        let result = JsFuture::from(promise)
187            .await
188            .map_err(|e| PwaError::CacheOperation(format!("Match promise failed: {:?}", e)))?;
189
190        if result.is_undefined() || result.is_null() {
191            Ok(None)
192        } else {
193            let response = result
194                .dyn_into::<web_sys::Response>()
195                .map_err(|_| PwaError::CacheOperation("Invalid response".to_string()))?;
196            Ok(Some(response))
197        }
198    }
199
200    /// Estimate response size from Content-Length header or by reading the body.
201    async fn estimate_response_size(response: &web_sys::Response) -> Result<Option<u64>> {
202        // Try to get Content-Length header
203        let headers = response.headers();
204        if let Ok(Some(length)) = headers.get("content-length") {
205            if let Ok(size) = length.parse::<u64>() {
206                return Ok(Some(size));
207            }
208        }
209
210        // Clone and read body to estimate size
211
212        // Note: Reading the body to estimate size is not implemented
213        // This would require ReadableStream support which may not be available
214        // in all web-sys versions. For now, we return None if Content-Length
215        // header is not present.
216
217        Ok(None)
218    }
219
220    /// Get the navigator object.
221    fn get_navigator() -> Result<web_sys::Navigator> {
222        let window = web_sys::window()
223            .ok_or_else(|| PwaError::InvalidState("No window available".to_string()))?;
224        Ok(window.navigator())
225    }
226}
227
228/// Cache eviction policy.
229#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
230pub enum EvictionPolicy {
231    /// Least Recently Used
232    Lru,
233
234    /// First In First Out
235    Fifo,
236
237    /// Least Frequently Used
238    Lfu,
239
240    /// Expire oldest entries first
241    OldestFirst,
242}
243
244/// Cache cleanup configuration.
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct CleanupConfig {
247    /// Maximum cache size in bytes
248    pub max_size: Option<u64>,
249
250    /// Maximum number of entries
251    pub max_entries: Option<usize>,
252
253    /// Eviction policy to use
254    pub eviction_policy: EvictionPolicy,
255
256    /// Clean up when storage usage exceeds this percentage
257    pub cleanup_threshold: f64,
258
259    /// Target usage percentage after cleanup
260    pub target_usage: f64,
261}
262
263impl Default for CleanupConfig {
264    fn default() -> Self {
265        Self {
266            max_size: Some(100 * 1024 * 1024), // 100 MB
267            max_entries: Some(100),
268            eviction_policy: EvictionPolicy::Lru,
269            cleanup_threshold: 0.9,
270            target_usage: 0.7,
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_storage_info() {
281        let info = StorageInfo {
282            usage: Some(50_000_000),
283            quota: Some(100_000_000),
284            usage_percentage: Some(0.5),
285        };
286
287        assert_eq!(info.available(), Some(50_000_000));
288        assert!(!info.is_nearly_full());
289        assert!(info.has_space_for(10_000_000));
290        assert!(!info.has_space_for(60_000_000));
291    }
292
293    #[test]
294    fn test_storage_info_nearly_full() {
295        let info = StorageInfo {
296            usage: Some(95_000_000),
297            quota: Some(100_000_000),
298            usage_percentage: Some(0.95),
299        };
300
301        assert!(info.is_nearly_full());
302    }
303
304    #[test]
305    fn test_cleanup_config_default() {
306        let config = CleanupConfig::default();
307        assert_eq!(config.max_size, Some(100 * 1024 * 1024));
308        assert_eq!(config.max_entries, Some(100));
309        assert_eq!(config.cleanup_threshold, 0.9);
310    }
311}