opticore-cache 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2541 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/index.ts
9
+ import dotenv from "dotenv";
10
+
11
+ // src/infrastructure/config/environmentLoader.config.ts
12
+ var EnvironmentLoader = class _EnvironmentLoader {
13
+ static instance;
14
+ configuration;
15
+ /**
16
+ * Constructeur privé pour forcer l'utilisation de getInstance()
17
+ */
18
+ constructor() {
19
+ this.loadConfiguration();
20
+ }
21
+ /**
22
+ * Récupère l'instance unique du EnvironmentLoader
23
+ *
24
+ * @returns EnvironmentLoader
25
+ */
26
+ static getInstance() {
27
+ if (!_EnvironmentLoader.instance) {
28
+ _EnvironmentLoader.instance = new _EnvironmentLoader();
29
+ }
30
+ return _EnvironmentLoader.instance;
31
+ }
32
+ /**
33
+ * Charge et parse la configuration depuis process.env
34
+ *
35
+ * @private
36
+ */
37
+ loadConfiguration() {
38
+ this.configuration = {
39
+ // Activation du cache
40
+ enabled: this.parseBoolean("CACHE_ENABLED", false),
41
+ // Configuration de base
42
+ defaultTTL: this.parseNumber("CACHE_DEFAULT_TTL", 3e5),
43
+ // 5 minutes
44
+ maxSize: this.parseNumber("CACHE_MAX_SIZE", 100),
45
+ evictionStrategy: this.parseEvictionStrategy("CACHE_STRATEGY", "lru"),
46
+ namespace: process.env.CACHE_NAMESPACE || "opticore-app",
47
+ // Configuration des routes à ignorer
48
+ skipRoutes: this.parseSkipRoutes("CACHE_SKIP_ROUTES"),
49
+ enableWildcards: this.parseBoolean("CACHE_SKIP_WILDCARDS", true),
50
+ userAware: this.parseBoolean("CACHE_USER_AWARE", false),
51
+ // Options avancées
52
+ debug: this.parseBoolean("CACHE_DEBUG", false),
53
+ // Performance
54
+ cleanupInterval: this.parseNumber("CACHE_CLEANUP_INTERVAL", 6e4)
55
+ // 1 minute
56
+ };
57
+ }
58
+ /**
59
+ * Parse une valeur booléenne depuis les variables d'environnement
60
+ *
61
+ * @param key - Clé de la variable d'environnement
62
+ * @param defaultValue - Valeur par défaut si non définie
63
+ * @returns boolean
64
+ */
65
+ parseBoolean(key, defaultValue) {
66
+ const value = process.env[key];
67
+ if (value === void 0) return defaultValue;
68
+ const normalizedValue = value.toLowerCase().trim();
69
+ return ["true", "1", "yes", "on"].includes(normalizedValue);
70
+ }
71
+ /**
72
+ * Parse un nombre depuis les variables d'environnement
73
+ *
74
+ * @param key - Clé de la variable d'environnement
75
+ * @param defaultValue - Valeur par défaut si non définie ou invalide
76
+ * @returns number
77
+ */
78
+ parseNumber(key, defaultValue) {
79
+ const value = process.env[key];
80
+ if (!value) return defaultValue;
81
+ const parsed = parseInt(value, 10);
82
+ return isNaN(parsed) ? defaultValue : parsed;
83
+ }
84
+ /**
85
+ * Parse la stratégie d'éviction depuis les variables d'environnement
86
+ *
87
+ * @param key - Clé de la variable d'environnement
88
+ * @param defaultValue - Valeur par défaut
89
+ * @returns 'lru' | 'fifo' | 'lfu'
90
+ */
91
+ parseEvictionStrategy(key, defaultValue) {
92
+ const value = process.env[key] || defaultValue;
93
+ const validStrategies = ["lru", "fifo", "lfu"];
94
+ return validStrategies.includes(value.toLowerCase()) ? value.toLowerCase() : defaultValue;
95
+ }
96
+ /**
97
+ * Parse la liste des routes à ignorer depuis les variables d'environnement
98
+ *
99
+ * @param key - Clé de la variable d'environnement
100
+ * @returns string[] - Liste des routes à ignorer
101
+ */
102
+ parseSkipRoutes(key) {
103
+ const value = process.env[key];
104
+ if (!value) return [];
105
+ return value.split(",").map((route) => route.trim()).filter((route) => route.length > 0);
106
+ }
107
+ /**
108
+ * Récupère la configuration complète
109
+ *
110
+ * @returns CacheConfiguration
111
+ */
112
+ getConfig() {
113
+ return { ...this.configuration };
114
+ }
115
+ /**
116
+ * Met à jour la configuration (pour les tests)
117
+ *
118
+ * @param updates - Mises à jour partielles de configuration
119
+ */
120
+ updateConfig(updates) {
121
+ this.configuration = { ...this.configuration, ...updates };
122
+ }
123
+ };
124
+
125
+ // src/infrastructure/cache/memoryCache.repository.ts
126
+ var MemoryCache = class {
127
+ /**
128
+ * Crée une instance de MemoryCache
129
+ * @param config - Configuration du cache
130
+ */
131
+ constructor(config) {
132
+ this.config = config;
133
+ this.statistics.maxSize = config.maxSize;
134
+ this.startCleanupCycle();
135
+ }
136
+ store = /* @__PURE__ */ new Map();
137
+ statistics = {
138
+ hits: 0,
139
+ misses: 0,
140
+ writes: 0,
141
+ size: 0,
142
+ maxSize: 0
143
+ };
144
+ cleanupInterval;
145
+ /**
146
+ * Stocke une valeur dans le cache
147
+ * @param key - Clé unique pour l'entrée
148
+ * @param value - Valeur à stocker
149
+ * @param ttl - Durée de vie en millisecondes
150
+ * @returns Promise<void>
151
+ */
152
+ async set(key, value, ttl) {
153
+ const now = Date.now();
154
+ const timeToLive = ttl || this.config.defaultTTL;
155
+ const entry = {
156
+ key,
157
+ value,
158
+ timeToLive,
159
+ createdAt: now,
160
+ expiresAt: now + timeToLive,
161
+ accessCount: 0,
162
+ lastAccessedAt: now
163
+ };
164
+ if (this.store.size >= this.config.maxSize) {
165
+ this.evictEntry();
166
+ }
167
+ this.store.set(key, entry);
168
+ this.statistics.writes++;
169
+ this.statistics.size = this.store.size;
170
+ }
171
+ /**
172
+ * Récupère une valeur depuis le cache
173
+ * @param key - Clé de l'entrée à récupérer
174
+ * @returns Promise<T | null>
175
+ */
176
+ async get(key) {
177
+ const entry = this.store.get(key);
178
+ if (!entry) {
179
+ this.statistics.misses++;
180
+ return null;
181
+ }
182
+ const now = Date.now();
183
+ if (now > entry.expiresAt) {
184
+ await this.delete(key);
185
+ this.statistics.misses++;
186
+ return null;
187
+ }
188
+ entry.accessCount++;
189
+ entry.lastAccessedAt = now;
190
+ if (this.config.evictionStrategy === "lru") {
191
+ this.store.delete(key);
192
+ this.store.set(key, entry);
193
+ }
194
+ this.statistics.hits++;
195
+ return entry.value;
196
+ }
197
+ /**
198
+ * Supprime une entrée du cache
199
+ *
200
+ * @param key - Clé de l'entrée à supprimer
201
+ * @returns Promise<void>
202
+ */
203
+ async delete(key) {
204
+ this.store.delete(key);
205
+ this.statistics.size = this.store.size;
206
+ }
207
+ /**
208
+ * Vérifie si une clé existe dans le cache
209
+ *
210
+ * @param key - Clé à vérifier
211
+ * @returns Promise<boolean>
212
+ */
213
+ async has(key) {
214
+ const entry = this.store.get(key);
215
+ if (!entry) return false;
216
+ if (Date.now() > entry.expiresAt) {
217
+ await this.delete(key);
218
+ return false;
219
+ }
220
+ return true;
221
+ }
222
+ /**
223
+ * Supprime toutes les entrées du cache
224
+ *
225
+ * @returns Promise<void>
226
+ */
227
+ async clear() {
228
+ this.store.clear();
229
+ this.statistics = {
230
+ hits: 0,
231
+ misses: 0,
232
+ writes: 0,
233
+ size: 0,
234
+ maxSize: this.config.maxSize
235
+ };
236
+ }
237
+ /**
238
+ * Récupère toutes les clés valides du cache
239
+ *
240
+ * @returns Promise<string[]>
241
+ */
242
+ async keys() {
243
+ const expiredKeys = [];
244
+ for (const [key, entry] of this.store.entries()) {
245
+ if (Date.now() > entry.expiresAt) {
246
+ expiredKeys.push(key);
247
+ }
248
+ }
249
+ for (const key of expiredKeys) {
250
+ await this.delete(key);
251
+ }
252
+ return Array.from(this.store.keys());
253
+ }
254
+ /**
255
+ * Récupère les statistiques d'utilisation du cache
256
+ *
257
+ * @returns Promise<CacheStatistics>
258
+ */
259
+ async getStatistics() {
260
+ const totalRequests = this.statistics.hits + this.statistics.misses;
261
+ const hitRate = totalRequests > 0 ? this.statistics.hits / totalRequests * 100 : 0;
262
+ const usagePercentage = this.statistics.maxSize > 0 ? this.statistics.size / this.statistics.maxSize * 100 : 0;
263
+ return {
264
+ ...this.statistics,
265
+ hitRate: parseFloat(hitRate.toFixed(2)),
266
+ usagePercentage: parseFloat(usagePercentage.toFixed(2))
267
+ };
268
+ }
269
+ /**
270
+ * Invalide les entrées dont la clé correspond au pattern
271
+ *
272
+ * @param pattern - Pattern de clés à invalider
273
+ * @returns Promise<number> - Nombre d'entrées invalidées
274
+ */
275
+ async invalidate(pattern) {
276
+ const keysToDelete = [];
277
+ const patternRegex = new RegExp(pattern.replace("*", ".*"));
278
+ for (const key of this.store.keys()) {
279
+ if (patternRegex.test(key)) {
280
+ keysToDelete.push(key);
281
+ }
282
+ }
283
+ for (const key of keysToDelete) {
284
+ await this.delete(key);
285
+ }
286
+ return keysToDelete.length;
287
+ }
288
+ /**
289
+ * Évince une entrée selon la stratégie configurée
290
+ *
291
+ * @private
292
+ */
293
+ evictEntry() {
294
+ if (this.store.size === 0) return;
295
+ let keyToEvict = null;
296
+ switch (this.config.evictionStrategy) {
297
+ case "lru":
298
+ keyToEvict = this.findLRUKey();
299
+ break;
300
+ case "fifo":
301
+ keyToEvict = this.findFIFOKey();
302
+ break;
303
+ case "lfu":
304
+ keyToEvict = this.findLFUKey();
305
+ break;
306
+ }
307
+ if (keyToEvict) {
308
+ this.store.delete(keyToEvict);
309
+ this.statistics.size = this.store.size;
310
+ }
311
+ }
312
+ /**
313
+ * Trouve la clé la moins récemment utilisée (LRU)
314
+ *
315
+ * @private
316
+ * @returns string | null
317
+ */
318
+ findLRUKey() {
319
+ let oldestKey = null;
320
+ let oldestTime = Date.now();
321
+ for (const [key, entry] of this.store.entries()) {
322
+ if (entry.lastAccessedAt < oldestTime) {
323
+ oldestTime = entry.lastAccessedAt;
324
+ oldestKey = key;
325
+ }
326
+ }
327
+ return oldestKey;
328
+ }
329
+ /**
330
+ * Trouve la clé la première entrée insérée (FIFO)
331
+ *
332
+ * @private
333
+ * @returns string | null
334
+ */
335
+ findFIFOKey() {
336
+ return this.store.keys().next().value || null;
337
+ }
338
+ /**
339
+ * Trouve la clé la moins fréquemment utilisée (LFU)
340
+ *
341
+ * @private
342
+ * @returns string | null
343
+ */
344
+ findLFUKey() {
345
+ let leastFrequentKey = null;
346
+ let leastAccessCount = Infinity;
347
+ for (const [key, entry] of this.store.entries()) {
348
+ if (entry.accessCount < leastAccessCount) {
349
+ leastAccessCount = entry.accessCount;
350
+ leastFrequentKey = key;
351
+ }
352
+ }
353
+ return leastFrequentKey;
354
+ }
355
+ /**
356
+ * Démarre le cycle de nettoyage des entrées expirées
357
+ *
358
+ * @private
359
+ */
360
+ startCleanupCycle() {
361
+ if (this.cleanupInterval) {
362
+ clearInterval(this.cleanupInterval);
363
+ }
364
+ this.cleanupInterval = setInterval(() => {
365
+ this.cleanupExpiredEntries();
366
+ }, this.config.cleanupInterval || 6e4);
367
+ }
368
+ /**
369
+ * Nettoie les entrées expirées du cache
370
+ *
371
+ * @private
372
+ */
373
+ cleanupExpiredEntries() {
374
+ const now = Date.now();
375
+ const expiredKeys = [];
376
+ for (const [key, entry] of this.store.entries()) {
377
+ if (now > entry.expiresAt) {
378
+ expiredKeys.push(key);
379
+ }
380
+ }
381
+ expiredKeys.forEach((key) => {
382
+ this.store.delete(key);
383
+ });
384
+ if (expiredKeys.length > 0) {
385
+ this.statistics.size = this.store.size;
386
+ if (this.config.debug) {
387
+ console.log(`[Cache] Cleaned up ${expiredKeys.length} expired entries`);
388
+ }
389
+ }
390
+ }
391
+ /**
392
+ * Arrête le cache et libère les ressources
393
+ */
394
+ dispose() {
395
+ if (this.cleanupInterval) {
396
+ clearInterval(this.cleanupInterval);
397
+ this.cleanupInterval = void 0;
398
+ }
399
+ this.store.clear();
400
+ }
401
+ };
402
+
403
+ // src/infrastructure/cache/diskCache.repository.ts
404
+ import fs from "fs";
405
+ import path from "path";
406
+ import { promisify } from "util";
407
+ var fsExists = promisify(fs.exists);
408
+ var fsMkdir = promisify(fs.mkdir);
409
+ var fsReadFile = promisify(fs.readFile);
410
+ var fsWriteFile = promisify(fs.writeFile);
411
+ var fsUnlink = promisify(fs.unlink);
412
+ var fsReaddir = promisify(fs.readdir);
413
+ var DiskCache = class {
414
+ /**
415
+ * Crée une instance de DiskCache
416
+ * @param config - Configuration du cache
417
+ */
418
+ constructor(config) {
419
+ this.config = config;
420
+ this.statistics.maxSize = config.maxSize;
421
+ this.cacheDir = this.getCacheDirectory();
422
+ try {
423
+ if (!fs.existsSync(this.cacheDir)) {
424
+ fs.mkdirSync(this.cacheDir, { recursive: true });
425
+ if (this.config.debug) {
426
+ console.log(`[DiskCache] Dossier cr\xE9\xE9: ${this.cacheDir}`);
427
+ }
428
+ }
429
+ } catch (error) {
430
+ console.error(`[DiskCache] Erreur cr\xE9ation du dossier: ${error.message}`);
431
+ }
432
+ this.loadIndexFromDisk().catch((err) => {
433
+ console.error("[DiskCache] Erreur chargement index:", err);
434
+ });
435
+ this.startCleanupCycle();
436
+ }
437
+ cacheDir;
438
+ statistics = {
439
+ hits: 0,
440
+ misses: 0,
441
+ writes: 0,
442
+ size: 0,
443
+ maxSize: 0
444
+ };
445
+ memoryIndex = /* @__PURE__ */ new Map();
446
+ cleanupInterval;
447
+ /**
448
+ * Détermine le répertoire de cache
449
+ * @private
450
+ */
451
+ getCacheDirectory() {
452
+ return process.env.CACHE_DISK_DIR || path.join(process.cwd(), "storage", "cache", this.config.namespace);
453
+ }
454
+ /**
455
+ * Initialise le répertoire de cache
456
+ * @private
457
+ */
458
+ async initializeCacheDirectory() {
459
+ try {
460
+ if (!await fsExists(this.cacheDir)) {
461
+ await fsMkdir(this.cacheDir, { recursive: true });
462
+ if (this.config.debug) {
463
+ console.log(`[DiskCache] Dossier cr\xE9\xE9: ${this.cacheDir}`);
464
+ }
465
+ }
466
+ const readmePath = path.join(this.cacheDir, "README.txt");
467
+ if (!await fsExists(readmePath)) {
468
+ const readmeContent = `# Cache Directory - ${this.config.namespace}
469
+
470
+ Ce dossier contient les fichiers de cache persist\xE9s sur disque.
471
+ Chaque fichier repr\xE9sente une entr\xE9e de cache avec sa valeur et m\xE9tadonn\xE9es.
472
+
473
+ Structure:
474
+ - ${this.config.namespace}_*.json : Fichiers de donn\xE9es de cache
475
+ - index.json : Index des cl\xE9s de cache (charg\xE9 en m\xE9moire)
476
+
477
+ NE PAS SUPPRIMER MANUELLEMENT ces fichiers pendant l'ex\xE9cution du serveur.
478
+ `;
479
+ await fsWriteFile(readmePath, readmeContent, "utf8");
480
+ }
481
+ } catch (error) {
482
+ console.error(`[DiskCache] Erreur d'initialisation du dossier: ${error.message}`);
483
+ throw error;
484
+ }
485
+ }
486
+ /**
487
+ * Charge l'index des clés depuis le disque
488
+ * @private
489
+ */
490
+ async loadIndexFromDisk() {
491
+ try {
492
+ const indexPath = path.join(this.cacheDir, "index.json");
493
+ if (await fsExists(indexPath)) {
494
+ const indexData = await fsReadFile(indexPath, "utf8");
495
+ const index = JSON.parse(indexData);
496
+ for (const [key, info] of Object.entries(index)) {
497
+ this.memoryIndex.set(key, info);
498
+ }
499
+ this.statistics.size = this.memoryIndex.size;
500
+ if (this.config.debug) {
501
+ console.log(`[DiskCache] Index charg\xE9: ${this.memoryIndex.size} entr\xE9es`);
502
+ }
503
+ } else {
504
+ await this.saveIndexToDisk();
505
+ }
506
+ } catch (error) {
507
+ console.error(`[DiskCache] Erreur de chargement de l'index: ${error.message}`);
508
+ }
509
+ }
510
+ /**
511
+ * Sauvegarde l'index sur le disque
512
+ * @private
513
+ */
514
+ async saveIndexToDisk() {
515
+ try {
516
+ const indexPath = path.join(this.cacheDir, "index.json");
517
+ const indexObject = {};
518
+ for (const [key, info] of this.memoryIndex.entries()) {
519
+ indexObject[key] = info;
520
+ }
521
+ await fsWriteFile(indexPath, JSON.stringify(indexObject, null, 2), "utf8");
522
+ } catch (error) {
523
+ console.error(`[DiskCache] Erreur de sauvegarde de l'index: ${error.message}`);
524
+ }
525
+ }
526
+ /**
527
+ * Génère un nom de fichier sécurisé pour une clé
528
+ * @param key - Clé de cache
529
+ * @returns string - Nom de fichier
530
+ * @private
531
+ */
532
+ keyToFilename(key) {
533
+ const safeKey = key.replace(/[^a-z0-9]/gi, "_").toLowerCase();
534
+ return `${this.config.namespace}_${safeKey}.json`;
535
+ }
536
+ /**
537
+ * Génère le chemin complet du fichier
538
+ * @param key - Clé de cache
539
+ * @returns string - Chemin du fichier
540
+ * @private
541
+ */
542
+ keyToFilePath(key) {
543
+ const filename = this.keyToFilename(key);
544
+ return path.join(this.cacheDir, filename);
545
+ }
546
+ /**
547
+ * Stocke une valeur dans le cache
548
+ */
549
+ async set(key, value, ttl) {
550
+ const now = Date.now();
551
+ const timeToLive = ttl || this.config.defaultTTL;
552
+ const expiresAt = now + timeToLive;
553
+ const entry = {
554
+ key,
555
+ value,
556
+ timeToLive,
557
+ createdAt: now,
558
+ expiresAt,
559
+ accessCount: 0,
560
+ lastAccessedAt: now
561
+ };
562
+ if (this.memoryIndex.size >= this.config.maxSize) {
563
+ await this.evictEntry();
564
+ }
565
+ const filePath = this.keyToFilePath(key);
566
+ try {
567
+ await fsWriteFile(filePath, JSON.stringify(entry, null, 2), "utf8");
568
+ this.memoryIndex.set(key, { filePath, expiresAt });
569
+ await this.saveIndexToDisk();
570
+ this.statistics.writes++;
571
+ this.statistics.size = this.memoryIndex.size;
572
+ if (this.config.debug) {
573
+ console.log(`[DiskCache] \xC9criture: ${key} -> ${filePath}`);
574
+ }
575
+ } catch (error) {
576
+ console.error(`[DiskCache] Erreur d'\xE9criture pour ${key}:`, error.message);
577
+ throw error;
578
+ }
579
+ }
580
+ /**
581
+ * Récupère une valeur depuis le cache
582
+ */
583
+ async get(key) {
584
+ const entryInfo = this.memoryIndex.get(key);
585
+ if (!entryInfo) {
586
+ this.statistics.misses++;
587
+ return null;
588
+ }
589
+ const now = Date.now();
590
+ if (now > entryInfo.expiresAt) {
591
+ await this.delete(key);
592
+ this.statistics.misses++;
593
+ return null;
594
+ }
595
+ try {
596
+ const fileContent = await fsReadFile(entryInfo.filePath, "utf8");
597
+ const entry = JSON.parse(fileContent);
598
+ if (now > entry.expiresAt) {
599
+ await this.delete(key);
600
+ this.statistics.misses++;
601
+ return null;
602
+ }
603
+ entry.accessCount++;
604
+ entry.lastAccessedAt = now;
605
+ await fsWriteFile(entryInfo.filePath, JSON.stringify(entry, null, 2), "utf8");
606
+ if (this.config.evictionStrategy === "lru") {
607
+ await fsWriteFile(entryInfo.filePath, JSON.stringify(entry, null, 2), "utf8");
608
+ }
609
+ this.statistics.hits++;
610
+ if (this.config.debug) {
611
+ console.log(`[DiskCache] Lecture: ${key} (acc\xE8s: ${entry.accessCount})`);
612
+ }
613
+ return entry.value;
614
+ } catch (error) {
615
+ console.error(`[DiskCache] Erreur de lecture pour ${key}:`, error.message);
616
+ await this.delete(key);
617
+ this.statistics.misses++;
618
+ return null;
619
+ }
620
+ }
621
+ /**
622
+ * Supprime une entrée du cache
623
+ */
624
+ async delete(key) {
625
+ const entryInfo = this.memoryIndex.get(key);
626
+ if (entryInfo) {
627
+ try {
628
+ if (await fsExists(entryInfo.filePath)) {
629
+ await fsUnlink(entryInfo.filePath);
630
+ }
631
+ this.memoryIndex.delete(key);
632
+ await this.saveIndexToDisk();
633
+ this.statistics.size = this.memoryIndex.size;
634
+ if (this.config.debug) {
635
+ console.log(`[DiskCache] Suppression: ${key}`);
636
+ }
637
+ } catch (error) {
638
+ console.error(`[DiskCache] Erreur de suppression pour ${key}:`, error.message);
639
+ }
640
+ }
641
+ }
642
+ /**
643
+ * Vérifie si une clé existe dans le cache
644
+ */
645
+ async has(key) {
646
+ const entryInfo = this.memoryIndex.get(key);
647
+ if (!entryInfo) {
648
+ return false;
649
+ }
650
+ if (Date.now() > entryInfo.expiresAt) {
651
+ await this.delete(key);
652
+ return false;
653
+ }
654
+ return await fsExists(entryInfo.filePath);
655
+ }
656
+ /**
657
+ * Supprime toutes les entrées du cache
658
+ */
659
+ async clear() {
660
+ try {
661
+ const files = await fsReaddir(this.cacheDir);
662
+ const cacheFiles = files.filter(
663
+ (file) => file.startsWith(`${this.config.namespace}_`) && file.endsWith(".json")
664
+ );
665
+ for (const file of cacheFiles) {
666
+ const filePath = path.join(this.cacheDir, file);
667
+ await fsUnlink(filePath);
668
+ }
669
+ const indexPath = path.join(this.cacheDir, "index.json");
670
+ if (await fsExists(indexPath)) {
671
+ await fsUnlink(indexPath);
672
+ }
673
+ this.memoryIndex.clear();
674
+ this.statistics = {
675
+ hits: 0,
676
+ misses: 0,
677
+ writes: 0,
678
+ size: 0,
679
+ maxSize: this.config.maxSize
680
+ };
681
+ await this.saveIndexToDisk();
682
+ if (this.config.debug) {
683
+ console.log(`[DiskCache] Cache vid\xE9: ${cacheFiles.length} fichiers supprim\xE9s`);
684
+ }
685
+ } catch (error) {
686
+ console.error(`[DiskCache] Erreur lors du vidage:`, error.message);
687
+ throw error;
688
+ }
689
+ }
690
+ /**
691
+ * Récupère toutes les clés valides du cache
692
+ */
693
+ async keys() {
694
+ await this.cleanupExpiredEntries();
695
+ return Array.from(this.memoryIndex.keys());
696
+ }
697
+ /**
698
+ * Récupère les statistiques d'utilisation du cache
699
+ */
700
+ async getStatistics() {
701
+ const totalRequests = this.statistics.hits + this.statistics.misses;
702
+ const hitRate = totalRequests > 0 ? this.statistics.hits / totalRequests * 100 : 0;
703
+ const usagePercentage = this.statistics.maxSize > 0 ? this.statistics.size / this.statistics.maxSize * 100 : 0;
704
+ return {
705
+ ...this.statistics,
706
+ hitRate: parseFloat(hitRate.toFixed(2)),
707
+ usagePercentage: parseFloat(usagePercentage.toFixed(2))
708
+ };
709
+ }
710
+ /**
711
+ * Invalide les entrées dont la clé correspond au pattern
712
+ */
713
+ async invalidate(pattern) {
714
+ const patternRegex = new RegExp(pattern.replace("*", ".*"));
715
+ const keysToDelete = [];
716
+ for (const key of this.memoryIndex.keys()) {
717
+ if (patternRegex.test(key)) {
718
+ keysToDelete.push(key);
719
+ }
720
+ }
721
+ for (const key of keysToDelete) {
722
+ await this.delete(key);
723
+ }
724
+ return keysToDelete.length;
725
+ }
726
+ /**
727
+ * Évince une entrée selon la stratégie configurée
728
+ * @private
729
+ */
730
+ async evictEntry() {
731
+ if (this.memoryIndex.size === 0) return;
732
+ let keyToEvict = null;
733
+ switch (this.config.evictionStrategy) {
734
+ case "lru":
735
+ keyToEvict = await this.findLRUKey();
736
+ break;
737
+ case "fifo":
738
+ keyToEvict = await this.findFIFOKey();
739
+ break;
740
+ case "lfu":
741
+ keyToEvict = await this.findLFUKey();
742
+ break;
743
+ }
744
+ if (keyToEvict) {
745
+ await this.delete(keyToEvict);
746
+ }
747
+ }
748
+ /**
749
+ * Trouve la clé la moins récemment utilisée (LRU)
750
+ * @private
751
+ */
752
+ async findLRUKey() {
753
+ let oldestKey = null;
754
+ let oldestTime = Date.now();
755
+ for (const [key, info] of this.memoryIndex.entries()) {
756
+ try {
757
+ const filePath = this.keyToFilePath(key);
758
+ if (await fsExists(filePath)) {
759
+ const fileContent = await fsReadFile(filePath, "utf8");
760
+ const entry = JSON.parse(fileContent);
761
+ if (entry.lastAccessedAt < oldestTime) {
762
+ oldestTime = entry.lastAccessedAt;
763
+ oldestKey = key;
764
+ }
765
+ }
766
+ } catch (error) {
767
+ continue;
768
+ }
769
+ }
770
+ return oldestKey;
771
+ }
772
+ /**
773
+ * Trouve la première entrée insérée (FIFO)
774
+ * @private
775
+ */
776
+ async findFIFOKey() {
777
+ let oldestKey = null;
778
+ let oldestTime = Date.now();
779
+ for (const [key, info] of this.memoryIndex.entries()) {
780
+ try {
781
+ const filePath = this.keyToFilePath(key);
782
+ if (await fsExists(filePath)) {
783
+ const fileContent = await fsReadFile(filePath, "utf8");
784
+ const entry = JSON.parse(fileContent);
785
+ if (entry.createdAt < oldestTime) {
786
+ oldestTime = entry.createdAt;
787
+ oldestKey = key;
788
+ }
789
+ }
790
+ } catch (error) {
791
+ continue;
792
+ }
793
+ }
794
+ return oldestKey;
795
+ }
796
+ /**
797
+ * Trouve la clé la moins fréquemment utilisée (LFU)
798
+ * @private
799
+ */
800
+ async findLFUKey() {
801
+ let leastFrequentKey = null;
802
+ let leastAccessCount = Infinity;
803
+ for (const [key, info] of this.memoryIndex.entries()) {
804
+ try {
805
+ const filePath = this.keyToFilePath(key);
806
+ if (await fsExists(filePath)) {
807
+ const fileContent = await fsReadFile(filePath, "utf8");
808
+ const entry = JSON.parse(fileContent);
809
+ if (entry.accessCount < leastAccessCount) {
810
+ leastAccessCount = entry.accessCount;
811
+ leastFrequentKey = key;
812
+ }
813
+ }
814
+ } catch (error) {
815
+ continue;
816
+ }
817
+ }
818
+ return leastFrequentKey;
819
+ }
820
+ /**
821
+ * Démarre le cycle de nettoyage des entrées expirées
822
+ * @private
823
+ */
824
+ startCleanupCycle() {
825
+ this.cleanupExpiredEntries();
826
+ if (this.cleanupInterval) {
827
+ clearInterval(this.cleanupInterval);
828
+ }
829
+ this.cleanupInterval = setInterval(() => {
830
+ this.cleanupExpiredEntries();
831
+ }, this.config.cleanupInterval);
832
+ if (this.config.debug) {
833
+ console.log(`[DiskCache] Cycle de nettoyage: ${this.config.cleanupInterval}ms`);
834
+ }
835
+ }
836
+ /**
837
+ * Nettoie les entrées expirées du cache
838
+ * @private
839
+ */
840
+ async cleanupExpiredEntries() {
841
+ const now = Date.now();
842
+ const expiredKeys = [];
843
+ for (const [key, info] of this.memoryIndex.entries()) {
844
+ if (now > info.expiresAt) {
845
+ expiredKeys.push(key);
846
+ }
847
+ }
848
+ for (const key of expiredKeys) {
849
+ await this.delete(key);
850
+ }
851
+ if (expiredKeys.length > 0 && this.config.debug) {
852
+ console.log(`[DiskCache] Nettoyage: ${expiredKeys.length} entr\xE9es expir\xE9es`);
853
+ }
854
+ }
855
+ /**
856
+ * Récupère le chemin du dossier de cache
857
+ * @returns string
858
+ */
859
+ getCacheDirectoryPath() {
860
+ return this.cacheDir;
861
+ }
862
+ /**
863
+ * Liste tous les fichiers de cache
864
+ * @returns Promise<string[]> - Liste des chemins de fichiers
865
+ */
866
+ async listCacheFiles() {
867
+ try {
868
+ const files = await fsReaddir(this.cacheDir);
869
+ return files.filter((file) => file.startsWith(`${this.config.namespace}_`) && file.endsWith(".json")).map((file) => path.join(this.cacheDir, file));
870
+ } catch (error) {
871
+ console.error(`[DiskCache] Erreur de listing:`, error.message);
872
+ return [];
873
+ }
874
+ }
875
+ /**
876
+ * Récupère les informations d'un fichier de cache
877
+ * @param key - Clé de cache
878
+ * @returns Promise<CacheEntry | null>
879
+ */
880
+ async inspectCacheFile(key) {
881
+ try {
882
+ const filePath = this.keyToFilePath(key);
883
+ if (await fsExists(filePath)) {
884
+ const content = await fsReadFile(filePath, "utf8");
885
+ return JSON.parse(content);
886
+ }
887
+ return null;
888
+ } catch (error) {
889
+ console.error(`[DiskCache] Erreur d'inspection:`, error.message);
890
+ return null;
891
+ }
892
+ }
893
+ /**
894
+ * Arrête le cache et libère les ressources
895
+ */
896
+ dispose() {
897
+ if (this.cleanupInterval) {
898
+ clearInterval(this.cleanupInterval);
899
+ this.cleanupInterval = void 0;
900
+ }
901
+ this.saveIndexToDisk().catch(console.error);
902
+ }
903
+ };
904
+
905
+ // src/infrastructure/cache/hybridCache.repository.ts
906
+ var HybridCache = class {
907
+ l1Cache;
908
+ // Premier niveau (mémoire)
909
+ l2Cache;
910
+ // Deuxième niveau (disque)
911
+ statistics = {
912
+ l1Hits: 0,
913
+ l2Hits: 0,
914
+ misses: 0,
915
+ writes: 0,
916
+ promotions: 0
917
+ // Nombre de fois où on a promu L2 → L1
918
+ };
919
+ config;
920
+ constructor(config) {
921
+ this.config = config;
922
+ this.l1Cache = new MemoryCache({
923
+ ...config,
924
+ maxSize: Math.floor(config.maxSize * 0.2)
925
+ // L1: 20% de la capacité totale
926
+ });
927
+ this.l2Cache = new DiskCache({
928
+ ...config,
929
+ maxSize: Math.floor(config.maxSize * 0.8)
930
+ // L2: 80% de la capacité totale
931
+ });
932
+ }
933
+ async set(key, value, ttl) {
934
+ try {
935
+ await this.l2Cache.set(key, value, ttl);
936
+ try {
937
+ await this.l1Cache.set(key, value, ttl);
938
+ } catch (l1Error) {
939
+ if (this.config.debug) {
940
+ console.log(`[HybridCache] L1 plein pour ${key}, gard\xE9 seulement en L2`);
941
+ }
942
+ }
943
+ this.statistics.writes++;
944
+ } catch (error) {
945
+ console.error(`[HybridCache] Erreur set pour ${key}:`, error.message);
946
+ throw error;
947
+ }
948
+ }
949
+ async get(key) {
950
+ const l1Value = await this.l1Cache.get(key);
951
+ if (l1Value !== null) {
952
+ this.statistics.l1Hits++;
953
+ if (this.config.debug) {
954
+ console.log(`[HybridCache] L1 HIT pour ${key}`);
955
+ }
956
+ return l1Value;
957
+ }
958
+ const l2Value = await this.l2Cache.get(key);
959
+ if (l2Value !== null) {
960
+ this.statistics.l2Hits++;
961
+ try {
962
+ await this.l1Cache.set(key, l2Value);
963
+ this.statistics.promotions++;
964
+ if (this.config.debug) {
965
+ console.log(`[HybridCache] Promotion L2\u2192L1 pour ${key}`);
966
+ }
967
+ } catch (promotionError) {
968
+ if (this.config.debug) {
969
+ console.log(`[HybridCache] Pas de promotion pour ${key} (L1 plein)`);
970
+ }
971
+ }
972
+ return l2Value;
973
+ }
974
+ this.statistics.misses++;
975
+ return null;
976
+ }
977
+ async delete(key) {
978
+ await Promise.all([
979
+ this.l1Cache.delete(key).catch(() => {
980
+ }),
981
+ // Ignorer les erreurs L1
982
+ this.l2Cache.delete(key)
983
+ ]);
984
+ }
985
+ async has(key) {
986
+ if (await this.l1Cache.has(key)) {
987
+ return true;
988
+ }
989
+ return await this.l2Cache.has(key);
990
+ }
991
+ async clear() {
992
+ await Promise.all([
993
+ this.l1Cache.clear(),
994
+ this.l2Cache.clear()
995
+ ]);
996
+ this.statistics = {
997
+ l1Hits: 0,
998
+ l2Hits: 0,
999
+ misses: 0,
1000
+ writes: 0,
1001
+ promotions: 0
1002
+ };
1003
+ }
1004
+ async keys() {
1005
+ const [l1Keys, l2Keys] = await Promise.all([
1006
+ this.l1Cache.keys(),
1007
+ this.l2Cache.keys()
1008
+ ]);
1009
+ const allKeys = /* @__PURE__ */ new Set([...l1Keys, ...l2Keys]);
1010
+ return Array.from(allKeys);
1011
+ }
1012
+ async getStatistics() {
1013
+ const l1Stats = await this.l1Cache.getStatistics();
1014
+ const l2Stats = await this.l2Cache.getStatistics();
1015
+ const totalHits = this.statistics.l1Hits + this.statistics.l2Hits;
1016
+ const totalRequests = totalHits + this.statistics.misses;
1017
+ const hitRate = totalRequests > 0 ? totalHits / totalRequests * 100 : 0;
1018
+ const totalSize = l1Stats.size + l2Stats.size;
1019
+ const totalMaxSize = l1Stats.maxSize + l2Stats.maxSize;
1020
+ const usagePercentage = totalMaxSize > 0 ? totalSize / totalMaxSize * 100 : 0;
1021
+ return {
1022
+ hits: totalHits,
1023
+ misses: this.statistics.misses,
1024
+ writes: this.statistics.writes,
1025
+ size: totalSize,
1026
+ maxSize: totalMaxSize,
1027
+ hitRate: parseFloat(hitRate.toFixed(2)),
1028
+ usagePercentage: parseFloat(usagePercentage.toFixed(2))
1029
+ };
1030
+ }
1031
+ async invalidate(pattern) {
1032
+ const [l1Count, l2Count] = await Promise.all([
1033
+ this.l1Cache.invalidate(pattern),
1034
+ this.l2Cache.invalidate(pattern)
1035
+ ]);
1036
+ return l1Count + l2Count;
1037
+ }
1038
+ /**
1039
+ * Statistiques détaillées du cache hybride
1040
+ */
1041
+ async getDetailedStatistics() {
1042
+ const totalHits = this.statistics.l1Hits + this.statistics.l2Hits;
1043
+ const totalRequests = totalHits + this.statistics.misses;
1044
+ return {
1045
+ performance: {
1046
+ totalRequests,
1047
+ totalHits,
1048
+ misses: this.statistics.misses,
1049
+ overallHitRate: totalRequests > 0 ? (totalHits / totalRequests * 100).toFixed(2) + "%" : "0%",
1050
+ l1HitRate: this.statistics.l1Hits > 0 ? (this.statistics.l1Hits / totalHits * 100).toFixed(2) + "%" : "0%",
1051
+ l2HitRate: this.statistics.l2Hits > 0 ? (this.statistics.l2Hits / totalHits * 100).toFixed(2) + "%" : "0%",
1052
+ promotions: this.statistics.promotions
1053
+ },
1054
+ levels: {
1055
+ l1: await this.l1Cache.getStatistics(),
1056
+ l2: await this.l2Cache.getStatistics()
1057
+ },
1058
+ cache: {
1059
+ l1Size: this.config.maxSize * 0.2,
1060
+ l2Size: this.config.maxSize * 0.8,
1061
+ strategy: this.config.evictionStrategy
1062
+ }
1063
+ };
1064
+ }
1065
+ /**
1066
+ * Synchronise L1 avec L2 (après redémarrage)
1067
+ */
1068
+ async warmupL1FromL2(keysToWarm = []) {
1069
+ let warmedCount = 0;
1070
+ const keys = keysToWarm.length > 0 ? keysToWarm : (await this.l2Cache.keys()).slice(0, 100);
1071
+ for (const key of keys) {
1072
+ try {
1073
+ const value = await this.l2Cache.get(key);
1074
+ if (value !== null) {
1075
+ await this.l1Cache.set(key, value);
1076
+ warmedCount++;
1077
+ }
1078
+ } catch (error) {
1079
+ continue;
1080
+ }
1081
+ }
1082
+ if (this.config.debug) {
1083
+ console.log(`[HybridCache] Warmup: ${warmedCount} cl\xE9s charg\xE9es de L2 vers L1`);
1084
+ }
1085
+ return warmedCount;
1086
+ }
1087
+ };
1088
+
1089
+ // src/infrastructure/cache/cacheFactory.repository.ts
1090
+ var CacheFactoryRepository = class {
1091
+ /**
1092
+ * Crée une instance de cache basée sur la configuration
1093
+ *
1094
+ * @param customConfig - Configuration personnalisée (optionnel)
1095
+ * @returns CacheRepository | null - Instance de cache ou null si désactivé
1096
+ */
1097
+ static createCache(customConfig) {
1098
+ const envLoader = EnvironmentLoader.getInstance();
1099
+ const baseConfig = envLoader.getConfig();
1100
+ const config = { ...baseConfig, ...customConfig };
1101
+ if (!config.enabled) {
1102
+ return null;
1103
+ }
1104
+ const storageType = (config.storageType || process.env.CACHE_STORAGE_TYPE || "hybrid").toLowerCase();
1105
+ switch (storageType) {
1106
+ case "memory":
1107
+ return new MemoryCache(config);
1108
+ case "disk":
1109
+ return new DiskCache(config);
1110
+ case "hybrid":
1111
+ default:
1112
+ return new HybridCache(config);
1113
+ }
1114
+ }
1115
+ /**
1116
+ * Crée un cache avec une configuration minimale
1117
+ *
1118
+ * @param enabled - Activer le cache
1119
+ * @param maxSize - Taille maximale
1120
+ * @returns CacheRepository | null
1121
+ */
1122
+ static createSimpleCache(enabled, maxSize = 100) {
1123
+ const envLoader = EnvironmentLoader.getInstance();
1124
+ const baseConfig = envLoader.getConfig();
1125
+ return this.createCache({ ...baseConfig, enabled, maxSize });
1126
+ }
1127
+ };
1128
+
1129
+ // src/core/usecases/cacheManagement.usecase.ts
1130
+ var CacheManagementUseCase = class {
1131
+ /**
1132
+ * Creates an instance of CacheManagementUseCase
1133
+ *
1134
+ * @param cacheRepository - Injected cache repository
1135
+ */
1136
+ constructor(cacheRepository) {
1137
+ this.cacheRepository = cacheRepository;
1138
+ }
1139
+ /**
1140
+ * Retrieves a value from cache or adds it if missing
1141
+ * "Cache-Aside" / "Lazy Loading" pattern
1142
+ *
1143
+ * @param key - Entry key
1144
+ * @param fetcher - Async function to fetch the value if missing
1145
+ * @param ttl - Specific time to live (optional)
1146
+ * @returns Promise<T> - Value from cache or fetcher result
1147
+ */
1148
+ async getOrSet(key, fetcher, ttl) {
1149
+ const cachedValue = await this.cacheRepository.get(key);
1150
+ if (cachedValue !== null) {
1151
+ return cachedValue;
1152
+ }
1153
+ const freshValue = await fetcher();
1154
+ await this.cacheRepository.set(key, freshValue, ttl);
1155
+ return freshValue;
1156
+ }
1157
+ /**
1158
+ * Invalidates all entries matching the pattern
1159
+ *
1160
+ * @param pattern - Pattern to select entries to invalidate
1161
+ * @returns Promise<number> - Number of invalidated entries
1162
+ */
1163
+ async invalidateByPattern(pattern) {
1164
+ return await this.cacheRepository.invalidate(pattern);
1165
+ }
1166
+ /**
1167
+ * Completely empties the cache
1168
+ *
1169
+ * @returns Promise<void>
1170
+ */
1171
+ async clearCache() {
1172
+ await this.cacheRepository.clear();
1173
+ }
1174
+ /**
1175
+ * Retrieves detailed cache statistics
1176
+ *
1177
+ * @returns Promise<CacheStatistics>
1178
+ */
1179
+ async getCacheStatistics() {
1180
+ return await this.cacheRepository.getStatistics();
1181
+ }
1182
+ /**
1183
+ * Checks the health status of the cache
1184
+ *
1185
+ * @returns Promise<{ healthy: boolean; message: string; statistics: CacheStatistics }>
1186
+ */
1187
+ async healthCheck() {
1188
+ try {
1189
+ const stats = await this.cacheRepository.getStatistics();
1190
+ const isHealthy = stats.size <= stats.maxSize * 0.9;
1191
+ return {
1192
+ healthy: isHealthy,
1193
+ message: isHealthy ? "Cache is operating normally" : "Cache is near capacity limit",
1194
+ statistics: stats
1195
+ };
1196
+ } catch (error) {
1197
+ return {
1198
+ healthy: false,
1199
+ message: `Cache health check failed: ${error.message}`,
1200
+ statistics: {
1201
+ hits: 0,
1202
+ misses: 0,
1203
+ writes: 0,
1204
+ size: 0,
1205
+ maxSize: 0,
1206
+ hitRate: 0,
1207
+ usagePercentage: 0
1208
+ }
1209
+ };
1210
+ }
1211
+ }
1212
+ };
1213
+
1214
+ // src/application/services/cacheService.service.ts
1215
+ import { translate, translationLoaderConfig } from "opticore-loader-translation";
1216
+ import { LoggerCore } from "opticore-logger";
1217
+ var CacheService = class _CacheService {
1218
+ cacheUseCase;
1219
+ cacheRepository;
1220
+ localLang;
1221
+ configLogger;
1222
+ /**
1223
+ * Creates a CacheService instance
1224
+ *
1225
+ * @param cacheRepository - Cache repository (may be null if disabled)
1226
+ * @param localLang
1227
+ * @param configLogger
1228
+ */
1229
+ constructor(cacheRepository, localLang, configLogger) {
1230
+ this.cacheRepository = cacheRepository;
1231
+ this.localLang = localLang;
1232
+ this.configLogger = configLogger;
1233
+ this.cacheUseCase = cacheRepository ? new CacheManagementUseCase(cacheRepository) : null;
1234
+ }
1235
+ /**
1236
+ * Retrieves a value or adds it if absent
1237
+ *
1238
+ * @param key - Input key
1239
+ * @param fetcher - Function to retrieve the value if absent
1240
+ * @param ttl - Specific time to live
1241
+ * @returns Promise<T> - Cache value or fetch result
1242
+ */
1243
+ async getOrSet(key, fetcher, ttl) {
1244
+ if (!this.cacheUseCase) {
1245
+ return fetcher();
1246
+ }
1247
+ return this.cacheUseCase.getOrSet(key, fetcher, ttl);
1248
+ }
1249
+ /**
1250
+ * Stores a value directly in the cache
1251
+ *
1252
+ * @param key - Input key
1253
+ * @param value - Value to store
1254
+ * @param ttl - Time to live
1255
+ * @returns Promise<void>
1256
+ */
1257
+ async set(key, value, ttl) {
1258
+ if (!this.cacheRepository) return;
1259
+ await this.cacheRepository.set(key, value, ttl);
1260
+ }
1261
+ /**
1262
+ * Retrieves a value from the cache
1263
+ *
1264
+ * @param key - Input key
1265
+ * @returns Promise<T | null>
1266
+ */
1267
+ async get(key) {
1268
+ if (!this.cacheRepository) return null;
1269
+ return this.cacheRepository.get(key);
1270
+ }
1271
+ /**
1272
+ * Removes an entry from the cache
1273
+ *
1274
+ * @param key - Entry key
1275
+ * @returns Promise<void>
1276
+ */
1277
+ async delete(key) {
1278
+ if (!this.cacheRepository) return;
1279
+ await this.cacheRepository.delete(key);
1280
+ }
1281
+ /**
1282
+ * Invalidates entries matching the pattern
1283
+ *
1284
+ * @param pattern - Pattern for selecting entries
1285
+ * @returns Promise<number> - Number of invalidated entries
1286
+ */
1287
+ async invalidate(pattern) {
1288
+ if (!this.cacheRepository) return 0;
1289
+ return this.cacheRepository.invalidate(pattern);
1290
+ }
1291
+ /**
1292
+ * Clear cache completely
1293
+ *
1294
+ * @returns Promise<void>
1295
+ */
1296
+ async clear() {
1297
+ if (!this.cacheRepository) return;
1298
+ await this.cacheRepository.clear();
1299
+ }
1300
+ /**
1301
+ * Get cache stats
1302
+ *
1303
+ * @returns Promise<CacheStatistics>
1304
+ */
1305
+ async getStatistics() {
1306
+ if (!this.cacheRepository) {
1307
+ return {
1308
+ hits: 0,
1309
+ misses: 0,
1310
+ writes: 0,
1311
+ size: 0,
1312
+ maxSize: 0,
1313
+ hitRate: 0,
1314
+ usagePercentage: 0
1315
+ };
1316
+ }
1317
+ return this.cacheRepository.getStatistics();
1318
+ }
1319
+ /**
1320
+ * Check the cache health status
1321
+ *
1322
+ * @returns Promise<{ healthy: boolean; message: string }>
1323
+ */
1324
+ async healthCheck() {
1325
+ this.translatorConfig();
1326
+ if (!this.cacheUseCase) {
1327
+ return {
1328
+ healthy: true,
1329
+ message: translate({
1330
+ key: "DISABLED_CACHE",
1331
+ localeLanguage: this.localLang,
1332
+ params: void 0
1333
+ })
1334
+ };
1335
+ }
1336
+ const health = await this.cacheUseCase.healthCheck();
1337
+ return {
1338
+ healthy: health.healthy,
1339
+ message: health.message
1340
+ };
1341
+ }
1342
+ /**
1343
+ * Check if the cache is enabled
1344
+ * @returns boolean
1345
+ */
1346
+ isEnabled() {
1347
+ return this.cacheRepository !== null;
1348
+ }
1349
+ /**
1350
+ * Retrieves all keys from the cache
1351
+ *
1352
+ * @returns Promise<string[]> - List of keys
1353
+ */
1354
+ async getKeys() {
1355
+ if (!this.cacheRepository) return [];
1356
+ return this.cacheRepository.keys();
1357
+ }
1358
+ /**
1359
+ * Checks if a key exists in the cache
1360
+ *
1361
+ * @param key - Key to check
1362
+ * @returns Promise<boolean>
1363
+ */
1364
+ async has(key) {
1365
+ if (!this.cacheRepository) return false;
1366
+ return this.cacheRepository.has(key);
1367
+ }
1368
+ /**
1369
+ * Creates and configures a full cache service
1370
+ *
1371
+ * @param customConfig - Custom configuration (optional)
1372
+ * @returns CacheService - Cache service configured
1373
+ */
1374
+ createCache(customConfig) {
1375
+ const cacheRepository = CacheFactoryRepository.createCache(customConfig);
1376
+ return new _CacheService(cacheRepository, this.localLang);
1377
+ }
1378
+ /**
1379
+ *
1380
+ * @private
1381
+ */
1382
+ translatorConfig() {
1383
+ return translationLoaderConfig({
1384
+ localLang: this.localLang,
1385
+ locationTranslationFile: ["utils", "translations"],
1386
+ packageName: "opticore-cache"
1387
+ });
1388
+ }
1389
+ logger() {
1390
+ return new LoggerCore(this.configLogger);
1391
+ }
1392
+ };
1393
+
1394
+ // src/infrastructure/config/routeValidator.config.ts
1395
+ var RouteValidator = class {
1396
+ /**
1397
+ * Crée une instance de RouteValidator
1398
+ *
1399
+ * @param skipRoutes - Liste des routes à ignorer
1400
+ * @param enableWildcards - Activer le support des wildcards
1401
+ */
1402
+ constructor(skipRoutes = [], enableWildcards = true) {
1403
+ this.skipRoutes = skipRoutes;
1404
+ this.enableWildcards = enableWildcards;
1405
+ this.compileRoutePatterns();
1406
+ }
1407
+ routePatterns = [];
1408
+ /**
1409
+ * Compile les patterns de route en expressions régulières
1410
+ *
1411
+ * @private
1412
+ */
1413
+ compileRoutePatterns() {
1414
+ this.routePatterns = this.skipRoutes.map((route) => {
1415
+ const pattern = this.parseRoutePattern(route);
1416
+ const regex = this.compileToRegex(pattern.path, this.enableWildcards);
1417
+ return {
1418
+ method: pattern.method,
1419
+ path: pattern.path,
1420
+ regexPattern: regex
1421
+ };
1422
+ });
1423
+ }
1424
+ /**
1425
+ * Parse un pattern de route en méthode et chemin
1426
+ *
1427
+ * @param routePattern - Pattern à parser (format: "METHOD:path" ou "path")
1428
+ * @returns {{ method?: string; path: string }}
1429
+ */
1430
+ parseRoutePattern(routePattern) {
1431
+ const methodSeparatorIndex = routePattern.indexOf(":");
1432
+ if (methodSeparatorIndex > 0) {
1433
+ const methodPart = routePattern.substring(0, methodSeparatorIndex).toUpperCase();
1434
+ const pathPart = routePattern.substring(methodSeparatorIndex + 1);
1435
+ const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
1436
+ if (httpMethods.includes(methodPart)) {
1437
+ return {
1438
+ method: methodPart,
1439
+ path: pathPart.trim()
1440
+ };
1441
+ }
1442
+ }
1443
+ return {
1444
+ method: void 0,
1445
+ // Toutes méthodes
1446
+ path: routePattern.trim()
1447
+ };
1448
+ }
1449
+ /**
1450
+ * Compile un chemin en expression régulière
1451
+ *
1452
+ * @param path - Chemin à compiler
1453
+ * @param enableWildcards - Activer le support des wildcards
1454
+ * @returns RegExp
1455
+ */
1456
+ compileToRegex(path2, enableWildcards) {
1457
+ if (!enableWildcards || !path2.includes("*")) {
1458
+ return new RegExp(`^${this.escapeRegex(path2)}$`, "i");
1459
+ }
1460
+ let regexString = this.escapeRegex(path2).replace(/\\\*/g, ".*");
1461
+ if (regexString.endsWith("/.*")) {
1462
+ regexString = regexString.replace(/\/\.\*$/, "(\\/.*)?");
1463
+ }
1464
+ return new RegExp(`^${regexString}$`, "i");
1465
+ }
1466
+ /**
1467
+ * Échappe les caractères spéciaux pour les expressions régulières
1468
+ *
1469
+ * @param string - Chaîne à échapper
1470
+ * @returns string
1471
+ */
1472
+ escapeRegex(string) {
1473
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1474
+ }
1475
+ /**
1476
+ * Vérifie si une requête correspond à un pattern de route ignorée
1477
+ *
1478
+ * @param request - Requête Express à vérifier
1479
+ * @returns boolean - True si la route doit être ignorée
1480
+ */
1481
+ shouldSkipRoute(request) {
1482
+ const requestMethod = request.method.toUpperCase();
1483
+ const requestPath = this.normalizePath(request.path);
1484
+ for (const pattern of this.routePatterns) {
1485
+ if (pattern.method && pattern.method !== requestMethod) {
1486
+ continue;
1487
+ }
1488
+ if (pattern.regexPattern.test(requestPath)) {
1489
+ return true;
1490
+ }
1491
+ const pathWithoutQuery = request?.originalUrl.split("?")[0];
1492
+ if (pattern.regexPattern.test(this.normalizePath(pathWithoutQuery))) {
1493
+ return true;
1494
+ }
1495
+ }
1496
+ return false;
1497
+ }
1498
+ /**
1499
+ * Normalise un chemin (supprime les slashes de fin)
1500
+ *
1501
+ * @param path - Chemin à normaliser
1502
+ * @returns string - Chemin normalisé
1503
+ */
1504
+ normalizePath(path2) {
1505
+ if (path2.length > 1 && path2.endsWith("/")) {
1506
+ return path2.slice(0, -1);
1507
+ }
1508
+ return path2;
1509
+ }
1510
+ /**
1511
+ * Récupère la liste des patterns compilés (pour debug)
1512
+ *
1513
+ * @returns RoutePattern[]
1514
+ */
1515
+ getCompiledPatterns() {
1516
+ return [...this.routePatterns];
1517
+ }
1518
+ };
1519
+
1520
+ // src/infrastructure/middleware/cache.middleware.ts
1521
+ var CacheMiddleware = class {
1522
+ routeValidator;
1523
+ config;
1524
+ /**
1525
+ * Crée une instance de CacheMiddleware
1526
+ */
1527
+ constructor() {
1528
+ const envLoader = EnvironmentLoader.getInstance();
1529
+ this.config = envLoader.getConfig();
1530
+ this.routeValidator = new RouteValidator(
1531
+ this.config.skipRoutes,
1532
+ this.config.enableWildcards
1533
+ );
1534
+ }
1535
+ /**
1536
+ * Crée un middleware Express configurable
1537
+ * @param cacheRepository - Repository de cache à utiliser
1538
+ * @param options - Options de configuration du middleware
1539
+ * @returns Middleware Express
1540
+ */
1541
+ createMiddleware(cacheRepository, options = {}) {
1542
+ return async (req, res, next) => {
1543
+ if (!cacheRepository || !this.config.enabled || options.enabled === false) {
1544
+ return next();
1545
+ }
1546
+ if (this.shouldSkipRequest(req, options)) {
1547
+ if (this.config.debug) {
1548
+ console.log(`[Cache] Skipping request: ${req.method} ${req.path}`);
1549
+ }
1550
+ return next();
1551
+ }
1552
+ if (req.method !== "GET") {
1553
+ return next();
1554
+ }
1555
+ const keyGenerator = options.keyGenerator || this.defaultKeyGenerator;
1556
+ const cacheKey = keyGenerator(req);
1557
+ const ttl = options.timeToLive || this.config.defaultTTL;
1558
+ try {
1559
+ const cachedResponse = await cacheRepository.get(cacheKey);
1560
+ if (cachedResponse !== null) {
1561
+ this.addCacheHeaders(res, "HIT", cacheKey);
1562
+ if (this.config.debug) {
1563
+ console.log(`[Cache] Cache hit for key: ${cacheKey}`);
1564
+ }
1565
+ return res.json(cachedResponse);
1566
+ }
1567
+ if (this.config.debug) {
1568
+ console.log(`[Cache] Cache miss for key: ${cacheKey}`);
1569
+ }
1570
+ const originalJson = res.json.bind(res);
1571
+ let responseBody;
1572
+ res.json = (body) => {
1573
+ responseBody = body;
1574
+ if (res.statusCode >= 200 && res.statusCode < 300) {
1575
+ const bodyToCache = options.responseTransformer ? options.responseTransformer(body) : body;
1576
+ cacheRepository.set(cacheKey, bodyToCache, ttl).then(() => {
1577
+ if (this.config.debug) {
1578
+ console.log(`[Cache] Successfully cached: ${cacheKey}`);
1579
+ }
1580
+ }).catch((error) => {
1581
+ console.error("[Cache] Error caching response:", error);
1582
+ });
1583
+ this.addCacheHeaders(res, "MISS", cacheKey, ttl);
1584
+ }
1585
+ return originalJson(body);
1586
+ };
1587
+ next();
1588
+ } catch (error) {
1589
+ if (this.config.debug) {
1590
+ console.error("[Cache] Middleware error:", error);
1591
+ }
1592
+ next();
1593
+ }
1594
+ };
1595
+ }
1596
+ /**
1597
+ * Détermine si une requête doit être skipée
1598
+ * @param req - Requête Express
1599
+ * @param options - Options du middleware
1600
+ * @returns boolean
1601
+ * @private
1602
+ */
1603
+ shouldSkipRequest(req, options) {
1604
+ if (options.shouldSkip && options.shouldSkip(req)) {
1605
+ return true;
1606
+ }
1607
+ if (this.routeValidator.shouldSkipRoute(req)) {
1608
+ return true;
1609
+ }
1610
+ const defaultSkipConditions = [
1611
+ // Header spécifique pour bypasser le cache
1612
+ req.headers["x-cache-bypass"] === "true",
1613
+ req.headers["x-no-cache"] === "true",
1614
+ // Certains chemins par défaut
1615
+ req.path.includes("/api/admin/"),
1616
+ req.path.includes("/api/debug/"),
1617
+ req.path === "/health",
1618
+ req.path === "/metrics",
1619
+ // Basic Authentication
1620
+ req.headers.authorization && !this.config.userAware
1621
+ ];
1622
+ return defaultSkipConditions.some((condition) => condition);
1623
+ }
1624
+ /**
1625
+ * Générateur de clé par défaut
1626
+ * @param req - Requête Express
1627
+ * @returns string - Clé de cache
1628
+ * @private
1629
+ */
1630
+ defaultKeyGenerator(req) {
1631
+ const keyParts = [
1632
+ this.config.namespace,
1633
+ req.method,
1634
+ this.normalizePath(req.path)
1635
+ ];
1636
+ if (this.config.userAware && req.user?.id) {
1637
+ keyParts.push(`uid:${req.user.id}`);
1638
+ }
1639
+ if (Object.keys(req.query).length > 0) {
1640
+ const queryString = Object.keys(req.query).sort().map((key) => `${key}=${req.query[key]}`).join("&");
1641
+ keyParts.push(`qs:${queryString}`);
1642
+ }
1643
+ return keyParts.join(":");
1644
+ }
1645
+ /**
1646
+ * Normalizes a path for the cache
1647
+ *
1648
+ * @param path - Path to be standardized
1649
+ * @returns string
1650
+ * @private
1651
+ */
1652
+ normalizePath(path2) {
1653
+ if (path2.length > 1 && path2.endsWith("/")) {
1654
+ return path2.slice(0, -1);
1655
+ }
1656
+ return path2;
1657
+ }
1658
+ /**
1659
+ * Adds HTTP headers related to the cache
1660
+ *
1661
+ * @param res - Express Response
1662
+ * @param status - Cache status (HIT/MISS)
1663
+ * @param key - Cache key
1664
+ * @param ttl - Lifespan (optional)
1665
+ * @private
1666
+ */
1667
+ addCacheHeaders(res, status, key, ttl) {
1668
+ res.setHeader("X-Cache-Status", status);
1669
+ res.setHeader("X-Cache-Key", key);
1670
+ if (ttl) {
1671
+ res.setHeader("X-Cache-TTL", ttl.toString());
1672
+ res.setHeader("X-Cache-Expires", new Date(Date.now() + ttl).toUTCString());
1673
+ }
1674
+ res.setHeader("X-Cache-Timestamp", (/* @__PURE__ */ new Date()).toISOString());
1675
+ }
1676
+ /**
1677
+ * Creates middleware to display cache statistics
1678
+ * @param cacheRepository - Cache repository
1679
+ * @returns Middleware Express
1680
+ */
1681
+ createStatsMiddleware(cacheRepository) {
1682
+ return async (req, res, next) => {
1683
+ if (req.path === "/api/cache/stats" && req.method === "GET") {
1684
+ try {
1685
+ if (!cacheRepository) {
1686
+ return res.json({
1687
+ enabled: false,
1688
+ message: "Cache is disabled",
1689
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1690
+ });
1691
+ }
1692
+ const stats = await cacheRepository.getStatistics();
1693
+ const compiledPatterns = this.routeValidator.getCompiledPatterns();
1694
+ res.json({
1695
+ ...stats,
1696
+ enabled: true,
1697
+ configuration: {
1698
+ ...this.config,
1699
+ skipRoutes: compiledPatterns.map((pattern) => ({
1700
+ method: pattern.method || "ALL",
1701
+ path: pattern.path,
1702
+ pattern: pattern.regexPattern.toString()
1703
+ }))
1704
+ },
1705
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1706
+ });
1707
+ } catch (error) {
1708
+ res.status(500).json({
1709
+ error: "Failed to retrieve cache statistics",
1710
+ details: error.message
1711
+ });
1712
+ }
1713
+ } else {
1714
+ next();
1715
+ }
1716
+ };
1717
+ }
1718
+ /**
1719
+ * Create a middleware to clear the cache
1720
+ *
1721
+ * @param cacheRepository - Cache repository
1722
+ * @returns Middleware Express
1723
+ */
1724
+ createClearMiddleware(cacheRepository) {
1725
+ return async (req, res, next) => {
1726
+ if (req.path === "/api/cache/clear" && req.method === "DELETE") {
1727
+ try {
1728
+ if (!cacheRepository) {
1729
+ return res.json({
1730
+ success: false,
1731
+ message: "Cache is disabled",
1732
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1733
+ });
1734
+ }
1735
+ const pattern = req.query.pattern;
1736
+ let message;
1737
+ let clearedCount = 0;
1738
+ if (pattern) {
1739
+ clearedCount = await cacheRepository.invalidate(pattern);
1740
+ message = `Cleared ${clearedCount} entries matching pattern: ${pattern}`;
1741
+ } else {
1742
+ await cacheRepository.clear();
1743
+ message = "Cache cleared successfully";
1744
+ }
1745
+ res.json({
1746
+ success: true,
1747
+ message,
1748
+ clearedCount,
1749
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1750
+ });
1751
+ } catch (error) {
1752
+ res.status(500).json({
1753
+ success: false,
1754
+ error: "Failed to clear cache",
1755
+ details: error.message
1756
+ });
1757
+ }
1758
+ } else {
1759
+ next();
1760
+ }
1761
+ };
1762
+ }
1763
+ };
1764
+
1765
+ // src/application/services/cacheMiddleware.service.ts
1766
+ var SCacheMiddleware = class {
1767
+ /**
1768
+ * Create and configure a complete caching service
1769
+ *
1770
+ * @param customConfig - Custom configuration (optional)
1771
+ * @returns CacheService - Cache service configured
1772
+ */
1773
+ static cacheService(customConfig) {
1774
+ const cacheRepository = CacheFactoryRepository.createCache(customConfig);
1775
+ return new CacheService(cacheRepository, customConfig?.localLang);
1776
+ }
1777
+ /**
1778
+ * Creates an Express middleware for automatic caching
1779
+ *
1780
+ * @param customConfig - Custom configuration (optional)
1781
+ * @returns Express middleware configured
1782
+ */
1783
+ static cacheMiddleware(customConfig) {
1784
+ const cacheRepository = CacheFactoryRepository.createCache(customConfig);
1785
+ const middleware = new CacheMiddleware();
1786
+ return middleware.createMiddleware(cacheRepository);
1787
+ }
1788
+ /**
1789
+ * Creates a middleware for cache statistics
1790
+ *
1791
+ * @returns Middleware Express
1792
+ */
1793
+ static createStatsMiddleware() {
1794
+ const cacheRepository = CacheFactoryRepository.createCache();
1795
+ const middleware = new CacheMiddleware();
1796
+ return middleware.createStatsMiddleware(cacheRepository);
1797
+ }
1798
+ /**
1799
+ * Create a middleware to clear the cache
1800
+ *
1801
+ * @returns Middleware Express
1802
+ */
1803
+ static createClearMiddleware() {
1804
+ const cacheRepository = CacheFactoryRepository.createCache();
1805
+ const middleware = new CacheMiddleware();
1806
+ return middleware.createClearMiddleware(cacheRepository);
1807
+ }
1808
+ };
1809
+
1810
+ // src/application/services/adaptedHttpCache.service.ts
1811
+ var AdaptedHttpCacheService = class {
1812
+ constructor(cacheService, namespace = "http-cache") {
1813
+ this.cacheService = cacheService;
1814
+ this.namespace = namespace;
1815
+ }
1816
+ stats = { totalRequests: 0, cacheHits: 0, cacheMisses: 0 };
1817
+ /**
1818
+ *
1819
+ * @param url
1820
+ * @param options
1821
+ * @param cacheOptions
1822
+ */
1823
+ async getWithCache(url, options, cacheOptions) {
1824
+ this.stats.totalRequests++;
1825
+ if (cacheOptions?.bypassCache) {
1826
+ const response = await this.executeRequest("GET", url, options);
1827
+ return this.mergeResponse(response);
1828
+ }
1829
+ const cacheKey = this.generateKey("GET", url, options, cacheOptions?.customKey);
1830
+ const fullKey = `${this.namespace}:${cacheKey}`;
1831
+ try {
1832
+ const cachedResponse = await this.cacheService.getOrSet(
1833
+ fullKey,
1834
+ async () => {
1835
+ this.stats.cacheMisses++;
1836
+ return await this.executeRequest("GET", url, options);
1837
+ },
1838
+ cacheOptions?.timeToLive
1839
+ );
1840
+ this.stats.cacheHits++;
1841
+ return this.mergeResponse(cachedResponse);
1842
+ } catch (error) {
1843
+ this.stats.cacheMisses++;
1844
+ const response = await this.executeRequest("GET", url, options);
1845
+ return this.mergeResponse(response);
1846
+ }
1847
+ }
1848
+ /**
1849
+ *
1850
+ * @param url
1851
+ * @param data
1852
+ * @param options
1853
+ * @param cacheResponse
1854
+ * @param cacheOptions
1855
+ */
1856
+ async postWithCache(url, data, options, cacheResponse = false, cacheOptions) {
1857
+ this.stats.totalRequests++;
1858
+ const requestOptions = {
1859
+ method: "POST",
1860
+ headers: {
1861
+ "Content-Type": "application/json",
1862
+ ...options?.headers
1863
+ },
1864
+ body: JSON.stringify(data),
1865
+ ...options
1866
+ };
1867
+ if (!cacheResponse || cacheOptions?.bypassCache) {
1868
+ const response = await this.executeRequest("POST", url, requestOptions);
1869
+ return this.mergeResponse(response);
1870
+ }
1871
+ const cacheKey = this.generateKey("POST", url, requestOptions, cacheOptions?.customKey);
1872
+ const fullCacheKey = `${cacheOptions?.namespace || this.namespace}:${cacheKey}`;
1873
+ try {
1874
+ const cachedResponse = await this.cacheService.getOrSet(
1875
+ fullCacheKey,
1876
+ async () => {
1877
+ this.stats.cacheMisses++;
1878
+ return await this.executeRequest("POST", url, requestOptions);
1879
+ },
1880
+ cacheOptions?.timeToLive
1881
+ );
1882
+ return this.mergeResponse(cachedResponse);
1883
+ } catch (error) {
1884
+ console.error(`Cache error for POST ${url}:`, error.message);
1885
+ this.stats.cacheMisses++;
1886
+ const response = await this.executeRequest("POST", url, requestOptions);
1887
+ return this.mergeResponse(response);
1888
+ }
1889
+ }
1890
+ /**
1891
+ *
1892
+ * @param method
1893
+ * @param url
1894
+ * @param options
1895
+ * @private
1896
+ */
1897
+ async executeRequest(method, url, options) {
1898
+ const response = await fetch(url, { method, ...options });
1899
+ const data = await response.json();
1900
+ return {
1901
+ data,
1902
+ metadata: {
1903
+ status: response.status,
1904
+ cached: false,
1905
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1906
+ url,
1907
+ method
1908
+ }
1909
+ };
1910
+ }
1911
+ /**
1912
+ *
1913
+ * @param response
1914
+ * @private
1915
+ */
1916
+ mergeResponse(response) {
1917
+ const result = { ...response.data, _metadata: response.metadata };
1918
+ return result;
1919
+ }
1920
+ /**
1921
+ *
1922
+ * @param method
1923
+ * @param url
1924
+ * @param options
1925
+ * @param customKey
1926
+ * @private
1927
+ */
1928
+ generateKey(method, url, options, customKey) {
1929
+ if (customKey) return customKey;
1930
+ const keyParts = [
1931
+ method,
1932
+ url,
1933
+ JSON.stringify(options?.headers || {}),
1934
+ options?.body ? JSON.stringify(options.body) : ""
1935
+ ];
1936
+ return Buffer.from(keyParts.join("|")).toString("base64");
1937
+ }
1938
+ /**
1939
+ *
1940
+ * @param urlOrPattern
1941
+ */
1942
+ async invalidateCache(urlOrPattern) {
1943
+ try {
1944
+ console.log(`Invalidating cache for pattern: ${urlOrPattern}`);
1945
+ const encodedPattern = Buffer.from(urlOrPattern).toString("base64").slice(0, 20);
1946
+ const searchPattern = `*${encodedPattern}*`;
1947
+ const invalidatedCount = await this.cacheService.invalidate(searchPattern);
1948
+ console.log(`Invalidated ${invalidatedCount} entries for pattern: ${urlOrPattern}`);
1949
+ return invalidatedCount;
1950
+ } catch (error) {
1951
+ console.error(`Error invalidating cache for ${urlOrPattern}:`, error.message);
1952
+ try {
1953
+ if (urlOrPattern.startsWith(this.namespace + ":")) {
1954
+ await this.cacheService.delete(urlOrPattern);
1955
+ return 1;
1956
+ }
1957
+ } catch (fallbackError) {
1958
+ }
1959
+ return 0;
1960
+ }
1961
+ }
1962
+ /**
1963
+ *
1964
+ */
1965
+ async clearHttpCache() {
1966
+ try {
1967
+ console.log("Clearing HTTP cache...");
1968
+ if (typeof this.cacheService.clear === "function") {
1969
+ await this.cacheService.clear();
1970
+ } else if (typeof this.cacheService.keys === "function") {
1971
+ const allKeys = await this.cacheService.getKeys();
1972
+ const httpKeys = allKeys.filter((key) => key.startsWith(this.namespace + ":"));
1973
+ for (const key of httpKeys) {
1974
+ await this.cacheService.delete(key);
1975
+ }
1976
+ console.log(`Cleared ${httpKeys.length} HTTP cache entries`);
1977
+ }
1978
+ this.stats = { totalRequests: 0, cacheHits: 0, cacheMisses: 0 };
1979
+ console.log("HTTP cache cleared successfully");
1980
+ } catch (error) {
1981
+ console.error("Error clearing HTTP cache:", error.message);
1982
+ throw new Error(`Failed to clear HTTP cache: ${error.message}`);
1983
+ }
1984
+ }
1985
+ async getStats() {
1986
+ try {
1987
+ const cacheStats = await this.cacheService.getStatistics();
1988
+ const cachedKeys = await this.cacheService.getKeys();
1989
+ const httpKeys = cachedKeys.filter((key) => key.startsWith(this.namespace + ":"));
1990
+ const totalRequests = this.stats.totalRequests;
1991
+ const hitRate = totalRequests > 0 ? this.stats.cacheHits / totalRequests * 100 : 0;
1992
+ return {
1993
+ totalRequests,
1994
+ cacheHits: this.stats.cacheHits,
1995
+ cacheMisses: this.stats.cacheMisses,
1996
+ cacheSize: httpKeys.length,
1997
+ hitRate: parseFloat(hitRate.toFixed(2)),
1998
+ cachedUrls: httpKeys,
1999
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2000
+ enabled: this.isEnabled()
2001
+ };
2002
+ } catch (error) {
2003
+ console.error("Error getting cache stats:", error.message);
2004
+ return this.getDefaultStats();
2005
+ }
2006
+ }
2007
+ getDefaultStats() {
2008
+ return {
2009
+ totalRequests: this.stats.totalRequests,
2010
+ cacheHits: this.stats.cacheHits,
2011
+ cacheMisses: this.stats.cacheMisses,
2012
+ cacheSize: 0,
2013
+ hitRate: 0,
2014
+ cachedUrls: [],
2015
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2016
+ enabled: false
2017
+ };
2018
+ }
2019
+ isEnabled() {
2020
+ try {
2021
+ if (this.cacheService.isEnabled && typeof this.cacheService.isEnabled === "function") {
2022
+ return this.cacheService.isEnabled();
2023
+ }
2024
+ return true;
2025
+ } catch {
2026
+ return false;
2027
+ }
2028
+ }
2029
+ };
2030
+
2031
+ // src/application/services/fetch.client.service.ts
2032
+ var FetchHttpClient = class {
2033
+ async request(url, options = {}) {
2034
+ const { method = "GET", headers = {}, body, timeout = 3e4 } = options;
2035
+ const controller = new AbortController();
2036
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
2037
+ try {
2038
+ const requestBody = body ? typeof body === "string" ? body : JSON.stringify(body) : void 0;
2039
+ const response = await fetch(url, {
2040
+ method,
2041
+ headers,
2042
+ body: requestBody,
2043
+ signal: controller.signal
2044
+ });
2045
+ clearTimeout(timeoutId);
2046
+ const text = await response.text();
2047
+ console.log(`[FetchClient] ${method} ${url} -> status ${response.status}, body length ${text.length}`);
2048
+ const contentType = response.headers.get("content-type") || "";
2049
+ let data;
2050
+ if (contentType.includes("application/json")) {
2051
+ try {
2052
+ data = JSON.parse(text);
2053
+ } catch (e) {
2054
+ console.warn(`[FetchClient] Invalid JSON received, treating as text`);
2055
+ data = text;
2056
+ }
2057
+ } else {
2058
+ data = text;
2059
+ }
2060
+ return {
2061
+ status: response.status,
2062
+ statusText: response.statusText,
2063
+ headers: Object.fromEntries(response.headers.entries()),
2064
+ data
2065
+ };
2066
+ } catch (error) {
2067
+ clearTimeout(timeoutId);
2068
+ throw error;
2069
+ }
2070
+ }
2071
+ };
2072
+
2073
+ // src/application/services/httpCacheClient.service.ts
2074
+ var HttpCacheClient = class {
2075
+ cacheService;
2076
+ httpClient;
2077
+ stats = { totalRequests: 0, cacheHits: 0, cacheMisses: 0 };
2078
+ defaultOptions;
2079
+ constructor(httpClient, cacheService, options) {
2080
+ this.httpClient = httpClient || new FetchHttpClient();
2081
+ this.defaultOptions = {
2082
+ baseURL: options?.baseURL,
2083
+ headers: options?.headers,
2084
+ timeout: options?.timeout || 3e4
2085
+ };
2086
+ if (cacheService) {
2087
+ this.cacheService = cacheService;
2088
+ } else {
2089
+ const cacheConfig = options?.cacheConfig || {};
2090
+ const repository = CacheFactoryRepository.createCache({
2091
+ namespace: cacheConfig.namespace || "http-cache",
2092
+ maxSize: cacheConfig.maxSize || 1e4,
2093
+ defaultTTL: cacheConfig.defaultTTL || 3e5,
2094
+ storageType: cacheConfig.storageType || "disk",
2095
+ CACHE_DISK_DIR: cacheConfig.diskDir || "./storage/cache/http",
2096
+ enabled: true
2097
+ });
2098
+ this.cacheService = new CacheService(repository, "en");
2099
+ }
2100
+ }
2101
+ async getWithCache(url, options, cacheOptions) {
2102
+ this.stats.totalRequests++;
2103
+ const fullUrl = this.buildUrl(url);
2104
+ const cacheKey = this.generateCacheKey("GET", fullUrl, options, cacheOptions?.customKey);
2105
+ const ttl = cacheOptions?.timeToLive;
2106
+ try {
2107
+ const cached = await this.cacheService.get(cacheKey);
2108
+ if (cached) {
2109
+ this.stats.cacheHits++;
2110
+ return {
2111
+ ...cached.data,
2112
+ _metadata: {
2113
+ ...cached.metadata,
2114
+ cached: true,
2115
+ cacheHit: true,
2116
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2117
+ }
2118
+ };
2119
+ }
2120
+ this.stats.cacheMisses++;
2121
+ const response = await this.executeRequest("GET", fullUrl, options);
2122
+ await this.cacheService.set(cacheKey, response, ttl);
2123
+ return {
2124
+ ...response.data,
2125
+ _metadata: {
2126
+ ...response.metadata,
2127
+ cached: false,
2128
+ cacheHit: false,
2129
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2130
+ }
2131
+ };
2132
+ } catch (error) {
2133
+ this.stats.cacheMisses++;
2134
+ const response = await this.executeRequest("GET", fullUrl, options);
2135
+ return {
2136
+ ...response.data,
2137
+ _metadata: {
2138
+ ...response.metadata,
2139
+ cached: false,
2140
+ cacheHit: false,
2141
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2142
+ }
2143
+ };
2144
+ }
2145
+ }
2146
+ async postWithCache(url, data, options, cacheResponse = false, cacheOptions) {
2147
+ this.stats.totalRequests++;
2148
+ const fullUrl = this.buildUrl(url);
2149
+ const requestOptions = {
2150
+ method: "POST",
2151
+ headers: {
2152
+ "Content-Type": "application/json",
2153
+ ...this.defaultOptions.headers,
2154
+ ...options?.headers
2155
+ },
2156
+ body: data ? JSON.stringify(data) : void 0,
2157
+ ...options
2158
+ };
2159
+ if (!cacheResponse) {
2160
+ const response = await this.executeRequest("POST", fullUrl, requestOptions);
2161
+ return {
2162
+ ...response.data,
2163
+ _metadata: response.metadata
2164
+ };
2165
+ }
2166
+ const cacheKey = this.generateCacheKey("POST", fullUrl, requestOptions, cacheOptions?.customKey);
2167
+ const ttl = cacheOptions?.timeToLive;
2168
+ try {
2169
+ const cached = await this.cacheService.get(cacheKey);
2170
+ if (cached) {
2171
+ this.stats.cacheHits++;
2172
+ return {
2173
+ ...cached.data,
2174
+ _metadata: cached.metadata
2175
+ };
2176
+ }
2177
+ this.stats.cacheMisses++;
2178
+ const response = await this.executeRequest("POST", fullUrl, requestOptions);
2179
+ await this.cacheService.set(cacheKey, response, ttl);
2180
+ return {
2181
+ ...response.data,
2182
+ _metadata: response.metadata
2183
+ };
2184
+ } catch (error) {
2185
+ this.stats.cacheMisses++;
2186
+ const response = await this.executeRequest("POST", fullUrl, requestOptions);
2187
+ return {
2188
+ ...response.data,
2189
+ _metadata: response.metadata
2190
+ };
2191
+ }
2192
+ }
2193
+ async executeRequest(method, url, options) {
2194
+ const startTime = Date.now();
2195
+ const response = await this.httpClient.request(url, {
2196
+ method,
2197
+ headers: options?.headers,
2198
+ body: options?.body,
2199
+ timeout: this.defaultOptions.timeout
2200
+ });
2201
+ const responseTime = Date.now() - startTime;
2202
+ if (response.status < 200 || response.status >= 300) {
2203
+ const errorMessage = typeof response.data === "string" ? response.data : JSON.stringify(response.data);
2204
+ throw new Error(`HTTP ${response.status}: ${errorMessage}`);
2205
+ }
2206
+ let result = response.data;
2207
+ if (typeof result === "string") {
2208
+ try {
2209
+ result = JSON.parse(result);
2210
+ } catch {
2211
+ console.warn(`[HttpCacheClient] Response is not JSON: ${String(result).substring(0, 100)}`);
2212
+ }
2213
+ }
2214
+ return {
2215
+ status: response.status,
2216
+ statusText: response.statusText || "OK",
2217
+ headers: response.headers || {},
2218
+ data: result,
2219
+ metadata: {
2220
+ cached: true,
2221
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2222
+ url,
2223
+ method,
2224
+ responseTime
2225
+ }
2226
+ };
2227
+ }
2228
+ buildUrl(url) {
2229
+ if (url.startsWith("http://") || url.startsWith("https://")) return url;
2230
+ return this.defaultOptions.baseURL ? this.defaultOptions.baseURL + url : url;
2231
+ }
2232
+ generateCacheKey(method, url, options, customKey) {
2233
+ if (customKey) return `http:${customKey}`;
2234
+ const keyParts = [
2235
+ method,
2236
+ url,
2237
+ JSON.stringify(options?.headers || {}),
2238
+ options?.body ? JSON.stringify(options.body) : ""
2239
+ ];
2240
+ return `http:${this.hashString(keyParts.join("|"))}`;
2241
+ }
2242
+ hashString(str) {
2243
+ let hash = 0;
2244
+ for (let i = 0; i < str.length; i++) {
2245
+ const char = str.charCodeAt(i);
2246
+ hash = (hash << 5) - hash + char;
2247
+ hash = hash & hash;
2248
+ }
2249
+ return Math.abs(hash).toString(36);
2250
+ }
2251
+ async invalidateCache(urlOrPattern) {
2252
+ return this.cacheService.invalidate(`*${urlOrPattern}*`);
2253
+ }
2254
+ async clearHttpCache() {
2255
+ await this.cacheService.clear();
2256
+ this.stats = { totalRequests: 0, cacheHits: 0, cacheMisses: 0 };
2257
+ }
2258
+ async getStats() {
2259
+ const cacheStats = await this.cacheService.getStatistics();
2260
+ const keys = await this.cacheService.getKeys();
2261
+ const httpKeys = keys.filter((k) => k.startsWith("http:"));
2262
+ const hitRate = this.stats.totalRequests > 0 ? this.stats.cacheHits / this.stats.totalRequests * 100 : 0;
2263
+ return {
2264
+ totalRequests: this.stats.totalRequests,
2265
+ cacheHits: this.stats.cacheHits,
2266
+ cacheMisses: this.stats.cacheMisses,
2267
+ cacheSize: httpKeys.length,
2268
+ hitRate: parseFloat(hitRate.toFixed(2)),
2269
+ cachedUrls: httpKeys,
2270
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2271
+ enabled: this.cacheService.isEnabled()
2272
+ };
2273
+ }
2274
+ isEnabled() {
2275
+ return this.cacheService.isEnabled();
2276
+ }
2277
+ };
2278
+
2279
+ // src/application/services/node-http.client.service.ts
2280
+ import * as http from "http";
2281
+ import * as https from "https";
2282
+ import { URL } from "url";
2283
+ var NodeHttpClient = class {
2284
+ constructor(config) {
2285
+ this.config = config;
2286
+ }
2287
+ async request(url, options = {}) {
2288
+ const { method = "GET", headers = {}, body, timeout = 3e4 } = options;
2289
+ const startTime = Date.now();
2290
+ return new Promise((resolve, reject) => {
2291
+ const parsedUrl = new URL(url);
2292
+ const isHttps = parsedUrl.protocol === "https:";
2293
+ const client = isHttps ? https : http;
2294
+ const requestOptions = {
2295
+ hostname: parsedUrl.hostname,
2296
+ port: parsedUrl.port || (isHttps ? 443 : 80),
2297
+ path: parsedUrl.pathname + parsedUrl.search,
2298
+ method,
2299
+ headers,
2300
+ timeout
2301
+ };
2302
+ const req = client.request(requestOptions, (res) => {
2303
+ let rawData = "";
2304
+ res.on("data", (chunk) => {
2305
+ rawData += chunk;
2306
+ });
2307
+ res.on("end", () => {
2308
+ const responseTime = Date.now() - startTime;
2309
+ const contentType = res.headers["content-type"] || "";
2310
+ let data;
2311
+ try {
2312
+ if (contentType.includes("application/json")) {
2313
+ data = JSON.parse(rawData);
2314
+ } else {
2315
+ data = rawData;
2316
+ }
2317
+ } catch {
2318
+ data = rawData;
2319
+ }
2320
+ resolve({
2321
+ status: res.statusCode || 200,
2322
+ statusText: res.statusMessage || "OK",
2323
+ headers: res.headers,
2324
+ data,
2325
+ metadata: {
2326
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2327
+ url,
2328
+ method,
2329
+ responseTime,
2330
+ cached: false
2331
+ }
2332
+ });
2333
+ });
2334
+ });
2335
+ req.on("error", (err) => {
2336
+ reject(err);
2337
+ });
2338
+ req.on("timeout", () => {
2339
+ req.destroy();
2340
+ reject(new Error(`Request timeout after ${timeout}ms`));
2341
+ });
2342
+ if (body) {
2343
+ const bodyString = typeof body === "string" ? body : JSON.stringify(body);
2344
+ req.write(bodyString);
2345
+ }
2346
+ req.end();
2347
+ });
2348
+ }
2349
+ };
2350
+
2351
+ // src/application/services/axios.client.service.ts
2352
+ var axios;
2353
+ var AxiosHttpClient = class {
2354
+ axiosInstance;
2355
+ constructor(config) {
2356
+ try {
2357
+ axios = __require("axios");
2358
+ } catch (e) {
2359
+ throw new Error("Axios is not installed. Please run: npm install axios");
2360
+ }
2361
+ this.axiosInstance = axios.create(config);
2362
+ }
2363
+ async request(url, options = {}) {
2364
+ const { method = "GET", headers = {}, body, timeout = 3e4 } = options;
2365
+ const startTime = Date.now();
2366
+ try {
2367
+ const response = await this.axiosInstance({
2368
+ method: method.toLowerCase(),
2369
+ url,
2370
+ headers,
2371
+ data: body,
2372
+ timeout,
2373
+ validateStatus: () => true
2374
+ });
2375
+ const responseTime = Date.now() - startTime;
2376
+ return {
2377
+ status: response.status,
2378
+ statusText: response.statusText,
2379
+ headers: response.headers,
2380
+ data: response.data,
2381
+ metadata: {
2382
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2383
+ url,
2384
+ method,
2385
+ responseTime,
2386
+ cached: false
2387
+ }
2388
+ };
2389
+ } catch (error) {
2390
+ if (error.response) {
2391
+ const responseTime = Date.now() - startTime;
2392
+ return {
2393
+ status: error.response.status,
2394
+ statusText: error.response.statusText,
2395
+ headers: error.response.headers,
2396
+ data: error.response.data,
2397
+ metadata: {
2398
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2399
+ url,
2400
+ method,
2401
+ responseTime,
2402
+ cached: false
2403
+ }
2404
+ };
2405
+ }
2406
+ throw error;
2407
+ }
2408
+ }
2409
+ };
2410
+
2411
+ // src/application/services/curl.client.service.ts
2412
+ import { exec } from "child_process";
2413
+ import { promisify as promisify2 } from "util";
2414
+ var execAsync = promisify2(exec);
2415
+ var CurlHttpClient = class {
2416
+ constructor(config) {
2417
+ this.config = config;
2418
+ }
2419
+ async request(url, options = {}) {
2420
+ const { method = "GET", headers = {}, timeout = 3e4, body } = options;
2421
+ const startTime = Date.now();
2422
+ const curlCmd = this.buildCommand(url, method, headers, timeout, body);
2423
+ try {
2424
+ const { stdout, stderr } = await execAsync(curlCmd, { timeout });
2425
+ if (stderr) {
2426
+ console.warn(`[CurlClient] stderr: ${stderr}`);
2427
+ }
2428
+ const responseTime = Date.now() - startTime;
2429
+ let data;
2430
+ try {
2431
+ data = JSON.parse(stdout);
2432
+ } catch {
2433
+ data = stdout;
2434
+ }
2435
+ return {
2436
+ status: 200,
2437
+ // approximation
2438
+ statusText: "OK",
2439
+ headers: {},
2440
+ data,
2441
+ metadata: {
2442
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2443
+ url,
2444
+ method,
2445
+ responseTime,
2446
+ cached: false
2447
+ }
2448
+ };
2449
+ } catch (error) {
2450
+ throw new Error(`Curl execution failed: ${error.message}`);
2451
+ }
2452
+ }
2453
+ buildCommand(url, method, headers, timeout, body) {
2454
+ const parts = [];
2455
+ if (this.config?.curlPath) {
2456
+ parts.push(this.config.curlPath);
2457
+ } else {
2458
+ parts.push("curl");
2459
+ }
2460
+ parts.push("-s", "-S");
2461
+ parts.push("--max-time", Math.ceil(timeout / 1e3).toString());
2462
+ if (method !== "GET") {
2463
+ parts.push("-X", method);
2464
+ }
2465
+ for (const [key, value] of Object.entries(headers)) {
2466
+ parts.push("-H", `"${key}: ${value}"`);
2467
+ }
2468
+ if (body) {
2469
+ const bodyString = typeof body === "string" ? body : JSON.stringify(body);
2470
+ parts.push("--data-binary", `"${bodyString}"`);
2471
+ }
2472
+ parts.push(url);
2473
+ if (this.config?.additionalArgs) {
2474
+ parts.push(...this.config.additionalArgs);
2475
+ }
2476
+ return parts.join(" ");
2477
+ }
2478
+ };
2479
+
2480
+ // src/application/services/httpCacheFactory.service.ts
2481
+ var HttpCacheFactory = class {
2482
+ static instances = /* @__PURE__ */ new Map();
2483
+ static create(appName = "default", config) {
2484
+ const instanceKey = `${appName}:${config?.clientType || "fetch"}`;
2485
+ if (!this.instances.has(instanceKey)) {
2486
+ let httpClient;
2487
+ switch (config?.clientType) {
2488
+ case "node-http":
2489
+ httpClient = new NodeHttpClient(config?.clientOptions);
2490
+ break;
2491
+ case "axios":
2492
+ httpClient = new AxiosHttpClient(config?.clientOptions);
2493
+ break;
2494
+ case "curl":
2495
+ httpClient = new CurlHttpClient(config?.clientOptions);
2496
+ break;
2497
+ case "fetch":
2498
+ default:
2499
+ httpClient = new FetchHttpClient();
2500
+ break;
2501
+ }
2502
+ const cacheConfig = {
2503
+ namespace: config?.namespace || `http-cache-${appName}`,
2504
+ maxSize: config?.maxSize || 1e4,
2505
+ defaultTTL: config?.defaultTTL || 3e5,
2506
+ storageType: config?.storageType || "disk",
2507
+ diskDir: config?.diskDir || `./storage/cache/${appName}`
2508
+ };
2509
+ const instance = new HttpCacheClient(httpClient, void 0, {
2510
+ cacheConfig,
2511
+ timeout: config?.clientOptions?.timeout,
2512
+ headers: config?.clientOptions?.headers,
2513
+ baseURL: config?.clientOptions?.baseURL
2514
+ });
2515
+ this.instances.set(instanceKey, instance);
2516
+ }
2517
+ return this.instances.get(instanceKey);
2518
+ }
2519
+ static createFromExistingCache(cacheService, namespace = "http-cache") {
2520
+ return new AdaptedHttpCacheService(cacheService, namespace);
2521
+ }
2522
+ static destroy(appName = "default", clientType) {
2523
+ const key = `${appName}:${clientType || "fetch"}`;
2524
+ this.instances.delete(key);
2525
+ }
2526
+ };
2527
+
2528
+ // src/index.ts
2529
+ dotenv.config();
2530
+ var index_default = {
2531
+ SCacheMiddleware,
2532
+ CacheService,
2533
+ CacheMiddleware,
2534
+ CacheFactoryRepository,
2535
+ RouteValidator,
2536
+ EnvironmentLoader,
2537
+ HttpCacheFactory
2538
+ };
2539
+ export {
2540
+ index_default as default
2541
+ };