mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
Camera streams on macOS Safari were failing with "FetchEvent.respondWith received an error: Load failed" because the Service Worker was intercepting MJPEG streaming responses. Safari has known issues handling continuous streaming responses through Service Workers. - Add exclusion for /camera/stream and /camera/snapshot URLs in SW fetch handler - Bump cache version to v24 to force SW update on clients The camera components already have robust error handling with reconnect logic, so bypassing the SW for these endpoints is safe and improves performance.
205 lines
5.4 KiB
JavaScript
205 lines
5.4 KiB
JavaScript
// Bambuddy Service Worker
|
|
const CACHE_NAME = 'bambuddy-v24';
|
|
const STATIC_CACHE = 'bambuddy-static-v24';
|
|
|
|
// Static assets to cache on install
|
|
const STATIC_ASSETS = [
|
|
'/',
|
|
'/manifest.json',
|
|
'/img/favicon.png',
|
|
'/img/favicon-16x16.png',
|
|
'/img/favicon-32x32.png',
|
|
'/img/android-chrome-192x192.png',
|
|
'/img/android-chrome-512x512.png',
|
|
'/img/apple-touch-icon.png',
|
|
'/img/bambuddy_logo_dark.png',
|
|
];
|
|
|
|
// Install event - cache static assets
|
|
self.addEventListener('install', (event) => {
|
|
console.log('[SW] Installing service worker...');
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE).then((cache) => {
|
|
console.log('[SW] Caching static assets');
|
|
return cache.addAll(STATIC_ASSETS);
|
|
})
|
|
);
|
|
// Activate immediately
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener('activate', (event) => {
|
|
console.log('[SW] Activating service worker...');
|
|
event.waitUntil(
|
|
caches.keys().then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter((name) => name !== CACHE_NAME && name !== STATIC_CACHE)
|
|
.map((name) => {
|
|
console.log('[SW] Deleting old cache:', name);
|
|
return caches.delete(name);
|
|
})
|
|
);
|
|
})
|
|
);
|
|
// Take control immediately
|
|
self.clients.claim();
|
|
});
|
|
|
|
// Fetch event - network-first for API, cache-first for static
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
const url = new URL(request.url);
|
|
|
|
// Skip non-GET requests
|
|
if (request.method !== 'GET') {
|
|
return;
|
|
}
|
|
|
|
// Skip WebSocket connections
|
|
if (url.protocol === 'ws:' || url.protocol === 'wss:') {
|
|
return;
|
|
}
|
|
|
|
// Skip camera stream/snapshot requests - Safari has issues with streaming through SW
|
|
if (url.pathname.includes('/camera/stream') || url.pathname.includes('/camera/snapshot')) {
|
|
return;
|
|
}
|
|
|
|
// API requests - network first, no cache (real-time data is critical)
|
|
if (url.pathname.startsWith('/api/')) {
|
|
event.respondWith(
|
|
fetch(request).catch(() => {
|
|
// Return offline response for API failures
|
|
return new Response(
|
|
JSON.stringify({ error: 'offline', message: 'You are currently offline' }),
|
|
{
|
|
status: 503,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Static assets - cache first, then network
|
|
if (
|
|
url.pathname.startsWith('/img/') ||
|
|
url.pathname.startsWith('/icons/') ||
|
|
url.pathname.endsWith('.png') ||
|
|
url.pathname.endsWith('.jpg') ||
|
|
url.pathname.endsWith('.svg') ||
|
|
url.pathname.endsWith('.ico')
|
|
) {
|
|
event.respondWith(
|
|
caches.match(request).then((cached) => {
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
return fetch(request).then((response) => {
|
|
// Cache successful responses
|
|
if (response.ok) {
|
|
const clone = response.clone();
|
|
caches.open(STATIC_CACHE).then((cache) => {
|
|
cache.put(request, clone);
|
|
});
|
|
}
|
|
return response;
|
|
});
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// JS/CSS assets - stale-while-revalidate
|
|
if (
|
|
url.pathname.startsWith('/assets/') ||
|
|
url.pathname.endsWith('.js') ||
|
|
url.pathname.endsWith('.css')
|
|
) {
|
|
event.respondWith(
|
|
caches.match(request).then((cached) => {
|
|
const fetchPromise = fetch(request).then((response) => {
|
|
if (response.ok) {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
cache.put(request, clone);
|
|
});
|
|
}
|
|
return response;
|
|
});
|
|
return cached || fetchPromise;
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// HTML pages - network first, fall back to cache
|
|
event.respondWith(
|
|
fetch(request)
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
cache.put(request, clone);
|
|
});
|
|
}
|
|
return response;
|
|
})
|
|
.catch(() => {
|
|
return caches.match(request).then((cached) => {
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
// Return cached index for SPA navigation
|
|
return caches.match('/');
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// Handle push notifications (for future use)
|
|
self.addEventListener('push', (event) => {
|
|
if (!event.data) return;
|
|
|
|
const data = event.data.json();
|
|
const options = {
|
|
body: data.body || 'New notification from Bambuddy',
|
|
icon: '/img/android-chrome-192x192.png',
|
|
badge: '/img/favicon-32x32.png',
|
|
vibrate: [100, 50, 100],
|
|
data: {
|
|
url: data.url || '/',
|
|
},
|
|
};
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(data.title || 'Bambuddy', options)
|
|
);
|
|
});
|
|
|
|
// Handle notification clicks
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close();
|
|
|
|
const url = event.notification.data?.url || '/';
|
|
|
|
event.waitUntil(
|
|
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
|
|
// Check if there's already a window open
|
|
for (const client of windowClients) {
|
|
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
|
client.navigate(url);
|
|
return client.focus();
|
|
}
|
|
}
|
|
// Open a new window if none exists
|
|
if (clients.openWindow) {
|
|
return clients.openWindow(url);
|
|
}
|
|
})
|
|
);
|
|
});
|