From 512e9a480f282b06f5bbc84e9e3a8c2b0661d246 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sat, 3 Jan 2026 18:55:12 +0100 Subject: [PATCH] refactor: remove caching-related API endpoints and configurations, update preferences structure, and clean up unused services --- docs/api.md | 22 +- docs/caching.md | 549 ---------- docs/services.md | 36 +- prisma/schema.prisma | 41 +- public/sw.js | 45 +- .../books/[bookId]/read-progress/route.ts | 21 - .../clear/[libraryId]/[seriesId]/route.ts | 46 - src/app/api/komga/cache/clear/route.ts | 34 - src/app/api/komga/cache/entries/route.ts | 27 - src/app/api/komga/cache/mode/route.ts | 60 -- src/app/api/komga/cache/size/route.ts | 31 - src/app/api/komga/home/route.ts | 31 - .../libraries/[libraryId]/series/route.ts | 37 - .../komga/series/[seriesId]/books/route.ts | 40 - src/app/api/komga/ttl-config/route.ts | 87 -- .../[libraryId]/ClientLibraryPage.tsx | 18 +- .../series/[seriesId]/ClientSeriesPage.tsx | 18 +- src/app/settings/page.tsx | 8 +- src/components/home/ClientHomePage.tsx | 16 +- src/components/layout/ClientLayout.tsx | 65 +- src/components/reader/PhotoswipeReader.tsx | 2 - src/components/reader/hooks/useImageLoader.ts | 2 - src/components/settings/AdvancedSettings.tsx | 103 +- src/components/settings/CacheModeSwitch.tsx | 61 -- src/components/settings/CacheSettings.tsx | 938 ------------------ src/components/settings/ClientSettings.tsx | 18 +- src/components/ui/book-cover.tsx | 4 +- src/components/ui/series-cover.tsx | 4 +- src/constants/errorCodes.ts | 10 - src/constants/errorMessages.ts | 10 - src/contexts/ImageCacheContext.tsx | 58 -- src/hooks/useImageUrl.ts | 14 - src/i18n/messages/en/common.json | 102 +- src/i18n/messages/fr/common.json | 102 +- src/lib/services/base-api.service.ts | 201 +--- src/lib/services/book.service.ts | 53 +- src/lib/services/circuit-breaker.service.ts | 114 --- src/lib/services/config-db.service.ts | 56 +- src/lib/services/home.service.ts | 129 +-- src/lib/services/library.service.ts | 61 +- src/lib/services/preferences.service.ts | 22 +- src/lib/services/request-monitor.service.ts | 44 - src/lib/services/request-queue.service.ts | 109 -- src/lib/services/series.service.ts | 104 +- src/lib/services/server-cache.service.ts | 730 -------------- src/lib/utils/image-url.ts | 3 +- src/types/cache.ts | 1 - src/types/komga.ts | 14 - src/types/preferences.ts | 16 - 49 files changed, 244 insertions(+), 4073 deletions(-) delete mode 100644 docs/caching.md delete mode 100644 src/app/api/komga/cache/clear/[libraryId]/[seriesId]/route.ts delete mode 100644 src/app/api/komga/cache/clear/route.ts delete mode 100644 src/app/api/komga/cache/entries/route.ts delete mode 100644 src/app/api/komga/cache/mode/route.ts delete mode 100644 src/app/api/komga/cache/size/route.ts delete mode 100644 src/app/api/komga/ttl-config/route.ts delete mode 100644 src/components/settings/CacheModeSwitch.tsx delete mode 100644 src/components/settings/CacheSettings.tsx delete mode 100644 src/contexts/ImageCacheContext.tsx delete mode 100644 src/hooks/useImageUrl.ts delete mode 100644 src/lib/services/circuit-breaker.service.ts delete mode 100644 src/lib/services/request-monitor.service.ts delete mode 100644 src/lib/services/request-queue.service.ts delete mode 100644 src/lib/services/server-cache.service.ts delete mode 100644 src/types/cache.ts diff --git a/docs/api.md b/docs/api.md index 5abe859..c1cc867 100644 --- a/docs/api.md +++ b/docs/api.md @@ -27,22 +27,6 @@ - **Body** : `{ url: string, username: string, password: string }` - **Réponse** : `{ message: string, config: Config }` -### GET /api/komga/ttl-config - -- **Description** : Récupération de la configuration TTL du cache -- **Réponse** : `{ defaultTTL: number, homeTTL: number, ... }` - -### POST /api/komga/ttl-config - -- **Description** : Sauvegarde de la configuration TTL -- **Body** : `{ defaultTTL: number, homeTTL: number, ... }` -- **Réponse** : `{ message: string, config: TTLConfig }` - -### GET /api/komga/cache/mode - -- **Description** : Récupération du mode de cache actuel -- **Réponse** : `{ mode: string }` - ## 📚 Bibliothèques ### GET /api/komga/libraries @@ -123,13 +107,13 @@ ### GET /api/preferences - **Description** : Récupération des préférences utilisateur -- **Réponse** : `{ showThumbnails: boolean, cacheMode: "memory" | "file", showOnlyUnread: boolean, debug: boolean }` +- **Réponse** : `{ showThumbnails: boolean, showOnlyUnread: boolean, displayMode: object, background: object, readerPrefetchCount: number }` ### PUT /api/preferences - **Description** : Mise à jour des préférences utilisateur -- **Body** : `{ showThumbnails?: boolean, cacheMode?: "memory" | "file", showOnlyUnread?: boolean, debug?: boolean }` -- **Réponse** : `{ showThumbnails: boolean, cacheMode: "memory" | "file", showOnlyUnread: boolean, debug: boolean }` +- **Body** : `{ showThumbnails?: boolean, showOnlyUnread?: boolean, displayMode?: object, background?: object, readerPrefetchCount?: number }` +- **Réponse** : `{ showThumbnails: boolean, showOnlyUnread: boolean, displayMode: object, background: object, readerPrefetchCount: number }` ## 🧪 Test diff --git a/docs/caching.md b/docs/caching.md deleted file mode 100644 index e50eef5..0000000 --- a/docs/caching.md +++ /dev/null @@ -1,549 +0,0 @@ -# Système de Caching - -Ce document décrit l'architecture et les stratégies de caching de StripStream. - -## Vue d'ensemble - -Le système de caching est organisé en **3 couches indépendantes** avec des responsabilités clairement définies : - -``` -┌─────────────────────────────────────────────────────────────┐ -│ NAVIGATEUR │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ Service Worker (Cache API) │ │ -│ │ → Offline support │ │ -│ │ → Images (covers + pages) │ │ -│ └────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ SERVEUR NEXT.JS │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ ServerCacheService │ │ -│ │ → Données API Komga │ │ -│ │ → Stale-while-revalidate │ │ -│ │ → Mode fichier ou mémoire │ │ -│ └────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ SERVEUR KOMGA │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Couche 1 : Service Worker (Client) - -### Fichier - -`public/sw.js` - -### Responsabilité - -- Support offline de l'application -- Cache persistant des images (couvertures et pages de livres) -- Cache des ressources statiques Next.js - -### Stratégies - -#### Images : Cache-First - -```javascript -// Pour toutes les images (covers + pages) -const isImageResource = (url) => { - return ( - (url.includes("/api/v1/books/") && - (url.includes("/pages") || url.includes("/thumbnail") || url.includes("/cover"))) || - (url.includes("/api/komga/images/") && - (url.includes("/series/") || url.includes("/books/")) && - url.includes("/thumbnail")) - ); -}; -``` - -**Comportement** : - -1. Vérifier si l'image est dans le cache -2. Si oui → retourner depuis le cache -3. Si non → fetch depuis le réseau -4. Si succès → mettre en cache + retourner -5. Si échec → retourner 404 - -**Avantages** : - -- Performance maximale (lecture instantanée depuis le cache) -- Fonctionne offline une fois les images chargées -- Économise la bande passante - -#### Navigation et ressources statiques : Network-First - -```javascript -// Pour les pages et ressources _next/static -event.respondWith( - fetch(request) - .then((response) => { - // Mise en cache si succès - if (response.ok && (isNextStaticResource || isNavigation)) { - cache.put(request, response.clone()); - } - return response; - }) - .catch(async () => { - // Fallback sur le cache si offline - const cachedResponse = await cache.match(request); - if (cachedResponse) return cachedResponse; - - // Page offline si navigation - if (request.mode === "navigate") { - return cache.match("/offline.html"); - } - }) -); -``` - -**Avantages** : - -- Toujours la dernière version quand online -- Fallback offline si nécessaire -- Navigation fluide même sans connexion - -### Caches - -| Cache | Usage | Stratégie | Taille | -| ----------------------- | ---------------------------- | ------------- | -------- | -| `stripstream-cache-v1` | Ressources statiques + pages | Network-First | ~5 MB | -| `stripstream-images-v1` | Images (covers + pages) | Cache-First | Illimité | - -### Nettoyage - -- Automatique lors de l'activation du Service Worker -- Suppression des anciennes versions de cache -- Pas d'expiration (contrôlé par l'utilisateur via les paramètres du navigateur) - -## Couche 2 : ServerCacheService (Serveur) - -### Fichier - -`src/lib/services/server-cache.service.ts` - -### Responsabilité - -- Cache des réponses API Komga côté serveur -- Optimisation des temps de réponse -- Réduction de la charge sur Komga - -### Stratégie : Stale-While-Revalidate - -Cette stratégie est **la clé de la performance** de l'application. - -#### Principe - -``` -Requête → Cache existe ? - ├─ Non → Fetch normal + mise en cache - └─ Oui → Cache valide ? - ├─ Oui → Retourne immédiatement - └─ Non → Retourne le cache expiré (stale) - ET revalide en background -``` - -#### Implémentation - -```typescript -async getOrSet( - key: string, - fetcher: () => Promise, - type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT" -): Promise { - const cacheKey = `${user.id}-${key}`; - const cachedResult = this.getStale(cacheKey); - - if (cachedResult !== null) { - const { data, isStale } = cachedResult; - - // Si le cache est expiré, revalider en background - if (isStale) { - this.revalidateInBackground(cacheKey, fetcher, type, key); - } - - return data as T; // Retour immédiat - } - - // Pas de cache, fetch normal - const data = await fetcher(); - this.set(cacheKey, data, type); - return data; -} -``` - -#### Avantages - -✅ **Temps de réponse constant** : Le cache expiré est retourné instantanément -✅ **Données fraîches** : Revalidation en background pour la prochaine requête -✅ **Pas de délai** : L'utilisateur ne subit jamais l'attente de revalidation -✅ **Résilience** : Même si Komga est lent, l'app reste rapide - -#### Inconvénients - -⚠️ Les données peuvent être légèrement obsolètes (jusqu'au prochain refresh) -⚠️ Nécessite un cache initialisé (première requête toujours lente) - -### Modes de stockage - -L'utilisateur peut choisir entre deux modes : - -#### Mode Mémoire (par défaut) - -```typescript -cacheMode: "memory"; -``` - -- Cache stocké en RAM -- **Performances** : Très rapide (lecture < 1ms) -- **Persistance** : Perdu au redémarrage du serveur -- **Capacité** : Limitée par la RAM disponible -- **Idéal pour** : Développement, faible charge - -#### Mode Fichier - -```typescript -cacheMode: "file"; -``` - -- Cache stocké sur disque (`.cache/`) -- **Performances** : Rapide (lecture 5-10ms) -- **Persistance** : Survit aux redémarrages -- **Capacité** : Limitée par l'espace disque -- **Idéal pour** : Production, haute charge - -### Time-To-Live (TTL) - -Chaque type de données a un TTL configuré : - -| Type | TTL par défaut | Justification | -| ----------- | -------------- | ---------------------------------- | -| `DEFAULT` | 5 minutes | Données génériques | -| `HOME` | 10 minutes | Page d'accueil (données agrégées) | -| `LIBRARIES` | 24 heures | Bibliothèques (rarement modifiées) | -| `SERIES` | 5 minutes | Séries (métadonnées + progression) | -| `BOOKS` | 5 minutes | Livres (métadonnées + progression) | -| `IMAGES` | 7 jours | Images (immuables) | - -#### Configuration personnalisée - -Les TTL peuvent être personnalisés par l'utilisateur via la base de données : - -```typescript -// Modèle Prisma : TTLConfig -{ - defaultTTL: 5 * 60 * 1000, - homeTTL: 10 * 60 * 1000, - librariesTTL: 24 * 60 * 60 * 1000, - seriesTTL: 5 * 60 * 1000, - booksTTL: 5 * 60 * 1000, - imagesTTL: 7 * 24 * 60 * 60 * 1000, -} -``` - -### Isolation par utilisateur - -Chaque utilisateur a son propre cache : - -```typescript -const cacheKey = `${user.id}-${key}`; -``` - -**Avantages** : - -- Pas de collision entre utilisateurs -- Progression de lecture individuelle -- Préférences personnalisées - -### Invalidation du cache - -Le cache peut être invalidé : - -#### Manuellement - -```typescript -await cacheService.delete(key); // Une clé -await cacheService.deleteAll(prefix); // Toutes les clés avec préfixe -await cacheService.clear(); // Tout le cache -``` - -#### Automatiquement - -- Lors d'une mise à jour de progression -- Lors d'un changement de favoris -- Lors de la suppression d'une série - -#### API - -``` -DELETE /api/komga/cache/clear // Vider tout le cache -DELETE /api/komga/home // Invalider le cache home -``` - -## Couche 3 : Cache HTTP (Navigateur) - -### Responsabilité - -- Cache basique géré par le navigateur -- Headers HTTP standard - -### Configuration - -#### Next.js ISR (Incremental Static Regeneration) - -```typescript -export const revalidate = 60; // Revalidation toutes les 60 secondes -``` - -Utilisé uniquement pour les routes avec rendu statique. - -#### Headers explicites (désactivé) - -Les headers HTTP explicites ont été **supprimés** car : - -- Le ServerCacheService gère déjà le caching efficacement -- Évite la confusion entre plusieurs couches de cache -- Simplifie le debugging - -Avant (supprimé) : - -```typescript -NextResponse.json(data, { - headers: { - "Cache-Control": "public, s-maxage=60, stale-while-revalidate=120", - }, -}); -``` - -Maintenant : - -```typescript -NextResponse.json(data); // Pas de headers -``` - -## Flow de données complet - -Exemple : Chargement de la page d'accueil - -``` -1. Utilisateur → GET / - ↓ -2. Next.js → HomeService.getHomeData() - ↓ -3. HomeService → ServerCacheService.getOrSet("home-ongoing", ...) - ↓ -4. ServerCacheService - ├─ Cache valide ? → Retourne immédiatement - ├─ Cache expiré ? → Retourne cache + revalide en background - └─ Pas de cache ? → Fetch Komga + mise en cache - ↓ -5. Response → Client - ↓ -6. Images → Service Worker (Cache-First) - ├─ En cache ? → Lecture instantanée - └─ Pas en cache ? → Fetch + mise en cache -``` - -### Temps de réponse typiques - -| Scénario | Temps | Détails | -| ----------------------------- | ----------- | -------------------------- | -| Cache ServerCache valide + SW | ~50ms | Optimal | -| Cache ServerCache expiré + SW | ~50ms | Revalidation en background | -| Pas de cache ServerCache + SW | ~200-500ms | Première requête | -| Cache SW uniquement | ~10ms | Images seulement | -| Tout à froid | ~500-1000ms | Pire cas | - -## Cas d'usage - -### 1. Première visite - -``` -User → App → Komga (tous les caches vides) -Temps : ~500-1000ms -``` - -### 2. Visite suivante (online) - -``` -User → ServerCache (valide) → Images SW -Temps : ~50ms -``` - -### 3. Cache expiré (online) - -``` -User → ServerCache (stale) → Retour immédiat - ↓ - Revalidation background → Mise à jour cache -Temps ressenti : ~50ms (aucun délai) -``` - -### 4. Mode offline - -``` -User → Service Worker cache uniquement -Fonctionnalités : -✅ Navigation entre pages déjà visitées -✅ Consultation des images déjà vues -❌ Nouvelles données (nécessite connexion) -``` - -## Monitoring et debug - -### Logs de cache (recommandé pour le dev) - -Activez les logs détaillés du cache serveur : - -```bash -# Dans docker-compose.dev.yml ou .env -CACHE_DEBUG=true -``` - -**Format des logs** : - -``` -[CACHE HIT] home-ongoing | HOME | 0.45ms # Cache valide -[CACHE STALE] home-ongoing | HOME | 0.52ms # Cache expiré (retourné + revalidation) -[CACHE MISS] home-ongoing | HOME # Pas de cache -[CACHE SET] home-ongoing | HOME | 324.18ms # Mise en cache -[CACHE REVALIDATE] home-ongoing | HOME | 287ms # Revalidation background -``` - -📖 **Documentation complète** : [docs/cache-debug.md](./cache-debug.md) - -### API de monitoring - -#### Taille du cache serveur - -```bash -GET /api/komga/cache/size -Response: { sizeInBytes: 15728640, itemCount: 234 } -``` - -#### Mode de cache actuel - -```bash -GET /api/komga/cache/mode -Response: { mode: "memory" } -``` - -#### Changer le mode - -```bash -POST /api/komga/cache/mode -Body: { mode: "file" } -``` - -#### Vider le cache - -```bash -POST /api/komga/cache/clear -``` - -### DevTools du navigateur - -#### Network Tab - -- Temps de réponse < 50ms = cache serveur -- Headers `X-Cache` si configurés -- Onglet "Timing" pour détails - -#### Application → Cache Storage - -Inspecter le Service Worker : - -- `stripstream-cache-v1` : Ressources statiques -- `stripstream-images-v1` : Images - -Actions disponibles : - -- Voir le contenu -- Supprimer des entrées -- Vider complètement - -#### Application → Service Workers - -- État du Service Worker -- "Unregister" pour le désactiver -- "Update" pour forcer une mise à jour - -## Optimisations futures possibles - -### 1. Cache Redis (optionnel) - -- Pour un déploiement multi-instances -- Cache partagé entre plusieurs serveurs -- TTL natif Redis - -### 2. Compression - -- Compresser les données en cache (Brotli/Gzip) -- Économie d'espace disque/mémoire -- Trade-off CPU vs espace - -### 3. Prefetching intelligent - -- Précharger les séries en cours de lecture -- Précharger les pages suivantes dans le reader -- Basé sur l'historique utilisateur - -### 4. Cache Analytics - -- Ratio hit/miss -- Temps de réponse moyens -- Identification des données les plus consultées - -## Bonnes pratiques - -### Pour les développeurs - -✅ **Utiliser BaseApiService.fetchWithCache()** - -```typescript -await this.fetchWithCache( - "cache-key", - async () => this.fetchFromApi(...), - "HOME" // Type de TTL -); -``` - -✅ **Invalider le cache après modification** - -```typescript -await HomeService.invalidateHomeCache(); -``` - -✅ **Choisir le bon TTL** - -- Court (1-5 min) : Données qui changent souvent -- Moyen (10-30 min) : Données agrégées -- Long (24h+) : Données quasi-statiques - -❌ **Ne pas cacher les mutations** -Les POST/PUT/DELETE ne doivent jamais être cachés - -❌ **Ne pas oublier l'isolation utilisateur** -Toujours préfixer avec `userId` pour les données personnelles - -### Pour les utilisateurs - -- **Mode mémoire** : Plus rapide, mais cache perdu au redémarrage -- **Mode fichier** : Persistant, idéal pour production -- **Vider le cache** : En cas de problème d'affichage -- **Offline** : Consulter les pages déjà visitées - -## Conclusion - -Le système de caching de StripStream est conçu pour : - -🎯 **Performance** : Temps de réponse constants grâce au stale-while-revalidate -🔒 **Fiabilité** : Fonctionne même si Komga est lent ou inaccessible -💾 **Flexibilité** : Mode mémoire ou fichier selon les besoins -🚀 **Offline-first** : Support complet du mode hors ligne -🧹 **Simplicité** : 3 couches bien définies, pas de redondance - -Le système est maintenu simple avec des responsabilités claires pour chaque couche, facilitant la maintenance et l'évolution future. diff --git a/docs/services.md b/docs/services.md index b7828d9..a9a9fdb 100644 --- a/docs/services.md +++ b/docs/services.md @@ -7,10 +7,12 @@ Service de gestion de l'authentification ### Méthodes - `loginUser(email: string, password: string): Promise` + - Authentifie un utilisateur - Retourne les données utilisateur - `createUser(email: string, password: string): Promise` + - Crée un nouvel utilisateur - Retourne les données utilisateur @@ -24,10 +26,11 @@ Service de gestion des bibliothèques ### Méthodes - `getLibraries(): Promise` + - Récupère la liste des bibliothèques - - Met en cache les résultats - `getLibrary(libraryId: string): Promise` + - Récupère une bibliothèque spécifique - Lance une erreur si non trouvée @@ -47,9 +50,11 @@ Service de gestion des séries ### Méthodes - `getSeries(seriesId: string): Promise` + - Récupère les détails d'une série - `getSeriesBooks(seriesId: string, page: number = 0, size: number = 24, unreadOnly: boolean = false): Promise>` + - Récupère les livres d'une série - Supporte la pagination et le filtrage @@ -63,15 +68,19 @@ Service de gestion des livres ### Méthodes - `getBook(bookId: string): Promise<{ book: KomgaBook; pages: number[] }>` + - Récupère les détails d'un livre et ses pages - `updateReadProgress(bookId: string, page: number, completed: boolean = false): Promise` + - Met à jour la progression de lecture - `getPage(bookId: string, pageNumber: number): Promise` + - Récupère une page spécifique d'un livre - `getCover(bookId: string): Promise` + - Récupère la couverture d'un livre - `getPageThumbnail(bookId: string, pageNumber: number): Promise` @@ -84,13 +93,15 @@ Service de gestion des images ### Méthodes - `getImage(path: string): Promise` + - Récupère une image depuis le serveur - - Gère le cache des images - `getSeriesThumbnailUrl(seriesId: string): string` + - Génère l'URL de la miniature d'une série - `getBookThumbnailUrl(bookId: string): string` + - Génère l'URL de la miniature d'un livre - `getBookPageUrl(bookId: string, pageNumber: number): string` @@ -103,29 +114,20 @@ Service de gestion de la configuration ### Méthodes - `getConfig(): Promise` + - Récupère la configuration Komga - `saveConfig(config: Config): Promise` + - Sauvegarde la configuration Komga - `getTTLConfig(): Promise` + - Récupère la configuration TTL - `saveTTLConfig(config: TTLConfig): Promise` - Sauvegarde la configuration TTL -## 🔄 ServerCacheService - -Service de gestion du cache serveur - -### Méthodes - -- `getCacheMode(): string` - - Récupère le mode de cache actuel - -- `clearCache(): void` - - Vide le cache serveur - ## ⭐ FavoriteService Service de gestion des favoris @@ -142,6 +144,7 @@ Service de gestion des préférences ### Méthodes - `getPreferences(): Promise` + - Récupère les préférences utilisateur - `savePreferences(preferences: Preferences): Promise` @@ -164,13 +167,12 @@ Service de base pour les appels API ### Méthodes - `buildUrl(config: Config, path: string, params?: Record): string` + - Construit une URL d'API - `getAuthHeaders(config: Config): Headers` + - Génère les en-têtes d'authentification - `fetchFromApi(url: string, headers: Headers, raw?: boolean): Promise` - Effectue un appel API avec gestion d'erreurs - -- `fetchWithCache(key: string, fetcher: () => Promise, type: CacheType): Promise` - - Effectue un appel API avec mise en cache diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ed1d2ed..e9529a2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,7 +21,6 @@ model User { // Relations config KomgaConfig? - ttlConfig TTLConfig? preferences Preferences? favorites Favorite[] @@ -42,37 +41,16 @@ model KomgaConfig { @@map("komgaconfigs") } -model TTLConfig { - id Int @id @default(autoincrement()) - userId Int @unique - defaultTTL Int @default(5) - homeTTL Int @default(5) - librariesTTL Int @default(1440) - seriesTTL Int @default(5) - booksTTL Int @default(5) - imagesTTL Int @default(1440) - imageCacheMaxAge Int @default(2592000) // 30 jours en secondes - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@map("ttlconfigs") -} - model Preferences { - id Int @id @default(autoincrement()) - userId Int @unique - showThumbnails Boolean @default(true) - cacheMode String @default("memory") // "memory" | "file" - showOnlyUnread Boolean @default(false) - displayMode Json - background Json - komgaMaxConcurrentRequests Int @default(5) - circuitBreakerConfig Json - readerPrefetchCount Int @default(5) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + userId Int @unique + showThumbnails Boolean @default(true) + showOnlyUnread Boolean @default(false) + displayMode Json + background Json + readerPrefetchCount Int @default(5) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -92,4 +70,3 @@ model Favorite { @@index([userId]) @@map("favorites") } - diff --git a/public/sw.js b/public/sw.js index aaa1742..ee0ae8c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,11 +1,8 @@ // StripStream Service Worker - Version 1 -// Architecture: Cache-as-you-go for images and static resources only -// API data caching is handled by ServerCacheService on the server +// Architecture: Cache-as-you-go for static resources only const VERSION = "v1"; const STATIC_CACHE = `stripstream-static-${VERSION}`; -const IMAGES_CACHE = `stripstream-images-${VERSION}`; -const DATA_CACHE = `stripstream-data-${VERSION}`; const RSC_CACHE = `stripstream-rsc-${VERSION}`; const BOOKS_CACHE = "stripstream-books"; // Never version this - managed by DownloadManager @@ -20,29 +17,18 @@ function isNextStaticResource(url) { return url.includes("/_next/static/"); } -function isImageRequest(url) { - return url.includes("/api/komga/images/"); -} - -function isApiDataRequest(url) { - return url.includes("/api/komga/") && !isImageRequest(url); -} - function isNextRSCRequest(request) { const url = new URL(request.url); return url.searchParams.has("_rsc") || request.headers.get("RSC") === "1"; } -// Removed: shouldCacheApiData - API data is no longer cached by SW -// API data caching is handled by ServerCacheService on the server - // ============================================================================ // Cache Strategies // ============================================================================ /** * Cache-First: Serve from cache, fallback to network - * Used for: Images, Next.js static resources + * Used for: Next.js static resources */ async function cacheFirstStrategy(request, cacheName, options = {}) { const cache = await caches.open(cacheName); @@ -70,7 +56,7 @@ async function cacheFirstStrategy(request, cacheName, options = {}) { /** * Stale-While-Revalidate: Serve from cache immediately, update in background - * Used for: API data, RSC payloads + * Used for: RSC payloads */ async function staleWhileRevalidateStrategy(request, cacheName) { const cache = await caches.open(cacheName); @@ -202,39 +188,24 @@ self.addEventListener("fetch", (event) => { return; } - // Route 1: Images → Cache-First with ignoreSearch - if (isImageRequest(url.href)) { - event.respondWith(cacheFirstStrategy(request, IMAGES_CACHE, { ignoreSearch: true })); - return; - } - - // Route 2: Next.js RSC payloads → Stale-While-Revalidate + // Route 1: Next.js RSC payloads → Stale-While-Revalidate if (isNextRSCRequest(request)) { event.respondWith(staleWhileRevalidateStrategy(request, RSC_CACHE)); return; } - // Route 3: API data → Network only (no SW caching) - // API data caching is handled by ServerCacheService on the server - // This avoids double caching and simplifies cache invalidation - if (isApiDataRequest(url.href)) { - // Let the request pass through to the network - // ServerCacheService will handle caching server-side - return; - } - - // Route 4: Next.js static resources → Cache-First with ignoreSearch + // Route 2: Next.js static resources → Cache-First with ignoreSearch if (isNextStaticResource(url.href)) { event.respondWith(cacheFirstStrategy(request, STATIC_CACHE, { ignoreSearch: true })); return; } - // Route 5: Navigation → Network-First with SPA fallback + // Route 3: Navigation → Network-First with SPA fallback if (request.mode === "navigate") { event.respondWith(navigationStrategy(request)); return; } - // Route 6: Everything else → Network only (no caching) - // This includes: API auth, preferences, and other dynamic content + // Route 4: Everything else → Network only (no caching) + // This includes: API calls, images, and other dynamic content }); diff --git a/src/app/api/komga/books/[bookId]/read-progress/route.ts b/src/app/api/komga/books/[bookId]/read-progress/route.ts index 1ce426f..b943774 100644 --- a/src/app/api/komga/books/[bookId]/read-progress/route.ts +++ b/src/app/api/komga/books/[bookId]/read-progress/route.ts @@ -1,7 +1,6 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { BookService } from "@/lib/services/book.service"; -import { SeriesService } from "@/lib/services/series.service"; import { ERROR_CODES } from "@/constants/errorCodes"; import { getErrorMessage } from "@/utils/errors"; import { AppError } from "@/utils/errors"; @@ -30,16 +29,6 @@ export async function PATCH( await BookService.updateReadProgress(bookId, page, completed); - // Invalider le cache de la série après avoir mis à jour la progression - try { - const seriesId = await BookService.getBookSeriesId(bookId); - await SeriesService.invalidateSeriesBooksCache(seriesId); - await SeriesService.invalidateSeriesCache(seriesId); - } catch (cacheError) { - // Ne pas faire échouer la requête si l'invalidation du cache échoue - logger.error({ err: cacheError }, "Erreur lors de l'invalidation du cache de la série:"); - } - return NextResponse.json({ message: "📖 Progression mise à jour avec succès" }); } catch (error) { logger.error({ err: error }, "Erreur lors de la mise à jour de la progression:"); @@ -77,16 +66,6 @@ export async function DELETE( await BookService.deleteReadProgress(bookId); - // Invalider le cache de la série après avoir supprimé la progression - try { - const seriesId = await BookService.getBookSeriesId(bookId); - await SeriesService.invalidateSeriesBooksCache(seriesId); - await SeriesService.invalidateSeriesCache(seriesId); - } catch (cacheError) { - // Ne pas faire échouer la requête si l'invalidation du cache échoue - logger.error({ err: cacheError }, "Erreur lors de l'invalidation du cache de la série:"); - } - return NextResponse.json({ message: "🗑️ Progression supprimée avec succès" }); } catch (error) { logger.error({ err: error }, "Erreur lors de la suppression de la progression:"); diff --git a/src/app/api/komga/cache/clear/[libraryId]/[seriesId]/route.ts b/src/app/api/komga/cache/clear/[libraryId]/[seriesId]/route.ts deleted file mode 100644 index 9d1aab6..0000000 --- a/src/app/api/komga/cache/clear/[libraryId]/[seriesId]/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextResponse } from "next/server"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import { getErrorMessage } from "@/utils/errors"; -import { LibraryService } from "@/lib/services/library.service"; -import { HomeService } from "@/lib/services/home.service"; -import { SeriesService } from "@/lib/services/series.service"; -import { revalidatePath } from "next/cache"; -import type { NextRequest } from "next/server"; -import logger from "@/lib/logger"; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ libraryId: string; seriesId: string }> } -) { - try { - const { libraryId, seriesId } = await params; - - await HomeService.invalidateHomeCache(); - revalidatePath("/"); - - if (libraryId) { - await LibraryService.invalidateLibrarySeriesCache(libraryId); - revalidatePath(`/library/${libraryId}`); - } - - if (seriesId) { - await SeriesService.invalidateSeriesBooksCache(seriesId); - await SeriesService.invalidateSeriesCache(seriesId); - revalidatePath(`/series/${seriesId}`); - } - - return NextResponse.json({ message: "🧹 Cache vidé avec succès" }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la suppression du cache:"); - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.CLEAR_ERROR, - name: "Cache clear error", - message: getErrorMessage(ERROR_CODES.CACHE.CLEAR_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/komga/cache/clear/route.ts b/src/app/api/komga/cache/clear/route.ts deleted file mode 100644 index 16ff7df..0000000 --- a/src/app/api/komga/cache/clear/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NextResponse } from "next/server"; -import type { ServerCacheService } from "@/lib/services/server-cache.service"; -import { getServerCacheService } from "@/lib/services/server-cache.service"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import { getErrorMessage } from "@/utils/errors"; -import { revalidatePath } from "next/cache"; -import logger from "@/lib/logger"; - -export async function POST() { - try { - const cacheService: ServerCacheService = await getServerCacheService(); - await cacheService.clear(); - - // Revalider toutes les pages importantes après le vidage du cache - revalidatePath("/"); - revalidatePath("/libraries"); - revalidatePath("/series"); - revalidatePath("/books"); - - return NextResponse.json({ message: "🧹 Cache vidé avec succès" }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la suppression du cache:"); - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.CLEAR_ERROR, - name: "Cache clear error", - message: getErrorMessage(ERROR_CODES.CACHE.CLEAR_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/komga/cache/entries/route.ts b/src/app/api/komga/cache/entries/route.ts deleted file mode 100644 index 0d149d9..0000000 --- a/src/app/api/komga/cache/entries/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextResponse } from "next/server"; -import type { ServerCacheService } from "@/lib/services/server-cache.service"; -import { getServerCacheService } from "@/lib/services/server-cache.service"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import { getErrorMessage } from "@/utils/errors"; -import logger from "@/lib/logger"; - -export async function GET() { - try { - const cacheService: ServerCacheService = await getServerCacheService(); - const entries = await cacheService.getCacheEntries(); - - return NextResponse.json({ entries }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la récupération des entrées du cache"); - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.SIZE_FETCH_ERROR, - name: "Cache entries fetch error", - message: getErrorMessage(ERROR_CODES.CACHE.SIZE_FETCH_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/komga/cache/mode/route.ts b/src/app/api/komga/cache/mode/route.ts deleted file mode 100644 index a6e2d04..0000000 --- a/src/app/api/komga/cache/mode/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextResponse } from "next/server"; -import type { CacheMode, ServerCacheService } from "@/lib/services/server-cache.service"; -import { getServerCacheService } from "@/lib/services/server-cache.service"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import { getErrorMessage } from "@/utils/errors"; -import type { NextRequest } from "next/server"; -import logger from "@/lib/logger"; - -export async function GET() { - try { - const cacheService: ServerCacheService = await getServerCacheService(); - return NextResponse.json({ mode: cacheService.getCacheMode() }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la récupération du mode de cache:"); - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.MODE_FETCH_ERROR, - name: "Cache mode fetch error", - message: getErrorMessage(ERROR_CODES.CACHE.MODE_FETCH_ERROR), - }, - }, - { status: 500 } - ); - } -} - -export async function POST(request: NextRequest) { - try { - const { mode }: { mode: CacheMode } = await request.json(); - if (mode !== "file" && mode !== "memory") { - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.INVALID_MODE, - name: "Invalid cache mode", - message: getErrorMessage(ERROR_CODES.CACHE.INVALID_MODE), - }, - }, - { status: 400 } - ); - } - - const cacheService: ServerCacheService = await getServerCacheService(); - cacheService.setCacheMode(mode); - return NextResponse.json({ mode: cacheService.getCacheMode() }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la mise à jour du mode de cache:"); - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.MODE_UPDATE_ERROR, - name: "Cache mode update error", - message: getErrorMessage(ERROR_CODES.CACHE.MODE_UPDATE_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/komga/cache/size/route.ts b/src/app/api/komga/cache/size/route.ts deleted file mode 100644 index e58c36d..0000000 --- a/src/app/api/komga/cache/size/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextResponse } from "next/server"; -import type { ServerCacheService } from "@/lib/services/server-cache.service"; -import { getServerCacheService } from "@/lib/services/server-cache.service"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import { getErrorMessage } from "@/utils/errors"; -import logger from "@/lib/logger"; - -export async function GET() { - try { - const cacheService: ServerCacheService = await getServerCacheService(); - const { sizeInBytes, itemCount } = await cacheService.getCacheSize(); - - return NextResponse.json({ - sizeInBytes, - itemCount, - mode: cacheService.getCacheMode(), - }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la récupération de la taille du cache:"); - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.SIZE_FETCH_ERROR, - name: "Cache size fetch error", - message: getErrorMessage(ERROR_CODES.CACHE.SIZE_FETCH_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/komga/home/route.ts b/src/app/api/komga/home/route.ts index ce5d085..48e3099 100644 --- a/src/app/api/komga/home/route.ts +++ b/src/app/api/komga/home/route.ts @@ -35,34 +35,3 @@ export async function GET() { ); } } - -export async function DELETE() { - try { - await HomeService.invalidateHomeCache(); - return NextResponse.json({ success: true }); - } catch (error) { - logger.error({ err: error }, "API Home - Erreur lors de l'invalidation du cache:"); - if (error instanceof AppError) { - return NextResponse.json( - { - error: { - code: error.code, - name: "Cache invalidation error", - message: getErrorMessage(error.code), - }, - }, - { status: 500 } - ); - } - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.DELETE_ERROR, - name: "Cache invalidation error", - message: getErrorMessage(ERROR_CODES.CACHE.DELETE_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/komga/libraries/[libraryId]/series/route.ts b/src/app/api/komga/libraries/[libraryId]/series/route.ts index 3dd7160..2693188 100644 --- a/src/app/api/komga/libraries/[libraryId]/series/route.ts +++ b/src/app/api/komga/libraries/[libraryId]/series/route.ts @@ -53,40 +53,3 @@ export async function GET( ); } } - -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ libraryId: string }> } -) { - try { - const libraryId: string = (await params).libraryId; - - await LibraryService.invalidateLibrarySeriesCache(libraryId); - - return NextResponse.json({ success: true }); - } catch (error) { - logger.error({ err: error }, "API Library Cache Invalidation - Erreur:"); - if (error instanceof AppError) { - return NextResponse.json( - { - error: { - code: error.code, - name: "Cache invalidation error", - message: getErrorMessage(error.code), - }, - }, - { status: 500 } - ); - } - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.DELETE_ERROR, - name: "Cache invalidation error", - message: getErrorMessage(ERROR_CODES.CACHE.DELETE_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/komga/series/[seriesId]/books/route.ts b/src/app/api/komga/series/[seriesId]/books/route.ts index 13b7a61..36b016a 100644 --- a/src/app/api/komga/series/[seriesId]/books/route.ts +++ b/src/app/api/komga/series/[seriesId]/books/route.ts @@ -52,43 +52,3 @@ export async function GET( ); } } - -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ seriesId: string }> } -) { - try { - const seriesId: string = (await params).seriesId; - - await Promise.all([ - SeriesService.invalidateSeriesBooksCache(seriesId), - SeriesService.invalidateSeriesCache(seriesId), - ]); - - return NextResponse.json({ success: true }); - } catch (error) { - logger.error({ err: error }, "API Series Cache Invalidation - Erreur:"); - if (error instanceof AppError) { - return NextResponse.json( - { - error: { - code: error.code, - name: "Cache invalidation error", - message: getErrorMessage(error.code), - }, - }, - { status: 500 } - ); - } - return NextResponse.json( - { - error: { - code: ERROR_CODES.CACHE.DELETE_ERROR, - name: "Cache invalidation error", - message: getErrorMessage(ERROR_CODES.CACHE.DELETE_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/komga/ttl-config/route.ts b/src/app/api/komga/ttl-config/route.ts deleted file mode 100644 index 150eab2..0000000 --- a/src/app/api/komga/ttl-config/route.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { NextResponse } from "next/server"; -import { ConfigDBService } from "@/lib/services/config-db.service"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import type { TTLConfig } from "@/types/komga"; -import { getErrorMessage } from "@/utils/errors"; -import type { NextRequest } from "next/server"; -import logger from "@/lib/logger"; - -export async function GET() { - try { - const config: TTLConfig | null = await ConfigDBService.getTTLConfig(); - return NextResponse.json(config); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la récupération de la configuration TTL"); - if (error instanceof Error) { - if (error.message === getErrorMessage(ERROR_CODES.MIDDLEWARE.UNAUTHORIZED)) { - return NextResponse.json( - { - error: { - name: "Unauthorized", - code: ERROR_CODES.MIDDLEWARE.UNAUTHORIZED, - message: getErrorMessage(ERROR_CODES.MIDDLEWARE.UNAUTHORIZED), - }, - }, - { status: 401 } - ); - } - } - return NextResponse.json( - { - error: { - name: "TTL fetch error", - code: ERROR_CODES.CONFIG.TTL_FETCH_ERROR, - message: getErrorMessage(ERROR_CODES.CONFIG.TTL_FETCH_ERROR), - }, - }, - { status: 500 } - ); - } -} - -export async function POST(request: NextRequest) { - try { - const data = await request.json(); - const config: TTLConfig = await ConfigDBService.saveTTLConfig(data); - - return NextResponse.json({ - message: "⏱️ Configuration TTL sauvegardée avec succès", - config: { - defaultTTL: config.defaultTTL, - homeTTL: config.homeTTL, - librariesTTL: config.librariesTTL, - seriesTTL: config.seriesTTL, - booksTTL: config.booksTTL, - imagesTTL: config.imagesTTL, - imageCacheMaxAge: config.imageCacheMaxAge, - }, - }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la sauvegarde de la configuration TTL"); - if ( - error instanceof Error && - error.message === getErrorMessage(ERROR_CODES.MIDDLEWARE.UNAUTHORIZED) - ) { - return NextResponse.json( - { - error: { - name: "Unauthorized", - code: ERROR_CODES.MIDDLEWARE.UNAUTHORIZED, - message: getErrorMessage(ERROR_CODES.MIDDLEWARE.UNAUTHORIZED), - }, - }, - { status: 401 } - ); - } - return NextResponse.json( - { - error: { - name: "TTL save error", - code: ERROR_CODES.CONFIG.TTL_SAVE_ERROR, - message: getErrorMessage(ERROR_CODES.CONFIG.TTL_SAVE_ERROR), - }, - }, - { status: 500 } - ); - } -} diff --git a/src/app/libraries/[libraryId]/ClientLibraryPage.tsx b/src/app/libraries/[libraryId]/ClientLibraryPage.tsx index c0c59f3..0d83666 100644 --- a/src/app/libraries/[libraryId]/ClientLibraryPage.tsx +++ b/src/app/libraries/[libraryId]/ClientLibraryPage.tsx @@ -59,9 +59,7 @@ export function ClientLibraryPage({ params.append("search", search); } - const response = await fetch(`/api/komga/libraries/${libraryId}/series?${params}`, { - cache: "default", // Utilise le cache HTTP du navigateur - }); + const response = await fetch(`/api/komga/libraries/${libraryId}/series?${params}`); if (!response.ok) { const errorData = await response.json(); @@ -84,16 +82,6 @@ export function ClientLibraryPage({ const handleRefresh = async (libraryId: string) => { try { - // Invalidate cache via API - const cacheResponse = await fetch(`/api/komga/libraries/${libraryId}/series`, { - method: "DELETE", - }); - - if (!cacheResponse.ok) { - throw new Error("Error invalidating cache"); - } - - // Recharger les données const params = new URLSearchParams({ page: String(currentPage - 1), size: String(effectivePageSize), @@ -105,7 +93,7 @@ export function ClientLibraryPage({ } const response = await fetch(`/api/komga/libraries/${libraryId}/series?${params}`, { - cache: "reload", // Force un nouveau fetch après invalidation + cache: "reload", }); if (!response.ok) { @@ -139,7 +127,7 @@ export function ClientLibraryPage({ } const response = await fetch(`/api/komga/libraries/${libraryId}/series?${params}`, { - cache: "reload", // Force un nouveau fetch lors du retry + cache: "reload", }); if (!response.ok) { diff --git a/src/app/series/[seriesId]/ClientSeriesPage.tsx b/src/app/series/[seriesId]/ClientSeriesPage.tsx index e528c21..91bf326 100644 --- a/src/app/series/[seriesId]/ClientSeriesPage.tsx +++ b/src/app/series/[seriesId]/ClientSeriesPage.tsx @@ -49,9 +49,7 @@ export function ClientSeriesPage({ unread: String(unreadOnly), }); - const response = await fetch(`/api/komga/series/${seriesId}/books?${params}`, { - cache: "default", // Utilise le cache HTTP du navigateur - }); + const response = await fetch(`/api/komga/series/${seriesId}/books?${params}`); if (!response.ok) { const errorData = await response.json(); @@ -74,16 +72,6 @@ export function ClientSeriesPage({ const handleRefresh = async (seriesId: string) => { try { - // Invalidate cache via API - const cacheResponse = await fetch(`/api/komga/series/${seriesId}/books`, { - method: "DELETE", - }); - - if (!cacheResponse.ok) { - throw new Error("Erreur lors de l'invalidation du cache"); - } - - // Recharger les données const params = new URLSearchParams({ page: String(currentPage - 1), size: String(effectivePageSize), @@ -91,7 +79,7 @@ export function ClientSeriesPage({ }); const response = await fetch(`/api/komga/series/${seriesId}/books?${params}`, { - cache: "reload", // Force un nouveau fetch après invalidation + cache: "reload", }); if (!response.ok) { @@ -121,7 +109,7 @@ export function ClientSeriesPage({ }); const response = await fetch(`/api/komga/series/${seriesId}/books?${params}`, { - cache: "reload", // Force un nouveau fetch lors du retry + cache: "reload", }); if (!response.ok) { diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 6a76475..72def25 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,7 +1,7 @@ import { ConfigDBService } from "@/lib/services/config-db.service"; import { ClientSettings } from "@/components/settings/ClientSettings"; import type { Metadata } from "next"; -import type { KomgaConfig, TTLConfig } from "@/types/komga"; +import type { KomgaConfig } from "@/types/komga"; import logger from "@/lib/logger"; export const dynamic = "force-dynamic"; @@ -13,7 +13,6 @@ export const metadata: Metadata = { export default async function SettingsPage() { let config: KomgaConfig | null = null; - let ttlConfig: TTLConfig | null = null; try { // Récupérer la configuration Komga @@ -27,13 +26,10 @@ export default async function SettingsPage() { password: null, }; } - - // Récupérer la configuration TTL - ttlConfig = await ConfigDBService.getTTLConfig(); } catch (error) { logger.error({ err: error }, "Erreur lors de la récupération de la configuration:"); // On ne fait rien si la config n'existe pas, on laissera le composant client gérer l'état initial } - return ; + return ; } diff --git a/src/components/home/ClientHomePage.tsx b/src/components/home/ClientHomePage.tsx index a69962b..2e270f4 100644 --- a/src/components/home/ClientHomePage.tsx +++ b/src/components/home/ClientHomePage.tsx @@ -22,9 +22,7 @@ export function ClientHomePage() { setError(null); try { - const response = await fetch("/api/komga/home", { - cache: "default", // Utilise le cache HTTP du navigateur - }); + const response = await fetch("/api/komga/home"); if (!response.ok) { const errorData = await response.json(); @@ -56,18 +54,8 @@ export function ClientHomePage() { const handleRefresh = async () => { try { - // Invalider le cache via l'API - const deleteResponse = await fetch("/api/komga/home", { - method: "DELETE", - }); - - if (!deleteResponse.ok) { - throw new Error("Erreur lors de l'invalidation du cache"); - } - - // Récupérer les nouvelles données const response = await fetch("/api/komga/home", { - cache: "reload", // Force un nouveau fetch après invalidation + cache: "reload", }); if (!response.ok) { diff --git a/src/components/layout/ClientLayout.tsx b/src/components/layout/ClientLayout.tsx index 1471946..65d6208 100644 --- a/src/components/layout/ClientLayout.tsx +++ b/src/components/layout/ClientLayout.tsx @@ -10,7 +10,6 @@ import { usePathname } from "next/navigation"; import { registerServiceWorker } from "@/lib/registerSW"; import { NetworkStatus } from "../ui/NetworkStatus"; import { usePreferences } from "@/contexts/PreferencesContext"; -import { ImageCacheProvider } from "@/contexts/ImageCacheContext"; import type { KomgaLibrary, KomgaSeries } from "@/types/komga"; import logger from "@/lib/logger"; @@ -152,39 +151,37 @@ export default function ClientLayout({ return ( - - {/* Background fixe pour les images et gradients */} - {hasCustomBackground &&
} -
- {!isPublicRoute && ( -
- )} - {!isPublicRoute && ( - - )} -
{children}
- - - -
- + {/* Background fixe pour les images et gradients */} + {hasCustomBackground &&
} +
+ {!isPublicRoute && ( +
+ )} + {!isPublicRoute && ( + + )} +
{children}
+ + + +
); } diff --git a/src/components/reader/PhotoswipeReader.tsx b/src/components/reader/PhotoswipeReader.tsx index 01dcc82..15ce8f7 100644 --- a/src/components/reader/PhotoswipeReader.tsx +++ b/src/components/reader/PhotoswipeReader.tsx @@ -75,8 +75,6 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP }, []); // Prefetch current and next pages - // Deduplication in useImageLoader prevents redundant requests - // Server queue (RequestQueueService) handles concurrency limits useEffect(() => { // Prefetch pages starting from current page prefetchPages(currentPage, prefetchCount); diff --git a/src/components/reader/hooks/useImageLoader.ts b/src/components/reader/hooks/useImageLoader.ts index 6f7dcab..e5cd2f1 100644 --- a/src/components/reader/hooks/useImageLoader.ts +++ b/src/components/reader/hooks/useImageLoader.ts @@ -99,8 +99,6 @@ export function useImageLoader({ ); // Prefetch multiple pages starting from a given page - // The server-side queue (RequestQueueService) handles concurrency limits - // We only deduplicate to avoid redundant HTTP requests const prefetchPages = useCallback( async (startPage: number, count: number = prefetchCount) => { const pagesToPrefetch = []; diff --git a/src/components/settings/AdvancedSettings.tsx b/src/components/settings/AdvancedSettings.tsx index 0f5f335..7b4ce83 100644 --- a/src/components/settings/AdvancedSettings.tsx +++ b/src/components/settings/AdvancedSettings.tsx @@ -2,7 +2,7 @@ import { useTranslate } from "@/hooks/useTranslate"; import { usePreferences } from "@/contexts/PreferencesContext"; import { useToast } from "@/components/ui/use-toast"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Activity, Shield } from "lucide-react"; +import { Activity } from "lucide-react"; import { SliderControl } from "@/components/ui/slider-control"; import logger from "@/lib/logger"; @@ -11,25 +11,6 @@ export function AdvancedSettings() { 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) { - logger.error({ err: error }, "Erreur:"); - toast({ - variant: "destructive", - title: t("settings.error.title"), - description: t("settings.error.message"), - }); - } - }; - const handlePrefetchChange = async (value: number) => { try { await updatePreferences({ @@ -49,28 +30,6 @@ export function AdvancedSettings() { } }; - 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) { - logger.error({ err: error }, "Erreur:"); - toast({ - variant: "destructive", - title: t("settings.error.title"), - description: t("settings.error.message"), - }); - } - }; - return (
{/* Performance Settings */} @@ -85,18 +44,6 @@ export function AdvancedSettings() { - - -
- - - {/* Circuit Breaker Configuration */} - - -
- - {t("settings.advanced.circuitBreaker.title")} -
- {t("settings.advanced.circuitBreaker.description")} -
- - handleCircuitBreakerChange("threshold", value)} - /> - -
- - handleCircuitBreakerChange("timeout", value)} - formatValue={(value) => `${value / 1000}s`} - /> - -
- - handleCircuitBreakerChange("resetTimeout", value)} - formatValue={(value) => `${value / 1000}s`} - /> - -
); } diff --git a/src/components/settings/CacheModeSwitch.tsx b/src/components/settings/CacheModeSwitch.tsx deleted file mode 100644 index 76af928..0000000 --- a/src/components/settings/CacheModeSwitch.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { useToast } from "@/components/ui/use-toast"; -import { usePreferences } from "@/contexts/PreferencesContext"; -import logger from "@/lib/logger"; - -export function CacheModeSwitch() { - const [isLoading, setIsLoading] = useState(false); - const { toast } = useToast(); - const { preferences, updatePreferences } = usePreferences(); - - const handleToggle = async (checked: boolean) => { - setIsLoading(true); - try { - // Mettre à jour les préférences - await updatePreferences({ cacheMode: checked ? "memory" : "file" }); - - // Mettre à jour le mode de cache côté serveur - const res = await fetch("/api/komga/cache/mode", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ mode: checked ? "memory" : "file" }), - }); - - if (!res.ok) throw new Error(); - - toast({ - title: "Mode de cache modifié", - description: `Le cache est maintenant en mode ${checked ? "mémoire" : "fichier"}`, - }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la modification du mode de cache:"); - toast({ - variant: "destructive", - title: "Erreur", - description: "Impossible de modifier le mode de cache", - }); - } finally { - setIsLoading(false); - } - }; - - return ( -
- - -
- ); -} diff --git a/src/components/settings/CacheSettings.tsx b/src/components/settings/CacheSettings.tsx deleted file mode 100644 index bfd908f..0000000 --- a/src/components/settings/CacheSettings.tsx +++ /dev/null @@ -1,938 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useTranslate } from "@/hooks/useTranslate"; -import { useToast } from "@/components/ui/use-toast"; -import { Trash2, Loader2, HardDrive, List, ChevronDown, ChevronUp, ImageOff } from "lucide-react"; -import { CacheModeSwitch } from "@/components/settings/CacheModeSwitch"; -import { Label } from "@/components/ui/label"; -import type { TTLConfigData } from "@/types/komga"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { useImageCache } from "@/contexts/ImageCacheContext"; -import logger from "@/lib/logger"; - -interface CacheSettingsProps { - initialTTLConfig: TTLConfigData | null; -} - -interface CacheSizeInfo { - sizeInBytes: number; - itemCount: number; -} - -interface CacheEntry { - key: string; - size: number; - expiry: number; - isExpired: boolean; -} - -interface ServiceWorkerCacheEntry { - url: string; - size: number; - cacheName: string; -} - -export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) { - const { t } = useTranslate(); - const { toast } = useToast(); - const { flushImageCache } = useImageCache(); - const [isCacheClearing, setIsCacheClearing] = useState(false); - const [isServiceWorkerClearing, setIsServiceWorkerClearing] = useState(false); - const [serverCacheSize, setServerCacheSize] = useState(null); - const [swCacheSize, setSwCacheSize] = useState(null); - const [apiCacheSize, setApiCacheSize] = useState(null); - const [isLoadingCacheSize, setIsLoadingCacheSize] = useState(true); - const [cacheEntries, setCacheEntries] = useState([]); - const [isLoadingEntries, setIsLoadingEntries] = useState(false); - const [showEntries, setShowEntries] = useState(false); - const [swCacheEntries, setSwCacheEntries] = useState([]); - const [isLoadingSwEntries, setIsLoadingSwEntries] = useState(false); - const [showSwEntries, setShowSwEntries] = useState(false); - const [expandedGroups, setExpandedGroups] = useState>({}); - const [expandedVersions, setExpandedVersions] = useState>({}); - const [ttlConfig, setTTLConfig] = useState( - initialTTLConfig || { - defaultTTL: 5, - homeTTL: 5, - librariesTTL: 1440, - seriesTTL: 5, - booksTTL: 5, - imagesTTL: 1440, - imageCacheMaxAge: 2592000, - } - ); - - const formatBytes = (bytes: number): string => { - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; - }; - - const formatDate = (timestamp: number): string => { - return new Date(timestamp).toLocaleString(); - }; - - const getTimeRemaining = (expiry: number): string => { - const now = Date.now(); - const diff = expiry - now; - - if (diff < 0) return t("settings.cache.entries.expired"); - - const minutes = Math.floor(diff / 60000); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) return t("settings.cache.entries.daysRemaining", { count: days }); - if (hours > 0) return t("settings.cache.entries.hoursRemaining", { count: hours }); - if (minutes > 0) return t("settings.cache.entries.minutesRemaining", { count: minutes }); - return t("settings.cache.entries.lessThanMinute"); - }; - - const getCacheType = (key: string): string => { - if (key.includes("/home")) return "HOME"; - if (key.includes("/libraries")) return "LIBRARIES"; - if (key.includes("/series/")) return "SERIES"; - if (key.includes("/books/")) return "BOOKS"; - if (key.includes("/images/")) return "IMAGES"; - return "DEFAULT"; - }; - - const fetchCacheSize = async () => { - setIsLoadingCacheSize(true); - try { - // Récupérer la taille du cache serveur - const serverResponse = await fetch("/api/komga/cache/size"); - if (serverResponse.ok) { - const serverData = await serverResponse.json(); - setServerCacheSize({ - sizeInBytes: serverData.sizeInBytes, - itemCount: serverData.itemCount, - }); - } - - // Calculer la taille du cache Service Worker - if ("caches" in window) { - const cacheNames = await caches.keys(); - let totalSize = 0; - let apiSize = 0; - - for (const cacheName of cacheNames) { - const cache = await caches.open(cacheName); - const requests = await cache.keys(); - - for (const request of requests) { - const response = await cache.match(request); - if (response) { - const blob = await response.clone().blob(); - totalSize += blob.size; - - // Calculer la taille du cache API séparément - if (cacheName.includes("api")) { - apiSize += blob.size; - } - } - } - } - - setSwCacheSize(totalSize); - setApiCacheSize(apiSize); - } - } catch (error) { - logger.error({ err: error }, "Erreur lors de la récupération de la taille du cache:"); - } finally { - setIsLoadingCacheSize(false); - } - }; - - const fetchCacheEntries = async () => { - setIsLoadingEntries(true); - try { - const response = await fetch("/api/komga/cache/entries"); - if (response.ok) { - const data = await response.json(); - setCacheEntries(data.entries); - } - } catch (error) { - logger.error({ err: error }, "Erreur lors de la récupération des entrées du cache:"); - } finally { - setIsLoadingEntries(false); - } - }; - - const toggleShowEntries = () => { - if (!showEntries && cacheEntries.length === 0) { - fetchCacheEntries(); - } - setShowEntries(!showEntries); - }; - - const fetchSwCacheEntries = async () => { - setIsLoadingSwEntries(true); - try { - if ("caches" in window) { - const entries: ServiceWorkerCacheEntry[] = []; - const cacheNames = await caches.keys(); - - for (const cacheName of cacheNames) { - const cache = await caches.open(cacheName); - const requests = await cache.keys(); - - for (const request of requests) { - const response = await cache.match(request); - if (response) { - const blob = await response.clone().blob(); - entries.push({ - url: request.url, - size: blob.size, - cacheName, - }); - } - } - } - - setSwCacheEntries(entries); - } - } catch (error) { - logger.error({ err: error }, "Erreur lors de la récupération des entrées du cache SW:"); - } finally { - setIsLoadingSwEntries(false); - } - }; - - const toggleShowSwEntries = () => { - if (!showSwEntries && swCacheEntries.length === 0) { - fetchSwCacheEntries(); - } - setShowSwEntries(!showSwEntries); - }; - - const getPathGroup = (url: string): string => { - try { - const urlObj = new URL(url); - const path = urlObj.pathname; - const segments = path.split("/").filter(Boolean); - - if (segments.length === 0) return "/"; - - // Pour /api/komga/images, grouper par type (series/books) - if ( - segments[0] === "api" && - segments[1] === "komga" && - segments[2] === "images" && - segments[3] - ) { - return `/${segments[0]}/${segments[1]}/${segments[2]}/${segments[3]}`; - } - - // Pour les autres, garder juste le premier segment - return `/${segments[0]}`; - } catch { - return "Autres"; - } - }; - - const getBaseUrl = (url: string): string => { - try { - const urlObj = new URL(url); - return urlObj.pathname; - } catch { - return url; - } - }; - - const groupVersions = (entries: ServiceWorkerCacheEntry[]) => { - const grouped = entries.reduce( - (acc, entry) => { - const baseUrl = getBaseUrl(entry.url); - if (!acc[baseUrl]) { - acc[baseUrl] = []; - } - acc[baseUrl].push(entry); - return acc; - }, - {} as Record - ); - - // Trier par date (le plus récent en premier) basé sur le paramètre v - Object.keys(grouped).forEach((key) => { - grouped[key].sort((a, b) => { - const aVersion = new URL(a.url).searchParams.get("v") || "0"; - const bVersion = new URL(b.url).searchParams.get("v") || "0"; - return Number(bVersion) - Number(aVersion); - }); - }); - - return grouped; - }; - - const groupEntriesByPath = (entries: ServiceWorkerCacheEntry[]) => { - const grouped = entries.reduce( - (acc, entry) => { - const pathGroup = getPathGroup(entry.url); - if (!acc[pathGroup]) { - acc[pathGroup] = []; - } - acc[pathGroup].push(entry); - return acc; - }, - {} as Record - ); - - // Trier chaque groupe par taille décroissante - Object.keys(grouped).forEach((key) => { - grouped[key].sort((a, b) => b.size - a.size); - }); - - // Trier les groupes par taille totale décroissante - const sortedGroups: Record = {}; - Object.entries(grouped) - .sort((a, b) => { - const aSize = getTotalSizeByType(a[1]); - const bSize = getTotalSizeByType(b[1]); - return bSize - aSize; - }) - .forEach(([key, value]) => { - sortedGroups[key] = value; - }); - - return sortedGroups; - }; - - const getTotalSizeByType = (entries: ServiceWorkerCacheEntry[]): number => { - return entries.reduce((sum, entry) => sum + entry.size, 0); - }; - - const toggleGroup = (groupName: string) => { - setExpandedGroups((prev) => ({ - ...prev, - [groupName]: !prev[groupName], - })); - }; - - const toggleVersions = (fileName: string) => { - setExpandedVersions((prev) => ({ - ...prev, - [fileName]: !prev[fileName], - })); - }; - - useEffect(() => { - fetchCacheSize(); - }, []); - - const handleClearCache = async () => { - setIsCacheClearing(true); - - try { - const response = await fetch("/api/komga/cache/clear", { - method: "POST", - }); - - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || t("settings.cache.error.message")); - } - - toast({ - title: t("settings.cache.title"), - description: t("settings.cache.messages.cleared"), - }); - - // Rafraîchir la taille du cache et les entrées - await fetchCacheSize(); - if (showEntries) { - await fetchCacheEntries(); - } - if (showSwEntries) { - await fetchSwCacheEntries(); - } - } catch (error) { - logger.error({ err: error }, "Erreur:"); - toast({ - variant: "destructive", - title: t("settings.cache.error.title"), - description: t("settings.cache.error.message"), - }); - } finally { - setIsCacheClearing(false); - } - }; - - const handleClearServiceWorkerCache = async () => { - setIsServiceWorkerClearing(true); - try { - if ("serviceWorker" in navigator && "caches" in window) { - const cacheNames = await caches.keys(); - await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName))); - - // Forcer la mise à jour du service worker - const registrations = await navigator.serviceWorker.getRegistrations(); - for (const registration of registrations) { - await registration.unregister(); - } - - toast({ - title: t("settings.cache.title"), - description: t("settings.cache.messages.serviceWorkerCleared"), - }); - - // Rafraîchir la taille du cache et les entrées - await fetchCacheSize(); - if (showEntries) { - await fetchCacheEntries(); - } - if (showSwEntries) { - await fetchSwCacheEntries(); - } - - // Recharger la page après 1 seconde pour réenregistrer le SW - setTimeout(() => { - window.location.reload(); - }, 1000); - } - } catch (error) { - logger.error({ err: error }, "Erreur lors de la suppression des caches:"); - toast({ - variant: "destructive", - title: t("settings.cache.error.title"), - description: t("settings.cache.error.serviceWorkerMessage"), - }); - } finally { - setIsServiceWorkerClearing(false); - } - }; - - const handleFlushImageCache = () => { - flushImageCache(); - toast({ - title: t("settings.cache.title"), - description: t("settings.cache.messages.imageCacheFlushed"), - }); - }; - - const handleTTLChange = (event: React.ChangeEvent) => { - const { name, value } = event.target; - setTTLConfig((prev) => ({ - ...prev, - [name]: parseInt(value || "0", 10), - })); - }; - - const handleSaveTTL = async (event: React.FormEvent) => { - event.preventDefault(); - - try { - const response = await fetch("/api/komga/ttl-config", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(ttlConfig), - }); - - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || t("settings.cache.error.message")); - } - - toast({ - title: t("settings.cache.title"), - description: t("settings.cache.messages.ttlSaved"), - }); - } catch (error) { - logger.error({ err: error }, "Erreur lors de la sauvegarde:"); - toast({ - variant: "destructive", - title: t("settings.cache.error.title"), - description: t("settings.cache.error.messagettl"), - }); - } - }; - - return ( - - - - - {t("settings.cache.title")} - - {t("settings.cache.description")} - - -
-
- -

{t("settings.cache.mode.description")}

-
- -
- - {/* Informations sur la taille du cache */} -
-
- - {t("settings.cache.size.title")} -
- - {isLoadingCacheSize ? ( -
{t("settings.cache.size.loading")}
- ) : ( -
-
-
{t("settings.cache.size.server")}
- {serverCacheSize ? ( -
-
{formatBytes(serverCacheSize.sizeInBytes)}
-
- {t("settings.cache.size.items", { count: serverCacheSize.itemCount })} -
-
- ) : ( -
- {t("settings.cache.size.error")} -
- )} -
- -
-
{t("settings.cache.size.serviceWorker")}
- {swCacheSize !== null ? ( -
{formatBytes(swCacheSize)}
- ) : ( -
- {t("settings.cache.size.error")} -
- )} -
- -
-
{t("settings.cache.size.api")}
- {apiCacheSize !== null ? ( -
{formatBytes(apiCacheSize)}
- ) : ( -
- {t("settings.cache.size.error")} -
- )} -
-
- )} -
- - {/* Aperçu des entrées du cache serveur */} -
- - - {showEntries && ( -
- {isLoadingEntries ? ( -
- - {t("settings.cache.entries.loading")} -
- ) : cacheEntries.length === 0 ? ( -
- {t("settings.cache.entries.empty")} -
- ) : ( -
-
- {cacheEntries.map((entry, index) => ( -
-
-
-
- {entry.key} -
-
- - {getCacheType(entry.key)} - - {formatBytes(entry.size)} -
-
-
-
- {getTimeRemaining(entry.expiry)} -
-
- {new Date(entry.expiry).toLocaleDateString()} -
-
-
-
- ))} -
-
- )} -
- )} -
- - {/* Aperçu des entrées du cache service worker */} -
- - - {showSwEntries && ( -
- {isLoadingSwEntries ? ( -
- - {t("settings.cache.entries.loading")} -
- ) : swCacheEntries.length === 0 ? ( -
- {t("settings.cache.entries.empty")} -
- ) : ( -
- {(() => { - const grouped = groupEntriesByPath(swCacheEntries); - return ( -
- {Object.entries(grouped).map(([pathGroup, entries]) => { - const isExpanded = expandedGroups[pathGroup]; - return ( -
- - {isExpanded && ( -
- {(() => { - const versionGroups = groupVersions(entries); - return Object.entries(versionGroups).map( - ([baseUrl, versions]) => { - const hasMultipleVersions = versions.length > 1; - const isVersionExpanded = expandedVersions[baseUrl]; - const totalSize = versions.reduce( - (sum, v) => sum + v.size, - 0 - ); - - if (!hasMultipleVersions) { - const entry = versions[0]; - return ( -
-
-
-
- {entry.url.replace(/^https?:\/\/[^/]+/, "")} -
-
-
- {formatBytes(entry.size)} -
-
-
- ); - } - - return ( -
- - {isVersionExpanded && ( -
- {versions.map((version, vIdx) => ( -
-
-
- {new URL(version.url).search || - "(no version)"} -
-
-
- {formatBytes(version.size)} -
-
- ))} -
- )} -
- ); - } - ); - })()} -
- )} -
- ); - })} -
- ); - })()} -
- )} -
- )} -
- - {/* Formulaire TTL */} -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -

- {t("settings.cache.ttl.imageCacheMaxAge.description")} -

-
- -
-
-
- - -
-
- -
-
- -
-
-
-
- ); -} diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx index 55786dc..0731081 100644 --- a/src/components/settings/ClientSettings.tsx +++ b/src/components/settings/ClientSettings.tsx @@ -1,21 +1,19 @@ "use client"; -import type { KomgaConfig, TTLConfigData } from "@/types/komga"; +import type { KomgaConfig } from "@/types/komga"; import { useTranslate } from "@/hooks/useTranslate"; 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"; +import { Monitor, Network } from "lucide-react"; interface ClientSettingsProps { initialConfig: KomgaConfig | null; - initialTTLConfig: TTLConfigData | null; } -export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettingsProps) { +export function ClientSettings({ initialConfig }: ClientSettingsProps) { const { t } = useTranslate(); return ( @@ -23,7 +21,7 @@ export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettin

{t("settings.title")}

- + {t("settings.tabs.display")} @@ -32,10 +30,6 @@ export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettin {t("settings.tabs.connection")} - - - {t("settings.tabs.cache")} - @@ -47,10 +41,6 @@ export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettin - - - -
); diff --git a/src/components/ui/book-cover.tsx b/src/components/ui/book-cover.tsx index cf9a9d8..d03a56f 100644 --- a/src/components/ui/book-cover.tsx +++ b/src/components/ui/book-cover.tsx @@ -4,7 +4,6 @@ import { CoverClient } from "./cover-client"; import { ProgressBar } from "./progress-bar"; import type { BookCoverProps } from "./cover-utils"; import { getImageUrl } from "@/lib/utils/image-url"; -import { useImageUrl } from "@/hooks/useImageUrl"; import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; import { MarkAsReadButton } from "./mark-as-read-button"; import { MarkAsUnreadButton } from "./mark-as-unread-button"; @@ -63,8 +62,7 @@ export function BookCover({ const { t } = useTranslate(); const { isAccessible } = useBookOfflineStatus(book.id); - const baseUrl = getImageUrl("book", book.id); - const imageUrl = useImageUrl(baseUrl); + const imageUrl = getImageUrl("book", book.id); const isCompleted = book.readProgress?.completed || false; const currentPage = ClientOfflineBookService.getCurrentPage(book); diff --git a/src/components/ui/series-cover.tsx b/src/components/ui/series-cover.tsx index a14c162..5757f37 100644 --- a/src/components/ui/series-cover.tsx +++ b/src/components/ui/series-cover.tsx @@ -4,7 +4,6 @@ import { CoverClient } from "./cover-client"; import { ProgressBar } from "./progress-bar"; import type { SeriesCoverProps } from "./cover-utils"; import { getImageUrl } from "@/lib/utils/image-url"; -import { useImageUrl } from "@/hooks/useImageUrl"; export function SeriesCover({ series, @@ -12,8 +11,7 @@ export function SeriesCover({ className, showProgressUi = true, }: SeriesCoverProps) { - const baseUrl = getImageUrl("series", series.id); - const imageUrl = useImageUrl(baseUrl); + const imageUrl = getImageUrl("series", series.id); const isCompleted = series.booksCount === series.booksReadCount; const readBooks = series.booksReadCount; diff --git a/src/constants/errorCodes.ts b/src/constants/errorCodes.ts index f30467d..5c6bca9 100644 --- a/src/constants/errorCodes.ts +++ b/src/constants/errorCodes.ts @@ -63,16 +63,6 @@ export const ERROR_CODES = { UPDATE_ERROR: "PREFERENCES_UPDATE_ERROR", CONTEXT_ERROR: "PREFERENCES_CONTEXT_ERROR", }, - CACHE: { - DELETE_ERROR: "CACHE_DELETE_ERROR", - SAVE_ERROR: "CACHE_SAVE_ERROR", - LOAD_ERROR: "CACHE_LOAD_ERROR", - CLEAR_ERROR: "CACHE_CLEAR_ERROR", - MODE_FETCH_ERROR: "CACHE_MODE_FETCH_ERROR", - MODE_UPDATE_ERROR: "CACHE_MODE_UPDATE_ERROR", - INVALID_MODE: "CACHE_INVALID_MODE", - SIZE_FETCH_ERROR: "CACHE_SIZE_FETCH_ERROR", - }, UI: { TABS_TRIGGER_ERROR: "UI_TABS_TRIGGER_ERROR", TABS_CONTENT_ERROR: "UI_TABS_CONTENT_ERROR", diff --git a/src/constants/errorMessages.ts b/src/constants/errorMessages.ts index e107de9..e0f68c5 100644 --- a/src/constants/errorMessages.ts +++ b/src/constants/errorMessages.ts @@ -60,16 +60,6 @@ export const ERROR_MESSAGES: Record = { [ERROR_CODES.PREFERENCES.CONTEXT_ERROR]: "🔄 usePreferences must be used within a PreferencesProvider", - // Cache - [ERROR_CODES.CACHE.DELETE_ERROR]: "🗑️ Error deleting cache", - [ERROR_CODES.CACHE.SAVE_ERROR]: "💾 Error saving to cache", - [ERROR_CODES.CACHE.LOAD_ERROR]: "📂 Error loading from cache", - [ERROR_CODES.CACHE.CLEAR_ERROR]: "🧹 Error clearing cache completely", - [ERROR_CODES.CACHE.MODE_FETCH_ERROR]: "⚙️ Error fetching cache mode", - [ERROR_CODES.CACHE.MODE_UPDATE_ERROR]: "⚙️ Error updating cache mode", - [ERROR_CODES.CACHE.INVALID_MODE]: "⚠️ Invalid cache mode. Must be 'file' or 'memory'", - [ERROR_CODES.CACHE.SIZE_FETCH_ERROR]: "📊 Error fetching cache size", - // UI [ERROR_CODES.UI.TABS_TRIGGER_ERROR]: "🔄 TabsTrigger must be used within a Tabs component", [ERROR_CODES.UI.TABS_CONTENT_ERROR]: "🔄 TabsContent must be used within a Tabs component", diff --git a/src/contexts/ImageCacheContext.tsx b/src/contexts/ImageCacheContext.tsx deleted file mode 100644 index f33c677..0000000 --- a/src/contexts/ImageCacheContext.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import React, { createContext, useContext, useState, useCallback, useEffect } from "react"; -import logger from "@/lib/logger"; - -interface ImageCacheContextType { - cacheVersion: string; - flushImageCache: () => void; - getImageUrl: (baseUrl: string) => string; -} - -const ImageCacheContext = createContext(undefined); - -export function ImageCacheProvider({ children }: { children: React.ReactNode }) { - const [cacheVersion, setCacheVersion] = useState(""); - - // Initialiser la version depuis localStorage au montage - useEffect(() => { - const storedVersion = localStorage.getItem("imageCacheVersion"); - if (storedVersion) { - setCacheVersion(storedVersion); - } else { - const newVersion = Date.now().toString(); - setCacheVersion(newVersion); - localStorage.setItem("imageCacheVersion", newVersion); - } - }, []); - - const flushImageCache = useCallback(() => { - const newVersion = Date.now().toString(); - setCacheVersion(newVersion); - localStorage.setItem("imageCacheVersion", newVersion); - logger.info(`🗑️ Image cache flushed - new version: ${newVersion}`); - }, []); - - const getImageUrl = useCallback( - (baseUrl: string) => { - if (!cacheVersion) return baseUrl; - const separator = baseUrl.includes("?") ? "&" : "?"; - return `${baseUrl}${separator}v=${cacheVersion}`; - }, - [cacheVersion] - ); - - return ( - - {children} - - ); -} - -export function useImageCache() { - const context = useContext(ImageCacheContext); - if (context === undefined) { - throw new Error("useImageCache must be used within an ImageCacheProvider"); - } - return context; -} diff --git a/src/hooks/useImageUrl.ts b/src/hooks/useImageUrl.ts deleted file mode 100644 index 70aeab8..0000000 --- a/src/hooks/useImageUrl.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useImageCache } from "@/contexts/ImageCacheContext"; -import { useMemo } from "react"; - -/** - * Hook pour obtenir une URL d'image avec cache busting - * Ajoute automatiquement ?v={cacheVersion} à l'URL - */ -export function useImageUrl(baseUrl: string): string { - const { getImageUrl } = useImageCache(); - - return useMemo(() => { - return getImageUrl(baseUrl); - }, [baseUrl, getImageUrl]); -} diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json index 96118d0..ba10d87 100644 --- a/src/i18n/messages/en/common.json +++ b/src/i18n/messages/en/common.json @@ -69,8 +69,7 @@ "title": "Preferences", "tabs": { "display": "Display", - "connection": "Connection", - "cache": "Cache" + "connection": "Connection" }, "display": { "title": "Display Preferences", @@ -106,31 +105,9 @@ "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": { @@ -159,75 +136,6 @@ "title": "Error saving configuration", "message": "An error occurred while saving the configuration" } - }, - "cache": { - "title": "Cache Configuration", - "description": "Manage data caching settings.", - "mode": { - "label": "Cache mode", - "description": "Memory cache is faster but doesn't persist between restarts" - }, - "size": { - "title": "Cache size", - "server": "Server cache", - "serviceWorker": "SW cache (total)", - "api": "API cache (data)", - "items": "{count} item(s)", - "loading": "Loading...", - "error": "Error loading" - }, - "ttl": { - "default": "Default TTL (minutes)", - "home": "Home page TTL", - "libraries": "Libraries TTL", - "series": "Series TTL", - "books": "Books TTL", - "images": "Images TTL", - "imageCacheMaxAge": { - "label": "HTTP Image Cache Duration", - "description": "Duration for images to be cached in the browser", - "options": { - "noCache": "No cache (0s)", - "oneHour": "1 hour (3600s)", - "oneDay": "1 day (86400s)", - "oneWeek": "1 week (604800s)", - "oneMonth": "1 month (2592000s) - Recommended", - "oneYear": "1 year (31536000s)" - } - } - }, - "buttons": { - "saveTTL": "Save TTL", - "clear": "Clear cache", - "clearing": "Clearing...", - "clearServiceWorker": "Clear service worker cache", - "clearingServiceWorker": "Clearing service worker cache...", - "flushImageCache": "Force reload images" - }, - "messages": { - "ttlSaved": "TTL configuration saved successfully", - "cleared": "Server cache cleared successfully", - "serviceWorkerCleared": "Service worker cache cleared successfully", - "imageCacheFlushed": "Images will be reloaded - refresh the page" - }, - "error": { - "title": "Error clearing cache", - "message": "An error occurred while clearing the cache", - "ttl": "Error saving TTL configuration", - "serviceWorkerMessage": "An error occurred while clearing the service worker cache" - }, - "entries": { - "title": "Cache content preview", - "serverTitle": "Server cache preview", - "serviceWorkerTitle": "Service worker cache preview", - "loading": "Loading entries...", - "empty": "No entries in cache", - "expired": "Expired", - "daysRemaining": "{count} day(s) remaining", - "hoursRemaining": "{count} hour(s) remaining", - "minutesRemaining": "{count} minute(s) remaining", - "lessThanMinute": "Less than a minute" - } } }, "library": { @@ -463,14 +371,6 @@ "PREFERENCES_UPDATE_ERROR": "Error updating preferences", "PREFERENCES_CONTEXT_ERROR": "Preferences context error", - "CACHE_DELETE_ERROR": "Error deleting cache", - "CACHE_SAVE_ERROR": "Error saving cache", - "CACHE_LOAD_ERROR": "Error loading cache", - "CACHE_CLEAR_ERROR": "Error clearing cache", - "CACHE_MODE_FETCH_ERROR": "Error fetching cache mode", - "CACHE_MODE_UPDATE_ERROR": "Error updating cache mode", - "CACHE_INVALID_MODE": "Invalid cache mode", - "UI_TABS_TRIGGER_ERROR": "Error triggering tabs", "UI_TABS_CONTENT_ERROR": "Error loading tabs content", diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json index 1b830f6..651e3f5 100644 --- a/src/i18n/messages/fr/common.json +++ b/src/i18n/messages/fr/common.json @@ -69,8 +69,7 @@ "title": "Préférences", "tabs": { "display": "Affichage", - "connection": "Connexion", - "cache": "Cache" + "connection": "Connexion" }, "display": { "title": "Préférences d'affichage", @@ -106,31 +105,9 @@ "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": { @@ -159,75 +136,6 @@ "title": "Erreur lors de la sauvegarde de la configuration", "message": "Une erreur est survenue lors de la sauvegarde de la configuration" } - }, - "cache": { - "title": "Configuration du Cache", - "description": "Gérez les paramètres de mise en cache des données.", - "mode": { - "label": "Mode de cache", - "description": "Le cache en mémoire est plus rapide mais ne persiste pas entre les redémarrages" - }, - "size": { - "title": "Taille du cache", - "server": "Cache serveur", - "serviceWorker": "Cache SW (total)", - "api": "Cache API (données)", - "items": "{count} élément(s)", - "loading": "Chargement...", - "error": "Erreur lors du chargement" - }, - "ttl": { - "default": "TTL par défaut (minutes)", - "home": "TTL page d'accueil", - "libraries": "TTL bibliothèques", - "series": "TTL séries", - "books": "TTL tomes", - "images": "TTL images", - "imageCacheMaxAge": { - "label": "Durée du cache HTTP des images", - "description": "Durée de conservation des images dans le cache du navigateur", - "options": { - "noCache": "Aucun cache (0s)", - "oneHour": "1 heure (3600s)", - "oneDay": "1 jour (86400s)", - "oneWeek": "1 semaine (604800s)", - "oneMonth": "1 mois (2592000s) - Recommandé", - "oneYear": "1 an (31536000s)" - } - } - }, - "buttons": { - "saveTTL": "Sauvegarder les TTL", - "clear": "Vider le cache", - "clearing": "Suppression...", - "clearServiceWorker": "Vider le cache du service worker", - "clearingServiceWorker": "Suppression du cache service worker...", - "flushImageCache": "Forcer le rechargement des images" - }, - "messages": { - "ttlSaved": "La configuration des TTL a été sauvegardée avec succès", - "cleared": "Cache serveur supprimé avec succès", - "serviceWorkerCleared": "Cache du service worker supprimé avec succès", - "imageCacheFlushed": "Les images seront rechargées - rafraîchissez la page" - }, - "error": { - "title": "Erreur lors de la suppression du cache", - "message": "Une erreur est survenue lors de la suppression du cache", - "ttl": "Erreur lors de la sauvegarde de la configuration TTL", - "serviceWorkerMessage": "Une erreur est survenue lors de la suppression du cache du service worker" - }, - "entries": { - "title": "Aperçu du contenu du cache", - "serverTitle": "Aperçu du cache serveur", - "serviceWorkerTitle": "Aperçu du cache service worker", - "loading": "Chargement des entrées...", - "empty": "Aucune entrée dans le cache", - "expired": "Expiré", - "daysRemaining": "{count} jour(s) restant(s)", - "hoursRemaining": "{count} heure(s) restante(s)", - "minutesRemaining": "{count} minute(s) restante(s)", - "lessThanMinute": "Moins d'une minute" - } } }, "library": { @@ -461,14 +369,6 @@ "PREFERENCES_UPDATE_ERROR": "Erreur lors de la mise à jour des préférences", "PREFERENCES_CONTEXT_ERROR": "Erreur de contexte des préférences", - "CACHE_DELETE_ERROR": "Erreur lors de la suppression du cache", - "CACHE_SAVE_ERROR": "Erreur lors de la sauvegarde du cache", - "CACHE_LOAD_ERROR": "Erreur lors du chargement du cache", - "CACHE_CLEAR_ERROR": "Erreur lors de la suppression du cache", - "CACHE_MODE_FETCH_ERROR": "Erreur lors de la récupération du mode de cache", - "CACHE_MODE_UPDATE_ERROR": "Erreur lors de la mise à jour du mode de cache", - "CACHE_INVALID_MODE": "Mode de cache invalide", - "UI_TABS_TRIGGER_ERROR": "Erreur lors du déclenchement des onglets", "UI_TABS_CONTENT_ERROR": "Erreur lors du chargement du contenu des onglets", diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts index 6c0aba1..393c659 100644 --- a/src/lib/services/base-api.service.ts +++ b/src/lib/services/base-api.service.ts @@ -1,19 +1,10 @@ import type { AuthConfig } from "@/types/auth"; -import type { CacheType } from "@/types/cache"; -import { getServerCacheService } from "./server-cache.service"; import { ConfigDBService } from "./config-db.service"; import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; import type { KomgaConfig } from "@/types/komga"; -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"; import logger from "@/lib/logger"; -export type { CacheType }; - interface KomgaRequestInit extends RequestInit { isImage?: boolean; noJson?: boolean; @@ -25,68 +16,7 @@ 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) { - 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"); - } - } - - /** - * 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) { - logger.error({ err: error }, "Failed to get preferences for circuit breaker"); - return { - threshold: 5, - timeout: 30000, - resetTimeout: 60000, - }; - } - }); - - this.circuitBreakerInitialized = true; - } catch (error) { - logger.error({ err: error }, "Failed to initialize circuit breaker"); - } - } - 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) { @@ -117,22 +47,6 @@ export abstract class BaseApiService { }); } - protected static async fetchWithCache( - key: string, - fetcher: () => Promise, - type: CacheType = "DEFAULT" - ): Promise { - const cacheService: ServerCacheService = await getServerCacheService(); - - try { - const result = await cacheService.getOrSet(key, fetcher, type); - - return result; - } catch (error) { - throw error; - } - } - protected static buildUrl( config: AuthConfig, path: string, @@ -159,12 +73,6 @@ export abstract class BaseApiService { return url.toString(); } - protected static async resolveWithFallback(url: string): Promise { - // DNS resolution is only needed server-side and causes build issues - // The fetch API will handle DNS resolution automatically - return url; - } - protected static async fetchFromApi( urlBuilder: KomgaUrlBuilder, headersOptions = {}, @@ -172,7 +80,7 @@ export abstract class BaseApiService { ): Promise { const config: AuthConfig = await this.getKomgaConfig(); const { path, params } = urlBuilder; - const url = await this.resolveWithFallback(this.buildUrl(config, path, params)); + const url = this.buildUrl(config, path, params); const headers: Headers = this.getAuthHeaders(config); if (headersOptions) { @@ -185,10 +93,6 @@ export abstract class BaseApiService { const startTime = isDebug ? Date.now() : 0; if (isDebug) { - const queueStats = { - active: RequestQueueService.getActiveCount(), - queued: RequestQueueService.getQueueLength(), - }; logger.info( { url, @@ -196,74 +100,64 @@ export abstract class BaseApiService { params, isImage: options.isImage, noJson: options.noJson, - queue: queueStats, }, "🔵 Komga Request" ); } - // Timeout réduit à 15 secondes pour éviter les blocages longs + // Timeout de 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...` - ); + let response: Response; - // 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; - } + try { + response = await fetch(url, { + headers, + ...options, + signal: controller.signal, + // @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...`); + + response = 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, + }); + } else if (fetchError?.cause?.code === "UND_ERR_CONNECT_TIMEOUT") { + // Retry automatique sur timeout de connexion (cold start) + logger.info(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`); + + response = await fetch(url, { + headers, + ...options, + signal: controller.signal, + // @ts-ignore - undici-specific options + connectTimeout: timeoutMs, + bodyTimeout: timeoutMs, + headersTimeout: timeoutMs, + }); + } else { + throw fetchError; + } + } + clearTimeout(timeoutId); if (isDebug) { @@ -320,7 +214,6 @@ export abstract class BaseApiService { throw error; } finally { clearTimeout(timeoutId); - RequestMonitorService.decrementActive(); } } } diff --git a/src/lib/services/book.service.ts b/src/lib/services/book.service.ts index d691c94..ebe887c 100644 --- a/src/lib/services/book.service.ts +++ b/src/lib/services/book.service.ts @@ -1,47 +1,33 @@ import { BaseApiService } from "./base-api.service"; -import type { KomgaBook, KomgaBookWithPages, TTLConfig } from "@/types/komga"; +import type { KomgaBook, KomgaBookWithPages } from "@/types/komga"; import type { ImageResponse } from "./image.service"; import { ImageService } from "./image.service"; import { PreferencesService } from "./preferences.service"; -import { ConfigDBService } from "./config-db.service"; import { SeriesService } from "./series.service"; import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; -import logger from "@/lib/logger"; + +// Cache HTTP navigateur : 30 jours (immutable car les images ne changent pas) +const IMAGE_CACHE_MAX_AGE = 2592000; export class BookService extends BaseApiService { - private static async getImageCacheMaxAge(): Promise { - try { - const ttlConfig: TTLConfig | null = await ConfigDBService.getTTLConfig(); - const maxAge = ttlConfig?.imageCacheMaxAge ?? 2592000; - return maxAge; - } catch (error) { - logger.error({ err: error }, "[ImageCache] Error fetching TTL config"); - return 2592000; // 30 jours par défaut en cas d'erreur - } - } static async getBook(bookId: string): Promise { try { - return this.fetchWithCache( - `book-${bookId}`, - async () => { - // Récupération parallèle des détails du tome et des pages - const [book, pages] = await Promise.all([ - this.fetchFromApi({ path: `books/${bookId}` }), - this.fetchFromApi<{ number: number }[]>({ path: `books/${bookId}/pages` }), - ]); + // Récupération parallèle des détails du tome et des pages + const [book, pages] = await Promise.all([ + this.fetchFromApi({ path: `books/${bookId}` }), + this.fetchFromApi<{ number: number }[]>({ path: `books/${bookId}/pages` }), + ]); - return { - book, - pages: pages.map((page: any) => page.number), - }; - }, - "BOOKS" - ); + return { + book, + pages: pages.map((page: any) => page.number), + }; } catch (error) { throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error); } } + public static async getNextBook(bookId: string, _seriesId: string): Promise { try { // Utiliser l'endpoint natif Komga pour obtenir le livre suivant @@ -63,7 +49,6 @@ export class BookService extends BaseApiService { static async getBookSeriesId(bookId: string): Promise { try { - // Récupérer le livre sans cache pour éviter les données obsolètes const book = await this.fetchFromApi({ path: `books/${bookId}` }); return book.seriesId; } catch (error) { @@ -136,12 +121,10 @@ export class BookService extends BaseApiService { response.buffer.byteOffset + response.buffer.byteLength ) as ArrayBuffer; - const maxAge = await this.getImageCacheMaxAge(); - return new Response(arrayBuffer, { headers: { "Content-Type": response.contentType || "image/jpeg", - "Cache-Control": `public, max-age=${maxAge}, immutable`, + "Cache-Control": `public, max-age=${IMAGE_CACHE_MAX_AGE}, immutable`, }, }); } catch (error) { @@ -153,7 +136,6 @@ export class BookService extends BaseApiService { try { // Récupérer les préférences de l'utilisateur const preferences = await PreferencesService.getPreferences(); - const maxAge = await this.getImageCacheMaxAge(); // Si l'utilisateur préfère les vignettes, utiliser la miniature if (preferences.showThumbnails) { @@ -161,7 +143,7 @@ export class BookService extends BaseApiService { return new Response(response.buffer.buffer as ArrayBuffer, { headers: { "Content-Type": response.contentType || "image/jpeg", - "Cache-Control": `public, max-age=${maxAge}, immutable`, + "Cache-Control": `public, max-age=${IMAGE_CACHE_MAX_AGE}, immutable`, }, }); } @@ -186,12 +168,11 @@ export class BookService extends BaseApiService { const response: ImageResponse = await ImageService.getImage( `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", - "Cache-Control": `public, max-age=${maxAge}, immutable`, + "Cache-Control": `public, max-age=${IMAGE_CACHE_MAX_AGE}, immutable`, }, }); } catch (error) { diff --git a/src/lib/services/circuit-breaker.service.ts b/src/lib/services/circuit-breaker.service.ts deleted file mode 100644 index 32ae8a6..0000000 --- a/src/lib/services/circuit-breaker.service.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * 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"; -import logger from "@/lib/logger"; - -interface CircuitBreakerState { - state: "CLOSED" | "OPEN" | "HALF_OPEN"; - failureCount: number; - lastFailureTime: number; - nextAttemptTime: number; -} - -class CircuitBreaker { - private state: CircuitBreakerState = { - state: "CLOSED", - failureCount: 0, - lastFailureTime: 0, - nextAttemptTime: 0, - }; - - private config = { - failureThreshold: 5, // Nombre d'échecs avant ouverture - recoveryTimeout: 30000, // 30s avant tentative de récupération - 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) { - logger.error({ err: error }, "Error getting circuit breaker config from preferences"); - 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"); - } - this.state.state = "HALF_OPEN"; - } - - try { - const result = await operation(); - this.onSuccess(); - return result; - } catch (error) { - await this.onFailure(config); - throw error; - } - } - - private onSuccess(): void { - if (this.state.state === "HALF_OPEN") { - this.state.failureCount = 0; - this.state.state = "CLOSED"; - logger.info("[CIRCUIT-BREAKER] ✅ Circuit closed - Komga recovered"); - } - } - - private async onFailure(config: typeof this.config): Promise { - this.state.failureCount++; - this.state.lastFailureTime = Date.now(); - - if (this.state.failureCount >= config.failureThreshold) { - 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)` - ); - } - } - - getState(): CircuitBreakerState { - return { ...this.state }; - } - - reset(): void { - this.state = { - state: "CLOSED", - failureCount: 0, - lastFailureTime: 0, - nextAttemptTime: 0, - }; - logger.info("[CIRCUIT-BREAKER] 🔄 Circuit reset"); - } -} - -export const CircuitBreakerService = new CircuitBreaker(); diff --git a/src/lib/services/config-db.service.ts b/src/lib/services/config-db.service.ts index 7dc7683..9083aea 100644 --- a/src/lib/services/config-db.service.ts +++ b/src/lib/services/config-db.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 { User, KomgaConfigData, TTLConfigData, KomgaConfig, TTLConfig } from "@/types/komga"; +import type { User, KomgaConfigData, KomgaConfig } from "@/types/komga"; export class ConfigDBService { private static async getCurrentUser(): Promise { @@ -62,58 +62,4 @@ export class ConfigDBService { throw new AppError(ERROR_CODES.CONFIG.FETCH_ERROR, {}, error); } } - - static async getTTLConfig(): Promise { - try { - const user: User | null = await this.getCurrentUser(); - const userId = parseInt(user.id, 10); - - const config = await prisma.tTLConfig.findUnique({ - where: { userId }, - }); - return config as TTLConfig | null; - } catch (error) { - if (error instanceof AppError) { - throw error; - } - throw new AppError(ERROR_CODES.CONFIG.TTL_FETCH_ERROR, {}, error); - } - } - - static async saveTTLConfig(data: TTLConfigData): Promise { - try { - const user: User | null = await this.getCurrentUser(); - const userId = parseInt(user.id, 10); - - const config = await prisma.tTLConfig.upsert({ - where: { userId }, - update: { - defaultTTL: data.defaultTTL, - homeTTL: data.homeTTL, - librariesTTL: data.librariesTTL, - seriesTTL: data.seriesTTL, - booksTTL: data.booksTTL, - imagesTTL: data.imagesTTL, - imageCacheMaxAge: data.imageCacheMaxAge, - }, - create: { - userId, - defaultTTL: data.defaultTTL, - homeTTL: data.homeTTL, - librariesTTL: data.librariesTTL, - seriesTTL: data.seriesTTL, - booksTTL: data.booksTTL, - imagesTTL: data.imagesTTL, - imageCacheMaxAge: data.imageCacheMaxAge, - }, - }); - - return config as TTLConfig; - } catch (error) { - if (error instanceof AppError) { - throw error; - } - throw new AppError(ERROR_CODES.CONFIG.TTL_SAVE_ERROR, {}, error); - } - } } diff --git a/src/lib/services/home.service.ts b/src/lib/services/home.service.ts index 9d4ffd0..c6eead2 100644 --- a/src/lib/services/home.service.ts +++ b/src/lib/services/home.service.ts @@ -2,7 +2,6 @@ import { BaseApiService } from "./base-api.service"; import type { KomgaBook, KomgaSeries } from "@/types/komga"; import type { LibraryResponse } from "@/types/library"; import type { HomeData } from "@/types/home"; -import { getServerCacheService } from "./server-cache.service"; import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; @@ -12,80 +11,55 @@ export class HomeService extends BaseApiService { static async getHomeData(): Promise { try { const [ongoing, ongoingBooks, recentlyRead, onDeck, latestSeries] = await Promise.all([ - this.fetchWithCache>( - "home-ongoing", - async () => - this.fetchFromApi>({ - path: "series", - params: { - read_status: "IN_PROGRESS", - sort: "readDate,desc", - page: "0", - size: "10", - media_status: "READY", - }, - }), - "HOME" - ), - this.fetchWithCache>( - "home-ongoing-books", - async () => - this.fetchFromApi>({ - path: "books", - params: { - read_status: "IN_PROGRESS", - sort: "readProgress.readDate,desc", - page: "0", - size: "10", - media_status: "READY", - }, - }), - "HOME" - ), - this.fetchWithCache>( - "home-recently-read", - async () => - this.fetchFromApi>({ - path: "books/latest", - params: { - page: "0", - size: "10", - media_status: "READY", - }, - }), - "HOME" - ), - this.fetchWithCache>( - "home-on-deck", - async () => - this.fetchFromApi>({ - path: "books/ondeck", - params: { - page: "0", - size: "10", - media_status: "READY", - }, - }), - "HOME" - ), - this.fetchWithCache>( - "home-latest-series", - async () => - this.fetchFromApi>({ - path: "series/latest", - params: { - page: "0", - size: "10", - media_status: "READY", - }, - }), - "HOME" - ), + this.fetchFromApi>({ + path: "series", + params: { + read_status: "IN_PROGRESS", + sort: "readDate,desc", + page: "0", + size: "10", + media_status: "READY", + }, + }), + this.fetchFromApi>({ + path: "books", + params: { + read_status: "IN_PROGRESS", + sort: "readProgress.readDate,desc", + page: "0", + size: "10", + media_status: "READY", + }, + }), + this.fetchFromApi>({ + path: "books/latest", + params: { + page: "0", + size: "10", + media_status: "READY", + }, + }), + this.fetchFromApi>({ + path: "books/ondeck", + params: { + page: "0", + size: "10", + media_status: "READY", + }, + }), + this.fetchFromApi>({ + path: "series/latest", + params: { + page: "0", + size: "10", + media_status: "READY", + }, + }), ]); return { ongoing: ongoing.content || [], - ongoingBooks: ongoingBooks.content || [], // Nouveau champ + ongoingBooks: ongoingBooks.content || [], recentlyRead: recentlyRead.content || [], onDeck: onDeck.content || [], latestSeries: latestSeries.content || [], @@ -97,17 +71,4 @@ export class HomeService extends BaseApiService { throw new AppError(ERROR_CODES.HOME.FETCH_ERROR, {}, error); } } - - static async invalidateHomeCache(): Promise { - try { - const cacheService = await getServerCacheService(); - await cacheService.delete("home-ongoing"); - await cacheService.delete("home-ongoing-books"); // Nouvelle clé de cache - await cacheService.delete("home-recently-read"); - await cacheService.delete("home-on-deck"); - await cacheService.delete("home-latest-series"); - } catch (error) { - throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error); - } - } } diff --git a/src/lib/services/library.service.ts b/src/lib/services/library.service.ts index 20de65d..1427dbd 100644 --- a/src/lib/services/library.service.ts +++ b/src/lib/services/library.service.ts @@ -1,7 +1,6 @@ import { BaseApiService } from "./base-api.service"; import type { LibraryResponse } from "@/types/library"; import type { Series } from "@/types/series"; -import { getServerCacheService } from "./server-cache.service"; import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; import type { KomgaLibrary } from "@/types/komga"; @@ -9,11 +8,7 @@ import type { KomgaLibrary } from "@/types/komga"; export class LibraryService extends BaseApiService { static async getLibraries(): Promise { try { - return this.fetchWithCache( - "libraries", - async () => this.fetchFromApi({ path: "libraries" }), - "LIBRARIES" - ); + return this.fetchFromApi({ path: "libraries" }); } catch (error) { throw new AppError(ERROR_CODES.LIBRARY.FETCH_ERROR, {}, error); } @@ -87,35 +82,24 @@ export class LibraryService extends BaseApiService { const searchBody = { condition }; - // Clé de cache incluant tous les paramètres - const cacheKey = `library-${libraryId}-series-p${page}-s${size}-u${unreadOnly}-q${ - search || "" - }`; + const params: Record = { + page: String(page), + size: String(size), + sort: "metadata.titleSort,asc", + }; - const response = await this.fetchWithCache>( - cacheKey, - async () => { - const params: Record = { - page: String(page), - size: String(size), - sort: "metadata.titleSort,asc", - }; + // Filtre de recherche Komga (recherche dans le titre) + if (search) { + params.search = search; + } - // Filtre de recherche Komga (recherche dans le titre) - if (search) { - params.search = search; - } - - return this.fetchFromApi>( - { path: "series/list", params }, - headers, - { - method: "POST", - body: JSON.stringify(searchBody), - } - ); - }, - "SERIES" + const response = await this.fetchFromApi>( + { path: "series/list", params }, + headers, + { + method: "POST", + body: JSON.stringify(searchBody), + } ); // Filtrer uniquement les séries supprimées côté client (léger) @@ -131,17 +115,6 @@ export class LibraryService extends BaseApiService { } } - static async invalidateLibrarySeriesCache(libraryId: string): Promise { - try { - const cacheService = await getServerCacheService(); - // Invalider toutes les clés de cache pour cette bibliothèque - // Format: library-{id}-series-p{page}-s{size}-u{unread}-q{search} - await cacheService.deleteAll(`library-${libraryId}-series-`); - } catch (error) { - throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error); - } - } - static async scanLibrary(libraryId: string, deep: boolean = false): Promise { try { await this.fetchFromApi( diff --git a/src/lib/services/preferences.service.ts b/src/lib/services/preferences.service.ts index 9bb6a3e..d80aeda 100644 --- a/src/lib/services/preferences.service.ts +++ b/src/lib/services/preferences.service.ts @@ -2,11 +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, - CircuitBreakerConfig, -} from "@/types/preferences"; +import type { UserPreferences, BackgroundPreferences } from "@/types/preferences"; import { defaultPreferences } from "@/types/preferences"; import type { User } from "@/types/komga"; import type { Prisma } from "@prisma/client"; @@ -37,7 +33,6 @@ export class PreferencesService { return { showThumbnails: preferences.showThumbnails, - cacheMode: preferences.cacheMode as "memory" | "file", showOnlyUnread: preferences.showOnlyUnread, displayMode: { ...defaultPreferences.displayMode, @@ -45,9 +40,7 @@ export class PreferencesService { viewMode: displayMode?.viewMode || defaultPreferences.displayMode.viewMode, }, 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) { @@ -65,17 +58,12 @@ export class PreferencesService { const updateData: Record = {}; 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.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 }, @@ -83,28 +71,20 @@ export class PreferencesService { create: { userId, showThumbnails: preferences.showThumbnails ?? defaultPreferences.showThumbnails, - 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, - komgaMaxConcurrentRequests: preferences.komgaMaxConcurrentRequests ?? 5, readerPrefetchCount: preferences.readerPrefetchCount ?? 5, }, }); return { showThumbnails: updatedPreferences.showThumbnails, - cacheMode: updatedPreferences.cacheMode as "memory" | "file", 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-monitor.service.ts b/src/lib/services/request-monitor.service.ts deleted file mode 100644 index 1a4f9c3..0000000 --- a/src/lib/services/request-monitor.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Service de monitoring des requêtes concurrentes vers Komga - * Permet de tracker le nombre de requêtes actives et d'alerter en cas de charge élevée - */ -import logger from "@/lib/logger"; - -class RequestMonitor { - private activeRequests = 0; - private readonly thresholds = { - warning: 10, - high: 20, - critical: 30, - }; - - incrementActive(): number { - this.activeRequests++; - this.checkThresholds(); - return this.activeRequests; - } - - decrementActive(): number { - this.activeRequests = Math.max(0, this.activeRequests - 1); - return this.activeRequests; - } - - getActiveCount(): number { - return this.activeRequests; - } - - 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) { - logger.warn(`[REQUEST-MONITOR] ⚠️ HIGH concurrency: ${count} active requests`); - } else if (count >= this.thresholds.warning) { - logger.info(`[REQUEST-MONITOR] ⚡ Warning concurrency: ${count} active requests`); - } - } -} - -// Singleton instance -export const RequestMonitorService = new RequestMonitor(); diff --git a/src/lib/services/request-queue.service.ts b/src/lib/services/request-queue.service.ts deleted file mode 100644 index 7658b9c..0000000 --- a/src/lib/services/request-queue.service.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Service de gestion de queue pour limiter les requêtes concurrentes vers Komga - * Évite de surcharger Komga avec trop de requêtes simultanées - */ -import logger from "@/lib/logger"; - -interface QueuedRequest { - execute: () => Promise; - resolve: (value: T) => void; - reject: (error: any) => void; -} - -class RequestQueue { - private queue: QueuedRequest[] = []; - private activeCount = 0; - private maxConcurrent: number; - private getMaxConcurrent: (() => Promise) | null = null; - - constructor(maxConcurrent?: number) { - // 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) { - logger.error({ err: error }, "Error getting maxConcurrent from preferences, using default"); - return this.maxConcurrent; - } - } - return this.maxConcurrent; - } - - async enqueue(execute: () => Promise): Promise { - return new Promise((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")); - return; - } - - this.queue.push({ execute, resolve, reject }); - this.processQueue(); - }); - } - - private async delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - private async processQueue(): Promise { - const maxConcurrent = await this.getCurrentMaxConcurrent(); - if (this.activeCount >= maxConcurrent || this.queue.length === 0) { - return; - } - - this.activeCount++; - const request = this.queue.shift(); - - if (!request) { - this.activeCount--; - return; - } - - 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"; - if (!isDebug) { - const delayMs = this.queue.length > 10 ? 500 : 200; - await this.delay(delayMs); - } - const result = await request.execute(); - request.resolve(result); - } catch (error) { - request.reject(error); - } finally { - this.activeCount--; - this.processQueue(); - } - } - - getActiveCount(): number { - return this.activeCount; - } - - getQueueLength(): number { - return this.queue.length; - } - - setMaxConcurrent(max: number): void { - this.maxConcurrent = max; - } -} - -// Singleton instance - Par défaut limite à 5 requêtes simultanées -export const RequestQueueService = new RequestQueue(5); diff --git a/src/lib/services/series.service.ts b/src/lib/services/series.service.ts index 41190c5..f0d0d88 100644 --- a/src/lib/services/series.service.ts +++ b/src/lib/services/series.service.ts @@ -1,50 +1,27 @@ import { BaseApiService } from "./base-api.service"; import type { LibraryResponse } from "@/types/library"; -import type { KomgaBook, KomgaSeries, TTLConfig } from "@/types/komga"; +import type { KomgaBook, KomgaSeries } from "@/types/komga"; import { BookService } from "./book.service"; import type { ImageResponse } from "./image.service"; import { ImageService } from "./image.service"; import { PreferencesService } from "./preferences.service"; -import { ConfigDBService } from "./config-db.service"; -import { getServerCacheService } from "./server-cache.service"; import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; import type { UserPreferences } from "@/types/preferences"; -import type { ServerCacheService } from "./server-cache.service"; import logger from "@/lib/logger"; +// Cache HTTP navigateur : 30 jours (immutable car les images ne changent pas) +const IMAGE_CACHE_MAX_AGE = 2592000; + export class SeriesService extends BaseApiService { - private static async getImageCacheMaxAge(): Promise { - try { - const ttlConfig: TTLConfig | null = await ConfigDBService.getTTLConfig(); - const maxAge = ttlConfig?.imageCacheMaxAge ?? 2592000; - return maxAge; - } catch (error) { - logger.error({ err: error }, "[ImageCache] Error fetching TTL config"); - return 2592000; // 30 jours par défaut en cas d'erreur - } - } static async getSeries(seriesId: string): Promise { try { - return this.fetchWithCache( - `series-${seriesId}`, - async () => this.fetchFromApi({ path: `series/${seriesId}` }), - "SERIES" - ); + return this.fetchFromApi({ path: `series/${seriesId}` }); } catch (error) { throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } } - static async invalidateSeriesCache(seriesId: string): Promise { - try { - const cacheService = await getServerCacheService(); - await cacheService.delete(`series-${seriesId}`); - } catch (error) { - throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error); - } - } - static async getSeriesBooks( seriesId: string, page: number = 0, @@ -96,28 +73,19 @@ export class SeriesService extends BaseApiService { const searchBody = { condition }; - // Clé de cache incluant tous les paramètres - const cacheKey = `series-${seriesId}-books-p${page}-s${size}-u${unreadOnly}`; + const params: Record = { + page: String(page), + size: String(size), + sort: "number,asc", + }; - const response = await this.fetchWithCache>( - cacheKey, - async () => { - const params: Record = { - page: String(page), - size: String(size), - sort: "number,asc", - }; - - return this.fetchFromApi>( - { path: "books/list", params }, - headers, - { - method: "POST", - body: JSON.stringify(searchBody), - } - ); - }, - "BOOKS" + const response = await this.fetchFromApi>( + { path: "books/list", params }, + headers, + { + method: "POST", + body: JSON.stringify(searchBody), + } ); // Filtrer uniquement les livres supprimés côté client (léger) @@ -133,36 +101,17 @@ export class SeriesService extends BaseApiService { } } - static async invalidateSeriesBooksCache(seriesId: string): Promise { - try { - const cacheService: ServerCacheService = await getServerCacheService(); - // Invalider toutes les clés de cache pour cette série - // Format: series-{id}-books-p{page}-s{size}-u{unread} - await cacheService.deleteAll(`series-${seriesId}-books-`); - } catch (error) { - throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error); - } - } - static async getFirstBook(seriesId: string): Promise { try { - return this.fetchWithCache( - `series-first-book-${seriesId}`, - async () => { - const data: LibraryResponse = await this.fetchFromApi< - LibraryResponse - >({ - path: `series/${seriesId}/books`, - params: { page: "0", size: "1" }, - }); - if (!data.content || data.content.length === 0) { - throw new AppError(ERROR_CODES.SERIES.NO_BOOKS_FOUND); - } + const data: LibraryResponse = await this.fetchFromApi>({ + path: `series/${seriesId}/books`, + params: { page: "0", size: "1" }, + }); + if (!data.content || data.content.length === 0) { + throw new AppError(ERROR_CODES.SERIES.NO_BOOKS_FOUND); + } - return data.content[0].id; - }, - "SERIES" - ); + return data.content[0].id; } catch (error) { logger.error({ err: error }, "Erreur lors de la récupération du premier livre"); throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); @@ -173,7 +122,6 @@ export class SeriesService extends BaseApiService { try { // Récupérer les préférences de l'utilisateur const preferences: UserPreferences = await PreferencesService.getPreferences(); - const maxAge = await this.getImageCacheMaxAge(); // Si l'utilisateur préfère les vignettes, utiliser la miniature if (preferences.showThumbnails) { @@ -181,7 +129,7 @@ export class SeriesService extends BaseApiService { return new Response(response.buffer.buffer as ArrayBuffer, { headers: { "Content-Type": response.contentType || "image/jpeg", - "Cache-Control": `public, max-age=${maxAge}, immutable`, + "Cache-Control": `public, max-age=${IMAGE_CACHE_MAX_AGE}, immutable`, }, }); } diff --git a/src/lib/services/server-cache.service.ts b/src/lib/services/server-cache.service.ts deleted file mode 100644 index 980d6ef..0000000 --- a/src/lib/services/server-cache.service.ts +++ /dev/null @@ -1,730 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { PreferencesService } from "./preferences.service"; -import { getCurrentUser } from "../auth-utils"; -import logger from "@/lib/logger"; - -export type CacheMode = "file" | "memory"; - -interface CacheConfig { - mode: CacheMode; -} - -class ServerCacheService { - private static instance: ServerCacheService; - private cacheDir: string; - private memoryCache: Map = new Map(); - private config: CacheConfig = { - mode: "memory", - }; - - // Configuration des temps de cache en millisecondes - private static readonly oneMinute = 1 * 60 * 1000; - private static readonly twoMinutes = 2 * 60 * 1000; - private static readonly fiveMinutes = 5 * 60 * 1000; - private static readonly tenMinutes = 10 * 60 * 1000; - private static readonly twentyFourHours = 24 * 60 * 60 * 1000; - private static readonly oneWeek = 7 * 24 * 60 * 60 * 1000; - private static readonly noCache = 0; - - // Configuration des temps de cache - // Optimisé pour la pagination native Komga : - // - Listes paginées (SERIES, BOOKS) : TTL court (2 min) car données fraîches + progression utilisateur - // - Données agrégées (HOME) : TTL moyen (10 min) car plusieurs sources - // - Données statiques (LIBRARIES) : TTL long (24h) car changent rarement - // - Images : TTL très long (7 jours) car immuables - private static readonly DEFAULT_TTL = { - DEFAULT: ServerCacheService.fiveMinutes, - HOME: ServerCacheService.tenMinutes, - LIBRARIES: ServerCacheService.twentyFourHours, - SERIES: ServerCacheService.twoMinutes, // Listes paginées avec progression - BOOKS: ServerCacheService.twoMinutes, // Listes paginées avec progression - IMAGES: ServerCacheService.oneWeek, - }; - - private constructor() { - this.cacheDir = path.join(process.cwd(), ".cache"); - this.ensureCacheDirectory(); - this.cleanExpiredCache(); - this.initializeCacheMode(); - } - - private async initializeCacheMode(): Promise { - try { - const user = await getCurrentUser(); - if (!user) { - this.setCacheMode("memory"); - return; - } - const preferences = await PreferencesService.getPreferences(); - this.setCacheMode(preferences.cacheMode); - } catch (error) { - logger.error({ err: error }, "Error initializing cache mode from preferences"); - // Keep default memory mode if preferences can't be loaded - } - } - - private ensureCacheDirectory(): void { - if (!fs.existsSync(this.cacheDir)) { - fs.mkdirSync(this.cacheDir, { recursive: true }); - } - } - - private getCacheFilePath(key: string): string { - // Nettoyer la clé des caractères spéciaux et des doubles slashes - const sanitizedKey = key.replace(/[<>:"|?*]/g, "_").replace(/\/+/g, "/"); - - const filePath = path.join(this.cacheDir, `${sanitizedKey}.json`); - - return filePath; - } - - private cleanExpiredCache(): void { - if (!fs.existsSync(this.cacheDir)) return; - - const cleanDirectory = (dirPath: string): boolean => { - if (!fs.existsSync(dirPath)) return true; - - const items = fs.readdirSync(dirPath); - let isEmpty = true; - - for (const item of items) { - const itemPath = path.join(dirPath, item); - - try { - const stats = fs.statSync(itemPath); - - if (stats.isDirectory()) { - const isSubDirEmpty = cleanDirectory(itemPath); - if (isSubDirEmpty) { - try { - fs.rmdirSync(itemPath); - } catch (error) { - logger.error( - { err: error, path: itemPath }, - `Could not remove directory ${itemPath}` - ); - isEmpty = false; - } - } else { - isEmpty = false; - } - } else if (stats.isFile() && item.endsWith(".json")) { - try { - const content = fs.readFileSync(itemPath, "utf-8"); - const cached = JSON.parse(content); - if (cached.expiry < Date.now()) { - fs.unlinkSync(itemPath); - } else { - isEmpty = false; - } - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not parse file ${itemPath}`); - // Si le fichier est corrompu, on le supprime - try { - fs.unlinkSync(itemPath); - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not remove file ${itemPath}`); - isEmpty = false; - } - } - } else { - isEmpty = false; - } - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not access ${itemPath}`); - // En cas d'erreur sur le fichier/dossier, on continue - isEmpty = false; - continue; - } - } - - return isEmpty; - }; - - cleanDirectory(this.cacheDir); - } - - public static async getInstance(): Promise { - if (!ServerCacheService.instance) { - ServerCacheService.instance = new ServerCacheService(); - await ServerCacheService.instance.initializeCacheMode(); - } - return ServerCacheService.instance; - } - - /** - * Retourne le TTL pour un type de données spécifique - */ - public getTTL(type: keyof typeof ServerCacheService.DEFAULT_TTL): number { - // Utiliser directement la valeur par défaut - return ServerCacheService.DEFAULT_TTL[type]; - } - - public setCacheMode(mode: CacheMode): void { - if (this.config.mode === mode) return; - - // Si on passe de mémoire à fichier, on sauvegarde le cache en mémoire - if (mode === "file" && this.config.mode === "memory") { - this.memoryCache.forEach((value, key) => { - if (value.expiry > Date.now()) { - this.saveToFile(key, value); - } - }); - this.memoryCache.clear(); - } - // Si on passe de fichier à mémoire, on charge le cache fichier en mémoire - else if (mode === "memory" && this.config.mode === "file") { - this.loadFileCacheToMemory(); - } - - this.config.mode = mode; - } - - public getCacheMode(): CacheMode { - return this.config.mode; - } - - private loadFileCacheToMemory(): void { - if (!fs.existsSync(this.cacheDir)) return; - - const loadDirectory = (dirPath: string) => { - const items = fs.readdirSync(dirPath); - - for (const item of items) { - const itemPath = path.join(dirPath, item); - try { - const stats = fs.statSync(itemPath); - - if (stats.isDirectory()) { - loadDirectory(itemPath); - } else if (stats.isFile() && item.endsWith(".json")) { - try { - const content = fs.readFileSync(itemPath, "utf-8"); - const cached = JSON.parse(content); - if (cached.expiry > Date.now()) { - const key = path.relative(this.cacheDir, itemPath).slice(0, -5); // Remove .json - this.memoryCache.set(key, cached); - } - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not parse file ${itemPath}`); - // Ignore les fichiers corrompus - } - } - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not access ${itemPath}`); - // Ignore les erreurs d'accès - } - } - }; - - loadDirectory(this.cacheDir); - } - - private saveToFile(key: string, value: { data: unknown; expiry: number }): void { - const filePath = this.getCacheFilePath(key); - const dirPath = path.dirname(filePath); - - try { - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } - fs.writeFileSync(filePath, JSON.stringify(value), "utf-8"); - } catch (error) { - logger.error({ err: error, path: filePath }, `Could not write cache file ${filePath}`); - } - } - - /** - * Met en cache des données avec une durée de vie - */ - set(key: string, data: any, type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"): void { - const cacheData = { - data, - expiry: Date.now() + this.getTTL(type), - }; - - if (this.config.mode === "memory") { - this.memoryCache.set(key, cacheData); - } else { - const filePath = this.getCacheFilePath(key); - const dirPath = path.dirname(filePath); - - try { - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } - fs.writeFileSync(filePath, JSON.stringify(cacheData), "utf-8"); - } catch (error) { - logger.error({ err: error, path: filePath }, `Error writing cache file ${filePath}`); - } - } - } - - /** - * Récupère des données du cache si elles sont valides - */ - get(key: string): any | null { - if (this.config.mode === "memory") { - const cached = this.memoryCache.get(key); - if (!cached) return null; - - if (cached.expiry > Date.now()) { - return cached.data; - } - - this.memoryCache.delete(key); - return null; - } - - const filePath = this.getCacheFilePath(key); - if (!fs.existsSync(filePath)) { - return null; - } - - try { - const content = fs.readFileSync(filePath, "utf-8"); - const cached = JSON.parse(content); - - if (cached.expiry > Date.now()) { - return cached.data; - } - - fs.unlinkSync(filePath); - return null; - } catch (error) { - logger.error({ err: error, path: filePath }, `Error reading cache file ${filePath}`); - return null; - } - } - - /** - * Récupère des données du cache même si elles sont expirées (stale) - * Retourne { data, isStale } ou null si pas de cache - */ - private getStale(key: string): { data: any; isStale: boolean } | null { - if (this.config.mode === "memory") { - const cached = this.memoryCache.get(key); - if (!cached) return null; - - return { - data: cached.data, - isStale: cached.expiry <= Date.now(), - }; - } - - const filePath = this.getCacheFilePath(key); - if (!fs.existsSync(filePath)) { - return null; - } - - try { - const content = fs.readFileSync(filePath, "utf-8"); - const cached = JSON.parse(content); - - return { - data: cached.data, - isStale: cached.expiry <= Date.now(), - }; - } catch (error) { - logger.error({ err: error, path: filePath }, `Error reading cache file ${filePath}`); - return null; - } - } - - /** - * Supprime une entrée du cache - */ - async delete(key: string): Promise { - const user = await getCurrentUser(); - if (!user) { - throw new Error("Utilisateur non authentifié"); - } - const cacheKey = `${user.id}-${key}`; - - if (this.config.mode === "memory") { - this.memoryCache.delete(cacheKey); - } else { - const filePath = this.getCacheFilePath(cacheKey); - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - } - } - - /** - * Supprime toutes les entrées du cache qui commencent par un préfixe - */ - async deleteAll(prefix: string): Promise { - const user = await getCurrentUser(); - if (!user) { - throw new Error("Utilisateur non authentifié"); - } - const prefixKey = `${user.id}-${prefix}`; - - if (this.config.mode === "memory") { - this.memoryCache.forEach((value, key) => { - if (key.startsWith(prefixKey)) { - this.memoryCache.delete(key); - } - }); - } else { - // En mode fichier, parcourir récursivement tous les fichiers et supprimer ceux qui correspondent - if (!fs.existsSync(this.cacheDir)) return; - - const deleteMatchingFiles = (dirPath: string): void => { - const items = fs.readdirSync(dirPath); - - for (const item of items) { - const itemPath = path.join(dirPath, item); - try { - const stats = fs.statSync(itemPath); - - if (stats.isDirectory()) { - deleteMatchingFiles(itemPath); - // Supprimer le répertoire s'il est vide après suppression des fichiers - try { - const remainingItems = fs.readdirSync(itemPath); - if (remainingItems.length === 0) { - fs.rmdirSync(itemPath); - } - } catch { - // Ignore les erreurs de suppression de répertoire - } - } else if (stats.isFile() && item.endsWith(".json")) { - // Extraire la clé du chemin relatif (sans l'extension .json) - const relativePath = path.relative(this.cacheDir, itemPath); - const key = relativePath.slice(0, -5).replace(/\\/g, "/"); // Remove .json and normalize slashes - - if (key.startsWith(prefixKey)) { - fs.unlinkSync(itemPath); - if (process.env.CACHE_DEBUG === "true") { - logger.debug(`🗑️ [CACHE DELETE] ${key}`); - } - } - } - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not delete cache file ${itemPath}`); - } - } - }; - - deleteMatchingFiles(this.cacheDir); - } - } - - /** - * Vide le cache - */ - clear(): void { - if (this.config.mode === "memory") { - this.memoryCache.clear(); - return; - } - - if (!fs.existsSync(this.cacheDir)) return; - - const removeDirectory = (dirPath: string) => { - if (!fs.existsSync(dirPath)) return; - - const items = fs.readdirSync(dirPath); - - for (const item of items) { - const itemPath = path.join(dirPath, item); - try { - const stats = fs.statSync(itemPath); - - if (stats.isDirectory()) { - removeDirectory(itemPath); - try { - fs.rmdirSync(itemPath); - } catch (error) { - logger.error( - { err: error, path: itemPath }, - `Could not remove directory ${itemPath}` - ); - } - } else { - try { - fs.unlinkSync(itemPath); - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not remove file ${itemPath}`); - } - } - } catch (error) { - logger.error({ err: error, path: itemPath }, `Error accessing ${itemPath}`); - } - } - }; - - try { - removeDirectory(this.cacheDir); - } catch (error) { - logger.error({ err: error }, "Error clearing cache"); - } - } - - /** - * Récupère des données du cache ou exécute la fonction si nécessaire - * Stratégie stale-while-revalidate: - * - Cache valide → retourne immédiatement - * - Cache expiré → retourne le cache expiré ET revalide en background - * - Pas de cache → fetch normalement - */ - async getOrSet( - key: string, - fetcher: () => Promise, - type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT" - ): Promise { - const startTime = performance.now(); - const user = await getCurrentUser(); - if (!user) { - throw new Error("Utilisateur non authentifié"); - } - - 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` - ); - } - - // Si le cache est expiré, revalider en background sans bloquer la réponse - if (isStale) { - // Fire and forget - revalidate en background - this.revalidateInBackground(cacheKey, fetcher, type, key); - } - - return data as T; - } - - // Pas de cache du tout, fetch normalement - 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") { - logger.debug(`💾 [CACHE SET] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`); - } - - return data; - } catch (error) { - throw error; - } - } - - /** - * Revalide le cache en background - */ - private async revalidateInBackground( - cacheKey: string, - fetcher: () => Promise, - type: keyof typeof ServerCacheService.DEFAULT_TTL, - debugKey: string - ): Promise { - try { - const startTime = performance.now(); - const data = await fetcher(); - this.set(cacheKey, data, type); - - if (process.env.CACHE_DEBUG === "true") { - const endTime = performance.now(); - logger.debug( - `🔄 [CACHE REVALIDATE] ${debugKey} | ${type} | ${(endTime - startTime).toFixed(2)}ms` - ); - } - } catch (error) { - logger.error({ err: error, key: debugKey }, `🔴 [CACHE REVALIDATE ERROR] ${debugKey}`); - // Ne pas relancer l'erreur car c'est en background - } - } - - invalidate(key: string): void { - this.delete(key); - } - - /** - * Calcule la taille approximative d'un objet en mémoire - */ - private calculateObjectSize(obj: unknown): number { - if (obj === null || obj === undefined) return 0; - - // Si c'est un Buffer, utiliser sa taille réelle - if (Buffer.isBuffer(obj)) { - return obj.length; - } - - // Si c'est un objet avec une propriété buffer (comme ImageResponse) - if (typeof obj === "object" && obj !== null) { - const objAny = obj as any; - if (objAny.buffer && Buffer.isBuffer(objAny.buffer)) { - // Taille du buffer + taille approximative des autres propriétés - let size = objAny.buffer.length; - // Ajouter la taille du contentType si présent - if (objAny.contentType && typeof objAny.contentType === "string") { - size += objAny.contentType.length * 2; // UTF-16 - } - return size; - } - } - - // Pour les autres types, utiliser JSON.stringify comme approximation - try { - return JSON.stringify(obj).length * 2; // x2 pour UTF-16 - } catch { - // Si l'objet n'est pas sérialisable, retourner une estimation - return 1000; // 1KB par défaut - } - } - - /** - * Calcule la taille du cache - */ - async getCacheSize(): Promise<{ sizeInBytes: number; itemCount: number }> { - if (this.config.mode === "memory") { - // Calculer la taille approximative en mémoire - let sizeInBytes = 0; - let itemCount = 0; - - this.memoryCache.forEach((value) => { - if (value.expiry > Date.now()) { - itemCount++; - // Calculer la taille du data + expiry (8 bytes pour le timestamp) - sizeInBytes += this.calculateObjectSize(value.data) + 8; - } - }); - - return { sizeInBytes, itemCount }; - } - - // Calculer la taille du cache sur disque - let sizeInBytes = 0; - let itemCount = 0; - - const calculateDirectorySize = (dirPath: string): void => { - if (!fs.existsSync(dirPath)) return; - - const items = fs.readdirSync(dirPath); - - for (const item of items) { - const itemPath = path.join(dirPath, item); - try { - const stats = fs.statSync(itemPath); - - if (stats.isDirectory()) { - calculateDirectorySize(itemPath); - } else if (stats.isFile() && item.endsWith(".json")) { - sizeInBytes += stats.size; - itemCount++; - } - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not access ${itemPath}`); - } - } - }; - - if (fs.existsSync(this.cacheDir)) { - calculateDirectorySize(this.cacheDir); - } - - return { sizeInBytes, itemCount }; - } - - /** - * Liste les entrées du cache avec leurs détails - */ - async getCacheEntries(): Promise< - Array<{ - key: string; - size: number; - expiry: number; - isExpired: boolean; - }> - > { - const entries: Array<{ - key: string; - size: number; - expiry: number; - isExpired: boolean; - }> = []; - - if (this.config.mode === "memory") { - this.memoryCache.forEach((value, key) => { - const size = this.calculateObjectSize(value.data) + 8; - entries.push({ - key, - size, - expiry: value.expiry, - isExpired: value.expiry <= Date.now(), - }); - }); - } else { - const collectEntries = (dirPath: string): void => { - if (!fs.existsSync(dirPath)) return; - - const items = fs.readdirSync(dirPath); - - for (const item of items) { - const itemPath = path.join(dirPath, item); - try { - const stats = fs.statSync(itemPath); - - if (stats.isDirectory()) { - collectEntries(itemPath); - } else if (stats.isFile() && item.endsWith(".json")) { - try { - const content = fs.readFileSync(itemPath, "utf-8"); - const cached = JSON.parse(content); - const key = path.relative(this.cacheDir, itemPath).slice(0, -5); - - entries.push({ - key, - size: stats.size, - expiry: cached.expiry, - isExpired: cached.expiry <= Date.now(), - }); - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not parse file ${itemPath}`); - } - } - } catch (error) { - logger.error({ err: error, path: itemPath }, `Could not access ${itemPath}`); - } - } - }; - - if (fs.existsSync(this.cacheDir)) { - collectEntries(this.cacheDir); - } - } - - return entries.sort((a, b) => b.expiry - a.expiry); - } -} - -// Créer une instance initialisée du service -let initializedInstance: Promise; - -export const getServerCacheService = async (): Promise => { - if (!initializedInstance) { - initializedInstance = ServerCacheService.getInstance(); - } - return initializedInstance; -}; - -// Exporter aussi la classe pour les tests -export { ServerCacheService }; diff --git a/src/lib/utils/image-url.ts b/src/lib/utils/image-url.ts index 6899c30..899a56f 100644 --- a/src/lib/utils/image-url.ts +++ b/src/lib/utils/image-url.ts @@ -1,6 +1,5 @@ /** - * Génère l'URL de base pour une image (sans cache version) - * Utilisez useImageUrl() dans les composants pour obtenir l'URL avec cache busting + * Génère l'URL pour une image (thumbnail de série ou de livre) */ export function getImageUrl(type: "series" | "book", id: string) { if (type === "series") { diff --git a/src/types/cache.ts b/src/types/cache.ts deleted file mode 100644 index 7b5735d..0000000 --- a/src/types/cache.ts +++ /dev/null @@ -1 +0,0 @@ -export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES"; diff --git a/src/types/komga.ts b/src/types/komga.ts index 553494f..5ee71e6 100644 --- a/src/types/komga.ts +++ b/src/types/komga.ts @@ -15,20 +15,6 @@ export interface KomgaConfig extends KomgaConfigData { userId: number; } -export interface TTLConfigData { - defaultTTL: number; - homeTTL: number; - librariesTTL: number; - seriesTTL: number; - booksTTL: number; - imagesTTL: number; - imageCacheMaxAge: number; // en secondes -} - -export interface TTLConfig extends TTLConfigData { - userId: number; -} - // Types liés à l'API Komga export interface KomgaUser { id: string; diff --git a/src/types/preferences.ts b/src/types/preferences.ts index e271aad..dde30d6 100644 --- a/src/types/preferences.ts +++ b/src/types/preferences.ts @@ -9,15 +9,8 @@ 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"; showOnlyUnread: boolean; displayMode: { compact: boolean; @@ -25,14 +18,11 @@ export interface UserPreferences { viewMode: "grid" | "list"; }; background: BackgroundPreferences; - komgaMaxConcurrentRequests: number; readerPrefetchCount: number; - circuitBreakerConfig: CircuitBreakerConfig; } export const defaultPreferences: UserPreferences = { showThumbnails: true, - cacheMode: "memory", showOnlyUnread: false, displayMode: { compact: false, @@ -44,13 +34,7 @@ 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