You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

258 lines
7.1 KiB

// 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('/');
}
})
);
});