/* Service Worker for Church Music Management System */ const CACHE_NAME = "church-music-v1"; const API_CACHE_NAME = "church-music-api-v1"; // Static assets to cache immediately const STATIC_ASSETS = [ "/", "/index.html", "/static/css/main.css", "/static/js/main.js", "/favicon.ico", "/manifest.json", ]; // API endpoints to cache with time-based expiration const API_CACHE_DURATION = 3 * 60 * 1000; // 3 minutes // Install event - cache static assets self.addEventListener("install", (event) => { console.log("[Service Worker] Installing..."); event.waitUntil( caches .open(CACHE_NAME) .then((cache) => { console.log("[Service Worker] Caching static assets"); // Don't fail if some assets are missing return Promise.allSettled( STATIC_ASSETS.map((url) => cache .add(url) .catch((err) => console.log(`[Service Worker] Failed to cache ${url}:`, err) ) ) ); }) .then(() => self.skipWaiting()) // Activate immediately ); }); // Activate event - clean up old caches self.addEventListener("activate", (event) => { console.log("[Service Worker] Activating..."); event.waitUntil( caches .keys() .then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME && name !== API_CACHE_NAME) .map((name) => { console.log("[Service Worker] Deleting old cache:", name); return caches.delete(name); }) ); }) .then(() => self.clients.claim()) // Take control immediately ); }); // Fetch event - serve from cache with network fallback self.addEventListener("fetch", (event) => { const { request } = event; const url = new URL(request.url); // Skip cross-origin requests if (url.origin !== location.origin) { return; } // Handle API requests separately if (url.pathname.startsWith("/api/")) { event.respondWith(handleApiRequest(request)); return; } // Handle static assets with cache-first strategy event.respondWith( caches.match(request).then((cachedResponse) => { if (cachedResponse) { console.log("[Service Worker] Serving from cache:", request.url); return cachedResponse; } // Not in cache, fetch from network return fetch(request) .then((response) => { // Cache successful responses if (response && response.status === 200) { const responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(request, responseClone); }); } return response; }) .catch((error) => { console.error("[Service Worker] Fetch failed:", error); // Return offline page if available return caches.match("/offline.html").catch(() => { return new Response("Offline - Please check your connection", { status: 503, statusText: "Service Unavailable", headers: new Headers({ "Content-Type": "text/plain", }), }); }); }); }) ); }); // Handle API requests with network-first, cache-fallback strategy async function handleApiRequest(request) { const url = new URL(request.url); // Only cache GET requests if (request.method !== "GET") { return fetch(request); } try { // Try network first const response = await fetch(request); if (response && response.status === 200) { // Clone and cache the response with timestamp const responseClone = response.clone(); const cache = await caches.open(API_CACHE_NAME); // Add timestamp header for expiration const cachedResponse = new Response(await responseClone.blob(), { status: responseClone.status, statusText: responseClone.statusText, headers: { ...Object.fromEntries(responseClone.headers.entries()), "sw-cached-at": Date.now().toString(), }, }); await cache.put(request, cachedResponse); console.log("[Service Worker] Cached API response:", url.pathname); } return response; } catch (error) { console.log("[Service Worker] Network failed, trying cache:", url.pathname); // Network failed, try cache const cachedResponse = await caches.match(request); if (cachedResponse) { // Check if cache is still fresh const cachedAt = cachedResponse.headers.get("sw-cached-at"); const now = Date.now(); if (cachedAt && now - parseInt(cachedAt) < API_CACHE_DURATION) { console.log("[Service Worker] Serving fresh cached API response"); return cachedResponse; } else { console.log("[Service Worker] Cached API response expired"); // Return stale cache with warning header return new Response(await cachedResponse.blob(), { status: cachedResponse.status, statusText: cachedResponse.statusText, headers: { ...Object.fromEntries(cachedResponse.headers.entries()), "sw-cache-status": "stale", }, }); } } // No cache available throw error; } } // Listen for messages from the client self.addEventListener("message", (event) => { if (event.data && event.data.type === "SKIP_WAITING") { self.skipWaiting(); } if (event.data && event.data.type === "CLEAR_CACHE") { caches .keys() .then((cacheNames) => { return Promise.all(cacheNames.map((name) => caches.delete(name))); }) .then(() => { event.ports[0].postMessage({ success: true }); }); } });