@umituz/react-native-storage 2.0.0 → 2.3.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 +23 -7
- package/src/__tests__/integration.test.ts +391 -0
- package/src/__tests__/mocks/asyncStorage.mock.ts +52 -0
- package/src/__tests__/performance.test.ts +351 -0
- package/src/__tests__/setup.ts +63 -0
- package/src/application/ports/IStorageRepository.ts +0 -12
- package/src/domain/entities/StorageResult.ts +1 -3
- package/src/domain/entities/__tests__/CachedValue.test.ts +149 -0
- package/src/domain/entities/__tests__/StorageResult.test.ts +122 -0
- package/src/domain/errors/StorageError.ts +0 -2
- package/src/domain/errors/__tests__/StorageError.test.ts +127 -0
- package/src/domain/utils/__tests__/devUtils.test.ts +97 -0
- package/src/domain/utils/devUtils.ts +37 -0
- package/src/domain/value-objects/StorageKey.ts +27 -29
- package/src/index.ts +9 -1
- package/src/infrastructure/adapters/StorageService.ts +8 -6
- package/src/infrastructure/repositories/AsyncStorageRepository.ts +27 -108
- package/src/infrastructure/repositories/BaseStorageOperations.ts +101 -0
- package/src/infrastructure/repositories/BatchStorageOperations.ts +42 -0
- package/src/infrastructure/repositories/StringStorageOperations.ts +44 -0
- package/src/infrastructure/repositories/__tests__/AsyncStorageRepository.test.ts +169 -0
- package/src/infrastructure/repositories/__tests__/BaseStorageOperations.test.ts +200 -0
- package/src/presentation/hooks/CacheStorageOperations.ts +95 -0
- package/src/presentation/hooks/__tests__/usePersistentCache.test.ts +404 -0
- package/src/presentation/hooks/__tests__/useStorage.test.ts +246 -0
- package/src/presentation/hooks/__tests__/useStorageState.test.ts +292 -0
- package/src/presentation/hooks/useCacheState.ts +55 -0
- package/src/presentation/hooks/usePersistentCache.ts +30 -39
- package/src/presentation/hooks/useStorage.ts +4 -3
- package/src/presentation/hooks/useStorageState.ts +24 -8
- package/src/presentation/hooks/useStore.ts +3 -1
- package/src/types/global.d.ts +40 -0
- package/LICENSE +0 -22
- package/src/presentation/hooks/usePersistedState.ts +0 -34
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Tests
|
|
3
|
+
*
|
|
4
|
+
* Performance and memory leak tests
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
8
|
+
import { useStorage, useStorageState, usePersistentCache } from '../../index';
|
|
9
|
+
import { AsyncStorage } from '../mocks/asyncStorage.mock';
|
|
10
|
+
import { mockPerformance, trackMemoryUsage } from '../setup';
|
|
11
|
+
|
|
12
|
+
describe('Performance Tests', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
(AsyncStorage as any).__clear();
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
mockPerformance();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('Memory Leak Prevention', () => {
|
|
20
|
+
it('should not leak memory with multiple hook instances', () => {
|
|
21
|
+
const memoryTracker = trackMemoryUsage();
|
|
22
|
+
|
|
23
|
+
// Create many hook instances
|
|
24
|
+
const hooks = Array.from({ length: 100 }, (_, i) =>
|
|
25
|
+
renderHook(() => useStorage())
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Add listeners
|
|
29
|
+
hooks.forEach(hook => {
|
|
30
|
+
hook.current.getItem(`key-${i}`, 'default');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const listenerCount = memoryTracker.getListenerCount();
|
|
34
|
+
expect(listenerCount).toBeLessThan(50); // Should be much less than 100
|
|
35
|
+
|
|
36
|
+
// Cleanup
|
|
37
|
+
hooks.forEach(hook => hook.unmount());
|
|
38
|
+
memoryTracker.cleanup();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should cleanup on unmount', () => {
|
|
42
|
+
const memoryTracker = trackMemoryUsage();
|
|
43
|
+
|
|
44
|
+
const { result, unmount } = renderHook(() => useStorageState('test', 'default'));
|
|
45
|
+
|
|
46
|
+
// Add some async operations
|
|
47
|
+
const promise = result.current[1]('test-value');
|
|
48
|
+
|
|
49
|
+
// Unmount before promise resolves
|
|
50
|
+
unmount();
|
|
51
|
+
|
|
52
|
+
const listenerCount = memoryTracker.getListenerCount();
|
|
53
|
+
expect(listenerCount).toBe(0);
|
|
54
|
+
|
|
55
|
+
memoryTracker.cleanup();
|
|
56
|
+
return promise;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should not create multiple CacheStorageOperations instances', () => {
|
|
60
|
+
const { result: hook1 } = renderHook(() =>
|
|
61
|
+
usePersistentCache('key1')
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const { result: hook2 } = renderHook(() =>
|
|
65
|
+
usePersistentCache('key2')
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const { result: hook3 } = renderHook(() =>
|
|
69
|
+
usePersistentCache('key3')
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// All should use the same instance (singleton pattern)
|
|
73
|
+
expect(hook1.current.setData).toBe(hook2.current.setData);
|
|
74
|
+
expect(hook2.current.setData).toBe(hook3.current.setData);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('Performance Benchmarks', () => {
|
|
79
|
+
it('should handle large datasets efficiently', async () => {
|
|
80
|
+
const { result: storageHook } = renderHook(() => useStorage());
|
|
81
|
+
|
|
82
|
+
// Create large dataset (10,000 items)
|
|
83
|
+
const largeData = Array.from({ length: 10000 }, (_, i) => ({
|
|
84
|
+
id: i,
|
|
85
|
+
name: `Item ${i}`,
|
|
86
|
+
description: `Description for item ${i}`,
|
|
87
|
+
metadata: {
|
|
88
|
+
created: Date.now(),
|
|
89
|
+
tags: [`tag${i % 10}`, `category${i % 5}`],
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const startTime = performance.now();
|
|
94
|
+
|
|
95
|
+
// Set large data
|
|
96
|
+
const setSuccess = await storageHook.current.setItem('large-data', largeData);
|
|
97
|
+
expect(setSuccess).toBe(true);
|
|
98
|
+
|
|
99
|
+
// Get large data
|
|
100
|
+
const retrievedData = await storageHook.current.getItem('large-data', []);
|
|
101
|
+
expect(retrievedData).toEqual(largeData);
|
|
102
|
+
|
|
103
|
+
const endTime = performance.now();
|
|
104
|
+
const duration = endTime - startTime;
|
|
105
|
+
|
|
106
|
+
// Should complete within reasonable time (less than 1 second)
|
|
107
|
+
expect(duration).toBeLessThan(1000);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle rapid operations efficiently', async () => {
|
|
111
|
+
const { result: storageHook } = renderHook(() => useStorage());
|
|
112
|
+
|
|
113
|
+
const iterations = 1000;
|
|
114
|
+
const startTime = performance.now();
|
|
115
|
+
|
|
116
|
+
// Rapid set/get operations
|
|
117
|
+
for (let i = 0; i < iterations; i++) {
|
|
118
|
+
const key = `rapid-key-${i}`;
|
|
119
|
+
const value = `rapid-value-${i}`;
|
|
120
|
+
|
|
121
|
+
await storageHook.current.setItem(key, value);
|
|
122
|
+
const retrieved = await storageHook.current.getItem(key, '');
|
|
123
|
+
expect(retrieved).toBe(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const endTime = performance.now();
|
|
127
|
+
const duration = endTime - startTime;
|
|
128
|
+
const avgTime = duration / iterations;
|
|
129
|
+
|
|
130
|
+
// Average operation should be fast (less than 1ms per operation)
|
|
131
|
+
expect(avgTime).toBeLessThan(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle concurrent operations efficiently', async () => {
|
|
135
|
+
const { result: storageHook } = renderHook(() => useStorage());
|
|
136
|
+
|
|
137
|
+
const concurrentOperations = 100;
|
|
138
|
+
const operations = Array.from({ length: concurrentOperations }, (_, i) =>
|
|
139
|
+
storageHook.current.setItem(`concurrent-key-${i}`, `concurrent-value-${i}`)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const startTime = performance.now();
|
|
143
|
+
|
|
144
|
+
// Execute all operations concurrently
|
|
145
|
+
const results = await Promise.all(operations);
|
|
146
|
+
|
|
147
|
+
const endTime = performance.now();
|
|
148
|
+
const duration = endTime - startTime;
|
|
149
|
+
|
|
150
|
+
// All operations should succeed
|
|
151
|
+
expect(results.every(success => success)).toBe(true);
|
|
152
|
+
|
|
153
|
+
// Concurrent operations should be faster than sequential
|
|
154
|
+
expect(duration).toBeLessThan(concurrentOperations * 10); // Less than 10ms per operation
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('Cache Performance', () => {
|
|
159
|
+
it('should handle cache operations efficiently', async () => {
|
|
160
|
+
const { result: cacheHook } = renderHook(() =>
|
|
161
|
+
usePersistentCache('performance-cache')
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const cacheOperations = 1000;
|
|
165
|
+
const startTime = performance.now();
|
|
166
|
+
|
|
167
|
+
// Rapid cache operations
|
|
168
|
+
for (let i = 0; i < cacheOperations; i++) {
|
|
169
|
+
const data = { id: i, value: `cache-value-${i}` };
|
|
170
|
+
|
|
171
|
+
await cacheHook.current.setData(data);
|
|
172
|
+
expect(cacheHook.current.data).toEqual(data);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const endTime = performance.now();
|
|
176
|
+
const duration = endTime - startTime;
|
|
177
|
+
const avgTime = duration / cacheOperations;
|
|
178
|
+
|
|
179
|
+
// Cache operations should be very fast
|
|
180
|
+
expect(avgTime).toBeLessThan(0.1); // Less than 0.1ms per operation
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should handle TTL checks efficiently', async () => {
|
|
184
|
+
const { result: cacheHook } = renderHook(() =>
|
|
185
|
+
usePersistentCache('ttl-cache', { ttl: 60000 })
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const data = { test: 'value' };
|
|
189
|
+
await cacheHook.current.setData(data);
|
|
190
|
+
|
|
191
|
+
const startTime = performance.now();
|
|
192
|
+
|
|
193
|
+
// Check TTL many times (should be cached)
|
|
194
|
+
for (let i = 0; i < 10000; i++) {
|
|
195
|
+
// Access isExpired property
|
|
196
|
+
const isExpired = cacheHook.current.isExpired;
|
|
197
|
+
expect(isExpired).toBe(false);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const endTime = performance.now();
|
|
201
|
+
const duration = endTime - startTime;
|
|
202
|
+
|
|
203
|
+
// TTL checks should be extremely fast
|
|
204
|
+
expect(duration).toBeLessThan(10); // Less than 10ms for 10,000 checks
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('State Performance', () => {
|
|
209
|
+
it('should handle state updates efficiently', async () => {
|
|
210
|
+
const { result: stateHook, waitForNextUpdate } = renderHook(() =>
|
|
211
|
+
useStorageState('performance-state', 'initial')
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await act(async () => {
|
|
215
|
+
await waitForNextUpdate();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const stateUpdates = 1000;
|
|
219
|
+
const startTime = performance.now();
|
|
220
|
+
|
|
221
|
+
// Rapid state updates
|
|
222
|
+
for (let i = 0; i < stateUpdates; i++) {
|
|
223
|
+
await act(async () => {
|
|
224
|
+
await stateHook.current[1](`update-${i}`);
|
|
225
|
+
});
|
|
226
|
+
expect(stateHook.current[0]).toBe(`update-${i}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const endTime = performance.now();
|
|
230
|
+
const duration = endTime - startTime;
|
|
231
|
+
const avgTime = duration / stateUpdates;
|
|
232
|
+
|
|
233
|
+
// State updates should be fast
|
|
234
|
+
expect(avgTime).toBeLessThan(1); // Less than 1ms per update
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should not re-render unnecessarily', async () => {
|
|
238
|
+
let renderCount = 0;
|
|
239
|
+
|
|
240
|
+
const TestComponent = () => {
|
|
241
|
+
renderCount++;
|
|
242
|
+
const [state] = useStorageState('render-test', 'default');
|
|
243
|
+
return null; // Don't render anything
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const { rerender } = renderHook(() => <TestComponent />);
|
|
247
|
+
|
|
248
|
+
const initialRenders = renderCount;
|
|
249
|
+
|
|
250
|
+
// Rerender with same props
|
|
251
|
+
rerender(<TestComponent />);
|
|
252
|
+
|
|
253
|
+
// Should not cause additional renders
|
|
254
|
+
expect(renderCount).toBe(initialRenders);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('Memory Usage', () => {
|
|
259
|
+
it('should maintain stable memory usage', async () => {
|
|
260
|
+
const { result: storageHook } = renderHook(() => useStorage());
|
|
261
|
+
|
|
262
|
+
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
|
263
|
+
|
|
264
|
+
// Create and destroy many objects
|
|
265
|
+
for (let i = 0; i < 1000; i++) {
|
|
266
|
+
const largeObject = {
|
|
267
|
+
id: i,
|
|
268
|
+
data: new Array(1000).fill(`data-${i}`),
|
|
269
|
+
nested: {
|
|
270
|
+
level1: { level2: { level3: new Array(100).fill(`nested-${i}`) } },
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
await storageHook.current.setItem(`temp-${i}`, largeObject);
|
|
275
|
+
await storageHook.current.removeItem(`temp-${i}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Force garbage collection if available
|
|
279
|
+
if (global.gc) {
|
|
280
|
+
global.gc();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
|
284
|
+
const memoryIncrease = finalMemory - initialMemory;
|
|
285
|
+
|
|
286
|
+
// Memory increase should be minimal
|
|
287
|
+
expect(memoryIncrease).toBeLessThan(1024 * 1024); // Less than 1MB increase
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should cleanup event listeners', () => {
|
|
291
|
+
const memoryTracker = trackMemoryUsage();
|
|
292
|
+
|
|
293
|
+
// Create and destroy many hooks
|
|
294
|
+
for (let i = 0; i < 100; i++) {
|
|
295
|
+
const { unmount } = renderHook(() => useStorageState(`cleanup-${i}`, 'default'));
|
|
296
|
+
unmount();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const listenerCount = memoryTracker.getListenerCount();
|
|
300
|
+
expect(listenerCount).toBe(0);
|
|
301
|
+
|
|
302
|
+
memoryTracker.cleanup();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Scalability Tests', () => {
|
|
307
|
+
it('should handle many keys efficiently', async () => {
|
|
308
|
+
const { result: storageHook } = renderHook(() => useStorage());
|
|
309
|
+
|
|
310
|
+
const keyCount = 10000;
|
|
311
|
+
const startTime = performance.now();
|
|
312
|
+
|
|
313
|
+
// Create many keys
|
|
314
|
+
for (let i = 0; i < keyCount; i++) {
|
|
315
|
+
await storageHook.current.setItem(`scale-key-${i}`, `scale-value-${i}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const endTime = performance.now();
|
|
319
|
+
const duration = endTime - startTime;
|
|
320
|
+
|
|
321
|
+
// Should handle many keys efficiently
|
|
322
|
+
expect(duration).toBeLessThan(keyCount * 0.1); // Less than 0.1ms per key
|
|
323
|
+
|
|
324
|
+
// Verify all keys exist
|
|
325
|
+
for (let i = 0; i < keyCount; i++) {
|
|
326
|
+
const exists = await storageHook.current.hasItem(`scale-key-${i}`);
|
|
327
|
+
expect(exists).toBe(true);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should handle large values efficiently', async () => {
|
|
332
|
+
const { result: storageHook } = renderHook(() => useStorage());
|
|
333
|
+
|
|
334
|
+
// Create very large value (1MB string)
|
|
335
|
+
const largeValue = 'x'.repeat(1024 * 1024);
|
|
336
|
+
const startTime = performance.now();
|
|
337
|
+
|
|
338
|
+
const setSuccess = await storageHook.current.setItem('large-value', largeValue);
|
|
339
|
+
expect(setSuccess).toBe(true);
|
|
340
|
+
|
|
341
|
+
const retrievedValue = await storageHook.current.getString('large-value', '');
|
|
342
|
+
expect(retrievedValue).toBe(largeValue);
|
|
343
|
+
|
|
344
|
+
const endTime = performance.now();
|
|
345
|
+
const duration = endTime - startTime;
|
|
346
|
+
|
|
347
|
+
// Large values should be handled efficiently
|
|
348
|
+
expect(duration).toBeLessThan(100); // Less than 100ms for 1MB
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Setup
|
|
3
|
+
*
|
|
4
|
+
* Global test configuration and mocks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import 'jest-environment-jsdom';
|
|
8
|
+
|
|
9
|
+
// Mock __DEV__ global
|
|
10
|
+
Object.defineProperty(globalThis, '__DEV__', {
|
|
11
|
+
value: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Mock AsyncStorage
|
|
16
|
+
jest.mock('@react-native-async-storage/async-storage', () => ({
|
|
17
|
+
default: {
|
|
18
|
+
getItem: jest.fn(),
|
|
19
|
+
setItem: jest.fn(),
|
|
20
|
+
removeItem: jest.fn(),
|
|
21
|
+
clear: jest.fn(),
|
|
22
|
+
getAllKeys: jest.fn(),
|
|
23
|
+
multiGet: jest.fn(),
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Mock console for __DEV__ checks
|
|
28
|
+
const originalConsole = { ...console };
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
// Restore console after each test
|
|
36
|
+
Object.assign(console, originalConsole);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Performance test utilities
|
|
40
|
+
export const mockPerformance = () => {
|
|
41
|
+
const mockPerformance = {
|
|
42
|
+
now: jest.fn(() => Date.now()),
|
|
43
|
+
mark: jest.fn(),
|
|
44
|
+
measure: jest.fn(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
globalThis.performance = mockPerformance as any;
|
|
48
|
+
return mockPerformance;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Memory leak test utilities
|
|
52
|
+
export const trackMemoryUsage = () => {
|
|
53
|
+
const listeners = new Set<() => void>();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
addListener: (listener: () => void) => {
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
return () => listeners.delete(listener);
|
|
59
|
+
},
|
|
60
|
+
getListenerCount: () => listeners.size,
|
|
61
|
+
cleanup: () => listeners.clear(),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Domain-Driven Design: Application port for storage operations
|
|
5
5
|
* Infrastructure layer implements this interface
|
|
6
|
-
*
|
|
7
|
-
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import type { StorageResult } from '../../domain/entities/StorageResult';
|
|
@@ -59,15 +57,5 @@ export interface IStorageRepository {
|
|
|
59
57
|
*/
|
|
60
58
|
getAllKeys(): Promise<StorageResult<string[]>>;
|
|
61
59
|
|
|
62
|
-
/**
|
|
63
|
-
* Get object from storage (alias for getItem for backward compatibility)
|
|
64
|
-
* @deprecated Use getItem instead
|
|
65
|
-
*/
|
|
66
|
-
getObject<T>(key: string, defaultValue: T): Promise<StorageResult<T>>;
|
|
67
60
|
|
|
68
|
-
/**
|
|
69
|
-
* Set object in storage (alias for setItem for backward compatibility)
|
|
70
|
-
* @deprecated Use setItem instead
|
|
71
|
-
*/
|
|
72
|
-
setObject<T>(key: string, value: T): Promise<StorageResult<T>>;
|
|
73
61
|
}
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Domain-Driven Design: Entity representing storage operation result
|
|
5
5
|
* Functional programming pattern for error handling (Result type)
|
|
6
|
-
*
|
|
7
|
-
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import type { StorageError } from '../errors/StorageError';
|
|
@@ -68,5 +66,5 @@ export const map = <T, U>(
|
|
|
68
66
|
if (isSuccess(result)) {
|
|
69
67
|
return success(fn(result.data));
|
|
70
68
|
}
|
|
71
|
-
return failure(result.error
|
|
69
|
+
return failure(result.error);
|
|
72
70
|
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CachedValue Entity Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for CachedValue entity
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createCachedValue,
|
|
9
|
+
isCacheExpired,
|
|
10
|
+
getRemainingTTL,
|
|
11
|
+
getCacheAge
|
|
12
|
+
} from '../../domain/entities/CachedValue';
|
|
13
|
+
|
|
14
|
+
describe('CachedValue Entity', () => {
|
|
15
|
+
const mockNow = 1000000;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.spyOn(Date, 'now').mockReturnValue(mockNow);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
jest.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('createCachedValue', () => {
|
|
26
|
+
it('should create cached value with default values', () => {
|
|
27
|
+
const data = { test: 'value' };
|
|
28
|
+
const cached = createCachedValue(data);
|
|
29
|
+
|
|
30
|
+
expect(cached.value).toBe(data);
|
|
31
|
+
expect(cached.timestamp).toBe(mockNow);
|
|
32
|
+
expect(cached.ttl).toBeDefined();
|
|
33
|
+
expect(cached.version).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should create cached value with custom TTL', () => {
|
|
37
|
+
const data = { test: 'value' };
|
|
38
|
+
const ttl = 60000; // 1 minute
|
|
39
|
+
const cached = createCachedValue(data, ttl);
|
|
40
|
+
|
|
41
|
+
expect(cached.value).toBe(data);
|
|
42
|
+
expect(cached.timestamp).toBe(mockNow);
|
|
43
|
+
expect(cached.ttl).toBe(ttl);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should create cached value with version', () => {
|
|
47
|
+
const data = { test: 'value' };
|
|
48
|
+
const version = 2;
|
|
49
|
+
const cached = createCachedValue(data, undefined, version);
|
|
50
|
+
|
|
51
|
+
expect(cached.value).toBe(data);
|
|
52
|
+
expect(cached.timestamp).toBe(mockNow);
|
|
53
|
+
expect(cached.version).toBe(version);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('isCacheExpired', () => {
|
|
58
|
+
it('should return false for fresh cache', () => {
|
|
59
|
+
const data = { test: 'value' };
|
|
60
|
+
const ttl = 60000; // 1 minute
|
|
61
|
+
const cached = createCachedValue(data, ttl);
|
|
62
|
+
|
|
63
|
+
expect(isCacheExpired(cached)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return true for expired cache', () => {
|
|
67
|
+
const data = { test: 'value' };
|
|
68
|
+
const ttl = 1000; // 1 second
|
|
69
|
+
const cached = createCachedValue(data, ttl, mockNow - 2000); // Created 2 seconds ago
|
|
70
|
+
|
|
71
|
+
expect(isCacheExpired(cached)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle version mismatch', () => {
|
|
75
|
+
const data = { test: 'value' };
|
|
76
|
+
const cached = createCachedValue(data, 60000, 1);
|
|
77
|
+
const currentVersion = 2;
|
|
78
|
+
|
|
79
|
+
expect(isCacheExpired(cached, currentVersion)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return false when version matches', () => {
|
|
83
|
+
const data = { test: 'value' };
|
|
84
|
+
const cached = createCachedValue(data, 60000, 1);
|
|
85
|
+
const currentVersion = 1;
|
|
86
|
+
|
|
87
|
+
expect(isCacheExpired(cached, currentVersion)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('getRemainingTTL', () => {
|
|
92
|
+
it('should return remaining TTL for fresh cache', () => {
|
|
93
|
+
const data = { test: 'value' };
|
|
94
|
+
const ttl = 60000; // 1 minute
|
|
95
|
+
const cached = createCachedValue(data, ttl, mockNow - 30000); // Created 30 seconds ago
|
|
96
|
+
|
|
97
|
+
const remaining = getRemainingTTL(cached);
|
|
98
|
+
expect(remaining).toBe(30000); // 30 seconds remaining
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return 0 for expired cache', () => {
|
|
102
|
+
const data = { test: 'value' };
|
|
103
|
+
const ttl = 1000; // 1 second
|
|
104
|
+
const cached = createCachedValue(data, ttl, mockNow - 2000); // Created 2 seconds ago
|
|
105
|
+
|
|
106
|
+
const remaining = getRemainingTTL(cached);
|
|
107
|
+
expect(remaining).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle missing TTL', () => {
|
|
111
|
+
const data = { test: 'value' };
|
|
112
|
+
const cached = createCachedValue(data);
|
|
113
|
+
|
|
114
|
+
const remaining = getRemainingTTL(cached);
|
|
115
|
+
expect(remaining).toBeGreaterThan(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('getCacheAge', () => {
|
|
120
|
+
it('should return correct age', () => {
|
|
121
|
+
const data = { test: 'value' };
|
|
122
|
+
const age = 30000; // 30 seconds
|
|
123
|
+
const cached = createCachedValue(data, undefined, undefined, mockNow - age);
|
|
124
|
+
|
|
125
|
+
const cacheAge = getCacheAge(cached);
|
|
126
|
+
expect(cacheAge).toBe(age);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return 0 for new cache', () => {
|
|
130
|
+
const data = { test: 'value' };
|
|
131
|
+
const cached = createCachedValue(data);
|
|
132
|
+
|
|
133
|
+
const cacheAge = getCacheAge(cached);
|
|
134
|
+
expect(cacheAge).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('Immutability', () => {
|
|
139
|
+
it('should create immutable cached value', () => {
|
|
140
|
+
const data = { test: 'value' };
|
|
141
|
+
const cached = createCachedValue(data);
|
|
142
|
+
|
|
143
|
+
// Attempt to modify
|
|
144
|
+
(cached as any).value = { modified: true };
|
|
145
|
+
|
|
146
|
+
expect(cached.value).toEqual(data);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|