feat: update service worker to version 2.4, enhance caching strategies for pages, and add service worker reinstallation functionality in CacheSettings component
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s

This commit is contained in:
Julien Froidefond
2026-01-04 07:39:07 +01:00
parent b8a0b85c54
commit 0d33462349
5 changed files with 294 additions and 126 deletions

View File

@@ -1,11 +1,11 @@
// StripStream Service Worker - Version 2
// Architecture: SWR (Stale-While-Revalidate) for all resources
const VERSION = "v2";
const VERSION = "v2.4";
const STATIC_CACHE = `stripstream-static-${VERSION}`;
const PAGES_CACHE = `stripstream-pages-${VERSION}`; // Navigation + RSC (client-side navigation)
const API_CACHE = `stripstream-api-${VERSION}`;
const IMAGES_CACHE = `stripstream-images-${VERSION}`;
const RSC_CACHE = `stripstream-rsc-${VERSION}`;
const BOOKS_CACHE = "stripstream-books"; // Never version this - managed by DownloadManager
const OFFLINE_PAGE = "/offline.html";
@@ -195,39 +195,50 @@ async function staleWhileRevalidateStrategy(request, cacheName, options = {}) {
}
/**
* Network-First: Try network, fallback to cache
* Navigation SWR: Serve from cache immediately, update in background
* Falls back to offline page if nothing cached
* Used for: Page navigations
*/
async function networkFirstStrategy(request, cacheName) {
async function navigationSWRStrategy(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (error) {
// Network failed - try cache
const cached = await cache.match(request);
if (cached) {
return cached;
}
// Start network request in background
const fetchPromise = fetch(request)
.then(async (response) => {
if (response.ok) {
await cache.put(request, response.clone());
}
return response;
})
.catch(() => null);
// Try to serve root page for SPA client-side routing
const rootPage = await cache.match("/");
if (rootPage) {
return rootPage;
}
// Last resort: offline page
const offlinePage = await cache.match(OFFLINE_PAGE);
if (offlinePage) {
return offlinePage;
}
throw error;
// Return cached version immediately if available
if (cached) {
return cached;
}
// No cache - wait for network
const response = await fetchPromise;
if (response) {
return response;
}
// Network failed and no cache - try fallbacks
// Try to serve root page for SPA client-side routing
const rootPage = await cache.match("/");
if (rootPage) {
return rootPage;
}
// Last resort: offline page (in static cache)
const staticCache = await caches.open(STATIC_CACHE);
const offlinePage = await staticCache.match(OFFLINE_PAGE);
if (offlinePage) {
return offlinePage;
}
throw new Error("Offline and no cached page available");
}
// ============================================================================
@@ -262,7 +273,7 @@ self.addEventListener("activate", (event) => {
(async () => {
// Clean up old caches, but preserve BOOKS_CACHE
const cacheNames = await caches.keys();
const currentCaches = [STATIC_CACHE, API_CACHE, IMAGES_CACHE, RSC_CACHE, BOOKS_CACHE];
const currentCaches = [STATIC_CACHE, PAGES_CACHE, API_CACHE, IMAGES_CACHE, BOOKS_CACHE];
const cachesToDelete = cacheNames.filter(
(name) => name.startsWith("stripstream-") && !currentCaches.includes(name)
@@ -295,20 +306,23 @@ self.addEventListener("message", async (event) => {
switch (type) {
case "GET_CACHE_STATS": {
try {
const [staticSize, apiSize, imagesSize, booksSize] = await Promise.all([
const [staticSize, pagesSize, apiSize, imagesSize, booksSize] = await Promise.all([
getCacheSize(STATIC_CACHE),
getCacheSize(PAGES_CACHE),
getCacheSize(API_CACHE),
getCacheSize(IMAGES_CACHE),
getCacheSize(BOOKS_CACHE),
]);
const staticCache = await caches.open(STATIC_CACHE);
const pagesCache = await caches.open(PAGES_CACHE);
const apiCache = await caches.open(API_CACHE);
const imagesCache = await caches.open(IMAGES_CACHE);
const booksCache = await caches.open(BOOKS_CACHE);
const [staticKeys, apiKeys, imagesKeys, booksKeys] = await Promise.all([
const [staticKeys, pagesKeys, apiKeys, imagesKeys, booksKeys] = await Promise.all([
staticCache.keys(),
pagesCache.keys(),
apiCache.keys(),
imagesCache.keys(),
booksCache.keys(),
@@ -318,10 +332,11 @@ self.addEventListener("message", async (event) => {
type: "CACHE_STATS",
payload: {
static: { size: staticSize, entries: staticKeys.length },
pages: { size: pagesSize, entries: pagesKeys.length },
api: { size: apiSize, entries: apiKeys.length },
images: { size: imagesSize, entries: imagesKeys.length },
books: { size: booksSize, entries: booksKeys.length },
total: staticSize + apiSize + imagesSize + booksSize,
total: staticSize + pagesSize + apiSize + imagesSize + booksSize,
},
});
} catch (error) {
@@ -341,15 +356,15 @@ self.addEventListener("message", async (event) => {
if (cacheType === "all" || cacheType === "static") {
cachesToClear.push(STATIC_CACHE);
}
if (cacheType === "all" || cacheType === "pages") {
cachesToClear.push(PAGES_CACHE);
}
if (cacheType === "all" || cacheType === "api") {
cachesToClear.push(API_CACHE);
}
if (cacheType === "all" || cacheType === "images") {
cachesToClear.push(IMAGES_CACHE);
}
if (cacheType === "all" || cacheType === "rsc") {
cachesToClear.push(RSC_CACHE);
}
// Note: BOOKS_CACHE is not cleared by default, only explicitly
await Promise.all(
@@ -395,6 +410,9 @@ self.addEventListener("message", async (event) => {
case "static":
cacheName = STATIC_CACHE;
break;
case "pages":
cacheName = PAGES_CACHE;
break;
case "api":
cacheName = API_CACHE;
break;
@@ -477,10 +495,10 @@ self.addEventListener("fetch", (event) => {
return;
}
// Route 2: Next.js RSC payloads → Stale-While-Revalidate
// Route 2: Next.js RSC payloads (client-side navigation) → SWR in PAGES_CACHE
if (isNextRSCRequest(request)) {
event.respondWith(
staleWhileRevalidateStrategy(request, RSC_CACHE, {
staleWhileRevalidateStrategy(request, PAGES_CACHE, {
notifyOnChange: false,
})
);
@@ -515,9 +533,9 @@ self.addEventListener("fetch", (event) => {
return;
}
// Route 6: Navigation → Network-First with SPA fallback
// Route 6: Navigation → SWR (cache first, revalidate in background)
if (request.mode === "navigate") {
event.respondWith(networkFirstStrategy(request, STATIC_CACHE));
event.respondWith(navigationSWRStrategy(request, PAGES_CACHE));
return;
}