@umituz/react-native-storage 2.6.0 → 2.6.1
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 +7 -3
- 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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler for Cache Operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class CacheError extends Error {
|
|
6
|
+
constructor(message: string, public readonly code: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'CacheError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ErrorHandler {
|
|
13
|
+
static handle(error: unknown, context: string): never {
|
|
14
|
+
if (error instanceof CacheError) {
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
throw new CacheError(`${context}: ${error.message}`, 'CACHE_ERROR');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
throw new CacheError(`${context}: Unknown error`, 'UNKNOWN_ERROR');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async withTimeout<T>(
|
|
26
|
+
promise: Promise<T>,
|
|
27
|
+
timeoutMs: number,
|
|
28
|
+
context: string
|
|
29
|
+
): Promise<T> {
|
|
30
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
reject(new CacheError(`${context}: Operation timed out after ${timeoutMs}ms`, 'TIMEOUT'));
|
|
33
|
+
}, timeoutMs);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
this.handle(error, context);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Matcher for Cache Keys
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const regexCache = new Map<string, RegExp>();
|
|
6
|
+
|
|
7
|
+
export class PatternMatcher {
|
|
8
|
+
static convertPatternToRegex(pattern: string): RegExp {
|
|
9
|
+
if (regexCache.has(pattern)) {
|
|
10
|
+
return regexCache.get(pattern)!;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const escapedPattern = pattern
|
|
14
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
15
|
+
.replace(/\*/g, '.*');
|
|
16
|
+
const regex = new RegExp(`^${escapedPattern}$`);
|
|
17
|
+
|
|
18
|
+
regexCache.set(pattern, regex);
|
|
19
|
+
return regex;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static matchesPattern(key: string, pattern: string): boolean {
|
|
23
|
+
const regex = this.convertPatternToRegex(pattern);
|
|
24
|
+
return regex.test(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static clearCache(): void {
|
|
28
|
+
regexCache.clear();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Class Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Cache } from '../Cache';
|
|
6
|
+
import type { CacheConfig } from '../types/Cache';
|
|
7
|
+
|
|
8
|
+
describe('Cache', () => {
|
|
9
|
+
let cache: Cache<string>;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
cache = new Cache<string>({ maxSize: 3, defaultTTL: 1000 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('Basic Operations', () => {
|
|
16
|
+
test('should set and get values', () => {
|
|
17
|
+
cache.set('key1', 'value1');
|
|
18
|
+
expect(cache.get('key1')).toBe('value1');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should return undefined for non-existent keys', () => {
|
|
22
|
+
expect(cache.get('nonexistent')).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should check if key exists', () => {
|
|
26
|
+
cache.set('key1', 'value1');
|
|
27
|
+
expect(cache.has('key1')).toBe(true);
|
|
28
|
+
expect(cache.has('nonexistent')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should delete keys', () => {
|
|
32
|
+
cache.set('key1', 'value1');
|
|
33
|
+
expect(cache.delete('key1')).toBe(true);
|
|
34
|
+
expect(cache.has('key1')).toBe(false);
|
|
35
|
+
expect(cache.delete('nonexistent')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should clear all keys', () => {
|
|
39
|
+
cache.set('key1', 'value1');
|
|
40
|
+
cache.set('key2', 'value2');
|
|
41
|
+
cache.clear();
|
|
42
|
+
expect(cache.has('key1')).toBe(false);
|
|
43
|
+
expect(cache.has('key2')).toBe(false);
|
|
44
|
+
expect(cache.getStats().size).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should return all keys', () => {
|
|
48
|
+
cache.set('key1', 'value1');
|
|
49
|
+
cache.set('key2', 'value2');
|
|
50
|
+
const keys = cache.keys();
|
|
51
|
+
expect(keys).toContain('key1');
|
|
52
|
+
expect(keys).toContain('key2');
|
|
53
|
+
expect(keys).toHaveLength(2);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('TTL (Time To Live)', () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
jest.useFakeTimers();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
jest.useRealTimers();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should expire entries after TTL', () => {
|
|
67
|
+
cache.set('key1', 'value1', 1000);
|
|
68
|
+
expect(cache.get('key1')).toBe('value1');
|
|
69
|
+
|
|
70
|
+
jest.advanceTimersByTime(1001);
|
|
71
|
+
expect(cache.get('key1')).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('should use default TTL when not specified', () => {
|
|
75
|
+
cache.set('key1', 'value1');
|
|
76
|
+
expect(cache.get('key1')).toBe('value1');
|
|
77
|
+
|
|
78
|
+
jest.advanceTimersByTime(1001);
|
|
79
|
+
expect(cache.get('key1')).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should call onExpire callback when entry expires', () => {
|
|
83
|
+
const onExpire = jest.fn();
|
|
84
|
+
const cacheWithCallback = new Cache<string>({
|
|
85
|
+
defaultTTL: 1000,
|
|
86
|
+
onExpire
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
cacheWithCallback.set('key1', 'value1');
|
|
90
|
+
jest.advanceTimersByTime(1001);
|
|
91
|
+
cacheWithCallback.get('key1'); // Trigger expiration check
|
|
92
|
+
|
|
93
|
+
expect(onExpire).toHaveBeenCalledWith('key1', expect.objectContaining({
|
|
94
|
+
value: 'value1',
|
|
95
|
+
timestamp: expect.any(Number),
|
|
96
|
+
ttl: 1000,
|
|
97
|
+
accessCount: 0,
|
|
98
|
+
lastAccess: expect.any(Number)
|
|
99
|
+
}));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('Eviction', () => {
|
|
104
|
+
test('should evict LRU when cache is full', () => {
|
|
105
|
+
const onEvict = jest.fn();
|
|
106
|
+
const cacheWithCallback = new Cache<string>({
|
|
107
|
+
maxSize: 2,
|
|
108
|
+
onEvict
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
cacheWithCallback.set('key1', 'value1');
|
|
112
|
+
cacheWithCallback.set('key2', 'value2');
|
|
113
|
+
|
|
114
|
+
// Access key1 to make it recently used
|
|
115
|
+
cacheWithCallback.get('key1');
|
|
116
|
+
|
|
117
|
+
// Add third item, should evict key2 (least recently used)
|
|
118
|
+
cacheWithCallback.set('key3', 'value3');
|
|
119
|
+
|
|
120
|
+
expect(cacheWithCallback.has('key1')).toBe(true);
|
|
121
|
+
expect(cacheWithCallback.has('key2')).toBe(false);
|
|
122
|
+
expect(cacheWithCallback.has('key3')).toBe(true);
|
|
123
|
+
expect(onEvict).toHaveBeenCalledWith('key2', expect.any(Object));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should not evict when updating existing key', () => {
|
|
127
|
+
cache.set('key1', 'value1');
|
|
128
|
+
cache.set('key2', 'value2');
|
|
129
|
+
cache.set('key3', 'value3');
|
|
130
|
+
|
|
131
|
+
// Update existing key, should not cause eviction
|
|
132
|
+
cache.set('key1', 'value1-updated');
|
|
133
|
+
|
|
134
|
+
expect(cache.has('key1')).toBe(true);
|
|
135
|
+
expect(cache.has('key2')).toBe(true);
|
|
136
|
+
expect(cache.has('key3')).toBe(true);
|
|
137
|
+
expect(cache.get('key1')).toBe('value1-updated');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('Pattern Invalidation', () => {
|
|
142
|
+
test('should invalidate keys matching pattern', () => {
|
|
143
|
+
cache.set('user:1', 'user1');
|
|
144
|
+
cache.set('user:2', 'user2');
|
|
145
|
+
cache.set('post:1', 'post1');
|
|
146
|
+
cache.set('admin', 'admin');
|
|
147
|
+
|
|
148
|
+
const invalidatedCount = cache.invalidatePattern('user:*');
|
|
149
|
+
|
|
150
|
+
expect(invalidatedCount).toBe(2);
|
|
151
|
+
expect(cache.has('user:1')).toBe(false);
|
|
152
|
+
expect(cache.has('user:2')).toBe(false);
|
|
153
|
+
expect(cache.has('post:1')).toBe(true);
|
|
154
|
+
expect(cache.has('admin')).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should handle complex patterns', () => {
|
|
158
|
+
cache.set('user:1:profile', 'profile1');
|
|
159
|
+
cache.set('user:2:profile', 'profile2');
|
|
160
|
+
cache.set('user:1:settings', 'settings1');
|
|
161
|
+
cache.set('post:1', 'post1');
|
|
162
|
+
|
|
163
|
+
const invalidatedCount = cache.invalidatePattern('user:*:profile');
|
|
164
|
+
|
|
165
|
+
expect(invalidatedCount).toBe(2);
|
|
166
|
+
expect(cache.has('user:1:profile')).toBe(false);
|
|
167
|
+
expect(cache.has('user:2:profile')).toBe(false);
|
|
168
|
+
expect(cache.has('user:1:settings')).toBe(true);
|
|
169
|
+
expect(cache.has('post:1')).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('should return 0 for non-matching patterns', () => {
|
|
173
|
+
cache.set('user:1', 'user1');
|
|
174
|
+
cache.set('post:1', 'post1');
|
|
175
|
+
|
|
176
|
+
const invalidatedCount = cache.invalidatePattern('admin:*');
|
|
177
|
+
|
|
178
|
+
expect(invalidatedCount).toBe(0);
|
|
179
|
+
expect(cache.has('user:1')).toBe(true);
|
|
180
|
+
expect(cache.has('post:1')).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Statistics', () => {
|
|
185
|
+
test('should track cache statistics correctly', () => {
|
|
186
|
+
// Initial stats
|
|
187
|
+
let stats = cache.getStats();
|
|
188
|
+
expect(stats.size).toBe(0);
|
|
189
|
+
expect(stats.hits).toBe(0);
|
|
190
|
+
expect(stats.misses).toBe(0);
|
|
191
|
+
expect(stats.evictions).toBe(0);
|
|
192
|
+
expect(stats.expirations).toBe(0);
|
|
193
|
+
|
|
194
|
+
// Set some values
|
|
195
|
+
cache.set('key1', 'value1');
|
|
196
|
+
cache.set('key2', 'value2');
|
|
197
|
+
|
|
198
|
+
stats = cache.getStats();
|
|
199
|
+
expect(stats.size).toBe(2);
|
|
200
|
+
|
|
201
|
+
// Hit
|
|
202
|
+
cache.get('key1');
|
|
203
|
+
stats = cache.getStats();
|
|
204
|
+
expect(stats.hits).toBe(1);
|
|
205
|
+
expect(stats.misses).toBe(0);
|
|
206
|
+
|
|
207
|
+
// Miss
|
|
208
|
+
cache.get('nonexistent');
|
|
209
|
+
stats = cache.getStats();
|
|
210
|
+
expect(stats.hits).toBe(1);
|
|
211
|
+
expect(stats.misses).toBe(1);
|
|
212
|
+
|
|
213
|
+
// Delete
|
|
214
|
+
cache.delete('key1');
|
|
215
|
+
stats = cache.getStats();
|
|
216
|
+
expect(stats.size).toBe(1);
|
|
217
|
+
|
|
218
|
+
// Clear
|
|
219
|
+
cache.clear();
|
|
220
|
+
stats = cache.getStats();
|
|
221
|
+
expect(stats.size).toBe(0);
|
|
222
|
+
expect(stats.hits).toBe(0);
|
|
223
|
+
expect(stats.misses).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('should return immutable stats object', () => {
|
|
227
|
+
cache.set('key1', 'value1');
|
|
228
|
+
const stats1 = cache.getStats();
|
|
229
|
+
const stats2 = cache.getStats();
|
|
230
|
+
|
|
231
|
+
expect(stats1).toEqual(stats2);
|
|
232
|
+
expect(stats1).not.toBe(stats2); // Different references
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Access Count and Last Access', () => {
|
|
237
|
+
test('should track access count and last access time', () => {
|
|
238
|
+
const startTime = Date.now();
|
|
239
|
+
|
|
240
|
+
cache.set('key1', 'value1');
|
|
241
|
+
|
|
242
|
+
// First access
|
|
243
|
+
cache.get('key1');
|
|
244
|
+
let stats = cache.getStats();
|
|
245
|
+
|
|
246
|
+
// Second access
|
|
247
|
+
jest.advanceTimersByTime(100);
|
|
248
|
+
cache.get('key1');
|
|
249
|
+
|
|
250
|
+
// Verify access tracking (internal implementation detail)
|
|
251
|
+
expect(cache.get('key1')).toBe('value1');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('Configuration', () => {
|
|
256
|
+
test('should use custom configuration', () => {
|
|
257
|
+
const customConfig: CacheConfig = {
|
|
258
|
+
maxSize: 10,
|
|
259
|
+
defaultTTL: 5000,
|
|
260
|
+
onEvict: jest.fn(),
|
|
261
|
+
onExpire: jest.fn(),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const customCache = new Cache<string>(customConfig);
|
|
265
|
+
|
|
266
|
+
customCache.set('key1', 'value1');
|
|
267
|
+
expect(customCache.get('key1')).toBe('value1');
|
|
268
|
+
|
|
269
|
+
// Should not expire before custom TTL
|
|
270
|
+
jest.advanceTimersByTime(4000);
|
|
271
|
+
expect(customCache.get('key1')).toBe('value1');
|
|
272
|
+
|
|
273
|
+
// Should expire after custom TTL
|
|
274
|
+
jest.advanceTimersByTime(1001);
|
|
275
|
+
expect(customCache.get('key1')).toBeUndefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('should use default configuration when not provided', () => {
|
|
279
|
+
const defaultCache = new Cache<string>();
|
|
280
|
+
|
|
281
|
+
defaultCache.set('key1', 'value1');
|
|
282
|
+
expect(defaultCache.get('key1')).toBe('value1');
|
|
283
|
+
|
|
284
|
+
// Should use default TTL (5 minutes)
|
|
285
|
+
jest.advanceTimersByTime(5 * 60 * 1000 - 1);
|
|
286
|
+
expect(defaultCache.get('key1')).toBe('value1');
|
|
287
|
+
|
|
288
|
+
jest.advanceTimersByTime(2);
|
|
289
|
+
expect(defaultCache.get('key1')).toBeUndefined();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CacheManager Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CacheManager, cacheManager } from '../CacheManager';
|
|
6
|
+
import type { CacheConfig } from '../types/Cache';
|
|
7
|
+
|
|
8
|
+
describe('CacheManager', () => {
|
|
9
|
+
let manager: CacheManager;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Reset singleton for testing
|
|
13
|
+
(CacheManager as any).instance = null;
|
|
14
|
+
manager = CacheManager.getInstance();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
manager.clearAll();
|
|
19
|
+
(CacheManager as any).instance = null;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('Singleton Pattern', () => {
|
|
23
|
+
test('should return the same instance', () => {
|
|
24
|
+
const instance1 = CacheManager.getInstance();
|
|
25
|
+
const instance2 = CacheManager.getInstance();
|
|
26
|
+
|
|
27
|
+
expect(instance1).toBe(instance2);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should create only one instance', () => {
|
|
31
|
+
const instance1 = CacheManager.getInstance();
|
|
32
|
+
const instance2 = new (CacheManager as any)();
|
|
33
|
+
|
|
34
|
+
expect(instance1).not.toBe(instance2);
|
|
35
|
+
expect(instance1).toBe(CacheManager.getInstance());
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Cache Management', () => {
|
|
40
|
+
test('should create and retrieve cache instances', () => {
|
|
41
|
+
const cache1 = manager.getCache<string>('cache1');
|
|
42
|
+
const cache2 = manager.getCache<number>('cache2');
|
|
43
|
+
|
|
44
|
+
expect(cache1).toBeDefined();
|
|
45
|
+
expect(cache2).toBeDefined();
|
|
46
|
+
expect(cache1).not.toBe(cache2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should return same cache instance for same name', () => {
|
|
50
|
+
const cache1 = manager.getCache<string>('cache1');
|
|
51
|
+
const cache2 = manager.getCache<string>('cache1');
|
|
52
|
+
|
|
53
|
+
expect(cache1).toBe(cache2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should create caches with different types', () => {
|
|
57
|
+
const stringCache = manager.getCache<string>('strings');
|
|
58
|
+
const numberCache = manager.getCache<number>('numbers');
|
|
59
|
+
const objectCache = manager.getCache<{ id: string }>('objects');
|
|
60
|
+
|
|
61
|
+
stringCache.set('key', 'value');
|
|
62
|
+
numberCache.set('key', 42);
|
|
63
|
+
objectCache.set('key', { id: 'test' });
|
|
64
|
+
|
|
65
|
+
expect(stringCache.get('key')).toBe('value');
|
|
66
|
+
expect(numberCache.get('key')).toBe(42);
|
|
67
|
+
expect(objectCache.get('key')).toEqual({ id: 'test' });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should apply configuration to cache', () => {
|
|
71
|
+
const config: CacheConfig = {
|
|
72
|
+
maxSize: 50,
|
|
73
|
+
defaultTTL: 10000,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const cache = manager.getCache<string>('configured-cache', config);
|
|
77
|
+
|
|
78
|
+
cache.set('key', 'value');
|
|
79
|
+
expect(cache.get('key')).toBe('value');
|
|
80
|
+
|
|
81
|
+
// Test configuration is applied (basic check)
|
|
82
|
+
const stats = cache.getStats();
|
|
83
|
+
expect(stats.size).toBe(1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('Cache Deletion', () => {
|
|
88
|
+
test('should delete specific cache', () => {
|
|
89
|
+
manager.getCache<string>('cache1');
|
|
90
|
+
manager.getCache<string>('cache2');
|
|
91
|
+
|
|
92
|
+
expect(manager.getCacheNames()).toContain('cache1');
|
|
93
|
+
expect(manager.getCacheNames()).toContain('cache2');
|
|
94
|
+
|
|
95
|
+
const deleted = manager.deleteCache('cache1');
|
|
96
|
+
|
|
97
|
+
expect(deleted).toBe(true);
|
|
98
|
+
expect(manager.getCacheNames()).not.toContain('cache1');
|
|
99
|
+
expect(manager.getCacheNames()).toContain('cache2');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('should return false when deleting non-existent cache', () => {
|
|
103
|
+
const deleted = manager.deleteCache('nonexistent');
|
|
104
|
+
|
|
105
|
+
expect(deleted).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('should clear cache before deletion', () => {
|
|
109
|
+
const cache = manager.getCache<string>('cache1');
|
|
110
|
+
cache.set('key1', 'value1');
|
|
111
|
+
cache.set('key2', 'value2');
|
|
112
|
+
|
|
113
|
+
expect(cache.getStats().size).toBe(2);
|
|
114
|
+
|
|
115
|
+
manager.deleteCache('cache1');
|
|
116
|
+
|
|
117
|
+
// Get new instance to verify old one was cleared
|
|
118
|
+
const newCache = manager.getCache<string>('cache1');
|
|
119
|
+
expect(newCache.getStats().size).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Clear All', () => {
|
|
124
|
+
test('should clear all caches', () => {
|
|
125
|
+
const cache1 = manager.getCache<string>('cache1');
|
|
126
|
+
const cache2 = manager.getCache<string>('cache2');
|
|
127
|
+
const cache3 = manager.getCache<string>('cache3');
|
|
128
|
+
|
|
129
|
+
cache1.set('key1', 'value1');
|
|
130
|
+
cache2.set('key2', 'value2');
|
|
131
|
+
cache3.set('key3', 'value3');
|
|
132
|
+
|
|
133
|
+
expect(cache1.getStats().size).toBe(1);
|
|
134
|
+
expect(cache2.getStats().size).toBe(1);
|
|
135
|
+
expect(cache3.getStats().size).toBe(1);
|
|
136
|
+
expect(manager.getCacheNames()).toHaveLength(3);
|
|
137
|
+
|
|
138
|
+
manager.clearAll();
|
|
139
|
+
|
|
140
|
+
expect(cache1.getStats().size).toBe(0);
|
|
141
|
+
expect(cache2.getStats().size).toBe(0);
|
|
142
|
+
expect(cache3.getStats().size).toBe(0);
|
|
143
|
+
expect(manager.getCacheNames()).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should handle clear all when no caches exist', () => {
|
|
147
|
+
expect(() => manager.clearAll()).not.toThrow();
|
|
148
|
+
expect(manager.getCacheNames()).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('Cache Names', () => {
|
|
153
|
+
test('should return list of cache names', () => {
|
|
154
|
+
expect(manager.getCacheNames()).toHaveLength(0);
|
|
155
|
+
|
|
156
|
+
manager.getCache<string>('cache1');
|
|
157
|
+
manager.getCache<string>('cache2');
|
|
158
|
+
manager.getCache<string>('cache3');
|
|
159
|
+
|
|
160
|
+
const names = manager.getCacheNames();
|
|
161
|
+
expect(names).toHaveLength(3);
|
|
162
|
+
expect(names).toContain('cache1');
|
|
163
|
+
expect(names).toContain('cache2');
|
|
164
|
+
expect(names).toContain('cache3');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should not duplicate cache names', () => {
|
|
168
|
+
manager.getCache<string>('cache1');
|
|
169
|
+
manager.getCache<string>('cache1'); // Same name
|
|
170
|
+
manager.getCache<string>('cache2');
|
|
171
|
+
|
|
172
|
+
const names = manager.getCacheNames();
|
|
173
|
+
expect(names).toHaveLength(2);
|
|
174
|
+
expect(names.filter(name => name === 'cache1')).toHaveLength(1);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('Memory Management', () => {
|
|
179
|
+
test('should handle large number of caches', () => {
|
|
180
|
+
const cacheCount = 100;
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < cacheCount; i++) {
|
|
183
|
+
const cache = manager.getCache<string>(`cache${i}`);
|
|
184
|
+
cache.set(`key${i}`, `value${i}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
expect(manager.getCacheNames()).toHaveLength(cacheCount);
|
|
188
|
+
|
|
189
|
+
// Verify all caches have their data
|
|
190
|
+
for (let i = 0; i < cacheCount; i++) {
|
|
191
|
+
const cache = manager.getCache<string>(`cache${i}`);
|
|
192
|
+
expect(cache.get(`key${i}`)).toBe(`value${i}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('should handle cache deletion in large scale', () => {
|
|
197
|
+
const cacheCount = 50;
|
|
198
|
+
|
|
199
|
+
// Create caches
|
|
200
|
+
for (let i = 0; i < cacheCount; i++) {
|
|
201
|
+
manager.getCache<string>(`cache${i}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
expect(manager.getCacheNames()).toHaveLength(cacheCount);
|
|
205
|
+
|
|
206
|
+
// Delete every other cache
|
|
207
|
+
for (let i = 0; i < cacheCount; i += 2) {
|
|
208
|
+
manager.deleteCache(`cache${i}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
expect(manager.getCacheNames()).toHaveLength(Math.floor(cacheCount / 2));
|
|
212
|
+
|
|
213
|
+
// Verify remaining caches still work
|
|
214
|
+
for (let i = 1; i < cacheCount; i += 2) {
|
|
215
|
+
const cache = manager.getCache<string>(`cache${i}`);
|
|
216
|
+
cache.set('test', 'value');
|
|
217
|
+
expect(cache.get('test')).toBe('value');
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Edge Cases', () => {
|
|
223
|
+
test('should handle empty string cache name', () => {
|
|
224
|
+
const cache = manager.getCache<string>('');
|
|
225
|
+
expect(cache).toBeDefined();
|
|
226
|
+
expect(manager.getCacheNames()).toContain('');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('should handle special characters in cache names', () => {
|
|
230
|
+
const specialNames = ['cache-with-dash', 'cache_with_underscore', 'cache.with.dots', 'cache with spaces'];
|
|
231
|
+
|
|
232
|
+
specialNames.forEach(name => {
|
|
233
|
+
const cache = manager.getCache<string>(name);
|
|
234
|
+
expect(cache).toBeDefined();
|
|
235
|
+
expect(manager.getCacheNames()).toContain(name);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('should handle very long cache names', () => {
|
|
240
|
+
const longName = 'a'.repeat(1000);
|
|
241
|
+
const cache = manager.getCache<string>(longName);
|
|
242
|
+
|
|
243
|
+
expect(cache).toBeDefined();
|
|
244
|
+
expect(manager.getCacheNames()).toContain(longName);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('cacheManager export', () => {
|
|
250
|
+
beforeEach(() => {
|
|
251
|
+
(CacheManager as any).instance = null;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
afterEach(() => {
|
|
255
|
+
cacheManager.clearAll();
|
|
256
|
+
(CacheManager as any).instance = null;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('should export singleton instance', () => {
|
|
260
|
+
expect(cacheManager).toBeDefined();
|
|
261
|
+
expect(cacheManager).toBeInstanceOf(CacheManager);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('should be the same instance as CacheManager.getInstance()', () => {
|
|
265
|
+
const instance = CacheManager.getInstance();
|
|
266
|
+
expect(cacheManager).toBe(instance);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('should work with exported instance', () => {
|
|
270
|
+
const cache = cacheManager.getCache<string>('test-cache');
|
|
271
|
+
cache.set('key', 'value');
|
|
272
|
+
|
|
273
|
+
expect(cache.get('key')).toBe('value');
|
|
274
|
+
expect(cacheManager.getCacheNames()).toContain('test-cache');
|
|
275
|
+
});
|
|
276
|
+
});
|