@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,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorHandler Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ErrorHandler, CacheError } from '../ErrorHandler';
|
|
6
|
+
|
|
7
|
+
describe('ErrorHandler', () => {
|
|
8
|
+
describe('CacheError', () => {
|
|
9
|
+
test('should create CacheError with message and code', () => {
|
|
10
|
+
const error = new CacheError('Test message', 'TEST_CODE');
|
|
11
|
+
|
|
12
|
+
expect(error).toBeInstanceOf(Error);
|
|
13
|
+
expect(error).toBeInstanceOf(CacheError);
|
|
14
|
+
expect(error.message).toBe('Test message');
|
|
15
|
+
expect(error.code).toBe('TEST_CODE');
|
|
16
|
+
expect(error.name).toBe('CacheError');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should have stack trace', () => {
|
|
20
|
+
const error = new CacheError('Test message', 'TEST_CODE');
|
|
21
|
+
|
|
22
|
+
expect(error.stack).toBeDefined();
|
|
23
|
+
expect(typeof error.stack).toBe('string');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should be serializable', () => {
|
|
27
|
+
const error = new CacheError('Test message', 'TEST_CODE');
|
|
28
|
+
const serialized = JSON.stringify(error);
|
|
29
|
+
const parsed = JSON.parse(serialized);
|
|
30
|
+
|
|
31
|
+
expect(parsed.message).toBe('Test message');
|
|
32
|
+
expect(parsed.code).toBe('TEST_CODE');
|
|
33
|
+
expect(parsed.name).toBe('CacheError');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('handle', () => {
|
|
38
|
+
test('should throw CacheError as-is', () => {
|
|
39
|
+
const originalError = new CacheError('Original error', 'ORIGINAL');
|
|
40
|
+
|
|
41
|
+
expect(() => {
|
|
42
|
+
ErrorHandler.handle(originalError, 'test context');
|
|
43
|
+
}).toThrow('Original error');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should wrap regular Error in CacheError', () => {
|
|
47
|
+
const originalError = new Error('Regular error');
|
|
48
|
+
|
|
49
|
+
expect(() => {
|
|
50
|
+
ErrorHandler.handle(originalError, 'test context');
|
|
51
|
+
}).toThrow(CacheError);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should include context in wrapped error message', () => {
|
|
55
|
+
const originalError = new Error('Regular error');
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
ErrorHandler.handle(originalError, 'test context');
|
|
59
|
+
} catch (error) {
|
|
60
|
+
expect((error as CacheError).message).toBe('test context: Regular error');
|
|
61
|
+
expect((error as CacheError).code).toBe('CACHE_ERROR');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should handle unknown error type', () => {
|
|
66
|
+
const unknownError = 'string error';
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
ErrorHandler.handle(unknownError, 'test context');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
expect((error as CacheError).message).toBe('test context: Unknown error');
|
|
72
|
+
expect((error as CacheError).code).toBe('UNKNOWN_ERROR');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should handle null error', () => {
|
|
77
|
+
try {
|
|
78
|
+
ErrorHandler.handle(null, 'test context');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
expect((error as CacheError).message).toBe('test context: Unknown error');
|
|
81
|
+
expect((error as CacheError).code).toBe('UNKNOWN_ERROR');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('should handle undefined error', () => {
|
|
86
|
+
try {
|
|
87
|
+
ErrorHandler.handle(undefined, 'test context');
|
|
88
|
+
} catch (error) {
|
|
89
|
+
expect((error as CacheError).message).toBe('test context: Unknown error');
|
|
90
|
+
expect((error as CacheError).code).toBe('UNKNOWN_ERROR');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should preserve original error properties when possible', () => {
|
|
95
|
+
const originalError = new Error('Original error');
|
|
96
|
+
(originalError as any).customProperty = 'custom value';
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
ErrorHandler.handle(originalError, 'test context');
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const cacheError = error as CacheError;
|
|
102
|
+
expect(cacheError.message).toBe('test context: Original error');
|
|
103
|
+
expect(cacheError.code).toBe('CACHE_ERROR');
|
|
104
|
+
// Note: original properties are not copied to maintain clean error structure
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('withTimeout', () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
jest.useFakeTimers();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
jest.useRealTimers();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should resolve promise before timeout', async () => {
|
|
119
|
+
const promise = Promise.resolve('success');
|
|
120
|
+
const result = await ErrorHandler.withTimeout(promise, 1000, 'test context');
|
|
121
|
+
|
|
122
|
+
expect(result).toBe('success');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('should timeout when promise takes too long', async () => {
|
|
126
|
+
const promise = new Promise(resolve => setTimeout(() => resolve('late'), 2000));
|
|
127
|
+
|
|
128
|
+
await expect(
|
|
129
|
+
ErrorHandler.withTimeout(promise, 1000, 'test context')
|
|
130
|
+
).rejects.toThrow(CacheError);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('should include timeout in error message', async () => {
|
|
134
|
+
const promise = new Promise(resolve => setTimeout(() => resolve('late'), 2000));
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await ErrorHandler.withTimeout(promise, 1000, 'test context');
|
|
138
|
+
} catch (error) {
|
|
139
|
+
expect((error as CacheError).message).toBe('test context: Operation timed out after 1000ms');
|
|
140
|
+
expect((error as CacheError).code).toBe('TIMEOUT');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('should handle promise rejection before timeout', async () => {
|
|
145
|
+
const promise = Promise.reject(new Error('Promise rejected'));
|
|
146
|
+
|
|
147
|
+
await expect(
|
|
148
|
+
ErrorHandler.withTimeout(promise, 1000, 'test context')
|
|
149
|
+
).rejects.toThrow(CacheError);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('should include context in promise rejection error', async () => {
|
|
153
|
+
const promise = Promise.reject(new Error('Promise rejected'));
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await ErrorHandler.withTimeout(promise, 1000, 'test context');
|
|
157
|
+
} catch (error) {
|
|
158
|
+
expect((error as CacheError).message).toBe('test context: Promise rejected');
|
|
159
|
+
expect((error as CacheError).code).toBe('CACHE_ERROR');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should handle zero timeout', async () => {
|
|
164
|
+
const promise = new Promise(resolve => setTimeout(() => resolve('success'), 100));
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
ErrorHandler.withTimeout(promise, 0, 'test context')
|
|
168
|
+
).rejects.toThrow(CacheError);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should handle negative timeout', async () => {
|
|
172
|
+
const promise = Promise.resolve('success');
|
|
173
|
+
|
|
174
|
+
// Negative timeout should trigger immediate timeout
|
|
175
|
+
await expect(
|
|
176
|
+
ErrorHandler.withTimeout(promise, -1000, 'test context')
|
|
177
|
+
).rejects.toThrow(CacheError);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('should cleanup timeout when promise resolves', async () => {
|
|
181
|
+
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
|
|
182
|
+
const promise = Promise.resolve('success');
|
|
183
|
+
|
|
184
|
+
await ErrorHandler.withTimeout(promise, 1000, 'test context');
|
|
185
|
+
|
|
186
|
+
// Should clear timeout after promise resolves
|
|
187
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
188
|
+
|
|
189
|
+
clearTimeoutSpy.mockRestore();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('should handle multiple concurrent timeouts', async () => {
|
|
193
|
+
const promise1 = Promise.resolve('success1');
|
|
194
|
+
const promise2 = Promise.resolve('success2');
|
|
195
|
+
const promise3 = new Promise(resolve => setTimeout(() => resolve('success3'), 2000));
|
|
196
|
+
|
|
197
|
+
const results = await Promise.allSettled([
|
|
198
|
+
ErrorHandler.withTimeout(promise1, 1000, 'context1'),
|
|
199
|
+
ErrorHandler.withTimeout(promise2, 1000, 'context2'),
|
|
200
|
+
ErrorHandler.withTimeout(promise3, 1000, 'context3'),
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
expect(results[0].status).toBe('fulfilled');
|
|
204
|
+
expect(results[1].status).toBe('fulfilled');
|
|
205
|
+
expect(results[2].status).toBe('rejected');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Integration with Cache Operations', () => {
|
|
210
|
+
test('should handle cache operation errors', () => {
|
|
211
|
+
const mockOperation = () => {
|
|
212
|
+
throw new Error('Cache operation failed');
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
expect(() => {
|
|
216
|
+
ErrorHandler.handle(mockOperation(), 'cache.set');
|
|
217
|
+
}).toThrow('cache.set: Cache operation failed');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should handle async cache operation errors', async () => {
|
|
221
|
+
const mockAsyncOperation = async () => {
|
|
222
|
+
throw new Error('Async cache operation failed');
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
await expect(
|
|
226
|
+
ErrorHandler.withTimeout(mockAsyncOperation(), 1000, 'cache.get')
|
|
227
|
+
).rejects.toThrow('cache.get: Async cache operation failed');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('should handle network timeout scenarios', async () => {
|
|
231
|
+
const mockNetworkCall = new Promise(resolve =>
|
|
232
|
+
setTimeout(() => resolve({ data: 'response' }), 5000)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
await expect(
|
|
236
|
+
ErrorHandler.withTimeout(mockNetworkCall, 1000, 'api.fetch')
|
|
237
|
+
).rejects.toThrow('api.fetch: Operation timed out after 1000ms');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('Error Code Constants', () => {
|
|
242
|
+
test('should use consistent error codes', () => {
|
|
243
|
+
const scenarios = [
|
|
244
|
+
{ error: new Error('test'), expectedCode: 'CACHE_ERROR' },
|
|
245
|
+
{ error: 'string', expectedCode: 'UNKNOWN_ERROR' },
|
|
246
|
+
{ error: null, expectedCode: 'UNKNOWN_ERROR' },
|
|
247
|
+
{ error: undefined, expectedCode: 'UNKNOWN_ERROR' },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
scenarios.forEach(({ error, expectedCode }) => {
|
|
251
|
+
try {
|
|
252
|
+
ErrorHandler.handle(error, 'test');
|
|
253
|
+
} catch (caught) {
|
|
254
|
+
expect((caught as CacheError).code).toBe(expectedCode);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('should use TIMEOUT code for timeout errors', async () => {
|
|
260
|
+
const promise = new Promise(resolve => setTimeout(() => resolve('late'), 2000));
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await ErrorHandler.withTimeout(promise, 1000, 'test');
|
|
264
|
+
} catch (error) {
|
|
265
|
+
expect((error as CacheError).code).toBe('TIMEOUT');
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('Edge Cases', () => {
|
|
271
|
+
test('should handle empty context string', () => {
|
|
272
|
+
const error = new Error('test error');
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
ErrorHandler.handle(error, '');
|
|
276
|
+
} catch (caught) {
|
|
277
|
+
expect((caught as CacheError).message).toBe(': test error');
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('should handle very long context string', () => {
|
|
282
|
+
const longContext = 'context'.repeat(1000);
|
|
283
|
+
const error = new Error('test error');
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
ErrorHandler.handle(error, longContext);
|
|
287
|
+
} catch (caught) {
|
|
288
|
+
expect((caught as CacheError).message).toBe(`${longContext}: test error`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('should handle special characters in context', () => {
|
|
293
|
+
const context = 'context-with-special-chars-!@#$%^&*()';
|
|
294
|
+
const error = new Error('test error');
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
ErrorHandler.handle(error, context);
|
|
298
|
+
} catch (caught) {
|
|
299
|
+
expect((caught as CacheError).message).toBe(`${context}: test error`);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PatternMatcher Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PatternMatcher } from '../PatternMatcher';
|
|
6
|
+
|
|
7
|
+
describe('PatternMatcher', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
PatternMatcher.clearCache();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('convertPatternToRegex', () => {
|
|
13
|
+
test('should handle simple patterns', () => {
|
|
14
|
+
const regex = PatternMatcher.convertPatternToRegex('user:*');
|
|
15
|
+
expect(regex.test('user:1')).toBe(true);
|
|
16
|
+
expect(regex.test('user:123')).toBe(true);
|
|
17
|
+
expect(regex.test('user:abc')).toBe(true);
|
|
18
|
+
expect(regex.test('admin:1')).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should handle multiple wildcards', () => {
|
|
22
|
+
const regex = PatternMatcher.convertPatternToRegex('*:*:*');
|
|
23
|
+
expect(regex.test('user:1:profile')).toBe(true);
|
|
24
|
+
expect(regex.test('post:2:comments')).toBe(true);
|
|
25
|
+
expect(regex.test('user:1')).toBe(false);
|
|
26
|
+
expect(regex.test('user')).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should handle exact matches', () => {
|
|
30
|
+
const regex = PatternMatcher.convertPatternToRegex('exact-key');
|
|
31
|
+
expect(regex.test('exact-key')).toBe(true);
|
|
32
|
+
expect(regex.test('exact-key-123')).toBe(false);
|
|
33
|
+
expect(regex.test('exact')).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should handle patterns with special characters', () => {
|
|
37
|
+
const regex = PatternMatcher.convertPatternToRegex('user.*profile');
|
|
38
|
+
expect(regex.test('user.123.profile')).toBe(true);
|
|
39
|
+
expect(regex.test('user.abc.profile')).toBe(true);
|
|
40
|
+
expect(regex.test('user.profile')).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should escape regex special characters', () => {
|
|
44
|
+
const regex = PatternMatcher.convertPatternToRegex('user.+?^${}()|[]\\');
|
|
45
|
+
expect(regex.test('user.+?^${}()|[]\\')).toBe(true);
|
|
46
|
+
expect(regex.test('user-something')).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should handle empty pattern', () => {
|
|
50
|
+
const regex = PatternMatcher.convertPatternToRegex('');
|
|
51
|
+
expect(regex.test('')).toBe(true);
|
|
52
|
+
expect(regex.test('anything')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should handle pattern with only wildcards', () => {
|
|
56
|
+
const regex = PatternMatcher.convertPatternToRegex('*');
|
|
57
|
+
expect(regex.test('anything')).toBe(true);
|
|
58
|
+
expect(regex.test('')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should handle complex patterns', () => {
|
|
62
|
+
const regex = PatternMatcher.convertPatternToRegex('cache:*:data:*');
|
|
63
|
+
expect(regex.test('cache:user:data:123')).toBe(true);
|
|
64
|
+
expect(regex.test('cache:post:data:456')).toBe(true);
|
|
65
|
+
expect(regex.test('cache:user:meta:123')).toBe(false);
|
|
66
|
+
expect(regex.test('user:data:123')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should handle patterns with dots and dashes', () => {
|
|
70
|
+
const regex = PatternMatcher.convertPatternToRegex('module.*-service.*');
|
|
71
|
+
expect(regex.test('module.auth-service.v1')).toBe(true);
|
|
72
|
+
expect(regex.test('module.user-service.v2')).toBe(true);
|
|
73
|
+
expect(regex.test('module.auth')).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('matchesPattern', () => {
|
|
78
|
+
test('should return true for matching patterns', () => {
|
|
79
|
+
expect(PatternMatcher.matchesPattern('user:1', 'user:*')).toBe(true);
|
|
80
|
+
expect(PatternMatcher.matchesPattern('post:123:comments', '*:*:*')).toBe(true);
|
|
81
|
+
expect(PatternMatcher.matchesPattern('exact', 'exact')).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should return false for non-matching patterns', () => {
|
|
85
|
+
expect(PatternMatcher.matchesPattern('admin:1', 'user:*')).toBe(false);
|
|
86
|
+
expect(PatternMatcher.matchesPattern('user:1', '*:*:*')).toBe(false);
|
|
87
|
+
expect(PatternMatcher.matchesPattern('exact', 'different')).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('should handle case-sensitive matching', () => {
|
|
91
|
+
expect(PatternMatcher.matchesPattern('User:1', 'user:*')).toBe(false);
|
|
92
|
+
expect(PatternMatcher.matchesPattern('user:1', 'User:*')).toBe(false);
|
|
93
|
+
expect(PatternMatcher.matchesPattern('USER:1', 'USER:*')).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('should handle empty strings', () => {
|
|
97
|
+
expect(PatternMatcher.matchesPattern('', '')).toBe(true);
|
|
98
|
+
expect(PatternMatcher.matchesPattern('', '*')).toBe(true);
|
|
99
|
+
expect(PatternMatcher.matchesPattern('test', '')).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('Regex Caching', () => {
|
|
104
|
+
test('should cache converted regex patterns', () => {
|
|
105
|
+
const pattern = 'user:*';
|
|
106
|
+
|
|
107
|
+
const regex1 = PatternMatcher.convertPatternToRegex(pattern);
|
|
108
|
+
const regex2 = PatternMatcher.convertPatternToRegex(pattern);
|
|
109
|
+
|
|
110
|
+
expect(regex1).toBe(regex2); // Same reference
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should create different regex for different patterns', () => {
|
|
114
|
+
const regex1 = PatternMatcher.convertPatternToRegex('user:*');
|
|
115
|
+
const regex2 = PatternMatcher.convertPatternToRegex('post:*');
|
|
116
|
+
|
|
117
|
+
expect(regex1).not.toBe(regex2); // Different references
|
|
118
|
+
expect(regex1.test('user:1')).toBe(true);
|
|
119
|
+
expect(regex1.test('post:1')).toBe(false);
|
|
120
|
+
expect(regex2.test('post:1')).toBe(true);
|
|
121
|
+
expect(regex2.test('user:1')).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('should clear cache', () => {
|
|
125
|
+
const pattern = 'user:*';
|
|
126
|
+
|
|
127
|
+
const regex1 = PatternMatcher.convertPatternToRegex(pattern);
|
|
128
|
+
PatternMatcher.clearCache();
|
|
129
|
+
const regex2 = PatternMatcher.convertPatternToRegex(pattern);
|
|
130
|
+
|
|
131
|
+
expect(regex1).not.toBe(regex2); // Different references after clear
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('should maintain cache performance', () => {
|
|
135
|
+
const pattern = 'very:complex:pattern:*:with:many:parts:*';
|
|
136
|
+
|
|
137
|
+
// First call - should create new regex
|
|
138
|
+
const start1 = performance.now();
|
|
139
|
+
const regex1 = PatternMatcher.convertPatternToRegex(pattern);
|
|
140
|
+
const end1 = performance.now();
|
|
141
|
+
|
|
142
|
+
// Second call - should use cached regex
|
|
143
|
+
const start2 = performance.now();
|
|
144
|
+
const regex2 = PatternMatcher.convertPatternToRegex(pattern);
|
|
145
|
+
const end2 = performance.now();
|
|
146
|
+
|
|
147
|
+
expect(regex1).toBe(regex2);
|
|
148
|
+
// Second call should be faster (though this might not always be true in tests)
|
|
149
|
+
expect(end2 - start2).toBeLessThanOrEqual(end1 - start1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('Edge Cases', () => {
|
|
154
|
+
test('should handle very long patterns', () => {
|
|
155
|
+
const longPattern = 'a'.repeat(1000) + '*';
|
|
156
|
+
const longKey = 'a'.repeat(1000) + 'suffix';
|
|
157
|
+
|
|
158
|
+
expect(PatternMatcher.matchesPattern(longKey, longPattern)).toBe(true);
|
|
159
|
+
expect(PatternMatcher.matchesPattern('a'.repeat(999), longPattern)).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('should handle patterns with only special characters', () => {
|
|
163
|
+
const regex = PatternMatcher.convertPatternToRegex('.+?^${}()|[]\\');
|
|
164
|
+
expect(regex.test('.+?^${}()|[]\\')).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should handle Unicode characters', () => {
|
|
168
|
+
expect(PatternMatcher.matchesPattern('üser:1', 'üser:*')).toBe(true);
|
|
169
|
+
expect(PatternMatcher.matchesPattern('用户:1', '用户:*')).toBe(true);
|
|
170
|
+
expect(PatternMatcher.matchesPattern('🚀:launch', '🚀:*')).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should handle null and undefined inputs gracefully', () => {
|
|
174
|
+
expect(() => {
|
|
175
|
+
PatternMatcher.convertPatternToRegex(null as any);
|
|
176
|
+
}).toThrow();
|
|
177
|
+
|
|
178
|
+
expect(() => {
|
|
179
|
+
PatternMatcher.convertPatternToRegex(undefined as any);
|
|
180
|
+
}).toThrow();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('should handle non-string inputs', () => {
|
|
184
|
+
expect(() => {
|
|
185
|
+
PatternMatcher.convertPatternToRegex(123 as any);
|
|
186
|
+
}).toThrow();
|
|
187
|
+
|
|
188
|
+
expect(() => {
|
|
189
|
+
PatternMatcher.convertPatternToRegex({} as any);
|
|
190
|
+
}).toThrow();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Performance Considerations', () => {
|
|
195
|
+
test('should handle large number of pattern matches efficiently', () => {
|
|
196
|
+
const pattern = 'cache:*:data:*';
|
|
197
|
+
const keys = Array.from({ length: 1000 }, (_, i) => `cache:${i}:data:${i * 2}`);
|
|
198
|
+
|
|
199
|
+
const start = performance.now();
|
|
200
|
+
|
|
201
|
+
keys.forEach(key => {
|
|
202
|
+
PatternMatcher.matchesPattern(key, pattern);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const end = performance.now();
|
|
206
|
+
|
|
207
|
+
// Should complete within reasonable time (adjust threshold as needed)
|
|
208
|
+
expect(end - start).toBeLessThan(100); // 100ms
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('should reuse cached regex for many matches', () => {
|
|
212
|
+
const pattern = 'user:*';
|
|
213
|
+
const keys = Array.from({ length: 1000 }, (_, i) => `user:${i}`);
|
|
214
|
+
|
|
215
|
+
// Pre-cache the regex
|
|
216
|
+
PatternMatcher.convertPatternToRegex(pattern);
|
|
217
|
+
|
|
218
|
+
const start = performance.now();
|
|
219
|
+
|
|
220
|
+
keys.forEach(key => {
|
|
221
|
+
PatternMatcher.matchesPattern(key, pattern);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const end = performance.now();
|
|
225
|
+
|
|
226
|
+
// Should be faster with cached regex
|
|
227
|
+
expect(end - start).toBeLessThan(50); // 50ms
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('Real-world Scenarios', () => {
|
|
232
|
+
test('should handle common cache key patterns', () => {
|
|
233
|
+
const scenarios = [
|
|
234
|
+
{ key: 'user:123:profile', pattern: 'user:*:profile', expected: true },
|
|
235
|
+
{ key: 'user:123:settings', pattern: 'user:*:profile', expected: false },
|
|
236
|
+
{ key: 'post:456:comments:789', pattern: 'post:*:comments:*', expected: true },
|
|
237
|
+
{ key: 'post:456:likes', pattern: 'post:*:comments:*', expected: false },
|
|
238
|
+
{ key: 'session:abc123', pattern: 'session:*', expected: true },
|
|
239
|
+
{ key: 'cache:api:user:123', pattern: 'cache:api:*', expected: true },
|
|
240
|
+
{ key: 'cache:db:user:123', pattern: 'cache:api:*', expected: false },
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
scenarios.forEach(({ key, pattern, expected }) => {
|
|
244
|
+
expect(PatternMatcher.matchesPattern(key, pattern)).toBe(expected);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('should handle API endpoint patterns', () => {
|
|
249
|
+
const apiPatterns = [
|
|
250
|
+
{ endpoint: '/api/v1/users/123', pattern: '/api/v1/users/*', expected: true },
|
|
251
|
+
{ endpoint: '/api/v1/posts/456/comments', pattern: '/api/v1/posts/*/comments', expected: true },
|
|
252
|
+
{ endpoint: '/api/v2/users/123', pattern: '/api/v1/users/*', expected: false },
|
|
253
|
+
{ endpoint: '/api/v1/users', pattern: '/api/v1/users/*', expected: false },
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
apiPatterns.forEach(({ endpoint, pattern, expected }) => {
|
|
257
|
+
expect(PatternMatcher.matchesPattern(endpoint, pattern)).toBe(expected);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIFO (First In First Out) Eviction Strategy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EvictionStrategy } from './EvictionStrategy';
|
|
6
|
+
import type { CacheEntry } from '../types/Cache';
|
|
7
|
+
|
|
8
|
+
export class FIFOStrategy<T> implements EvictionStrategy<T> {
|
|
9
|
+
findKeyToEvict(entries: Map<string, CacheEntry<T>>): string | undefined {
|
|
10
|
+
return entries.keys().next().value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LFU (Least Frequently Used) Eviction Strategy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EvictionStrategy } from './EvictionStrategy';
|
|
6
|
+
import type { CacheEntry } from '../types/Cache';
|
|
7
|
+
|
|
8
|
+
export class LFUStrategy<T> implements EvictionStrategy<T> {
|
|
9
|
+
findKeyToEvict(entries: Map<string, CacheEntry<T>>): string | undefined {
|
|
10
|
+
let least: string | undefined;
|
|
11
|
+
let leastCount = Infinity;
|
|
12
|
+
|
|
13
|
+
for (const [key, entry] of entries.entries()) {
|
|
14
|
+
if (entry.accessCount < leastCount) {
|
|
15
|
+
leastCount = entry.accessCount;
|
|
16
|
+
least = key;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return least;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU (Least Recently Used) Eviction Strategy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EvictionStrategy } from './EvictionStrategy';
|
|
6
|
+
import type { CacheEntry } from '../types/Cache';
|
|
7
|
+
|
|
8
|
+
export class LRUStrategy<T> implements EvictionStrategy<T> {
|
|
9
|
+
findKeyToEvict(entries: Map<string, CacheEntry<T>>): string | undefined {
|
|
10
|
+
let oldest: string | undefined;
|
|
11
|
+
let oldestTime = Infinity;
|
|
12
|
+
|
|
13
|
+
for (const [key, entry] of entries.entries()) {
|
|
14
|
+
if (entry.lastAccess < oldestTime) {
|
|
15
|
+
oldestTime = entry.lastAccess;
|
|
16
|
+
oldest = key;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return oldest;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTL (Time To Live) Eviction Strategy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EvictionStrategy } from './EvictionStrategy';
|
|
6
|
+
import type { CacheEntry } from '../types/Cache';
|
|
7
|
+
|
|
8
|
+
export class TTLStrategy<T> implements EvictionStrategy<T> {
|
|
9
|
+
findKeyToEvict(entries: Map<string, CacheEntry<T>>): string | undefined {
|
|
10
|
+
let nearest: string | undefined;
|
|
11
|
+
let nearestExpiry = Infinity;
|
|
12
|
+
|
|
13
|
+
for (const [key, entry] of entries.entries()) {
|
|
14
|
+
const expiry = entry.timestamp + entry.ttl;
|
|
15
|
+
if (expiry < nearestExpiry) {
|
|
16
|
+
nearestExpiry = expiry;
|
|
17
|
+
nearest = key;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return nearest;
|
|
22
|
+
}
|
|
23
|
+
}
|