Files
Todo/public/sw.js
Paul Bailey 509de75e1a Fix Safari/iPhone performance issues - remove 300ms click delay
- Added touch-action: manipulation CSS to eliminate 300ms tap delay
- Optimized service worker to skip cross-origin requests
- Updated viewport settings to prevent iOS delays
- Added format-detection meta tag
- Enabled Next.js performance optimizations
- Created comprehensive performance fix documentation
2025-12-03 20:54:55 +00:00

283 lines
7.5 KiB
JavaScript

// Service Worker for Ultimate Lists App
// Implements caching strategies for offline functionality
const CACHE_NAME = 'ultimate-lists-v2';
const RUNTIME_CACHE = 'ultimate-lists-runtime-v2';
// Static assets to cache on install
const STATIC_ASSETS = [
'/',
'/offline',
'/auth/login',
'/auth/register',
'/auth/error',
'/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;
}
// Skip cross-origin requests for performance
if (url.origin !== self.location.origin) {
return;
}
// Handle authentication-related requests
if (isAuthRequest(url)) {
event.respondWith(handleAuthRequest(request));
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('.');
}
// Helper: Check if request is authentication-related
function isAuthRequest(url) {
return url.pathname.startsWith('/auth/') ||
url.pathname.startsWith('/api/auth/');
}
// Strategy: Handle authentication requests
async function handleAuthRequest(request) {
const url = new URL(request.url);
// For authentication API calls, always try network first
if (url.pathname.startsWith('/api/auth/')) {
try {
const response = await fetch(request);
return response;
} catch (error) {
console.error('Authentication API request failed (offline):', error);
// Return a JSON error response for API calls
return new Response(
JSON.stringify({
error: 'Authentication requires an internet connection. Please check your connection and try again.'
}),
{
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'application/json' }
}
);
}
}
// For authentication pages, use cache-first with network fallback
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (error) {
console.error('Authentication page request failed:', error);
// Return cached version if available
if (cached) {
return cached;
}
// Otherwise redirect to offline page
return caches.match('/offline');
}
}
// 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) => {
// Handle authentication errors (401, 403)
if (response.status === 401 || response.status === 403) {
// Don't cache authentication errors
return 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);
// Handle authentication errors (401, 403)
if (response.status === 401 || response.status === 403) {
// Don't cache authentication errors
return response;
}
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;
}
}
// Background Sync - sync pending mutations when connection is restored
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-mutations') {
event.waitUntil(syncMutations());
}
});
// Sync mutations from the queue
async function syncMutations() {
try {
// Notify all clients to trigger sync
const clients = await self.clients.matchAll();
clients.forEach((client) => {
client.postMessage({
type: 'SYNC_MUTATIONS',
});
});
} catch (error) {
console.error('Background sync failed:', error);
throw error; // Re-throw to retry later
}
}
// Listen for messages from clients
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'REGISTER_SYNC') {
// Register background sync
if (self.registration.sync) {
self.registration.sync.register('sync-mutations').catch((error) => {
console.error('Failed to register background sync:', error);
});
}
}
});