389 lines
12 KiB
JavaScript
389 lines
12 KiB
JavaScript
// SecureBit.chat Service Worker
|
|
// Conservative PWA Edition v4.4.18 - Minimal Caching Strategy
|
|
|
|
const CACHE_NAME = 'securebit-pwa-v4.4.18';
|
|
const STATIC_CACHE = 'securebit-pwa-static-v4.4.18';
|
|
const DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.4.18';
|
|
|
|
// Essential files for PWA offline functionality
|
|
const STATIC_ASSETS = [
|
|
'/',
|
|
'/index.html',
|
|
'/manifest.json',
|
|
|
|
// Core PWA files only
|
|
'/dist/app.js',
|
|
'/dist/app-boot.js',
|
|
|
|
// Essential styles for PWA
|
|
'/src/styles/pwa.css',
|
|
|
|
// PWA icons (required for install)
|
|
'/logo/icon-192x192.png',
|
|
'/logo/icon-512x512.png',
|
|
'/logo/favicon.ico',
|
|
|
|
// PWA components only
|
|
'/src/pwa/pwa-manager.js',
|
|
'/src/pwa/install-prompt.js',
|
|
'/src/scripts/pwa-register.js',
|
|
'/src/scripts/pwa-offline-test.js'
|
|
];
|
|
|
|
// Sensitive files that should never be cached
|
|
const SENSITIVE_PATTERNS = [
|
|
/\/api\//,
|
|
/preimage/,
|
|
/payment/,
|
|
/session/,
|
|
/auth/,
|
|
/verification/
|
|
];
|
|
|
|
// Network first patterns (always try network first)
|
|
const NETWORK_FIRST_PATTERNS = [
|
|
/\/api\//,
|
|
/\/session\//,
|
|
/\/payment\//,
|
|
/\/verification\//,
|
|
/preimage/,
|
|
/auth/
|
|
];
|
|
|
|
// Cache first patterns (only essential PWA assets)
|
|
const CACHE_FIRST_PATTERNS = [
|
|
/manifest\.json$/,
|
|
/logo\/icon-.*\.png$/,
|
|
/logo\/favicon\.ico$/,
|
|
/src\/styles\/pwa\.css$/,
|
|
/src\/pwa\/.*\.js$/,
|
|
/src\/scripts\/pwa-.*\.js$/
|
|
];
|
|
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'PWA_INSTALLED') {
|
|
self.clients.matchAll().then(clients => {
|
|
clients.forEach(client => {
|
|
client.postMessage({ type: 'PWA_INSTALL_DETECTED' });
|
|
});
|
|
});
|
|
}
|
|
});
|
|
// Install event - cache static assets with better error handling
|
|
self.addEventListener('install', (event) => {
|
|
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE)
|
|
.then(async (cache) => {
|
|
|
|
// Cache assets one by one to handle failures gracefully
|
|
const cachePromises = STATIC_ASSETS.map(async (url) => {
|
|
try {
|
|
// Skip sensitive patterns
|
|
if (SENSITIVE_PATTERNS.some(pattern => pattern.test(url))) {
|
|
return;
|
|
}
|
|
|
|
await cache.add(url);
|
|
} catch (error) {
|
|
console.warn(`⚠️ Failed to cache ${url}:`, error.message);
|
|
// Continue with other assets even if one fails
|
|
}
|
|
});
|
|
|
|
await Promise.allSettled(cachePromises);
|
|
|
|
// Force activation of new service worker
|
|
return self.skipWaiting();
|
|
})
|
|
.catch((error) => {
|
|
console.error('❌ Failed to open cache:', error);
|
|
// Still skip waiting to activate the service worker
|
|
return self.skipWaiting();
|
|
})
|
|
);
|
|
});
|
|
|
|
// Activate event - clean up old caches and notify about updates
|
|
self.addEventListener('activate', (event) => {
|
|
|
|
event.waitUntil(
|
|
caches.keys().then(cacheNames => {
|
|
return Promise.all(
|
|
cacheNames.map(cacheName => {
|
|
// Remove old caches
|
|
if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE && cacheName !== CACHE_NAME) {
|
|
console.log(`🗑️ Removing old cache: ${cacheName}`);
|
|
return caches.delete(cacheName);
|
|
}
|
|
})
|
|
);
|
|
}).then(() => {
|
|
|
|
// Notify all clients about the update
|
|
return self.clients.claim().then(() => {
|
|
self.clients.matchAll().then(clients => {
|
|
clients.forEach(client => {
|
|
client.postMessage({
|
|
type: 'SW_ACTIVATED',
|
|
timestamp: Date.now()
|
|
});
|
|
});
|
|
});
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// Удаляем дублирующийся код activate event
|
|
|
|
// Fetch event - handle requests with security-aware caching
|
|
self.addEventListener('fetch', (event) => {
|
|
const url = new URL(event.request.url);
|
|
|
|
// Skip non-GET requests
|
|
if (event.request.method !== 'GET') {
|
|
return;
|
|
}
|
|
|
|
// Skip sensitive endpoints
|
|
if (SENSITIVE_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
|
console.log('🔒 Skipping cache for sensitive endpoint:', url.pathname);
|
|
return;
|
|
}
|
|
|
|
// Skip chrome-extension and non-http requests
|
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
return;
|
|
}
|
|
|
|
event.respondWith(handleRequest(event.request));
|
|
});
|
|
|
|
// Conservative request handling - only cache PWA essentials
|
|
async function handleRequest(request) {
|
|
const url = new URL(request.url);
|
|
|
|
try {
|
|
// Strategy 1: Cache First (only for essential PWA assets)
|
|
if (CACHE_FIRST_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
|
return await cacheFirst(request);
|
|
}
|
|
|
|
// Strategy 2: Network First (for all other requests)
|
|
if (NETWORK_FIRST_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
|
return await networkFirst(request);
|
|
}
|
|
|
|
// Strategy 3: Network First for everything else (no aggressive caching)
|
|
return await networkFirst(request);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Request handling failed:', error);
|
|
return await handleOffline(request);
|
|
}
|
|
}
|
|
|
|
// Cache First strategy with Response cloning fix
|
|
async function cacheFirst(request) {
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
if (networkResponse && networkResponse.ok) {
|
|
// Clone the response before using it
|
|
const responseToCache = networkResponse.clone();
|
|
const cache = await caches.open(STATIC_CACHE);
|
|
cache.put(request, responseToCache);
|
|
}
|
|
return networkResponse;
|
|
} catch (error) {
|
|
console.warn('⚠️ Cache-first strategy failed:', error.message);
|
|
return await handleOffline(request);
|
|
}
|
|
}
|
|
|
|
// Network First strategy with Response cloning fix
|
|
async function networkFirst(request) {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
if (networkResponse && networkResponse.ok) {
|
|
// Only cache non-sensitive successful responses
|
|
if (!SENSITIVE_PATTERNS.some(pattern => pattern.test(request.url))) {
|
|
// Clone the response before caching
|
|
const responseToCache = networkResponse.clone();
|
|
const cache = await caches.open(DYNAMIC_CACHE);
|
|
cache.put(request, responseToCache);
|
|
}
|
|
}
|
|
return networkResponse;
|
|
} catch (error) {
|
|
console.warn('⚠️ Network-first strategy failed:', error.message);
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
return await handleOffline(request);
|
|
}
|
|
}
|
|
|
|
// Stale While Revalidate strategy with Response cloning fix
|
|
async function staleWhileRevalidate(request) {
|
|
const cachedResponse = await caches.match(request);
|
|
|
|
const networkResponsePromise = fetch(request)
|
|
.then((networkResponse) => {
|
|
if (networkResponse && networkResponse.ok &&
|
|
!SENSITIVE_PATTERNS.some(pattern => pattern.test(request.url))) {
|
|
// Clone the response before caching
|
|
const responseToCache = networkResponse.clone();
|
|
caches.open(DYNAMIC_CACHE)
|
|
.then(cache => cache.put(request, responseToCache))
|
|
.catch(error => console.warn('⚠️ Cache update failed:', error.message));
|
|
}
|
|
return networkResponse;
|
|
})
|
|
.catch(error => {
|
|
console.warn('⚠️ Network request failed:', error.message);
|
|
return null;
|
|
});
|
|
|
|
return cachedResponse || networkResponsePromise || handleOffline(request);
|
|
}
|
|
|
|
// Offline fallback - minimal caching for PWA only
|
|
async function handleOffline(request) {
|
|
const url = new URL(request.url);
|
|
|
|
// For navigation requests, return cached index.html
|
|
if (request.destination === 'document' || request.mode === 'navigate') {
|
|
const cachedIndex = await caches.match('/index.html');
|
|
if (cachedIndex) {
|
|
return cachedIndex;
|
|
}
|
|
|
|
// Fallback to root if index.html not found
|
|
const cachedRoot = await caches.match('/');
|
|
if (cachedRoot) {
|
|
return cachedRoot;
|
|
}
|
|
}
|
|
|
|
// For PWA assets, try to return cached version
|
|
if (CACHE_FIRST_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
|
const cachedAsset = await caches.match(request);
|
|
if (cachedAsset) {
|
|
return cachedAsset;
|
|
}
|
|
}
|
|
|
|
// Return a generic offline response for everything else
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Offline',
|
|
message: 'Network unavailable - PWA offline mode',
|
|
url: url.pathname
|
|
}),
|
|
{
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}
|
|
);
|
|
}
|
|
|
|
// Background sync for failed requests
|
|
self.addEventListener('sync', (event) => {
|
|
|
|
if (event.tag === 'retry-failed-requests') {
|
|
event.waitUntil(retryFailedRequests());
|
|
}
|
|
});
|
|
|
|
async function retryFailedRequests() {
|
|
try {
|
|
// Get all cached requests that failed
|
|
const cache = await caches.open(DYNAMIC_CACHE);
|
|
const requests = await cache.keys();
|
|
|
|
for (const request of requests) {
|
|
try {
|
|
// Try to fetch the request again
|
|
const response = await fetch(request);
|
|
if (response.ok) {
|
|
// Update cache with successful response
|
|
await cache.put(request, response);
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ Retry failed for:', request.url, error.message);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Failed to retry requests:', error);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Notification click handler
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close();
|
|
|
|
event.waitUntil(
|
|
clients.openWindow('/')
|
|
);
|
|
});
|
|
|
|
// Message handler for communication with main thread
|
|
self.addEventListener('message', (event) => {
|
|
|
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting();
|
|
}
|
|
|
|
if (event.data && event.data.type === 'CACHE_CLEAR') {
|
|
event.waitUntil(clearCaches());
|
|
}
|
|
|
|
if (event.data && event.data.type === 'CACHE_STATUS') {
|
|
event.waitUntil(getCacheStatus().then(status => {
|
|
event.ports[0].postMessage(status);
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Clear all caches
|
|
async function clearCaches() {
|
|
const cacheNames = await caches.keys();
|
|
await Promise.all(
|
|
cacheNames.map(cacheName => caches.delete(cacheName))
|
|
);
|
|
}
|
|
|
|
// Get cache status
|
|
async function getCacheStatus() {
|
|
const cacheNames = await caches.keys();
|
|
const status = {};
|
|
|
|
for (const cacheName of cacheNames) {
|
|
const cache = await caches.open(cacheName);
|
|
const keys = await cache.keys();
|
|
status[cacheName] = keys.length;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
// Error handler
|
|
self.addEventListener('error', (event) => {
|
|
console.error('❌ Service Worker error:', event.error);
|
|
});
|
|
|
|
// Unhandled rejection handler
|
|
self.addEventListener('unhandledrejection', (event) => {
|
|
console.error('❌ Service Worker unhandled rejection:', event.reason);
|
|
});
|