Spaces:
Running
Running
File size: 5,783 Bytes
b8cc2bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | /**
* 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 });
});
}
});
|