This repository has been archived on 2026-02-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
serengo/src/service-worker.ts
Zias van Nes 96a173b73b feat:use local proxy for media
use local proxy for media so that media doesnt need to be requested from
r2 everytime but can be cached locally. this also fixes some csp issues
ive been having.
2025-11-17 10:48:40 +01:00

274 lines
7.5 KiB
TypeScript

// Disables access to DOM typings like `HTMLElement` which are not available
// inside a service worker and instantiates the correct globals
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
// Ensures that the `$service-worker` import has proper type definitions
/// <reference types="@sveltejs/kit" />
// Only necessary if you have an import from `$env/static/public`
/// <reference types="../.svelte-kit/ambient.d.ts" />
import { build, files, version } from '$service-worker';
// This gives `self` the correct types
const self = globalThis.self as unknown as ServiceWorkerGlobalScope;
// Create cache names for this deployment
const CACHE = `cache-${version}`;
const RUNTIME_CACHE = `runtime-${version}`;
const IMAGE_CACHE = `images-${version}`;
const R2_CACHE = `r2-${version}`;
const ASSETS = [
...build, // the app itself
...files // everything in `static`
];
// Assets to precache for better performance
const CRITICAL_ASSETS = ['/cafe-bg-compressed.jpg', '/fonts/Washington.ttf', '/logo.svg'];
self.addEventListener('install', (event) => {
// Create a new cache and add all files to it
async function addFilesToCache() {
const cache = await caches.open(CACHE);
const imageCache = await caches.open(IMAGE_CACHE);
// Cache core assets
await cache.addAll(ASSETS);
// Precache critical assets with error handling
await Promise.allSettled(
CRITICAL_ASSETS.map(async (asset) => {
try {
const response = await fetch(asset);
if (response.ok) {
if (
asset.includes('jpg') ||
asset.includes('jpeg') ||
asset.includes('png') ||
asset.includes('webp')
) {
await imageCache.put(asset, response);
} else {
await cache.put(asset, response);
}
}
} catch (error) {
console.warn(`Failed to cache ${asset}:`, error);
}
})
);
}
event.waitUntil(addFilesToCache());
// Skip waiting to activate immediately
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
// Remove previous cached data from disk
async function deleteOldCaches() {
const currentCaches = [CACHE, RUNTIME_CACHE, IMAGE_CACHE, R2_CACHE];
for (const key of await caches.keys()) {
if (!currentCaches.includes(key)) {
await caches.delete(key);
}
}
}
event.waitUntil(deleteOldCaches());
// Claim clients immediately
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
// ignore POST requests etc
if (event.request.method !== 'GET') return;
async function respond() {
const url = new URL(event.request.url);
const cache = await caches.open(CACHE);
const runtimeCache = await caches.open(RUNTIME_CACHE);
const imageCache = await caches.open(IMAGE_CACHE);
const r2Cache = await caches.open(R2_CACHE);
// `build`/`files` can always be served from the cache
if (ASSETS.includes(url.pathname)) {
const response = await cache.match(url.pathname);
if (response) {
return response;
}
}
// Handle images with cache-first strategy
if (url.pathname.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)) {
const cachedResponse = await imageCache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(event.request);
if (response.ok) {
imageCache.put(event.request, response.clone());
}
return response;
} catch {
// Return a fallback image or the cached version
return cachedResponse || new Response('Image not available', { status: 404 });
}
}
// Handle fonts with cache-first strategy
if (url.pathname.match(/\.(woff|woff2|ttf|otf)$/i)) {
const cachedResponse = await cache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
}
// Handle R2 resources and local media proxy with cache-first strategy
if (url.hostname.includes('.r2.dev') || url.hostname.includes('.r2.cloudflarestorage.com') || url.pathname.startsWith('/api/media/')) {
const cachedResponse = await r2Cache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(event.request);
if (response.ok) {
// Cache R2 resources for a long time as they're typically immutable
r2Cache.put(event.request, response.clone());
}
return response;
} catch {
// Return cached version if available, or fall through to other cache checks
return cachedResponse || new Response('Media resource not available', { status: 404 });
}
}
// Handle OpenStreetMap tiles with cache-first strategy
if (url.hostname === 'tile.openstreetmap.org') {
const cachedResponse = await r2Cache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(event.request);
if (response.ok) {
// Cache map tiles for a long time
r2Cache.put(event.request, response.clone());
}
return response;
} catch {
return cachedResponse || new Response('Map tile not available', { status: 404 });
}
}
// Handle API and dynamic content with network-first strategy
if (url.pathname.startsWith('/api/') || url.searchParams.has('_data')) {
try {
const response = await fetch(event.request);
if (response.ok) {
runtimeCache.put(event.request, response.clone());
}
return response;
} catch (err) {
const cachedResponse = await runtimeCache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
throw err;
}
}
// for everything else, try the network first, but
// fall back to the cache if we're offline
try {
const response = await fetch(event.request);
// if we're offline, fetch can return a value that is not a Response
// instead of throwing - and we can't pass this non-Response to respondWith
if (!(response instanceof Response)) {
throw new Error('invalid response from fetch');
}
if (response.status === 200) {
runtimeCache.put(event.request, response.clone());
}
return response;
} catch (err) {
// Try all caches for fallback
const cachedResponse =
(await cache.match(event.request)) ||
(await runtimeCache.match(event.request)) ||
(await imageCache.match(event.request)) ||
(await r2Cache.match(event.request));
if (cachedResponse) {
return cachedResponse;
}
// if there's no cache, then just error out
// as there is nothing we can do to respond to this request
throw err;
}
}
event.respondWith(respond());
});
// Handle push notifications
self.addEventListener('push', (event) => {
if (!event.data) {
return;
}
const data = event.data.json();
const title = data.title || 'Serengo';
const options = {
body: data.body || '',
icon: data.icon || '/logo.svg',
badge: data.badge || '/logo.svg',
tag: data.tag || 'default',
data: data.data || {},
requireInteraction: false,
vibrate: [200, 100, 200]
};
event.waitUntil(self.registration.showNotification(title, options));
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const urlToOpen = event.notification.data?.url || '/';
event.waitUntil(
self.clients
.matchAll({
type: 'window',
includeUncontrolled: true
})
.then((clientList) => {
// Check if there's already a window open
for (const client of clientList) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// Otherwise, open a new window
if (self.clients.openWindow) {
return self.clients.openWindow(urlToOpen);
}
})
);
});