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
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
This commit is contained in:
@@ -8,11 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Database,
|
||||
@@ -27,10 +23,13 @@ import {
|
||||
Loader2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
LayoutGrid,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
|
||||
interface CacheStats {
|
||||
static: { size: number; entries: number };
|
||||
pages: { size: number; entries: number };
|
||||
api: { size: number; entries: number };
|
||||
images: { size: number; entries: number };
|
||||
books: { size: number; entries: number };
|
||||
@@ -42,7 +41,7 @@ interface CacheEntry {
|
||||
size: number;
|
||||
}
|
||||
|
||||
type CacheType = "static" | "api" | "images" | "books";
|
||||
type CacheType = "static" | "pages" | "api" | "images" | "books";
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
@@ -195,12 +194,20 @@ function CacheItem({
|
||||
export function CacheSettings() {
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { isSupported, isReady, version, getCacheStats, getCacheEntries, clearCache } =
|
||||
useServiceWorker();
|
||||
const {
|
||||
isSupported,
|
||||
isReady,
|
||||
version,
|
||||
getCacheStats,
|
||||
getCacheEntries,
|
||||
clearCache,
|
||||
reinstallServiceWorker,
|
||||
} = useServiceWorker();
|
||||
|
||||
const [stats, setStats] = useState<CacheStats | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [clearingCache, setClearingCache] = useState<string | null>(null);
|
||||
const [isReinstalling, setIsReinstalling] = useState(false);
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
if (!isReady) return;
|
||||
@@ -218,7 +225,7 @@ export function CacheSettings() {
|
||||
loadStats();
|
||||
}, [loadStats]);
|
||||
|
||||
const handleClearCache = async (cacheType: "all" | "static" | "api" | "images") => {
|
||||
const handleClearCache = async (cacheType: "all" | "static" | "pages" | "api" | "images") => {
|
||||
setClearingCache(cacheType);
|
||||
try {
|
||||
const success = await clearCache(cacheType);
|
||||
@@ -247,6 +254,28 @@ export function CacheSettings() {
|
||||
[getCacheEntries]
|
||||
);
|
||||
|
||||
const handleReinstall = async () => {
|
||||
setIsReinstalling(true);
|
||||
try {
|
||||
const success = await reinstallServiceWorker();
|
||||
if (!success) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("settings.error.title"),
|
||||
description: t("settings.cache.reinstallError"),
|
||||
});
|
||||
}
|
||||
// If success, the page will reload automatically
|
||||
} catch {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("settings.error.title"),
|
||||
description: t("settings.cache.reinstallError"),
|
||||
});
|
||||
setIsReinstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Calculer le pourcentage du cache utilisé (basé sur 100MB limite images)
|
||||
const maxCacheSize = 100 * 1024 * 1024; // 100MB
|
||||
const usagePercent = stats ? Math.min((stats.images.size / maxCacheSize) * 100, 100) : 0;
|
||||
@@ -328,6 +357,17 @@ export function CacheSettings() {
|
||||
isClearing={clearingCache === "static"}
|
||||
onLoadEntries={handleLoadEntries}
|
||||
/>
|
||||
<CacheItem
|
||||
icon={<LayoutGrid className="h-4 w-4" />}
|
||||
label={t("settings.cache.pages")}
|
||||
size={stats.pages.size}
|
||||
entries={stats.pages.entries}
|
||||
cacheType="pages"
|
||||
description={t("settings.cache.pagesDesc")}
|
||||
onClear={() => handleClearCache("pages")}
|
||||
isClearing={clearingCache === "pages"}
|
||||
onLoadEntries={handleLoadEntries}
|
||||
/>
|
||||
<CacheItem
|
||||
icon={<FileJson className="h-4 w-4" />}
|
||||
label={t("settings.cache.api")}
|
||||
@@ -365,27 +405,55 @@ export function CacheSettings() {
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-muted-foreground py-8">
|
||||
{t("settings.cache.unavailable")}
|
||||
</p>
|
||||
<div className="text-center py-8 space-y-4">
|
||||
<p className="text-muted-foreground">{t("settings.cache.unavailable")}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReinstall}
|
||||
disabled={isReinstalling}
|
||||
className="gap-2"
|
||||
>
|
||||
{isReinstalling ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
)}
|
||||
{t("settings.cache.reinstall")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bouton vider tout */}
|
||||
{stats && stats.total > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full gap-2"
|
||||
onClick={() => handleClearCache("all")}
|
||||
disabled={clearingCache !== null}
|
||||
>
|
||||
{clearingCache === "all" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
{t("settings.cache.clearAll")}
|
||||
</Button>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full gap-2"
|
||||
onClick={() => handleClearCache("all")}
|
||||
disabled={clearingCache !== null}
|
||||
>
|
||||
{clearingCache === "all" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
{t("settings.cache.clearAll")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={handleReinstall}
|
||||
disabled={isReinstalling}
|
||||
>
|
||||
{isReinstalling ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
)}
|
||||
{t("settings.cache.reinstall")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { createContext, useContext, useEffect, useState, useCallback, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { registerServiceWorker } from "@/lib/registerSW";
|
||||
import { registerServiceWorker, unregisterServiceWorker } from "@/lib/registerSW";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
interface CacheStats {
|
||||
static: { size: number; entries: number };
|
||||
pages: { size: number; entries: number };
|
||||
api: { size: number; entries: number };
|
||||
images: { size: number; entries: number };
|
||||
books: { size: number; entries: number };
|
||||
@@ -23,7 +24,7 @@ interface CacheUpdate {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
type CacheType = "all" | "static" | "api" | "images" | "rsc" | "books";
|
||||
type CacheType = "all" | "static" | "pages" | "api" | "images" | "books";
|
||||
|
||||
interface ServiceWorkerContextValue {
|
||||
isSupported: boolean;
|
||||
@@ -38,6 +39,7 @@ interface ServiceWorkerContextValue {
|
||||
clearCache: (cacheType?: CacheType) => Promise<boolean>;
|
||||
skipWaiting: () => void;
|
||||
reloadForUpdate: () => void;
|
||||
reinstallServiceWorker: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const ServiceWorkerContext = createContext<ServiceWorkerContextValue | null>(null);
|
||||
@@ -53,76 +55,113 @@ export function ServiceWorkerProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Handle messages from service worker
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const { type, payload } = event.data || {};
|
||||
try {
|
||||
// Ignore messages without proper data structure
|
||||
if (!event.data || typeof event.data !== "object") return;
|
||||
|
||||
switch (type) {
|
||||
case "SW_ACTIVATED":
|
||||
setIsReady(true);
|
||||
setVersion(payload?.version || null);
|
||||
break;
|
||||
// Only handle messages from our service worker (check for known message types)
|
||||
const knownTypes = [
|
||||
"SW_ACTIVATED",
|
||||
"SW_VERSION",
|
||||
"CACHE_UPDATED",
|
||||
"CACHE_STATS",
|
||||
"CACHE_STATS_ERROR",
|
||||
"CACHE_CLEARED",
|
||||
"CACHE_CLEAR_ERROR",
|
||||
"CACHE_ENTRIES",
|
||||
"CACHE_ENTRIES_ERROR",
|
||||
];
|
||||
|
||||
case "SW_VERSION":
|
||||
setVersion(payload?.version || null);
|
||||
break;
|
||||
const type = event.data.type;
|
||||
if (typeof type !== "string" || !knownTypes.includes(type)) return;
|
||||
|
||||
case "CACHE_UPDATED":
|
||||
setCacheUpdates((prev) => {
|
||||
// Avoid duplicates for the same URL within 1 second
|
||||
const existing = prev.find(
|
||||
(u) => u.url === payload.url && Date.now() - u.timestamp < 1000
|
||||
);
|
||||
if (existing) return prev;
|
||||
return [...prev, { url: payload.url, timestamp: payload.timestamp }];
|
||||
});
|
||||
break;
|
||||
const payload = event.data.payload;
|
||||
|
||||
case "CACHE_STATS":
|
||||
const statsResolver = pendingRequests.current.get("CACHE_STATS");
|
||||
if (statsResolver) {
|
||||
statsResolver(payload);
|
||||
pendingRequests.current.delete("CACHE_STATS");
|
||||
switch (type) {
|
||||
case "SW_ACTIVATED":
|
||||
setIsReady(true);
|
||||
setVersion(payload?.version || null);
|
||||
break;
|
||||
|
||||
case "SW_VERSION":
|
||||
setVersion(payload?.version || null);
|
||||
break;
|
||||
|
||||
case "CACHE_UPDATED": {
|
||||
const url = typeof payload?.url === "string" ? payload.url : null;
|
||||
const timestamp = typeof payload?.timestamp === "number" ? payload.timestamp : Date.now();
|
||||
if (url) {
|
||||
setCacheUpdates((prev) => {
|
||||
// Avoid duplicates for the same URL within 1 second
|
||||
const existing = prev.find((u) => u.url === url && Date.now() - u.timestamp < 1000);
|
||||
if (existing) return prev;
|
||||
return [...prev, { url, timestamp }];
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "CACHE_STATS_ERROR":
|
||||
const statsErrorResolver = pendingRequests.current.get("CACHE_STATS");
|
||||
if (statsErrorResolver) {
|
||||
statsErrorResolver(null);
|
||||
pendingRequests.current.delete("CACHE_STATS");
|
||||
}
|
||||
break;
|
||||
case "CACHE_STATS":
|
||||
const statsResolver = pendingRequests.current.get("CACHE_STATS");
|
||||
if (statsResolver) {
|
||||
statsResolver(payload);
|
||||
pendingRequests.current.delete("CACHE_STATS");
|
||||
}
|
||||
break;
|
||||
|
||||
case "CACHE_CLEARED":
|
||||
const clearResolver = pendingRequests.current.get("CACHE_CLEARED");
|
||||
if (clearResolver) {
|
||||
clearResolver(true);
|
||||
pendingRequests.current.delete("CACHE_CLEARED");
|
||||
}
|
||||
break;
|
||||
case "CACHE_STATS_ERROR":
|
||||
const statsErrorResolver = pendingRequests.current.get("CACHE_STATS");
|
||||
if (statsErrorResolver) {
|
||||
statsErrorResolver(null);
|
||||
pendingRequests.current.delete("CACHE_STATS");
|
||||
}
|
||||
break;
|
||||
|
||||
case "CACHE_CLEAR_ERROR":
|
||||
const clearErrorResolver = pendingRequests.current.get("CACHE_CLEARED");
|
||||
if (clearErrorResolver) {
|
||||
clearErrorResolver(false);
|
||||
pendingRequests.current.delete("CACHE_CLEARED");
|
||||
}
|
||||
break;
|
||||
case "CACHE_CLEARED":
|
||||
const clearResolver = pendingRequests.current.get("CACHE_CLEARED");
|
||||
if (clearResolver) {
|
||||
clearResolver(true);
|
||||
pendingRequests.current.delete("CACHE_CLEARED");
|
||||
}
|
||||
break;
|
||||
|
||||
case "CACHE_ENTRIES":
|
||||
const entriesResolver = pendingRequests.current.get("CACHE_ENTRIES");
|
||||
if (entriesResolver) {
|
||||
entriesResolver(payload.entries);
|
||||
pendingRequests.current.delete("CACHE_ENTRIES");
|
||||
}
|
||||
break;
|
||||
case "CACHE_CLEAR_ERROR":
|
||||
const clearErrorResolver = pendingRequests.current.get("CACHE_CLEARED");
|
||||
if (clearErrorResolver) {
|
||||
clearErrorResolver(false);
|
||||
pendingRequests.current.delete("CACHE_CLEARED");
|
||||
}
|
||||
break;
|
||||
|
||||
case "CACHE_ENTRIES_ERROR":
|
||||
const entriesErrorResolver = pendingRequests.current.get("CACHE_ENTRIES");
|
||||
if (entriesErrorResolver) {
|
||||
entriesErrorResolver(null);
|
||||
pendingRequests.current.delete("CACHE_ENTRIES");
|
||||
case "CACHE_ENTRIES": {
|
||||
const entriesResolver = pendingRequests.current.get("CACHE_ENTRIES");
|
||||
if (entriesResolver) {
|
||||
entriesResolver(payload?.entries || null);
|
||||
pendingRequests.current.delete("CACHE_ENTRIES");
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "CACHE_ENTRIES_ERROR": {
|
||||
const entriesErrorResolver = pendingRequests.current.get("CACHE_ENTRIES");
|
||||
if (entriesErrorResolver) {
|
||||
entriesErrorResolver(null);
|
||||
pendingRequests.current.delete("CACHE_ENTRIES");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Ignore unknown message types
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore message handling errors to prevent app crashes
|
||||
// This can happen with malformed messages or during SW reinstall
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("[SW Context] Error handling message:", error, event.data);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -263,6 +302,40 @@ export function ServiceWorkerProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reinstallServiceWorker = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
// Unregister all service workers
|
||||
await unregisterServiceWorker();
|
||||
setIsReady(false);
|
||||
setVersion(null);
|
||||
|
||||
// Re-register
|
||||
const registration = await registerServiceWorker({
|
||||
onSuccess: () => {
|
||||
setIsReady(true);
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({ type: "GET_VERSION" });
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error({ err: error }, "Service worker re-registration failed");
|
||||
},
|
||||
});
|
||||
|
||||
if (registration) {
|
||||
// Force update check
|
||||
await registration.update();
|
||||
// Reload page to ensure new SW takes control
|
||||
window.location.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, "Failed to reinstall service worker");
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ServiceWorkerContext.Provider
|
||||
value={{
|
||||
@@ -278,6 +351,7 @@ export function ServiceWorkerProvider({ children }: { children: ReactNode }) {
|
||||
clearCache,
|
||||
skipWaiting,
|
||||
reloadForUpdate,
|
||||
reinstallServiceWorker,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -146,6 +146,8 @@
|
||||
"imagesQuota": "{used}% of images quota used",
|
||||
"static": "Static resources",
|
||||
"staticDesc": "Next.js scripts, styles and assets",
|
||||
"pages": "Visited pages",
|
||||
"pagesDesc": "Home, libraries, series and details",
|
||||
"api": "API data",
|
||||
"apiDesc": "Series, books and library metadata",
|
||||
"images": "Images",
|
||||
@@ -157,6 +159,8 @@
|
||||
"clearedDesc": "Cache has been cleared successfully",
|
||||
"clearError": "Error clearing cache",
|
||||
"unavailable": "Cache statistics unavailable",
|
||||
"reinstall": "Reinstall Service Worker",
|
||||
"reinstallError": "Error reinstalling Service Worker",
|
||||
"entry": "entry",
|
||||
"entries": "entries",
|
||||
"loadingEntries": "Loading entries...",
|
||||
|
||||
@@ -146,6 +146,8 @@
|
||||
"imagesQuota": "{used}% du quota images utilisé",
|
||||
"static": "Ressources statiques",
|
||||
"staticDesc": "Scripts, styles et assets Next.js",
|
||||
"pages": "Pages visitées",
|
||||
"pagesDesc": "Home, bibliothèques, séries et détails",
|
||||
"api": "Données API",
|
||||
"apiDesc": "Métadonnées des séries, livres et bibliothèques",
|
||||
"images": "Images",
|
||||
@@ -157,6 +159,8 @@
|
||||
"clearedDesc": "Le cache a été vidé avec succès",
|
||||
"clearError": "Erreur lors du vidage du cache",
|
||||
"unavailable": "Statistiques du cache non disponibles",
|
||||
"reinstall": "Réinstaller le Service Worker",
|
||||
"reinstallError": "Erreur lors de la réinstallation du Service Worker",
|
||||
"entry": "entrée",
|
||||
"entries": "entrées",
|
||||
"loadingEntries": "Chargement des entrées...",
|
||||
|
||||
Reference in New Issue
Block a user