Repository navigation
Expand file tree
/
Copy pathsw.js
More file actions
270 lines (242 loc) · 9.87 KB
/
Copy pathsw.js
File metadata and controls
270 lines (242 loc) · 9.87 KB
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/* GlitchRealm Service Worker - Advanced Optimizations */
const CACHE_PREFIX = 'gr-v11'; // v11: Fix SW intercepting Firebase CDN + auth bridge hang
const STATIC_CACHE = `${CACHE_PREFIX}-static`;
const RUNTIME_CACHE = `${CACHE_PREFIX}-runtime`;
const IMAGE_CACHE = `${CACHE_PREFIX}-images`;
const PRECACHE_URLS = [
'/offline.html',
'/css/styles.css',
'/assets/Favicon and Icons/favicon.svg',
'/assets/Favicon and Icons/favicon.ico',
'/assets/Favicon and Icons/web-app-manifest-192x192.png'
];
// External resources we want to cache-fast-path (opaque responses ok)
const BMC_WIDGET_URL = 'https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js';
// Same-origin paths that should NEVER be cached (always fetch fresh)
const NEVER_CACHE_PATHS = [
'/js/firebase-core.js',
'/subdomains/news/firebase-core.js',
'/js/script.js',
'/subdomains/news/script.js',
'/js/portal-auth.js',
'/js/signin.js',
'/js/auth-redirect-handler.js',
'/auth-bridge'
];
// Only applied to same-origin requests (cross-origin handled separately)
function shouldNeverCache(url) {
const pathname = url.pathname;
// Never cache HTML pages
if (pathname.endsWith('.html') || pathname === '/') return true;
// Never cache same-origin firebase scripts
if (pathname.includes('firebase')) return true;
// Never cache specific paths
return NEVER_CACHE_PATHS.some(pattern => pathname.includes(pattern));
}
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => Promise.all(
keys.filter((k) => !k.startsWith(CACHE_PREFIX)).map((k) => caches.delete(k))
))
);
self.clients.claim();
// Enable navigation preload for faster HTML fetch when supported
if ('navigationPreload' in self.registration) {
try { self.registration.navigationPreload.enable(); } catch(e) {}
}
});
// Strategy helpers
async function staleWhileRevalidate(request, cacheName = RUNTIME_CACHE) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const networkPromise = fetch(request, { cache: 'no-cache' }).then((response) => {
if (response && response.status === 200 && request.method === 'GET') {
cache.put(request, response.clone());
}
return response;
}).catch(() => cached);
return cached || networkPromise;
}
async function cacheFirst(request, cacheName = RUNTIME_CACHE) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
// Accept opaque responses (cross-origin no-cors) as cacheable as well
const ok = (response && response.status === 200) || (response && response.type === 'opaque');
if (ok) {
try { cache.put(request, response.clone()); } catch (e) { /* ignore put errors for opaque */ }
}
return response;
} catch (e) {
// Network failed: fall back to cached asset if available
if (cached) return cached;
// If this was an image (e.g., favicon) try several known fallbacks (handles news subdomain)
if (request.destination === 'image') {
const FALLBACK_IMAGES = [
'/assets/Favicon and Icons/favicon.ico',
'/subdomains/news/assets/Favicon and Icons/favicon.ico',
'/favicon.ico',
'/assets/Favicon and Icons/favicon.svg'
];
for (const path of FALLBACK_IMAGES) {
try {
const f = await caches.match(path);
if (f) return f;
} catch (e2) { /* ignore */ }
}
// Final fallback: return a minimal inline SVG favicon so the page doesn't error
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#00fff9"/></svg>';
return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml' } });
}
// As a last resort, rethrow so caller can handle or browser gets the network error
throw e;
}
}
async function networkFirst(request, cacheName = RUNTIME_CACHE, timeout = 3000) {
const cache = await caches.open(cacheName);
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(request, { signal: controller.signal, cache: 'no-cache' });
clearTimeout(timeoutId);
if (response && response.status === 200) cache.put(request, response.clone());
return response;
} catch (e) {
const cached = await cache.match(request);
if (cached) return cached;
throw e;
}
}
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Bypass non-GET requests
if (request.method !== 'GET') return;
// --- Cross-origin handling FIRST (before shouldNeverCache) ---
// This ensures Firebase CDN (gstatic.com/firebasejs/...) is NOT intercepted
// Cache Google Fonts aggressively (cross-origin allowed)
if (url.origin === 'https://fonts.gstatic.com') {
event.respondWith(cacheFirst(request));
return;
}
if (url.origin === 'https://fonts.googleapis.com') {
event.respondWith(staleWhileRevalidate(request));
return;
}
// Buy Me a Coffee widget script: cache-first after first fetch to remove repeat lag
if (request.url === BMC_WIDGET_URL) {
event.respondWith(cacheFirst(request));
return;
}
// Supabase storage: Cache images from Supabase CDN (cross-origin with mode: 'no-cors')
if (url.hostname.includes('supabase.co')) {
event.respondWith(
(async () => {
try {
const response = await fetch(request, { mode: 'no-cors' });
// Cache the opaque response
const cache = await caches.open(IMAGE_CACHE);
cache.put(request, response.clone());
return response;
} catch (e) {
// Try cached version if network fails
const cached = await caches.match(request);
if (cached) return cached;
throw e;
}
})()
);
return;
}
// All other cross-origin requests: let the browser handle natively
// (Firebase CDN, Firebase APIs, etc. pass through untouched)
if (url.origin !== self.location.origin) return;
// --- Same-origin handling ---
// HTML/navigation: Network-first with fallback to offline page (don't cache HTML).
// Must run before shouldNeverCache() so .html and / navigations consume the
// navigation preload response instead of fetching the document a second time.
// Handle both `request.destination === 'document'` and navigation-mode requests
if (request.mode === 'navigate' || request.destination === 'document') {
event.respondWith(
(async () => {
try {
// Use navigation preload if available (safe await even if undefined)
const preload = await (event.preloadResponse || Promise.resolve(null));
const response = preload || await fetch(request, { cache: 'no-cache' });
// DO NOT cache HTML responses - always fetch fresh for auth state
return response;
} catch (e) {
// Network failed — serve offline page immediately (don't use cached HTML)
const offlinePage = await caches.match('/offline.html');
if (offlinePage) return offlinePage;
// As last resort, return a generic Response
return new Response('Offline - Cannot reach GlitchRealm servers', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain' }
});
}
})()
);
return;
}
// NEVER cache auth-sensitive or HTML files
if (shouldNeverCache(url)) {
event.respondWith(
fetch(request, { cache: 'no-cache' }).catch(() => caches.match('/offline.html'))
);
return;
}
// Assets: images via Netlify Image Transformations when possible; fonts/images cache-first; CSS/JS SWR
if (request.destination === 'image' || url.pathname.startsWith('/assets/')) {
// Only transform same-origin /assets/ images; skip SVG/ICO/GIF
const isSameOrigin = url.origin === self.location.origin;
const underAssets = url.pathname.startsWith('/assets/');
const ext = url.pathname.split('.').pop().toLowerCase();
const transformable = ['jpg','jpeg','png','bmp','tiff','webp','avif'].includes(ext);
if (isSameOrigin && underAssets && transformable) {
const accept = request.headers.get('Accept') || '';
const prefersAvif = accept.includes('image/avif');
const prefersWebp = accept.includes('image/webp');
const fm = prefersAvif ? 'avif' : (prefersWebp ? 'webp' : null);
if (fm) {
const transformURL = new URL('/.netlify/images', self.location.origin);
transformURL.searchParams.set('url', url.pathname);
transformURL.searchParams.set('fm', fm);
transformURL.searchParams.set('q', '80'); // Increased quality to 80
transformURL.searchParams.set('w', '1200'); // Max width to reduce oversized images
const transformedRequest = new Request(transformURL.toString(), {
method: 'GET',
headers: request.headers,
credentials: request.credentials,
mode: 'cors'
});
event.respondWith(cacheFirst(transformedRequest, IMAGE_CACHE));
return;
}
}
// Fallback: standard cache-first for images and /assets
event.respondWith(cacheFirst(request, IMAGE_CACHE));
return;
}
// CSS: stale-while-revalidate (cosmetic, safe to serve cached)
if (request.destination === 'style') {
event.respondWith(staleWhileRevalidate(request));
return;
}
// JS: network-first so users always get fresh scripts (fixes ctrl+shift+r requirement)
if (request.destination === 'script') {
event.respondWith(networkFirst(request, RUNTIME_CACHE, 4000));
return;
}
// Default: network-first
event.respondWith(networkFirst(request, RUNTIME_CACHE, 4000));
});