feat: add Progressive Web App (PWA) support with service worker registration, manifest, update notifications, and install prompts
This commit is contained in:
+237
@@ -0,0 +1,237 @@
|
||||
/* ObsiGate Service Worker - PWA Support */
|
||||
|
||||
const CACHE_VERSION = 'obsigate-v1.4.0';
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
|
||||
const MAX_DYNAMIC_CACHE_SIZE = 50;
|
||||
|
||||
// Assets to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/static/index.html',
|
||||
'/static/app.js',
|
||||
'/static/style.css',
|
||||
'/static/manifest.json'
|
||||
];
|
||||
|
||||
// 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.map(url => new Request(url, { cache: 'reload' })));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[SW] Failed to cache static assets:', err);
|
||||
})
|
||||
.then(() => 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.startsWith('obsigate-') && name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
|
||||
.map((name) => {
|
||||
console.log('[SW] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - serve from cache, fallback 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 SSE connections
|
||||
if (url.pathname === '/api/events') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip authentication endpoints
|
||||
if (url.pathname.startsWith('/api/auth/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API requests - Network first, cache fallback
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(networkFirstStrategy(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets - Cache first, network fallback
|
||||
event.respondWith(cacheFirstStrategy(request));
|
||||
});
|
||||
|
||||
// Cache first strategy (for static assets)
|
||||
async function cacheFirstStrategy(request) {
|
||||
try {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const cache = await caches.open(STATIC_CACHE);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('[SW] Cache first strategy failed:', error);
|
||||
|
||||
// Return offline page if available
|
||||
const offlinePage = await caches.match('/static/index.html');
|
||||
if (offlinePage) {
|
||||
return offlinePage;
|
||||
}
|
||||
|
||||
return new Response('Offline - Unable to fetch resource', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'text/plain'
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Network first strategy (for API calls)
|
||||
async function networkFirstStrategy(request) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful GET responses
|
||||
if (networkResponse && networkResponse.status === 200 && request.method === 'GET') {
|
||||
const cache = await caches.open(DYNAMIC_CACHE);
|
||||
cache.put(request, networkResponse.clone());
|
||||
|
||||
// Limit dynamic cache size
|
||||
limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE);
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.log('[SW] Network failed, trying cache:', request.url);
|
||||
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Return offline response
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Offline',
|
||||
message: 'Unable to fetch data. Please check your connection.'
|
||||
}), {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Limit cache size
|
||||
async function limitCacheSize(cacheName, maxSize) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const keys = await cache.keys();
|
||||
|
||||
if (keys.length > maxSize) {
|
||||
// Delete oldest entries
|
||||
const deleteCount = keys.length - maxSize;
|
||||
for (let i = 0; i < deleteCount; i++) {
|
||||
await cache.delete(keys[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message event - handle messages from clients
|
||||
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.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Sync event - background sync for offline actions
|
||||
self.addEventListener('sync', (event) => {
|
||||
console.log('[SW] Background sync:', event.tag);
|
||||
|
||||
if (event.tag === 'sync-data') {
|
||||
event.waitUntil(syncData());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncData() {
|
||||
// Placeholder for background sync logic
|
||||
console.log('[SW] Syncing data...');
|
||||
}
|
||||
|
||||
// Push notification event
|
||||
self.addEventListener('push', (event) => {
|
||||
const options = {
|
||||
body: event.data ? event.data.text() : 'New update available',
|
||||
icon: '/static/icons/icon-192x192.png',
|
||||
badge: '/static/icons/icon-72x72.png',
|
||||
vibrate: [200, 100, 200],
|
||||
data: {
|
||||
dateOfArrival: Date.now(),
|
||||
primaryKey: 1
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
action: 'explore',
|
||||
title: 'Ouvrir ObsiGate'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: 'Fermer'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification('ObsiGate', options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click event
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'explore') {
|
||||
event.waitUntil(
|
||||
clients.openWindow('/')
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user