@asaidimu/utils-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/index.mjs ADDED
@@ -0,0 +1,650 @@
1
+ // node_modules/uuid/dist/esm/stringify.js
2
+ var byteToHex = [];
3
+ for (let i = 0; i < 256; ++i) {
4
+ byteToHex.push((i + 256).toString(16).slice(1));
5
+ }
6
+ function unsafeStringify(arr, offset = 0) {
7
+ return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
8
+ }
9
+
10
+ // node_modules/uuid/dist/esm/rng.js
11
+ import { randomFillSync } from "crypto";
12
+ var rnds8Pool = new Uint8Array(256);
13
+ var poolPtr = rnds8Pool.length;
14
+ function rng() {
15
+ if (poolPtr > rnds8Pool.length - 16) {
16
+ randomFillSync(rnds8Pool);
17
+ poolPtr = 0;
18
+ }
19
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
20
+ }
21
+
22
+ // node_modules/uuid/dist/esm/native.js
23
+ import { randomUUID } from "crypto";
24
+ var native_default = { randomUUID };
25
+
26
+ // node_modules/uuid/dist/esm/v4.js
27
+ function v4(options, buf, offset) {
28
+ if (native_default.randomUUID && !buf && !options) {
29
+ return native_default.randomUUID();
30
+ }
31
+ options = options || {};
32
+ const rnds = options.random ?? options.rng?.() ?? rng();
33
+ if (rnds.length < 16) {
34
+ throw new Error("Random bytes length must be >= 16");
35
+ }
36
+ rnds[6] = rnds[6] & 15 | 64;
37
+ rnds[8] = rnds[8] & 63 | 128;
38
+ if (buf) {
39
+ offset = offset || 0;
40
+ if (offset < 0 || offset + 16 > buf.length) {
41
+ throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
42
+ }
43
+ for (let i = 0; i < 16; ++i) {
44
+ buf[offset + i] = rnds[i];
45
+ }
46
+ return buf;
47
+ }
48
+ return unsafeStringify(rnds);
49
+ }
50
+ var v4_default = v4;
51
+
52
+ // src/cache/cache.ts
53
+ var Cache = class {
54
+ cache = /* @__PURE__ */ new Map();
55
+ queries = /* @__PURE__ */ new Map();
56
+ fetching = /* @__PURE__ */ new Map();
57
+ defaultOptions;
58
+ metrics;
59
+ eventListeners = /* @__PURE__ */ new Map();
60
+ gcTimer;
61
+ // Persistence-related properties
62
+ persistenceId;
63
+ persistenceUnsubscribe;
64
+ persistenceDebounceTimer;
65
+ isHandlingRemoteUpdate = false;
66
+ constructor(defaultOptions = {}) {
67
+ if (defaultOptions.staleTime !== void 0 && defaultOptions.staleTime < 0) {
68
+ console.warn("CacheOptions: staleTime should be non-negative. Using 0.");
69
+ defaultOptions.staleTime = 0;
70
+ }
71
+ if (defaultOptions.cacheTime !== void 0 && defaultOptions.cacheTime < 0) {
72
+ console.warn("CacheOptions: cacheTime should be non-negative. Using 0.");
73
+ defaultOptions.cacheTime = 0;
74
+ }
75
+ if (defaultOptions.retryAttempts !== void 0 && defaultOptions.retryAttempts < 0) {
76
+ console.warn("CacheOptions: retryAttempts should be non-negative. Using 0.");
77
+ defaultOptions.retryAttempts = 0;
78
+ }
79
+ if (defaultOptions.retryDelay !== void 0 && defaultOptions.retryDelay < 0) {
80
+ console.warn("CacheOptions: retryDelay should be non-negative. Using 0.");
81
+ defaultOptions.retryDelay = 0;
82
+ }
83
+ if (defaultOptions.maxSize !== void 0 && defaultOptions.maxSize < 0) {
84
+ console.warn("CacheOptions: maxSize should be non-negative. Using 0.");
85
+ defaultOptions.maxSize = 0;
86
+ }
87
+ this.defaultOptions = {
88
+ staleTime: 5 * 60 * 1e3,
89
+ cacheTime: 30 * 60 * 1e3,
90
+ retryAttempts: 3,
91
+ retryDelay: 1e3,
92
+ maxSize: 1e3,
93
+ enableMetrics: true,
94
+ persistence: void 0,
95
+ persistenceId: void 0,
96
+ serializeValue: (value) => value,
97
+ deserializeValue: (value) => value,
98
+ persistenceDebounceTime: 500,
99
+ ...defaultOptions
100
+ };
101
+ this.metrics = { hits: 0, misses: 0, fetches: 0, errors: 0, evictions: 0, staleHits: 0 };
102
+ this.persistenceId = this.defaultOptions.persistenceId || v4_default();
103
+ this.startGarbageCollection();
104
+ this.initializePersistence();
105
+ }
106
+ async initializePersistence() {
107
+ const { persistence } = this.defaultOptions;
108
+ if (!persistence)
109
+ return;
110
+ try {
111
+ const persistedState = await persistence.get();
112
+ if (persistedState) {
113
+ this.deserializeAndLoadCache(persistedState);
114
+ this.emitEvent({ type: "persistence", key: this.persistenceId, timestamp: Date.now(), event: "load_success", message: `Cache loaded for ID: ${this.persistenceId}` });
115
+ }
116
+ } catch (error) {
117
+ console.error(`Cache (${this.persistenceId}): Failed to load state from persistence:`, error);
118
+ this.emitEvent({ type: "persistence", key: this.persistenceId, timestamp: Date.now(), event: "load_fail", error, message: `Failed to load cache for ID: ${this.persistenceId}` });
119
+ }
120
+ if (typeof persistence.subscribe === "function") {
121
+ try {
122
+ this.persistenceUnsubscribe = persistence.subscribe(
123
+ this.persistenceId,
124
+ (remoteState) => {
125
+ this.handleRemoteStateChange(remoteState);
126
+ }
127
+ );
128
+ } catch (error) {
129
+ console.error(`Cache (${this.persistenceId}): Failed to subscribe to persistence:`, error);
130
+ }
131
+ }
132
+ }
133
+ serializeCache() {
134
+ const serializableEntries = [];
135
+ const { serializeValue } = this.defaultOptions;
136
+ for (const [key, entry] of this.cache) {
137
+ if (entry.isLoading && entry.data === void 0 && entry.lastUpdated === 0)
138
+ continue;
139
+ serializableEntries.push([
140
+ key,
141
+ {
142
+ data: serializeValue(entry.data),
143
+ lastUpdated: entry.lastUpdated,
144
+ lastAccessed: entry.lastAccessed,
145
+ accessCount: entry.accessCount,
146
+ error: entry.error ? { name: entry.error.name, message: entry.error.message, stack: entry.error.stack } : void 0
147
+ }
148
+ ]);
149
+ }
150
+ return serializableEntries;
151
+ }
152
+ deserializeAndLoadCache(persistedState) {
153
+ this.isHandlingRemoteUpdate = true;
154
+ const tempCache = /* @__PURE__ */ new Map();
155
+ const { deserializeValue } = this.defaultOptions;
156
+ for (const [key, sEntry] of persistedState) {
157
+ let errorInstance;
158
+ if (sEntry.error) {
159
+ errorInstance = new Error(sEntry.error.message);
160
+ errorInstance.name = sEntry.error.name;
161
+ errorInstance.stack = sEntry.error.stack;
162
+ }
163
+ tempCache.set(key, {
164
+ data: deserializeValue(sEntry.data),
165
+ lastUpdated: sEntry.lastUpdated,
166
+ lastAccessed: sEntry.lastAccessed,
167
+ accessCount: sEntry.accessCount,
168
+ error: errorInstance,
169
+ isLoading: false
170
+ });
171
+ }
172
+ this.cache = tempCache;
173
+ this.enforceSizeLimit(false);
174
+ this.isHandlingRemoteUpdate = false;
175
+ }
176
+ schedulePersistState() {
177
+ if (!this.defaultOptions.persistence || this.isHandlingRemoteUpdate) {
178
+ return;
179
+ }
180
+ if (this.persistenceDebounceTimer) {
181
+ clearTimeout(this.persistenceDebounceTimer);
182
+ }
183
+ this.persistenceDebounceTimer = setTimeout(async () => {
184
+ try {
185
+ const stateToPersist = this.serializeCache();
186
+ await this.defaultOptions.persistence.set(this.persistenceId, stateToPersist);
187
+ this.emitEvent({ type: "persistence", key: this.persistenceId, timestamp: Date.now(), event: "save_success" });
188
+ } catch (error) {
189
+ console.error(`Cache (${this.persistenceId}): Failed to persist state:`, error);
190
+ this.emitEvent({ type: "persistence", key: this.persistenceId, timestamp: Date.now(), event: "save_fail", error });
191
+ }
192
+ }, this.defaultOptions.persistenceDebounceTime);
193
+ }
194
+ handleRemoteStateChange(remoteState) {
195
+ if (this.isHandlingRemoteUpdate || !remoteState)
196
+ return;
197
+ this.isHandlingRemoteUpdate = true;
198
+ const { deserializeValue } = this.defaultOptions;
199
+ const newCache = /* @__PURE__ */ new Map();
200
+ let changed = false;
201
+ for (const [key, sEntry] of remoteState) {
202
+ let errorInstance;
203
+ if (sEntry.error) {
204
+ errorInstance = new Error(sEntry.error.message);
205
+ errorInstance.name = sEntry.error.name;
206
+ errorInstance.stack = sEntry.error.stack;
207
+ }
208
+ const newEntry = {
209
+ data: deserializeValue(sEntry.data),
210
+ lastUpdated: sEntry.lastUpdated,
211
+ lastAccessed: sEntry.lastAccessed,
212
+ accessCount: sEntry.accessCount,
213
+ error: errorInstance,
214
+ isLoading: false
215
+ };
216
+ newCache.set(key, newEntry);
217
+ const localEntry = this.cache.get(key);
218
+ if (!localEntry || localEntry.lastUpdated < newEntry.lastUpdated || JSON.stringify(localEntry.data) !== JSON.stringify(newEntry.data)) {
219
+ changed = true;
220
+ }
221
+ }
222
+ if (this.cache.size !== newCache.size)
223
+ changed = true;
224
+ if (changed) {
225
+ this.cache = newCache;
226
+ this.enforceSizeLimit(false);
227
+ this.emitEvent({ type: "persistence", key: this.persistenceId, timestamp: Date.now(), event: "remote_update", message: "Cache updated from remote state." });
228
+ }
229
+ this.isHandlingRemoteUpdate = false;
230
+ }
231
+ registerQuery(key, fetchFunction, options = {}) {
232
+ if (options.staleTime !== void 0 && options.staleTime < 0)
233
+ options.staleTime = 0;
234
+ if (options.cacheTime !== void 0 && options.cacheTime < 0)
235
+ options.cacheTime = 0;
236
+ this.queries.set(key, {
237
+ fetchFunction,
238
+ options: { ...this.defaultOptions, ...options }
239
+ });
240
+ }
241
+ async get(key, options) {
242
+ const query = this.queries.get(key);
243
+ if (!query) {
244
+ throw new Error(`No query registered for key: ${key}`);
245
+ }
246
+ let cacheEntry = this.cache.get(key);
247
+ const isEntryStale = this.isStale(cacheEntry, query.options);
248
+ let placeholderCreated = false;
249
+ if (cacheEntry) {
250
+ cacheEntry.lastAccessed = Date.now();
251
+ cacheEntry.accessCount++;
252
+ this.updateMetrics("hits");
253
+ if (isEntryStale)
254
+ this.updateMetrics("staleHits");
255
+ this.emitEvent({ type: "hit", key, timestamp: Date.now(), data: cacheEntry.data, isStale: isEntryStale });
256
+ } else {
257
+ this.updateMetrics("misses");
258
+ this.emitEvent({ type: "miss", key, timestamp: Date.now() });
259
+ if (!options?.waitForFresh || isEntryStale) {
260
+ const newPlaceholder = {
261
+ data: void 0,
262
+ lastUpdated: 0,
263
+ lastAccessed: Date.now(),
264
+ accessCount: 1,
265
+ isLoading: true,
266
+ error: void 0
267
+ };
268
+ this.cache.set(key, newPlaceholder);
269
+ cacheEntry = newPlaceholder;
270
+ placeholderCreated = true;
271
+ }
272
+ }
273
+ if (options?.waitForFresh && (!cacheEntry || isEntryStale || cacheEntry.isLoading)) {
274
+ try {
275
+ const freshData = await this.fetchAndWait(key, query);
276
+ if (placeholderCreated && this.cache.get(key) === cacheEntry)
277
+ this.schedulePersistState();
278
+ return freshData;
279
+ } catch (error) {
280
+ if (options.throwOnError)
281
+ throw error;
282
+ return this.cache.get(key)?.data;
283
+ }
284
+ }
285
+ if (!cacheEntry || isEntryStale || cacheEntry && !cacheEntry.isLoading && cacheEntry.lastUpdated === 0 && !cacheEntry.error) {
286
+ if (cacheEntry && !cacheEntry.isLoading) {
287
+ cacheEntry.isLoading = true;
288
+ } else if (!cacheEntry) {
289
+ const tempEntry = { data: void 0, lastUpdated: 0, lastAccessed: Date.now(), accessCount: 0, isLoading: true, error: void 0 };
290
+ this.cache.set(key, tempEntry);
291
+ cacheEntry = tempEntry;
292
+ placeholderCreated = true;
293
+ }
294
+ this.fetch(key, query).catch(() => {
295
+ });
296
+ }
297
+ if (placeholderCreated) {
298
+ this.schedulePersistState();
299
+ }
300
+ if (cacheEntry?.error && options?.throwOnError) {
301
+ throw cacheEntry.error;
302
+ }
303
+ return cacheEntry?.data;
304
+ }
305
+ peek(key) {
306
+ const cacheEntry = this.cache.get(key);
307
+ if (cacheEntry) {
308
+ cacheEntry.lastAccessed = Date.now();
309
+ cacheEntry.accessCount++;
310
+ }
311
+ return cacheEntry?.data;
312
+ }
313
+ has(key) {
314
+ const cacheEntry = this.cache.get(key);
315
+ const query = this.queries.get(key);
316
+ if (!cacheEntry || !query)
317
+ return false;
318
+ return !this.isStale(cacheEntry, query.options) && !cacheEntry.isLoading;
319
+ }
320
+ async fetch(key, query) {
321
+ if (this.fetching.has(key)) {
322
+ return this.fetching.get(key);
323
+ }
324
+ let cacheEntry = this.cache.get(key);
325
+ if (!cacheEntry) {
326
+ cacheEntry = { data: void 0, lastUpdated: 0, lastAccessed: Date.now(), accessCount: 0, isLoading: true, error: void 0 };
327
+ this.cache.set(key, cacheEntry);
328
+ this.schedulePersistState();
329
+ } else if (!cacheEntry.isLoading) {
330
+ cacheEntry.isLoading = true;
331
+ cacheEntry.error = void 0;
332
+ }
333
+ const fetchPromise = this.performFetchWithRetry(key, query, cacheEntry);
334
+ this.fetching.set(key, fetchPromise);
335
+ try {
336
+ return await fetchPromise;
337
+ } finally {
338
+ this.fetching.delete(key);
339
+ }
340
+ }
341
+ async fetchAndWait(key, query) {
342
+ const existingFetch = this.fetching.get(key);
343
+ if (existingFetch) {
344
+ return existingFetch;
345
+ }
346
+ const result = await this.fetch(key, query);
347
+ if (result === void 0) {
348
+ const currentEntry = this.cache.get(key);
349
+ if (currentEntry?.error)
350
+ throw currentEntry.error;
351
+ throw new Error(`Failed to fetch data for key: ${key} after retries.`);
352
+ }
353
+ return result;
354
+ }
355
+ async performFetchWithRetry(key, query, entry) {
356
+ const { retryAttempts, retryDelay } = query.options;
357
+ let lastError;
358
+ entry.isLoading = true;
359
+ for (let attempt = 0; attempt <= retryAttempts; attempt++) {
360
+ try {
361
+ this.emitEvent({ type: "fetch", key, timestamp: Date.now(), attempt });
362
+ this.updateMetrics("fetches");
363
+ const data = await query.fetchFunction();
364
+ entry.data = data;
365
+ entry.lastUpdated = Date.now();
366
+ entry.isLoading = false;
367
+ entry.error = void 0;
368
+ this.cache.set(key, entry);
369
+ this.schedulePersistState();
370
+ this.enforceSizeLimit();
371
+ return data;
372
+ } catch (error) {
373
+ lastError = error;
374
+ this.emitEvent({ type: "error", key, timestamp: Date.now(), error: lastError, attempt });
375
+ if (attempt < retryAttempts) {
376
+ await this.delay(retryDelay * Math.pow(2, attempt));
377
+ }
378
+ }
379
+ }
380
+ this.updateMetrics("errors");
381
+ entry.error = lastError;
382
+ entry.isLoading = false;
383
+ this.cache.set(key, entry);
384
+ this.schedulePersistState();
385
+ return void 0;
386
+ }
387
+ isStale(cacheEntry, options) {
388
+ if (!cacheEntry || cacheEntry.error)
389
+ return true;
390
+ if (cacheEntry.isLoading && !cacheEntry.data)
391
+ return true;
392
+ const { staleTime } = options;
393
+ if (staleTime === 0 || staleTime === Infinity)
394
+ return false;
395
+ return Date.now() - cacheEntry.lastUpdated > staleTime;
396
+ }
397
+ async invalidate(key, refetch = true) {
398
+ const cacheEntry = this.cache.get(key);
399
+ const query = this.queries.get(key);
400
+ let changed = false;
401
+ if (cacheEntry) {
402
+ changed = cacheEntry.lastUpdated !== 0 || cacheEntry.error !== void 0;
403
+ cacheEntry.lastUpdated = 0;
404
+ cacheEntry.error = void 0;
405
+ this.emitEvent({ type: "invalidation", key, timestamp: Date.now() });
406
+ if (changed)
407
+ this.schedulePersistState();
408
+ if (refetch && query) {
409
+ this.fetch(key, query).catch(() => {
410
+ });
411
+ }
412
+ }
413
+ }
414
+ async invalidatePattern(pattern, refetch = true) {
415
+ const keysToInvalidate = [];
416
+ let anyChanged = false;
417
+ for (const key of this.cache.keys()) {
418
+ if (pattern.test(key))
419
+ keysToInvalidate.push(key);
420
+ }
421
+ keysToInvalidate.forEach((key) => {
422
+ const cacheEntry = this.cache.get(key);
423
+ if (cacheEntry) {
424
+ if (cacheEntry.lastUpdated !== 0 || cacheEntry.error !== void 0)
425
+ anyChanged = true;
426
+ cacheEntry.lastUpdated = 0;
427
+ cacheEntry.error = void 0;
428
+ this.emitEvent({ type: "invalidation", key, timestamp: Date.now() });
429
+ }
430
+ });
431
+ if (anyChanged)
432
+ this.schedulePersistState();
433
+ if (refetch && keysToInvalidate.length > 0) {
434
+ await Promise.all(keysToInvalidate.map((key) => {
435
+ const query = this.queries.get(key);
436
+ return query ? this.fetch(key, query).catch(() => {
437
+ }) : Promise.resolve();
438
+ }));
439
+ }
440
+ }
441
+ async prefetch(key) {
442
+ const query = this.queries.get(key);
443
+ if (!query) {
444
+ console.warn(`Cannot prefetch: No query registered for key: ${key}`);
445
+ return;
446
+ }
447
+ const cacheEntry = this.cache.get(key);
448
+ if (!cacheEntry || this.isStale(cacheEntry, query.options)) {
449
+ this.fetch(key, query).catch(() => {
450
+ });
451
+ }
452
+ }
453
+ async refresh(key) {
454
+ const query = this.queries.get(key);
455
+ if (!query) {
456
+ console.warn(`Cannot refresh: No query registered for key: ${key}`);
457
+ return void 0;
458
+ }
459
+ this.fetching.delete(key);
460
+ let entry = this.cache.get(key);
461
+ if (!entry) {
462
+ entry = { data: void 0, lastUpdated: 0, lastAccessed: Date.now(), accessCount: 0, isLoading: true, error: void 0 };
463
+ this.cache.set(key, entry);
464
+ this.schedulePersistState();
465
+ } else {
466
+ entry.isLoading = true;
467
+ entry.error = void 0;
468
+ }
469
+ const fetchPromise = this.performFetchWithRetry(key, query, entry);
470
+ this.fetching.set(key, fetchPromise);
471
+ try {
472
+ return await fetchPromise;
473
+ } finally {
474
+ this.fetching.delete(key);
475
+ }
476
+ }
477
+ setData(key, data) {
478
+ const existingEntry = this.cache.get(key);
479
+ const oldData = existingEntry?.data;
480
+ const newEntry = {
481
+ data,
482
+ lastUpdated: Date.now(),
483
+ lastAccessed: Date.now(),
484
+ accessCount: (existingEntry?.accessCount || 0) + 1,
485
+ isLoading: false,
486
+ error: void 0
487
+ };
488
+ this.cache.set(key, newEntry);
489
+ this.schedulePersistState();
490
+ this.enforceSizeLimit();
491
+ this.emitEvent({ type: "set_data", key, timestamp: Date.now(), newData: data, oldData });
492
+ }
493
+ remove(key) {
494
+ this.fetching.delete(key);
495
+ const existed = this.cache.has(key);
496
+ const deleted = this.cache.delete(key);
497
+ if (deleted && existed) {
498
+ this.schedulePersistState();
499
+ }
500
+ return deleted;
501
+ }
502
+ enforceSizeLimit(schedulePersist = true) {
503
+ const { maxSize } = this.defaultOptions;
504
+ if (maxSize === Infinity || this.cache.size <= maxSize)
505
+ return;
506
+ let evictedCount = 0;
507
+ if (maxSize === 0) {
508
+ evictedCount = this.cache.size;
509
+ for (const key of this.cache.keys()) {
510
+ this.cache.delete(key);
511
+ this.emitEvent({ type: "eviction", key, timestamp: Date.now(), reason: "size_limit_zero" });
512
+ this.updateMetrics("evictions");
513
+ }
514
+ } else {
515
+ const entries = Array.from(this.cache.entries()).sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed);
516
+ const numToEvict = this.cache.size - maxSize;
517
+ if (numToEvict > 0) {
518
+ const toEvict = entries.slice(0, numToEvict);
519
+ toEvict.forEach(([key]) => {
520
+ if (this.cache.delete(key)) {
521
+ evictedCount++;
522
+ this.emitEvent({ type: "eviction", key, timestamp: Date.now(), reason: "size_limit_lru" });
523
+ this.updateMetrics("evictions");
524
+ }
525
+ });
526
+ }
527
+ }
528
+ if (evictedCount > 0 && schedulePersist) {
529
+ this.schedulePersistState();
530
+ }
531
+ }
532
+ startGarbageCollection() {
533
+ const { cacheTime } = this.defaultOptions;
534
+ if (cacheTime === Infinity || cacheTime <= 0)
535
+ return;
536
+ const interval = Math.max(1e3, Math.min(cacheTime / 4, 5 * 60 * 1e3));
537
+ this.gcTimer = setInterval(() => this.garbageCollect(), interval);
538
+ }
539
+ garbageCollect() {
540
+ const now = Date.now();
541
+ let collected = 0;
542
+ const keysToDelete = [];
543
+ for (const [key, entry] of this.cache) {
544
+ if (entry.isLoading)
545
+ continue;
546
+ const query = this.queries.get(key);
547
+ const itemCacheTime = query?.options.cacheTime ?? this.defaultOptions.cacheTime;
548
+ if (itemCacheTime === Infinity || itemCacheTime <= 0)
549
+ continue;
550
+ if (now - entry.lastAccessed > itemCacheTime) {
551
+ keysToDelete.push(key);
552
+ }
553
+ }
554
+ if (keysToDelete.length > 0) {
555
+ keysToDelete.forEach((key) => {
556
+ if (this.cache.delete(key)) {
557
+ this.fetching.delete(key);
558
+ this.emitEvent({ type: "eviction", key, timestamp: Date.now(), reason: "garbage_collected_idle" });
559
+ this.updateMetrics("evictions");
560
+ collected++;
561
+ }
562
+ });
563
+ this.schedulePersistState();
564
+ }
565
+ return collected;
566
+ }
567
+ getStats() {
568
+ const totalRequests = this.metrics.hits + this.metrics.misses;
569
+ const hitRate = totalRequests > 0 ? this.metrics.hits / totalRequests : 0;
570
+ const staleHitRate = this.metrics.hits > 0 ? this.metrics.staleHits / this.metrics.hits : 0;
571
+ const entries = Array.from(this.cache.entries()).map(([key, entry]) => {
572
+ const query = this.queries.get(key);
573
+ const isStale = query ? this.isStale(entry, query.options) : true;
574
+ return { key, lastAccessed: entry.lastAccessed, lastUpdated: entry.lastUpdated, accessCount: entry.accessCount, isStale, isLoading: entry.isLoading, error: !!entry.error };
575
+ });
576
+ return { size: this.cache.size, metrics: { ...this.metrics }, hitRate, staleHitRate, entries };
577
+ }
578
+ on(event, listener) {
579
+ if (!this.eventListeners.has(event)) {
580
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
581
+ }
582
+ this.eventListeners.get(event).add(listener);
583
+ }
584
+ off(event, listener) {
585
+ this.eventListeners.get(event)?.delete(listener);
586
+ }
587
+ emitEvent(event) {
588
+ const listeners = this.eventListeners.get(event.type);
589
+ if (!listeners)
590
+ return;
591
+ listeners.forEach((listener) => {
592
+ try {
593
+ listener(event);
594
+ } catch (error) {
595
+ const idInfo = event.type === "persistence" ? `for ID ${event.key}` : `for key ${event.key}`;
596
+ console.error(`Cache event listener error during ${event.type} ${idInfo}:`, error);
597
+ }
598
+ });
599
+ }
600
+ updateMetrics(type, amount = 1) {
601
+ if (this.defaultOptions.enableMetrics) {
602
+ this.metrics[type] = (this.metrics[type] || 0) + amount;
603
+ }
604
+ }
605
+ delay(ms) {
606
+ return new Promise((resolve) => setTimeout(resolve, ms));
607
+ }
608
+ async clear() {
609
+ const hadItems = this.cache.size > 0;
610
+ this.cache.clear();
611
+ this.fetching.clear();
612
+ if (this.defaultOptions.enableMetrics) {
613
+ this.metrics = { hits: 0, misses: 0, fetches: 0, errors: 0, evictions: 0, staleHits: 0 };
614
+ }
615
+ if (this.defaultOptions.persistence) {
616
+ try {
617
+ await this.defaultOptions.persistence.clear();
618
+ this.emitEvent({ type: "persistence", key: this.persistenceId, timestamp: Date.now(), event: "clear_success" });
619
+ } catch (error) {
620
+ console.error(`Cache (${this.persistenceId}): Failed to clear persisted state:`, error);
621
+ this.emitEvent({ type: "persistence", key: this.persistenceId, timestamp: Date.now(), event: "clear_fail", error });
622
+ }
623
+ } else if (hadItems) {
624
+ this.schedulePersistState();
625
+ }
626
+ }
627
+ destroy() {
628
+ if (this.gcTimer) {
629
+ clearInterval(this.gcTimer);
630
+ this.gcTimer = void 0;
631
+ }
632
+ if (this.persistenceDebounceTimer)
633
+ clearTimeout(this.persistenceDebounceTimer);
634
+ if (this.persistenceUnsubscribe) {
635
+ try {
636
+ this.persistenceUnsubscribe();
637
+ } catch (error) {
638
+ console.error(`Cache (${this.persistenceId}): Error unsubscribing persistence:`, error);
639
+ }
640
+ }
641
+ this.cache.clear();
642
+ this.fetching.clear();
643
+ this.queries.clear();
644
+ this.eventListeners.clear();
645
+ this.metrics = { hits: 0, misses: 0, fetches: 0, errors: 0, evictions: 0, staleHits: 0 };
646
+ }
647
+ };
648
+ export {
649
+ Cache
650
+ };