From 7cc72dc13de5a35decd4c6599ee6e05c2e7a3d5f Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 24 Oct 2025 17:50:58 +0200 Subject: [PATCH] feat: implement advanced settings for user preferences, allowing configuration of max concurrent requests, reader prefetch count, and circuit breaker settings --- prisma/schema.prisma | 2 +- src/components/reader/PhotoswipeReader.tsx | 4 +- src/components/reader/hooks/useImageLoader.ts | 4 +- src/components/settings/AdvancedSettings.tsx | 325 ++++++++++++++++++ src/components/settings/ClientSettings.tsx | 2 + src/components/ui/separator.tsx | 27 ++ src/components/ui/skeleton.tsx | 16 + src/i18n/messages/en/common.json | 31 ++ src/i18n/messages/fr/common.json | 31 ++ src/lib/services/base-api.service.ts | 65 ++++ src/lib/services/circuit-breaker.service.ts | 47 ++- src/lib/services/preferences.service.ts | 17 +- src/lib/services/request-queue.service.ts | 39 ++- src/types/preferences.ts | 16 + 14 files changed, 601 insertions(+), 25 deletions(-) create mode 100644 src/components/settings/AdvancedSettings.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/skeleton.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 484ff87..ed1d2ed 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -68,7 +68,7 @@ model Preferences { showOnlyUnread Boolean @default(false) displayMode Json background Json - komgaMaxConcurrentRequests Int @default(2) + komgaMaxConcurrentRequests Int @default(5) circuitBreakerConfig Json readerPrefetchCount Int @default(5) createdAt DateTime @default(now()) diff --git a/src/components/reader/PhotoswipeReader.tsx b/src/components/reader/PhotoswipeReader.tsx index f63d8c0..fc14b8b 100644 --- a/src/components/reader/PhotoswipeReader.tsx +++ b/src/components/reader/PhotoswipeReader.tsx @@ -16,8 +16,10 @@ import { NavigationBar } from "./components/NavigationBar"; import { EndOfSeriesModal } from "./components/EndOfSeriesModal"; import { PageDisplay } from "./components/PageDisplay"; import { ReaderContainer } from "./components/ReaderContainer"; +import { usePreferences } from "@/contexts/PreferencesContext"; export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderProps) { + const { preferences } = usePreferences(); const [showControls, setShowControls] = useState(false); const [showThumbnails, setShowThumbnails] = useState(false); const lastClickTimeRef = useRef(0); @@ -30,7 +32,7 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP const { loadedImages, imageBlobUrls, prefetchPages, prefetchNextBook, handleForceReload, getPageUrl, prefetchCount } = useImageLoader({ bookId: book.id, pages, - prefetchCount: 5, + prefetchCount: preferences.readerPrefetchCount, nextBook: nextBook ? { id: nextBook.id, pages: [] } : null }); const { currentPage, showEndMessage, navigateToPage, handlePreviousPage, handleNextPage } = usePageNavigation({ diff --git a/src/components/reader/hooks/useImageLoader.ts b/src/components/reader/hooks/useImageLoader.ts index 7d90fda..536f664 100644 --- a/src/components/reader/hooks/useImageLoader.ts +++ b/src/components/reader/hooks/useImageLoader.ts @@ -10,11 +10,11 @@ type ImageKey = number | string; // Support both numeric pages and prefixed keys interface UseImageLoaderProps { bookId: string; pages: number[]; - prefetchCount?: number; // Nombre de pages à précharger (défaut: 2) + prefetchCount?: number; // Nombre de pages à précharger (défaut: 5) nextBook?: { id: string; pages: number[] } | null; // Livre suivant pour prefetch } -export function useImageLoader({ bookId, pages: _pages, prefetchCount = 2, nextBook }: UseImageLoaderProps) { +export function useImageLoader({ bookId, pages: _pages, prefetchCount = 5, nextBook }: UseImageLoaderProps) { const [loadedImages, setLoadedImages] = useState>({}); const [imageBlobUrls, setImageBlobUrls] = useState>({}); const loadedImagesRef = useRef(loadedImages); diff --git a/src/components/settings/AdvancedSettings.tsx b/src/components/settings/AdvancedSettings.tsx new file mode 100644 index 0000000..993720e --- /dev/null +++ b/src/components/settings/AdvancedSettings.tsx @@ -0,0 +1,325 @@ +import { useTranslate } from "@/hooks/useTranslate"; +import { usePreferences } from "@/contexts/PreferencesContext"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { useToast } from "@/components/ui/use-toast"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Activity, ImageIcon, Shield, Info } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; + +export function AdvancedSettings() { + const { t } = useTranslate(); + const { toast } = useToast(); + const { preferences, updatePreferences } = usePreferences(); + + const handleMaxConcurrentChange = async (value: number) => { + try { + await updatePreferences({ + komgaMaxConcurrentRequests: value, + }); + toast({ + title: t("settings.title"), + description: t("settings.komga.messages.configSaved"), + }); + } catch (error) { + console.error("Erreur:", error); + toast({ + variant: "destructive", + title: t("settings.error.title"), + description: t("settings.error.message"), + }); + } + }; + + const handlePrefetchChange = async (value: number) => { + try { + await updatePreferences({ + readerPrefetchCount: value, + }); + toast({ + title: t("settings.title"), + description: t("settings.komga.messages.configSaved"), + }); + } catch (error) { + console.error("Erreur:", error); + toast({ + variant: "destructive", + title: t("settings.error.title"), + description: t("settings.error.message"), + }); + } + }; + + const handleCircuitBreakerChange = async (field: string, value: number) => { + try { + await updatePreferences({ + circuitBreakerConfig: { + ...preferences.circuitBreakerConfig, + [field]: value, + }, + }); + toast({ + title: t("settings.title"), + description: t("settings.komga.messages.configSaved"), + }); + } catch (error) { + console.error("Erreur:", error); + toast({ + variant: "destructive", + title: t("settings.error.title"), + description: t("settings.error.message"), + }); + } + }; + + return ( +
+ {/* Performance Settings */} + + +
+ + Performance +
+ + Optimisez les performances et la réactivité de l'application + +
+ + {/* Concurrent Requests */} +
+
+
+
+ + + {preferences.komgaMaxConcurrentRequests} + +
+

+ {t("settings.advanced.maxConcurrentRequests.description")} +

+
+
+
+ handleMaxConcurrentChange(parseInt(e.target.value))} + className="flex-1 cursor-pointer" + /> + handleMaxConcurrentChange(parseInt(e.target.value) || 1)} + className="w-20" + /> +
+
+ +
+ + {/* Reader Prefetch Count */} +
+
+
+
+ + + + {preferences.readerPrefetchCount} + +
+

+ {t("settings.advanced.prefetchCount.description")} +

+
+
+
+ handlePrefetchChange(parseInt(e.target.value))} + className="flex-1 cursor-pointer" + /> + handlePrefetchChange(parseInt(e.target.value) || 0)} + className="w-20" + /> +
+
+ + + + {/* Circuit Breaker Configuration */} + + +
+ + {t("settings.advanced.circuitBreaker.title")} +
+ + {t("settings.advanced.circuitBreaker.description")} + +
+ + {/* Threshold */} +
+
+
+
+ + + {preferences.circuitBreakerConfig.threshold} + +
+

+ {t("settings.advanced.circuitBreaker.threshold.description")} +

+
+
+
+ + handleCircuitBreakerChange("threshold", parseInt(e.target.value)) + } + className="flex-1 cursor-pointer" + /> + + handleCircuitBreakerChange("threshold", parseInt(e.target.value) || 5) + } + className="w-20" + /> +
+
+ +
+ + {/* Timeout */} +
+
+
+
+ + + {(preferences.circuitBreakerConfig.timeout ?? 30000) / 1000}s + +
+

+ {t("settings.advanced.circuitBreaker.timeout.description")} +

+
+
+
+ + handleCircuitBreakerChange("timeout", parseInt(e.target.value)) + } + className="flex-1 cursor-pointer" + /> + + handleCircuitBreakerChange("timeout", (parseInt(e.target.value) || 30) * 1000) + } + className="w-20" + /> +
+
+ + {t("settings.advanced.circuitBreaker.timeout.unit")} +
+
+ +
+ + {/* Reset Timeout */} +
+
+
+
+ + + {(preferences.circuitBreakerConfig.resetTimeout ?? 60000) / 1000}s + +
+

+ {t("settings.advanced.circuitBreaker.resetTimeout.description")} +

+
+
+
+ + handleCircuitBreakerChange("resetTimeout", parseInt(e.target.value)) + } + className="flex-1 cursor-pointer" + /> + + handleCircuitBreakerChange("resetTimeout", (parseInt(e.target.value) || 60) * 1000) + } + className="w-20" + /> +
+
+ + {t("settings.advanced.circuitBreaker.resetTimeout.unit")} +
+
+ + +
+ ); +} diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx index d25702d..6ae1b82 100644 --- a/src/components/settings/ClientSettings.tsx +++ b/src/components/settings/ClientSettings.tsx @@ -6,6 +6,7 @@ import { DisplaySettings } from "./DisplaySettings"; import { KomgaSettings } from "./KomgaSettings"; import { CacheSettings } from "./CacheSettings"; import { BackgroundSettings } from "./BackgroundSettings"; +import { AdvancedSettings } from "./AdvancedSettings"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Monitor, Network, HardDrive } from "lucide-react"; @@ -44,6 +45,7 @@ export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettin + diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..a9087c3 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +interface SeparatorProps extends React.HTMLAttributes { + orientation?: "horizontal" | "vertical"; + decorative?: boolean; +} + +const Separator = React.forwardRef( + ({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( +
+ ) +); +Separator.displayName = "Separator"; + +export { Separator }; + diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..2c8324d --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,16 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; + diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json index 938c180..0d6f89a 100644 --- a/src/i18n/messages/en/common.json +++ b/src/i18n/messages/en/common.json @@ -102,6 +102,37 @@ "save": "Save" } }, + "advanced": { + "title": "Advanced Settings", + "description": "Configure advanced performance and reliability settings.", + "save": "Save settings", + "maxConcurrentRequests": { + "label": "Max Concurrent Requests", + "description": "Maximum number of simultaneous requests to Komga server (1-10)" + }, + "prefetchCount": { + "label": "Reader Prefetch Count", + "description": "Number of pages to preload in the reader (0-20)" + }, + "circuitBreaker": { + "title": "Circuit Breaker", + "description": "Automatic protection against server overload", + "threshold": { + "label": "Failure Threshold", + "description": "Number of consecutive failures before opening the circuit (1-20)" + }, + "timeout": { + "label": "Request Timeout", + "description": "Maximum wait time for a request before considering it failed", + "unit": "milliseconds (1000ms = 1 second)" + }, + "resetTimeout": { + "label": "Reset Timeout", + "description": "Time to wait before attempting to close the circuit", + "unit": "milliseconds (1000ms = 1 second)" + } + } + }, "error": { "title": "Error", "message": "An error occurred while updating preferences" diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json index a72986b..7ba289b 100644 --- a/src/i18n/messages/fr/common.json +++ b/src/i18n/messages/fr/common.json @@ -102,6 +102,37 @@ "save": "Enregistrer" } }, + "advanced": { + "title": "Paramètres avancés", + "description": "Configurez les paramètres avancés de performance et de fiabilité.", + "save": "Enregistrer les paramètres", + "maxConcurrentRequests": { + "label": "Requêtes simultanées max", + "description": "Nombre maximum de requêtes simultanées vers le serveur Komga (1-10)" + }, + "prefetchCount": { + "label": "Préchargement du lecteur", + "description": "Nombre de pages à précharger dans le lecteur (0-20)" + }, + "circuitBreaker": { + "title": "Disjoncteur", + "description": "Protection automatique contre la surcharge du serveur", + "threshold": { + "label": "Seuil d'échec", + "description": "Nombre d'échecs consécutifs avant ouverture du circuit (1-20)" + }, + "timeout": { + "label": "Délai d'expiration", + "description": "Temps d'attente maximum pour une requête avant de la considérer comme échouée", + "unit": "millisecondes (1000ms = 1 seconde)" + }, + "resetTimeout": { + "label": "Délai de réinitialisation", + "description": "Temps d'attente avant de tenter de fermer le circuit", + "unit": "millisecondes (1000ms = 1 seconde)" + } + } + }, "error": { "title": "Erreur", "message": "Une erreur est survenue lors de la mise à jour des préférences" diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts index 2792ee6..502e9c8 100644 --- a/src/lib/services/base-api.service.ts +++ b/src/lib/services/base-api.service.ts @@ -9,6 +9,7 @@ import type { ServerCacheService } from "./server-cache.service"; import { RequestMonitorService } from "./request-monitor.service"; import { RequestQueueService } from "./request-queue.service"; import { CircuitBreakerService } from "./circuit-breaker.service"; +import { PreferencesService } from "./preferences.service"; export type { CacheType }; @@ -23,7 +24,71 @@ interface KomgaUrlBuilder { } export abstract class BaseApiService { + private static requestQueueInitialized = false; + private static circuitBreakerInitialized = false; + + /** + * Initialise le RequestQueueService avec les préférences de l'utilisateur + */ + private static async initializeRequestQueue(): Promise { + if (this.requestQueueInitialized) { + return; + } + + try { + // Configurer le getter qui récupère dynamiquement la valeur depuis les préférences + RequestQueueService.setMaxConcurrentGetter(async () => { + try { + const preferences = await PreferencesService.getPreferences(); + return preferences.komgaMaxConcurrentRequests; + } catch (error) { + console.error('Failed to get preferences for request queue:', error); + return 5; // Valeur par défaut + } + }); + + this.requestQueueInitialized = true; + } catch (error) { + console.error('Failed to initialize request queue:', error); + } + } + + /** + * Initialise le CircuitBreakerService avec les préférences de l'utilisateur + */ + private static async initializeCircuitBreaker(): Promise { + if (this.circuitBreakerInitialized) { + return; + } + + try { + // Configurer le getter qui récupère dynamiquement la config depuis les préférences + CircuitBreakerService.setConfigGetter(async () => { + try { + const preferences = await PreferencesService.getPreferences(); + return preferences.circuitBreakerConfig; + } catch (error) { + console.error('Failed to get preferences for circuit breaker:', error); + return { + threshold: 5, + timeout: 30000, + resetTimeout: 60000, + }; + } + }); + + this.circuitBreakerInitialized = true; + } catch (error) { + console.error('Failed to initialize circuit breaker:', error); + } + } + protected static async getKomgaConfig(): Promise { + // Initialiser les services si ce n'est pas déjà fait + await Promise.all([ + this.initializeRequestQueue(), + this.initializeCircuitBreaker(), + ]); try { const config: KomgaConfig | null = await ConfigDBService.getConfig(); if (!config) { diff --git a/src/lib/services/circuit-breaker.service.ts b/src/lib/services/circuit-breaker.service.ts index 703e65e..93e8595 100644 --- a/src/lib/services/circuit-breaker.service.ts +++ b/src/lib/services/circuit-breaker.service.ts @@ -2,6 +2,8 @@ * Circuit Breaker pour éviter de surcharger Komga quand il est défaillant * Évite l'effet avalanche en coupant les requêtes vers un service défaillant */ +import type { CircuitBreakerConfig } from "@/types/preferences"; + interface CircuitBreakerState { state: 'CLOSED' | 'OPEN' | 'HALF_OPEN'; failureCount: number; @@ -17,13 +19,44 @@ class CircuitBreaker { nextAttemptTime: 0, }; - private readonly config = { + private config = { failureThreshold: 5, // Nombre d'échecs avant ouverture recoveryTimeout: 30000, // 30s avant tentative de récupération - successThreshold: 3, // Nombre de succès pour fermer le circuit + resetTimeout: 60000, // Délai de reset après échec }; + private getConfigFromPreferences: (() => Promise) | null = null; + + /** + * Configure une fonction pour récupérer dynamiquement la config depuis les préférences + */ + setConfigGetter(getter: () => Promise): void { + this.getConfigFromPreferences = getter; + } + + /** + * Récupère la config actuelle, soit depuis les préférences, soit depuis les valeurs par défaut + */ + private async getCurrentConfig(): Promise { + if (this.getConfigFromPreferences) { + try { + const prefConfig = await this.getConfigFromPreferences(); + return { + failureThreshold: prefConfig.threshold ?? 5, + recoveryTimeout: prefConfig.timeout ?? 30000, + resetTimeout: prefConfig.resetTimeout ?? 60000, + }; + } catch (error) { + console.error('Error getting circuit breaker config from preferences:', error); + return this.config; + } + } + return this.config; + } + async execute(operation: () => Promise): Promise { + const config = await this.getCurrentConfig(); + if (this.state.state === 'OPEN') { if (Date.now() < this.state.nextAttemptTime) { throw new Error('Circuit breaker is OPEN - Komga service unavailable'); @@ -36,7 +69,7 @@ class CircuitBreaker { this.onSuccess(); return result; } catch (error) { - this.onFailure(); + await this.onFailure(config); throw error; } } @@ -50,14 +83,14 @@ class CircuitBreaker { } } - private onFailure(): void { + private async onFailure(config: typeof this.config): Promise { this.state.failureCount++; this.state.lastFailureTime = Date.now(); - if (this.state.failureCount >= this.config.failureThreshold) { + if (this.state.failureCount >= config.failureThreshold) { this.state.state = 'OPEN'; - this.state.nextAttemptTime = Date.now() + this.config.recoveryTimeout; - console.warn(`[CIRCUIT-BREAKER] 🔴 Circuit OPEN - Komga failing (${this.state.failureCount} failures)`); + this.state.nextAttemptTime = Date.now() + config.resetTimeout; + console.warn(`[CIRCUIT-BREAKER] 🔴 Circuit OPEN - Komga failing (${this.state.failureCount} failures, reset in ${config.resetTimeout}ms)`); } } diff --git a/src/lib/services/preferences.service.ts b/src/lib/services/preferences.service.ts index 6e9a23a..844b475 100644 --- a/src/lib/services/preferences.service.ts +++ b/src/lib/services/preferences.service.ts @@ -2,7 +2,7 @@ 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 } 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"; @@ -35,6 +35,9 @@ export class PreferencesService { showOnlyUnread: preferences.showOnlyUnread, displayMode: preferences.displayMode as UserPreferences["displayMode"], background: preferences.background as unknown as BackgroundPreferences, + komgaMaxConcurrentRequests: preferences.komgaMaxConcurrentRequests, + readerPrefetchCount: preferences.readerPrefetchCount, + circuitBreakerConfig: preferences.circuitBreakerConfig as unknown as CircuitBreakerConfig, }; } catch (error) { if (error instanceof AppError) { @@ -55,6 +58,9 @@ export class PreferencesService { 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; const updatedPreferences = await prisma.preferences.upsert({ where: { userId }, @@ -66,9 +72,9 @@ export class PreferencesService { showOnlyUnread: preferences.showOnlyUnread ?? defaultPreferences.showOnlyUnread, displayMode: preferences.displayMode ?? defaultPreferences.displayMode, background: (preferences.background ?? defaultPreferences.background) as unknown as Prisma.InputJsonValue, - circuitBreakerConfig: {}, - komgaMaxConcurrentRequests: 2, - readerPrefetchCount: 5, + circuitBreakerConfig: (preferences.circuitBreakerConfig ?? defaultPreferences.circuitBreakerConfig) as unknown as Prisma.InputJsonValue, + komgaMaxConcurrentRequests: preferences.komgaMaxConcurrentRequests ?? 5, + readerPrefetchCount: preferences.readerPrefetchCount ?? 5, }, }); @@ -78,6 +84,9 @@ export class PreferencesService { showOnlyUnread: updatedPreferences.showOnlyUnread, displayMode: updatedPreferences.displayMode as UserPreferences["displayMode"], background: updatedPreferences.background as unknown as BackgroundPreferences, + komgaMaxConcurrentRequests: updatedPreferences.komgaMaxConcurrentRequests, + readerPrefetchCount: updatedPreferences.readerPrefetchCount, + circuitBreakerConfig: updatedPreferences.circuitBreakerConfig as unknown as CircuitBreakerConfig, }; } catch (error) { if (error instanceof AppError) { diff --git a/src/lib/services/request-queue.service.ts b/src/lib/services/request-queue.service.ts index 1a1b6f5..512b46d 100644 --- a/src/lib/services/request-queue.service.ts +++ b/src/lib/services/request-queue.service.ts @@ -13,11 +13,33 @@ class RequestQueue { private queue: QueuedRequest[] = []; private activeCount = 0; private maxConcurrent: number; + private getMaxConcurrent: (() => Promise) | null = null; constructor(maxConcurrent?: number) { - // Lire depuis env ou utiliser la valeur par défaut - const envValue = process.env.KOMGA_MAX_CONCURRENT_REQUESTS; - this.maxConcurrent = maxConcurrent ?? (envValue ? parseInt(envValue, 10) : 5); + // Valeur par défaut + this.maxConcurrent = maxConcurrent ?? 5; + } + + /** + * Configure une fonction pour récupérer dynamiquement le max concurrent depuis les préférences + */ + setMaxConcurrentGetter(getter: () => Promise): void { + this.getMaxConcurrent = getter; + } + + /** + * Récupère la valeur de maxConcurrent, soit depuis les préférences, soit depuis la valeur fixe + */ + private async getCurrentMaxConcurrent(): Promise { + if (this.getMaxConcurrent) { + try { + return await this.getMaxConcurrent(); + } catch (error) { + console.error('Error getting maxConcurrent from preferences, using default:', error); + return this.maxConcurrent; + } + } + return this.maxConcurrent; } async enqueue(execute: () => Promise): Promise { @@ -38,7 +60,8 @@ class RequestQueue { } private async processQueue(): Promise { - if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) { + const maxConcurrent = await this.getCurrentMaxConcurrent(); + if (this.activeCount >= maxConcurrent || this.queue.length === 0) { return; } @@ -77,10 +100,6 @@ class RequestQueue { } } -// Singleton instance - Par défaut limite à 2 requêtes simultanées (configurable via KOMGA_MAX_CONCURRENT_REQUESTS) -export const RequestQueueService = new RequestQueue( - process.env.KOMGA_MAX_CONCURRENT_REQUESTS - ? parseInt(process.env.KOMGA_MAX_CONCURRENT_REQUESTS, 10) - : 2 -); +// Singleton instance - Par défaut limite à 5 requêtes simultanées +export const RequestQueueService = new RequestQueue(5); diff --git a/src/types/preferences.ts b/src/types/preferences.ts index 6438bc9..e2f56c8 100644 --- a/src/types/preferences.ts +++ b/src/types/preferences.ts @@ -9,6 +9,12 @@ export interface BackgroundPreferences { komgaLibraries?: string[]; // IDs des bibliothèques Komga sélectionnées } +export interface CircuitBreakerConfig { + threshold?: number; + timeout?: number; + resetTimeout?: number; +} + export interface UserPreferences { showThumbnails: boolean; cacheMode: "memory" | "file"; @@ -18,6 +24,9 @@ export interface UserPreferences { itemsPerPage: number; }; background: BackgroundPreferences; + komgaMaxConcurrentRequests: number; + readerPrefetchCount: number; + circuitBreakerConfig: CircuitBreakerConfig; } export const defaultPreferences: UserPreferences = { @@ -33,6 +42,13 @@ export const defaultPreferences: UserPreferences = { opacity: 10, blur: 0, }, + komgaMaxConcurrentRequests: 5, + readerPrefetchCount: 5, + circuitBreakerConfig: { + threshold: 5, + timeout: 30000, + resetTimeout: 60000, + }, }; // Dégradés prédéfinis