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/README.md +314 -0
- package/dist/index.cjs +2565 -0
- package/dist/index.d.cts +597 -0
- package/dist/index.d.ts +597 -0
- package/dist/index.js +2541 -0
- package/dist/utils/translations/message.translation.en.json +3 -0
- package/dist/utils/translations/message.translation.fr.json +174 -0
- package/package.json +53 -0
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
|
+
};
|