wu-framework 1.1.15 → 1.1.17

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