Spaces:
Running
Running
| /** | |
| * Keet Service Worker | |
| * Story 3.1: Offline-first caching strategy | |
| * | |
| * Strategy: | |
| * - App shell (HTML, CSS, JS): Cache-first, update in background | |
| * - Model files: Cache-first (large files, rarely change) | |
| * - API/Dynamic: Network-first with fallback | |
| * - Cross-origin isolation headers injected for SharedArrayBuffer / WebGPU support | |
| */ | |
| const CACHE_NAME = 'keet-v2'; | |
| const MODEL_CACHE = 'keet-models-v1'; | |
| // Base path from SW script URL (works at / and /keet/ on GitHub Pages) | |
| const BASE = (() => { | |
| const path = self.location.pathname; | |
| const i = path.lastIndexOf('/'); | |
| return i >= 0 ? path.slice(0, i + 1) : '/'; | |
| })(); | |
| // App shell files to pre-cache (base-relative) | |
| const APP_SHELL = [ | |
| BASE, | |
| BASE + 'index.html', | |
| BASE + 'manifest.json', | |
| ]; | |
| // Model file patterns (cached on-demand) | |
| const MODEL_PATTERNS = [ | |
| /\.onnx\.data$/, | |
| /\.onnx$/, | |
| /\.bin$/, | |
| /vocab\.txt$/, | |
| /tokenizer\.json$/, | |
| ]; | |
| /** | |
| * Add Cross-Origin Isolation headers to a response. | |
| * This enables SharedArrayBuffer and WebGPU on static hosts (e.g. GitHub Pages) | |
| * that don't allow custom response headers. | |
| * Equivalent to what coi-serviceworker.js does in the parakeet.js demo. | |
| */ | |
| function addCOIHeaders(response) { | |
| if (response.status === 0) { | |
| return response; | |
| } | |
| const newHeaders = new Headers(response.headers); | |
| newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp"); | |
| newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin"); | |
| newHeaders.set("Cross-Origin-Opener-Policy", "same-origin"); | |
| return new Response(response.body, { | |
| status: response.status, | |
| statusText: response.statusText, | |
| headers: newHeaders, | |
| }); | |
| } | |
| // Install event - pre-cache app shell | |
| self.addEventListener('install', (event) => { | |
| console.log('[SW] Installing...'); | |
| event.waitUntil( | |
| caches.open(CACHE_NAME) | |
| .then((cache) => { | |
| console.log('[SW] Pre-caching app shell'); | |
| return cache.addAll(APP_SHELL); | |
| }) | |
| .then(() => self.skipWaiting()) | |
| ); | |
| }); | |
| // Activate event - clean old caches | |
| self.addEventListener('activate', (event) => { | |
| console.log('[SW] Activating...'); | |
| event.waitUntil( | |
| caches.keys() | |
| .then((cacheNames) => { | |
| return Promise.all( | |
| cacheNames | |
| .filter((name) => name !== CACHE_NAME && name !== MODEL_CACHE) | |
| .map((name) => { | |
| console.log('[SW] Deleting old cache:', name); | |
| return caches.delete(name); | |
| }) | |
| ); | |
| }) | |
| .then(() => self.clients.claim()) | |
| ); | |
| }); | |
| // Fetch event - serve from cache or network, inject COI headers | |
| self.addEventListener('fetch', (event) => { | |
| const url = new URL(event.request.url); | |
| // Skip non-GET requests | |
| if (event.request.method !== 'GET') return; | |
| // Skip chrome-extension and other non-http(s) requests | |
| if (!url.protocol.startsWith('http')) return; | |
| // Check if this is a model file | |
| const isModelFile = MODEL_PATTERNS.some((pattern) => pattern.test(url.pathname)); | |
| if (isModelFile) { | |
| // Model files: Cache-first (they're large and rarely change) | |
| event.respondWith( | |
| caches.open(MODEL_CACHE) | |
| .then((cache) => { | |
| return cache.match(event.request) | |
| .then((cached) => { | |
| if (cached) { | |
| console.log('[SW] Model from cache:', url.pathname); | |
| return cached; | |
| } | |
| console.log('[SW] Fetching model:', url.pathname); | |
| return fetch(event.request) | |
| .then((response) => { | |
| if (response.ok) { | |
| cache.put(event.request, response.clone()); | |
| } | |
| return response; | |
| }); | |
| }); | |
| }) | |
| ); | |
| return; | |
| } | |
| // App shell: Cache-first with network fallback + COI headers | |
| if (url.origin === self.location.origin) { | |
| event.respondWith( | |
| caches.match(event.request) | |
| .then((cached) => { | |
| const fetchPromise = fetch(event.request) | |
| .then((response) => { | |
| if (response.ok) { | |
| const responseClone = response.clone(); | |
| caches.open(CACHE_NAME) | |
| .then((cache) => cache.put(event.request, responseClone)); | |
| } | |
| return addCOIHeaders(response); | |
| }) | |
| .catch(() => cached); | |
| return cached ? addCOIHeaders(cached) : fetchPromise; | |
| }) | |
| ); | |
| return; | |
| } | |
| // External resources: Network-first + COI headers | |
| event.respondWith( | |
| fetch(event.request) | |
| .then((response) => addCOIHeaders(response)) | |
| .catch(() => caches.match(event.request)) | |
| ); | |
| }); | |
| // Message handler for cache management | |
| self.addEventListener('message', (event) => { | |
| if (event.data.type === 'SKIP_WAITING') { | |
| self.skipWaiting(); | |
| } | |
| if (event.data.type === 'CLEAR_MODEL_CACHE') { | |
| caches.delete(MODEL_CACHE) | |
| .then(() => { | |
| console.log('[SW] Model cache cleared'); | |
| event.ports[0].postMessage({ success: true }); | |
| }); | |
| } | |
| }); | |