// Biblical Guide Service Worker const CACHE_NAME = 'biblical-guide-v1.0.1'; const STATIC_CACHE = 'biblical-guide-static-v1.0.1'; const DYNAMIC_CACHE = 'biblical-guide-dynamic-v1.0.1'; const BIBLE_CACHE = 'biblical-guide-bible-v1.0.1'; // Static resources that should be cached immediately const STATIC_ASSETS = [ '/', '/manifest.json', '/icon-192.png', '/icon-512.png', '/biblical-guide-og-image.png', '/offline.html', // We'll create this fallback page ]; // Bible API endpoints that should be cached const BIBLE_API_PATTERNS = [ /\/api\/bible\/versions/, /\/api\/bible\/books/, /\/api\/bible\/chapter/, /\/api\/bible\/verses/, ]; // Dynamic content that should be cached with network-first strategy const DYNAMIC_PATTERNS = [ /\/api\/daily-verse/, /\/api\/search/, ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( Promise.all([ caches.open(STATIC_CACHE).then((cache) => { console.log('[SW] Caching static assets'); return cache.addAll(STATIC_ASSETS.map(url => new Request(url, {credentials: 'same-origin'}))); }), caches.open(BIBLE_CACHE).then(() => { console.log('[SW] Bible cache initialized'); }) ]).then(() => { console.log('[SW] Installation complete'); // Skip waiting to activate immediately return self.skipWaiting(); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( Promise.all([ // Clean up old caches caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE && cacheName !== BIBLE_CACHE && cacheName !== CACHE_NAME) { console.log('[SW] Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }), // Take control of all pages immediately self.clients.claim() ]).then(() => { console.log('[SW] Activation complete'); }) ); }); // Fetch event - handle all network requests self.addEventListener('fetch', (event) => { const { request } = event; const { url, method } = request; // Only handle GET requests if (method !== 'GET') { return; } // Handle Bible content downloads (store in Bible cache) if (BIBLE_API_PATTERNS.some(pattern => pattern.test(url))) { event.respondWith(handleBibleRequest(request)); return; } // Handle dynamic content (network-first with cache fallback) if (DYNAMIC_PATTERNS.some(pattern => pattern.test(url))) { event.respondWith(handleDynamicRequest(request)); return; } // Handle static assets (cache-first) if (isStaticAsset(url)) { event.respondWith(handleStaticRequest(request)); return; } // Handle navigation requests (HTML pages) if (request.mode === 'navigate') { event.respondWith(handleNavigationRequest(request)); return; } // Default: network-first for everything else event.respondWith(handleDefaultRequest(request)); }); // Bible content strategy: Cache-first with network update async function handleBibleRequest(request) { const cache = await caches.open(BIBLE_CACHE); const cached = await cache.match(request); if (cached) { console.log('[SW] Serving Bible content from cache:', request.url); // Update cache in background updateBibleCache(request, cache); return cached; } try { console.log('[SW] Fetching Bible content from network:', request.url); const response = await fetch(request); if (response.ok) { // Clone before caching const responseClone = response.clone(); await cache.put(request, responseClone); console.log('[SW] Bible content cached:', request.url); } return response; } catch (error) { console.error('[SW] Bible content fetch failed:', error); throw error; } } // Update Bible cache in background async function updateBibleCache(request, cache) { try { const response = await fetch(request); if (response.ok) { await cache.put(request, response.clone()); console.log('[SW] Bible cache updated:', request.url); } } catch (error) { console.log('[SW] Background Bible cache update failed:', error); } } // Dynamic content strategy: Network-first with cache fallback async function handleDynamicRequest(request) { const cache = await caches.open(DYNAMIC_CACHE); try { console.log('[SW] Fetching dynamic content from network:', request.url); const response = await fetch(request); if (response.ok) { // Cache successful responses await cache.put(request, response.clone()); console.log('[SW] Dynamic content cached:', request.url); } return response; } catch (error) { console.log('[SW] Network failed, checking cache for:', request.url); const cached = await cache.match(request); if (cached) { console.log('[SW] Serving dynamic content from cache:', request.url); return cached; } throw error; } } // Static assets strategy: Cache-first async function handleStaticRequest(request) { const cache = await caches.open(STATIC_CACHE); const cached = await cache.match(request); if (cached) { console.log('[SW] Serving static asset from cache:', request.url); return cached; } try { console.log('[SW] Fetching static asset from network:', request.url); const response = await fetch(request); if (response.ok) { await cache.put(request, response.clone()); console.log('[SW] Static asset cached:', request.url); } return response; } catch (error) { console.error('[SW] Static asset fetch failed:', error); throw error; } } // Navigation strategy: Network-first with offline fallback async function handleNavigationRequest(request) { try { console.log('[SW] Fetching navigation from network:', request.url); const response = await fetch(request); if (response.ok) { // Cache successful navigation responses const cache = await caches.open(DYNAMIC_CACHE); await cache.put(request, response.clone()); } return response; } catch (error) { console.log('[SW] Navigation network failed, checking cache:', request.url); const cache = await caches.open(DYNAMIC_CACHE); const cached = await cache.match(request); if (cached) { console.log('[SW] Serving navigation from cache:', request.url); return cached; } // Return offline fallback page console.log('[SW] Serving offline fallback page'); const offlineCache = await caches.open(STATIC_CACHE); const offlinePage = await offlineCache.match('/offline.html'); return offlinePage || new Response('Offline - Please check your connection', { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'text/plain' } }); } } // Default strategy: Network-first async function handleDefaultRequest(request) { try { return await fetch(request); } catch (error) { console.log('[SW] Default request failed:', request.url); throw error; } } // Helper function to check if URL is a static asset function isStaticAsset(url) { return url.includes('/_next/') || url.includes('/icon-') || url.includes('/manifest.json') || url.includes('/biblical-guide-og-image.png') || url.includes('.css') || url.includes('.js') || url.includes('.woff') || url.includes('.woff2'); } // Background sync for Bible downloads self.addEventListener('sync', (event) => { console.log('[SW] Background sync triggered:', event.tag); if (event.tag === 'download-bible-version') { event.waitUntil(handleBibleDownloadSync()); } }); // Handle Bible version download in background async function handleBibleDownloadSync() { try { console.log('[SW] Processing background Bible download'); // Get pending downloads from IndexedDB const pendingDownloads = await getPendingBibleDownloads(); for (const download of pendingDownloads) { await downloadBibleVersion(download); } } catch (error) { console.error('[SW] Background Bible download failed:', error); } } // Get pending downloads from IndexedDB async function getPendingBibleDownloads() { return new Promise((resolve, reject) => { const request = indexedDB.open('BibleStorage', 1); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['downloads'], 'readonly'); const store = transaction.objectStore('downloads'); const getAllRequest = store.getAll(); getAllRequest.onsuccess = () => { resolve(getAllRequest.result.filter(d => d.status === 'pending')); }; getAllRequest.onerror = () => reject(getAllRequest.error); }; request.onerror = () => reject(request.error); }); } // Download a Bible version async function downloadBibleVersion(download) { try { console.log('[SW] Downloading Bible version:', download.versionId); // Download books list const booksResponse = await fetch(`/api/bible/books?version=${download.versionId}`); if (!booksResponse.ok) throw new Error('Failed to fetch books'); const books = await booksResponse.json(); // Download each chapter for (const book of books.books) { for (let chapter = 1; chapter <= book.chaptersCount; chapter++) { const chapterUrl = `/api/bible/chapter?book=${book.id}&chapter=${chapter}&version=${download.versionId}`; await fetch(chapterUrl); // This will be cached by the fetch handler } } // Mark download as complete await updateDownloadStatus(download.versionId, 'completed'); console.log('[SW] Bible version download completed:', download.versionId); } catch (error) { console.error('[SW] Bible version download failed:', error); await updateDownloadStatus(download.versionId, 'failed'); } } // Update download status in IndexedDB async function updateDownloadStatus(versionId, status) { return new Promise((resolve, reject) => { const request = indexedDB.open('BibleStorage', 1); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['downloads'], 'readwrite'); const store = transaction.objectStore('downloads'); const getRequest = store.get(versionId); getRequest.onsuccess = () => { const download = getRequest.result; if (download) { download.status = status; download.updatedAt = new Date().toISOString(); const putRequest = store.put(download); putRequest.onsuccess = () => resolve(); putRequest.onerror = () => reject(putRequest.error); } else { resolve(); // Download not found, ignore } }; getRequest.onerror = () => reject(getRequest.error); }; request.onerror = () => reject(request.error); }); } // Message handling for communication with the main thread self.addEventListener('message', (event) => { console.log('[SW] Message received:', event.data); if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CLEAR_CACHE') { clearAllCaches().then(() => { event.ports[0].postMessage({success: true}); }); } if (event.data && event.data.type === 'CACHE_BIBLE_VERSION') { const { versionId } = event.data; cacheBibleVersion(versionId).then(() => { event.ports[0].postMessage({success: true, versionId}); }).catch((error) => { event.ports[0].postMessage({success: false, error: error.message, versionId}); }); } }); // Clear all caches async function clearAllCaches() { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map(name => caches.delete(name))); console.log('[SW] All caches cleared'); } // Cache a specific Bible version async function cacheBibleVersion(versionId) { console.log('[SW] Caching Bible version:', versionId); try { // Download and cache all content for this version const booksResponse = await fetch(`/api/bible/books?version=${versionId}`); if (!booksResponse.ok) throw new Error('Failed to fetch books'); const books = await booksResponse.json(); const cache = await caches.open(BIBLE_CACHE); // Cache books list await cache.put(`/api/bible/books?version=${versionId}`, booksResponse.clone()); // Cache each chapter for (const book of books.books) { for (let chapter = 1; chapter <= book.chaptersCount; chapter++) { const chapterUrl = `/api/bible/chapter?book=${book.id}&chapter=${chapter}&version=${versionId}`; const chapterResponse = await fetch(chapterUrl); if (chapterResponse.ok) { await cache.put(chapterUrl, chapterResponse.clone()); } } } console.log('[SW] Bible version cached successfully:', versionId); } catch (error) { console.error('[SW] Failed to cache Bible version:', error); throw error; } } console.log('[SW] Service worker script loaded');