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:
- 💻 Practice with the PWA exercise above
- 🏗️ Build a real PWA with all the features you’ve learned
- 📚 Move on to our next tutorial: “WebSockets with TypeScript: Real-Time Communication”
- 🌟 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! 🎉🔌✨