fix: cache file KO if reload

This commit is contained in:
Julien Froidefond
2025-02-23 16:03:07 +01:00
parent 442f318be8
commit 54d8a0684c
12 changed files with 210 additions and 183 deletions

View File

@@ -1,15 +1,8 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { serverCacheService } from "@/lib/services/server-cache.service"; import { getServerCacheService } from "@/lib/services/server-cache.service";
export async function POST() { export async function POST() {
try { const cacheService = await getServerCacheService();
serverCacheService.clear(); cacheService.clear();
return NextResponse.json({ message: "Cache serveur supprimé avec succès" }); return NextResponse.json({ message: "Cache cleared" });
} catch (error) {
console.error("Erreur lors de la suppression du cache serveur:", error);
return NextResponse.json(
{ error: "Erreur lors de la suppression du cache serveur" },
{ status: 500 }
);
}
} }

View File

@@ -1,8 +1,9 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { serverCacheService } from "@/lib/services/server-cache.service"; import { getServerCacheService } from "@/lib/services/server-cache.service";
export async function GET() { export async function GET() {
return NextResponse.json({ mode: serverCacheService.getCacheMode() }); const cacheService = await getServerCacheService();
return NextResponse.json({ mode: cacheService.getCacheMode() });
} }
export async function POST(request: Request) { export async function POST(request: Request) {
@@ -15,8 +16,9 @@ export async function POST(request: Request) {
); );
} }
serverCacheService.setCacheMode(mode); const cacheService = await getServerCacheService();
return NextResponse.json({ mode: serverCacheService.getCacheMode() }); cacheService.setCacheMode(mode);
return NextResponse.json({ mode: cacheService.getCacheMode() });
} catch (error) { } catch (error) {
console.error("Erreur lors de la mise à jour du mode de cache:", error); console.error("Erreur lors de la mise à jour du mode de cache:", error);
return NextResponse.json({ error: "Invalid request" }, { status: 400 }); return NextResponse.json({ error: "Invalid request" }, { status: 400 });

View File

@@ -15,7 +15,7 @@ async function refreshLibrary(libraryId: string) {
"use server"; "use server";
try { try {
await LibraryService.clearLibrarySeriesCache(libraryId); await LibraryService.invalidateLibrarySeriesCache(libraryId);
revalidatePath(`/libraries/${libraryId}`); revalidatePath(`/libraries/${libraryId}`);
return { success: true }; return { success: true };

View File

@@ -7,7 +7,7 @@ async function refreshHome() {
"use server"; "use server";
try { try {
await HomeService.clearHomeCache(); await HomeService.invalidateHomeCache();
revalidatePath("/"); revalidatePath("/");
return { success: true }; return { success: true };
} catch (error) { } catch (error) {

View File

@@ -28,8 +28,8 @@ async function refreshSeries(seriesId: string) {
"use server"; "use server";
try { try {
await SeriesService.clearSeriesBooksCache(seriesId); await SeriesService.invalidateSeriesBooksCache(seriesId);
await SeriesService.clearSeriesCache(seriesId); await SeriesService.invalidateSeriesCache(seriesId);
revalidatePath(`/series/${seriesId}`); revalidatePath(`/series/${seriesId}`);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, useCallback } from "react";
import { KomgaBook } from "@/types/komga"; import { KomgaBook } from "@/types/komga";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
@@ -31,12 +31,11 @@ export function DownloadManager() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const { toast } = useToast(); const { toast } = useToast();
const getStorageKey = (bookId: string) => `book-status-${bookId}`; const getStorageKey = useCallback((bookId: string) => `book-status-${bookId}`, []);
const loadDownloadedBooks = async () => { const loadDownloadedBooks = useCallback(async () => {
setIsLoading(true); setIsLoading(true);
try { try {
// Récupère tous les livres du localStorage
const books: DownloadedBook[] = []; const books: DownloadedBook[] = [];
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i);
@@ -70,9 +69,9 @@ export function DownloadManager() {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; }, [toast]);
const updateBookStatuses = () => { const updateBookStatuses = useCallback(() => {
setDownloadedBooks((prevBooks) => { setDownloadedBooks((prevBooks) => {
return prevBooks.map((downloadedBook) => { return prevBooks.map((downloadedBook) => {
const status = JSON.parse( const status = JSON.parse(
@@ -87,29 +86,25 @@ export function DownloadManager() {
}; };
}); });
}); });
}; }, [getStorageKey]);
useEffect(() => { useEffect(() => {
loadDownloadedBooks(); loadDownloadedBooks();
// Écoute les changements de statut des livres
const handleStorageChange = (e: StorageEvent) => { const handleStorageChange = (e: StorageEvent) => {
if (e.key?.startsWith("book-status-")) { if (e.key?.startsWith("book-status-")) {
updateBookStatuses(); updateBookStatuses();
} }
}; };
// Écoute les changements dans d'autres onglets
window.addEventListener("storage", handleStorageChange); window.addEventListener("storage", handleStorageChange);
// Écoute les changements dans l'onglet courant
const interval = setInterval(updateBookStatuses, 1000); const interval = setInterval(updateBookStatuses, 1000);
return () => { return () => {
window.removeEventListener("storage", handleStorageChange); window.removeEventListener("storage", handleStorageChange);
clearInterval(interval); clearInterval(interval);
}; };
}, []); }, [loadDownloadedBooks, updateBookStatuses]);
const handleDeleteBook = async (book: KomgaBook) => { const handleDeleteBook = async (book: KomgaBook) => {
try { try {

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { Download, Check, Loader2 } from "lucide-react"; import { Download, Check, Loader2 } from "lucide-react";
import { Button } from "./button"; import { Button } from "./button";
import { useToast } from "./use-toast"; import { useToast } from "./use-toast";
@@ -27,22 +27,29 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
const [downloadProgress, setDownloadProgress] = useState(0); const [downloadProgress, setDownloadProgress] = useState(0);
const { toast } = useToast(); const { toast } = useToast();
const getStorageKey = (bookId: string) => `book-status-${bookId}`; const getStorageKey = useCallback((bookId: string) => `book-status-${bookId}`, []);
const getBookStatus = (bookId: string): BookDownloadStatus => { const getBookStatus = useCallback(
(bookId: string): BookDownloadStatus => {
try { try {
const status = localStorage.getItem(getStorageKey(bookId)); const status = localStorage.getItem(getStorageKey(bookId));
return status ? JSON.parse(status) : { status: "idle", progress: 0, timestamp: 0 }; return status ? JSON.parse(status) : { status: "idle", progress: 0, timestamp: 0 };
} catch { } catch {
return { status: "idle", progress: 0, timestamp: 0 }; return { status: "idle", progress: 0, timestamp: 0 };
} }
}; },
[getStorageKey]
);
const setBookStatus = (bookId: string, status: BookDownloadStatus) => { const setBookStatus = useCallback(
(bookId: string, status: BookDownloadStatus) => {
localStorage.setItem(getStorageKey(bookId), JSON.stringify(status)); localStorage.setItem(getStorageKey(bookId), JSON.stringify(status));
}; },
[getStorageKey]
);
const downloadBook = async (startFromPage: number = 1) => { const downloadBook = useCallback(
async (startFromPage: number = 1) => {
try { try {
const cache = await caches.open("stripstream-books"); const cache = await caches.open("stripstream-books");
@@ -81,7 +88,10 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
await new Promise((resolve) => setTimeout(resolve, 1000)); // Attendre 1s avant de réessayer await new Promise((resolve) => setTimeout(resolve, 1000)); // Attendre 1s avant de réessayer
continue; continue;
} }
await cache.put(`/api/komga/images/books/${book.id}/pages/${i}`, pageResponse.clone()); await cache.put(
`/api/komga/images/books/${book.id}/pages/${i}`,
pageResponse.clone()
);
break; // Sortir de la boucle si réussi break; // Sortir de la boucle si réussi
} catch (error) { } catch (error) {
retryCount++; retryCount++;
@@ -147,36 +157,11 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
setIsLoading(false); setIsLoading(false);
setDownloadProgress(0); setDownloadProgress(0);
} }
}; },
[book.id, book.media.pagesCount, getBookStatus, setBookStatus, toast]
);
// Vérifie si le livre est déjà disponible hors ligne const checkOfflineAvailability = useCallback(async () => {
useEffect(() => {
const checkStatus = async () => {
const storedStatus = getBookStatus(book.id);
// Si le livre est marqué comme en cours de téléchargement
if (storedStatus.status === "downloading") {
// Si le téléchargement a commencé il y a plus de 5 minutes, on considère qu'il a échoué
if (Date.now() - storedStatus.timestamp > 5 * 60 * 1000) {
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
setIsLoading(false);
setDownloadProgress(0);
} else {
// On reprend le téléchargement là où il s'était arrêté
setIsLoading(true);
setDownloadProgress(storedStatus.progress);
const startFromPage = (storedStatus.lastDownloadedPage || 0) + 1;
downloadBook(startFromPage);
}
}
await checkOfflineAvailability();
};
checkStatus();
}, [book.id]);
const checkOfflineAvailability = async () => {
if (!("caches" in window)) return; if (!("caches" in window)) return;
try { try {
@@ -209,8 +194,31 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
console.error("Erreur lors de la vérification du cache:", error); console.error("Erreur lors de la vérification du cache:", error);
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() }); setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
} }
}, [book.id, book.media.pagesCount, setBookStatus]);
useEffect(() => {
const checkStatus = async () => {
const storedStatus = getBookStatus(book.id);
if (storedStatus.status === "downloading") {
if (Date.now() - storedStatus.timestamp > 5 * 60 * 1000) {
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
setIsLoading(false);
setDownloadProgress(0);
} else {
setIsLoading(true);
setDownloadProgress(storedStatus.progress);
const startFromPage = (storedStatus.lastDownloadedPage || 0) + 1;
downloadBook(startFromPage);
}
}
await checkOfflineAvailability();
}; };
checkStatus();
}, [book.id, checkOfflineAvailability, downloadBook, getBookStatus, setBookStatus]);
const handleToggleOffline = async () => { const handleToggleOffline = async () => {
if (!("caches" in window)) { if (!("caches" in window)) {
toast({ toast({

View File

@@ -1,5 +1,5 @@
import { AuthConfig } from "@/types/auth"; import { AuthConfig } from "@/types/auth";
import { serverCacheService } from "./server-cache.service"; import { getServerCacheService } from "./server-cache.service";
import { ConfigDBService } from "./config-db.service"; import { ConfigDBService } from "./config-db.service";
// Types de cache disponibles // Types de cache disponibles
@@ -51,7 +51,8 @@ export abstract class BaseApiService {
fetcher: () => Promise<T>, fetcher: () => Promise<T>,
type: CacheType = "DEFAULT" type: CacheType = "DEFAULT"
): Promise<T> { ): Promise<T> {
return serverCacheService.getOrSet(key, fetcher, type); const cacheService = await getServerCacheService();
return cacheService.getOrSet(key, fetcher, type);
} }
protected static handleError(error: unknown, defaultMessage: string): never { protected static handleError(error: unknown, defaultMessage: string): never {

View File

@@ -1,7 +1,7 @@
import { BaseApiService } from "./base-api.service"; import { BaseApiService } from "./base-api.service";
import { KomgaBook, KomgaSeries } from "@/types/komga"; import { KomgaBook, KomgaSeries } from "@/types/komga";
import { LibraryResponse } from "@/types/library"; import { LibraryResponse } from "@/types/library";
import { serverCacheService } from "./server-cache.service"; import { getServerCacheService } from "./server-cache.service";
interface HomeData { interface HomeData {
ongoing: KomgaSeries[]; ongoing: KomgaSeries[];
@@ -67,9 +67,10 @@ export class HomeService extends BaseApiService {
} }
} }
static async clearHomeCache() { static async invalidateHomeCache(): Promise<void> {
serverCacheService.delete("home-ongoing"); const cacheService = await getServerCacheService();
serverCacheService.delete("home-recently-read"); cacheService.delete("home-ongoing");
serverCacheService.delete("home-on-deck"); cacheService.delete("home-recently-read");
cacheService.delete("home-on-deck");
} }
} }

View File

@@ -1,7 +1,7 @@
import { BaseApiService } from "./base-api.service"; import { BaseApiService } from "./base-api.service";
import { Library, LibraryResponse } from "@/types/library"; import { Library, LibraryResponse } from "@/types/library";
import { Series } from "@/types/series"; import { Series } from "@/types/series";
import { serverCacheService } from "./server-cache.service"; import { getServerCacheService } from "./server-cache.service";
export class LibraryService extends BaseApiService { export class LibraryService extends BaseApiService {
static async getLibraries(): Promise<Library[]> { static async getLibraries(): Promise<Library[]> {
@@ -134,7 +134,8 @@ export class LibraryService extends BaseApiService {
} }
} }
static async clearLibrarySeriesCache(libraryId: string) { static async invalidateLibrarySeriesCache(libraryId: string): Promise<void> {
serverCacheService.delete(`library-${libraryId}-all-series`); const cacheService = await getServerCacheService();
cacheService.delete(`library-${libraryId}-all-series`);
} }
} }

View File

@@ -4,7 +4,7 @@ import { KomgaBook, KomgaSeries } from "@/types/komga";
import { BookService } from "./book.service"; import { BookService } from "./book.service";
import { ImageService } from "./image.service"; import { ImageService } from "./image.service";
import { PreferencesService } from "./preferences.service"; import { PreferencesService } from "./preferences.service";
import { serverCacheService } from "./server-cache.service"; import { getServerCacheService } from "./server-cache.service";
export class SeriesService extends BaseApiService { export class SeriesService extends BaseApiService {
static async getSeries(seriesId: string): Promise<KomgaSeries> { static async getSeries(seriesId: string): Promise<KomgaSeries> {
@@ -19,8 +19,9 @@ export class SeriesService extends BaseApiService {
} }
} }
static async clearSeriesCache(seriesId: string) { static async invalidateSeriesCache(seriesId: string): Promise<void> {
serverCacheService.delete(`series-${seriesId}`); const cacheService = await getServerCacheService();
cacheService.delete(`series-${seriesId}`);
} }
static async getAllSeriesBooks(seriesId: string): Promise<KomgaBook[]> { static async getAllSeriesBooks(seriesId: string): Promise<KomgaBook[]> {
@@ -125,8 +126,9 @@ export class SeriesService extends BaseApiService {
} }
} }
static async clearSeriesBooksCache(seriesId: string) { static async invalidateSeriesBooksCache(seriesId: string): Promise<void> {
serverCacheService.delete(`series-${seriesId}-all-books`); const cacheService = await getServerCacheService();
cacheService.delete(`series-${seriesId}-all-books`);
} }
static async getFirstBook(seriesId: string): Promise<string> { static async getFirstBook(seriesId: string): Promise<string> {

View File

@@ -1,5 +1,6 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { PreferencesService } from "./preferences.service";
type CacheMode = "file" | "memory"; type CacheMode = "file" | "memory";
@@ -37,6 +38,17 @@ class ServerCacheService {
this.cacheDir = path.join(process.cwd(), ".cache"); this.cacheDir = path.join(process.cwd(), ".cache");
this.ensureCacheDirectory(); this.ensureCacheDirectory();
this.cleanExpiredCache(); this.cleanExpiredCache();
this.initializeCacheMode();
}
private async initializeCacheMode(): Promise<void> {
try {
const preferences = await PreferencesService.getPreferences();
this.setCacheMode(preferences.cacheMode);
} catch (error) {
console.error("Error initializing cache mode from preferences:", error);
// Keep default memory mode if preferences can't be loaded
}
} }
private ensureCacheDirectory(): void { private ensureCacheDirectory(): void {
@@ -117,9 +129,10 @@ class ServerCacheService {
cleanDirectory(this.cacheDir); cleanDirectory(this.cacheDir);
} }
public static getInstance(): ServerCacheService { public static async getInstance(): Promise<ServerCacheService> {
if (!ServerCacheService.instance) { if (!ServerCacheService.instance) {
ServerCacheService.instance = new ServerCacheService(); ServerCacheService.instance = new ServerCacheService();
await ServerCacheService.instance.initializeCacheMode();
} }
return ServerCacheService.instance; return ServerCacheService.instance;
} }
@@ -376,4 +389,15 @@ class ServerCacheService {
} }
} }
export const serverCacheService = ServerCacheService.getInstance(); // Créer une instance initialisée du service
let initializedInstance: Promise<ServerCacheService>;
export const getServerCacheService = async (): Promise<ServerCacheService> => {
if (!initializedInstance) {
initializedInstance = ServerCacheService.getInstance();
}
return initializedInstance;
};
// Exporter aussi la classe pour les tests
export { ServerCacheService };