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 });
            });
    }
});