@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
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Hooks Integration Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Mock React for testing environment
|
|
6
|
+
const mockReact = require('react');
|
|
7
|
+
|
|
8
|
+
// Simple mock for renderHook
|
|
9
|
+
const renderHook = (hook: Function) => {
|
|
10
|
+
const result = { current: hook() };
|
|
11
|
+
|
|
12
|
+
const rerender = () => {
|
|
13
|
+
result.current = hook();
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const unmount = () => {
|
|
17
|
+
// Cleanup logic would go here
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return { result, rerender, unmount };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Mock act function
|
|
24
|
+
const act = (callback: Function) => {
|
|
25
|
+
callback();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
import { useCache } from '../useCache';
|
|
29
|
+
import { useCachedValue } from '../useCachedValue';
|
|
30
|
+
import { cacheManager } from '../../domain/CacheManager';
|
|
31
|
+
|
|
32
|
+
describe('React Hooks Integration', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
cacheManager.clearAll();
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('useCache', () => {
|
|
39
|
+
test('should provide cache operations', () => {
|
|
40
|
+
const { result } = renderHook(() => useCache<string>('test-cache'));
|
|
41
|
+
|
|
42
|
+
expect(result.current).toHaveProperty('set');
|
|
43
|
+
expect(result.current).toHaveProperty('get');
|
|
44
|
+
expect(result.current).toHaveProperty('has');
|
|
45
|
+
expect(result.current).toHaveProperty('remove');
|
|
46
|
+
expect(result.current).toHaveProperty('clear');
|
|
47
|
+
expect(result.current).toHaveProperty('invalidatePattern');
|
|
48
|
+
expect(result.current).toHaveProperty('getStats');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should set and get values', () => {
|
|
52
|
+
const { result } = renderHook(() => useCache<string>('test-cache'));
|
|
53
|
+
|
|
54
|
+
act(() => {
|
|
55
|
+
result.current.set('key1', 'value1');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result.current.get('key1')).toBe('value1');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should check if key exists', () => {
|
|
62
|
+
const { result } = renderHook(() => useCache<string>('test-cache'));
|
|
63
|
+
|
|
64
|
+
expect(result.current.has('key1')).toBe(false);
|
|
65
|
+
|
|
66
|
+
act(() => {
|
|
67
|
+
result.current.set('key1', 'value1');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(result.current.has('key1')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should remove keys', () => {
|
|
74
|
+
const { result } = renderHook(() => useCache<string>('test-cache'));
|
|
75
|
+
|
|
76
|
+
act(() => {
|
|
77
|
+
result.current.set('key1', 'value1');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result.current.has('key1')).toBe(true);
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
const removed = result.current.remove('key1');
|
|
84
|
+
expect(removed).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.current.has('key1')).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('should clear all keys', () => {
|
|
91
|
+
const { result } = renderHook(() => useCache<string>('test-cache'));
|
|
92
|
+
|
|
93
|
+
act(() => {
|
|
94
|
+
result.current.set('key1', 'value1');
|
|
95
|
+
result.current.set('key2', 'value2');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.current.getStats().size).toBe(2);
|
|
99
|
+
|
|
100
|
+
act(() => {
|
|
101
|
+
result.current.clear();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result.current.getStats().size).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should invalidate patterns', () => {
|
|
108
|
+
const { result } = renderHook(() => useCache<string>('test-cache'));
|
|
109
|
+
|
|
110
|
+
act(() => {
|
|
111
|
+
result.current.set('user:1', 'user1');
|
|
112
|
+
result.current.set('user:2', 'user2');
|
|
113
|
+
result.current.set('post:1', 'post1');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
act(() => {
|
|
117
|
+
const count = result.current.invalidatePattern('user:*');
|
|
118
|
+
expect(count).toBe(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result.current.has('user:1')).toBe(false);
|
|
122
|
+
expect(result.current.has('user:2')).toBe(false);
|
|
123
|
+
expect(result.current.has('post:1')).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should get cache statistics', () => {
|
|
127
|
+
const { result } = renderHook(() => useCache<string>('test-cache'));
|
|
128
|
+
|
|
129
|
+
const initialStats = result.current.getStats();
|
|
130
|
+
expect(initialStats.size).toBe(0);
|
|
131
|
+
expect(initialStats.hits).toBe(0);
|
|
132
|
+
expect(initialStats.misses).toBe(0);
|
|
133
|
+
|
|
134
|
+
act(() => {
|
|
135
|
+
result.current.set('key1', 'value1');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const afterSetStats = result.current.getStats();
|
|
139
|
+
expect(afterSetStats.size).toBe(1);
|
|
140
|
+
|
|
141
|
+
act(() => {
|
|
142
|
+
result.current.get('key1'); // hit
|
|
143
|
+
result.current.get('nonexistent'); // miss
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const afterAccessStats = result.current.getStats();
|
|
147
|
+
expect(afterAccessStats.hits).toBe(1);
|
|
148
|
+
expect(afterAccessStats.misses).toBe(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should use custom cache configuration', () => {
|
|
152
|
+
const { result } = renderHook(() =>
|
|
153
|
+
useCache<string>('custom-cache', { maxSize: 5, defaultTTL: 2000 })
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
act(() => {
|
|
157
|
+
result.current.set('key1', 'value1');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(result.current.get('key1')).toBe('value1');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should maintain separate caches for different names', () => {
|
|
164
|
+
const { result: result1 } = renderHook(() => useCache<string>('cache1'));
|
|
165
|
+
const { result: result2 } = renderHook(() => useCache<number>('cache2'));
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
result1.current.set('key', 'string-value');
|
|
169
|
+
result2.current.set('key', 42);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result1.current.get('key')).toBe('string-value');
|
|
173
|
+
expect(result2.current.get('key')).toBe(42);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('should handle rapid operations without memory leaks', () => {
|
|
177
|
+
const { result } = renderHook(() => useCache<string>('rapid-cache'));
|
|
178
|
+
|
|
179
|
+
// Perform many operations rapidly
|
|
180
|
+
for (let i = 0; i < 100; i++) {
|
|
181
|
+
act(() => {
|
|
182
|
+
result.current.set(`key${i}`, `value${i}`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
expect(result.current.getStats().size).toBe(100);
|
|
187
|
+
|
|
188
|
+
// Clear many operations rapidly
|
|
189
|
+
for (let i = 0; i < 100; i++) {
|
|
190
|
+
act(() => {
|
|
191
|
+
result.current.remove(`key${i}`);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
expect(result.current.getStats().size).toBe(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('useCachedValue', () => {
|
|
200
|
+
test('should load and cache value', async () => {
|
|
201
|
+
const mockFetcher = jest.fn().mockResolvedValue('fetched-value');
|
|
202
|
+
|
|
203
|
+
const { result } = renderHook(() =>
|
|
204
|
+
useCachedValue('test-cache', 'test-key', mockFetcher)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result.current.isLoading).toBe(true);
|
|
208
|
+
expect(result.current.value).toBeUndefined();
|
|
209
|
+
expect(result.current.error).toBe(null);
|
|
210
|
+
|
|
211
|
+
await act(async () => {
|
|
212
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(result.current.isLoading).toBe(false);
|
|
216
|
+
expect(result.current.value).toBe('fetched-value');
|
|
217
|
+
expect(result.current.error).toBe(null);
|
|
218
|
+
expect(mockFetcher).toHaveBeenCalledTimes(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('should use cached value on subsequent renders', async () => {
|
|
222
|
+
const mockFetcher = jest.fn().mockResolvedValue('fetched-value');
|
|
223
|
+
|
|
224
|
+
const { result, rerender } = renderHook(() =>
|
|
225
|
+
useCachedValue('test-cache', 'test-key', mockFetcher)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Initial load
|
|
229
|
+
await act(async () => {
|
|
230
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(mockFetcher).toHaveBeenCalledTimes(1);
|
|
234
|
+
|
|
235
|
+
// Rerender - should use cache
|
|
236
|
+
rerender();
|
|
237
|
+
|
|
238
|
+
expect(result.current.value).toBe('fetched-value');
|
|
239
|
+
expect(mockFetcher).toHaveBeenCalledTimes(1); // Still only called once
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('should handle fetcher errors', async () => {
|
|
243
|
+
const mockError = new Error('Fetch failed');
|
|
244
|
+
const mockFetcher = jest.fn().mockRejectedValue(mockError);
|
|
245
|
+
|
|
246
|
+
const { result } = renderHook(() =>
|
|
247
|
+
useCachedValue('test-cache', 'test-key', mockFetcher)
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
await act(async () => {
|
|
251
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(result.current.isLoading).toBe(false);
|
|
255
|
+
expect(result.current.value).toBeUndefined();
|
|
256
|
+
expect(result.current.error).toBe(mockError);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('should invalidate cached value', async () => {
|
|
260
|
+
const mockFetcher = jest.fn().mockResolvedValue('initial-value');
|
|
261
|
+
|
|
262
|
+
const { result } = renderHook(() =>
|
|
263
|
+
useCachedValue('test-cache', 'test-key', mockFetcher)
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Initial load
|
|
267
|
+
await act(async () => {
|
|
268
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(result.current.value).toBe('initial-value');
|
|
272
|
+
|
|
273
|
+
// Invalidate
|
|
274
|
+
act(() => {
|
|
275
|
+
result.current.invalidate();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(result.current.value).toBeUndefined();
|
|
279
|
+
|
|
280
|
+
// Should fetch again on next render
|
|
281
|
+
mockFetcher.mockResolvedValue('new-value');
|
|
282
|
+
|
|
283
|
+
await act(async () => {
|
|
284
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(result.current.value).toBe('new-value');
|
|
288
|
+
expect(mockFetcher).toHaveBeenCalledTimes(2);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('should invalidate pattern', async () => {
|
|
292
|
+
const mockFetcher = jest.fn().mockResolvedValue('value');
|
|
293
|
+
|
|
294
|
+
const { result } = renderHook(() =>
|
|
295
|
+
useCachedValue('test-cache', 'user:123', mockFetcher)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Load initial value
|
|
299
|
+
await act(async () => {
|
|
300
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(result.current.value).toBe('value');
|
|
304
|
+
|
|
305
|
+
// Invalidate pattern
|
|
306
|
+
act(() => {
|
|
307
|
+
const count = result.current.invalidatePattern('user:*');
|
|
308
|
+
expect(count).toBe(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(result.current.value).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('should refetch manually', async () => {
|
|
315
|
+
const mockFetcher = jest.fn()
|
|
316
|
+
.mockResolvedValueOnce('initial-value')
|
|
317
|
+
.mockResolvedValueOnce('refetched-value');
|
|
318
|
+
|
|
319
|
+
const { result } = renderHook(() =>
|
|
320
|
+
useCachedValue('test-cache', 'test-key', mockFetcher)
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Initial load
|
|
324
|
+
await act(async () => {
|
|
325
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(result.current.value).toBe('initial-value');
|
|
329
|
+
expect(mockFetcher).toHaveBeenCalledTimes(1);
|
|
330
|
+
|
|
331
|
+
// Refetch
|
|
332
|
+
act(() => {
|
|
333
|
+
result.current.refetch();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await act(async () => {
|
|
337
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(result.current.value).toBe('refetched-value');
|
|
341
|
+
expect(mockFetcher).toHaveBeenCalledTimes(2);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('should use custom TTL', async () => {
|
|
345
|
+
jest.useFakeTimers();
|
|
346
|
+
|
|
347
|
+
const mockFetcher = jest.fn().mockResolvedValue('value');
|
|
348
|
+
|
|
349
|
+
const { result } = renderHook(() =>
|
|
350
|
+
useCachedValue('test-cache', 'test-key', mockFetcher, { ttl: 1000 })
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Load initial value
|
|
354
|
+
await act(async () => {
|
|
355
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(result.current.value).toBe('value');
|
|
359
|
+
expect(mockFetcher).toHaveBeenCalledTimes(1);
|
|
360
|
+
|
|
361
|
+
// Fast forward past TTL
|
|
362
|
+
jest.advanceTimersByTime(1001);
|
|
363
|
+
|
|
364
|
+
// Should fetch again
|
|
365
|
+
await act(async () => {
|
|
366
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(mockFetcher).toHaveBeenCalledTimes(2);
|
|
370
|
+
|
|
371
|
+
jest.useRealTimers();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('should handle dependency changes', async () => {
|
|
375
|
+
const mockFetcher1 = jest.fn().mockResolvedValue('value1');
|
|
376
|
+
const mockFetcher2 = jest.fn().mockResolvedValue('value2');
|
|
377
|
+
|
|
378
|
+
const { result, rerender } = renderHook(
|
|
379
|
+
({ fetcher }) => useCachedValue('test-cache', 'test-key', fetcher),
|
|
380
|
+
{ initialProps: { fetcher: mockFetcher1 } }
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Initial load
|
|
384
|
+
await act(async () => {
|
|
385
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(result.current.value).toBe('value1');
|
|
389
|
+
expect(mockFetcher1).toHaveBeenCalledTimes(1);
|
|
390
|
+
|
|
391
|
+
// Change fetcher
|
|
392
|
+
rerender({ fetcher: mockFetcher2 });
|
|
393
|
+
|
|
394
|
+
await act(async () => {
|
|
395
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(result.current.value).toBe('value2');
|
|
399
|
+
expect(mockFetcher2).toHaveBeenCalledTimes(1);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('should handle concurrent requests', async () => {
|
|
403
|
+
let resolveCount = 0;
|
|
404
|
+
const mockFetcher = jest.fn(() => {
|
|
405
|
+
return new Promise(resolve => {
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
resolveCount++;
|
|
408
|
+
resolve(`value-${resolveCount}`);
|
|
409
|
+
}, 100);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const { result } = renderHook(() =>
|
|
414
|
+
useCachedValue('test-cache', 'test-key', mockFetcher)
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Trigger multiple rapid renders
|
|
418
|
+
act(() => {});
|
|
419
|
+
act(() => {});
|
|
420
|
+
act(() => {});
|
|
421
|
+
|
|
422
|
+
// Wait for completion
|
|
423
|
+
await act(async () => {
|
|
424
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Should only call fetcher once despite multiple renders
|
|
428
|
+
expect(mockFetcher).toHaveBeenCalledTimes(1);
|
|
429
|
+
expect(result.current.value).toBe('value-1');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('should cleanup on unmount', async () => {
|
|
433
|
+
const mockFetcher = jest.fn().mockResolvedValue('value');
|
|
434
|
+
|
|
435
|
+
const { unmount } = renderHook(() =>
|
|
436
|
+
useCachedValue('test-cache', 'test-key', mockFetcher)
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// Start loading
|
|
440
|
+
expect(result.current.isLoading).toBe(true);
|
|
441
|
+
|
|
442
|
+
// Unmount while loading
|
|
443
|
+
unmount();
|
|
444
|
+
|
|
445
|
+
// Complete the fetch after unmount
|
|
446
|
+
await act(async () => {
|
|
447
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Should not cause errors
|
|
451
|
+
expect(true).toBe(true);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe('Hook Integration', () => {
|
|
456
|
+
test('should work together with useCache and useCachedValue', async () => {
|
|
457
|
+
const mockFetcher = jest.fn().mockResolvedValue('fetched-value');
|
|
458
|
+
|
|
459
|
+
const { result: cacheResult } = renderHook(() => useCache<string>('shared-cache'));
|
|
460
|
+
const { result: valueResult } = renderHook(() =>
|
|
461
|
+
useCachedValue('shared-cache', 'shared-key', mockFetcher)
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
// Load value through useCachedValue
|
|
465
|
+
await act(async () => {
|
|
466
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(valueResult.current.value).toBe('fetched-value');
|
|
470
|
+
|
|
471
|
+
// Value should be accessible through useCache
|
|
472
|
+
expect(cacheResult.current.get('shared-key')).toBe('fetched-value');
|
|
473
|
+
|
|
474
|
+
// Invalidate through useCache
|
|
475
|
+
act(() => {
|
|
476
|
+
cacheResult.current.invalidatePattern('*');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
expect(valueResult.current.value).toBeUndefined();
|
|
480
|
+
expect(cacheResult.current.has('shared-key')).toBe(false);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('should handle multiple hooks with same cache', async () => {
|
|
484
|
+
const mockFetcher1 = jest.fn().mockResolvedValue('value1');
|
|
485
|
+
const mockFetcher2 = jest.fn().mockResolvedValue('value2');
|
|
486
|
+
|
|
487
|
+
const { result: result1 } = renderHook(() =>
|
|
488
|
+
useCachedValue('shared-cache', 'key1', mockFetcher1)
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const { result: result2 } = renderHook(() =>
|
|
492
|
+
useCachedValue('shared-cache', 'key2', mockFetcher2)
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Load both values
|
|
496
|
+
await act(async () => {
|
|
497
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(result1.current.value).toBe('value1');
|
|
501
|
+
expect(result2.current.value).toBe('value2');
|
|
502
|
+
|
|
503
|
+
// Invalidate one should not affect the other
|
|
504
|
+
act(() => {
|
|
505
|
+
result1.current.invalidate();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
expect(result1.current.value).toBeUndefined();
|
|
509
|
+
expect(result2.current.value).toBe('value2'); // Should remain
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCache Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useRef, useState } from 'react';
|
|
6
|
+
import { cacheManager } from '../domain/CacheManager';
|
|
7
|
+
import type { CacheConfig } from '../domain/types/Cache';
|
|
8
|
+
|
|
9
|
+
export function useCache<T>(cacheName: string, config?: CacheConfig) {
|
|
10
|
+
const cacheRef = useRef(cacheManager.getCache<T>(cacheName, config));
|
|
11
|
+
const cache = cacheRef.current!;
|
|
12
|
+
const [, forceUpdate] = useState({});
|
|
13
|
+
|
|
14
|
+
const triggerUpdate = useCallback(() => {
|
|
15
|
+
forceUpdate({});
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const set = useCallback(
|
|
19
|
+
(key: string, value: T, ttl?: number) => {
|
|
20
|
+
cache.set(key, value, ttl);
|
|
21
|
+
triggerUpdate();
|
|
22
|
+
},
|
|
23
|
+
[cache, triggerUpdate]
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const get = useCallback(
|
|
27
|
+
(key: string): T | undefined => {
|
|
28
|
+
return cache.get(key);
|
|
29
|
+
},
|
|
30
|
+
[cache]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const has = useCallback(
|
|
34
|
+
(key: string): boolean => {
|
|
35
|
+
return cache.has(key);
|
|
36
|
+
},
|
|
37
|
+
[cache]
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const remove = useCallback(
|
|
41
|
+
(key: string): boolean => {
|
|
42
|
+
const result = cache.delete(key);
|
|
43
|
+
triggerUpdate();
|
|
44
|
+
return result;
|
|
45
|
+
},
|
|
46
|
+
[cache, triggerUpdate]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const clear = useCallback(() => {
|
|
50
|
+
cache.clear();
|
|
51
|
+
triggerUpdate();
|
|
52
|
+
}, [cache, triggerUpdate]);
|
|
53
|
+
|
|
54
|
+
const invalidatePattern = useCallback(
|
|
55
|
+
(pattern: string): number => {
|
|
56
|
+
const count = cache.invalidatePattern(pattern);
|
|
57
|
+
triggerUpdate();
|
|
58
|
+
return count;
|
|
59
|
+
},
|
|
60
|
+
[cache, triggerUpdate]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const getStats = useCallback(() => {
|
|
64
|
+
return cache.getStats();
|
|
65
|
+
}, [cache]);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
set,
|
|
69
|
+
get,
|
|
70
|
+
has,
|
|
71
|
+
remove,
|
|
72
|
+
clear,
|
|
73
|
+
invalidatePattern,
|
|
74
|
+
getStats,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCachedValue Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { cacheManager } from '../domain/CacheManager';
|
|
7
|
+
import type { CacheConfig } from '../domain/types/Cache';
|
|
8
|
+
|
|
9
|
+
export function useCachedValue<T>(
|
|
10
|
+
cacheName: string,
|
|
11
|
+
key: string,
|
|
12
|
+
fetcher: () => Promise<T>,
|
|
13
|
+
config?: CacheConfig & { ttl?: number }
|
|
14
|
+
) {
|
|
15
|
+
const [value, setValue] = useState<T | undefined>(undefined);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [error, setError] = useState<Error | null>(null);
|
|
18
|
+
|
|
19
|
+
const fetcherRef = useRef(fetcher);
|
|
20
|
+
const configRef = useRef(config);
|
|
21
|
+
|
|
22
|
+
fetcherRef.current = fetcher;
|
|
23
|
+
configRef.current = config;
|
|
24
|
+
|
|
25
|
+
const loadValue = useCallback(async () => {
|
|
26
|
+
const cache = cacheManager.getCache<T>(cacheName, configRef.current);
|
|
27
|
+
const cached = cache.get(key);
|
|
28
|
+
|
|
29
|
+
if (cached !== undefined) {
|
|
30
|
+
setValue(cached);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
setError(null);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const data = await fetcherRef.current!();
|
|
39
|
+
cache.set(key, data, configRef.current?.ttl);
|
|
40
|
+
setValue(data);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setError(err as Error);
|
|
43
|
+
} finally {
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, [cacheName, key]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
let isMounted = true;
|
|
50
|
+
|
|
51
|
+
loadValue().then(() => {
|
|
52
|
+
if (!isMounted) {
|
|
53
|
+
setValue(undefined);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
isMounted = false;
|
|
59
|
+
};
|
|
60
|
+
}, [loadValue]);
|
|
61
|
+
|
|
62
|
+
const invalidate = useCallback(() => {
|
|
63
|
+
const cache = cacheManager.getCache<T>(cacheName);
|
|
64
|
+
cache.delete(key);
|
|
65
|
+
setValue(undefined);
|
|
66
|
+
}, [cacheName, key]);
|
|
67
|
+
|
|
68
|
+
const invalidatePattern = useCallback((pattern: string): number => {
|
|
69
|
+
const cache = cacheManager.getCache<T>(cacheName);
|
|
70
|
+
const count = cache.invalidatePattern(pattern);
|
|
71
|
+
setValue(undefined);
|
|
72
|
+
return count;
|
|
73
|
+
}, [cacheName]);
|
|
74
|
+
|
|
75
|
+
const refetch = useCallback(() => {
|
|
76
|
+
setValue(undefined);
|
|
77
|
+
loadValue();
|
|
78
|
+
}, [loadValue]);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
value,
|
|
82
|
+
isLoading,
|
|
83
|
+
error,
|
|
84
|
+
invalidate,
|
|
85
|
+
invalidatePattern,
|
|
86
|
+
refetch,
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -138,3 +138,31 @@ export {
|
|
|
138
138
|
|
|
139
139
|
export { useCacheState } from './presentation/hooks/useCacheState';
|
|
140
140
|
export { CacheStorageOperations } from './presentation/hooks/CacheStorageOperations';
|
|
141
|
+
|
|
142
|
+
// =============================================================================
|
|
143
|
+
// IN-MEMORY CACHE LAYER (Merged from @umituz/react-native-cache)
|
|
144
|
+
// =============================================================================
|
|
145
|
+
|
|
146
|
+
export { Cache } from './cache/domain/Cache';
|
|
147
|
+
export { CacheManager, cacheManager } from './cache/domain/CacheManager';
|
|
148
|
+
export { CacheStatsTracker } from './cache/domain/CacheStatsTracker';
|
|
149
|
+
export { PatternMatcher } from './cache/domain/PatternMatcher';
|
|
150
|
+
export { ErrorHandler, CacheError } from './cache/domain/ErrorHandler';
|
|
151
|
+
export { TTLCache } from './cache/infrastructure/TTLCache';
|
|
152
|
+
|
|
153
|
+
export type {
|
|
154
|
+
CacheEntry,
|
|
155
|
+
CacheConfig,
|
|
156
|
+
CacheStats,
|
|
157
|
+
EvictionStrategy,
|
|
158
|
+
} from './cache/domain/types/Cache';
|
|
159
|
+
|
|
160
|
+
export type { EvictionStrategy as IEvictionStrategy } from './cache/domain/strategies/EvictionStrategy';
|
|
161
|
+
|
|
162
|
+
export { LRUStrategy } from './cache/domain/strategies/LRUStrategy';
|
|
163
|
+
export { LFUStrategy } from './cache/domain/strategies/LFUStrategy';
|
|
164
|
+
export { FIFOStrategy } from './cache/domain/strategies/FIFOStrategy';
|
|
165
|
+
export { TTLStrategy as TTLEvictionStrategy } from './cache/domain/strategies/TTLStrategy';
|
|
166
|
+
|
|
167
|
+
export { useCache } from './cache/presentation/useCache';
|
|
168
|
+
export { useCachedValue } from './cache/presentation/useCachedValue';
|