Skip to main content

oxigdal_pwa/
lib.rs

1//! Progressive Web App capabilities for OxiGDAL.
2//!
3//! This crate provides comprehensive PWA functionality including:
4//! - Service worker integration
5//! - Offline caching strategies
6//! - Background sync
7//! - Push notifications
8//! - Web app manifest generation
9//! - PWA lifecycle management
10//! - Geospatial data caching optimizations
11//!
12//! # Examples
13//!
14//! ## Basic Service Worker Registration
15//!
16//! ```no_run
17//! use oxigdal_pwa::service_worker::ServiceWorkerRegistry;
18//!
19//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
20//! let registry = ServiceWorkerRegistry::with_script_url("/sw.js")
21//!     .with_scope("/app");
22//!
23//! let registration = registry.register().await?;
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! ## Caching Strategies
29//!
30//! ```no_run
31//! use oxigdal_pwa::cache::strategies::{CacheStrategy, StrategyType};
32//!
33//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
34//! // Create a cache-first strategy for static assets
35//! let strategy = CacheStrategy::cache_first("static-assets");
36//!
37//! // Or network-first for API calls
38//! let api_strategy = CacheStrategy::network_first("api-cache");
39//! # Ok(())
40//! # }
41//! ```
42//!
43//! ## Geospatial Tile Caching
44//!
45//! ```no_run
46//! use oxigdal_pwa::cache::geospatial::{GeospatialCache, BoundingBox};
47//!
48//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
49//! let cache = GeospatialCache::with_defaults();
50//!
51//! // Cache tiles for an area
52//! let bbox = BoundingBox::new(-180.0, -85.0, 180.0, 85.0)?;
53//! let tiles = cache.prefetch_tiles(&bbox, 0..5, "https://tiles.example.com").await?;
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! ## Push Notifications
59//!
60//! ```no_run
61//! use oxigdal_pwa::notifications::{NotificationManager, NotificationConfig};
62//!
63//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
64//! let manager = NotificationManager::new();
65//!
66//! // Request permission
67//! let permission = NotificationManager::request_permission().await?;
68//!
69//! if permission.is_granted() {
70//!     let config = NotificationConfig::new("New Data Available")
71//!         .with_body("Your geospatial data has been updated")
72//!         .with_icon("/icon.png");
73//!
74//!     manager.show(&config).await?;
75//! }
76//! # Ok(())
77//! # }
78//! ```
79//!
80//! ## Web App Manifest
81//!
82//! ```
83//! use oxigdal_pwa::manifest::{ManifestBuilder, DisplayMode};
84//!
85//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
86//! let manifest = ManifestBuilder::geospatial("GeoApp", "Geo")
87//!     .description("A powerful geospatial PWA")
88//!     .colors("#ffffff", "#007bff")
89//!     .add_standard_icons("/icons")
90//!     .build();
91//!
92//! let json = manifest.to_json()?;
93//! # Ok(())
94//! # }
95//! ```
96//!
97//! # Features
98//!
99//! - `default`: Basic PWA functionality with console error hooks
100//! - `notifications`: Push notification support
101//! - `background-sync`: Background synchronization
102//! - `geospatial-cache`: Geospatial-specific caching optimizations
103
104#![warn(missing_docs)]
105#![deny(unsafe_code)]
106
107pub mod cache;
108pub mod error;
109pub mod lifecycle;
110pub mod manifest;
111pub mod notifications;
112pub mod service_worker;
113pub mod sync;
114
115// Re-export commonly used types
116pub use error::{PwaError, Result};
117
118pub use cache::{
119    CacheManager, CacheStorageManager,
120    geospatial::{BoundingBox, GeospatialCache, TileCoord},
121    strategies::{CacheStrategy, StrategyType},
122};
123
124pub use lifecycle::{
125    DisplayModeDetection, InstallPrompt, InstallState, PwaLifecycle, UpdateManager,
126};
127
128pub use manifest::{
129    AppIcon, DisplayMode, ManifestBuilder, Orientation, Screenshot, WebAppManifest,
130};
131
132pub use notifications::{
133    NotificationAction, NotificationConfig, NotificationManager, Permission,
134    PushNotificationManager,
135};
136
137pub use service_worker::{
138    ServiceWorkerEvents, ServiceWorkerMessaging, ServiceWorkerRegistry, ServiceWorkerScope,
139    get_registration, get_service_worker_container, is_service_worker_supported,
140    register_service_worker,
141};
142
143pub use sync::{BackgroundSync, QueuedOperation, SyncCoordinator, SyncOptions, SyncQueue};
144
145/// Initialize PWA with console error panic hook.
146pub fn initialize() {
147    #[cfg(feature = "console_error_panic_hook")]
148    console_error_panic_hook::set_once();
149}
150
151/// PWA configuration and initialization.
152pub struct PwaConfig {
153    /// Service worker script URL
154    pub service_worker_url: String,
155
156    /// Service worker scope
157    pub scope: Option<String>,
158
159    /// Enable automatic cache management
160    pub enable_cache_management: bool,
161
162    /// Enable background sync
163    pub enable_background_sync: bool,
164
165    /// Enable notifications
166    pub enable_notifications: bool,
167
168    /// Enable geospatial cache optimizations
169    pub enable_geospatial_cache: bool,
170}
171
172impl Default for PwaConfig {
173    fn default() -> Self {
174        Self {
175            service_worker_url: "/sw.js".to_string(),
176            scope: None,
177            enable_cache_management: true,
178            enable_background_sync: false,
179            enable_notifications: false,
180            enable_geospatial_cache: true,
181        }
182    }
183}
184
185impl PwaConfig {
186    /// Create a new PWA configuration.
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// Set the service worker URL.
192    pub fn with_service_worker_url(mut self, url: impl Into<String>) -> Self {
193        self.service_worker_url = url.into();
194        self
195    }
196
197    /// Set the scope.
198    pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
199        self.scope = Some(scope.into());
200        self
201    }
202
203    /// Enable cache management.
204    pub fn with_cache_management(mut self, enable: bool) -> Self {
205        self.enable_cache_management = enable;
206        self
207    }
208
209    /// Enable background sync.
210    pub fn with_background_sync(mut self, enable: bool) -> Self {
211        self.enable_background_sync = enable;
212        self
213    }
214
215    /// Enable notifications.
216    pub fn with_notifications(mut self, enable: bool) -> Self {
217        self.enable_notifications = enable;
218        self
219    }
220
221    /// Enable geospatial cache.
222    pub fn with_geospatial_cache(mut self, enable: bool) -> Self {
223        self.enable_geospatial_cache = enable;
224        self
225    }
226}
227
228/// Main PWA application manager.
229pub struct PwaApp {
230    config: PwaConfig,
231    lifecycle: PwaLifecycle,
232    cache_manager: Option<CacheManager>,
233    geospatial_cache: Option<GeospatialCache>,
234    notification_manager: Option<NotificationManager>,
235    sync_coordinator: Option<SyncCoordinator>,
236}
237
238impl PwaApp {
239    /// Create a new PWA application.
240    pub fn new(config: PwaConfig) -> Self {
241        Self {
242            config,
243            lifecycle: PwaLifecycle::new(),
244            cache_manager: None,
245            geospatial_cache: None,
246            notification_manager: None,
247            sync_coordinator: None,
248        }
249    }
250
251    /// Create with default configuration.
252    pub fn with_defaults() -> Self {
253        Self::new(PwaConfig::default())
254    }
255
256    /// Initialize the PWA application.
257    pub async fn initialize(&mut self) -> Result<()> {
258        // Initialize console error hook
259        initialize();
260
261        // Initialize lifecycle
262        self.lifecycle.initialize()?;
263
264        // Register service worker
265        let registration = register_service_worker(
266            &self.config.service_worker_url,
267            self.config.scope.as_deref(),
268        )
269        .await?;
270
271        self.lifecycle.set_registration(registration.clone());
272
273        // Initialize cache management
274        if self.config.enable_cache_management {
275            self.cache_manager = Some(CacheManager::new("oxigdal-pwa-cache"));
276        }
277
278        // Initialize geospatial cache
279        if self.config.enable_geospatial_cache {
280            self.geospatial_cache = Some(GeospatialCache::with_defaults());
281        }
282
283        // Initialize notification manager
284        if self.config.enable_notifications {
285            self.notification_manager = Some(NotificationManager::new());
286        }
287
288        // Initialize sync coordinator
289        if self.config.enable_background_sync {
290            self.sync_coordinator = Some(SyncCoordinator::new(registration));
291        }
292
293        Ok(())
294    }
295
296    /// Get the lifecycle manager.
297    pub fn lifecycle(&self) -> &PwaLifecycle {
298        &self.lifecycle
299    }
300
301    /// Get mutable lifecycle manager.
302    pub fn lifecycle_mut(&mut self) -> &mut PwaLifecycle {
303        &mut self.lifecycle
304    }
305
306    /// Get the cache manager.
307    pub fn cache_manager(&self) -> Option<&CacheManager> {
308        self.cache_manager.as_ref()
309    }
310
311    /// Get the geospatial cache.
312    pub fn geospatial_cache(&self) -> Option<&GeospatialCache> {
313        self.geospatial_cache.as_ref()
314    }
315
316    /// Get the notification manager.
317    pub fn notification_manager(&self) -> Option<&NotificationManager> {
318        self.notification_manager.as_ref()
319    }
320
321    /// Get the sync coordinator.
322    pub fn sync_coordinator(&self) -> Option<&SyncCoordinator> {
323        self.sync_coordinator.as_ref()
324    }
325
326    /// Get mutable sync coordinator.
327    pub fn sync_coordinator_mut(&mut self) -> Option<&mut SyncCoordinator> {
328        self.sync_coordinator.as_mut()
329    }
330
331    /// Check if running as PWA.
332    pub fn is_pwa(&self) -> bool {
333        self.lifecycle.is_pwa()
334    }
335
336    /// Check if install prompt is available.
337    pub fn can_install(&self) -> bool {
338        self.lifecycle.can_install()
339    }
340
341    /// Show install prompt.
342    pub async fn prompt_install(&mut self) -> Result<bool> {
343        self.lifecycle.prompt_install().await
344    }
345
346    /// Get the configuration.
347    pub fn config(&self) -> &PwaConfig {
348        &self.config
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_pwa_config() {
358        let config = PwaConfig::new()
359            .with_service_worker_url("/custom-sw.js")
360            .with_scope("/app")
361            .with_cache_management(true)
362            .with_geospatial_cache(true);
363
364        assert_eq!(config.service_worker_url, "/custom-sw.js");
365        assert_eq!(config.scope, Some("/app".to_string()));
366        assert!(config.enable_cache_management);
367        assert!(config.enable_geospatial_cache);
368    }
369
370    #[test]
371    #[cfg(target_arch = "wasm32")]
372    fn test_pwa_app_creation() {
373        let app = PwaApp::with_defaults();
374        assert_eq!(app.config.service_worker_url, "/sw.js");
375        assert!(app.cache_manager.is_none()); // Not initialized yet
376    }
377
378    #[test]
379    fn test_pwa_config_builder() {
380        let config = PwaConfig::default();
381        assert_eq!(config.service_worker_url, "/sw.js");
382        assert!(config.enable_cache_management);
383        assert!(config.enable_geospatial_cache);
384    }
385}