wu-framework 1.1.14 → 1.1.16
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/LICENSE +39 -39
- package/README.md +408 -408
- package/dist/wu-framework.cjs.js.map +1 -1
- package/dist/wu-framework.dev.js +15151 -15151
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js.map +1 -1
- package/integrations/astro/README.md +127 -127
- package/integrations/astro/WuApp.astro +63 -63
- package/integrations/astro/WuShell.astro +39 -39
- package/integrations/astro/index.js +68 -68
- package/integrations/astro/package.json +38 -38
- package/integrations/astro/types.d.ts +53 -53
- package/package.json +161 -161
- package/src/adapters/angular/ai.js +30 -30
- package/src/adapters/angular/index.d.ts +154 -154
- package/src/adapters/angular/index.js +932 -932
- package/src/adapters/angular.d.ts +3 -3
- package/src/adapters/angular.js +3 -3
- package/src/adapters/index.js +168 -168
- package/src/adapters/lit/ai.js +20 -20
- package/src/adapters/lit/index.d.ts +120 -120
- package/src/adapters/lit/index.js +721 -721
- package/src/adapters/lit.d.ts +3 -3
- package/src/adapters/lit.js +3 -3
- package/src/adapters/preact/ai.js +33 -33
- package/src/adapters/preact/index.d.ts +108 -108
- package/src/adapters/preact/index.js +661 -661
- package/src/adapters/preact.d.ts +3 -3
- package/src/adapters/preact.js +3 -3
- package/src/adapters/react/index.js +48 -54
- package/src/adapters/react.d.ts +3 -3
- package/src/adapters/react.js +3 -3
- package/src/adapters/shared.js +64 -64
- package/src/adapters/solid/ai.js +32 -32
- package/src/adapters/solid/index.d.ts +101 -101
- package/src/adapters/solid/index.js +586 -586
- package/src/adapters/solid.d.ts +3 -3
- package/src/adapters/solid.js +3 -3
- package/src/adapters/svelte/ai.js +31 -31
- package/src/adapters/svelte/index.d.ts +166 -166
- package/src/adapters/svelte/index.js +798 -798
- package/src/adapters/svelte.d.ts +3 -3
- package/src/adapters/svelte.js +3 -3
- package/src/adapters/vanilla/ai.js +30 -30
- package/src/adapters/vanilla/index.d.ts +179 -179
- package/src/adapters/vanilla/index.js +785 -785
- package/src/adapters/vanilla.d.ts +3 -3
- package/src/adapters/vanilla.js +3 -3
- package/src/adapters/vue/ai.js +52 -52
- package/src/adapters/vue/index.d.ts +299 -299
- package/src/adapters/vue/index.js +610 -610
- package/src/adapters/vue.d.ts +3 -3
- package/src/adapters/vue.js +3 -3
- package/src/ai/wu-ai-actions.js +261 -261
- package/src/ai/wu-ai-agent.js +546 -546
- package/src/ai/wu-ai-browser-primitives.js +354 -354
- package/src/ai/wu-ai-browser.js +380 -380
- package/src/ai/wu-ai-context.js +332 -332
- package/src/ai/wu-ai-conversation.js +613 -613
- package/src/ai/wu-ai-orchestrate.js +1021 -1021
- package/src/ai/wu-ai-permissions.js +381 -381
- package/src/ai/wu-ai-provider.js +700 -700
- package/src/ai/wu-ai-schema.js +225 -225
- package/src/ai/wu-ai-triggers.js +396 -396
- package/src/ai/wu-ai.js +804 -804
- package/src/core/wu-app.js +236 -236
- package/src/core/wu-cache.js +477 -477
- package/src/core/wu-core.js +1398 -1398
- package/src/core/wu-error-boundary.js +382 -382
- package/src/core/wu-event-bus.js +348 -348
- package/src/core/wu-hooks.js +350 -350
- package/src/core/wu-html-parser.js +190 -190
- package/src/core/wu-iframe-sandbox.js +328 -328
- package/src/core/wu-loader.js +272 -272
- package/src/core/wu-logger.js +134 -134
- package/src/core/wu-manifest.js +509 -509
- package/src/core/wu-mcp-bridge.js +432 -432
- package/src/core/wu-overrides.js +510 -510
- package/src/core/wu-performance.js +228 -228
- package/src/core/wu-plugin.js +348 -348
- package/src/core/wu-prefetch.js +414 -414
- package/src/core/wu-proxy-sandbox.js +476 -476
- package/src/core/wu-sandbox.js +779 -779
- package/src/core/wu-script-executor.js +113 -113
- package/src/core/wu-snapshot-sandbox.js +227 -227
- package/src/core/wu-strategies.js +256 -256
- package/src/core/wu-style-bridge.js +477 -477
- package/src/index.js +224 -224
- package/src/utils/dependency-resolver.js +327 -327
package/src/core/wu-cache.js
CHANGED
|
@@ -1,477 +1,477 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 💾 WU-CACHE: SECURE INTERNAL CACHING
|
|
3
|
-
*
|
|
4
|
-
* Sistema de caché INTERNO con rate limiting
|
|
5
|
-
* - Rate limiting para prevenir abuso
|
|
6
|
-
* - Cache persistente y en memoria
|
|
7
|
-
* - TTL y LRU eviction
|
|
8
|
-
*
|
|
9
|
-
* ⚠️ USO INTERNO: No exponer en API pública
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { logger } from './wu-logger.js';
|
|
13
|
-
|
|
14
|
-
export class WuCache {
|
|
15
|
-
constructor(options = {}) {
|
|
16
|
-
this.config = {
|
|
17
|
-
maxSize: options.maxSize || 50, // MB
|
|
18
|
-
maxItems: options.maxItems || 100,
|
|
19
|
-
defaultTTL: options.defaultTTL || 3600000, // 1 hour
|
|
20
|
-
persistent: options.persistent !== false,
|
|
21
|
-
storage: options.storage || 'memory'
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// 🔐 Rate limiting configuration
|
|
25
|
-
this.rateLimiting = {
|
|
26
|
-
enabled: options.rateLimiting !== false,
|
|
27
|
-
maxOpsPerSecond: options.maxOpsPerSecond || 100,
|
|
28
|
-
windowMs: 1000, // 1 second window
|
|
29
|
-
cooldownMs: options.cooldownMs || 5000, // 5 second cooldown after limit
|
|
30
|
-
operations: [],
|
|
31
|
-
inCooldown: false,
|
|
32
|
-
cooldownUntil: 0
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// Memory cache
|
|
36
|
-
this.memoryCache = new Map();
|
|
37
|
-
|
|
38
|
-
// LRU tracking
|
|
39
|
-
this.accessOrder = new Map();
|
|
40
|
-
|
|
41
|
-
// Statistics
|
|
42
|
-
this.stats = {
|
|
43
|
-
hits: 0,
|
|
44
|
-
misses: 0,
|
|
45
|
-
sets: 0,
|
|
46
|
-
evictions: 0,
|
|
47
|
-
size: 0,
|
|
48
|
-
rateLimited: 0 // 🔐 Contador de operaciones rechazadas
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 🔐 CHECK RATE LIMIT: Verificar si la operación está permitida
|
|
54
|
-
* @returns {boolean} true si la operación está permitida
|
|
55
|
-
*/
|
|
56
|
-
_checkRateLimit() {
|
|
57
|
-
if (!this.rateLimiting.enabled) return true;
|
|
58
|
-
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
|
|
61
|
-
// Verificar si estamos en cooldown
|
|
62
|
-
if (this.rateLimiting.inCooldown) {
|
|
63
|
-
if (now < this.rateLimiting.cooldownUntil) {
|
|
64
|
-
this.stats.rateLimited++;
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
// Cooldown terminado
|
|
68
|
-
this.rateLimiting.inCooldown = false;
|
|
69
|
-
this.rateLimiting.operations = [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Limpiar operaciones antiguas (fuera de la ventana)
|
|
73
|
-
const windowStart = now - this.rateLimiting.windowMs;
|
|
74
|
-
this.rateLimiting.operations = this.rateLimiting.operations.filter(
|
|
75
|
-
ts => ts > windowStart
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// Verificar límite
|
|
79
|
-
if (this.rateLimiting.operations.length >= this.rateLimiting.maxOpsPerSecond) {
|
|
80
|
-
// Activar cooldown
|
|
81
|
-
this.rateLimiting.inCooldown = true;
|
|
82
|
-
this.rateLimiting.cooldownUntil = now + this.rateLimiting.cooldownMs;
|
|
83
|
-
this.stats.rateLimited++;
|
|
84
|
-
logger.warn(`[WuCache] 🚫 Rate limit exceeded. Cooldown for ${this.rateLimiting.cooldownMs}ms`);
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Registrar operación
|
|
89
|
-
this.rateLimiting.operations.push(now);
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 🔐 GET RATE LIMIT STATUS
|
|
95
|
-
*/
|
|
96
|
-
getRateLimitStatus() {
|
|
97
|
-
const now = Date.now();
|
|
98
|
-
return {
|
|
99
|
-
enabled: this.rateLimiting.enabled,
|
|
100
|
-
inCooldown: this.rateLimiting.inCooldown,
|
|
101
|
-
cooldownRemaining: this.rateLimiting.inCooldown
|
|
102
|
-
? Math.max(0, this.rateLimiting.cooldownUntil - now)
|
|
103
|
-
: 0,
|
|
104
|
-
currentOps: this.rateLimiting.operations.length,
|
|
105
|
-
maxOps: this.rateLimiting.maxOpsPerSecond,
|
|
106
|
-
rateLimited: this.stats.rateLimited
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 🔍 GET: Obtener valor del cache
|
|
112
|
-
* @param {string} key - Clave
|
|
113
|
-
* @returns {*} Valor cacheado o null
|
|
114
|
-
*/
|
|
115
|
-
get(key) {
|
|
116
|
-
// 🔐 Check rate limit
|
|
117
|
-
if (!this._checkRateLimit()) {
|
|
118
|
-
return null; // Silently fail on rate limit
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 1. Buscar en memoria
|
|
122
|
-
if (this.memoryCache.has(key)) {
|
|
123
|
-
const entry = this.memoryCache.get(key);
|
|
124
|
-
|
|
125
|
-
// Verificar TTL
|
|
126
|
-
if (this.isExpired(entry)) {
|
|
127
|
-
this.delete(key);
|
|
128
|
-
this.stats.misses++;
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Actualizar acceso (LRU)
|
|
133
|
-
this.accessOrder.set(key, Date.now());
|
|
134
|
-
this.stats.hits++;
|
|
135
|
-
|
|
136
|
-
return entry.value;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 2. Buscar en storage persistente
|
|
140
|
-
if (this.config.persistent) {
|
|
141
|
-
const stored = this.getFromStorage(key);
|
|
142
|
-
if (stored) {
|
|
143
|
-
// Restaurar a memoria
|
|
144
|
-
this.memoryCache.set(key, stored);
|
|
145
|
-
this.accessOrder.set(key, Date.now());
|
|
146
|
-
this.stats.hits++;
|
|
147
|
-
return stored.value;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
this.stats.misses++;
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 💾 SET: Guardar valor en cache
|
|
157
|
-
* @param {string} key - Clave
|
|
158
|
-
* @param {*} value - Valor
|
|
159
|
-
* @param {number} ttl - Time to live (ms)
|
|
160
|
-
* @returns {boolean}
|
|
161
|
-
*/
|
|
162
|
-
set(key, value, ttl) {
|
|
163
|
-
// 🔐 Check rate limit
|
|
164
|
-
if (!this._checkRateLimit()) {
|
|
165
|
-
return false; // Reject on rate limit
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const entry = {
|
|
170
|
-
key,
|
|
171
|
-
value,
|
|
172
|
-
timestamp: Date.now(),
|
|
173
|
-
ttl: ttl || this.config.defaultTTL,
|
|
174
|
-
size: this.estimateSize(value)
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// Verificar si necesitamos hacer espacio
|
|
178
|
-
const hasSpace = this.ensureSpace(entry.size);
|
|
179
|
-
if (hasSpace === false) {
|
|
180
|
-
logger.warn(`[WuCache] ⚠️ Cannot cache item: ${key} (too large)`);
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Guardar en memoria
|
|
185
|
-
this.memoryCache.set(key, entry);
|
|
186
|
-
this.accessOrder.set(key, Date.now());
|
|
187
|
-
|
|
188
|
-
// Guardar en storage persistente
|
|
189
|
-
if (this.config.persistent) {
|
|
190
|
-
this.saveToStorage(key, entry);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
this.stats.sets++;
|
|
194
|
-
this.stats.size += entry.size;
|
|
195
|
-
|
|
196
|
-
return true;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
logger.warn('[WuCache] ⚠️ Failed to set cache:', error);
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* 🗑️ DELETE: Eliminar del cache
|
|
205
|
-
* @param {string} key - Clave
|
|
206
|
-
*/
|
|
207
|
-
delete(key) {
|
|
208
|
-
const entry = this.memoryCache.get(key);
|
|
209
|
-
if (entry) {
|
|
210
|
-
this.stats.size -= entry.size;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
this.memoryCache.delete(key);
|
|
214
|
-
this.accessOrder.delete(key);
|
|
215
|
-
|
|
216
|
-
if (this.config.persistent) {
|
|
217
|
-
this.deleteFromStorage(key);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 🧹 CLEAR: Limpiar todo el cache
|
|
223
|
-
*/
|
|
224
|
-
clear() {
|
|
225
|
-
this.memoryCache.clear();
|
|
226
|
-
this.accessOrder.clear();
|
|
227
|
-
this.stats.size = 0;
|
|
228
|
-
|
|
229
|
-
if (this.config.persistent) {
|
|
230
|
-
this.clearStorage();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
logger.debug('[WuCache] 🧹 Cache cleared');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* ⏰ IS EXPIRED: Verificar si entrada expiró
|
|
238
|
-
* @param {Object} entry - Entrada del cache
|
|
239
|
-
* @returns {boolean}
|
|
240
|
-
*/
|
|
241
|
-
isExpired(entry) {
|
|
242
|
-
if (!entry.ttl) return false;
|
|
243
|
-
return Date.now() - entry.timestamp > entry.ttl;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* 📏 ESTIMATE SIZE: Estimar tamaño de un valor
|
|
248
|
-
* @param {*} value - Valor
|
|
249
|
-
* @returns {number} Tamaño en bytes
|
|
250
|
-
*/
|
|
251
|
-
estimateSize(value) {
|
|
252
|
-
if (typeof value === 'string') {
|
|
253
|
-
return value.length * 2; // UTF-16
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (typeof value === 'object') {
|
|
257
|
-
try {
|
|
258
|
-
return JSON.stringify(value).length * 2;
|
|
259
|
-
} catch {
|
|
260
|
-
return 1000; // Estimación por defecto
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return 100; // Tamaño por defecto para primitivos
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* 🎯 ENSURE SPACE: Asegurar espacio en cache (LRU eviction)
|
|
269
|
-
* @param {number} neededSize - Tamaño necesario
|
|
270
|
-
*/
|
|
271
|
-
ensureSpace(neededSize) {
|
|
272
|
-
const maxSizeBytes = this.config.maxSize * 1024 * 1024;
|
|
273
|
-
|
|
274
|
-
// 🛡️ FIX: Validar que el item no sea más grande que el máximo permitido
|
|
275
|
-
if (neededSize > maxSizeBytes) {
|
|
276
|
-
logger.warn(`[WuCache] ⚠️ Item size (${neededSize}) exceeds max cache size (${maxSizeBytes}). Skipping.`);
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// 🛡️ FIX: Límite de iteraciones para evitar loop infinito
|
|
281
|
-
const maxIterations = this.config.maxItems + 10;
|
|
282
|
-
let iterations = 0;
|
|
283
|
-
|
|
284
|
-
// Verificar si necesitamos limpiar
|
|
285
|
-
while ((this.stats.size + neededSize > maxSizeBytes ||
|
|
286
|
-
this.memoryCache.size >= this.config.maxItems) &&
|
|
287
|
-
iterations < maxIterations) {
|
|
288
|
-
|
|
289
|
-
iterations++;
|
|
290
|
-
|
|
291
|
-
// 🛡️ FIX: Si el cache está vacío pero aún no hay espacio, salir
|
|
292
|
-
if (this.memoryCache.size === 0) {
|
|
293
|
-
logger.warn('[WuCache] ⚠️ Cache empty but still no space. Breaking loop.');
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Encontrar entrada menos recientemente usada (LRU)
|
|
298
|
-
let oldestKey = null;
|
|
299
|
-
let oldestTime = Infinity;
|
|
300
|
-
|
|
301
|
-
for (const [key, time] of this.accessOrder) {
|
|
302
|
-
if (time < oldestTime) {
|
|
303
|
-
oldestTime = time;
|
|
304
|
-
oldestKey = key;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (oldestKey) {
|
|
309
|
-
logger.debug(`[WuCache] 🗑️ Evicting LRU entry: ${oldestKey}`);
|
|
310
|
-
this.delete(oldestKey);
|
|
311
|
-
this.stats.evictions++;
|
|
312
|
-
} else {
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// 🛡️ FIX: Log si alcanzamos el límite de iteraciones
|
|
318
|
-
if (iterations >= maxIterations) {
|
|
319
|
-
console.error(`[WuCache] 🚨 Max eviction iterations reached (${maxIterations}). Possible infinite loop prevented.`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* 💽 GET FROM STORAGE: Obtener del storage persistente
|
|
327
|
-
* @param {string} key - Clave
|
|
328
|
-
* @returns {Object|null}
|
|
329
|
-
*/
|
|
330
|
-
getFromStorage(key) {
|
|
331
|
-
try {
|
|
332
|
-
const storage = this.getStorage();
|
|
333
|
-
const stored = storage.getItem(`wu_cache_${key}`);
|
|
334
|
-
|
|
335
|
-
if (stored) {
|
|
336
|
-
return JSON.parse(stored);
|
|
337
|
-
}
|
|
338
|
-
} catch (error) {
|
|
339
|
-
logger.warn('[WuCache] ⚠️ Failed to get from storage:', error);
|
|
340
|
-
}
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* 💾 SAVE TO STORAGE: Guardar en storage persistente
|
|
346
|
-
* @param {string} key - Clave
|
|
347
|
-
* @param {Object} entry - Entrada
|
|
348
|
-
*/
|
|
349
|
-
saveToStorage(key, entry) {
|
|
350
|
-
const storage = this.getStorage();
|
|
351
|
-
try {
|
|
352
|
-
storage.setItem(`wu_cache_${key}`, JSON.stringify(entry));
|
|
353
|
-
} catch (error) {
|
|
354
|
-
// Storage lleno, limpiar entradas antiguas
|
|
355
|
-
logger.warn('[WuCache] ⚠️ Storage full, cleaning old entries');
|
|
356
|
-
this.cleanOldStorageEntries();
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
storage.setItem(`wu_cache_${key}`, JSON.stringify(entry));
|
|
360
|
-
} catch {
|
|
361
|
-
logger.warn('[WuCache] ⚠️ Failed to save to storage after cleanup');
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* 🗑️ DELETE FROM STORAGE: Eliminar del storage
|
|
368
|
-
* @param {string} key - Clave
|
|
369
|
-
*/
|
|
370
|
-
deleteFromStorage(key) {
|
|
371
|
-
try {
|
|
372
|
-
const storage = this.getStorage();
|
|
373
|
-
storage.removeItem(`wu_cache_${key}`);
|
|
374
|
-
} catch (error) {
|
|
375
|
-
logger.warn('[WuCache] ⚠️ Failed to delete from storage:', error);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* 🧹 CLEAR STORAGE: Limpiar storage
|
|
381
|
-
*/
|
|
382
|
-
clearStorage() {
|
|
383
|
-
try {
|
|
384
|
-
const storage = this.getStorage();
|
|
385
|
-
const keys = Object.keys(storage);
|
|
386
|
-
|
|
387
|
-
keys.forEach(key => {
|
|
388
|
-
if (key.startsWith('wu_cache_')) {
|
|
389
|
-
storage.removeItem(key);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
} catch (error) {
|
|
393
|
-
logger.warn('[WuCache] ⚠️ Failed to clear storage:', error);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* 🧹 CLEAN OLD STORAGE ENTRIES: Limpiar entradas antiguas del storage
|
|
399
|
-
*/
|
|
400
|
-
cleanOldStorageEntries() {
|
|
401
|
-
try {
|
|
402
|
-
const storage = this.getStorage();
|
|
403
|
-
const keys = Object.keys(storage);
|
|
404
|
-
const entries = [];
|
|
405
|
-
|
|
406
|
-
// Recopilar todas las entradas con timestamp
|
|
407
|
-
keys.forEach(key => {
|
|
408
|
-
if (key.startsWith('wu_cache_')) {
|
|
409
|
-
try {
|
|
410
|
-
const entry = JSON.parse(storage.getItem(key));
|
|
411
|
-
entries.push({ key, timestamp: entry.timestamp });
|
|
412
|
-
} catch {}
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// Ordenar por timestamp (más antiguas primero)
|
|
417
|
-
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
418
|
-
|
|
419
|
-
// Eliminar 25% de entradas más antiguas
|
|
420
|
-
const toRemove = Math.ceil(entries.length * 0.25);
|
|
421
|
-
for (let i = 0; i < toRemove; i++) {
|
|
422
|
-
storage.removeItem(entries[i].key);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
logger.debug(`[WuCache] 🧹 Cleaned ${toRemove} old storage entries`);
|
|
426
|
-
} catch (error) {
|
|
427
|
-
logger.warn('[WuCache] ⚠️ Failed to clean old storage entries:', error);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* 💽 GET STORAGE: Obtener instancia de storage
|
|
433
|
-
* @returns {Storage}
|
|
434
|
-
*/
|
|
435
|
-
getStorage() {
|
|
436
|
-
if (this.config.storage === 'localStorage') {
|
|
437
|
-
return window.localStorage;
|
|
438
|
-
} else if (this.config.storage === 'sessionStorage') {
|
|
439
|
-
return window.sessionStorage;
|
|
440
|
-
}
|
|
441
|
-
// Fallback a memoria
|
|
442
|
-
return {
|
|
443
|
-
getItem: () => null,
|
|
444
|
-
setItem: () => {},
|
|
445
|
-
removeItem: () => {},
|
|
446
|
-
clear: () => {}
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* 📊 GET STATS: Obtener estadísticas del cache
|
|
452
|
-
* @returns {Object}
|
|
453
|
-
*/
|
|
454
|
-
getStats() {
|
|
455
|
-
const hitRate = this.stats.hits + this.stats.misses > 0
|
|
456
|
-
? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2)
|
|
457
|
-
: 0;
|
|
458
|
-
|
|
459
|
-
return {
|
|
460
|
-
...this.stats,
|
|
461
|
-
hitRate: `${hitRate}%`,
|
|
462
|
-
items: this.memoryCache.size,
|
|
463
|
-
sizeMB: (this.stats.size / 1024 / 1024).toFixed(2)
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* ⚙️ CONFIGURE: Actualizar configuración
|
|
469
|
-
* @param {Object} config - Nueva configuración
|
|
470
|
-
*/
|
|
471
|
-
configure(config) {
|
|
472
|
-
this.config = {
|
|
473
|
-
...this.config,
|
|
474
|
-
...config
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 💾 WU-CACHE: SECURE INTERNAL CACHING
|
|
3
|
+
*
|
|
4
|
+
* Sistema de caché INTERNO con rate limiting
|
|
5
|
+
* - Rate limiting para prevenir abuso
|
|
6
|
+
* - Cache persistente y en memoria
|
|
7
|
+
* - TTL y LRU eviction
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ USO INTERNO: No exponer en API pública
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from './wu-logger.js';
|
|
13
|
+
|
|
14
|
+
export class WuCache {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.config = {
|
|
17
|
+
maxSize: options.maxSize || 50, // MB
|
|
18
|
+
maxItems: options.maxItems || 100,
|
|
19
|
+
defaultTTL: options.defaultTTL || 3600000, // 1 hour
|
|
20
|
+
persistent: options.persistent !== false,
|
|
21
|
+
storage: options.storage || 'memory'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// 🔐 Rate limiting configuration
|
|
25
|
+
this.rateLimiting = {
|
|
26
|
+
enabled: options.rateLimiting !== false,
|
|
27
|
+
maxOpsPerSecond: options.maxOpsPerSecond || 100,
|
|
28
|
+
windowMs: 1000, // 1 second window
|
|
29
|
+
cooldownMs: options.cooldownMs || 5000, // 5 second cooldown after limit
|
|
30
|
+
operations: [],
|
|
31
|
+
inCooldown: false,
|
|
32
|
+
cooldownUntil: 0
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Memory cache
|
|
36
|
+
this.memoryCache = new Map();
|
|
37
|
+
|
|
38
|
+
// LRU tracking
|
|
39
|
+
this.accessOrder = new Map();
|
|
40
|
+
|
|
41
|
+
// Statistics
|
|
42
|
+
this.stats = {
|
|
43
|
+
hits: 0,
|
|
44
|
+
misses: 0,
|
|
45
|
+
sets: 0,
|
|
46
|
+
evictions: 0,
|
|
47
|
+
size: 0,
|
|
48
|
+
rateLimited: 0 // 🔐 Contador de operaciones rechazadas
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 🔐 CHECK RATE LIMIT: Verificar si la operación está permitida
|
|
54
|
+
* @returns {boolean} true si la operación está permitida
|
|
55
|
+
*/
|
|
56
|
+
_checkRateLimit() {
|
|
57
|
+
if (!this.rateLimiting.enabled) return true;
|
|
58
|
+
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
|
|
61
|
+
// Verificar si estamos en cooldown
|
|
62
|
+
if (this.rateLimiting.inCooldown) {
|
|
63
|
+
if (now < this.rateLimiting.cooldownUntil) {
|
|
64
|
+
this.stats.rateLimited++;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
// Cooldown terminado
|
|
68
|
+
this.rateLimiting.inCooldown = false;
|
|
69
|
+
this.rateLimiting.operations = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Limpiar operaciones antiguas (fuera de la ventana)
|
|
73
|
+
const windowStart = now - this.rateLimiting.windowMs;
|
|
74
|
+
this.rateLimiting.operations = this.rateLimiting.operations.filter(
|
|
75
|
+
ts => ts > windowStart
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Verificar límite
|
|
79
|
+
if (this.rateLimiting.operations.length >= this.rateLimiting.maxOpsPerSecond) {
|
|
80
|
+
// Activar cooldown
|
|
81
|
+
this.rateLimiting.inCooldown = true;
|
|
82
|
+
this.rateLimiting.cooldownUntil = now + this.rateLimiting.cooldownMs;
|
|
83
|
+
this.stats.rateLimited++;
|
|
84
|
+
logger.warn(`[WuCache] 🚫 Rate limit exceeded. Cooldown for ${this.rateLimiting.cooldownMs}ms`);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Registrar operación
|
|
89
|
+
this.rateLimiting.operations.push(now);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 🔐 GET RATE LIMIT STATUS
|
|
95
|
+
*/
|
|
96
|
+
getRateLimitStatus() {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
return {
|
|
99
|
+
enabled: this.rateLimiting.enabled,
|
|
100
|
+
inCooldown: this.rateLimiting.inCooldown,
|
|
101
|
+
cooldownRemaining: this.rateLimiting.inCooldown
|
|
102
|
+
? Math.max(0, this.rateLimiting.cooldownUntil - now)
|
|
103
|
+
: 0,
|
|
104
|
+
currentOps: this.rateLimiting.operations.length,
|
|
105
|
+
maxOps: this.rateLimiting.maxOpsPerSecond,
|
|
106
|
+
rateLimited: this.stats.rateLimited
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 🔍 GET: Obtener valor del cache
|
|
112
|
+
* @param {string} key - Clave
|
|
113
|
+
* @returns {*} Valor cacheado o null
|
|
114
|
+
*/
|
|
115
|
+
get(key) {
|
|
116
|
+
// 🔐 Check rate limit
|
|
117
|
+
if (!this._checkRateLimit()) {
|
|
118
|
+
return null; // Silently fail on rate limit
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 1. Buscar en memoria
|
|
122
|
+
if (this.memoryCache.has(key)) {
|
|
123
|
+
const entry = this.memoryCache.get(key);
|
|
124
|
+
|
|
125
|
+
// Verificar TTL
|
|
126
|
+
if (this.isExpired(entry)) {
|
|
127
|
+
this.delete(key);
|
|
128
|
+
this.stats.misses++;
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Actualizar acceso (LRU)
|
|
133
|
+
this.accessOrder.set(key, Date.now());
|
|
134
|
+
this.stats.hits++;
|
|
135
|
+
|
|
136
|
+
return entry.value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Buscar en storage persistente
|
|
140
|
+
if (this.config.persistent) {
|
|
141
|
+
const stored = this.getFromStorage(key);
|
|
142
|
+
if (stored) {
|
|
143
|
+
// Restaurar a memoria
|
|
144
|
+
this.memoryCache.set(key, stored);
|
|
145
|
+
this.accessOrder.set(key, Date.now());
|
|
146
|
+
this.stats.hits++;
|
|
147
|
+
return stored.value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.stats.misses++;
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 💾 SET: Guardar valor en cache
|
|
157
|
+
* @param {string} key - Clave
|
|
158
|
+
* @param {*} value - Valor
|
|
159
|
+
* @param {number} ttl - Time to live (ms)
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
set(key, value, ttl) {
|
|
163
|
+
// 🔐 Check rate limit
|
|
164
|
+
if (!this._checkRateLimit()) {
|
|
165
|
+
return false; // Reject on rate limit
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const entry = {
|
|
170
|
+
key,
|
|
171
|
+
value,
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
ttl: ttl || this.config.defaultTTL,
|
|
174
|
+
size: this.estimateSize(value)
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Verificar si necesitamos hacer espacio
|
|
178
|
+
const hasSpace = this.ensureSpace(entry.size);
|
|
179
|
+
if (hasSpace === false) {
|
|
180
|
+
logger.warn(`[WuCache] ⚠️ Cannot cache item: ${key} (too large)`);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Guardar en memoria
|
|
185
|
+
this.memoryCache.set(key, entry);
|
|
186
|
+
this.accessOrder.set(key, Date.now());
|
|
187
|
+
|
|
188
|
+
// Guardar en storage persistente
|
|
189
|
+
if (this.config.persistent) {
|
|
190
|
+
this.saveToStorage(key, entry);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.stats.sets++;
|
|
194
|
+
this.stats.size += entry.size;
|
|
195
|
+
|
|
196
|
+
return true;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.warn('[WuCache] ⚠️ Failed to set cache:', error);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 🗑️ DELETE: Eliminar del cache
|
|
205
|
+
* @param {string} key - Clave
|
|
206
|
+
*/
|
|
207
|
+
delete(key) {
|
|
208
|
+
const entry = this.memoryCache.get(key);
|
|
209
|
+
if (entry) {
|
|
210
|
+
this.stats.size -= entry.size;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.memoryCache.delete(key);
|
|
214
|
+
this.accessOrder.delete(key);
|
|
215
|
+
|
|
216
|
+
if (this.config.persistent) {
|
|
217
|
+
this.deleteFromStorage(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 🧹 CLEAR: Limpiar todo el cache
|
|
223
|
+
*/
|
|
224
|
+
clear() {
|
|
225
|
+
this.memoryCache.clear();
|
|
226
|
+
this.accessOrder.clear();
|
|
227
|
+
this.stats.size = 0;
|
|
228
|
+
|
|
229
|
+
if (this.config.persistent) {
|
|
230
|
+
this.clearStorage();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
logger.debug('[WuCache] 🧹 Cache cleared');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* ⏰ IS EXPIRED: Verificar si entrada expiró
|
|
238
|
+
* @param {Object} entry - Entrada del cache
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
isExpired(entry) {
|
|
242
|
+
if (!entry.ttl) return false;
|
|
243
|
+
return Date.now() - entry.timestamp > entry.ttl;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 📏 ESTIMATE SIZE: Estimar tamaño de un valor
|
|
248
|
+
* @param {*} value - Valor
|
|
249
|
+
* @returns {number} Tamaño en bytes
|
|
250
|
+
*/
|
|
251
|
+
estimateSize(value) {
|
|
252
|
+
if (typeof value === 'string') {
|
|
253
|
+
return value.length * 2; // UTF-16
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (typeof value === 'object') {
|
|
257
|
+
try {
|
|
258
|
+
return JSON.stringify(value).length * 2;
|
|
259
|
+
} catch {
|
|
260
|
+
return 1000; // Estimación por defecto
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return 100; // Tamaño por defecto para primitivos
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 🎯 ENSURE SPACE: Asegurar espacio en cache (LRU eviction)
|
|
269
|
+
* @param {number} neededSize - Tamaño necesario
|
|
270
|
+
*/
|
|
271
|
+
ensureSpace(neededSize) {
|
|
272
|
+
const maxSizeBytes = this.config.maxSize * 1024 * 1024;
|
|
273
|
+
|
|
274
|
+
// 🛡️ FIX: Validar que el item no sea más grande que el máximo permitido
|
|
275
|
+
if (neededSize > maxSizeBytes) {
|
|
276
|
+
logger.warn(`[WuCache] ⚠️ Item size (${neededSize}) exceeds max cache size (${maxSizeBytes}). Skipping.`);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 🛡️ FIX: Límite de iteraciones para evitar loop infinito
|
|
281
|
+
const maxIterations = this.config.maxItems + 10;
|
|
282
|
+
let iterations = 0;
|
|
283
|
+
|
|
284
|
+
// Verificar si necesitamos limpiar
|
|
285
|
+
while ((this.stats.size + neededSize > maxSizeBytes ||
|
|
286
|
+
this.memoryCache.size >= this.config.maxItems) &&
|
|
287
|
+
iterations < maxIterations) {
|
|
288
|
+
|
|
289
|
+
iterations++;
|
|
290
|
+
|
|
291
|
+
// 🛡️ FIX: Si el cache está vacío pero aún no hay espacio, salir
|
|
292
|
+
if (this.memoryCache.size === 0) {
|
|
293
|
+
logger.warn('[WuCache] ⚠️ Cache empty but still no space. Breaking loop.');
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Encontrar entrada menos recientemente usada (LRU)
|
|
298
|
+
let oldestKey = null;
|
|
299
|
+
let oldestTime = Infinity;
|
|
300
|
+
|
|
301
|
+
for (const [key, time] of this.accessOrder) {
|
|
302
|
+
if (time < oldestTime) {
|
|
303
|
+
oldestTime = time;
|
|
304
|
+
oldestKey = key;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (oldestKey) {
|
|
309
|
+
logger.debug(`[WuCache] 🗑️ Evicting LRU entry: ${oldestKey}`);
|
|
310
|
+
this.delete(oldestKey);
|
|
311
|
+
this.stats.evictions++;
|
|
312
|
+
} else {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 🛡️ FIX: Log si alcanzamos el límite de iteraciones
|
|
318
|
+
if (iterations >= maxIterations) {
|
|
319
|
+
console.error(`[WuCache] 🚨 Max eviction iterations reached (${maxIterations}). Possible infinite loop prevented.`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 💽 GET FROM STORAGE: Obtener del storage persistente
|
|
327
|
+
* @param {string} key - Clave
|
|
328
|
+
* @returns {Object|null}
|
|
329
|
+
*/
|
|
330
|
+
getFromStorage(key) {
|
|
331
|
+
try {
|
|
332
|
+
const storage = this.getStorage();
|
|
333
|
+
const stored = storage.getItem(`wu_cache_${key}`);
|
|
334
|
+
|
|
335
|
+
if (stored) {
|
|
336
|
+
return JSON.parse(stored);
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
logger.warn('[WuCache] ⚠️ Failed to get from storage:', error);
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 💾 SAVE TO STORAGE: Guardar en storage persistente
|
|
346
|
+
* @param {string} key - Clave
|
|
347
|
+
* @param {Object} entry - Entrada
|
|
348
|
+
*/
|
|
349
|
+
saveToStorage(key, entry) {
|
|
350
|
+
const storage = this.getStorage();
|
|
351
|
+
try {
|
|
352
|
+
storage.setItem(`wu_cache_${key}`, JSON.stringify(entry));
|
|
353
|
+
} catch (error) {
|
|
354
|
+
// Storage lleno, limpiar entradas antiguas
|
|
355
|
+
logger.warn('[WuCache] ⚠️ Storage full, cleaning old entries');
|
|
356
|
+
this.cleanOldStorageEntries();
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
storage.setItem(`wu_cache_${key}`, JSON.stringify(entry));
|
|
360
|
+
} catch {
|
|
361
|
+
logger.warn('[WuCache] ⚠️ Failed to save to storage after cleanup');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 🗑️ DELETE FROM STORAGE: Eliminar del storage
|
|
368
|
+
* @param {string} key - Clave
|
|
369
|
+
*/
|
|
370
|
+
deleteFromStorage(key) {
|
|
371
|
+
try {
|
|
372
|
+
const storage = this.getStorage();
|
|
373
|
+
storage.removeItem(`wu_cache_${key}`);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
logger.warn('[WuCache] ⚠️ Failed to delete from storage:', error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 🧹 CLEAR STORAGE: Limpiar storage
|
|
381
|
+
*/
|
|
382
|
+
clearStorage() {
|
|
383
|
+
try {
|
|
384
|
+
const storage = this.getStorage();
|
|
385
|
+
const keys = Object.keys(storage);
|
|
386
|
+
|
|
387
|
+
keys.forEach(key => {
|
|
388
|
+
if (key.startsWith('wu_cache_')) {
|
|
389
|
+
storage.removeItem(key);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
} catch (error) {
|
|
393
|
+
logger.warn('[WuCache] ⚠️ Failed to clear storage:', error);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 🧹 CLEAN OLD STORAGE ENTRIES: Limpiar entradas antiguas del storage
|
|
399
|
+
*/
|
|
400
|
+
cleanOldStorageEntries() {
|
|
401
|
+
try {
|
|
402
|
+
const storage = this.getStorage();
|
|
403
|
+
const keys = Object.keys(storage);
|
|
404
|
+
const entries = [];
|
|
405
|
+
|
|
406
|
+
// Recopilar todas las entradas con timestamp
|
|
407
|
+
keys.forEach(key => {
|
|
408
|
+
if (key.startsWith('wu_cache_')) {
|
|
409
|
+
try {
|
|
410
|
+
const entry = JSON.parse(storage.getItem(key));
|
|
411
|
+
entries.push({ key, timestamp: entry.timestamp });
|
|
412
|
+
} catch {}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Ordenar por timestamp (más antiguas primero)
|
|
417
|
+
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
418
|
+
|
|
419
|
+
// Eliminar 25% de entradas más antiguas
|
|
420
|
+
const toRemove = Math.ceil(entries.length * 0.25);
|
|
421
|
+
for (let i = 0; i < toRemove; i++) {
|
|
422
|
+
storage.removeItem(entries[i].key);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
logger.debug(`[WuCache] 🧹 Cleaned ${toRemove} old storage entries`);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
logger.warn('[WuCache] ⚠️ Failed to clean old storage entries:', error);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 💽 GET STORAGE: Obtener instancia de storage
|
|
433
|
+
* @returns {Storage}
|
|
434
|
+
*/
|
|
435
|
+
getStorage() {
|
|
436
|
+
if (this.config.storage === 'localStorage') {
|
|
437
|
+
return window.localStorage;
|
|
438
|
+
} else if (this.config.storage === 'sessionStorage') {
|
|
439
|
+
return window.sessionStorage;
|
|
440
|
+
}
|
|
441
|
+
// Fallback a memoria
|
|
442
|
+
return {
|
|
443
|
+
getItem: () => null,
|
|
444
|
+
setItem: () => {},
|
|
445
|
+
removeItem: () => {},
|
|
446
|
+
clear: () => {}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* 📊 GET STATS: Obtener estadísticas del cache
|
|
452
|
+
* @returns {Object}
|
|
453
|
+
*/
|
|
454
|
+
getStats() {
|
|
455
|
+
const hitRate = this.stats.hits + this.stats.misses > 0
|
|
456
|
+
? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2)
|
|
457
|
+
: 0;
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
...this.stats,
|
|
461
|
+
hitRate: `${hitRate}%`,
|
|
462
|
+
items: this.memoryCache.size,
|
|
463
|
+
sizeMB: (this.stats.size / 1024 / 1024).toFixed(2)
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* ⚙️ CONFIGURE: Actualizar configuración
|
|
469
|
+
* @param {Object} config - Nueva configuración
|
|
470
|
+
*/
|
|
471
|
+
configure(config) {
|
|
472
|
+
this.config = {
|
|
473
|
+
...this.config,
|
|
474
|
+
...config
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|