oxigdal_pwa/cache/
storage.rs1use crate::error::{PwaError, Result};
4use serde::{Deserialize, Serialize};
5use wasm_bindgen::JsCast;
6use wasm_bindgen_futures::JsFuture;
7use web_sys::StorageEstimate;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct StorageInfo {
12 pub usage: Option<u64>,
14
15 pub quota: Option<u64>,
17
18 pub usage_percentage: Option<f64>,
20}
21
22impl StorageInfo {
23 pub fn from_estimate(estimate: &StorageEstimate) -> Self {
25 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 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 pub fn is_nearly_full(&self) -> bool {
66 self.usage_percentage.map(|p| p > 0.9).unwrap_or(false)
67 }
68
69 pub fn has_space_for(&self, size: u64) -> bool {
71 self.available().map(|a| a >= size).unwrap_or(false)
72 }
73}
74
75pub struct CacheStorageManager;
77
78impl CacheStorageManager {
79 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 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 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 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 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 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 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 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 async fn estimate_response_size(response: &web_sys::Response) -> Result<Option<u64>> {
202 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 Ok(None)
218 }
219
220 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#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
230pub enum EvictionPolicy {
231 Lru,
233
234 Fifo,
236
237 Lfu,
239
240 OldestFirst,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct CleanupConfig {
247 pub max_size: Option<u64>,
249
250 pub max_entries: Option<usize>,
252
253 pub eviction_policy: EvictionPolicy,
255
256 pub cleanup_threshold: f64,
258
259 pub target_usage: f64,
261}
262
263impl Default for CleanupConfig {
264 fn default() -> Self {
265 Self {
266 max_size: Some(100 * 1024 * 1024), 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}