chore: update various components and services for improved functionality and consistency, including formatting adjustments and minor refactors
This commit is contained in:
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -67,4 +67,3 @@ class RequestDeduplicationService {
|
||||
|
||||
// Singleton instance
|
||||
export const requestDeduplicationService = new RequestDeduplicationService();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user