Files
game-timer/sw.js
2025-03-26 05:04:50 +01:00

189 lines
5.5 KiB
JavaScript

// Service Worker version
const CACHE_VERSION = 'v1.0.0';
const CACHE_NAME = `game-timer-${CACHE_VERSION}`;
// Files to cache
const CACHE_FILES = [
'/',
'/index.html',
'/app.js',
'/audio.js',
'/deeplinks.js',
'/styles.css',
'/manifest.json',
'/icons/android-chrome-192x192.png',
'/icons/android-chrome-512x512.png',
'/icons/apple-touch-icon.png',
'/icons/favicon-32x32.png',
'/icons/favicon-16x16.png'
];
// Valid deep link actions
const VALID_ACTIONS = ['start', 'pause', 'toggle', 'nextplayer', 'reset'];
// Install event - Cache files
self.addEventListener('install', event => {
console.log('[ServiceWorker] Install');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(CACHE_FILES);
})
.then(() => {
console.log('[ServiceWorker] Skip waiting on install');
return self.skipWaiting();
})
);
});
// Activate event - Clean old caches
self.addEventListener('activate', event => {
console.log('[ServiceWorker] Activate');
event.waitUntil(
caches.keys().then(keyList => {
return Promise.all(keyList.map(key => {
if (key !== CACHE_NAME) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
.then(() => {
console.log('[ServiceWorker] Claiming clients');
return self.clients.claim();
})
);
});
// Fetch event - Serve from cache, fallback to network
self.addEventListener('fetch', event => {
console.log('[ServiceWorker] Fetch', event.request.url);
// For navigation requests that include our deep link parameters,
// skip the cache and go straight to network
if (event.request.mode === 'navigate') {
const url = new URL(event.request.url);
// Check if request has action parameter or hash
if (url.searchParams.has('action') || url.hash.includes('action=')) {
console.log('[ServiceWorker] Processing deep link navigation');
// Verify the action is valid
const action = url.searchParams.get('action') ||
new URLSearchParams(url.hash.substring(1)).get('action');
if (action && VALID_ACTIONS.includes(action)) {
console.log('[ServiceWorker] Valid action found:', action);
// For navigation requests with valid actions, let the request go through
// so our app can handle the deep link
return;
}
}
}
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request)
.then(res => {
// Check if we should cache this response
if (shouldCacheResponse(event.request, res)) {
return caches.open(CACHE_NAME)
.then(cache => {
console.log('[ServiceWorker] Caching new resource:', event.request.url);
cache.put(event.request, res.clone());
return res;
});
}
return res;
});
})
.catch(error => {
console.log('[ServiceWorker] Fetch failed; returning offline page', error);
// You could return a custom offline page here
})
);
});
// Helper function to determine if a response should be cached
function shouldCacheResponse(request, response) {
// Only cache GET requests
if (request.method !== 'GET') return false;
// Don't cache errors
if (!response || response.status !== 200) return false;
// Check if URL should be cached
const url = new URL(request.url);
// Don't cache query parameters (except common ones for content)
if (url.search && !url.search.match(/\?(v|version|cache)=/)) return false;
return true;
}
// Handle deep links from other apps (including Flic)
self.addEventListener('message', event => {
if (event.data && event.data.type === 'PROCESS_ACTION') {
const action = event.data.action;
console.log('[ServiceWorker] Received action message:', action);
// Validate the action
if (!VALID_ACTIONS.includes(action)) {
console.warn('[ServiceWorker] Invalid action received:', action);
return;
}
// Broadcast the action to all clients
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({
type: 'ACTION',
action: action
});
});
});
}
});
self.addEventListener('push', event => {
console.log('[ServiceWorker] Push received');
const data = event.data ? event.data.json() : {};
const title = data.title || 'Game Timer Notification';
const options = {
body: data.body || 'You have a new notification',
icon: '/icons/android-chrome-192x192.png',
badge: '/icons/android-chrome-192x192.png',
data: data
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// This helps with navigation after app is installed
self.addEventListener('notificationclick', event => {
console.log('[ServiceWorker] Notification click received');
event.notification.close();
// Handle the notification click
event.waitUntil(
self.clients.matchAll({ type: 'window' })
.then(clientList => {
for (const client of clientList) {
if (client.url.startsWith(self.location.origin) && 'focus' in client) {
return client.focus();
}
}
if (self.clients.openWindow) {
return self.clients.openWindow('/');
}
})
);
});