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 CACHE_NAME = "stripstream-cache-v3";
|
||||||
const IMAGES_CACHE_NAME = "stripstream-images-v1";
|
const IMAGES_CACHE_NAME = "stripstream-images-v3";
|
||||||
const OFFLINE_PAGE = "/offline.html";
|
const OFFLINE_PAGE = "/offline.html";
|
||||||
|
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
@@ -10,6 +10,16 @@ const STATIC_ASSETS = [
|
|||||||
"/images/icons/icon-512x512.png",
|
"/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
|
// Installation du service worker
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
event.waitUntil(
|
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
|
// Activation et nettoyage des anciens caches
|
||||||
self.addEventListener("activate", (event) => {
|
self.addEventListener("activate", (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
|
Promise.all([
|
||||||
|
// Supprimer les anciens caches
|
||||||
caches.keys().then((cacheNames) => {
|
caches.keys().then((cacheNames) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
cacheNames
|
cacheNames
|
||||||
.filter((name) => name !== CACHE_NAME && name !== IMAGES_CACHE_NAME)
|
.filter((name) => name !== CACHE_NAME && name !== IMAGES_CACHE_NAME)
|
||||||
.map((name) => caches.delete(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
|
// Stratégie Cache-First pour les images
|
||||||
const imageCacheStrategy = async (request) => {
|
const imageCacheStrategy = async (request) => {
|
||||||
const cache = await caches.open(IMAGES_CACHE_NAME);
|
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
|
// Pour les ressources statiques de Next.js et les autres requêtes : Network-First
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
fetch(event.request)
|
fetch(event.request)
|
||||||
.then((response) => {
|
.then(async (response) => {
|
||||||
// Mettre en cache les ressources statiques de Next.js et les pages
|
// Mettre en cache les ressources statiques de Next.js et les pages
|
||||||
if (
|
if (
|
||||||
response.ok &&
|
response.ok &&
|
||||||
(isNextStaticResource(event.request.url) || event.request.mode === "navigate")
|
(isNextStaticResource(event.request.url) || event.request.mode === "navigate")
|
||||||
) {
|
) {
|
||||||
const responseToCache = response.clone();
|
const responseToCache = response.clone();
|
||||||
caches.open(CACHE_NAME).then((cache) => {
|
const cache = await caches.open(CACHE_NAME);
|
||||||
cache.put(event.request, responseToCache);
|
|
||||||
});
|
// 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;
|
return response;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
const [isLoadingSwEntries, setIsLoadingSwEntries] = useState(false);
|
const [isLoadingSwEntries, setIsLoadingSwEntries] = useState(false);
|
||||||
const [showSwEntries, setShowSwEntries] = useState(false);
|
const [showSwEntries, setShowSwEntries] = useState(false);
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||||
|
const [expandedVersions, setExpandedVersions] = useState<Record<string, boolean>>({});
|
||||||
const [ttlConfig, setTTLConfig] = useState<TTLConfigData>(
|
const [ttlConfig, setTTLConfig] = useState<TTLConfigData>(
|
||||||
initialTTLConfig || {
|
initialTTLConfig || {
|
||||||
defaultTTL: 5,
|
defaultTTL: 5,
|
||||||
@@ -197,7 +198,7 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
setShowSwEntries(!showSwEntries);
|
setShowSwEntries(!showSwEntries);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFirstPathSegment = (url: string): string => {
|
const getPathGroup = (url: string): string => {
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const path = urlObj.pathname;
|
const path = urlObj.pathname;
|
||||||
@@ -205,16 +206,56 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
|
|
||||||
if (segments.length === 0) return '/';
|
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]}`;
|
return `/${segments[0]}`;
|
||||||
} catch {
|
} catch {
|
||||||
return 'Autres';
|
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 groupEntriesByPath = (entries: ServiceWorkerCacheEntry[]) => {
|
||||||
const grouped = entries.reduce(
|
const grouped = entries.reduce(
|
||||||
(acc, entry) => {
|
(acc, entry) => {
|
||||||
const pathGroup = getFirstPathSegment(entry.url);
|
const pathGroup = getPathGroup(entry.url);
|
||||||
if (!acc[pathGroup]) {
|
if (!acc[pathGroup]) {
|
||||||
acc[pathGroup] = [];
|
acc[pathGroup] = [];
|
||||||
}
|
}
|
||||||
@@ -229,7 +270,19 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
grouped[key].sort((a, b) => b.size - a.size);
|
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 => {
|
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(() => {
|
useEffect(() => {
|
||||||
fetchCacheSize();
|
fetchCacheSize();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -291,6 +351,13 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
if ("serviceWorker" in navigator && "caches" in window) {
|
if ("serviceWorker" in navigator && "caches" in window) {
|
||||||
const cacheNames = await caches.keys();
|
const cacheNames = await caches.keys();
|
||||||
await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)));
|
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({
|
toast({
|
||||||
title: t("settings.cache.title"),
|
title: t("settings.cache.title"),
|
||||||
description: t("settings.cache.messages.serviceWorkerCleared"),
|
description: t("settings.cache.messages.serviceWorkerCleared"),
|
||||||
@@ -304,6 +371,11 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
if (showSwEntries) {
|
if (showSwEntries) {
|
||||||
await fetchSwCacheEntries();
|
await fetchSwCacheEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recharger la page après 1 seconde pour réenregistrer le SW
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la suppression des caches:", error);
|
console.error("Erreur lors de la suppression des caches:", error);
|
||||||
@@ -546,8 +618,17 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
</button>
|
</button>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="space-y-1 pl-2">
|
<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 items-start justify-between gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-mono text-xs truncate text-muted-foreground" title={entry.url}>
|
<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>
|
</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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user