Files
worklenz/worklenz-frontend/public/sw.js
chamikaJ 833879e0e8 feat(logout): implement cache cleanup and service worker unregistration on logout
- Added a new utility, CacheCleanup, to handle clearing caches and unregistering the service worker during user logout.
- Enhanced the LoggingOutPage to utilize CacheCleanup for clearing local session and caches before redirecting to the login page.
- Introduced ModuleErrorBoundary to manage module loading errors, providing user feedback and options to retry or reload the application.
- Updated App component to include global error handlers for improved error management related to module loading issues.
2025-07-15 16:08:07 +05:30

367 lines
10 KiB
JavaScript

// Worklenz Service Worker
// Provides offline functionality, caching, and performance improvements
const CACHE_VERSION = 'v1.0.0';
const CACHE_NAMES = {
STATIC: `worklenz-static-${CACHE_VERSION}`,
DYNAMIC: `worklenz-dynamic-${CACHE_VERSION}`,
API: `worklenz-api-${CACHE_VERSION}`,
IMAGES: `worklenz-images-${CACHE_VERSION}`
};
// Resources to cache immediately on install
const STATIC_CACHE_URLS = [
'/',
'/index.html',
'/favicon.ico',
'/env-config.js',
'/manifest.json',
// Ant Design and other critical CSS/JS will be cached as they're requested
];
// API endpoints that can be cached
const CACHEABLE_API_PATTERNS = [
/\/api\/project-categories/,
/\/api\/project-statuses/,
/\/api\/task-priorities/,
/\/api\/task-statuses/,
/\/api\/job-titles/,
/\/api\/teams\/\d+\/members/,
/\/api\/auth\/user/, // Cache user info for offline access
];
// Resources that should never be cached
const NEVER_CACHE_PATTERNS = [
/\/api\/auth\/login/,
/\/api\/auth\/logout/,
/\/api\/notifications/,
/\/socket\.io/,
/\.hot-update\./,
/sw\.js$/,
/chrome-extension/,
/moz-extension/,
];
// Install event - Cache static resources
self.addEventListener('install', event => {
console.log('Service Worker: Installing...');
event.waitUntil(
(async () => {
try {
const cache = await caches.open(CACHE_NAMES.STATIC);
await cache.addAll(STATIC_CACHE_URLS);
console.log('Service Worker: Static resources cached');
// Skip waiting to activate immediately
await self.skipWaiting();
} catch (error) {
console.error('Service Worker: Installation failed', error);
}
})()
);
});
// Activate event - Clean up old caches
self.addEventListener('activate', event => {
console.log('Service Worker: Activating...');
event.waitUntil(
(async () => {
try {
// Clean up old caches
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter(name =>
Object.values(CACHE_NAMES).every(currentCache => currentCache !== name)
);
await Promise.all(
oldCaches.map(cacheName => caches.delete(cacheName))
);
console.log('Service Worker: Old caches cleaned up');
// Take control of all pages
await self.clients.claim();
} catch (error) {
console.error('Service Worker: Activation failed', error);
}
})()
);
});
// Fetch event - Handle all network requests
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests and browser extensions
if (request.method !== 'GET' || NEVER_CACHE_PATTERNS.some(pattern => pattern.test(url.href))) {
return;
}
event.respondWith(handleFetchRequest(request));
});
// Main fetch handler with different strategies based on resource type
async function handleFetchRequest(request) {
const url = new URL(request.url);
try {
// Static assets - Cache First strategy
if (isStaticAsset(url)) {
return await cacheFirstStrategy(request, CACHE_NAMES.STATIC);
}
// Images - Cache First with long-term storage
if (isImageRequest(url)) {
return await cacheFirstStrategy(request, CACHE_NAMES.IMAGES);
}
// API requests - Network First with fallback
if (isAPIRequest(url)) {
return await networkFirstStrategy(request, CACHE_NAMES.API);
}
// HTML pages - Stale While Revalidate
if (isHTMLRequest(request)) {
return await staleWhileRevalidateStrategy(request, CACHE_NAMES.DYNAMIC);
}
// Everything else - Network First
return await networkFirstStrategy(request, CACHE_NAMES.DYNAMIC);
} catch (error) {
console.error('Service Worker: Fetch failed', error);
return createOfflineResponse(request);
}
}
// Cache First Strategy - Try cache first, fallback to network
async function cacheFirstStrategy(request, cacheName) {
const cache = await caches.open(cacheName);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.status === 200) {
// Clone before caching as response can only be used once
const responseClone = networkResponse.clone();
await cache.put(request, responseClone);
}
return networkResponse;
} catch (error) {
console.error('Cache First: Network failed', error);
throw error;
}
}
// Network First Strategy - Try network first, fallback to cache
async function networkFirstStrategy(request, cacheName) {
const cache = await caches.open(cacheName);
try {
const networkResponse = await fetch(request);
if (networkResponse.status === 200) {
// Cache successful responses
const responseClone = networkResponse.clone();
await cache.put(request, responseClone);
}
return networkResponse;
} catch (error) {
console.warn('Network First: Network failed, trying cache', error);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
// Stale While Revalidate Strategy - Return cached version while updating in background
async function staleWhileRevalidateStrategy(request, cacheName) {
const cache = await caches.open(cacheName);
const cachedResponse = await cache.match(request);
// Fetch from network in background
const networkResponsePromise = fetch(request).then(async networkResponse => {
if (networkResponse.status === 200) {
const responseClone = networkResponse.clone();
await cache.put(request, responseClone);
}
return networkResponse;
}).catch(error => {
console.warn('Stale While Revalidate: Background update failed', error);
});
// Return cached version immediately if available
if (cachedResponse) {
return cachedResponse;
}
// If no cached version, wait for network
return await networkResponsePromise;
}
// Helper functions to identify resource types
function isStaticAsset(url) {
return /\.(js|css|woff2?|ttf|eot)$/.test(url.pathname) ||
url.pathname.includes('/assets/') ||
url.pathname === '/' ||
url.pathname === '/index.html' ||
url.pathname === '/favicon.ico' ||
url.pathname === '/env-config.js';
}
function isImageRequest(url) {
return /\.(png|jpg|jpeg|gif|svg|webp|ico)$/.test(url.pathname) ||
url.pathname.includes('/file-types/');
}
function isAPIRequest(url) {
return url.pathname.startsWith('/api/') ||
CACHEABLE_API_PATTERNS.some(pattern => pattern.test(url.pathname));
}
function isHTMLRequest(request) {
return request.headers.get('accept')?.includes('text/html');
}
// Create offline fallback response
function createOfflineResponse(request) {
if (isImageRequest(new URL(request.url))) {
// Return a simple SVG placeholder for images
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="150" viewBox="0 0 200 150">
<rect width="200" height="150" fill="#f0f0f0"/>
<text x="100" y="75" text-anchor="middle" fill="#999" font-family="Arial, sans-serif" font-size="14">
Offline
</text>
</svg>`;
return new Response(svg, {
headers: { 'Content-Type': 'image/svg+xml' }
});
}
if (isAPIRequest(new URL(request.url))) {
// Return empty array or error for API requests
return new Response(JSON.stringify({
error: 'Offline',
message: 'This feature requires an internet connection'
}), {
status: 503,
headers: { 'Content-Type': 'application/json' }
});
}
// For HTML requests, try to return cached index.html
return caches.match('/') || new Response('Offline', { status: 503 });
}
// Handle background sync events (for future implementation)
self.addEventListener('sync', event => {
console.log('Service Worker: Background sync', event.tag);
if (event.tag === 'background-sync') {
event.waitUntil(handleBackgroundSync());
}
});
async function handleBackgroundSync() {
// This is where you would handle queued actions when coming back online
console.log('Service Worker: Handling background sync');
// Example: Send queued task updates, sync offline changes, etc.
// Implementation would depend on your app's specific needs
}
// Handle push notification events (for future implementation)
self.addEventListener('push', event => {
if (!event.data) return;
const options = {
body: event.data.text(),
icon: '/favicon.ico',
badge: '/favicon.ico',
vibrate: [200, 100, 200],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
}
};
event.waitUntil(
self.registration.showNotification('Worklenz', options)
);
});
// Handle notification click events
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
self.clients.openWindow('/')
);
});
// Message handling for communication with main thread
self.addEventListener('message', event => {
const { type, payload } = event.data;
switch (type) {
case 'SKIP_WAITING':
self.skipWaiting();
break;
case 'GET_VERSION':
event.ports[0].postMessage({ version: CACHE_VERSION });
break;
case 'CLEAR_CACHE':
clearAllCaches().then(() => {
event.ports[0].postMessage({ success: true });
});
break;
case 'LOGOUT':
// Special handler for logout - clear all caches and unregister
handleLogout().then(() => {
event.ports[0].postMessage({ success: true });
});
break;
default:
console.log('Service Worker: Unknown message type', type);
}
});
async function clearAllCaches() {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
console.log('Service Worker: All caches cleared');
}
async function handleLogout() {
try {
// Clear all caches
await clearAllCaches();
// Unregister the service worker to force fresh registration on next visit
await self.registration.unregister();
console.log('Service Worker: Logout handled - caches cleared and unregistered');
} catch (error) {
console.error('Service Worker: Error during logout handling', error);
throw error;
}
}
console.log('Service Worker: Loaded successfully');