feat: Set up PWA infrastructure with service worker, offline support, and manifest
- Created service worker (public/sw.js) with caching strategies - Cache-first strategy for static assets - Stale-while-revalidate for API responses and pages - Network-first fallback strategy - Implemented offline fallback page (/offline) - Updated manifest.json with complete PWA metadata - Added service worker registration in app layout - Created ServiceWorkerRegistration component for client-side registration - Generated SVG icons for PWA (192x192 and 512x512) - Added PWA meta tags for iOS Safari support Validates Requirements: 4.1, 4.3, 4.4
This commit is contained in:
164
public/sw.js
Normal file
164
public/sw.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// Service Worker for Ultimate Lists App
|
||||
// Implements caching strategies for offline functionality
|
||||
|
||||
const CACHE_NAME = 'ultimate-lists-v1';
|
||||
const RUNTIME_CACHE = 'ultimate-lists-runtime-v1';
|
||||
|
||||
// Static assets to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/offline',
|
||||
'/manifest.json',
|
||||
'/icon-192.svg',
|
||||
'/icon-512.svg',
|
||||
];
|
||||
|
||||
// Install event - cache static assets
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(STATIC_ASSETS).catch((error) => {
|
||||
console.error('Failed to cache static assets:', error);
|
||||
// Continue even if some assets fail to cache
|
||||
return Promise.resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
// Activate immediately
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name !== CACHE_NAME && name !== RUNTIME_CACHE)
|
||||
.map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
);
|
||||
// Take control of all pages immediately
|
||||
return self.clients.claim();
|
||||
});
|
||||
|
||||
// Fetch event - implement caching strategies
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip chrome extensions and other protocols
|
||||
if (!url.protocol.startsWith('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache-first strategy for static assets
|
||||
if (isStaticAsset(url)) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Stale-while-revalidate for API responses and pages
|
||||
if (isApiRequest(url) || isPageRequest(url)) {
|
||||
event.respondWith(staleWhileRevalidate(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Network-first for everything else
|
||||
event.respondWith(networkFirst(request));
|
||||
});
|
||||
|
||||
// Helper: Check if request is for a static asset
|
||||
function isStaticAsset(url) {
|
||||
const staticExtensions = ['.js', '.css', '.png', '.jpg', '.jpeg', '.svg', '.ico', '.woff', '.woff2'];
|
||||
return staticExtensions.some((ext) => url.pathname.endsWith(ext));
|
||||
}
|
||||
|
||||
// Helper: Check if request is an API request
|
||||
function isApiRequest(url) {
|
||||
return url.pathname.startsWith('/api/');
|
||||
}
|
||||
|
||||
// Helper: Check if request is a page request
|
||||
function isPageRequest(url) {
|
||||
return url.origin === self.location.origin && !url.pathname.includes('.');
|
||||
}
|
||||
|
||||
// Strategy: Cache-first (for static assets)
|
||||
async function cacheFirst(request) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Cache-first fetch failed:', error);
|
||||
return new Response('Network error', { status: 408 });
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy: Stale-while-revalidate (for API and pages)
|
||||
async function staleWhileRevalidate(request) {
|
||||
const cache = await caches.open(RUNTIME_CACHE);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
const fetchPromise = fetch(request).then((response) => {
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
}).catch((error) => {
|
||||
console.error('Stale-while-revalidate fetch failed:', error);
|
||||
// If we have a cached version, return it on network error
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
// Otherwise, return offline page for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
return caches.match('/offline');
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
return cached || fetchPromise;
|
||||
}
|
||||
|
||||
// Strategy: Network-first (fallback)
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(RUNTIME_CACHE);
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
const cache = await caches.open(RUNTIME_CACHE);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Return offline page for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
return caches.match('/offline');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user