/** * Browser worker/runtime helpers. * Keep the top-level module compact, then expose focused namespaces that we * can later split into separate files without changing the import surface. */ import { isElectronHost, isTauriHost } from './host.js'; import { getConfig, isDevelopment, CONFIG_KEYS } from './env.js'; const SW_PATH = '/sw.js'; const SW_SCOPE = '/'; const DEV_SW_RESET_KEY = '__bface_dev_sw_reset__'; async function clearAllCaches() { if ('caches' in window) { try { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map((name) => { console.log('[SW] Clearing cache:', name); return caches.delete(name); })); console.log('[SW] All caches cleared'); return cacheNames.length; } catch (error) { console.error('[SW] Failed to clear caches:', error); throw error; } } return 0; } async function unregisterAllServiceWorkers() { if ('serviceWorker' in navigator) { try { const registrations = await navigator.serviceWorker.getRegistrations(); await Promise.all(registrations.map((reg) => { console.log('[SW] Unregistering service worker:', reg.scope); return reg.unregister(); })); console.log(`[SW] Unregistered ${registrations.length} service worker(s)`); return registrations.length; } catch (error) { console.error('[SW] Failed to unregister service workers:', error); throw error; } } return 0; } async function clearAllStorage() { try { localStorage.clear(); console.log('[Storage] localStorage cleared'); sessionStorage.clear(); console.log('[Storage] sessionStorage cleared'); if ('indexedDB' in window) { const databases = await indexedDB.databases(); await Promise.all(databases.map((db) => { if (db.name) { return new Promise((resolve) => { const deleteReq = indexedDB.deleteDatabase(db.name); deleteReq.onsuccess = () => { console.log(`[Storage] IndexedDB database "${db.name}" deleted`); resolve(); }; deleteReq.onerror = () => { console.warn(`[Storage] Failed to delete IndexedDB database "${db.name}"`); resolve(); }; }); } return Promise.resolve(); })); } console.log('[Storage] All storage cleared'); return true; } catch (error) { console.error('[Storage] Failed to clear storage:', error); throw error; } } async function clearPWACache() { console.log('Clearing all PWA caches and storage...'); try { const swCount = await unregisterAllServiceWorkers(); const cacheCount = await clearAllCaches(); await clearAllStorage(); console.log(`PWA cache cleared: ${swCount} service worker(s), ${cacheCount} cache(s), and all storage`); console.log('Reload the page to re-register service workers'); return { serviceWorkers: swCount, caches: cacheCount, storage: true }; } catch (error) { console.error('Failed to clear PWA cache:', error); throw error; } } async function registerServiceWorker() { if (isElectronHost() || isTauriHost()) { await unregisterAllServiceWorkers(); console.log('[SW] Skipping service worker registration in desktop host'); return null; } const devHost = await getConfig(CONFIG_KEYS.DEV_HOST, isDevelopment()); if (devHost) { console.log('[SW] Skipping service worker registration in development'); return null; } if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register(SW_PATH, { scope: SW_SCOPE, updateViaCache: 'none' }); console.log('Service Worker registered:', registration); await registration.update(); registration.addEventListener('updatefound', () => { const newWorker = registration.installing; if (newWorker) { newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed') { if (navigator.serviceWorker.controller) { console.log('[SW] New service worker available, reloading...'); window.location.reload(); } else { console.log('[SW] Service worker installed for the first time'); } } }); } }); return registration; } catch (error) { console.error('Service Worker registration failed:', error); throw error; } } console.warn('Service Workers are not supported'); return null; } async function resetServiceWorkers() { if (isElectronHost() || isTauriHost()) { await unregisterAllServiceWorkers(); return false; } const devHost = await getConfig(CONFIG_KEYS.DEV_HOST, isDevelopment()); if (!devHost || typeof window === 'undefined' || !('serviceWorker' in navigator)) { return false; } const hasController = Boolean(navigator.serviceWorker.controller); const alreadyReset = sessionStorage.getItem(DEV_SW_RESET_KEY) === '1'; if (!hasController || alreadyReset) { return false; } try { await unregisterAllServiceWorkers(); await clearAllCaches(); } catch (error) { console.warn('[SW] Failed to reset dev service workers:', error); } sessionStorage.setItem(DEV_SW_RESET_KEY, '1'); window.location.reload(); return true; } async function getServiceWorkerStatus() { if (isElectronHost() || isTauriHost()) { return 'Desktop Disabled'; } if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) { return 'Not Supported'; } const devHost = await getConfig(CONFIG_KEYS.DEV_HOST, isDevelopment()); try { if (devHost) { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { registration.update(); return 'Active'; } return 'Development Disabled'; } await navigator.serviceWorker.ready; return 'Active'; } catch (error) { console.warn('[SW] Failed to resolve service worker status:', error); return 'Not Supported'; } } async function unregisterServiceWorker() { if (isElectronHost() || isTauriHost()) { return; } if ('serviceWorker' in navigator) { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { await registration.unregister(); console.log('Service Worker unregistered'); } } } export const sw = { clearAllCaches, unregisterAllServiceWorkers, clearAllStorage, clearPWACache, registerServiceWorker, resetServiceWorkers, getServiceWorkerStatus, unregisterServiceWorker };