Files
MyClub/frontend/build/service-worker.js
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

317 lines
8.6 KiB
JavaScript

/* eslint-disable no-restricted-globals */
// Service Worker for PWA support and offline functionality
const CACHE_VERSION = 'v1.0.2';
const CACHE_NAME = `fotbal-club-cache-${CACHE_VERSION}`;
// Rate limiting for background updates
const BACKGROUND_UPDATE_INTERVAL = 5000; // 5 seconds minimum between updates
const lastBackgroundUpdates = new Map();
// Assets to cache on install
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.json',
'/favicon.ico',
'/logo192.png',
'/logo512.png',
'/robots.txt',
];
// API endpoints to cache
const API_CACHE_ENDPOINTS = [
'/api/v1/settings/public',
'/api/v1/seo',
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[SW] Caching static assets');
return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' })));
}).catch((error) => {
console.error('[SW] Failed to cache static assets:', error);
})
);
// 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)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
// Take control immediately
return self.clients.claim();
});
// Fetch event - serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Skip Chrome extensions and non-http(s) requests
if (!url.protocol.startsWith('http')) {
return;
}
// Only handle same-origin requests
if (url.origin !== self.location.origin) {
return;
}
// Skip admin routes
if (url.pathname.startsWith('/admin')) {
return;
}
// Skip background update requests to prevent infinite loops
if (request.headers.get('X-SW-Background-Update') === 'true') {
return;
}
// Handle SPA navigations with app shell fallback
if (request.mode === 'navigate') {
event.respondWith(handleNavigationRequest(request));
return;
}
// Handle API requests
if (url.pathname.startsWith('/api/')) {
event.respondWith(handleAPIRequest(request));
return;
}
// Handle static assets and pages
event.respondWith(handleStaticRequest(request));
});
// Handle static requests - Cache First strategy
async function handleStaticRequest(request) {
try {
// Try cache first
const cachedResponse = await caches.match(request);
if (cachedResponse) {
// For static assets with long cache headers, don't update in background
const url = new URL(request.url);
const isStaticAsset = url.pathname.match(/\.(png|jpg|jpeg|gif|svg|webp|ico|woff|woff2|ttf|eot)$/);
const hasLongCache = cachedResponse.headers.get('cache-control')?.includes('max-age=31536000');
// Only update in background if it's not a static asset with long cache
if (!isStaticAsset || !hasLongCache) {
fetchAndUpdateCache(request);
}
return cachedResponse;
}
// Not in cache - fetch from network
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('[SW] Fetch failed:', error);
// Return app shell (index.html) if available
const cachedOffline = await caches.match('/index.html');
if (cachedOffline) {
return cachedOffline;
}
// Return basic offline response
return new Response('Offline - Please check your connection', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain' },
});
}
}
// Handle SPA navigation requests - Network First with index.html fallback
async function handleNavigationRequest(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse && networkResponse.ok) {
return networkResponse;
}
} catch (error) {
console.error('[SW] Navigation fetch failed:', error);
}
// Fallback to cached index.html (app shell)
const cachedIndex = await caches.match('/index.html');
if (cachedIndex) {
return cachedIndex;
}
return new Response('Offline - Please check your connection', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain' },
});
}
// Handle API requests - Network First strategy with cache fallback
async function handleAPIRequest(request) {
try {
// Try network first for fresh data
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.log('[SW] Network failed, trying cache:', request.url);
// Fall back to cache
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return error response
return new Response(
JSON.stringify({ error: 'Offline - cached data not available' }),
{
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'application/json' },
}
);
}
}
// Update cache in background
async function fetchAndUpdateCache(request) {
try {
// Skip if this is already a background update request to prevent infinite loops
if (request.headers.get('X-SW-Background-Update') === 'true') {
return;
}
// Rate limit background updates to prevent excessive requests
const now = Date.now();
const lastUpdate = lastBackgroundUpdates.get(request.url);
if (lastUpdate && (now - lastUpdate) < BACKGROUND_UPDATE_INTERVAL) {
return;
}
lastBackgroundUpdates.set(request.url, now);
// Use fetch with cache: 'no-store' to bypass service worker and avoid infinite loops
const response = await fetch(request.url, {
cache: 'no-store',
headers: { 'X-SW-Background-Update': 'true' }
});
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
// Use the original request as key, but the new response
cache.put(request, response);
}
} catch (error) {
// Silent fail - we already returned cached version
}
}
// Handle background sync for offline actions
self.addEventListener('sync', (event) => {
console.log('[SW] Background sync:', event.tag);
if (event.tag === 'sync-data') {
event.waitUntil(syncOfflineData());
}
});
async function syncOfflineData() {
// Implement offline data sync logic here
// For example, sync form submissions, votes, etc.
console.log('[SW] Syncing offline data...');
}
// Handle push notifications
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
const title = data.title || 'Fotbal Club';
const options = {
body: data.body || 'Nová notifikace',
icon: '/logo192.png',
badge: '/logo192.png',
data: data.url || '/',
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const urlToOpen = event.notification.data || '/';
event.waitUntil(
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();
}
}
// Open new window
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// Message handler for manual cache updates
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CLEAR_CACHE') {
event.waitUntil(
caches.delete(CACHE_NAME).then(() => {
return caches.open(CACHE_NAME);
})
);
}
});
console.log('[SW] Service Worker loaded successfully');