+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 116 of 355

🔌 Service Workers: Offline Functionality in TypeScript

Master Service Workers for offline capabilities, caching strategies, and progressive web apps with type-safe implementations 🚀

🚀Intermediate
21 min read

Prerequisites

  • Understanding of Web Workers and browser APIs 📝
  • Basic knowledge of HTTP and caching ⚡
  • Experience with Promise-based APIs 💻

What you'll learn

  • Master Service Worker lifecycle and registration 🎯
  • Implement robust caching strategies for offline functionality 🏗️
  • Build progressive web apps with background sync 🐛
  • Create type-safe Service Worker communication patterns ✨

🎯 Introduction

Welcome to the powerful world of Service Workers! 🔌 If Web Workers are about parallel processing, then Service Workers are about making your web app work everywhere - online, offline, or with a poor connection. They’re the backbone of Progressive Web Apps (PWAs)!

Think of Service Workers as network proxies 🌐 that sit between your app and the internet. They can intercept network requests, cache resources, sync data in the background, and even push notifications. It’s like having a smart assistant that ensures your app works seamlessly regardless of network conditions!

By the end of this tutorial, you’ll be a master of offline-first development, able to build resilient web applications that provide excellent user experiences in any network environment. Let’s make the web work everywhere! 🌍

📚 Understanding Service Workers

🤔 What Are Service Workers?

Service Workers are special scripts that run in the background, separate from your web page. They act as programmable network proxies, allowing you to control how network requests from your page are handled. Think of them as smart middleware for the web!

// 🌟 Basic Service Worker registration
// main.ts - Register the service worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('✅ Service Worker registered:', registration.scope);
    } catch (error) {
      console.error('❌ Service Worker registration failed:', error);
    }
  });
}

// sw.ts - Service Worker script
// 🎯 Service Worker event listeners
self.addEventListener('install', (event: ExtendableEvent) => {
  console.log('🚀 Service Worker installing...');
  
  event.waitUntil(
    caches.open('app-cache-v1').then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/app.js'
      ]);
    })
  );
});

self.addEventListener('fetch', (event: FetchEvent) => {
  console.log('🌐 Intercepting request:', event.request.url);
  
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 📦 Return cached version or fetch from network
      return response || fetch(event.request);
    })
  );
});

💡 Key Characteristics

  • 🌐 Network Proxy: Intercept and control all network requests
  • 📦 Persistent Caching: Cache resources for offline access
  • 🔄 Background Sync: Sync data when connectivity returns
  • 📱 PWA Foundation: Enable app-like experiences
  • 🛡️ HTTPS Only: Security requirement (except localhost)
// 🎨 TypeScript types for Service Worker APIs
interface ServiceWorkerRegistration {
  active: ServiceWorker | null;
  installing: ServiceWorker | null;
  waiting: ServiceWorker | null;
  scope: string;
  update(): Promise<void>;
  unregister(): Promise<boolean>;
}

interface ExtendableEvent extends Event {
  waitUntil(promise: Promise<any>): void;
}

interface FetchEvent extends ExtendableEvent {
  request: Request;
  respondWith(response: Promise<Response> | Response): void;
}

🆚 Service Workers vs Web Workers

// 🧵 Web Workers - Parallel computation
class ComputationWorker {
  private worker: Worker;
  
  constructor() {
    this.worker = new Worker('/computation-worker.js');
  }
  
  async processData(data: number[]): Promise<number[]> {
    // 🔄 Offload heavy computation to avoid blocking UI
    return new Promise((resolve, reject) => {
      this.worker.postMessage(data);
      this.worker.onmessage = (e) => resolve(e.data);
      this.worker.onerror = reject;
    });
  }
}

// 🔌 Service Workers - Network interception and caching
class OfflineManager {
  static async register(): Promise<ServiceWorkerRegistration> {
    if (!('serviceWorker' in navigator)) {
      throw new Error('Service Workers not supported');
    }
    
    // 📝 Register service worker for offline functionality
    return navigator.serviceWorker.register('/sw.js');
  }
  
  static async cacheResources(resources: string[]): Promise<void> {
    // 💾 Pre-cache important resources
    const cache = await caches.open('app-cache-v1');
    await cache.addAll(resources);
  }
  
  static async getFromCacheOrNetwork(request: Request): Promise<Response> {
    // 🔄 Cache-first strategy
    const cachedResponse = await caches.match(request);
    return cachedResponse || fetch(request);
  }
}

🔧 Service Worker Lifecycle and Registration

📝 Registration and Management

// 🏗️ Comprehensive Service Worker management
class ServiceWorkerManager {
  private registration: ServiceWorkerRegistration | null = null;
  private swUpdateAvailable = false;
  private onUpdateCallbacks: Array<() => void> = [];
  
  constructor(private swPath: string = '/sw.js') {}
  
  // 📝 Register Service Worker with error handling
  async register(): Promise<ServiceWorkerRegistration> {
    if (!this.isSupported()) {
      throw new Error('Service Workers are not supported in this browser');
    }
    
    try {
      console.log('🚀 Registering Service Worker...');
      
      this.registration = await navigator.serviceWorker.register(this.swPath, {
        scope: '/' // Control all pages under root
      });
      
      console.log('✅ Service Worker registered successfully');
      console.log(`📍 Scope: ${this.registration.scope}`);
      
      // 🔄 Set up update detection
      this.setupUpdateDetection();
      
      return this.registration;
      
    } catch (error) {
      console.error('❌ Service Worker registration failed:', error);
      throw error;
    }
  }
  
  // 🔍 Check for Service Worker support
  private isSupported(): boolean {
    return 'serviceWorker' in navigator && 'caches' in window;
  }
  
  // 🔄 Set up Service Worker update detection
  private setupUpdateDetection(): void {
    if (!this.registration) return;
    
    // 📦 Check for updates periodically
    this.registration.addEventListener('updatefound', () => {
      console.log('🆕 New Service Worker version found');
      
      const newWorker = this.registration!.installing;
      if (newWorker) {
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            console.log('📥 New Service Worker installed, update available');
            this.swUpdateAvailable = true;
            this.notifyUpdateAvailable();
          }
        });
      }
    });
    
    // 🔄 Handle controller change (new SW activated)
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      console.log('🔄 Service Worker controller changed');
      window.location.reload(); // Reload to use new Service Worker
    });
  }
  
  // 📢 Notify about available updates
  private notifyUpdateAvailable(): void {
    this.onUpdateCallbacks.forEach(callback => callback());
  }
  
  // 📡 Subscribe to update notifications
  onUpdateAvailable(callback: () => void): void {
    this.onUpdateCallbacks.push(callback);
  }
  
  // 🔄 Trigger Service Worker update
  async triggerUpdate(): Promise<void> {
    if (!this.registration) {
      throw new Error('No Service Worker registration found');
    }
    
    console.log('🔄 Checking for Service Worker updates...');
    await this.registration.update();
  }
  
  // ⚡ Activate waiting Service Worker
  async activateUpdate(): Promise<void> {
    if (!this.registration || !this.registration.waiting) {
      throw new Error('No waiting Service Worker found');
    }
    
    console.log('⚡ Activating new Service Worker...');
    
    // 📤 Tell the waiting SW to skip waiting
    this.registration.waiting.postMessage({ type: 'SKIP_WAITING' });
  }
  
  // 🗑️ Unregister Service Worker
  async unregister(): Promise<boolean> {
    if (!this.registration) {
      console.log('⚠️ No Service Worker to unregister');
      return false;
    }
    
    try {
      const result = await this.registration.unregister();
      console.log('🗑️ Service Worker unregistered successfully');
      this.registration = null;
      return result;
    } catch (error) {
      console.error('❌ Service Worker unregistration failed:', error);
      throw error;
    }
  }
  
  // 📊 Get Service Worker status
  getStatus(): {
    isRegistered: boolean;
    isSupported: boolean;
    updateAvailable: boolean;
    scope?: string;
    state?: string;
  } {
    return {
      isRegistered: !!this.registration,
      isSupported: this.isSupported(),
      updateAvailable: this.swUpdateAvailable,
      scope: this.registration?.scope,
      state: this.registration?.active?.state
    };
  }
}

// 🎮 Usage example
async function setupServiceWorker(): Promise<void> {
  const swManager = new ServiceWorkerManager('/service-worker.js');
  
  try {
    // 📝 Register Service Worker
    await swManager.register();
    
    // 📡 Listen for updates
    swManager.onUpdateAvailable(() => {
      console.log('🆕 App update available!');
      
      // 🔄 Show update notification to user
      if (confirm('New version available! Reload to update?')) {
        swManager.activateUpdate();
      }
    });
    
    // 📊 Check status
    const status = swManager.getStatus();
    console.log('📊 Service Worker status:', status);
    
  } catch (error) {
    console.error('❌ Service Worker setup failed:', error);
  }
}

🎨 Service Worker Script Implementation

// 🔧 sw.ts - Type-safe Service Worker implementation
declare const self: ServiceWorkerGlobalScope;

interface CacheStrategy {
  name: string;
  version: number;
  resources: string[];
  maxAge?: number;
}

class TypedServiceWorker {
  private readonly CACHE_NAME = 'app-cache-v1';
  private readonly RUNTIME_CACHE = 'runtime-cache-v1';
  private readonly MAX_CACHE_ENTRIES = 100;
  
  // 🛠️ Cache strategies configuration
  private cacheStrategies: CacheStrategy[] = [
    {
      name: 'static-assets',
      version: 1,
      resources: [
        '/',
        '/index.html',
        '/styles.css',
        '/app.js',
        '/manifest.json'
      ]
    },
    {
      name: 'api-cache',
      version: 1,
      resources: [],
      maxAge: 5 * 60 * 1000 // 5 minutes
    }
  ];
  
  constructor() {
    this.setupEventListeners();
  }
  
  // 🎯 Set up all Service Worker event listeners
  private setupEventListeners(): void {
    self.addEventListener('install', this.handleInstall.bind(this));
    self.addEventListener('activate', this.handleActivate.bind(this));
    self.addEventListener('fetch', this.handleFetch.bind(this));
    self.addEventListener('message', this.handleMessage.bind(this));
  }
  
  // 🚀 Handle Service Worker installation
  private handleInstall(event: ExtendableEvent): void {
    console.log('🚀 Service Worker installing...');
    
    event.waitUntil(
      this.precacheStaticAssets()
        .then(() => {
          console.log('✅ Static assets pre-cached');
          // 🏃‍♂️ Take control immediately
          return self.skipWaiting();
        })
        .catch((error) => {
          console.error('❌ Pre-caching failed:', error);
          throw error;
        })
    );
  }
  
  // ⚡ Handle Service Worker activation
  private handleActivate(event: ExtendableEvent): void {
    console.log('⚡ Service Worker activating...');
    
    event.waitUntil(
      Promise.all([
        this.cleanupOldCaches(),
        self.clients.claim() // Take control of all pages
      ]).then(() => {
        console.log('✅ Service Worker activated and controlling all pages');
      })
    );
  }
  
  // 🌐 Handle fetch events (network interception)
  private handleFetch(event: FetchEvent): void {
    const { request } = event;
    const url = new URL(request.url);
    
    // 🔍 Choose caching strategy based on request type
    if (this.isStaticAsset(url)) {
      event.respondWith(this.cacheFirstStrategy(request));
    } else if (this.isAPIRequest(url)) {
      event.respondWith(this.networkFirstStrategy(request));
    } else if (this.isNavigationRequest(request)) {
      event.respondWith(this.navigationStrategy(request));
    } else {
      // 🌐 For everything else, just fetch from network
      event.respondWith(fetch(request));
    }
  }
  
  // 📬 Handle messages from main thread
  private handleMessage(event: ExtendableMessageEvent): void {
    const { data } = event;
    
    switch (data.type) {
      case 'SKIP_WAITING':
        console.log('📨 Received SKIP_WAITING message');
        self.skipWaiting();
        break;
        
      case 'CACHE_URLS':
        console.log('📨 Received CACHE_URLS message');
        event.waitUntil(this.cacheUrls(data.urls));
        break;
        
      case 'CLEAR_CACHE':
        console.log('📨 Received CLEAR_CACHE message');
        event.waitUntil(this.clearCache(data.cacheName));
        break;
        
      default:
        console.log('📨 Unknown message type:', data.type);
    }
  }
  
  // 💾 Pre-cache static assets
  private async precacheStaticAssets(): Promise<void> {
    const cache = await caches.open(this.CACHE_NAME);
    const staticStrategy = this.cacheStrategies.find(s => s.name === 'static-assets');
    
    if (staticStrategy) {
      await cache.addAll(staticStrategy.resources);
    }
  }
  
  // 🧹 Clean up old caches
  private async cleanupOldCaches(): Promise<void> {
    const cacheNames = await caches.keys();
    const oldCaches = cacheNames.filter(name => 
      name !== this.CACHE_NAME && name !== this.RUNTIME_CACHE
    );
    
    await Promise.all(
      oldCaches.map(cacheName => {
        console.log('🗑️ Deleting old cache:', cacheName);
        return caches.delete(cacheName);
      })
    );
  }
  
  // 📦 Cache-first strategy (for static assets)
  private async cacheFirstStrategy(request: Request): Promise<Response> {
    try {
      // 🔍 Try cache first
      const cachedResponse = await caches.match(request);
      if (cachedResponse) {
        console.log('📦 Serving from cache:', request.url);
        return cachedResponse;
      }
      
      // 🌐 Fallback to network
      console.log('🌐 Fetching from network:', request.url);
      const networkResponse = await fetch(request);
      
      // 💾 Cache the response for next time
      if (networkResponse.status === 200) {
        const cache = await caches.open(this.CACHE_NAME);
        cache.put(request, networkResponse.clone());
      }
      
      return networkResponse;
      
    } catch (error) {
      console.error('❌ Cache-first strategy failed:', error);
      
      // 🚨 Return offline fallback
      return this.getOfflineFallback(request);
    }
  }
  
  // 🌐 Network-first strategy (for API requests)
  private async networkFirstStrategy(request: Request): Promise<Response> {
    try {
      // 🌐 Try network first
      console.log('🌐 Fetching from network:', request.url);
      const networkResponse = await fetch(request);
      
      // 💾 Cache successful responses
      if (networkResponse.status === 200) {
        const cache = await caches.open(this.RUNTIME_CACHE);
        cache.put(request, networkResponse.clone());
        
        // 🧹 Cleanup old entries
        this.cleanupRuntimeCache();
      }
      
      return networkResponse;
      
    } catch (error) {
      console.log('🔍 Network failed, trying cache:', request.url);
      
      // 🔍 Fallback to cache
      const cachedResponse = await caches.match(request);
      if (cachedResponse) {
        return cachedResponse;
      }
      
      // 🚨 Return error response
      return new Response(JSON.stringify({ 
        error: 'Offline', 
        message: 'No cached version available' 
      }), {
        status: 503,
        headers: { 'Content-Type': 'application/json' }
      });
    }
  }
  
  // 🧭 Navigation strategy (for page requests)
  private async navigationStrategy(request: Request): Promise<Response> {
    try {
      // 🌐 Try network first for navigation
      return await fetch(request);
    } catch (error) {
      // 📦 Fallback to cached index.html
      const cachedResponse = await caches.match('/index.html');
      if (cachedResponse) {
        return cachedResponse;
      }
      
      // 🚨 Return offline page
      return this.getOfflinePage();
    }
  }
  
  // 🔍 Helper methods for request classification
  private isStaticAsset(url: URL): boolean {
    const staticExtensions = ['.css', '.js', '.png', '.jpg', '.svg', '.woff', '.woff2'];
    return staticExtensions.some(ext => url.pathname.endsWith(ext));
  }
  
  private isAPIRequest(url: URL): boolean {
    return url.pathname.startsWith('/api/');
  }
  
  private isNavigationRequest(request: Request): boolean {
    return request.mode === 'navigate';
  }
  
  // 🚨 Offline fallbacks
  private async getOfflineFallback(request: Request): Promise<Response> {
    const url = new URL(request.url);
    
    if (url.pathname.endsWith('.html')) {
      return this.getOfflinePage();
    }
    
    return new Response('Offline', { status: 503 });
  }
  
  private async getOfflinePage(): Promise<Response> {
    const offlineHtml = `
      <!DOCTYPE html>
      <html>
      <head>
        <title>Offline</title>
        <style>
          body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
          .offline { color: #666; }
        </style>
      </head>
      <body>
        <div class="offline">
          <h1>🔌 You're Offline</h1>
          <p>Please check your internet connection and try again.</p>
        </div>
      </body>
      </html>
    `;
    
    return new Response(offlineHtml, {
      headers: { 'Content-Type': 'text/html' }
    });
  }
  
  // 🧹 Runtime cache management
  private async cleanupRuntimeCache(): Promise<void> {
    const cache = await caches.open(this.RUNTIME_CACHE);
    const keys = await cache.keys();
    
    if (keys.length > this.MAX_CACHE_ENTRIES) {
      // 🗑️ Delete oldest entries
      const keysToDelete = keys.slice(0, keys.length - this.MAX_CACHE_ENTRIES);
      await Promise.all(keysToDelete.map(key => cache.delete(key)));
    }
  }
  
  // 💾 Cache specific URLs
  private async cacheUrls(urls: string[]): Promise<void> {
    const cache = await caches.open(this.RUNTIME_CACHE);
    await cache.addAll(urls);
  }
  
  // 🗑️ Clear specific cache
  private async clearCache(cacheName?: string): Promise<void> {
    if (cacheName) {
      await caches.delete(cacheName);
    } else {
      const cacheNames = await caches.keys();
      await Promise.all(cacheNames.map(name => caches.delete(name)));
    }
  }
}

// 🚀 Initialize the Service Worker
const serviceWorker = new TypedServiceWorker();

💡 Advanced Caching Strategies

🎮 Example 1: E-commerce App Caching

// 🛒 E-commerce specific caching strategies
class EcommerceCacheManager {
  private readonly PRODUCT_CACHE = 'products-v1';
  private readonly USER_CACHE = 'user-data-v1';
  private readonly IMAGES_CACHE = 'product-images-v1';
  private readonly CHECKOUT_CACHE = 'checkout-v1';
  
  // 🏪 Product catalog caching strategy
  async handleProductRequest(event: FetchEvent): Promise<Response> {
    const url = new URL(event.request.url);
    
    if (url.pathname.includes('/api/products')) {
      return this.staleWhileRevalidateStrategy(event.request, this.PRODUCT_CACHE);
    }
    
    if (url.pathname.includes('/api/product/')) {
      return this.cacheFirstWithTTL(event.request, this.PRODUCT_CACHE, 10 * 60 * 1000); // 10 minutes
    }
    
    return fetch(event.request);
  }
  
  // 🖼️ Product image caching (long-term cache)
  async handleImageRequest(event: FetchEvent): Promise<Response> {
    const url = new URL(event.request.url);
    
    if (url.pathname.includes('/images/products')) {
      return this.cacheFirstStrategy(event.request, this.IMAGES_CACHE);
    }
    
    return fetch(event.request);
  }
  
  // 👤 User data caching (network-first with short TTL)
  async handleUserRequest(event: FetchEvent): Promise<Response> {
    const url = new URL(event.request.url);
    
    if (url.pathname.includes('/api/user')) {
      return this.networkFirstWithFallback(event.request, this.USER_CACHE);
    }
    
    return fetch(event.request);
  }
  
  // 💳 Checkout process (network-only, never cache)
  async handleCheckoutRequest(event: FetchEvent): Promise<Response> {
    const url = new URL(event.request.url);
    
    if (url.pathname.includes('/api/checkout') || 
        url.pathname.includes('/api/payment')) {
      // 🚫 Never cache sensitive operations
      return fetch(event.request);
    }
    
    return fetch(event.request);
  }
  
  // 🔄 Stale-while-revalidate strategy
  private async staleWhileRevalidateStrategy(
    request: Request, 
    cacheName: string
  ): Promise<Response> {
    
    const cache = await caches.open(cacheName);
    const cachedResponse = await cache.match(request);
    
    // 🌐 Always start network request
    const networkPromise = fetch(request).then(response => {
      if (response.status === 200) {
        // 💾 Update cache in background
        cache.put(request, response.clone());
      }
      return response;
    }).catch(error => {
      console.error('🌐 Network request failed:', error);
      throw error;
    });
    
    // 📦 Return cached version immediately if available
    if (cachedResponse) {
      console.log('📦 Serving stale content, revalidating in background');
      return cachedResponse;
    }
    
    // 🌐 Wait for network if no cache
    console.log('🌐 No cache available, waiting for network');
    return networkPromise;
  }
  
  // ⏰ Cache-first with TTL
  private async cacheFirstWithTTL(
    request: Request, 
    cacheName: string, 
    ttl: number
  ): Promise<Response> {
    
    const cache = await caches.open(cacheName);
    const cachedResponse = await cache.match(request);
    
    if (cachedResponse) {
      // 🕒 Check if cache is still valid
      const cachedDate = cachedResponse.headers.get('cached-date');
      if (cachedDate) {
        const age = Date.now() - parseInt(cachedDate, 10);
        if (age < ttl) {
          console.log(`📦 Serving from cache (age: ${Math.round(age / 1000)}s)`);
          return cachedResponse;
        }
      }
      console.log('⏰ Cache expired, fetching fresh data');
    }
    
    // 🌐 Fetch fresh data
    try {
      const networkResponse = await fetch(request);
      
      if (networkResponse.status === 200) {
        // 💾 Cache with timestamp
        const responseWithTimestamp = new Response(networkResponse.body, {
          status: networkResponse.status,
          statusText: networkResponse.statusText,
          headers: {
            ...networkResponse.headers,
            'cached-date': Date.now().toString()
          }
        });
        
        cache.put(request, responseWithTimestamp.clone());
        return responseWithTimestamp;
      }
      
      return networkResponse;
      
    } catch (error) {
      // 🔍 Return stale cache if network fails
      if (cachedResponse) {
        console.log('🌐 Network failed, serving stale cache');
        return cachedResponse;
      }
      throw error;
    }
  }
  
  // 🌐 Network-first with cache fallback
  private async networkFirstWithFallback(
    request: Request, 
    cacheName: string
  ): Promise<Response> {
    
    try {
      // 🌐 Try network first
      const networkResponse = await fetch(request);
      
      if (networkResponse.status === 200) {
        // 💾 Update cache
        const cache = await caches.open(cacheName);
        cache.put(request, networkResponse.clone());
      }
      
      return networkResponse;
      
    } catch (error) {
      console.log('🌐 Network failed, trying cache fallback');
      
      // 📦 Fallback to cache
      const cache = await caches.open(cacheName);
      const cachedResponse = await cache.match(request);
      
      if (cachedResponse) {
        return cachedResponse;
      }
      
      // 🚨 Return offline response
      return new Response(JSON.stringify({
        error: 'Offline',
        message: 'Unable to fetch data and no cached version available'
      }), {
        status: 503,
        headers: { 'Content-Type': 'application/json' }
      });
    }
  }
  
  // 📦 Simple cache-first strategy
  private async cacheFirstStrategy(
    request: Request, 
    cacheName: string
  ): Promise<Response> {
    
    const cache = await caches.open(cacheName);
    const cachedResponse = await cache.match(request);
    
    if (cachedResponse) {
      return cachedResponse;
    }
    
    // 🌐 Fetch and cache
    const networkResponse = await fetch(request);
    
    if (networkResponse.status === 200) {
      cache.put(request, networkResponse.clone());
    }
    
    return networkResponse;
  }
  
  // 🧹 Cache management utilities
  async clearExpiredCache(cacheName: string, maxAge: number): Promise<void> {
    const cache = await caches.open(cacheName);
    const keys = await cache.keys();
    
    for (const request of keys) {
      const response = await cache.match(request);
      if (response) {
        const cachedDate = response.headers.get('cached-date');
        if (cachedDate) {
          const age = Date.now() - parseInt(cachedDate, 10);
          if (age > maxAge) {
            console.log('🗑️ Deleting expired cache entry:', request.url);
            await cache.delete(request);
          }
        }
      }
    }
  }
  
  async getCacheStats(): Promise<{
    [cacheName: string]: {
      entries: number;
      totalSize: number;
      oldestEntry: string;
      newestEntry: string;
    };
  }> {
    const cacheNames = [this.PRODUCT_CACHE, this.USER_CACHE, this.IMAGES_CACHE];
    const stats: any = {};
    
    for (const cacheName of cacheNames) {
      const cache = await caches.open(cacheName);
      const keys = await cache.keys();
      
      let totalSize = 0;
      let oldestTime = Date.now();
      let newestTime = 0;
      let oldestEntry = '';
      let newestEntry = '';
      
      for (const request of keys) {
        const response = await cache.match(request);
        if (response) {
          const cachedDate = response.headers.get('cached-date');
          if (cachedDate) {
            const time = parseInt(cachedDate, 10);
            if (time < oldestTime) {
              oldestTime = time;
              oldestEntry = request.url;
            }
            if (time > newestTime) {
              newestTime = time;
              newestEntry = request.url;
            }
          }
          
          // 📏 Estimate response size
          const blob = await response.blob();
          totalSize += blob.size;
        }
      }
      
      stats[cacheName] = {
        entries: keys.length,
        totalSize,
        oldestEntry,
        newestEntry
      };
    }
    
    return stats;
  }
}

📊 Example 2: Background Sync for Data Synchronization

// 🔄 Background sync for offline data synchronization
interface SyncQueueItem {
  id: string;
  type: string;
  data: any;
  timestamp: number;
  retries: number;
  maxRetries: number;
}

class BackgroundSyncManager {
  private readonly SYNC_QUEUE_KEY = 'sync-queue';
  private readonly MAX_RETRIES = 3;
  
  // 📝 Register background sync
  async registerBackgroundSync(tag: string): Promise<void> {
    if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
      const registration = await navigator.serviceWorker.ready;
      await registration.sync.register(tag);
      console.log('📝 Background sync registered:', tag);
    } else {
      console.warn('⚠️ Background sync not supported');
      // 🔄 Fallback to immediate sync attempt
      this.fallbackSync();
    }
  }
  
  // 📤 Add data to sync queue
  async addToSyncQueue(type: string, data: any): Promise<string> {
    const item: SyncQueueItem = {
      id: `sync_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
      type,
      data,
      timestamp: Date.now(),
      retries: 0,
      maxRetries: this.MAX_RETRIES
    };
    
    // 💾 Store in IndexedDB or localStorage
    const queue = await this.getSyncQueue();
    queue.push(item);
    await this.saveSyncQueue(queue);
    
    console.log('📤 Added to sync queue:', item.id);
    
    // 🔄 Try to sync immediately if online
    if (navigator.onLine) {
      this.processSyncQueue();
    } else {
      // 📝 Register for background sync
      this.registerBackgroundSync('background-sync');
    }
    
    return item.id;
  }
  
  // 🔄 Process sync queue (called by Service Worker)
  async processSyncQueue(): Promise<void> {
    console.log('🔄 Processing sync queue...');
    
    const queue = await this.getSyncQueue();
    const processedItems: string[] = [];
    
    for (const item of queue) {
      try {
        await this.syncItem(item);
        processedItems.push(item.id);
        console.log('✅ Synced item:', item.id);
        
      } catch (error) {
        console.error('❌ Sync failed for item:', item.id, error);
        
        // 🔄 Increment retry count
        item.retries++;
        
        if (item.retries >= item.maxRetries) {
          console.error('💥 Max retries exceeded for item:', item.id);
          processedItems.push(item.id); // Remove from queue
          
          // 📊 Log failed sync for analytics
          this.logFailedSync(item, error);
        }
      }
    }
    
    // 🧹 Remove successfully synced items
    const updatedQueue = queue.filter(item => !processedItems.includes(item.id));
    await this.saveSyncQueue(updatedQueue);
    
    console.log(`✅ Sync complete. Processed: ${processedItems.length}, Remaining: ${updatedQueue.length}`);
  }
  
  // 📡 Sync individual item
  private async syncItem(item: SyncQueueItem): Promise<void> {
    const { type, data } = item;
    
    switch (type) {
      case 'CREATE_ORDER':
        await this.syncCreateOrder(data);
        break;
        
      case 'UPDATE_PROFILE':
        await this.syncUpdateProfile(data);
        break;
        
      case 'SUBMIT_REVIEW':
        await this.syncSubmitReview(data);
        break;
        
      case 'ANALYTICS_EVENT':
        await this.syncAnalyticsEvent(data);
        break;
        
      default:
        throw new Error(`Unknown sync type: ${type}`);
    }
  }
  
  // 🛒 Sync order creation
  private async syncCreateOrder(orderData: any): Promise<void> {
    const response = await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${orderData.token}`
      },
      body: JSON.stringify(orderData.order)
    });
    
    if (!response.ok) {
      throw new Error(`Order sync failed: ${response.status}`);
    }
    
    const result = await response.json();
    console.log('🛒 Order synced successfully:', result.orderId);
    
    // 📬 Notify main thread about successful sync
    this.notifyMainThread('ORDER_SYNCED', result);
  }
  
  // 👤 Sync profile update
  private async syncUpdateProfile(profileData: any): Promise<void> {
    const response = await fetch('/api/user/profile', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${profileData.token}`
      },
      body: JSON.stringify(profileData.profile)
    });
    
    if (!response.ok) {
      throw new Error(`Profile sync failed: ${response.status}`);
    }
    
    console.log('👤 Profile synced successfully');
    this.notifyMainThread('PROFILE_SYNCED', { success: true });
  }
  
  // ⭐ Sync review submission
  private async syncSubmitReview(reviewData: any): Promise<void> {
    const response = await fetch('/api/reviews', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${reviewData.token}`
      },
      body: JSON.stringify(reviewData.review)
    });
    
    if (!response.ok) {
      throw new Error(`Review sync failed: ${response.status}`);
    }
    
    const result = await response.json();
    console.log('⭐ Review synced successfully:', result.reviewId);
    this.notifyMainThread('REVIEW_SYNCED', result);
  }
  
  // 📊 Sync analytics event
  private async syncAnalyticsEvent(eventData: any): Promise<void> {
    const response = await fetch('/api/analytics/events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(eventData)
    });
    
    if (!response.ok) {
      throw new Error(`Analytics sync failed: ${response.status}`);
    }
    
    console.log('📊 Analytics event synced');
  }
  
  // 🔄 Fallback sync for unsupported browsers
  private async fallbackSync(): Promise<void> {
    console.log('🔄 Running fallback sync...');
    
    if (navigator.onLine) {
      await this.processSyncQueue();
    } else {
      // 👂 Listen for online event
      window.addEventListener('online', () => {
        console.log('🌐 Connection restored, running fallback sync');
        this.processSyncQueue();
      }, { once: true });
    }
  }
  
  // 📬 Notify main thread about sync results
  private async notifyMainThread(type: string, data: any): Promise<void> {
    const clients = await self.clients.matchAll();
    clients.forEach(client => {
      client.postMessage({
        type: 'SYNC_RESULT',
        syncType: type,
        data
      });
    });
  }
  
  // 📊 Log failed sync attempts
  private logFailedSync(item: SyncQueueItem, error: any): void {
    console.error('📊 Logging failed sync:', {
      itemId: item.id,
      type: item.type,
      retries: item.retries,
      error: error.message,
      timestamp: Date.now()
    });
    
    // 📤 Could send to analytics service
    // analytics.track('sync_failed', { ... });
  }
  
  // 💾 Storage helpers (IndexedDB or localStorage)
  private async getSyncQueue(): Promise<SyncQueueItem[]> {
    // 🎯 In a real implementation, you'd use IndexedDB
    const stored = localStorage.getItem(this.SYNC_QUEUE_KEY);
    return stored ? JSON.parse(stored) : [];
  }
  
  private async saveSyncQueue(queue: SyncQueueItem[]): Promise<void> {
    localStorage.setItem(this.SYNC_QUEUE_KEY, JSON.stringify(queue));
  }
  
  // 📊 Get sync queue status
  async getSyncStatus(): Promise<{
    queueLength: number;
    oldestItem?: SyncQueueItem;
    itemsByType: Record<string, number>;
  }> {
    const queue = await this.getSyncQueue();
    const itemsByType: Record<string, number> = {};
    
    queue.forEach(item => {
      itemsByType[item.type] = (itemsByType[item.type] || 0) + 1;
    });
    
    return {
      queueLength: queue.length,
      oldestItem: queue.length > 0 ? queue[0] : undefined,
      itemsByType
    };
  }
  
  // 🧹 Clear sync queue
  async clearSyncQueue(): Promise<void> {
    await this.saveSyncQueue([]);
    console.log('🧹 Sync queue cleared');
  }
}

// 🔄 Service Worker background sync event handler
self.addEventListener('sync', (event: SyncEvent) => {
  console.log('🔄 Background sync event:', event.tag);
  
  if (event.tag === 'background-sync') {
    const syncManager = new BackgroundSyncManager();
    event.waitUntil(syncManager.processSyncQueue());
  }
});

🚀 Progressive Web App Features

🧙‍♂️ Push Notifications

// 📬 Push notification management
class PushNotificationManager {
  private vapidPublicKey = 'your-vapid-public-key';
  
  // 📝 Request notification permission and subscribe
  async subscribe(): Promise<PushSubscription | null> {
    if (!('Notification' in window) || !('serviceWorker' in navigator)) {
      console.warn('⚠️ Push notifications not supported');
      return null;
    }
    
    // 🔔 Request permission
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') {
      console.log('🚫 Notification permission denied');
      return null;
    }
    
    // 📝 Get service worker registration
    const registration = await navigator.serviceWorker.ready;
    
    // 📡 Subscribe to push service
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
    });
    
    // 📤 Send subscription to server
    await this.sendSubscriptionToServer(subscription);
    
    console.log('✅ Push notification subscription successful');
    return subscription;
  }
  
  // 📤 Send subscription to server
  private async sendSubscriptionToServer(subscription: PushSubscription): Promise<void> {
    await fetch('/api/push/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        subscription,
        userAgent: navigator.userAgent
      })
    });
  }
  
  // 🔧 VAPID key conversion utility
  private urlBase64ToUint8Array(base64String: string): Uint8Array {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/-/g, '+')
      .replace(/_/g, '/');
    
    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);
    
    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    
    return outputArray;
  }
  
  // 📱 Show local notification
  async showLocalNotification(
    title: string, 
    options: NotificationOptions = {}
  ): Promise<void> {
    
    if (!('Notification' in window)) {
      console.warn('⚠️ Notifications not supported');
      return;
    }
    
    if (Notification.permission === 'granted') {
      const registration = await navigator.serviceWorker.ready;
      
      await registration.showNotification(title, {
        icon: '/icons/notification-icon.png',
        badge: '/icons/badge-icon.png',
        ...options
      });
    }
  }
}

// 📬 Service Worker push event handler
self.addEventListener('push', (event: PushEvent) => {
  console.log('📬 Push message received');
  
  let notificationData = {
    title: 'New Notification',
    body: 'You have a new message',
    icon: '/icons/notification-icon.png',
    badge: '/icons/badge-icon.png',
    tag: 'default',
    data: {}
  };
  
  if (event.data) {
    try {
      notificationData = { ...notificationData, ...event.data.json() };
    } catch (error) {
      console.error('❌ Error parsing push data:', error);
    }
  }
  
  event.waitUntil(
    self.registration.showNotification(notificationData.title, {
      body: notificationData.body,
      icon: notificationData.icon,
      badge: notificationData.badge,
      tag: notificationData.tag,
      data: notificationData.data,
      actions: [
        {
          action: 'view',
          title: 'View',
          icon: '/icons/view-icon.png'
        },
        {
          action: 'dismiss',
          title: 'Dismiss',
          icon: '/icons/dismiss-icon.png'
        }
      ]
    })
  );
});

// 🖱️ Notification click handler
self.addEventListener('notificationclick', (event: NotificationEvent) => {
  console.log('🖱️ Notification clicked:', event.action);
  
  event.notification.close();
  
  const clickAction = event.action || 'default';
  const notificationData = event.notification.data;
  
  event.waitUntil(
    self.clients.matchAll({ type: 'window' }).then((clients) => {
      // 🔍 Check if app is already open
      for (const client of clients) {
        if (client.url.includes(self.location.origin) && 'focus' in client) {
          // 📬 Send message to existing client
          client.postMessage({
            type: 'NOTIFICATION_CLICKED',
            action: clickAction,
            data: notificationData
          });
          return client.focus();
        }
      }
      
      // 🆕 Open new window if no existing client
      if (clients.openWindow) {
        return clients.openWindow('/');
      }
    })
  );
});

🎯 App Installation and Updates

// 📱 PWA installation management
class PWAInstallManager {
  private deferredPrompt: BeforeInstallPromptEvent | null = null;
  private isInstalled = false;
  
  constructor() {
    this.setupInstallListeners();
    this.checkInstallStatus();
  }
  
  // 🎯 Set up installation event listeners
  private setupInstallListeners(): void {
    // 📱 Capture install prompt
    window.addEventListener('beforeinstallprompt', (event) => {
      console.log('📱 Install prompt available');
      event.preventDefault();
      this.deferredPrompt = event as BeforeInstallPromptEvent;
      this.showInstallButton();
    });
    
    // ✅ App installed
    window.addEventListener('appinstalled', () => {
      console.log('✅ PWA installed successfully');
      this.isInstalled = true;
      this.hideInstallButton();
      this.onInstalled();
    });
  }
  
  // 🔍 Check if app is already installed
  private checkInstallStatus(): void {
    // 📱 Check for standalone mode (installed PWA)
    if (window.matchMedia('(display-mode: standalone)').matches ||
        (window.navigator as any).standalone === true) {
      this.isInstalled = true;
      console.log('📱 PWA is running in installed mode');
    }
  }
  
  // 📱 Trigger install prompt
  async promptInstall(): Promise<boolean> {
    if (!this.deferredPrompt) {
      console.log('⚠️ No install prompt available');
      return false;
    }
    
    try {
      // 📱 Show install prompt
      this.deferredPrompt.prompt();
      
      // ⏳ Wait for user choice
      const choiceResult = await this.deferredPrompt.userChoice;
      
      if (choiceResult.outcome === 'accepted') {
        console.log('✅ User accepted install prompt');
        return true;
      } else {
        console.log('❌ User dismissed install prompt');
        return false;
      }
      
    } catch (error) {
      console.error('❌ Install prompt failed:', error);
      return false;
    } finally {
      this.deferredPrompt = null;
    }
  }
  
  // 🎨 UI management for install button
  private showInstallButton(): void {
    const installButton = document.getElementById('install-button');
    if (installButton) {
      installButton.style.display = 'block';
      installButton.addEventListener('click', () => {
        this.promptInstall();
      });
    }
  }
  
  private hideInstallButton(): void {
    const installButton = document.getElementById('install-button');
    if (installButton) {
      installButton.style.display = 'none';
    }
  }
  
  // 🎉 Handle successful installation
  private onInstalled(): void {
    // 📊 Track installation analytics
    console.log('📊 Tracking PWA installation');
    
    // 🎁 Show welcome message
    this.showWelcomeMessage();
    
    // 🔔 Request notification permission
    this.requestNotificationPermission();
  }
  
  private showWelcomeMessage(): void {
    // 🎉 Show install success message
    const message = document.createElement('div');
    message.className = 'install-success-message';
    message.innerHTML = `
      <div style="
        position: fixed;
        top: 20px;
        right: 20px;
        background: #4CAF50;
        color: white;
        padding: 15px;
        border-radius: 5px;
        z-index: 1000;
      ">
        🎉 App installed successfully!
      </div>
    `;
    
    document.body.appendChild(message);
    
    // 🗑️ Remove message after 3 seconds
    setTimeout(() => {
      document.body.removeChild(message);
    }, 3000);
  }
  
  private async requestNotificationPermission(): Promise<void> {
    if ('Notification' in window && Notification.permission === 'default') {
      const permission = await Notification.requestPermission();
      console.log('🔔 Notification permission:', permission);
    }
  }
  
  // 📊 Get installation status
  getInstallStatus(): {
    isInstallable: boolean;
    isInstalled: boolean;
    canPrompt: boolean;
  } {
    return {
      isInstallable: !!this.deferredPrompt,
      isInstalled: this.isInstalled,
      canPrompt: !!this.deferredPrompt
    };
  }
}

// 🔄 App update management
class PWAUpdateManager {
  private registration: ServiceWorkerRegistration | null = null;
  private updateAvailable = false;
  
  constructor(registration: ServiceWorkerRegistration) {
    this.registration = registration;
    this.setupUpdateDetection();
  }
  
  // 🔄 Set up update detection
  private setupUpdateDetection(): void {
    if (!this.registration) return;
    
    // 🆕 New service worker installing
    this.registration.addEventListener('updatefound', () => {
      const newWorker = this.registration!.installing;
      
      if (newWorker) {
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            console.log('🆕 App update available');
            this.updateAvailable = true;
            this.showUpdateNotification();
          }
        });
      }
    });
  }
  
  // 📢 Show update notification
  private showUpdateNotification(): void {
    const updateBanner = document.createElement('div');
    updateBanner.id = 'update-banner';
    updateBanner.innerHTML = `
      <div style="
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        background: #2196F3;
        color: white;
        padding: 15px;
        text-align: center;
        z-index: 1001;
      ">
        🆕 A new version is available!
        <button id="update-button" style="
          margin-left: 10px;
          background: white;
          color: #2196F3;
          border: none;
          padding: 5px 10px;
          border-radius: 3px;
          cursor: pointer;
        ">
          Update Now
        </button>
        <button id="dismiss-update" style="
          margin-left: 10px;
          background: transparent;
          color: white;
          border: 1px solid white;
          padding: 5px 10px;
          border-radius: 3px;
          cursor: pointer;
        ">
          Later
        </button>
      </div>
    `;
    
    document.body.appendChild(updateBanner);
    
    // 🔄 Handle update button click
    document.getElementById('update-button')?.addEventListener('click', () => {
      this.applyUpdate();
    });
    
    // ❌ Handle dismiss button click
    document.getElementById('dismiss-update')?.addEventListener('click', () => {
      this.dismissUpdate();
    });
  }
  
  // ⚡ Apply the update
  private applyUpdate(): void {
    if (!this.registration || !this.registration.waiting) {
      console.error('❌ No waiting service worker found');
      return;
    }
    
    // 📤 Tell the waiting SW to activate
    this.registration.waiting.postMessage({ type: 'SKIP_WAITING' });
    
    // 🔄 Reload the page to use new service worker
    window.location.reload();
  }
  
  // ❌ Dismiss update notification
  private dismissUpdate(): void {
    const updateBanner = document.getElementById('update-banner');
    if (updateBanner) {
      document.body.removeChild(updateBanner);
    }
  }
  
  // 🔍 Check for updates manually
  async checkForUpdates(): Promise<boolean> {
    if (!this.registration) {
      return false;
    }
    
    try {
      await this.registration.update();
      return this.updateAvailable;
    } catch (error) {
      console.error('❌ Update check failed:', error);
      return false;
    }
  }
}

⚠️ Common Pitfalls and Solutions

😱 Pitfall 1: Cache Storage Quota Issues

// ❌ Dangerous - unlimited caching without quota management
class UncontrolledCache {
  async cacheEverything(requests: Request[]): Promise<void> {
    const cache = await caches.open('unlimited-cache');
    
    // 💥 This could exceed storage quota!
    await cache.addAll(requests.map(r => r.url));
  }
}

// ✅ Safe - quota-aware caching with cleanup
class QuotaAwareCache {
  private readonly MAX_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
  private readonly CACHE_NAME = 'managed-cache-v1';
  
  async cacheResources(requests: Request[]): Promise<void> {
    try {
      // 📊 Check available quota first
      const quota = await this.getStorageQuota();
      console.log(`💾 Storage quota: ${this.formatBytes(quota.quota)}, Used: ${this.formatBytes(quota.usage)}`);
      
      if (quota.usage / quota.quota > 0.8) {
        console.log('⚠️ Storage quota nearly full, cleaning up...');
        await this.cleanupOldCache();
      }
      
      const cache = await caches.open(this.CACHE_NAME);
      
      // 📦 Cache resources with size checking
      for (const request of requests) {
        await this.cacheWithSizeCheck(cache, request);
      }
      
    } catch (error) {
      if (error.name === 'QuotaExceededError') {
        console.error('💥 Storage quota exceeded!');
        await this.emergencyCleanup();
      } else {
        throw error;
      }
    }
  }
  
  // 📊 Get storage quota information
  private async getStorageQuota(): Promise<{ quota: number; usage: number }> {
    if ('storage' in navigator && 'estimate' in navigator.storage) {
      const estimate = await navigator.storage.estimate();
      return {
        quota: estimate.quota || 0,
        usage: estimate.usage || 0
      };
    }
    
    // 🔄 Fallback for unsupported browsers
    return { quota: 100 * 1024 * 1024, usage: 0 }; // Assume 100MB
  }
  
  // 📦 Cache with size checking
  private async cacheWithSizeCheck(cache: Cache, request: Request): Promise<void> {
    try {
      const response = await fetch(request);
      
      // 📏 Check response size
      const responseSize = this.getResponseSize(response);
      
      if (responseSize > 5 * 1024 * 1024) { // 5MB limit per resource
        console.warn(`⚠️ Skipping large resource: ${request.url} (${this.formatBytes(responseSize)})`);
        return;
      }
      
      // 💾 Cache the response
      await cache.put(request, response);
      
    } catch (error) {
      console.error(`❌ Failed to cache ${request.url}:`, error);
    }
  }
  
  // 📏 Estimate response size
  private getResponseSize(response: Response): number {
    const contentLength = response.headers.get('content-length');
    return contentLength ? parseInt(contentLength, 10) : 0;
  }
  
  // 🧹 Clean up old cache entries
  private async cleanupOldCache(): Promise<void> {
    console.log('🧹 Starting cache cleanup...');
    
    const cache = await caches.open(this.CACHE_NAME);
    const keys = await cache.keys();
    
    // 🗑️ Remove oldest entries (simple LRU)
    const entriesToRemove = Math.floor(keys.length * 0.3); // Remove 30%
    
    for (let i = 0; i < entriesToRemove; i++) {
      await cache.delete(keys[i]);
      console.log(`🗑️ Removed cache entry: ${keys[i].url}`);
    }
    
    console.log(`✅ Cleanup complete. Removed ${entriesToRemove} entries`);
  }
  
  // 🚨 Emergency cleanup when quota exceeded
  private async emergencyCleanup(): Promise<void> {
    console.log('🚨 Emergency cache cleanup...');
    
    // 🗑️ Clear all caches except essential ones
    const cacheNames = await caches.keys();
    const essentialCaches = ['app-shell-v1', 'critical-resources-v1'];
    
    for (const cacheName of cacheNames) {
      if (!essentialCaches.includes(cacheName)) {
        await caches.delete(cacheName);
        console.log(`🗑️ Emergency: Deleted cache ${cacheName}`);
      }
    }
  }
  
  // 🎨 Format bytes for display
  private formatBytes(bytes: number): string {
    if (bytes === 0) return '0 Bytes';
    
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
  
  // 📊 Get cache statistics
  async getCacheStats(): Promise<{
    cacheCount: number;
    totalSize: number;
    largestCache: string;
    quotaUsage: number;
  }> {
    const cacheNames = await caches.keys();
    const quota = await this.getStorageQuota();
    
    let totalSize = 0;
    let largestCache = '';
    let largestSize = 0;
    
    for (const cacheName of cacheNames) {
      const cache = await caches.open(cacheName);
      const keys = await cache.keys();
      
      let cacheSize = 0;
      for (const request of keys) {
        const response = await cache.match(request);
        if (response) {
          cacheSize += this.getResponseSize(response);
        }
      }
      
      totalSize += cacheSize;
      
      if (cacheSize > largestSize) {
        largestSize = cacheSize;
        largestCache = cacheName;
      }
    }
    
    return {
      cacheCount: cacheNames.length,
      totalSize,
      largestCache,
      quotaUsage: quota.usage / quota.quota
    };
  }
}

🤯 Pitfall 2: Service Worker Update Loops

// ❌ Dangerous - can cause infinite update loops
class ProblematicServiceWorker {
  constructor() {
    // 💥 This can cause update loops!
    self.addEventListener('install', () => {
      self.skipWaiting(); // Immediately activates new SW
    });
    
    self.addEventListener('activate', () => {
      self.clients.claim(); // Takes control immediately
    });
  }
}

// ✅ Safe - controlled update process
class SafeServiceWorker {
  private updateCheckInterval: number | null = null;
  
  constructor() {
    this.setupSafeEventListeners();
  }
  
  private setupSafeEventListeners(): void {
    // 🚀 Install event - prepare but don't activate immediately
    self.addEventListener('install', (event: ExtendableEvent) => {
      console.log('🚀 Service Worker installing...');
      
      event.waitUntil(
        this.precacheResources().then(() => {
          console.log('✅ Resources precached');
          // ⏳ Don't skip waiting automatically
          // Let user control when to activate
        })
      );
    });
    
    // ⚡ Activate event - clean up and take control
    self.addEventListener('activate', (event: ExtendableEvent) => {
      console.log('⚡ Service Worker activating...');
      
      event.waitUntil(
        Promise.all([
          this.cleanupOldCaches(),
          // 🎯 Only claim clients if no other SW is controlling
          this.claimClientsIfSafe()
        ]).then(() => {
          console.log('✅ Service Worker activated safely');
        })
      );
    });
    
    // 📬 Message event - handle update requests
    self.addEventListener('message', (event: ExtendableMessageEvent) => {
      if (event.data && event.data.type === 'SKIP_WAITING') {
        console.log('📬 Received skip waiting request');
        self.skipWaiting();
      }
    });
  }
  
  // 🎯 Safe client claiming
  private async claimClientsIfSafe(): Promise<void> {
    // 🔍 Check if there are any controlled clients
    const clients = await self.clients.matchAll({ type: 'window' });
    
    if (clients.length === 0) {
      // 🆕 No existing clients, safe to claim
      console.log('🎯 No existing clients, claiming control');
      return self.clients.claim();
    } else {
      // 📢 There are existing clients, let them decide
      console.log('📢 Existing clients found, notifying about update');
      
      clients.forEach(client => {
        client.postMessage({
          type: 'NEW_VERSION_AVAILABLE',
          message: 'A new version of the app is available'
        });
      });
    }
  }
  
  // 💾 Precache essential resources
  private async precacheResources(): Promise<void> {
    const essentialResources = [
      '/',
      '/offline.html',
      '/app.css',
      '/app.js'
    ];
    
    const cache = await caches.open('app-shell-v1');
    
    // 📦 Cache resources one by one with error handling
    for (const resource of essentialResources) {
      try {
        await cache.add(resource);
        console.log(`✅ Cached: ${resource}`);
      } catch (error) {
        console.error(`❌ Failed to cache ${resource}:`, error);
        // 🔄 Don't fail the entire installation for one resource
      }
    }
  }
  
  // 🧹 Clean up old caches safely
  private async cleanupOldCaches(): Promise<void> {
    const currentCaches = ['app-shell-v1', 'runtime-cache-v1'];
    const cacheNames = await caches.keys();
    
    const oldCaches = cacheNames.filter(name => !currentCaches.includes(name));
    
    await Promise.all(
      oldCaches.map(async (cacheName) => {
        console.log(`🗑️ Deleting old cache: ${cacheName}`);
        return caches.delete(cacheName);
      })
    );
  }
  
  // 🔄 Controlled update checking
  startPeriodicUpdateCheck(intervalMs: number = 60000): void {
    if (this.updateCheckInterval) {
      clearInterval(this.updateCheckInterval);
    }
    
    this.updateCheckInterval = setInterval(async () => {
      try {
        // 🔍 Check for updates without forcing immediate activation
        const registration = await self.registration.update();
        console.log('🔍 Update check completed');
      } catch (error) {
        console.error('❌ Update check failed:', error);
      }
    }, intervalMs) as any;
  }
  
  stopPeriodicUpdateCheck(): void {
    if (this.updateCheckInterval) {
      clearInterval(this.updateCheckInterval);
      this.updateCheckInterval = null;
    }
  }
}

// 🔄 Main thread update handler
class SafeUpdateHandler {
  private registration: ServiceWorkerRegistration;
  
  constructor(registration: ServiceWorkerRegistration) {
    this.registration = registration;
    this.setupUpdateHandling();
  }
  
  private setupUpdateHandling(): void {
    // 📬 Listen for messages from Service Worker
    navigator.serviceWorker.addEventListener('message', (event) => {
      if (event.data && event.data.type === 'NEW_VERSION_AVAILABLE') {
        this.showUpdatePrompt();
      }
    });
    
    // 🔄 Listen for controller changes
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      console.log('🔄 New Service Worker took control');
      
      // ⚠️ Only reload if user explicitly requested update
      if (localStorage.getItem('pending-sw-update') === 'true') {
        localStorage.removeItem('pending-sw-update');
        window.location.reload();
      }
    });
  }
  
  private showUpdatePrompt(): void {
    const shouldUpdate = confirm(
      'A new version of the app is available. Would you like to update now?'
    );
    
    if (shouldUpdate) {
      this.applyUpdate();
    }
  }
  
  private applyUpdate(): void {
    if (this.registration.waiting) {
      // 📝 Mark that user requested update
      localStorage.setItem('pending-sw-update', 'true');
      
      // 📤 Tell waiting SW to activate
      this.registration.waiting.postMessage({ type: 'SKIP_WAITING' });
    }
  }
}

🛠️ Best Practices and Production Tips

🎯 Performance Optimization

// 📊 Advanced Service Worker performance optimization
class PerformanceOptimizedServiceWorker {
  private performanceMetrics = new Map<string, number>();
  private cacheHitRate = new Map<string, { hits: number; misses: number }>();
  
  // ⚡ Optimized fetch handler with performance tracking
  async handleFetch(event: FetchEvent): Promise<Response> {
    const startTime = performance.now();
    const url = new URL(event.request.url);
    
    try {
      let response: Response;
      
      // 🎯 Route to appropriate strategy
      if (this.isStaticAsset(url)) {
        response = await this.optimizedCacheFirst(event.request);
      } else if (this.isAPIRequest(url)) {
        response = await this.optimizedNetworkFirst(event.request);
      } else {
        response = await fetch(event.request);
      }
      
      // 📊 Track performance metrics
      const duration = performance.now() - startTime;
      this.recordMetric(event.request.url, duration);
      
      return response;
      
    } catch (error) {
      console.error(`❌ Fetch failed for ${event.request.url}:`, error);
      return this.getFallbackResponse(event.request);
    }
  }
  
  // 🚀 Optimized cache-first with streaming
  private async optimizedCacheFirst(request: Request): Promise<Response> {
    const cacheKey = this.getCacheKey(request);
    
    // 🔍 Try cache first
    const cachedResponse = await caches.match(request);
    
    if (cachedResponse) {
      this.recordCacheHit(cacheKey);
      
      // 🔄 Start background update for critical resources
      if (this.isCriticalResource(request)) {
        this.backgroundUpdate(request);
      }
      
      return cachedResponse;
    }
    
    this.recordCacheMiss(cacheKey);
    
    // 🌐 Fetch from network
    const networkResponse = await fetch(request);
    
    // 💾 Cache in background if successful
    if (networkResponse.status === 200) {
      this.backgroundCache(request, networkResponse.clone());
    }
    
    return networkResponse;
  }
  
  // 🌐 Optimized network-first with timeout
  private async optimizedNetworkFirst(request: Request): Promise<Response> {
    const cacheKey = this.getCacheKey(request);
    const timeout = this.getNetworkTimeout(request);
    
    try {
      // 🌐 Network request with timeout
      const networkResponse = await this.fetchWithTimeout(request, timeout);
      
      this.recordCacheHit(cacheKey); // Network success
      
      // 💾 Update cache in background
      if (networkResponse.status === 200) {
        this.backgroundCache(request, networkResponse.clone());
      }
      
      return networkResponse;
      
    } catch (error) {
      console.log(`🌐 Network failed for ${request.url}, trying cache`);
      
      // 🔍 Fallback to cache
      const cachedResponse = await caches.match(request);
      
      if (cachedResponse) {
        this.recordCacheMiss(cacheKey); // Network failed, cache hit
        return cachedResponse;
      }
      
      throw error;
    }
  }
  
  // ⏰ Fetch with timeout
  private async fetchWithTimeout(request: Request, timeoutMs: number): Promise<Response> {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
    
    try {
      const response = await fetch(request, { 
        signal: controller.signal 
      });
      clearTimeout(timeoutId);
      return response;
    } catch (error) {
      clearTimeout(timeoutId);
      throw error;
    }
  }
  
  // 🔄 Background cache update
  private async backgroundCache(request: Request, response: Response): Promise<void> {
    try {
      const cache = await caches.open(this.getCacheName(request));
      await cache.put(request, response);
    } catch (error) {
      console.error('❌ Background cache failed:', error);
    }
  }
  
  // 🔄 Background resource update
  private async backgroundUpdate(request: Request): Promise<void> {
    try {
      const response = await fetch(request);
      if (response.status === 200) {
        await this.backgroundCache(request, response);
      }
    } catch (error) {
      console.error('❌ Background update failed:', error);
    }
  }
  
  // ⏰ Get network timeout based on request type
  private getNetworkTimeout(request: Request): number {
    const url = new URL(request.url);
    
    if (url.pathname.includes('/api/')) {
      return 5000; // 5s for API requests
    }
    
    return 10000; // 10s for other requests
  }
  
  // 🎯 Get cache name based on request type
  private getCacheName(request: Request): string {
    const url = new URL(request.url);
    
    if (this.isStaticAsset(url)) {
      return 'static-assets-v1';
    } else if (this.isAPIRequest(url)) {
      return 'api-cache-v1';
    }
    
    return 'runtime-cache-v1';
  }
  
  // 🔑 Generate cache key
  private getCacheKey(request: Request): string {
    return `${request.method}:${request.url}`;
  }
  
  // 🔍 Check if resource is critical
  private isCriticalResource(request: Request): boolean {
    const url = new URL(request.url);
    const criticalPaths = ['/app.js', '/app.css', '/index.html'];
    return criticalPaths.some(path => url.pathname.includes(path));
  }
  
  // 📊 Performance metrics tracking
  private recordMetric(url: string, duration: number): void {
    const key = new URL(url).pathname;
    this.performanceMetrics.set(key, duration);
    
    // 🧹 Keep only recent metrics
    if (this.performanceMetrics.size > 100) {
      const oldestKey = this.performanceMetrics.keys().next().value;
      this.performanceMetrics.delete(oldestKey);
    }
  }
  
  private recordCacheHit(cacheKey: string): void {
    const stats = this.cacheHitRate.get(cacheKey) || { hits: 0, misses: 0 };
    stats.hits++;
    this.cacheHitRate.set(cacheKey, stats);
  }
  
  private recordCacheMiss(cacheKey: string): void {
    const stats = this.cacheHitRate.get(cacheKey) || { hits: 0, misses: 0 };
    stats.misses++;
    this.cacheHitRate.set(cacheKey, stats);
  }
  
  // 📊 Get performance report
  getPerformanceReport(): {
    averageResponseTime: number;
    cacheHitRates: Array<{ url: string; hitRate: number; total: number }>;
    slowestRequests: Array<{ url: string; duration: number }>;
  } {
    // Calculate average response time
    const durations = Array.from(this.performanceMetrics.values());
    const averageResponseTime = durations.length > 0 
      ? durations.reduce((sum, duration) => sum + duration, 0) / durations.length 
      : 0;
    
    // Calculate cache hit rates
    const cacheHitRates = Array.from(this.cacheHitRate.entries()).map(([url, stats]) => ({
      url,
      hitRate: stats.hits / (stats.hits + stats.misses),
      total: stats.hits + stats.misses
    }));
    
    // Find slowest requests
    const slowestRequests = Array.from(this.performanceMetrics.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10)
      .map(([url, duration]) => ({ url, duration }));
    
    return {
      averageResponseTime,
      cacheHitRates,
      slowestRequests
    };
  }
  
  // 🚨 Fallback responses
  private async getFallbackResponse(request: Request): Promise<Response> {
    const url = new URL(request.url);
    
    if (request.mode === 'navigate') {
      // 📄 Return cached offline page
      const offlineResponse = await caches.match('/offline.html');
      if (offlineResponse) {
        return offlineResponse;
      }
    }
    
    if (this.isAPIRequest(url)) {
      // 📡 Return offline API response
      return new Response(JSON.stringify({
        error: 'Offline',
        message: 'This feature is not available offline'
      }), {
        status: 503,
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    // 🚫 Generic offline response
    return new Response('Offline', { status: 503 });
  }
  
  // 🔍 Helper methods
  private isStaticAsset(url: URL): boolean {
    const staticExtensions = ['.css', '.js', '.png', '.jpg', '.svg', '.woff', '.woff2'];
    return staticExtensions.some(ext => url.pathname.endsWith(ext));
  }
  
  private isAPIRequest(url: URL): boolean {
    return url.pathname.startsWith('/api/');
  }
}

🧪 Hands-On Exercise

🎯 Challenge: Build a Complete PWA with Offline Support

Create a comprehensive PWA that demonstrates all Service Worker capabilities:

📋 Requirements:

  • ✅ Implement multiple caching strategies for different content types
  • 🏷️ Add background sync for form submissions and user actions
  • 👤 Enable push notifications with user preference management
  • 📅 Support app installation with update notifications
  • 🎨 Create an offline-first experience with graceful degradation

🚀 Bonus Points:

  • Add performance monitoring and cache optimization
  • Implement progressive enhancement based on network conditions
  • Create a cache management dashboard for users

💡 Solution Framework

🔍 Click to see solution framework
// 🎯 Complete PWA implementation framework

// Main PWA Manager
class PWAManager {
  private swManager: ServiceWorkerManager;
  private cacheManager: QuotaAwareCache;
  private syncManager: BackgroundSyncManager;
  private installManager: PWAInstallManager;
  private notificationManager: PushNotificationManager;
  
  constructor() {
    this.initializeManagers();
    this.setupEventListeners();
  }
  
  private initializeManagers(): void {
    this.swManager = new ServiceWorkerManager('/sw.js');
    this.cacheManager = new QuotaAwareCache();
    this.syncManager = new BackgroundSyncManager();
    this.installManager = new PWAInstallManager();
    this.notificationManager = new PushNotificationManager();
  }
  
  async initialize(): Promise<void> {
    try {
      // 📝 Register Service Worker
      await this.swManager.register();
      
      // 📦 Pre-cache critical resources
      await this.cacheManager.cacheResources([
        new Request('/'),
        new Request('/offline.html'),
        new Request('/app.css'),
        new Request('/app.js')
      ]);
      
      // 🔔 Set up notifications if supported
      await this.notificationManager.subscribe();
      
      console.log('✅ PWA initialized successfully');
      
    } catch (error) {
      console.error('❌ PWA initialization failed:', error);
    }
  }
  
  private setupEventListeners(): void {
    // 🌐 Network status monitoring
    window.addEventListener('online', () => {
      console.log('🌐 Back online, syncing pending data');
      this.syncManager.processSyncQueue();
    });
    
    window.addEventListener('offline', () => {
      console.log('📡 Gone offline, enabling offline mode');
      this.showOfflineIndicator();
    });
    
    // 📱 Install prompt handling
    this.installManager.onInstall(() => {
      this.showInstallSuccessMessage();
    });
    
    // 🔄 Service Worker update handling
    this.swManager.onUpdateAvailable(() => {
      this.showUpdateNotification();
    });
  }
  
  // 📤 Submit form with offline support
  async submitForm(formData: FormData, endpoint: string): Promise<void> {
    if (navigator.onLine) {
      try {
        await this.submitFormOnline(formData, endpoint);
      } catch (error) {
        await this.queueForBackgroundSync('FORM_SUBMISSION', { formData, endpoint });
      }
    } else {
      await this.queueForBackgroundSync('FORM_SUBMISSION', { formData, endpoint });
    }
  }
  
  private async submitFormOnline(formData: FormData, endpoint: string): Promise<void> {
    const response = await fetch(endpoint, {
      method: 'POST',
      body: formData
    });
    
    if (!response.ok) {
      throw new Error(`Form submission failed: ${response.status}`);
    }
  }
  
  private async queueForBackgroundSync(type: string, data: any): Promise<void> {
    await this.syncManager.addToSyncQueue(type, data);
    this.showOfflineSubmissionMessage();
  }
  
  // UI feedback methods
  private showOfflineIndicator(): void {
    // Implementation for offline indicator
  }
  
  private showInstallSuccessMessage(): void {
    // Implementation for install success message
  }
  
  private showUpdateNotification(): void {
    // Implementation for update notification
  }
  
  private showOfflineSubmissionMessage(): void {
    // Implementation for offline submission feedback
  }
  
  // 📊 Get PWA status
  async getStatus(): Promise<{
    serviceWorker: any;
    cache: any;
    sync: any;
    install: any;
  }> {
    return {
      serviceWorker: this.swManager.getStatus(),
      cache: await this.cacheManager.getCacheStats(),
      sync: await this.syncManager.getSyncStatus(),
      install: this.installManager.getInstallStatus()
    };
  }
}

// Usage
const pwa = new PWAManager();
await pwa.initialize();

🎓 Key Takeaways

You’ve mastered Service Workers! Here’s what you can now do:

  • Implement robust offline functionality with smart caching strategies 💪
  • Build Progressive Web Apps with install and update capabilities 🛡️
  • Handle background sync for seamless data synchronization 🎯
  • Manage cache storage efficiently with quota awareness 🐛
  • Create resilient web applications that work in any network condition 🚀

Remember: Service Workers are the foundation of modern offline-first web applications. They make your app work everywhere! 🤝

🤝 Next Steps

Congratulations! 🎉 You’ve mastered Service Workers and offline functionality!

Here’s what to do next:

  1. 💻 Practice with the PWA exercise above
  2. 🏗️ Build a real PWA with all the features you’ve learned
  3. 📚 Move on to our next tutorial: “WebSockets with TypeScript: Real-Time Communication”
  4. 🌟 Share your Service Worker knowledge with others!

Remember: The best PWAs feel native and work seamlessly regardless of network conditions. Focus on user experience and progressive enhancement! 🚀


Happy offline coding! 🎉🔌✨