feat: update service worker caching strategy to handle versioning and duplicate entries, enhance CacheSettings component with version grouping and improved UI for cache management
This commit is contained in:
96
public/sw.js
96
public/sw.js
@@ -1,5 +1,5 @@
|
||||
const CACHE_NAME = "stripstream-cache-v1";
|
||||
const IMAGES_CACHE_NAME = "stripstream-images-v1";
|
||||
const CACHE_NAME = "stripstream-cache-v3";
|
||||
const IMAGES_CACHE_NAME = "stripstream-images-v3";
|
||||
const OFFLINE_PAGE = "/offline.html";
|
||||
|
||||
const STATIC_ASSETS = [
|
||||
@@ -10,6 +10,16 @@ const STATIC_ASSETS = [
|
||||
"/images/icons/icon-512x512.png",
|
||||
];
|
||||
|
||||
// Fonction pour obtenir l'URL de base sans les query params
|
||||
const getBaseUrl = (url) => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.origin + urlObj.pathname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
// Installation du service worker
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
@@ -20,16 +30,58 @@ self.addEventListener("install", (event) => {
|
||||
);
|
||||
});
|
||||
|
||||
// Fonction pour nettoyer les doublons dans un cache
|
||||
const cleanDuplicatesInCache = async (cacheName) => {
|
||||
const cache = await caches.open(cacheName);
|
||||
const keys = await cache.keys();
|
||||
|
||||
// Grouper par URL de base
|
||||
const grouped = {};
|
||||
for (const key of keys) {
|
||||
const baseUrl = getBaseUrl(key.url);
|
||||
if (!grouped[baseUrl]) {
|
||||
grouped[baseUrl] = [];
|
||||
}
|
||||
grouped[baseUrl].push(key);
|
||||
}
|
||||
|
||||
// Pour chaque groupe, garder seulement la version la plus récente
|
||||
const deletePromises = [];
|
||||
for (const baseUrl in grouped) {
|
||||
const versions = grouped[baseUrl];
|
||||
if (versions.length > 1) {
|
||||
// Trier par query params (version) décroissant
|
||||
versions.sort((a, b) => {
|
||||
const aVersion = new URL(a.url).searchParams.get('v') || '0';
|
||||
const bVersion = new URL(b.url).searchParams.get('v') || '0';
|
||||
return Number(bVersion) - Number(aVersion);
|
||||
});
|
||||
// Supprimer toutes sauf la première (plus récente)
|
||||
for (let i = 1; i < versions.length; i++) {
|
||||
deletePromises.push(cache.delete(versions[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
};
|
||||
|
||||
// Activation et nettoyage des anciens caches
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
// Supprimer les anciens caches
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name !== CACHE_NAME && name !== IMAGES_CACHE_NAME)
|
||||
.map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
}),
|
||||
// Nettoyer les doublons dans les caches actuels
|
||||
cleanDuplicatesInCache(CACHE_NAME),
|
||||
cleanDuplicatesInCache(IMAGES_CACHE_NAME),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -57,6 +109,22 @@ const isImageResource = (url) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Fonction pour nettoyer les anciennes versions d'un fichier
|
||||
const cleanOldVersions = async (cacheName, request) => {
|
||||
const cache = await caches.open(cacheName);
|
||||
const baseUrl = getBaseUrl(request.url);
|
||||
|
||||
// Récupérer toutes les requêtes en cache
|
||||
const keys = await cache.keys();
|
||||
|
||||
// Supprimer toutes les requêtes qui ont la même URL de base
|
||||
const deletePromises = keys
|
||||
.filter(key => getBaseUrl(key.url) === baseUrl)
|
||||
.map(key => cache.delete(key));
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
};
|
||||
|
||||
// Stratégie Cache-First pour les images
|
||||
const imageCacheStrategy = async (request) => {
|
||||
const cache = await caches.open(IMAGES_CACHE_NAME);
|
||||
@@ -113,16 +181,30 @@ self.addEventListener("fetch", (event) => {
|
||||
// Pour les ressources statiques de Next.js et les autres requêtes : Network-First
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
// Mettre en cache les ressources statiques de Next.js et les pages
|
||||
if (
|
||||
response.ok &&
|
||||
(isNextStaticResource(event.request.url) || event.request.mode === "navigate")
|
||||
) {
|
||||
const responseToCache = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
|
||||
// Nettoyer les anciennes versions avant de mettre en cache la nouvelle
|
||||
if (isNextStaticResource(event.request.url)) {
|
||||
try {
|
||||
await cleanOldVersions(CACHE_NAME, event.request);
|
||||
} catch (error) {
|
||||
console.warn("Error cleaning old versions:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre en cache la nouvelle version
|
||||
try {
|
||||
await cache.put(event.request, responseToCache);
|
||||
} catch (error) {
|
||||
console.warn("Error caching response:", error);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
})
|
||||
|
||||
@@ -47,6 +47,7 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
const [isLoadingSwEntries, setIsLoadingSwEntries] = useState(false);
|
||||
const [showSwEntries, setShowSwEntries] = useState(false);
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
const [expandedVersions, setExpandedVersions] = useState<Record<string, boolean>>({});
|
||||
const [ttlConfig, setTTLConfig] = useState<TTLConfigData>(
|
||||
initialTTLConfig || {
|
||||
defaultTTL: 5,
|
||||
@@ -197,7 +198,7 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
setShowSwEntries(!showSwEntries);
|
||||
};
|
||||
|
||||
const getFirstPathSegment = (url: string): string => {
|
||||
const getPathGroup = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const path = urlObj.pathname;
|
||||
@@ -205,16 +206,56 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
|
||||
if (segments.length === 0) return '/';
|
||||
|
||||
// Pour /api/komga/images, grouper par type (series/books)
|
||||
if (segments[0] === 'api' && segments[1] === 'komga' && segments[2] === 'images' && segments[3]) {
|
||||
return `/${segments[0]}/${segments[1]}/${segments[2]}/${segments[3]}`;
|
||||
}
|
||||
|
||||
// Pour les autres, garder juste le premier segment
|
||||
return `/${segments[0]}`;
|
||||
} catch {
|
||||
return 'Autres';
|
||||
}
|
||||
};
|
||||
|
||||
const getBaseUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.pathname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
const groupVersions = (entries: ServiceWorkerCacheEntry[]) => {
|
||||
const grouped = entries.reduce(
|
||||
(acc, entry) => {
|
||||
const baseUrl = getBaseUrl(entry.url);
|
||||
if (!acc[baseUrl]) {
|
||||
acc[baseUrl] = [];
|
||||
}
|
||||
acc[baseUrl].push(entry);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ServiceWorkerCacheEntry[]>
|
||||
);
|
||||
|
||||
// Trier par date (le plus récent en premier) basé sur le paramètre v
|
||||
Object.keys(grouped).forEach((key) => {
|
||||
grouped[key].sort((a, b) => {
|
||||
const aVersion = new URL(a.url).searchParams.get('v') || '0';
|
||||
const bVersion = new URL(b.url).searchParams.get('v') || '0';
|
||||
return Number(bVersion) - Number(aVersion);
|
||||
});
|
||||
});
|
||||
|
||||
return grouped;
|
||||
};
|
||||
|
||||
const groupEntriesByPath = (entries: ServiceWorkerCacheEntry[]) => {
|
||||
const grouped = entries.reduce(
|
||||
(acc, entry) => {
|
||||
const pathGroup = getFirstPathSegment(entry.url);
|
||||
const pathGroup = getPathGroup(entry.url);
|
||||
if (!acc[pathGroup]) {
|
||||
acc[pathGroup] = [];
|
||||
}
|
||||
@@ -229,7 +270,19 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
grouped[key].sort((a, b) => b.size - a.size);
|
||||
});
|
||||
|
||||
return grouped;
|
||||
// Trier les groupes par taille totale décroissante
|
||||
const sortedGroups: Record<string, ServiceWorkerCacheEntry[]> = {};
|
||||
Object.entries(grouped)
|
||||
.sort((a, b) => {
|
||||
const aSize = getTotalSizeByType(a[1]);
|
||||
const bSize = getTotalSizeByType(b[1]);
|
||||
return bSize - aSize;
|
||||
})
|
||||
.forEach(([key, value]) => {
|
||||
sortedGroups[key] = value;
|
||||
});
|
||||
|
||||
return sortedGroups;
|
||||
};
|
||||
|
||||
const getTotalSizeByType = (entries: ServiceWorkerCacheEntry[]): number => {
|
||||
@@ -243,6 +296,13 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleVersions = (fileName: string) => {
|
||||
setExpandedVersions((prev) => ({
|
||||
...prev,
|
||||
[fileName]: !prev[fileName],
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCacheSize();
|
||||
}, []);
|
||||
@@ -291,6 +351,13 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
if ("serviceWorker" in navigator && "caches" in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)));
|
||||
|
||||
// Forcer la mise à jour du service worker
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
for (const registration of registrations) {
|
||||
await registration.unregister();
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("settings.cache.title"),
|
||||
description: t("settings.cache.messages.serviceWorkerCleared"),
|
||||
@@ -304,6 +371,11 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
if (showSwEntries) {
|
||||
await fetchSwCacheEntries();
|
||||
}
|
||||
|
||||
// Recharger la page après 1 seconde pour réenregistrer le SW
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la suppression des caches:", error);
|
||||
@@ -546,8 +618,17 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="space-y-1 pl-2">
|
||||
{entries.map((entry, index) => (
|
||||
<div key={index} className="py-1">
|
||||
{(() => {
|
||||
const versionGroups = groupVersions(entries);
|
||||
return Object.entries(versionGroups).map(([baseUrl, versions]) => {
|
||||
const hasMultipleVersions = versions.length > 1;
|
||||
const isVersionExpanded = expandedVersions[baseUrl];
|
||||
const totalSize = versions.reduce((sum, v) => sum + v.size, 0);
|
||||
|
||||
if (!hasMultipleVersions) {
|
||||
const entry = versions[0];
|
||||
return (
|
||||
<div key={baseUrl} className="py-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono text-xs truncate text-muted-foreground" title={entry.url}>
|
||||
@@ -559,11 +640,57 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={baseUrl} className="py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleVersions(baseUrl)}
|
||||
className="w-full flex items-start justify-between gap-2 hover:bg-muted/30 rounded p-1 -m-1 transition-colors"
|
||||
>
|
||||
<div className="flex-1 min-w-0 flex items-center gap-1">
|
||||
{isVersionExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronUp className="h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
<div className="font-mono text-xs truncate text-muted-foreground" title={baseUrl}>
|
||||
{baseUrl}
|
||||
</div>
|
||||
<span className="inline-flex items-center rounded-full bg-orange-500/10 px-1.5 py-0.5 text-xs font-medium text-orange-600 dark:text-orange-400 flex-shrink-0">
|
||||
{versions.length} versions
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground whitespace-nowrap font-medium">
|
||||
{formatBytes(totalSize)}
|
||||
</div>
|
||||
</button>
|
||||
{isVersionExpanded && (
|
||||
<div className="pl-4 mt-1 space-y-1">
|
||||
{versions.map((version, vIdx) => (
|
||||
<div key={vIdx} className="py-0.5 flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono text-xs truncate text-muted-foreground/70" title={version.url}>
|
||||
{new URL(version.url).search || "(no version)"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground/70 whitespace-nowrap">
|
||||
{formatBytes(version.size)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user