- Updated localization files for various languages, including English, German, Spanish, Portuguese, and Chinese, to ensure consistency and accuracy across the application. - Added new keys and updated existing ones to support recent UI changes and features, particularly in project views, task lists, and admin center settings. - Enhanced the structure of localization files to improve maintainability and facilitate future updates. - Implemented performance optimizations in the frontend components to better handle localization data.
371 lines
10 KiB
JavaScript
371 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');
|