const CACHE_NAME = 'jarvis-real-ar-v5' const BASE_PATH = new URL(self.registration.scope).pathname.replace(/\/?$/, '/') const shellURL = path => new URL(path, self.registration.scope).href const START_URL = shellURL('./?device=POCO-M5&mode=vision_pro&source=manifest') const APP_SHELL = [ shellURL('./'), shellURL('./index.html'), shellURL('./manifest.json'), START_URL, shellURL('./ui/panels.css'), shellURL('./ui/mobile.css'), shellURL('./ui/hud.css'), shellURL('./ui/animations.css'), ] function isSameOriginAsset(url) { return url.origin === self.location.origin } function discoverAppAssets(html) { if (!html) return [] const urls = new Set() const re = /<(?:script|link)\b[^>]*(?:\ssrc|\shref)=["']([^"']+)["'][^>]*>/gi for (const match of html.matchAll(re)) { try { const url = new URL(match[1], shellURL('./index.html')) if (isSameOriginAsset(url)) urls.add(url.href) } catch { // Ignore malformed asset URLs. } } return [...urls] } function discoverDynamicImports(js, baseUrl) { if (!js) return [] const urls = new Set() const re = /import\(\s*["']([^"']+)["']\s*\)/g for (const match of js.matchAll(re)) { try { const url = new URL(match[1], baseUrl) if (isSameOriginAsset(url)) urls.add(url.href) } catch { // Ignore malformed dynamic import specifiers. } } return [...urls] } async function cacheAppShell() { const cache = await caches.open(CACHE_NAME) const urls = new Set(APP_SHELL) const seen = new Set() const queue = [...APP_SHELL] try { const response = await fetch(shellURL('./index.html'), { cache: 'reload' }) if (response.ok) { const html = await response.text() discoverAppAssets(html).forEach(url => { urls.add(url) queue.push(url) }) await cache.put(shellURL('./index.html'), new Response(html, { status: response.status, statusText: response.statusText, headers: response.headers, })) } } catch { // Shell HTML is best-effort during install. } while (queue.length) { const url = queue.shift() if (!url || seen.has(url)) continue seen.add(url) try { const response = await fetch(url, { cache: 'reload' }) const contentType = response.headers.get('content-type') || '' const expectedCSS = url.endsWith('.css') const expectedJS = url.endsWith('.js') if (response.ok && (!expectedCSS || contentType.includes('text/css'))) { await cache.put(url, response.clone()) if (expectedJS) { const js = await response.text() discoverDynamicImports(js, url).forEach(nextUrl => { if (!seen.has(nextUrl)) queue.push(nextUrl) }) } } } catch { await Promise.resolve() } } } self.addEventListener('install', event => { event.waitUntil(cacheAppShell().then(() => self.skipWaiting())) }) self.addEventListener('activate', event => { event.waitUntil( caches.keys() .then(keys => Promise.all(keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)))) .then(() => self.clients.claim()) ) }) self.addEventListener('fetch', event => { const request = event.request if (request.method !== 'GET') return const url = new URL(request.url) if (url.origin !== self.location.origin) return const relativePath = url.pathname.startsWith(BASE_PATH) ? url.pathname.slice(BASE_PATH.length - 1) : url.pathname if (relativePath.startsWith('/api/') || relativePath.startsWith('/scene-ws')) return event.respondWith( fetch(request) .then(response => { if (response.ok) { const copy = response.clone() caches.open(CACHE_NAME).then(cache => cache.put(request, copy)).catch(() => undefined) } return response }) .catch(() => caches.match(request).then(cached => { if (cached) return cached if (request.mode === 'navigate') { return caches.match(shellURL('./')).then(shell => shell || caches.match(shellURL('./index.html'))) } return new Response('Offline: this AR asset is not cached.', { status: 503, headers: { 'content-type': 'text/plain' } }) })) ) })