// The Nexus — Service Worker // Cache-first for assets, network-first for API calls const CACHE_NAME = 'nexus-v1'; const ASSET_CACHE = 'nexus-assets-v1'; const CORE_ASSETS = [ '/', '/index.html', '/app.js', '/style.css', '/manifest.json', '/ws-client.js', 'https://unpkg.com/three@0.183.0/build/three.module.js', 'https://unpkg.com/three@0.183.0/examples/jsm/controls/OrbitControls.js', 'https://unpkg.com/three@0.183.0/examples/jsm/postprocessing/EffectComposer.js', 'https://unpkg.com/three@0.183.0/examples/jsm/postprocessing/RenderPass.js', 'https://unpkg.com/three@0.183.0/examples/jsm/postprocessing/UnrealBloomPass.js', 'https://unpkg.com/three@0.183.0/examples/jsm/postprocessing/ShaderPass.js', 'https://unpkg.com/three@0.183.0/examples/jsm/shaders/CopyShader.js', 'https://unpkg.com/three@0.183.0/examples/jsm/shaders/LuminosityHighPassShader.js', ]; // Install: precache core assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(ASSET_CACHE).then((cache) => cache.addAll(CORE_ASSETS)) .then(() => self.skipWaiting()) ); }); // Activate: clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all( keys .filter((key) => key !== CACHE_NAME && key !== ASSET_CACHE) .map((key) => caches.delete(key)) ) ).then(() => self.clients.claim()) ); }); self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Network-first for API calls (Gitea / WebSocket upgrades / portals.json live data) if ( url.pathname.startsWith('/api/') || url.hostname.includes('143.198.27.163') || request.headers.get('Upgrade') === 'websocket' ) { event.respondWith(networkFirst(request)); return; } // Cache-first for everything else (local assets + CDN) event.respondWith(cacheFirst(request)); }); async function cacheFirst(request) { const cached = await caches.match(request); if (cached) return cached; try { const response = await fetch(request); if (response.ok) { const cache = await caches.open(ASSET_CACHE); cache.put(request, response.clone()); } return response; } catch { // Offline and not cached — return a minimal fallback for navigation if (request.mode === 'navigate') { const fallback = await caches.match('/index.html'); if (fallback) return fallback; } return new Response('Offline', { status: 503, statusText: 'Service Unavailable' }); } } async function networkFirst(request) { try { const response = await fetch(request); if (response.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, response.clone()); } return response; } catch { const cached = await caches.match(request); return cached || new Response('Offline', { status: 503, statusText: 'Service Unavailable' }); } }