/**
* STP Ecommerce — Service Worker (Phase 4 PWA)
*
* Strategy:
* - App shell (CSS/JS/fonts): Cache First
* - Pages: Network First with offline fallback
* - Images: Cache First with expiration
* - API calls: Network Only (never cache)
*/
const CACHE_VERSION = 'v1';
const APP_SHELL_CACHE = `stp-shell-${CACHE_VERSION}`;
const PAGES_CACHE = `stp-pages-${CACHE_VERSION}`;
const IMAGES_CACHE = `stp-images-${CACHE_VERSION}`;
const OFFLINE_PAGE = '/offline';
const APP_SHELL_ASSETS = [
'/',
OFFLINE_PAGE,
'/build/assets/app.css',
'/build/assets/app.js',
'/images/logo.svg',
];
// ── Install ────────────────────────────────────────────────────────
self.addEventListener('install', event => {
event.waitUntil(
caches.open(APP_SHELL_CACHE)
.then(cache => cache.addAll(APP_SHELL_ASSETS))
.then(() => self.skipWaiting())
);
});
// ── Activate (clean old caches) ────────────────────────────────────
self.addEventListener('activate', event => {
const validCaches = [APP_SHELL_CACHE, PAGES_CACHE, IMAGES_CACHE];
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => !validCaches.includes(k)).map(k => caches.delete(k)))
).then(() => self.clients.claim())
);
});
// ── Fetch ──────────────────────────────────────────────────────────
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET and API calls
if (request.method !== 'GET') return;
if (url.pathname.startsWith('/api/')) return;
if (url.pathname.startsWith('/admin')) return;
if (url.pathname.startsWith('/vendor')) return;
if (url.pathname.startsWith('/livewire/')) return;
// Images → Cache First
if (request.destination === 'image') {
event.respondWith(cacheFirst(request, IMAGES_CACHE, 60 * 60 * 24 * 30)); // 30 days
return;
}
// Static assets (shell) → Cache First
if (['style', 'script', 'font'].includes(request.destination)) {
event.respondWith(cacheFirst(request, APP_SHELL_CACHE));
return;
}
// HTML pages → Network First with offline fallback
if (request.headers.get('Accept')?.includes('text/html')) {
event.respondWith(networkFirst(request));
return;
}
});
async function cacheFirst(request, cacheName) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(cacheName);
cache.put(request, response.clone());
}
return response;
}
async function networkFirst(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(PAGES_CACHE);
cache.put(request, response.clone());
}
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
return caches.match(OFFLINE_PAGE);
}
}