Merge pull request #3 from julienfroidefond/feat/debugmode

Feat/debugmode
This commit is contained in:
2025-02-23 22:37:06 +01:00
committed by GitHub
19 changed files with 669 additions and 104 deletions

1
.gitignore vendored
View File

@@ -37,6 +37,7 @@ next-env.d.ts
.vscode
.cache
debug-logs
# Environment variables
.env

View File

@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from "next/server";
import { DebugService } from "@/lib/services/debug.service";
export async function GET() {
try {
const logs = await DebugService.getRequestLogs();
return NextResponse.json(logs);
} catch (error) {
return NextResponse.json({ error: "Erreur lors de la récupération des logs" }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const timing = await request.json();
await DebugService.logRequest(timing);
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: "Erreur lors de l'enregistrement du log" }, { status: 500 });
}
}
export async function DELETE() {
try {
await DebugService.clearLogs();
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: "Erreur lors de la suppression des logs" }, { status: 500 });
}
}

View File

@@ -3,8 +3,9 @@ import { ClientBookWrapper } from "@/components/reader/ClientBookWrapper";
import { BookSkeleton } from "@/components/skeletons/BookSkeleton";
import { BookService } from "@/lib/services/book.service";
import { notFound } from "next/navigation";
import { withPageTiming } from "@/lib/hoc/withPageTiming";
export default async function BookPage({ params }: { params: { bookId: string } }) {
async function BookPage({ params }: { params: { bookId: string } }) {
try {
const data = await BookService.getBook(params.bookId);
@@ -18,3 +19,5 @@ export default async function BookPage({ params }: { params: { bookId: string }
notFound();
}
}
export default withPageTiming("BookPage", BookPage);

View File

@@ -1,7 +1,8 @@
import { PageHeader } from "@/components/layout/PageHeader";
import { DownloadManager } from "@/components/downloads/DownloadManager";
import { withPageTiming } from "@/lib/hoc/withPageTiming";
export default function DownloadsPage() {
function DownloadsPage() {
return (
<>
<PageHeader title="Téléchargements" description="Gérez vos livres disponibles hors ligne" />
@@ -9,3 +10,5 @@ export default function DownloadsPage() {
</>
);
}
export default withPageTiming("DownloadsPage", DownloadsPage);

View File

@@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
import "@/styles/globals.css";
import { cn } from "@/lib/utils";
import ClientLayout from "@/components/layout/ClientLayout";
import { PreferencesProvider } from "@/contexts/PreferencesContext";
const inter = Inter({ subsets: ["latin"] });
@@ -113,7 +114,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
/>
</head>
<body className={cn("min-h-screen bg-background font-sans antialiased", inter.className)}>
<PreferencesProvider>
<ClientLayout>{children}</ClientLayout>
</PreferencesProvider>
</body>
</html>
);

View File

@@ -3,6 +3,7 @@ import { LibraryService } from "@/lib/services/library.service";
import { PreferencesService } from "@/lib/services/preferences.service";
import { revalidatePath } from "next/cache";
import { RefreshButton } from "@/components/library/RefreshButton";
import { withPageTiming } from "@/lib/hoc/withPageTiming";
interface PageProps {
params: { libraryId: string };
@@ -49,7 +50,7 @@ async function getLibrarySeries(
}
}
export default async function LibraryPage({ params, searchParams }: PageProps) {
async function LibraryPage({ params, searchParams }: PageProps) {
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
const preferences = await PreferencesService.getPreferences();
@@ -105,3 +106,5 @@ export default async function LibraryPage({ params, searchParams }: PageProps) {
);
}
}
export default withPageTiming("LibraryPage", LibraryPage);

View File

@@ -0,0 +1,22 @@
import { LibrariesContent } from "@/components/libraries/LibrariesContent";
import { LibraryService } from "@/lib/services/library.service";
import { withPageTiming } from "@/lib/hoc/withPageTiming";
async function LibrariesPage() {
try {
const libraries = await LibraryService.getLibraries();
return <LibrariesContent libraries={libraries} />;
} catch (error) {
return (
<main className="container mx-auto px-4 py-8">
<div className="rounded-md bg-destructive/15 p-4">
<p className="text-sm text-destructive">
{error instanceof Error ? error.message : "Une erreur est survenue"}
</p>
</div>
</main>
);
}
}
export default withPageTiming("LibrariesPage", LibrariesPage);

View File

@@ -2,6 +2,7 @@ import { HomeContent } from "@/components/home/HomeContent";
import { HomeService } from "@/lib/services/home.service";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { withPageTiming } from "@/lib/hoc/withPageTiming";
async function refreshHome() {
"use server";
@@ -16,7 +17,7 @@ async function refreshHome() {
}
}
export default async function HomePage() {
async function HomePage() {
try {
const data = await HomeService.getHomeData();
@@ -38,3 +39,5 @@ export default async function HomePage() {
);
}
}
export default withPageTiming("HomePage", HomePage);

View File

@@ -3,6 +3,7 @@ import { SeriesHeader } from "@/components/series/SeriesHeader";
import { SeriesService } from "@/lib/services/series.service";
import { PreferencesService } from "@/lib/services/preferences.service";
import { revalidatePath } from "next/cache";
import { withPageTiming } from "@/lib/hoc/withPageTiming";
interface PageProps {
params: { seriesId: string };
@@ -38,7 +39,7 @@ async function refreshSeries(seriesId: string) {
}
}
export default async function SeriesPage({ params, searchParams }: PageProps) {
async function SeriesPage({ params, searchParams }: PageProps) {
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
const preferences = await PreferencesService.getPreferences();
@@ -76,3 +77,5 @@ export default async function SeriesPage({ params, searchParams }: PageProps) {
);
}
}
export default withPageTiming("SeriesPage", SeriesPage);

View File

@@ -0,0 +1,229 @@
"use client";
import { usePreferences } from "@/contexts/PreferencesContext";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import {
X,
Database,
Minimize2,
Maximize2,
Clock,
CircleDot,
Layout,
RefreshCw,
Globe,
} from "lucide-react";
import { CacheType } from "@/lib/services/base-api.service";
interface RequestTiming {
url: string;
startTime: number;
endTime: number;
duration: number;
timestamp: string;
fromCache: boolean;
cacheType?: CacheType;
mongoAccess?: {
operation: string;
duration: number;
};
pageRender?: {
page: string;
};
}
function formatTime(timestamp: string) {
const date = new Date(timestamp);
return date.toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
function formatDuration(duration: number) {
return Math.round(duration);
}
export function DebugInfo() {
const [logs, setLogs] = useState<RequestTiming[]>([]);
const [isMinimized, setIsMinimized] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const pathname = usePathname();
const fetchLogs = async () => {
try {
setIsRefreshing(true);
const response = await fetch("/api/debug");
if (response.ok) {
const data = await response.json();
setLogs(data);
}
} catch (error) {
console.error("Erreur lors de la récupération des logs:", error);
} finally {
setIsRefreshing(false);
}
};
// Rafraîchir les logs au montage et à chaque changement de page
useEffect(() => {
fetchLogs();
}, [pathname]);
// Rafraîchir les logs périodiquement si la fenêtre n'est pas minimisée
useEffect(() => {
if (isMinimized) return;
const interval = setInterval(() => {
fetchLogs();
}, 5000); // Rafraîchir toutes les 5 secondes
return () => clearInterval(interval);
}, [isMinimized]);
const clearLogs = async () => {
try {
await fetch("/api/debug", { method: "DELETE" });
setLogs([]);
} catch (error) {
console.error("Erreur lors de la suppression des logs:", error);
}
};
const sortedLogs = [...logs].reverse();
return (
<div
className={`fixed bottom-4 right-4 bg-zinc-900 border border-zinc-700 rounded-lg shadow-lg p-4 text-zinc-100 z-50 ${
isMinimized ? "w-auto" : "w-[800px] max-h-[50vh] overflow-auto"
}`}
>
<div className="flex items-center justify-between mb-4 sticky top-0 bg-zinc-900 pb-2">
<div className="flex items-center gap-2">
<h2 className="font-bold text-lg">DEBUG</h2>
{!isMinimized && (
<span className="text-xs text-zinc-400">
{sortedLogs.length} entrée{sortedLogs.length > 1 ? "s" : ""}
</span>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={fetchLogs}
className="hover:bg-zinc-700 rounded-full p-1.5"
aria-label="Rafraîchir les logs"
disabled={isRefreshing}
>
<RefreshCw className={`h-5 w-5 ${isRefreshing ? "animate-spin" : ""}`} />
</button>
<button
onClick={() => setIsMinimized(!isMinimized)}
className="hover:bg-zinc-700 rounded-full p-1.5"
aria-label={isMinimized ? "Agrandir" : "Minimiser"}
>
{isMinimized ? <Maximize2 className="h-5 w-5" /> : <Minimize2 className="h-5 w-5" />}
</button>
<button
onClick={clearLogs}
className="hover:bg-zinc-700 rounded-full p-1.5"
aria-label="Effacer les logs"
>
<X className="h-5 w-5" />
</button>
</div>
</div>
{!isMinimized && (
<div className="space-y-3">
{sortedLogs.length === 0 ? (
<p className="text-sm opacity-75">Aucune requête enregistrée</p>
) : (
sortedLogs.map((log, index) => (
<div key={index} className="text-sm space-y-1.5 bg-zinc-800 p-2 rounded">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2 min-w-0 flex-1">
{log.fromCache && (
<div title={`Cache: ${log.cacheType || "DEFAULT"}`} className="flex-shrink-0">
<Database className="h-4 w-4" />
</div>
)}
{log.mongoAccess && (
<div
title={`MongoDB: ${log.mongoAccess.operation}`}
className="flex-shrink-0"
>
<CircleDot className="h-4 w-4 text-blue-400" />
</div>
)}
{log.pageRender && (
<div title={`Page Render: ${log.pageRender.page}`} className="flex-shrink-0">
<Layout className="h-4 w-4 text-purple-400" />
</div>
)}
{!log.fromCache && !log.mongoAccess && !log.pageRender && (
<div title="API Call" className="flex-shrink-0">
<Globe className="h-4 w-4 text-rose-400" />
</div>
)}
<span className="font-medium truncate" title={log.url}>
{log.url}
</span>
</div>
<div className="flex items-center gap-3 flex-shrink-0">
<div className="flex items-center gap-1 text-zinc-400" title="Heure du log">
<Clock className="h-3 w-3" />
<span>{formatTime(log.timestamp)}</span>
</div>
<span
className={`${
log.pageRender
? "text-purple-400"
: log.mongoAccess
? "text-blue-400"
: log.fromCache
? "text-emerald-400"
: "text-rose-400"
}`}
>
{formatDuration(log.duration)}ms
</span>
{log.mongoAccess && (
<span className="text-blue-400" title="Temps d'accès MongoDB">
+{formatDuration(log.mongoAccess.duration)}ms
</span>
)}
</div>
</div>
<div className="h-1.5 bg-zinc-700 rounded-full overflow-hidden">
<div
className={`h-full ${
log.pageRender
? "bg-purple-500"
: log.fromCache
? "bg-emerald-500"
: "bg-rose-500"
}`}
style={{
width: `${Math.min((log.duration / 1000) * 100, 100)}%`,
}}
/>
{log.mongoAccess && (
<div
className="h-full bg-blue-500"
style={{
width: `${Math.min((log.mongoAccess.duration / 1000) * 100, 100)}%`,
marginTop: "-6px",
}}
/>
)}
</div>
</div>
))
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,13 @@
"use client";
import { usePreferences } from "@/contexts/PreferencesContext";
import { DebugInfo } from "./DebugInfo";
export function DebugWrapper() {
const { preferences } = usePreferences();
if (!preferences.debug) {
return null;
}
return <DebugInfo />;
}

View File

@@ -11,6 +11,7 @@ import { PreferencesProvider } from "@/contexts/PreferencesContext";
import { registerServiceWorker } from "@/lib/registerSW";
import { NetworkStatus } from "../ui/NetworkStatus";
import { LoadingBar } from "@/components/ui/loading-bar";
import { DebugWrapper } from "@/components/debug/DebugWrapper";
// Routes qui ne nécessitent pas d'authentification
const publicRoutes = ["/login", "/register"];
@@ -71,6 +72,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
<InstallPWA />
<Toaster />
<NetworkStatus />
<DebugWrapper />
</div>
</PreferencesProvider>
</ThemeProvider>

View File

@@ -28,7 +28,6 @@ export function PreferencesProvider({ children }: { children: React.ReactNode })
const [preferences, setPreferences] = useState<UserPreferences>(defaultPreferences);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchPreferences = async () => {
try {
const response = await fetch("/api/preferences");
@@ -47,6 +46,7 @@ export function PreferencesProvider({ children }: { children: React.ReactNode })
}
};
useEffect(() => {
fetchPreferences();
}, []);
@@ -64,14 +64,17 @@ export function PreferencesProvider({ children }: { children: React.ReactNode })
const updatedPreferences = await response.json();
setPreferences((prev) => {
const newState = {
setPreferences((prev) => ({
...prev,
...updatedPreferences,
};
return newState;
});
}));
// Forcer un rafraîchissement des préférences
await fetchPreferences();
return updatedPreferences;
} catch (error) {
console.error("Erreur lors de la mise à jour des préférences:", error);
throw error;
}
};

View File

@@ -0,0 +1,15 @@
import { DebugService } from "@/lib/services/debug.service";
type PageComponent = (props: any) => Promise<JSX.Element> | JSX.Element;
export function withPageTiming(pageName: string, Component: PageComponent) {
return async function PageWithTiming(props: any) {
const start = performance.now();
const result = await Promise.resolve(Component(props));
const duration = performance.now() - start;
// Log le temps de rendu
await DebugService.logPageRender(pageName + JSON.stringify(props.params), duration);
return result;
};
}

View File

@@ -1,6 +1,7 @@
import { AuthConfig } from "@/types/auth";
import { getServerCacheService } from "./server-cache.service";
import { ConfigDBService } from "./config-db.service";
import { DebugService } from "./debug.service";
// Types de cache disponibles
export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES";
@@ -52,7 +53,14 @@ export abstract class BaseApiService {
type: CacheType = "DEFAULT"
): Promise<T> {
const cacheService = await getServerCacheService();
return cacheService.getOrSet(key, fetcher, type);
try {
const result = await cacheService.getOrSet(key, fetcher, type);
return result;
} catch (error) {
throw error;
}
}
protected static handleError(error: unknown, defaultMessage: string): never {
@@ -88,6 +96,7 @@ export abstract class BaseApiService {
headersOptions = {},
options: KomgaRequestInit = {}
): Promise<T> {
const startTime = performance.now();
const config = await this.getKomgaConfig();
const { path, params } = urlBuilder;
const url = this.buildUrl(config, path, params);
@@ -98,16 +107,36 @@ export abstract class BaseApiService {
headers.set(key as string, value as string);
}
}
// console.log("🛜 Fetching from", url);
// console.log("Headers", headers);
// console.log("headersOptions", headersOptions);
// console.log("options", options);
try {
const response = await fetch(url, { headers, ...options });
const endTime = performance.now();
// Log la requête
await DebugService.logRequest({
url: path,
startTime,
endTime,
fromCache: false,
});
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`);
}
return options.isImage ? response : response.json();
} catch (error) {
const endTime = performance.now();
// Log aussi les erreurs
await DebugService.logRequest({
url: path,
startTime,
endTime,
fromCache: false,
});
throw error;
}
}
}

View File

@@ -1,6 +1,7 @@
import connectDB from "@/lib/mongodb";
import { KomgaConfig } from "@/lib/models/config.model";
import { TTLConfig } from "@/lib/models/ttl-config.model";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
interface User {
@@ -54,19 +55,27 @@ export class ConfigDBService {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("getConfig", async () => {
const config = await KomgaConfig.findOne({ userId: user.id });
if (!config) {
throw new Error("Configuration non trouvée");
return config;
});
}
static async getTTLConfig() {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("getTTLConfig", async () => {
const config = await TTLConfig.findOne({ userId: user.id });
return config;
});
}
static async saveTTLConfig(data: TTLConfigData) {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("saveTTLConfig", async () => {
const config = await TTLConfig.findOneAndUpdate(
{ userId: user.id },
{
@@ -75,35 +84,7 @@ export class ConfigDBService {
},
{ upsert: true, new: true }
);
return config;
}
static async getTTLConfig() {
const user = this.getCurrentUser();
await connectDB();
const config = await TTLConfig.findOne({ userId: user.id });
if (!config) {
// Retourner la configuration par défaut si aucune configuration n'existe
return {
defaultTTL: 5,
homeTTL: 5,
librariesTTL: 1440,
seriesTTL: 5,
booksTTL: 5,
imagesTTL: 1440,
};
}
return {
defaultTTL: config.defaultTTL,
homeTTL: config.homeTTL,
librariesTTL: config.librariesTTL,
seriesTTL: config.seriesTTL,
booksTTL: config.booksTTL,
imagesTTL: config.imagesTTL,
};
});
}
}

View File

@@ -0,0 +1,181 @@
import fs from "fs/promises";
import path from "path";
import { CacheType } from "./base-api.service";
import { AuthServerService } from "./auth-server.service";
import { PreferencesService } from "./preferences.service";
interface RequestTiming {
url: string;
startTime: number;
endTime: number;
duration: number;
timestamp: string;
fromCache: boolean;
cacheType?: CacheType;
mongoAccess?: {
operation: string;
duration: number;
};
pageRender?: {
page: string;
duration: number;
};
}
export class DebugService {
private static getCurrentUserId(): string {
const user = AuthServerService.getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
}
return user.id;
}
private static getLogFilePath(userId: string): string {
return path.join(process.cwd(), "debug-logs", `${userId}.json`);
}
private static async ensureDebugDir(): Promise<void> {
const debugDir = path.join(process.cwd(), "debug-logs");
try {
await fs.access(debugDir);
} catch {
await fs.mkdir(debugDir, { recursive: true });
}
}
static async logRequest(timing: Omit<RequestTiming, "duration" | "timestamp">) {
try {
const userId = await this.getCurrentUserId();
const preferences = await PreferencesService.getPreferences();
if (!preferences.debug) {
return;
}
await this.ensureDebugDir();
const filePath = this.getLogFilePath(userId);
let logs: RequestTiming[] = [];
try {
const content = await fs.readFile(filePath, "utf-8");
logs = JSON.parse(content);
} catch {
// Le fichier n'existe pas encore ou est vide
}
const newTiming: RequestTiming = {
...timing,
duration: timing.endTime - timing.startTime,
timestamp: new Date().toISOString(),
};
// Garde les 100 dernières requêtes
logs = [...logs.slice(-99), newTiming];
await fs.writeFile(filePath, JSON.stringify(logs, null, 2));
} catch (error) {
// Ignore les erreurs de logging
console.error("Erreur lors de l'enregistrement du log:", error);
}
}
static async getRequestLogs(): Promise<RequestTiming[]> {
try {
const userId = await this.getCurrentUserId();
const filePath = this.getLogFilePath(userId);
const content = await fs.readFile(filePath, "utf-8");
return JSON.parse(content);
} catch {
return [];
}
}
static async clearLogs(): Promise<void> {
try {
const userId = await this.getCurrentUserId();
const filePath = this.getLogFilePath(userId);
await fs.writeFile(filePath, "[]");
} catch {
// Ignore les erreurs si le fichier n'existe pas
}
}
static async logPageRender(page: string, duration: number) {
try {
const userId = await this.getCurrentUserId();
const preferences = await PreferencesService.getPreferences();
if (!preferences.debug) {
return;
}
await this.ensureDebugDir();
const filePath = this.getLogFilePath(userId);
let logs: RequestTiming[] = [];
try {
const content = await fs.readFile(filePath, "utf-8");
logs = JSON.parse(content);
} catch {
// Le fichier n'existe pas encore ou est vide
}
const now = performance.now();
const newTiming: RequestTiming = {
url: `Page Render: ${page}`,
startTime: now - duration,
endTime: now,
duration,
timestamp: new Date().toISOString(),
fromCache: false,
pageRender: {
page,
duration,
},
};
// Garde les 100 dernières requêtes
logs = [...logs.slice(-99), newTiming];
await fs.writeFile(filePath, JSON.stringify(logs, null, 2));
} catch (error) {
// Ignore les erreurs de logging
console.error("Erreur lors de l'enregistrement du log de page:", error);
}
}
static async measureMongoOperation<T>(operation: string, func: () => Promise<T>): Promise<T> {
const startTime = performance.now();
try {
const preferences = await PreferencesService.getPreferences();
if (!preferences.debug) {
return func();
}
const result = await func();
const endTime = performance.now();
await this.logRequest({
url: `MongoDB: ${operation}`,
startTime,
endTime,
fromCache: false,
mongoAccess: {
operation,
duration: endTime - startTime,
},
});
return result;
} catch (error) {
const endTime = performance.now();
await this.logRequest({
url: `MongoDB Error: ${operation}`,
startTime,
endTime,
fromCache: false,
mongoAccess: {
operation,
duration: endTime - startTime,
},
});
throw error;
}
}
}

View File

@@ -1,5 +1,6 @@
import connectDB from "@/lib/mongodb";
import { FavoriteModel } from "@/lib/models/favorite.model";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
interface User {
@@ -33,12 +34,13 @@ export class FavoriteService {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("isFavorite", async () => {
const favorite = await FavoriteModel.findOne({
userId: user.id,
seriesId: seriesId,
});
return !!favorite;
});
} catch (error) {
console.error("Erreur lors de la vérification du favori:", error);
return false;
@@ -53,11 +55,13 @@ export class FavoriteService {
const user = this.getCurrentUser();
await connectDB();
await DebugService.measureMongoOperation("addToFavorites", async () => {
await FavoriteModel.findOneAndUpdate(
{ userId: user.id, seriesId },
{ userId: user.id, seriesId },
{ upsert: true }
);
});
this.dispatchFavoritesChanged();
} catch (error) {
@@ -74,10 +78,12 @@ export class FavoriteService {
const user = this.getCurrentUser();
await connectDB();
await DebugService.measureMongoOperation("removeFromFavorites", async () => {
await FavoriteModel.findOneAndDelete({
userId: user.id,
seriesId,
});
});
this.dispatchFavoritesChanged();
} catch (error) {
@@ -90,15 +96,36 @@ export class FavoriteService {
* Récupère tous les IDs des séries favorites
*/
static async getAllFavoriteIds(): Promise<string[]> {
try {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("getAllFavoriteIds", async () => {
const favorites = await FavoriteModel.find({ userId: user.id });
return favorites.map((favorite) => favorite.seriesId);
} catch (error) {
console.error("Erreur lors de la récupération des favoris:", error);
return [];
}
});
}
static async addFavorite(seriesId: string) {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("addFavorite", async () => {
const favorite = await FavoriteModel.findOneAndUpdate(
{ userId: user.id, seriesId },
{ userId: user.id, seriesId },
{ upsert: true, new: true }
);
return favorite;
});
}
static async removeFavorite(seriesId: string): Promise<boolean> {
const user = this.getCurrentUser();
await connectDB();
return DebugService.measureMongoOperation("removeFavorite", async () => {
const result = await FavoriteModel.deleteOne({ userId: user.id, seriesId });
return result.deletedCount > 0;
});
}
}

View File

@@ -1,6 +1,7 @@
import fs from "fs";
import path from "path";
import { PreferencesService } from "./preferences.service";
import { DebugService } from "./debug.service";
type CacheMode = "file" | "memory";
@@ -370,13 +371,26 @@ class ServerCacheService {
fetcher: () => Promise<T>,
type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"
): Promise<T> {
const startTime = performance.now();
const cached = this.get(key);
if (cached !== null) {
const endTime = performance.now();
// Log la requête avec l'indication du cache
await DebugService.logRequest({
url: key,
startTime,
endTime,
fromCache: true,
cacheType: type,
});
return cached as T;
}
try {
const data = await fetcher();
this.set(key, data, type);
return data;
} catch (error) {