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