Skip to main content

oxigdal_pwa/cache/
mod.rs

1//! Offline caching strategies for PWA.
2
3pub mod geospatial;
4pub mod storage;
5pub mod strategies;
6
7use crate::error::{PwaError, Result};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use wasm_bindgen::JsCast;
11use wasm_bindgen_futures::JsFuture;
12use web_sys::{Cache, CacheStorage, Request, Response};
13
14pub use geospatial::GeospatialCache;
15pub use storage::CacheStorageManager;
16pub use strategies::{CacheStrategy, StrategyType};
17
18/// Get the cache storage API.
19pub fn get_cache_storage() -> Result<CacheStorage> {
20    let window = web_sys::window()
21        .ok_or_else(|| PwaError::InvalidState("No window available".to_string()))?;
22
23    let caches = window
24        .caches()
25        .map_err(|_| PwaError::CacheOperation("CacheStorage not available".to_string()))?;
26
27    Ok(caches)
28}
29
30/// Open a cache by name.
31pub async fn open_cache(name: &str) -> Result<Cache> {
32    let caches = get_cache_storage()?;
33    let promise = caches.open(name);
34
35    let result = JsFuture::from(promise)
36        .await
37        .map_err(|e| PwaError::CacheOperation(format!("Cache open failed: {:?}", e)))?;
38
39    result
40        .dyn_into::<Cache>()
41        .map_err(|_| PwaError::CacheOperation("Invalid cache object".to_string()))
42}
43
44/// Delete a cache by name.
45pub async fn delete_cache(name: &str) -> Result<bool> {
46    let caches = get_cache_storage()?;
47    let promise = caches.delete(name);
48
49    let result = JsFuture::from(promise)
50        .await
51        .map_err(|e| PwaError::CacheOperation(format!("Cache delete failed: {:?}", e)))?;
52
53    result
54        .as_bool()
55        .ok_or_else(|| PwaError::CacheOperation("Invalid delete result".to_string()))
56}
57
58/// Check if a cache exists.
59pub async fn has_cache(name: &str) -> Result<bool> {
60    let caches = get_cache_storage()?;
61    let promise = caches.has(name);
62
63    let result = JsFuture::from(promise)
64        .await
65        .map_err(|e| PwaError::CacheOperation(format!("Cache has failed: {:?}", e)))?;
66
67    result
68        .as_bool()
69        .ok_or_else(|| PwaError::CacheOperation("Invalid has result".to_string()))
70}
71
72/// Get all cache names.
73pub async fn get_cache_names() -> Result<Vec<String>> {
74    let caches = get_cache_storage()?;
75    let promise = caches.keys();
76
77    let result = JsFuture::from(promise)
78        .await
79        .map_err(|e| PwaError::CacheOperation(format!("Cache keys failed: {:?}", e)))?;
80
81    let array = js_sys::Array::from(&result);
82    let mut names = Vec::new();
83
84    for i in 0..array.length() {
85        if let Some(name) = array.get(i).as_string() {
86            names.push(name);
87        }
88    }
89
90    Ok(names)
91}
92
93/// Cache entry metadata.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct CacheEntryMetadata {
96    /// Cache name
97    pub cache_name: String,
98
99    /// Request URL
100    pub url: String,
101
102    /// Timestamp when cached
103    pub cached_at: DateTime<Utc>,
104
105    /// Expiration time
106    pub expires_at: Option<DateTime<Utc>>,
107
108    /// Size in bytes
109    pub size: Option<usize>,
110
111    /// Custom metadata
112    pub custom: Option<serde_json::Value>,
113}
114
115/// Cache manager for managing cache operations.
116pub struct CacheManager {
117    cache_name: String,
118}
119
120impl CacheManager {
121    /// Create a new cache manager.
122    pub fn new(cache_name: impl Into<String>) -> Self {
123        Self {
124            cache_name: cache_name.into(),
125        }
126    }
127
128    /// Open the cache.
129    pub async fn open(&self) -> Result<Cache> {
130        open_cache(&self.cache_name).await
131    }
132
133    /// Put a request/response pair into the cache.
134    pub async fn put(&self, request: &Request, response: &Response) -> Result<()> {
135        let cache = self.open().await?;
136        let promise = cache.put_with_request(request, response);
137
138        JsFuture::from(promise)
139            .await
140            .map_err(|e| PwaError::CacheOperation(format!("Cache put failed: {:?}", e)))?;
141
142        Ok(())
143    }
144
145    /// Add a request to the cache (fetches and caches).
146    pub async fn add(&self, request: &Request) -> Result<()> {
147        let cache = self.open().await?;
148        let promise = cache.add_with_request(request);
149
150        JsFuture::from(promise)
151            .await
152            .map_err(|e| PwaError::CacheOperation(format!("Cache add failed: {:?}", e)))?;
153
154        Ok(())
155    }
156
157    /// Add multiple requests to the cache.
158    pub async fn add_all(&self, requests: &[Request]) -> Result<()> {
159        let cache = self.open().await?;
160
161        let array = js_sys::Array::new();
162        for request in requests {
163            array.push(request);
164        }
165
166        let promise = cache.add_all_with_request_sequence(&array);
167
168        JsFuture::from(promise)
169            .await
170            .map_err(|e| PwaError::CacheOperation(format!("Cache add_all failed: {:?}", e)))?;
171
172        Ok(())
173    }
174
175    /// Match a request in the cache.
176    pub async fn match_request(&self, request: &Request) -> Result<Option<Response>> {
177        let cache = self.open().await?;
178        let promise = cache.match_with_request(request);
179
180        let result = JsFuture::from(promise)
181            .await
182            .map_err(|e| PwaError::CacheOperation(format!("Cache match failed: {:?}", e)))?;
183
184        if result.is_undefined() || result.is_null() {
185            Ok(None)
186        } else {
187            let response = result
188                .dyn_into::<Response>()
189                .map_err(|_| PwaError::CacheOperation("Invalid response object".to_string()))?;
190            Ok(Some(response))
191        }
192    }
193
194    /// Delete a request from the cache.
195    pub async fn delete(&self, request: &Request) -> Result<bool> {
196        let cache = self.open().await?;
197        let promise = cache.delete_with_request(request);
198
199        let result = JsFuture::from(promise)
200            .await
201            .map_err(|e| PwaError::CacheOperation(format!("Cache delete failed: {:?}", e)))?;
202
203        result
204            .as_bool()
205            .ok_or_else(|| PwaError::CacheOperation("Invalid delete result".to_string()))
206    }
207
208    /// Get all requests in the cache.
209    pub async fn keys(&self) -> Result<Vec<Request>> {
210        let cache = self.open().await?;
211        let promise = cache.keys();
212
213        let result = JsFuture::from(promise)
214            .await
215            .map_err(|e| PwaError::CacheOperation(format!("Cache keys failed: {:?}", e)))?;
216
217        let array = js_sys::Array::from(&result);
218        let mut requests = Vec::new();
219
220        for i in 0..array.length() {
221            if let Ok(request) = array.get(i).dyn_into::<Request>() {
222                requests.push(request);
223            }
224        }
225
226        Ok(requests)
227    }
228
229    /// Clear all entries in the cache.
230    pub async fn clear(&self) -> Result<()> {
231        let requests = self.keys().await?;
232
233        for request in requests {
234            self.delete(&request).await?;
235        }
236
237        Ok(())
238    }
239
240    /// Delete the entire cache.
241    pub async fn destroy(&self) -> Result<bool> {
242        delete_cache(&self.cache_name).await
243    }
244
245    /// Get the cache name.
246    pub fn name(&self) -> &str {
247        &self.cache_name
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_cache_manager_creation() {
257        let manager = CacheManager::new("test-cache");
258        assert_eq!(manager.name(), "test-cache");
259    }
260
261    #[test]
262    fn test_cache_entry_metadata() {
263        let metadata = CacheEntryMetadata {
264            cache_name: "test".to_string(),
265            url: "https://example.com".to_string(),
266            cached_at: Utc::now(),
267            expires_at: None,
268            size: Some(1024),
269            custom: None,
270        };
271
272        assert_eq!(metadata.cache_name, "test");
273        assert_eq!(metadata.size, Some(1024));
274    }
275}