Alex Chang
Feb 2026
3 tabs
// service-worker.js
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/icon-192.png',
'/icon-512.png',
];
// Install event - cache resources
self.addEventListener('install', (event) => {
console.log('Service Worker installing');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Caching app shell');
return cache.addAll(urlsToCache);
})
.then(() => self.skipWaiting()) // Activate immediately
);
});
// Activate event - clean old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker activating');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim()) // Take control of all pages
);
});
// Fetch event - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Cache hit - return response
if (response) {
return response;
}
// Clone the request
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then((response) => {
// Check if valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
.catch(() => {
// Return offline page if available
return caches.match('/offline.html');
})
);
});
// Background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
async function syncMessages() {
const db = await openDB();
const messages = await db.getAll('outbox');
for (const message of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message),
});
await db.delete('outbox', message.id);
} catch (error) {
console.error('Sync failed:', error);
}
}
}
// Push notifications
self.addEventListener('push', (event) => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icon-192.png',
badge: '/badge-72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: data.id,
},
actions: [
{ action: 'explore', title: 'View' },
{ action: 'close', title: 'Close' },
],
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/notifications')
);
}
});
// Caching strategies
// 1. Cache First (good for static assets)
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((response) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
}
});
// 2. Network First (good for API calls)
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match(event.request))
);
}
});
// 3. Stale While Revalidate (good for frequently updated content)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((response) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('SW registered:', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available
if (confirm('New version available! Reload?')) {
window.location.reload();
}
}
});
});
} catch (error) {
console.error('SW registration failed:', error);
}
});
}
// Install prompt
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Show install button
const installBtn = document.getElementById('install-btn');
installBtn.style.display = 'block';
installBtn.addEventListener('click', async () => {
installBtn.style.display = 'none';
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User response: ${outcome}`);
deferredPrompt = null;
});
});
// Push notifications
async function subscribeToPushNotifications() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
});
await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription),
});
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}
// Background sync
async function sendMessage(message) {
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
// Save to IndexedDB
await saveToOutbox(message);
// Register sync
await registration.sync.register('sync-messages');
} else {
// Fallback: send immediately
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message),
});
}
}
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A comprehensive PWA example",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4285f4",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"categories": ["productivity", "utilities"],
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png"
},
{
"src": "/screenshots/mobile.png",
"sizes": "750x1334",
"type": "image/png"
}
],
"shortcuts": [
{
"name": "New Message",
"short_name": "New",
"description": "Create a new message",
"url": "/new",
"icons": [{ "src": "/icons/new.png", "sizes": "192x192" }]
}
]
}
3 files · javascript, json
Explain with highlit
Progressive Web Apps (PWAs) combine web and native app features. I register service workers to intercept network requests and enable offline functionality. The service worker lifecycle includes install, activate, and fetch events. Cache API stores assets for offline access with different caching strategies. The workbox library simplifies service worker development with precaching and runtime caching. The web app manifest (manifest.json) defines app metadata, icons, and display mode. Install prompts let users add PWAs to home screens. Background sync queues requests when offline. Push notifications engage users. PWAs provide app-like experiences with web technology.
Related snips
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
javascript
promises
async-await
by Alex Chang
1 tab
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"
export default class extends Controller {
connect() {
// Global shortcuts
Keyboard shortcuts with Stimulus and Mousetrap
stimulus
javascript
ux
by Jordan Lee
2 tabs
javascript
// Get canvas and context
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 1. Basic shapes
// Rectangle (filled)
Canvas API for graphics and animations
canvas
javascript
graphics
by Alex Chang
1 tab
javascript
// Basic event listener
const button = document.getElementById('myButton');
button.addEventListener('click', function(event) {
console.log('Button clicked!');
console.log('Event type:', event.type);
Event handling and event delegation patterns in JavaScript
javascript
events
event-delegation
by Alex Chang
1 tab
javascript
// Basic GET request
fetch('https://api.example.com/users')
.then(response => {
console.log('Status:', response.status);
console.log('OK:', response.ok);
return response.json();
Fetch API for HTTP requests and AJAX communication
javascript
fetch
api
by Alex Chang
1 tab
erb
<turbo-stream action="set_title">
<template><%= "Inbox (#{@unread_count})" %></template>
</turbo-stream>
Turbo Streams: update document title with a custom action
rails
hotwire
turbo
by Henry Kim
2 tabs
Share this code
Here's the card — post it anywhere.