@umituz/react-native-storage 2.6.0 → 2.6.2
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/package.json +8 -4
- package/src/cache/__tests__/PerformanceAndMemory.test.ts +386 -0
- package/src/cache/__tests__/setup.ts +19 -0
- package/src/cache/domain/Cache.ts +146 -0
- package/src/cache/domain/CacheManager.ts +48 -0
- package/src/cache/domain/CacheStatsTracker.ts +49 -0
- package/src/cache/domain/ErrorHandler.ts +42 -0
- package/src/cache/domain/PatternMatcher.ts +30 -0
- package/src/cache/domain/__tests__/Cache.test.ts +292 -0
- package/src/cache/domain/__tests__/CacheManager.test.ts +276 -0
- package/src/cache/domain/__tests__/ErrorHandler.test.ts +303 -0
- package/src/cache/domain/__tests__/PatternMatcher.test.ts +261 -0
- package/src/cache/domain/strategies/EvictionStrategy.ts +9 -0
- package/src/cache/domain/strategies/FIFOStrategy.ts +12 -0
- package/src/cache/domain/strategies/LFUStrategy.ts +22 -0
- package/src/cache/domain/strategies/LRUStrategy.ts +22 -0
- package/src/cache/domain/strategies/TTLStrategy.ts +23 -0
- package/src/cache/domain/strategies/__tests__/EvictionStrategies.test.ts +293 -0
- package/src/cache/domain/types/Cache.ts +28 -0
- package/src/cache/index.ts +28 -0
- package/src/cache/infrastructure/TTLCache.ts +103 -0
- package/src/cache/infrastructure/__tests__/TTLCache.test.ts +303 -0
- package/src/cache/presentation/__tests__/ReactHooks.test.ts +512 -0
- package/src/cache/presentation/useCache.ts +76 -0
- package/src/cache/presentation/useCachedValue.ts +88 -0
- package/src/cache/types.d.ts +3 -0
- package/src/index.ts +28 -0
- package/src/types/global.d.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-storage",
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.6.2",
|
|
4
|
+
"description": "Unified storage solution with AsyncStorage persistence, Zustand state management, and in-memory caching for React Native",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
"async-storage",
|
|
18
18
|
"zustand",
|
|
19
19
|
"persistence",
|
|
20
|
-
"secure-storage"
|
|
20
|
+
"secure-storage",
|
|
21
|
+
"cache",
|
|
22
|
+
"in-memory",
|
|
23
|
+
"ttl",
|
|
24
|
+
"lru"
|
|
21
25
|
],
|
|
22
26
|
"author": "Ümit UZ <umit@umituz.com>",
|
|
23
27
|
"license": "MIT",
|
|
@@ -49,4 +53,4 @@
|
|
|
49
53
|
"README.md",
|
|
50
54
|
"LICENSE"
|
|
51
55
|
]
|
|
52
|
-
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance and Memory Leak Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Cache } from '../Cache';
|
|
6
|
+
import { TTLCache } from '../TTLCache';
|
|
7
|
+
import { cacheManager } from '../CacheManager';
|
|
8
|
+
import { PatternMatcher } from '../PatternMatcher';
|
|
9
|
+
import { renderHook, act } from '@testing-library/react';
|
|
10
|
+
import { useCache } from '../../presentation/useCache';
|
|
11
|
+
|
|
12
|
+
describe('Performance and Memory Leak Tests', () => {
|
|
13
|
+
describe('Cache Performance', () => {
|
|
14
|
+
test('should handle large number of entries efficiently', () => {
|
|
15
|
+
const cache = new Cache<string>({ maxSize: 10000 });
|
|
16
|
+
const startTime = performance.now();
|
|
17
|
+
|
|
18
|
+
// Add 10,000 entries
|
|
19
|
+
for (let i = 0; i < 10000; i++) {
|
|
20
|
+
cache.set(`key${i}`, `value${i}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const insertTime = performance.now() - startTime;
|
|
24
|
+
|
|
25
|
+
// Test retrieval performance
|
|
26
|
+
const retrieveStart = performance.now();
|
|
27
|
+
for (let i = 0; i < 10000; i++) {
|
|
28
|
+
cache.get(`key${i}`);
|
|
29
|
+
}
|
|
30
|
+
const retrieveTime = performance.now() - retrieveStart;
|
|
31
|
+
|
|
32
|
+
expect(cache.getStats().size).toBe(10000);
|
|
33
|
+
expect(insertTime).toBeLessThan(1000); // 1 second for 10k inserts
|
|
34
|
+
expect(retrieveTime).toBeLessThan(500); // 0.5 second for 10k retrievals
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should handle rapid eviction without performance degradation', () => {
|
|
38
|
+
const cache = new Cache<string>({ maxSize: 100 });
|
|
39
|
+
const startTime = performance.now();
|
|
40
|
+
|
|
41
|
+
// Add and rapidly evict entries
|
|
42
|
+
for (let i = 0; i < 1000; i++) {
|
|
43
|
+
cache.set(`key${i}`, `value${i}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const endTime = performance.now();
|
|
47
|
+
const duration = endTime - startTime;
|
|
48
|
+
|
|
49
|
+
expect(cache.getStats().size).toBe(100); // Should maintain max size
|
|
50
|
+
expect(cache.getStats().evictions).toBeGreaterThan(800); // Many evictions
|
|
51
|
+
expect(duration).toBeLessThan(1000); // Should complete quickly
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should handle pattern invalidation efficiently', () => {
|
|
55
|
+
const cache = new Cache<string>();
|
|
56
|
+
|
|
57
|
+
// Add entries with different patterns
|
|
58
|
+
for (let i = 0; i < 1000; i++) {
|
|
59
|
+
cache.set(`user:${i}:profile`, `profile${i}`);
|
|
60
|
+
cache.set(`user:${i}:settings`, `settings${i}`);
|
|
61
|
+
cache.set(`post:${i}`, `post${i}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const startTime = performance.now();
|
|
65
|
+
const invalidatedCount = cache.invalidatePattern('user:*:profile');
|
|
66
|
+
const endTime = performance.now();
|
|
67
|
+
|
|
68
|
+
const duration = endTime - startTime;
|
|
69
|
+
|
|
70
|
+
expect(invalidatedCount).toBe(1000);
|
|
71
|
+
expect(duration).toBeLessThan(100); // Should be very fast
|
|
72
|
+
expect(cache.getStats().size).toBe(2000); // posts + settings remain
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Memory Management', () => {
|
|
77
|
+
test('should not memory leak with cache destruction', () => {
|
|
78
|
+
const caches: TTLCache<string>[] = [];
|
|
79
|
+
|
|
80
|
+
// Create many caches
|
|
81
|
+
for (let i = 0; i < 100; i++) {
|
|
82
|
+
const cache = new TTLCache<string>({ cleanupIntervalMs: 100 });
|
|
83
|
+
cache.set(`key${i}`, `value${i}`);
|
|
84
|
+
caches.push(cache);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Destroy all caches
|
|
88
|
+
const startTime = performance.now();
|
|
89
|
+
caches.forEach(cache => cache.destroy());
|
|
90
|
+
const endTime = performance.now();
|
|
91
|
+
|
|
92
|
+
const duration = endTime - startTime;
|
|
93
|
+
|
|
94
|
+
expect(duration).toBeLessThan(1000); // Should destroy quickly
|
|
95
|
+
|
|
96
|
+
// Operations on destroyed caches should be safe
|
|
97
|
+
caches.forEach(cache => {
|
|
98
|
+
expect(() => cache.set('test', 'value')).not.toThrow();
|
|
99
|
+
expect(cache.get('test')).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should handle cache manager memory efficiently', () => {
|
|
104
|
+
const cacheNames: string[] = [];
|
|
105
|
+
|
|
106
|
+
// Create many caches through manager
|
|
107
|
+
for (let i = 0; i < 1000; i++) {
|
|
108
|
+
const name = `cache-${i}`;
|
|
109
|
+
cacheNames.push(name);
|
|
110
|
+
const cache = cacheManager.getCache<string>(name);
|
|
111
|
+
cache.set(`key${i}`, `value${i}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
expect(cacheManager.getCacheNames()).toHaveLength(1000);
|
|
115
|
+
|
|
116
|
+
// Delete all caches
|
|
117
|
+
const startTime = performance.now();
|
|
118
|
+
cacheNames.forEach(name => cacheManager.deleteCache(name));
|
|
119
|
+
const endTime = performance.now();
|
|
120
|
+
|
|
121
|
+
const duration = endTime - startTime;
|
|
122
|
+
|
|
123
|
+
expect(duration).toBeLessThan(1000);
|
|
124
|
+
expect(cacheManager.getCacheNames()).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('should cleanup pattern matcher cache', () => {
|
|
128
|
+
// Create many unique patterns to fill cache
|
|
129
|
+
for (let i = 0; i < 1000; i++) {
|
|
130
|
+
PatternMatcher.convertPatternToRegex(`pattern-${i}-*`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Clear cache and verify memory is freed
|
|
134
|
+
PatternMatcher.clearCache();
|
|
135
|
+
|
|
136
|
+
// Should still work after clear
|
|
137
|
+
expect(PatternMatcher.matchesPattern('test-key', 'test-*')).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('React Hooks Performance', () => {
|
|
142
|
+
test('should handle many hook instances without memory leaks', () => {
|
|
143
|
+
const hooks: Array<ReturnType<typeof useCache<string>>> = [];
|
|
144
|
+
|
|
145
|
+
// Create many hook instances
|
|
146
|
+
for (let i = 0; i < 100; i++) {
|
|
147
|
+
const { result } = renderHook(() => useCache<string>(`test-cache-${i}`));
|
|
148
|
+
hooks.push(result.current);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Perform operations on all hooks
|
|
152
|
+
const startTime = performance.now();
|
|
153
|
+
hooks.forEach((hook, index) => {
|
|
154
|
+
act(() => {
|
|
155
|
+
hook.set(`key${index}`, `value${index}`);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
const endTime = performance.now();
|
|
159
|
+
|
|
160
|
+
const duration = endTime - startTime;
|
|
161
|
+
|
|
162
|
+
expect(duration).toBeLessThan(2000); // Should complete within 2 seconds
|
|
163
|
+
|
|
164
|
+
// Verify all operations worked
|
|
165
|
+
hooks.forEach((hook, index) => {
|
|
166
|
+
expect(hook.get(`key${index}`)).toBe(`value${index}`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Cleanup all hooks
|
|
170
|
+
hooks.forEach(() => {
|
|
171
|
+
// Hooks will be automatically cleaned up when unmounted
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('should handle rapid hook re-renders efficiently', () => {
|
|
176
|
+
const { result, rerender } = renderHook(() => useCache<string>('rapid-cache'));
|
|
177
|
+
|
|
178
|
+
const startTime = performance.now();
|
|
179
|
+
|
|
180
|
+
// Perform many rapid operations
|
|
181
|
+
for (let i = 0; i < 1000; i++) {
|
|
182
|
+
act(() => {
|
|
183
|
+
result.current.set(`key${i}`, `value${i}`);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const endTime = performance.now();
|
|
188
|
+
const duration = endTime - startTime;
|
|
189
|
+
|
|
190
|
+
expect(duration).toBeLessThan(3000); // Should complete within 3 seconds
|
|
191
|
+
expect(result.current.getStats().size).toBeLessThanOrEqual(100); // Limited by eviction
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Stress Tests', () => {
|
|
196
|
+
test('should handle concurrent operations safely', async () => {
|
|
197
|
+
const cache = new Cache<string>({ maxSize: 1000 });
|
|
198
|
+
const promises: Promise<void>[] = [];
|
|
199
|
+
|
|
200
|
+
// Create concurrent operations
|
|
201
|
+
for (let i = 0; i < 100; i++) {
|
|
202
|
+
promises.push(
|
|
203
|
+
new Promise<void>((resolve) => {
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
for (let j = 0; j < 10; j++) {
|
|
206
|
+
const key = `concurrent-${i}-${j}`;
|
|
207
|
+
const value = `value-${i}-${j}`;
|
|
208
|
+
cache.set(key, value);
|
|
209
|
+
cache.get(key);
|
|
210
|
+
}
|
|
211
|
+
resolve();
|
|
212
|
+
}, Math.random() * 100);
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Wait for all operations to complete
|
|
218
|
+
await Promise.all(promises);
|
|
219
|
+
|
|
220
|
+
// Verify cache is in consistent state
|
|
221
|
+
const stats = cache.getStats();
|
|
222
|
+
expect(stats.size).toBeLessThanOrEqual(1000);
|
|
223
|
+
expect(stats.hits + stats.misses).toBeGreaterThan(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('should handle TTL cache under stress', async () => {
|
|
227
|
+
jest.useFakeTimers();
|
|
228
|
+
|
|
229
|
+
const cache = new TTLCache<string>({
|
|
230
|
+
maxSize: 500,
|
|
231
|
+
defaultTTL: 100,
|
|
232
|
+
cleanupIntervalMs: 50
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Add many entries with short TTL
|
|
236
|
+
for (let i = 0; i < 1000; i++) {
|
|
237
|
+
cache.set(`stress-key${i}`, `stress-value${i}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Advance time to trigger multiple cleanup cycles
|
|
241
|
+
for (let i = 0; i < 10; i++) {
|
|
242
|
+
jest.advanceTimersByTime(50);
|
|
243
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Cache should handle stress without errors
|
|
247
|
+
expect(() => cache.get('any-key')).not.toThrow();
|
|
248
|
+
|
|
249
|
+
const stats = cache.getStats();
|
|
250
|
+
expect(stats.expirations).toBeGreaterThan(0);
|
|
251
|
+
|
|
252
|
+
cache.destroy();
|
|
253
|
+
jest.useRealTimers();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('should handle pattern matching stress test', () => {
|
|
257
|
+
const patterns: string[] = [];
|
|
258
|
+
const keys: string[] = [];
|
|
259
|
+
|
|
260
|
+
// Generate many patterns and keys
|
|
261
|
+
for (let i = 0; i < 1000; i++) {
|
|
262
|
+
patterns.push(`pattern-${i}-*`);
|
|
263
|
+
keys.push(`pattern-${i}-value`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const startTime = performance.now();
|
|
267
|
+
|
|
268
|
+
// Test all pattern matches
|
|
269
|
+
patterns.forEach((pattern, index) => {
|
|
270
|
+
PatternMatcher.matchesPattern(keys[index], pattern);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const endTime = performance.now();
|
|
274
|
+
const duration = endTime - startTime;
|
|
275
|
+
|
|
276
|
+
expect(duration).toBeLessThan(100); // Should be very fast with caching
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('Memory Leak Detection', () => {
|
|
281
|
+
test('should not leak memory with repeated cache operations', () => {
|
|
282
|
+
const cache = new Cache<string>();
|
|
283
|
+
const initialMemory = process.memoryUsage().heapUsed;
|
|
284
|
+
|
|
285
|
+
// Perform many operations
|
|
286
|
+
for (let cycle = 0; cycle < 100; cycle++) {
|
|
287
|
+
// Add many entries
|
|
288
|
+
for (let i = 0; i < 100; i++) {
|
|
289
|
+
cache.set(`cycle-${cycle}-key-${i}`, `value-${i}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Clear cache
|
|
293
|
+
cache.clear();
|
|
294
|
+
|
|
295
|
+
// Force garbage collection if available
|
|
296
|
+
if (global.gc) {
|
|
297
|
+
global.gc();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const finalMemory = process.memoryUsage().heapUsed;
|
|
302
|
+
const memoryIncrease = finalMemory - initialMemory;
|
|
303
|
+
|
|
304
|
+
// Memory increase should be minimal (allowing for some variance)
|
|
305
|
+
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024); // Less than 10MB
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('should not leak memory with repeated hook mount/unmount', () => {
|
|
309
|
+
const initialMemory = process.memoryUsage().heapUsed;
|
|
310
|
+
|
|
311
|
+
// Mount and unmount hooks repeatedly
|
|
312
|
+
for (let i = 0; i < 100; i++) {
|
|
313
|
+
const { unmount } = renderHook(() => useCache<string>(`test-cache-${i}`));
|
|
314
|
+
|
|
315
|
+
act(() => {
|
|
316
|
+
// Perform some operations
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
unmount();
|
|
320
|
+
|
|
321
|
+
// Force garbage collection if available
|
|
322
|
+
if (global.gc) {
|
|
323
|
+
global.gc();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const finalMemory = process.memoryUsage().heapUsed;
|
|
328
|
+
const memoryIncrease = finalMemory - initialMemory;
|
|
329
|
+
|
|
330
|
+
// Memory increase should be minimal
|
|
331
|
+
expect(memoryIncrease).toBeLessThan(5 * 1024 * 1024); // Less than 5MB
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('Performance Regression Tests', () => {
|
|
336
|
+
test('should maintain performance with large cache sizes', () => {
|
|
337
|
+
const sizes = [100, 1000, 5000, 10000];
|
|
338
|
+
|
|
339
|
+
sizes.forEach(size => {
|
|
340
|
+
const cache = new Cache<string>({ maxSize: size });
|
|
341
|
+
const startTime = performance.now();
|
|
342
|
+
|
|
343
|
+
// Fill cache
|
|
344
|
+
for (let i = 0; i < size; i++) {
|
|
345
|
+
cache.set(`key${i}`, `value${i}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Random access pattern
|
|
349
|
+
for (let i = 0; i < size; i++) {
|
|
350
|
+
const randomIndex = Math.floor(Math.random() * size);
|
|
351
|
+
cache.get(`key${randomIndex}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const endTime = performance.now();
|
|
355
|
+
const duration = endTime - startTime;
|
|
356
|
+
|
|
357
|
+
// Performance should scale reasonably
|
|
358
|
+
const opsPerMs = (size * 2) / duration; // inserts + gets
|
|
359
|
+
expect(opsPerMs).toBeGreaterThan(10); // At least 10 ops per ms
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('should maintain pattern matching performance', () => {
|
|
364
|
+
const patternComplexities = [
|
|
365
|
+
'simple:*',
|
|
366
|
+
'complex:*:pattern:*:here',
|
|
367
|
+
'very:complex:pattern:*:with:many:parts:*:and:sections',
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
patternComplexities.forEach(pattern => {
|
|
371
|
+
const startTime = performance.now();
|
|
372
|
+
|
|
373
|
+
// Test many matches
|
|
374
|
+
for (let i = 0; i < 1000; i++) {
|
|
375
|
+
PatternMatcher.matchesPattern(`test:${i}:value`, pattern);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const endTime = performance.now();
|
|
379
|
+
const duration = endTime - startTime;
|
|
380
|
+
|
|
381
|
+
// Even complex patterns should be fast
|
|
382
|
+
expect(duration).toBeLessThan(50); // Less than 50ms for 1000 matches
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Setup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Mock console methods in test environment
|
|
6
|
+
(global as any).console = {
|
|
7
|
+
...console,
|
|
8
|
+
log: jest.fn(),
|
|
9
|
+
warn: jest.fn(),
|
|
10
|
+
error: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Mock __DEV__ for testing
|
|
14
|
+
(global as any).__DEV__ = true;
|
|
15
|
+
|
|
16
|
+
// Mock timers globally
|
|
17
|
+
jest.useFakeTimers({
|
|
18
|
+
doNotFake: ['nextTick', 'setImmediate']
|
|
19
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Cache
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CacheEntry, CacheConfig, CacheStats, EvictionStrategy } from './types/Cache';
|
|
6
|
+
import { CacheStatsTracker } from './CacheStatsTracker';
|
|
7
|
+
import { PatternMatcher } from './PatternMatcher';
|
|
8
|
+
import { LRUStrategy } from './strategies/LRUStrategy';
|
|
9
|
+
import { LFUStrategy } from './strategies/LFUStrategy';
|
|
10
|
+
import { FIFOStrategy } from './strategies/FIFOStrategy';
|
|
11
|
+
import { TTLStrategy } from './strategies/TTLStrategy';
|
|
12
|
+
|
|
13
|
+
export class Cache<T = unknown> {
|
|
14
|
+
private store = new Map<string, CacheEntry<T>>();
|
|
15
|
+
private config: Required<CacheConfig>;
|
|
16
|
+
private statsTracker = new CacheStatsTracker();
|
|
17
|
+
private strategies = {
|
|
18
|
+
lru: new LRUStrategy<T>(),
|
|
19
|
+
lfu: new LFUStrategy<T>(),
|
|
20
|
+
fifo: new FIFOStrategy<T>(),
|
|
21
|
+
ttl: new TTLStrategy<T>(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
constructor(config: CacheConfig = {}) {
|
|
25
|
+
this.config = {
|
|
26
|
+
maxSize: config.maxSize || 100,
|
|
27
|
+
defaultTTL: config.defaultTTL || 5 * 60 * 1000,
|
|
28
|
+
onEvict: config.onEvict || (() => { }),
|
|
29
|
+
onExpire: config.onExpire || (() => { }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set(key: string, value: T, ttl?: number): void {
|
|
34
|
+
if (this.store.size >= this.config.maxSize && !this.store.has(key)) {
|
|
35
|
+
this.evictOne('lru');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const entry: CacheEntry<T> = {
|
|
39
|
+
value,
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
ttl: ttl || this.config.defaultTTL,
|
|
42
|
+
accessCount: 0,
|
|
43
|
+
lastAccess: Date.now(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.store.set(key, entry);
|
|
47
|
+
this.statsTracker.updateSize(this.store.size);
|
|
48
|
+
|
|
49
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__ && typeof console !== 'undefined' && console.log) {
|
|
50
|
+
console.log(`Cache: Set key "${key}" with TTL ${entry.ttl}ms`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get(key: string): T | undefined {
|
|
55
|
+
const entry = this.store.get(key);
|
|
56
|
+
|
|
57
|
+
if (!entry) {
|
|
58
|
+
this.statsTracker.recordMiss();
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.isExpired(entry)) {
|
|
63
|
+
this.delete(key);
|
|
64
|
+
this.statsTracker.recordMiss();
|
|
65
|
+
this.statsTracker.recordExpiration();
|
|
66
|
+
this.config.onExpire(key, entry);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
entry.accessCount++;
|
|
71
|
+
entry.lastAccess = Date.now();
|
|
72
|
+
this.statsTracker.recordHit();
|
|
73
|
+
return entry.value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
has(key: string): boolean {
|
|
77
|
+
const entry = this.store.get(key);
|
|
78
|
+
if (!entry) return false;
|
|
79
|
+
if (this.isExpired(entry)) {
|
|
80
|
+
this.delete(key);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
delete(key: string): boolean {
|
|
87
|
+
const deleted = this.store.delete(key);
|
|
88
|
+
if (deleted) {
|
|
89
|
+
this.statsTracker.updateSize(this.store.size);
|
|
90
|
+
}
|
|
91
|
+
return deleted;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
invalidatePattern(pattern: string): number {
|
|
95
|
+
const regex = PatternMatcher.convertPatternToRegex(pattern);
|
|
96
|
+
let invalidatedCount = 0;
|
|
97
|
+
|
|
98
|
+
for (const key of this.store.keys()) {
|
|
99
|
+
if (regex.test(key)) {
|
|
100
|
+
this.store.delete(key);
|
|
101
|
+
invalidatedCount++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.statsTracker.updateSize(this.store.size);
|
|
106
|
+
return invalidatedCount;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
clear(): void {
|
|
110
|
+
this.store.clear();
|
|
111
|
+
this.statsTracker.reset();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getStats(): CacheStats {
|
|
115
|
+
return this.statsTracker.getStats();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
keys(): string[] {
|
|
119
|
+
return Array.from(this.store.keys());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private isExpired(entry: CacheEntry<T>): boolean {
|
|
123
|
+
return Date.now() - entry.timestamp > entry.ttl;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private evictOne(strategy: EvictionStrategy): void {
|
|
127
|
+
const evictionStrategy = this.strategies[strategy];
|
|
128
|
+
if (!evictionStrategy) return;
|
|
129
|
+
|
|
130
|
+
const keyToEvict = evictionStrategy.findKeyToEvict(this.store);
|
|
131
|
+
if (keyToEvict) {
|
|
132
|
+
const entry = this.store.get(keyToEvict);
|
|
133
|
+
this.store.delete(keyToEvict);
|
|
134
|
+
this.statsTracker.recordEviction();
|
|
135
|
+
this.statsTracker.updateSize(this.store.size);
|
|
136
|
+
|
|
137
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
138
|
+
console.log(`Cache: Evicted key "${keyToEvict}" using ${strategy} strategy`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (entry) {
|
|
142
|
+
this.config.onEvict(keyToEvict, entry);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Manager
|
|
3
|
+
* Manages multiple cache instances
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Cache } from './Cache';
|
|
7
|
+
import type { CacheConfig } from './types/Cache';
|
|
8
|
+
|
|
9
|
+
export class CacheManager {
|
|
10
|
+
private static instance: CacheManager;
|
|
11
|
+
private caches = new Map<string, Cache<any>>();
|
|
12
|
+
|
|
13
|
+
private constructor() {}
|
|
14
|
+
|
|
15
|
+
static getInstance(): CacheManager {
|
|
16
|
+
if (!CacheManager.instance) {
|
|
17
|
+
CacheManager.instance = new CacheManager();
|
|
18
|
+
}
|
|
19
|
+
return CacheManager.instance;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getCache<T>(name: string, config?: CacheConfig): Cache<T> {
|
|
23
|
+
if (!this.caches.has(name)) {
|
|
24
|
+
this.caches.set(name, new Cache<T>(config));
|
|
25
|
+
}
|
|
26
|
+
return this.caches.get(name)!;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
deleteCache(name: string): boolean {
|
|
30
|
+
const cache = this.caches.get(name);
|
|
31
|
+
if (cache) {
|
|
32
|
+
cache.clear();
|
|
33
|
+
return this.caches.delete(name);
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
clearAll(): void {
|
|
39
|
+
this.caches.forEach((cache) => cache.clear());
|
|
40
|
+
this.caches.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getCacheNames(): string[] {
|
|
44
|
+
return Array.from(this.caches.keys());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const cacheManager = CacheManager.getInstance();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Statistics Tracker
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CacheStats } from './types/Cache';
|
|
6
|
+
|
|
7
|
+
export class CacheStatsTracker {
|
|
8
|
+
private stats: CacheStats = {
|
|
9
|
+
size: 0,
|
|
10
|
+
hits: 0,
|
|
11
|
+
misses: 0,
|
|
12
|
+
evictions: 0,
|
|
13
|
+
expirations: 0,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
recordHit(): void {
|
|
17
|
+
this.stats.hits++;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
recordMiss(): void {
|
|
21
|
+
this.stats.misses++;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
recordEviction(): void {
|
|
25
|
+
this.stats.evictions++;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
recordExpiration(): void {
|
|
29
|
+
this.stats.expirations++;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
updateSize(size: number): void {
|
|
33
|
+
this.stats.size = size;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getStats(): CacheStats {
|
|
37
|
+
return { ...this.stats };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
reset(): void {
|
|
41
|
+
this.stats = {
|
|
42
|
+
size: 0,
|
|
43
|
+
hits: 0,
|
|
44
|
+
misses: 0,
|
|
45
|
+
evictions: 0,
|
|
46
|
+
expirations: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|