const SHELL = 'bible-shell-v2'; const API = 'bible-api-v1'; // Chapter text, interlinear cross-refs, and (BSB) chapter audio all come from // these origins — cache them so the reader, reading plans, and audio all keep // working offline once you've visited a passage before. const BIBLE_ORIGINS = ['https://bible.helloao.org', 'https://bolls.life', 'https://audio.bible.helloao.org']; self.addEventListener('install', (e) => { e.waitUntil( caches.open(SHELL) .then(c => c.addAll(['/', '/index.html'])) .then(() => self.skipWaiting()) ); }); self.addEventListener('activate', (e) => { e.waitUntil( caches.keys() .then(keys => Promise.all( keys.filter(k => k !== SHELL && k !== API).map(k => caches.delete(k)) )) .then(() => self.clients.claim()) ); }); self.addEventListener('fetch', (e) => { const { request } = e; const url = request.url; // Never intercept this app's own API routes — let Express handle them. (Only same-origin ones: the // Bible text at bible.helloao.org/api/... must pass through to the offline cache below.) const { origin, pathname } = new URL(url); if (origin === self.location.origin && pathname.startsWith('/api/')) return; // External Bible API (text, cross-refs, audio): network-first, fall back to cache. // Audio elements often issue Range requests, which come back as 206 Partial // Content — the Cache API throws on those, so only cache full (200) responses. if (BIBLE_ORIGINS.some(o => url.startsWith(o))) { e.respondWith( fetch(request) .then(res => { // Clone *now*: once the page starts reading the body a later clone() throws and nothing is saved. if (res.status === 200) { const copy = res.clone(); e.waitUntil(caches.open(API).then(c => c.put(request, copy)).catch(() => {})); } return res; }) .catch(() => caches.open(API).then(c => c.match(request)).then( hit => hit ?? new Response('{"error":"offline"}', { status: 503, headers: { 'Content-Type': 'application/json' }, }) ) ) ); return; } // App-shell navigation: network-first, fall back to cached index.html if (request.mode === 'navigate') { e.respondWith( fetch(request) .then(res => { if (res.ok) { const copy = res.clone(); e.waitUntil(caches.open(SHELL).then(c => c.put(request, copy)).catch(() => {})); } return res; }) .catch(() => caches.open(SHELL).then(c => c.match('/index.html').then(hit => hit ?? c.match('/')) ) ) ); return; } // Same-origin static assets (hashed JS/CSS/images): cache-first if (url.startsWith(self.location.origin)) { e.respondWith( caches.open(SHELL).then(c => c.match(request).then(hit => { if (hit) return hit; return fetch(request).then(res => { if (res.ok) { const copy = res.clone(); e.waitUntil(c.put(request, copy).catch(() => {})); } return res; }); }) ) ); } });