// JAYNES MAX — Service Worker // Hifadhi "app shell" + maktaba za Shaka/hls ili app ifunguke hata bila mtandao. // API, video na streams HAZIHIFADHIWI (zinahitaji mtandao). Movie zilizopakuliwa ziko OPFS ndani ya app. const VERSION = 'jm-v9'; const RUNTIME = 'jm-cdn-v1'; const SHELL = ['./', './index.html', './manifest.webmanifest', './config.js', './icon-192.png', './icon-512.png', './icon-maskable-512.png', './apple-touch-icon.png']; const CDN_HOST = 'cdnjs.cloudflare.com'; const CDN = [ 'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.13/hls.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.10.0/shaka-player.compiled.js', ]; self.addEventListener('install', (e) => { e.waitUntil((async () => { // Kila faili peke yake: faili moja likikosekana, install HAIFEI (addAll ingefeli yote) const shell = await caches.open(VERSION); await Promise.all(SHELL.map((u) => shell.add(u).catch(() => {}))); const rt = await caches.open(RUNTIME); await Promise.all(CDN.map((u) => fetch(u, { mode: 'no-cors' }).then((r) => rt.put(u, r)).catch(() => {}))); await self.skipWaiting(); })()); }); self.addEventListener('activate', (e) => { e.waitUntil( caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== VERSION && k !== RUNTIME).map((k) => caches.delete(k)))) .then(() => self.clients.claim()) ); }); function shellFallback() { return caches.match('./index.html', { ignoreSearch: true }) .then((r) => r || caches.match('./', { ignoreSearch: true })); } self.addEventListener('fetch', (e) => { const req = e.request; if (req.method !== 'GET') return; const url = new URL(req.url); // Maktaba za CDN (hls/shaka): cache-first ili zipatikane offline na zipakie haraka if (url.hostname === CDN_HOST) { e.respondWith( caches.match(req).then((hit) => hit || fetch(req).then((res) => { if (res && (res.ok || res.type === 'opaque')) { const copy = res.clone(); caches.open(RUNTIME).then((c) => c.put(req, copy)); } return res; })) ); return; } // Usiguse chochote kingine nje ya asili yetu (Supabase API, video, picha za CDN n.k.) if (url.origin !== self.location.origin) return; // Range requests (video/sauti) — acha mtandao ushughulikie if (req.headers.has('range')) return; // Ukurasa mkuu: network-first, lakini mtandao ukichelewa (>3.5s) au ukikosekana → cache. // Hii inazuia app "kuning'inia" ukiwa na mtandao dhaifu / bila bando. if (req.mode === 'navigate') { e.respondWith(new Promise((resolve) => { let settled = false; const settle = (r) => { if (!settled && r) { settled = true; resolve(r); } }; const timer = setTimeout(() => { shellFallback().then(settle); }, 3500); fetch(req).then((res) => { clearTimeout(timer); if (res && res.ok) { const copy = res.clone(); caches.open(VERSION).then((c) => c.put('./index.html', copy)); } settle(res); }).catch(() => { clearTimeout(timer); shellFallback().then((r) => { if (!settled) { settled = true; resolve(r || Response.error()); } }); }); })); return; } // config.js: network-first (funguo/URL zikibadilishwa zifike mara moja), cache ikiwa offline if (url.pathname.endsWith('/config.js')) { e.respondWith( fetch(req).then((res) => { if (res.ok) { const copy = res.clone(); caches.open(VERSION).then((c) => c.put(req, copy)); } return res; }) .catch(() => caches.match(req, { ignoreSearch: true })) ); return; } // Faili tuli (icons, manifest): cache-first e.respondWith( caches.match(req).then((hit) => hit || fetch(req).then((res) => { if (res.ok) { const copy = res.clone(); caches.open(VERSION).then((c) => c.put(req, copy)); } return res; })) ); }); // ── Web Push (inatoka Supabase Edge Function 'send-push') ── self.addEventListener('push', (e) => { let d = {}; try { d = e.data ? e.data.json() : {}; } catch (_) { d = { body: e.data && e.data.text() }; } const opts = { body: d.body || '', icon: './icon-192.png', badge: './icon-192.png', tag: d.id ? 'jm-ann-' + d.id : undefined, data: { url: d.url || './' }, }; if (d.image) opts.image = d.image; // picha kubwa (Android/Chrome) e.waitUntil(self.registration.showNotification(d.title || 'JAYNES MAX', opts)); }); // Kubonyeza notification: fungua app moja kwa moja kwenye ukurasa ulioainishwa (#/title/ID, #/live, ...) self.addEventListener('notificationclick', (e) => { e.notification.close(); const raw = (e.notification.data && e.notification.data.url) || './'; const target = new URL(raw, self.registration.scope).href; e.waitUntil((async () => { const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); for (const c of all) { if (c.url.startsWith(self.registration.scope)) { try { c.postMessage({ type: 'jm-open', url: target }); } catch (_) {} if ('focus' in c) await c.focus(); return; } } await self.clients.openWindow(target); })()); });