// HikeMap Service Worker // Increment version to force cache refresh const CACHE_NAME = 'hikemap-v1.2.0'; const urlsToCache = [ '/', '/index.html', '/manifest.json', '/default.kml', '/animations.js', '/icon-192x192.png', '/icon-512x512.png', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', 'https://unpkg.com/leaflet-rotate@0.2.8/dist/leaflet-rotate-src.js', 'https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css' ]; // Cache map tiles separately with a different strategy const MAP_TILE_CACHE = 'hikemap-tiles-v1'; const GEOCACHE_CACHE = 'hikemap-geocaches-v1'; // Install event - cache essential files self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Opened cache'); return cache.addAll(urlsToCache); }) .then(() => self.skipWaiting()) ); }); // Activate event - clean up old caches self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME && cacheName !== MAP_TILE_CACHE && cacheName !== GEOCACHE_CACHE) { console.log('Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }).then(() => self.clients.claim()) ); }); // Fetch event - serve from cache when possible self.addEventListener('fetch', event => { const url = new URL(event.request.url); // Skip non-http(s) requests (chrome-extension://, etc.) if (!url.protocol.startsWith('http')) { return; } // Handle map tiles with cache-first strategy if (url.hostname.includes('tile.openstreetmap.org') || url.hostname.includes('mt0.google.com') || url.hostname.includes('mt1.google.com')) { event.respondWith( caches.open(MAP_TILE_CACHE).then(cache => { return cache.match(event.request).then(response => { if (response) { return response; } // Fetch and cache the tile return fetch(event.request).then(response => { // Only cache successful responses if (response.status === 200) { cache.put(event.request, response.clone()); } return response; }).catch(() => { // Return a placeholder tile if offline return new Response('', { status: 204 }); }); }); }) ); return; } // Handle ALL API calls with network-first strategy if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(event.request) .then(response => { return response; }) .catch(() => { // Fall back to cache for API calls if offline return caches.match(event.request); }) ); return; } // Handle HTML files with network-first strategy (always get fresh version) if (event.request.destination === 'document' || url.pathname === '/' || url.pathname.endsWith('.html')) { event.respondWith( fetch(event.request) .then(response => { // Cache the fresh version if (response.status === 200) { const responseToCache = response.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseToCache); }); } return response; }) .catch(() => { // Fall back to cache only if network fails return caches.match(event.request); }) ); return; } // Handle geocache/KML data with network-first if (url.pathname.includes('/save-kml') || url.pathname.includes('/geocaches')) { event.respondWith( fetch(event.request) .then(response => { if (response.status === 200) { const responseToCache = response.clone(); caches.open(GEOCACHE_CACHE).then(cache => { cache.put(event.request, responseToCache); }); } return response; }) .catch(() => { return caches.match(event.request); }) ); return; } // Default strategy: cache-first for static assets (CSS, JS libraries, images) event.respondWith( caches.match(event.request) .then(response => { if (response) { return response; } // Clone the request because it's a stream const fetchRequest = event.request.clone(); return fetch(fetchRequest).then(response => { // Check if valid response if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // Clone the response because it's a stream const responseToCache = response.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseToCache); }); return response; }); }) .catch(() => { // Offline fallback if (event.request.destination === 'document') { return caches.match('/index.html'); } }) ); }); // Background sync for uploading tracks when back online self.addEventListener('sync', event => { if (event.tag === 'sync-tracks') { event.waitUntil(syncTracks()); } }); async function syncTracks() { // This would sync any offline changes when connection is restored console.log('Syncing tracks with server...'); // Implementation would go here } // Handle messages from the main app self.addEventListener('message', event => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } }); // Handle push notifications self.addEventListener('push', event => { if (!event.data) { console.log('Push notification without data'); return; } const options = event.data.json(); event.waitUntil( self.registration.showNotification(options.title || 'HikeMap Alert', { body: options.body || 'You have a new notification', icon: options.icon || '/icon-192x192.png', badge: options.badge || '/icon-72x72.png', vibrate: [200, 100, 200], tag: options.tag || 'hikemap-notification', data: options.data || {}, actions: [ { action: 'view', title: 'View', icon: '/icon-72x72.png' }, { action: 'close', title: 'Dismiss' } ] }) ); }); // Handle notification clicks self.addEventListener('notificationclick', event => { event.notification.close(); if (event.action === 'close') { return; } // Open or focus the app event.waitUntil( clients.matchAll({ type: 'window' }).then(clientList => { // If app is already open, focus it for (const client of clientList) { if (client.url.includes('maps.bibbit.duckdns.org') && 'focus' in client) { return client.focus(); } } // If app is not open, open it if (clients.openWindow) { return clients.openWindow('/'); } }) ); });