/** * SERVICE WORKER: Token Refresh * * PURPOSE: Handle automatic token refresh in background * FEATURES: * - Periodically check if token needs refresh * - Refresh token before expiry * - Intercept requests to add auth headers * - Handle token expiry gracefully * * INSTALLED: Before app loads * ACTIVATED: On page load * RUNS: Periodically in background */ // Token check interval (every 60 seconds) const TOKEN_CHECK_INTERVAL = 60 * 1000; // Token refresh buffer (refresh 5 minutes before expiry) const REFRESH_BUFFER = 5 * 60 * 1000; // ============================================================================ // INSTALLATION // ============================================================================ /** * EVENT: install * * Called when service worker is registered * Used to cache critical assets */ self.addEventListener('install', (event: ExtendEvent) => { console.log('[ServiceWorker] Installing...'); event.waitUntil( Promise.resolve().then(() => { console.log('[ServiceWorker] Installed'); // Skip waiting - activate immediately self.skipWaiting(); }) ); }); // ============================================================================ // ACTIVATION // ============================================================================ /** * EVENT: activate * * Called when service worker takes control * Used to clean up old caches */ self.addEventListener('activate', (event: ExtendEvent) => { console.log('[ServiceWorker] Activating...'); event.waitUntil( Promise.resolve().then(() => { console.log('[ServiceWorker] Activated'); // Take control of all pages return (self as any).clients.claim(); }) ); }); // ============================================================================ // TOKEN REFRESH LOOP // ============================================================================ /** * Periodically check and refresh token */ async function startTokenRefreshLoop() { console.log('[ServiceWorker] Starting token refresh loop'); setInterval(async () => { try { // Get token expiry from session storage // (accessed via message from client since SW can't access sessionStorage directly) // This is handled via postMessage from client console.log('[ServiceWorker] Checking token...'); // Token refresh is initiated from client via postMessage // because ServiceWorker can't access sessionStorage directly } catch (error) { console.error('[ServiceWorker] Token check error:', error); } }, TOKEN_CHECK_INTERVAL); } // Start the loop when activated startTokenRefreshLoop(); // ============================================================================ // MESSAGE HANDLING // ============================================================================ /** * EVENT: message * * Called when client (page) sends message to ServiceWorker * Used to: * - Check if token needs refresh * - Perform token refresh * - Get current token status */ self.addEventListener('message', async (event: ExtendMessageEvent) => { const { type, payload } = event.data; console.log('[ServiceWorker] Received message:', type); try { if (type === 'REFRESH_TOKEN') { // Client asking us to refresh token const { refreshToken, apiUrl } = payload; console.log('[ServiceWorker] Refreshing token...'); // Call backend to refresh token const response = await fetch(`${apiUrl}/api/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }), }); if (!response.ok) { throw new Error('Token refresh failed'); } const data = await response.json() as any; console.log('[ServiceWorker] Token refreshed successfully'); // Send new token back to client event.ports[0].postMessage({ success: true, accessToken: data.accessToken, expiresIn: data.expiresIn, }); } else if (type === 'CHECK_TOKEN') { // Client asking if token needs refresh const { tokenExpiry } = payload; const now = Date.now(); const timeUntilExpiry = tokenExpiry - now; const needsRefresh = timeUntilExpiry < REFRESH_BUFFER; console.log(`[ServiceWorker] Token expires in ${Math.round(timeUntilExpiry / 1000)}s`); event.ports[0].postMessage({ needsRefresh, timeUntilExpiry, }); } else if (type === 'GET_STATUS') { // Client asking for ServiceWorker status event.ports[0].postMessage({ active: true, version: '1.0.0', }); } } catch (error) { console.error('[ServiceWorker] Message handling error:', error); event.ports[0].postMessage({ error: (error as Error).message, }); } }); // ============================================================================ // REQUEST INTERCEPTION (future use) // ============================================================================ /** * EVENT: fetch * * Called for every network request * Could be used to: * - Intercept API requests * - Add auth headers * - Retry on auth failure * * Disabled for now since we handle auth in the app * Enables when needed for request interception */ self.addEventListener('fetch', (event: ExtendFetchEvent) => { // Currently, just pass through // In future, could intercept API calls to add auth headers // or retry on 401 by refreshing token }); // ============================================================================ // TYPES // ============================================================================ interface ExtendEvent extends Event { waitUntil(promise: Promise): void; } interface ExtendMessageEvent extends ExtendEvent { data: { type: string; payload?: any; }; ports: MessagePort[]; } interface ExtendFetchEvent extends ExtendEvent { request: Request; respondWith(response: Response | Promise): void; }