Reported from an iPhone, with a screenshot: tap the message box, the keyboard comes up, and the whole app has slid off the top — header gone, only the bottom edge of the composer left, black underneath it down to the keyboard. That is the visual viewport being panned. WebKit shrinks it for the keyboard and then moves it down inside the layout viewport (which it does not shrink) to reveal the focused field. The shell had shrunk correctly, to the top --sb-vh of the layout viewport; the window had simply moved off it. No CSS declines that pan, and it is not a document scroll, so there is no scrollTop to put back. So the shell is pinned to the visible rect: fixed to the layout viewport and translated by visualViewport.offsetTop. The earlier objection to pinning is kept where it was right — HEIGHT still moves on resize only, which is what stopped the layout twitching under the finger, and the offset rides a transform rather than `top`. The first cut of this had a guard that silently disabled it: the pan was only followed when window.innerHeight was much taller than the visual viewport, i.e. "a keyboard is covering us". That assumes iOS never resizes the layout viewport, which stopped being safe once WebKit started honouring interactive-widget. Only a pinch-zoomed page switches the pan off now, by scale, and offsetTop is believed whenever it is non-zero. The offset is also re-read across the keyboard's animation on focusin/focusout: on iOS 26 the viewport is documented not to settle back immediately on dismissal. The message list stays at the newest message across a keyboard open or close, instead of holding a scroll position from the taller layout and leaving the last few messages under the composer. Two smaller things on the same screen: The QR code closes itself once the safety number appears. It was only dismissed when both exchange flags cleared, and those stay set through verification, so a one-time credential sat on top of the code the user was meant to be reading. The disconnect button's label is hidden wherever the button becomes a 40px square. Its own rule stopped at 560px while the square starts at 768px, so between the two the word was still inside the box, pushing the icon off centre. ?vvdebug=1 paints the viewport numbers over the app — visible rect height, offset, scale, what the shell resolved to. A phone cannot be put on a debugger and none of those numbers show up in a screenshot of the result; this one is how the guard above was caught. Off unless named in the URL. Claude-Session: https://claude.ai/code/session_01R63J7dexujFQCJpDAeKxr9
603 lines
21 KiB
JavaScript
603 lines
21 KiB
JavaScript
// SecureBit.chat Service Worker
|
|
// Conservative PWA Edition v4.7.56 - Minimal Caching Strategy
|
|
// Enhanced with version-aware cache management
|
|
|
|
// Dynamic version detection from meta.json
|
|
let APP_VERSION = 'v4.7.56';
|
|
let CACHE_NAME = 'securebit-pwa-v4.7.56';
|
|
let STATIC_CACHE = 'securebit-pwa-static-v4.7.56';
|
|
let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
|
|
|
|
// Build stamp — rewritten by scripts/post-build.js on every release so this file's
|
|
// bytes change each deploy. That is what makes the browser detect a new Service Worker,
|
|
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
|
|
const SW_BUILD_VERSION = '1789190079159';
|
|
|
|
// Locale subdirectories, rewritten by scripts/build-i18n.js. Each localized page is a
|
|
// separate document at its own URL, so the shell has to be cached and served per
|
|
// locale — otherwise an offline visitor on /de/ is handed the English page back.
|
|
const SW_LOCALES = ['de', 'fr', 'es', 'uk', 'ru', 'zh', 'ko', 'hi', 'ar', 'he', 'fa', 'ur'];
|
|
const LOCALE_SHELLS = SW_LOCALES.flatMap((code) => [`/${code}/`, `/${code}/index.html`, `/${code}/manifest.json`]);
|
|
|
|
// Dictionary modules, rewritten by scripts/build-i18n.js alongside SW_LOCALES. Each
|
|
// locale now ships its own strings rather than all thirteen riding inside the bundles,
|
|
// so a shell cached for offline use is only half a page without the matching dictionary
|
|
// — the visitor would get their own language's layout wrapped around English text.
|
|
//
|
|
// Only two of these are precached: the default locale's, which every page needs because
|
|
// t() falls back to it, and the alias index.js imports. Downloading the other twelve up
|
|
// front would put back most of the weight this split was for. They are allowlisted for
|
|
// caching instead, so the one a visitor actually loads is stored on the way past.
|
|
// BEGIN generated locale dictionaries
|
|
const SW_DICTS = ['/src/i18n/dict/default.js', '/src/i18n/dict/en.js', '/src/i18n/dict/de.js', '/src/i18n/dict/fr.js', '/src/i18n/dict/es.js', '/src/i18n/dict/uk.js', '/src/i18n/dict/ru.js', '/src/i18n/dict/zh.js', '/src/i18n/dict/ko.js', '/src/i18n/dict/hi.js', '/src/i18n/dict/ar.js', '/src/i18n/dict/he.js', '/src/i18n/dict/fa.js', '/src/i18n/dict/ur.js'];
|
|
const SW_PRECACHE_DICTS = ['/src/i18n/dict/default.js', '/src/i18n/dict/en.js'];
|
|
// END generated locale dictionaries
|
|
|
|
// The locale a request belongs to, as a shell path. Falls back to the root shell.
|
|
function shellFor(pathname) {
|
|
const code = SW_LOCALES.find((c) => pathname === `/${c}` || pathname.startsWith(`/${c}/`));
|
|
return code ? `/${code}/index.html` : '/index.html';
|
|
}
|
|
|
|
// Load version from meta.json on install
|
|
async function getAppVersion() {
|
|
try {
|
|
const response = await fetch('/meta.json?t=' + Date.now(), {
|
|
cache: 'no-store',
|
|
headers: {
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
|
}
|
|
});
|
|
if (response.ok) {
|
|
const meta = await response.json();
|
|
const version = meta.version || meta.buildVersion || 'v4.7.56';
|
|
APP_VERSION = version;
|
|
CACHE_NAME = `securebit-pwa-${version}`;
|
|
STATIC_CACHE = `securebit-pwa-static-${version}`;
|
|
DYNAMIC_CACHE = `securebit-pwa-dynamic-${version}`;
|
|
return version;
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ Failed to load version from meta.json, using default');
|
|
}
|
|
return APP_VERSION;
|
|
}
|
|
|
|
// Essential files for PWA offline functionality
|
|
// DO NOT include JS files from dist/ - they should load from network for updates
|
|
const STATIC_ASSETS = [
|
|
'/',
|
|
'/index.html',
|
|
'/manifest.json',
|
|
|
|
// DO NOT cache /dist/app.js and /dist/app-boot.js - they should be updated
|
|
// This allows the update system to work correctly
|
|
|
|
// 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-install-capture.js',
|
|
'/src/scripts/pwa-register.js',
|
|
'/src/scripts/pwa-offline-test.js',
|
|
// Blocking in <head> and decides the theme before first paint, so an offline load
|
|
// that misses it paints dark and then corrects itself once the network returns.
|
|
'/src/scripts/theme-boot.js',
|
|
|
|
// The install prompt is precached and imports these at runtime; without them it
|
|
// would fail to load offline, which is exactly when it is most likely to be shown.
|
|
'/src/i18n/index.js',
|
|
'/src/i18n/generated.js',
|
|
...SW_PRECACHE_DICTS,
|
|
|
|
// Localized app shells (empty when the site is single-locale).
|
|
...LOCALE_SHELLS
|
|
];
|
|
|
|
// 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$/
|
|
];
|
|
|
|
// Explicit allowlist for any response that may be written to Cache Storage.
|
|
// Unknown GET responses are deliberately excluded by default.
|
|
const CACHEABLE_PATHS = new Set([
|
|
'/',
|
|
'/index.html',
|
|
'/manifest.json',
|
|
'/src/styles/pwa.css',
|
|
'/logo/icon-192x192.png',
|
|
'/logo/icon-512x512.png',
|
|
'/logo/favicon.ico',
|
|
'/src/pwa/pwa-manager.js',
|
|
'/src/pwa/install-prompt.js',
|
|
'/src/scripts/pwa-install-capture.js',
|
|
'/src/scripts/pwa-register.js',
|
|
'/src/scripts/pwa-offline-test.js',
|
|
'/src/scripts/theme-boot.js',
|
|
'/src/i18n/index.js',
|
|
'/src/i18n/generated.js',
|
|
...SW_DICTS,
|
|
...LOCALE_SHELLS
|
|
]);
|
|
|
|
function isSensitivePath(pathname) {
|
|
return SENSITIVE_PATTERNS.some(pattern => pattern.test(pathname));
|
|
}
|
|
|
|
function isCacheableStaticPath(pathname) {
|
|
return CACHEABLE_PATHS.has(pathname);
|
|
}
|
|
|
|
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(
|
|
getAppVersion().then(async (version) => {
|
|
console.log('📦 Service Worker installing with version:', version);
|
|
|
|
return 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;
|
|
}
|
|
|
|
// Add cache-busting for meta.json
|
|
if (url.includes('meta.json')) {
|
|
url = url + '?t=' + Date.now();
|
|
}
|
|
|
|
// cache: 'reload' bypasses the browser's own HTTP cache
|
|
// for this fetch and replaces what is in it. Without it a
|
|
// client that once received a shell under the year-long
|
|
// immutable header (see deploy/nginx.conf) would keep
|
|
// re-caching that same stale copy on every install, and
|
|
// never see a release again.
|
|
await cache.add(new Request(url, { cache: 'reload' }));
|
|
} 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(
|
|
getAppVersion().then(async (version) => {
|
|
console.log('✅ Service Worker activating with version:', version);
|
|
|
|
const cacheNames = await caches.keys();
|
|
|
|
// Remove all old caches that don't match current version
|
|
const deletePromises = cacheNames.map(cacheName => {
|
|
// Remove caches that don't match current version
|
|
if (cacheName !== STATIC_CACHE &&
|
|
cacheName !== DYNAMIC_CACHE &&
|
|
cacheName !== CACHE_NAME &&
|
|
cacheName.startsWith('securebit-pwa-')) {
|
|
console.log(`🗑️ Removing old cache: ${cacheName}`);
|
|
return caches.delete(cacheName);
|
|
}
|
|
});
|
|
|
|
await Promise.all(deletePromises);
|
|
|
|
// Notify all clients about the update
|
|
return self.clients.claim().then(() => {
|
|
self.clients.matchAll().then(clients => {
|
|
clients.forEach(client => {
|
|
client.postMessage({
|
|
type: 'SW_ACTIVATED',
|
|
version: version,
|
|
timestamp: Date.now()
|
|
});
|
|
});
|
|
});
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// Removed duplicate activate event code
|
|
|
|
// 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 (isSensitivePath(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;
|
|
}
|
|
|
|
// Network-first for meta.json (never cache)
|
|
if (url.pathname === '/meta.json' || url.pathname.endsWith('/meta.json')) {
|
|
event.respondWith(
|
|
fetch(event.request, {
|
|
cache: 'no-store',
|
|
headers: {
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache'
|
|
}
|
|
}).catch(() => {
|
|
// Fallback if network is unavailable
|
|
return new Response(JSON.stringify({
|
|
version: APP_VERSION,
|
|
error: 'Network unavailable'
|
|
}), {
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Network-first for JS files from dist/ (don't cache for updates)
|
|
if (url.pathname.startsWith('/dist/') && (url.pathname.endsWith('.js') || url.pathname.endsWith('.mjs'))) {
|
|
event.respondWith(
|
|
fetch(event.request, {
|
|
cache: 'no-store',
|
|
headers: {
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache'
|
|
}
|
|
}).catch((error) => {
|
|
// Log error for debugging
|
|
console.warn('⚠️ Failed to fetch JS file:', url.pathname, error.message);
|
|
// Try to get from cache as fallback
|
|
return caches.match(event.request).then(cachedResponse => {
|
|
if (cachedResponse) {
|
|
console.log('📦 Using cached version of:', url.pathname);
|
|
return cachedResponse;
|
|
}
|
|
// Only return 503 if no cache available
|
|
return new Response('Network unavailable', {
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
});
|
|
});
|
|
})
|
|
);
|
|
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 (
|
|
url.origin === self.location.origin &&
|
|
isCacheableStaticPath(url.pathname) &&
|
|
!isSensitivePath(url.pathname) &&
|
|
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 url = new URL(request.url);
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
if (
|
|
networkResponse &&
|
|
networkResponse.ok &&
|
|
url.origin === self.location.origin &&
|
|
isCacheableStaticPath(url.pathname) &&
|
|
!isSensitivePath(url.pathname)
|
|
) {
|
|
// 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) {
|
|
const url = new URL(request.url);
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
if (networkResponse && networkResponse.ok) {
|
|
// Only cache explicitly known-safe static responses.
|
|
if (
|
|
url.origin === self.location.origin &&
|
|
isCacheableStaticPath(url.pathname) &&
|
|
!isSensitivePath(url.pathname)
|
|
) {
|
|
// Clone the response before caching
|
|
const responseToCache = networkResponse.clone();
|
|
const cache = await caches.open(DYNAMIC_CACHE);
|
|
cache.put(request, responseToCache).catch(err => {
|
|
console.warn('⚠️ Cache put failed (non-critical):', err.message);
|
|
});
|
|
}
|
|
return networkResponse;
|
|
}
|
|
// If response is not ok, try cache
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
return networkResponse; // Return the non-ok response anyway
|
|
} 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 url = new URL(request.url);
|
|
const cachedResponse = await caches.match(request);
|
|
|
|
const networkResponsePromise = fetch(request)
|
|
.then((networkResponse) => {
|
|
if (
|
|
networkResponse &&
|
|
networkResponse.ok &&
|
|
url.origin === self.location.origin &&
|
|
isCacheableStaticPath(url.pathname) &&
|
|
!isSensitivePath(url.pathname)
|
|
) {
|
|
// 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') {
|
|
// Serve the shell for the locale that was requested, so an offline visitor on
|
|
// /de/ does not silently get the English page.
|
|
const localeShell = await caches.match(shellFor(url.pathname));
|
|
if (localeShell) {
|
|
return localeShell;
|
|
}
|
|
|
|
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);
|
|
});
|