javascript
examples
examples.js⚡javascript
/**
* 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,
};
}