chore: update various components and services for improved functionality and consistency, including formatting adjustments and minor refactors

This commit is contained in:
Julien Froidefond
2025-12-07 09:54:05 +01:00
parent 4f5724c0ff
commit 39e3328123
141 changed files with 5292 additions and 3243 deletions

View File

@@ -3,7 +3,7 @@ import type { UserData } from "@/lib/services/auth-server.service";
export async function getCurrentUser(): Promise<UserData | null> {
const session = await auth();
if (!session?.user) {
return null;
}
@@ -23,14 +23,14 @@ export async function isAdmin(): Promise<boolean> {
export async function requireAdmin(): Promise<UserData> {
const user = await getCurrentUser();
if (!user) {
throw new Error("Unauthenticated");
}
if (!user.roles.includes("ROLE_ADMIN")) {
throw new Error("Forbidden: Admin access required");
}
return user;
}

View File

@@ -22,7 +22,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
credentials.email as string,
credentials.password as string
);
return {
id: userData.id,
email: userData.email,
@@ -61,4 +61,4 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
secret: process.env.NEXTAUTH_SECRET,
trustHost: true,
useSecureCookies: false,
});
});

View File

@@ -1,9 +1,9 @@
import pino from 'pino';
import pino from "pino";
const isProduction = process.env.NODE_ENV === 'production';
const isProduction = process.env.NODE_ENV === "production";
const logger = pino({
level: isProduction ? 'info' : 'debug',
level: isProduction ? "info" : "debug",
timestamp: pino.stdTimeFunctions.isoTime,
...(isProduction
? {
@@ -15,15 +15,18 @@ const logger = pino({
},
log: (object) => {
// Format readable timestamp in production
if (object.time && (typeof object.time === 'string' || typeof object.time === 'number')) {
if (
object.time &&
(typeof object.time === "string" || typeof object.time === "number")
) {
const date = new Date(object.time);
object.time = date.toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
object.time = date.toLocaleString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
return object;
@@ -32,11 +35,11 @@ const logger = pino({
}
: {
transport: {
target: 'pino-pretty',
target: "pino-pretty",
options: {
colorize: true,
translateTime: 'SYS:dd/mm/yyyy HH:MM:ss',
ignore: 'pid,hostname',
translateTime: "SYS:dd/mm/yyyy HH:MM:ss",
ignore: "pid,hostname",
singleLine: true,
},
},
@@ -49,10 +52,9 @@ const logger = pino({
});
// Prevent memory leaks in development (Node.js runtime only)
if (!isProduction && typeof process.stdout !== 'undefined') {
if (!isProduction && typeof process.stdout !== "undefined") {
process.stdout.setMaxListeners?.(20);
process.stderr.setMaxListeners?.(20);
}
export default logger;

View File

@@ -4,11 +4,11 @@ import logger from "@/lib/logger";
export async function getAuthSession(request: NextRequest) {
try {
const token = await getToken({
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
});
if (!token) {
return null;
}
@@ -18,7 +18,7 @@ export async function getAuthSession(request: NextRequest) {
id: token.sub!,
email: token.email!,
roles: JSON.parse(token.roles as string),
}
},
};
} catch (error) {
logger.error({ err: error }, "Auth error in middleware");

View File

@@ -11,4 +11,3 @@ if (process.env.NODE_ENV !== "production") {
}
export default prisma;

View File

@@ -182,19 +182,18 @@ export class AdminService {
try {
await requireAdmin();
const [totalUsers, usersWithKomga, usersWithPreferences] =
await Promise.all([
prisma.user.count(),
prisma.komgaConfig.count(),
prisma.preferences.count(),
]);
const [totalUsers, usersWithKomga, usersWithPreferences] = await Promise.all([
prisma.user.count(),
prisma.komgaConfig.count(),
prisma.preferences.count(),
]);
// Count admin users by fetching all users and filtering
const allUsers = await prisma.user.findMany({
select: { roles: true },
});
const totalAdmins = allUsers.filter(user =>
Array.isArray(user.roles) && user.roles.includes("ROLE_ADMIN")
const totalAdmins = allUsers.filter(
(user) => Array.isArray(user.roles) && user.roles.includes("ROLE_ADMIN")
).length;
return {
@@ -211,4 +210,3 @@ export class AdminService {
}
}
}

View File

@@ -43,14 +43,14 @@ export abstract class BaseApiService {
const preferences = await PreferencesService.getPreferences();
return preferences.komgaMaxConcurrentRequests;
} catch (error) {
logger.error({ err: error }, 'Failed to get preferences for request queue');
logger.error({ err: error }, "Failed to get preferences for request queue");
return 5; // Valeur par défaut
}
});
this.requestQueueInitialized = true;
} catch (error) {
logger.error({ err: error }, 'Failed to initialize request queue');
logger.error({ err: error }, "Failed to initialize request queue");
}
}
@@ -69,7 +69,7 @@ export abstract class BaseApiService {
const preferences = await PreferencesService.getPreferences();
return preferences.circuitBreakerConfig;
} catch (error) {
logger.error({ err: error }, 'Failed to get preferences for circuit breaker');
logger.error({ err: error }, "Failed to get preferences for circuit breaker");
return {
threshold: 5,
timeout: 30000,
@@ -77,19 +77,16 @@ export abstract class BaseApiService {
};
}
});
this.circuitBreakerInitialized = true;
} catch (error) {
logger.error({ err: error }, 'Failed to initialize circuit breaker');
logger.error({ err: error }, "Failed to initialize circuit breaker");
}
}
protected static async getKomgaConfig(): Promise<AuthConfig> {
// Initialiser les services si ce n'est pas déjà fait
await Promise.all([
this.initializeRequestQueue(),
this.initializeCircuitBreaker(),
]);
await Promise.all([this.initializeRequestQueue(), this.initializeCircuitBreaker()]);
try {
const config: KomgaConfig | null = await ConfigDBService.getConfig();
if (!config) {
@@ -176,103 +173,114 @@ export abstract class BaseApiService {
}
}
const isDebug = process.env.KOMGA_DEBUG === 'true';
const isDebug = process.env.KOMGA_DEBUG === "true";
const startTime = isDebug ? Date.now() : 0;
if (isDebug) {
const queueStats = {
active: RequestQueueService.getActiveCount(),
queued: RequestQueueService.getQueueLength(),
};
logger.info({
url,
method: options.method || 'GET',
params,
isImage: options.isImage,
noJson: options.noJson,
queue: queueStats,
}, '🔵 Komga Request');
logger.info(
{
url,
method: options.method || "GET",
params,
isImage: options.isImage,
noJson: options.noJson,
queue: queueStats,
},
"🔵 Komga Request"
);
}
// Timeout réduit à 15 secondes pour éviter les blocages longs
const timeoutMs = 15000;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
// Utiliser le circuit breaker pour éviter de surcharger Komga
const response = await CircuitBreakerService.execute(async () => {
// Enqueue la requête pour limiter la concurrence
return await RequestQueueService.enqueue(async () => {
try {
return await fetch(url, {
headers,
...options,
signal: controller.signal,
// Configure undici connection timeouts
// @ts-ignore - undici-specific options not in standard fetch types
connectTimeout: timeoutMs,
bodyTimeout: timeoutMs,
headersTimeout: timeoutMs,
});
} catch (fetchError: any) {
// Gestion spécifique des erreurs DNS
if (fetchError?.cause?.code === 'EAI_AGAIN' || fetchError?.code === 'EAI_AGAIN') {
logger.error(`DNS resolution failed for ${url}. Retrying with different DNS settings...`);
// Retry avec des paramètres DNS différents
return await fetch(url, {
headers,
try {
return await fetch(url, {
headers,
...options,
signal: controller.signal,
// @ts-ignore - undici-specific options
connectTimeout: timeoutMs,
bodyTimeout: timeoutMs,
headersTimeout: timeoutMs,
// Force IPv4 si IPv6 pose problème
// @ts-ignore
family: 4,
});
}
// Retry automatique sur timeout de connexion (cold start)
if (fetchError?.cause?.code === 'UND_ERR_CONNECT_TIMEOUT') {
logger.info(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`);
return await fetch(url, {
headers,
...options,
signal: controller.signal,
// @ts-ignore - undici-specific options
// Configure undici connection timeouts
// @ts-ignore - undici-specific options not in standard fetch types
connectTimeout: timeoutMs,
bodyTimeout: timeoutMs,
headersTimeout: timeoutMs,
});
} catch (fetchError: any) {
// Gestion spécifique des erreurs DNS
if (fetchError?.cause?.code === "EAI_AGAIN" || fetchError?.code === "EAI_AGAIN") {
logger.error(
`DNS resolution failed for ${url}. Retrying with different DNS settings...`
);
// Retry avec des paramètres DNS différents
return await fetch(url, {
headers,
...options,
signal: controller.signal,
// @ts-ignore - undici-specific options
connectTimeout: timeoutMs,
bodyTimeout: timeoutMs,
headersTimeout: timeoutMs,
// Force IPv4 si IPv6 pose problème
// @ts-ignore
family: 4,
});
}
// Retry automatique sur timeout de connexion (cold start)
if (fetchError?.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
logger.info(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`);
return await fetch(url, {
headers,
...options,
signal: controller.signal,
// @ts-ignore - undici-specific options
connectTimeout: timeoutMs,
bodyTimeout: timeoutMs,
headersTimeout: timeoutMs,
});
}
throw fetchError;
}
throw fetchError;
}
});
});
clearTimeout(timeoutId);
if (isDebug) {
const duration = Date.now() - startTime;
logger.info({
url,
status: response.status,
duration: `${duration}ms`,
ok: response.ok,
}, '🟢 Komga Response');
logger.info(
{
url,
status: response.status,
duration: `${duration}ms`,
ok: response.ok,
},
"🟢 Komga Response"
);
}
if (!response.ok) {
if (isDebug) {
logger.error({
url,
status: response.status,
statusText: response.statusText,
}, '🔴 Komga Error Response');
logger.error(
{
url,
status: response.status,
statusText: response.statusText,
},
"🔴 Komga Error Response"
);
}
throw new AppError(ERROR_CODES.KOMGA.HTTP_ERROR, {
status: response.status,
@@ -283,20 +291,23 @@ export abstract class BaseApiService {
if (options.isImage) {
return response as T;
}
if (options.noJson) {
return undefined as T;
}
return response.json();
} catch (error) {
if (isDebug) {
const duration = Date.now() - startTime;
logger.error({
url,
error: error instanceof Error ? error.message : String(error),
duration: `${duration}ms`,
}, '🔴 Komga Request Failed');
logger.error(
{
url,
error: error instanceof Error ? error.message : String(error),
duration: `${duration}ms`,
},
"🔴 Komga Request Failed"
);
}
throw error;
} finally {

View File

@@ -17,7 +17,7 @@ export class BookService extends BaseApiService {
const maxAge = ttlConfig?.imageCacheMaxAge ?? 2592000;
return maxAge;
} catch (error) {
logger.error({ err: error }, '[ImageCache] Error fetching TTL config');
logger.error({ err: error }, "[ImageCache] Error fetching TTL config");
return 2592000; // 30 jours par défaut en cas d'erreur
}
}
@@ -29,7 +29,7 @@ export class BookService extends BaseApiService {
// Récupération parallèle des détails du tome et des pages
const [book, pages] = await Promise.all([
this.fetchFromApi<KomgaBook>({ path: `books/${bookId}` }),
this.fetchFromApi<{ number: number }[]>({ path: `books/${bookId}/pages` })
this.fetchFromApi<{ number: number }[]>({ path: `books/${bookId}/pages` }),
]);
return {
@@ -107,15 +107,15 @@ export class BookService extends BaseApiService {
const response: ImageResponse = await ImageService.getImage(
`books/${bookId}/pages/${adjustedPageNumber}?zero_based=true`
);
// Convertir le Buffer Node.js en ArrayBuffer proprement
const arrayBuffer = response.buffer.buffer.slice(
response.buffer.byteOffset,
response.buffer.byteOffset + response.buffer.byteLength
) as ArrayBuffer;
const maxAge = await this.getImageCacheMaxAge();
return new Response(arrayBuffer, {
headers: {
"Content-Type": response.contentType || "image/jpeg",
@@ -165,7 +165,7 @@ export class BookService extends BaseApiService {
`books/${bookId}/pages/${pageNumber}/thumbnail?zero_based=true`
);
const maxAge = await this.getImageCacheMaxAge();
return new Response(response.buffer.buffer as ArrayBuffer, {
headers: {
"Content-Type": response.contentType || "image/jpeg",
@@ -184,14 +184,16 @@ export class BookService extends BaseApiService {
static async getRandomBookFromLibraries(libraryIds: string[]): Promise<string> {
try {
if (libraryIds.length === 0) {
throw new AppError(ERROR_CODES.LIBRARY.NOT_FOUND, { message: "Aucune bibliothèque sélectionnée" });
throw new AppError(ERROR_CODES.LIBRARY.NOT_FOUND, {
message: "Aucune bibliothèque sélectionnée",
});
}
const { LibraryService } = await import("./library.service");
// Essayer d'abord d'utiliser le cache des bibliothèques
const allSeriesFromCache: Series[] = [];
for (const libraryId of libraryIds) {
try {
// Essayer de récupérer les séries depuis le cache (rapide si en cache)
@@ -219,12 +221,14 @@ export class BookService extends BaseApiService {
// Si pas de cache, faire une requête légère : prendre une page de séries d'une bibliothèque au hasard
const randomLibraryIndex = Math.floor(Math.random() * libraryIds.length);
const randomLibraryId = libraryIds[randomLibraryIndex];
// Récupérer juste une page de séries (pas toutes)
const seriesResponse = await LibraryService.getLibrarySeries(randomLibraryId, 0, 20);
if (seriesResponse.content.length === 0) {
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, { message: "Aucune série trouvée dans les bibliothèques sélectionnées" });
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {
message: "Aucune série trouvée dans les bibliothèques sélectionnées",
});
}
// Choisir une série au hasard parmi celles récupérées
@@ -235,7 +239,9 @@ export class BookService extends BaseApiService {
const books = await SeriesService.getAllSeriesBooks(randomSeries.id);
if (books.length === 0) {
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, { message: "Aucun livre trouvé dans la série" });
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {
message: "Aucun livre trouvé dans la série",
});
}
const randomBookIndex = Math.floor(Math.random() * books.length);

View File

@@ -6,7 +6,7 @@ import type { CircuitBreakerConfig } from "@/types/preferences";
import logger from "@/lib/logger";
interface CircuitBreakerState {
state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
state: "CLOSED" | "OPEN" | "HALF_OPEN";
failureCount: number;
lastFailureTime: number;
nextAttemptTime: number;
@@ -14,7 +14,7 @@ interface CircuitBreakerState {
class CircuitBreaker {
private state: CircuitBreakerState = {
state: 'CLOSED',
state: "CLOSED",
failureCount: 0,
lastFailureTime: 0,
nextAttemptTime: 0,
@@ -48,7 +48,7 @@ class CircuitBreaker {
resetTimeout: prefConfig.resetTimeout ?? 60000,
};
} catch (error) {
logger.error({ err: error }, 'Error getting circuit breaker config from preferences');
logger.error({ err: error }, "Error getting circuit breaker config from preferences");
return this.config;
}
}
@@ -57,12 +57,12 @@ class CircuitBreaker {
async execute<T>(operation: () => Promise<T>): Promise<T> {
const config = await this.getCurrentConfig();
if (this.state.state === 'OPEN') {
if (this.state.state === "OPEN") {
if (Date.now() < this.state.nextAttemptTime) {
throw new Error('Circuit breaker is OPEN - Komga service unavailable');
throw new Error("Circuit breaker is OPEN - Komga service unavailable");
}
this.state.state = 'HALF_OPEN';
this.state.state = "HALF_OPEN";
}
try {
@@ -76,10 +76,10 @@ class CircuitBreaker {
}
private onSuccess(): void {
if (this.state.state === 'HALF_OPEN') {
if (this.state.state === "HALF_OPEN") {
this.state.failureCount = 0;
this.state.state = 'CLOSED';
logger.info('[CIRCUIT-BREAKER] ✅ Circuit closed - Komga recovered');
this.state.state = "CLOSED";
logger.info("[CIRCUIT-BREAKER] ✅ Circuit closed - Komga recovered");
}
}
@@ -88,9 +88,11 @@ class CircuitBreaker {
this.state.lastFailureTime = Date.now();
if (this.state.failureCount >= config.failureThreshold) {
this.state.state = 'OPEN';
this.state.state = "OPEN";
this.state.nextAttemptTime = Date.now() + config.resetTimeout;
logger.warn(`[CIRCUIT-BREAKER] 🔴 Circuit OPEN - Komga failing (${this.state.failureCount} failures, reset in ${config.resetTimeout}ms)`);
logger.warn(
`[CIRCUIT-BREAKER] 🔴 Circuit OPEN - Komga failing (${this.state.failureCount} failures, reset in ${config.resetTimeout}ms)`
);
}
}
@@ -100,12 +102,12 @@ class CircuitBreaker {
reset(): void {
this.state = {
state: 'CLOSED',
state: "CLOSED",
failureCount: 0,
lastFailureTime: 0,
nextAttemptTime: 0,
};
logger.info('[CIRCUIT-BREAKER] 🔄 Circuit reset');
logger.info("[CIRCUIT-BREAKER] 🔄 Circuit reset");
}
}

View File

@@ -2,7 +2,11 @@ import prisma from "@/lib/prisma";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { UserPreferences, BackgroundPreferences, CircuitBreakerConfig } from "@/types/preferences";
import type {
UserPreferences,
BackgroundPreferences,
CircuitBreakerConfig,
} from "@/types/preferences";
import { defaultPreferences } from "@/types/preferences";
import type { User } from "@/types/komga";
import type { Prisma } from "@prisma/client";
@@ -20,17 +24,17 @@ export class PreferencesService {
try {
const user = await this.getCurrentUser();
const userId = parseInt(user.id, 10);
const preferences = await prisma.preferences.findUnique({
where: { userId },
});
if (!preferences) {
return { ...defaultPreferences };
}
const displayMode = preferences.displayMode as UserPreferences["displayMode"];
return {
showThumbnails: preferences.showThumbnails,
cacheMode: preferences.cacheMode as "memory" | "file",
@@ -57,16 +61,21 @@ export class PreferencesService {
try {
const user = await this.getCurrentUser();
const userId = parseInt(user.id, 10);
const updateData: Record<string, any> = {};
if (preferences.showThumbnails !== undefined) updateData.showThumbnails = preferences.showThumbnails;
if (preferences.showThumbnails !== undefined)
updateData.showThumbnails = preferences.showThumbnails;
if (preferences.cacheMode !== undefined) updateData.cacheMode = preferences.cacheMode;
if (preferences.showOnlyUnread !== undefined) updateData.showOnlyUnread = preferences.showOnlyUnread;
if (preferences.showOnlyUnread !== undefined)
updateData.showOnlyUnread = preferences.showOnlyUnread;
if (preferences.displayMode !== undefined) updateData.displayMode = preferences.displayMode;
if (preferences.background !== undefined) updateData.background = preferences.background;
if (preferences.komgaMaxConcurrentRequests !== undefined) updateData.komgaMaxConcurrentRequests = preferences.komgaMaxConcurrentRequests;
if (preferences.readerPrefetchCount !== undefined) updateData.readerPrefetchCount = preferences.readerPrefetchCount;
if (preferences.circuitBreakerConfig !== undefined) updateData.circuitBreakerConfig = preferences.circuitBreakerConfig;
if (preferences.komgaMaxConcurrentRequests !== undefined)
updateData.komgaMaxConcurrentRequests = preferences.komgaMaxConcurrentRequests;
if (preferences.readerPrefetchCount !== undefined)
updateData.readerPrefetchCount = preferences.readerPrefetchCount;
if (preferences.circuitBreakerConfig !== undefined)
updateData.circuitBreakerConfig = preferences.circuitBreakerConfig;
const updatedPreferences = await prisma.preferences.upsert({
where: { userId },
@@ -77,8 +86,10 @@ export class PreferencesService {
cacheMode: preferences.cacheMode ?? defaultPreferences.cacheMode,
showOnlyUnread: preferences.showOnlyUnread ?? defaultPreferences.showOnlyUnread,
displayMode: preferences.displayMode ?? defaultPreferences.displayMode,
background: (preferences.background ?? defaultPreferences.background) as unknown as Prisma.InputJsonValue,
circuitBreakerConfig: (preferences.circuitBreakerConfig ?? defaultPreferences.circuitBreakerConfig) as unknown as Prisma.InputJsonValue,
background: (preferences.background ??
defaultPreferences.background) as unknown as Prisma.InputJsonValue,
circuitBreakerConfig: (preferences.circuitBreakerConfig ??
defaultPreferences.circuitBreakerConfig) as unknown as Prisma.InputJsonValue,
komgaMaxConcurrentRequests: preferences.komgaMaxConcurrentRequests ?? 5,
readerPrefetchCount: preferences.readerPrefetchCount ?? 5,
},
@@ -92,7 +103,8 @@ export class PreferencesService {
background: updatedPreferences.background as unknown as BackgroundPreferences,
komgaMaxConcurrentRequests: updatedPreferences.komgaMaxConcurrentRequests,
readerPrefetchCount: updatedPreferences.readerPrefetchCount,
circuitBreakerConfig: updatedPreferences.circuitBreakerConfig as unknown as CircuitBreakerConfig,
circuitBreakerConfig:
updatedPreferences.circuitBreakerConfig as unknown as CircuitBreakerConfig,
};
} catch (error) {
if (error instanceof AppError) {

View File

@@ -67,4 +67,3 @@ class RequestDeduplicationService {
// Singleton instance
export const requestDeduplicationService = new RequestDeduplicationService();

View File

@@ -29,7 +29,7 @@ class RequestMonitor {
private checkThresholds(): void {
const count = this.activeRequests;
if (count >= this.thresholds.critical) {
logger.warn(`[REQUEST-MONITOR] 🔴 CRITICAL concurrency: ${count} active requests`);
} else if (count >= this.thresholds.high) {
@@ -42,5 +42,3 @@ class RequestMonitor {
// Singleton instance
export const RequestMonitorService = new RequestMonitor();

View File

@@ -36,7 +36,7 @@ class RequestQueue {
try {
return await this.getMaxConcurrent();
} catch (error) {
logger.error({ err: error }, 'Error getting maxConcurrent from preferences, using default');
logger.error({ err: error }, "Error getting maxConcurrent from preferences, using default");
return this.maxConcurrent;
}
}
@@ -47,17 +47,17 @@ class RequestQueue {
return new Promise<T>((resolve, reject) => {
// Limiter la taille de la queue pour éviter l'accumulation
if (this.queue.length >= 50) {
reject(new Error('Request queue is full - Komga may be overloaded'));
reject(new Error("Request queue is full - Komga may be overloaded"));
return;
}
this.queue.push({ execute, resolve, reject });
this.processQueue();
});
}
private async delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
private async processQueue(): Promise<void> {
@@ -77,7 +77,7 @@ class RequestQueue {
try {
// Délai adaptatif : plus long si la queue est pleine
// Désactivé en mode debug pour ne pas ralentir les tests
const isDebug = process.env.KOMGA_DEBUG === 'true';
const isDebug = process.env.KOMGA_DEBUG === "true";
if (!isDebug) {
const delayMs = this.queue.length > 10 ? 500 : 200;
await this.delay(delayMs);
@@ -107,4 +107,3 @@ class RequestQueue {
// Singleton instance - Par défaut limite à 5 requêtes simultanées
export const RequestQueueService = new RequestQueue(5);

View File

@@ -20,7 +20,7 @@ export class SeriesService extends BaseApiService {
const maxAge = ttlConfig?.imageCacheMaxAge ?? 2592000;
return maxAge;
} catch (error) {
logger.error({ err: error }, '[ImageCache] Error fetching TTL config');
logger.error({ err: error }, "[ImageCache] Error fetching TTL config");
return 2592000; // 30 jours par défaut en cas d'erreur
}
}

View File

@@ -94,7 +94,10 @@ class ServerCacheService {
try {
fs.rmdirSync(itemPath);
} catch (error) {
logger.error({ err: error, path: itemPath }, `Could not remove directory ${itemPath}`);
logger.error(
{ err: error, path: itemPath },
`Could not remove directory ${itemPath}`
);
isEmpty = false;
}
} else {
@@ -393,7 +396,10 @@ class ServerCacheService {
try {
fs.rmdirSync(itemPath);
} catch (error) {
logger.error({ err: error, path: itemPath }, `Could not remove directory ${itemPath}`);
logger.error(
{ err: error, path: itemPath },
`Could not remove directory ${itemPath}`
);
}
} else {
try {
@@ -435,16 +441,18 @@ class ServerCacheService {
const cacheKey = `${user.id}-${key}`;
const cachedResult = this.getStale(cacheKey);
if (cachedResult !== null) {
const { data, isStale } = cachedResult;
const endTime = performance.now();
// Debug logging
if (process.env.CACHE_DEBUG === 'true') {
const icon = isStale ? '⚠️' : '✅';
const status = isStale ? 'STALE' : 'HIT';
logger.debug(`${icon} [CACHE ${status}] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
if (process.env.CACHE_DEBUG === "true") {
const icon = isStale ? "⚠️" : "✅";
const status = isStale ? "STALE" : "HIT";
logger.debug(
`${icon} [CACHE ${status}] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`
);
}
// Si le cache est expiré, revalider en background sans bloquer la réponse
@@ -457,19 +465,19 @@ class ServerCacheService {
}
// Pas de cache du tout, fetch normalement
if (process.env.CACHE_DEBUG === 'true') {
if (process.env.CACHE_DEBUG === "true") {
logger.debug(`❌ [CACHE MISS] ${key} | ${type}`);
}
try {
const data = await fetcher();
this.set(cacheKey, data, type);
const endTime = performance.now();
if (process.env.CACHE_DEBUG === 'true') {
if (process.env.CACHE_DEBUG === "true") {
logger.debug(`💾 [CACHE SET] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
}
return data;
} catch (error) {
throw error;
@@ -489,10 +497,12 @@ class ServerCacheService {
const startTime = performance.now();
const data = await fetcher();
this.set(cacheKey, data, type);
if (process.env.CACHE_DEBUG === 'true') {
if (process.env.CACHE_DEBUG === "true") {
const endTime = performance.now();
logger.debug(`🔄 [CACHE REVALIDATE] ${debugKey} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
logger.debug(
`🔄 [CACHE REVALIDATE] ${debugKey} | ${type} | ${(endTime - startTime).toFixed(2)}ms`
);
}
} catch (error) {
logger.error({ err: error, key: debugKey }, `🔴 [CACHE REVALIDATE ERROR] ${debugKey}`);

View File

@@ -53,10 +53,7 @@ export class UserService {
}
}
static async changePassword(
currentPassword: string,
newPassword: string
): Promise<void> {
static async changePassword(currentPassword: string, newPassword: string): Promise<void> {
try {
const currentUser = await getCurrentUser();
if (!currentUser) {
@@ -128,4 +125,3 @@ export class UserService {
}
}
}

View File

@@ -8,4 +8,3 @@ export function getImageUrl(type: "series" | "book", id: string) {
}
return `/api/komga/images/books/${id}/thumbnail`;
}