javascript

examples

examples.js
/**
 * Service Workers and PWA Examples
 *
 * Demonstrates offline-capable Progressive Web App features
 */

// =============================================================================
// 1. Service Worker Registration
// =============================================================================

/**
 * Register Service Worker with comprehensive handling
 */
class ServiceWorkerManager {
  constructor(swPath = '/sw.js', options = {}) {
    this.swPath = swPath;
    this.options = options;
    this.registration = null;
  }

  /**
   * Register the Service Worker
   */
  async register() {
    if (!('serviceWorker' in navigator)) {
      throw new Error('Service Workers not supported');
    }

    try {
      this.registration = await navigator.serviceWorker.register(this.swPath, {
        scope: this.options.scope || '/',
      });

      console.log('SW registered:', this.registration.scope);

      // Set up update handling
      this.setupUpdateHandler();

      return this.registration;
    } catch (error) {
      console.error('SW registration failed:', error);
      throw error;
    }
  }

  /**
   * Set up handlers for SW updates
   */
  setupUpdateHandler() {
    this.registration.addEventListener('updatefound', () => {
      const newWorker = this.registration.installing;
      console.log('New SW found, installing...');

      newWorker.addEventListener('statechange', () => {
        console.log('SW state:', newWorker.state);

        if (newWorker.state === 'installed') {
          if (navigator.serviceWorker.controller) {
            // New version available
            this.onUpdateAvailable?.(newWorker);
          } else {
            // First install
            this.onFirstInstall?.();
          }
        }
      });
    });
  }

  /**
   * Force update check
   */
  async checkForUpdates() {
    if (this.registration) {
      await this.registration.update();
    }
  }

  /**
   * Skip waiting and activate new SW
   */
  async skipWaiting() {
    if (this.registration?.waiting) {
      this.registration.waiting.postMessage({ type: 'SKIP_WAITING' });
    }
  }

  /**
   * Unregister Service Worker
   */
  async unregister() {
    if (this.registration) {
      const success = await this.registration.unregister();
      console.log('SW unregistered:', success);
      return success;
    }
    return false;
  }

  /**
   * Check if SW is controlling page
   */
  get isControlling() {
    return !!navigator.serviceWorker.controller;
  }
}

// =============================================================================
// 2. Service Worker Script (sw.js content)
// =============================================================================

/**
 * Service Worker implementation example
 * This would be in a separate sw.js file
 */
const swScript = `
const CACHE_NAME = 'app-v1';
const STATIC_CACHE = 'static-v1';
const DYNAMIC_CACHE = 'dynamic-v1';

// Assets to cache on install
const STATIC_ASSETS = [
    '/',
    '/index.html',
    '/styles.css',
    '/app.js',
    '/offline.html',
    '/icons/icon-192.png'
];

// Install event
self.addEventListener('install', event => {
    console.log('[SW] Installing...');
    
    event.waitUntil(
        caches.open(STATIC_CACHE)
            .then(cache => cache.addAll(STATIC_ASSETS))
            .then(() => self.skipWaiting())
    );
});

// Activate event
self.addEventListener('activate', event => {
    console.log('[SW] Activating...');
    
    event.waitUntil(
        Promise.all([
            // Clean old caches
            caches.keys().then(keys => {
                return Promise.all(
                    keys
                        .filter(key => !key.includes('v1'))
                        .map(key => caches.delete(key))
                );
            }),
            // Take control immediately
            self.clients.claim()
        ])
    );
});

// Fetch event with routing
self.addEventListener('fetch', event => {
    const { request } = event;
    const url = new URL(request.url);
    
    // Skip non-GET requests
    if (request.method !== 'GET') return;
    
    // Skip chrome-extension and other schemes
    if (!url.protocol.startsWith('http')) return;
    
    // Route based on request type
    if (request.destination === 'image') {
        event.respondWith(cacheFirst(request));
    } else if (url.pathname.startsWith('/api/')) {
        event.respondWith(networkFirst(request));
    } else {
        event.respondWith(staleWhileRevalidate(request));
    }
});

// Cache first strategy
async function cacheFirst(request) {
    const cached = await caches.match(request);
    if (cached) return cached;
    
    try {
        const response = await fetch(request);
        const cache = await caches.open(DYNAMIC_CACHE);
        cache.put(request, response.clone());
        return response;
    } catch (error) {
        return caches.match('/offline.html');
    }
}

// Network first strategy
async function networkFirst(request) {
    try {
        const response = await fetch(request);
        const cache = await caches.open(DYNAMIC_CACHE);
        cache.put(request, response.clone());
        return response;
    } catch (error) {
        return caches.match(request);
    }
}

// Stale while revalidate
async function staleWhileRevalidate(request) {
    const cache = await caches.open(DYNAMIC_CACHE);
    const cached = await cache.match(request);
    
    const fetchPromise = fetch(request).then(response => {
        cache.put(request, response.clone());
        return response;
    }).catch(() => cached);
    
    return cached || fetchPromise;
}

// Handle messages from main thread
self.addEventListener('message', event => {
    if (event.data?.type === 'SKIP_WAITING') {
        self.skipWaiting();
    }
});
`;

// =============================================================================
// 3. Caching Strategies
// =============================================================================

/**
 * Caching utility class for main thread
 */
class CacheManager {
  constructor(cacheName = 'app-cache') {
    this.cacheName = cacheName;
  }

  /**
   * Add items to cache
   */
  async addAll(urls) {
    const cache = await caches.open(this.cacheName);
    return cache.addAll(urls);
  }

  /**
   * Add single item to cache
   */
  async add(url) {
    const cache = await caches.open(this.cacheName);
    return cache.add(url);
  }

  /**
   * Get from cache
   */
  async get(request) {
    return caches.match(request);
  }

  /**
   * Put response in cache
   */
  async put(request, response) {
    const cache = await caches.open(this.cacheName);
    return cache.put(request, response);
  }

  /**
   * Delete from cache
   */
  async delete(request) {
    const cache = await caches.open(this.cacheName);
    return cache.delete(request);
  }

  /**
   * Clear entire cache
   */
  async clear() {
    return caches.delete(this.cacheName);
  }

  /**
   * Get all cached URLs
   */
  async keys() {
    const cache = await caches.open(this.cacheName);
    const requests = await cache.keys();
    return requests.map((req) => req.url);
  }

  /**
   * Get cache storage estimate
   */
  static async getStorageEstimate() {
    if ('storage' in navigator && 'estimate' in navigator.storage) {
      return navigator.storage.estimate();
    }
    return null;
  }
}

// =============================================================================
// 4. Offline Detection and Handling
// =============================================================================

/**
 * Offline/Online status manager
 */
class NetworkStatus {
  constructor() {
    this.listeners = new Set();
    this.setupListeners();
  }

  setupListeners() {
    window.addEventListener('online', () => this.notify(true));
    window.addEventListener('offline', () => this.notify(false));
  }

  get isOnline() {
    return navigator.onLine;
  }

  notify(online) {
    this.listeners.forEach((callback) => callback(online));
  }

  onChange(callback) {
    this.listeners.add(callback);
    return () => this.listeners.delete(callback);
  }

  /**
   * Wait for online status
   */
  async waitForOnline(timeout = 30000) {
    if (this.isOnline) return true;

    return new Promise((resolve) => {
      const timer = setTimeout(() => {
        window.removeEventListener('online', handler);
        resolve(false);
      }, timeout);

      const handler = () => {
        clearTimeout(timer);
        resolve(true);
      };

      window.addEventListener('online', handler, { once: true });
    });
  }
}

/**
 * Offline-first request handler
 */
class OfflineRequest {
  constructor(dbName = 'offline-requests') {
    this.dbName = dbName;
    this.networkStatus = new NetworkStatus();
  }

  /**
   * Queue request for later
   */
  async queueRequest(url, options) {
    const request = {
      id: Date.now().toString(36) + Math.random().toString(36).slice(2),
      url,
      options,
      timestamp: Date.now(),
    };

    // Store in IndexedDB
    const db = await this.openDB();
    const tx = db.transaction('requests', 'readwrite');
    await tx.objectStore('requests').add(request);

    console.log('Request queued:', request.id);
    return request.id;
  }

  /**
   * Process queued requests when online
   */
  async processQueue() {
    if (!this.networkStatus.isOnline) {
      console.log('Still offline, skipping queue processing');
      return [];
    }

    const db = await this.openDB();
    const tx = db.transaction('requests', 'readonly');
    const requests = await this.promisify(tx.objectStore('requests').getAll());

    const results = [];

    for (const request of requests) {
      try {
        const response = await fetch(request.url, request.options);
        results.push({ id: request.id, success: true, response });

        // Remove from queue
        const deleteTx = db.transaction('requests', 'readwrite');
        await this.promisify(
          deleteTx.objectStore('requests').delete(request.id)
        );
      } catch (error) {
        results.push({ id: request.id, success: false, error });
      }
    }

    return results;
  }

  async openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains('requests')) {
          db.createObjectStore('requests', { keyPath: 'id' });
        }
      };
    });
  }

  promisify(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

// =============================================================================
// 5. PWA Install Prompt
// =============================================================================

/**
 * PWA installation manager
 */
class PWAInstaller {
  constructor() {
    this.deferredPrompt = null;
    this.isInstalled = false;
    this.listeners = {
      canInstall: new Set(),
      installed: new Set(),
    };

    this.setupListeners();
  }

  setupListeners() {
    // Capture install prompt
    window.addEventListener('beforeinstallprompt', (event) => {
      event.preventDefault();
      this.deferredPrompt = event;
      this.notifyCanInstall();
    });

    // Detect installation
    window.addEventListener('appinstalled', () => {
      this.isInstalled = true;
      this.deferredPrompt = null;
      this.notifyInstalled();
    });

    // Check if already installed (standalone mode)
    if (window.matchMedia('(display-mode: standalone)').matches) {
      this.isInstalled = true;
    }
  }

  /**
   * Check if install is available
   */
  get canInstall() {
    return !!this.deferredPrompt;
  }

  /**
   * Prompt user to install
   */
  async promptInstall() {
    if (!this.deferredPrompt) {
      throw new Error('Install prompt not available');
    }

    this.deferredPrompt.prompt();
    const { outcome } = await this.deferredPrompt.userChoice;

    this.deferredPrompt = null;
    return outcome; // 'accepted' or 'dismissed'
  }

  onCanInstall(callback) {
    this.listeners.canInstall.add(callback);
    if (this.canInstall) callback();
    return () => this.listeners.canInstall.delete(callback);
  }

  onInstalled(callback) {
    this.listeners.installed.add(callback);
    if (this.isInstalled) callback();
    return () => this.listeners.installed.delete(callback);
  }

  notifyCanInstall() {
    this.listeners.canInstall.forEach((cb) => cb());
  }

  notifyInstalled() {
    this.listeners.installed.forEach((cb) => cb());
  }
}

// =============================================================================
// 6. Push Notifications
// =============================================================================

/**
 * Push notification manager
 */
class PushManager {
  constructor(vapidPublicKey, subscribeEndpoint) {
    this.vapidPublicKey = vapidPublicKey;
    this.subscribeEndpoint = subscribeEndpoint;
  }

  /**
   * Check if push is supported
   */
  get isSupported() {
    return 'PushManager' in window && 'Notification' in window;
  }

  /**
   * Get current permission status
   */
  get permission() {
    return Notification.permission;
  }

  /**
   * Request notification permission
   */
  async requestPermission() {
    if (!this.isSupported) {
      throw new Error('Push notifications not supported');
    }

    const permission = await Notification.requestPermission();
    return permission;
  }

  /**
   * Subscribe to push notifications
   */
  async subscribe() {
    if (this.permission !== 'granted') {
      const permission = await this.requestPermission();
      if (permission !== 'granted') {
        throw new Error('Notification permission denied');
      }
    }

    const registration = await navigator.serviceWorker.ready;

    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey),
    });

    // Send subscription to server
    await fetch(this.subscribeEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(subscription),
    });

    return subscription;
  }

  /**
   * Unsubscribe from push notifications
   */
  async unsubscribe() {
    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.getSubscription();

    if (subscription) {
      await subscription.unsubscribe();
      return true;
    }

    return false;
  }

  /**
   * Check current subscription
   */
  async getSubscription() {
    const registration = await navigator.serviceWorker.ready;
    return registration.pushManager.getSubscription();
  }

  /**
   * Convert VAPID key for use with pushManager
   */
  urlBase64ToUint8Array(base64String) {
    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;
  }
}

// =============================================================================
// 7. Background Sync
// =============================================================================

/**
 * Background sync manager
 */
class BackgroundSyncManager {
  constructor() {
    this.pendingData = [];
  }

  /**
   * Check if background sync is supported
   */
  get isSupported() {
    return 'serviceWorker' in navigator && 'SyncManager' in window;
  }

  /**
   * Register a sync event
   */
  async registerSync(tag) {
    if (!this.isSupported) {
      throw new Error('Background sync not supported');
    }

    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register(tag);
    console.log('Sync registered:', tag);
  }

  /**
   * Queue data and register sync
   */
  async queueSync(tag, data) {
    // Store data in IndexedDB
    const db = await this.openDB();
    const tx = db.transaction('sync-queue', 'readwrite');
    await this.promisify(
      tx.objectStore('sync-queue').add({
        tag,
        data,
        timestamp: Date.now(),
      })
    );

    // Register sync event
    await this.registerSync(tag);
  }

  async openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('sync-db', 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains('sync-queue')) {
          db.createObjectStore('sync-queue', {
            keyPath: 'id',
            autoIncrement: true,
          });
        }
      };
    });
  }

  promisify(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

// =============================================================================
// 8. Complete PWA Example
// =============================================================================

/**
 * Full PWA implementation
 */
class PWAApp {
  constructor(options = {}) {
    this.options = {
      swPath: '/sw.js',
      vapidKey: null,
      subscribeEndpoint: '/api/push/subscribe',
      ...options,
    };

    this.sw = new ServiceWorkerManager(this.options.swPath);
    this.cache = new CacheManager();
    this.network = new NetworkStatus();
    this.installer = new PWAInstaller();
    this.offlineRequest = new OfflineRequest();

    if (this.options.vapidKey) {
      this.push = new PushManager(
        this.options.vapidKey,
        this.options.subscribeEndpoint
      );
    }

    this.sync = new BackgroundSyncManager();
  }

  /**
   * Initialize the PWA
   */
  async init() {
    // Register Service Worker
    await this.sw.register();

    // Set up update notification
    this.sw.onUpdateAvailable = () => {
      this.showUpdateNotification();
    };

    // Set up offline handling
    this.network.onChange((online) => {
      if (online) {
        this.onOnline();
      } else {
        this.onOffline();
      }
    });

    // Set up install button
    this.installer.onCanInstall(() => {
      this.showInstallButton();
    });

    console.log('PWA initialized');
  }

  showUpdateNotification() {
    // Implement UI notification
    console.log('New version available');
  }

  showInstallButton() {
    // Implement install button UI
    console.log('Install available');
  }

  onOnline() {
    console.log('Back online');
    this.offlineRequest.processQueue();
  }

  onOffline() {
    console.log('Gone offline');
  }

  /**
   * Make offline-capable fetch
   */
  async fetch(url, options) {
    try {
      return await fetch(url, options);
    } catch (error) {
      if (!this.network.isOnline) {
        await this.offlineRequest.queueRequest(url, options);
        throw new Error('Request queued for offline sync');
      }
      throw error;
    }
  }
}

// =============================================================================
// Export
// =============================================================================

if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    ServiceWorkerManager,
    CacheManager,
    NetworkStatus,
    OfflineRequest,
    PWAInstaller,
    PushManager,
    BackgroundSyncManager,
    PWAApp,
    swScript,
  };
}
Examples - JavaScript Tutorial | DeepML