@umituz/react-native-storage 1.5.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.
Files changed (39) hide show
  1. package/README.md +0 -0
  2. package/package.json +28 -10
  3. package/src/__tests__/integration.test.ts +391 -0
  4. package/src/__tests__/mocks/asyncStorage.mock.ts +52 -0
  5. package/src/__tests__/performance.test.ts +351 -0
  6. package/src/__tests__/setup.ts +63 -0
  7. package/src/application/ports/IStorageRepository.ts +0 -12
  8. package/src/domain/constants/CacheDefaults.ts +64 -0
  9. package/src/domain/entities/CachedValue.ts +86 -0
  10. package/src/domain/entities/StorageResult.ts +1 -3
  11. package/src/domain/entities/__tests__/CachedValue.test.ts +149 -0
  12. package/src/domain/entities/__tests__/StorageResult.test.ts +122 -0
  13. package/src/domain/errors/StorageError.ts +0 -2
  14. package/src/domain/errors/__tests__/StorageError.test.ts +127 -0
  15. package/src/domain/factories/StoreFactory.ts +33 -0
  16. package/src/domain/types/Store.ts +18 -0
  17. package/src/domain/utils/CacheKeyGenerator.ts +66 -0
  18. package/src/domain/utils/__tests__/devUtils.test.ts +97 -0
  19. package/src/domain/utils/devUtils.ts +37 -0
  20. package/src/domain/value-objects/StorageKey.ts +27 -29
  21. package/src/index.ts +59 -1
  22. package/src/infrastructure/adapters/StorageService.ts +8 -6
  23. package/src/infrastructure/repositories/AsyncStorageRepository.ts +27 -108
  24. package/src/infrastructure/repositories/BaseStorageOperations.ts +101 -0
  25. package/src/infrastructure/repositories/BatchStorageOperations.ts +42 -0
  26. package/src/infrastructure/repositories/StringStorageOperations.ts +44 -0
  27. package/src/infrastructure/repositories/__tests__/AsyncStorageRepository.test.ts +169 -0
  28. package/src/infrastructure/repositories/__tests__/BaseStorageOperations.test.ts +200 -0
  29. package/src/presentation/hooks/CacheStorageOperations.ts +95 -0
  30. package/src/presentation/hooks/__tests__/usePersistentCache.test.ts +404 -0
  31. package/src/presentation/hooks/__tests__/useStorage.test.ts +246 -0
  32. package/src/presentation/hooks/__tests__/useStorageState.test.ts +292 -0
  33. package/src/presentation/hooks/useCacheState.ts +55 -0
  34. package/src/presentation/hooks/usePersistentCache.ts +154 -0
  35. package/src/presentation/hooks/useStorage.ts +4 -3
  36. package/src/presentation/hooks/useStorageState.ts +24 -8
  37. package/src/presentation/hooks/useStore.ts +15 -0
  38. package/src/types/global.d.ts +40 -0
  39. package/LICENSE +0 -22
@@ -0,0 +1,404 @@
1
+ /**
2
+ * usePersistentCache Hook Tests
3
+ *
4
+ * Unit tests for usePersistentCache hook
5
+ */
6
+
7
+ import { renderHook, act } from '@testing-library/react-hooks';
8
+ import { usePersistentCache } from '../usePersistentCache';
9
+ import { AsyncStorage } from '../../__tests__/mocks/asyncStorage.mock';
10
+ import { DEFAULT_TTL } from '../../../domain/constants/CacheDefaults';
11
+
12
+ describe('usePersistentCache Hook', () => {
13
+ beforeEach(() => {
14
+ (AsyncStorage as any).__clear();
15
+ jest.clearAllMocks();
16
+ jest.spyOn(Date, 'now').mockReturnValue(1000000);
17
+ });
18
+
19
+ afterEach(() => {
20
+ jest.restoreAllMocks();
21
+ });
22
+
23
+ describe('Initial Load', () => {
24
+ it('should load cached data', async () => {
25
+ const key = 'test-cache';
26
+ const data = { test: 'value' };
27
+ const cachedValue = {
28
+ value: data,
29
+ cachedAt: 1000000 - 10000, // 10 seconds ago
30
+ expiresAt: 1000000 + 50000, // 50 seconds from now
31
+ };
32
+
33
+ await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
34
+
35
+ const { result, waitForNextUpdate } = renderHook(() =>
36
+ usePersistentCache(key)
37
+ );
38
+
39
+ await act(async () => {
40
+ await waitForNextUpdate();
41
+ });
42
+
43
+ expect(result.current.data).toEqual(data);
44
+ expect(result.current.isLoading).toBe(false);
45
+ expect(result.current.isExpired).toBe(false);
46
+ });
47
+
48
+ it('should handle missing cache', async () => {
49
+ const key = 'missing-cache';
50
+
51
+ const { result, waitForNextUpdate } = renderHook(() =>
52
+ usePersistentCache(key)
53
+ );
54
+
55
+ await act(async () => {
56
+ await waitForNextUpdate();
57
+ });
58
+
59
+ expect(result.current.data).toBeNull();
60
+ expect(result.current.isLoading).toBe(false);
61
+ expect(result.current.isExpired).toBe(true);
62
+ });
63
+
64
+ it('should handle expired cache', async () => {
65
+ const key = 'expired-cache';
66
+ const data = { test: 'value' };
67
+ const cachedValue = {
68
+ value: data,
69
+ cachedAt: 1000000 - 120000, // 2 minutes ago
70
+ expiresAt: 1000000 - 60000, // 1 minute ago (expired)
71
+ };
72
+
73
+ await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
74
+
75
+ const { result, waitForNextUpdate } = renderHook(() =>
76
+ usePersistentCache(key)
77
+ );
78
+
79
+ await act(async () => {
80
+ await waitForNextUpdate();
81
+ });
82
+
83
+ expect(result.current.data).toEqual(data);
84
+ expect(result.current.isLoading).toBe(false);
85
+ expect(result.current.isExpired).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('setData', () => {
90
+ it('should set data to cache', async () => {
91
+ const key = 'test-cache';
92
+ const data = { test: 'value' };
93
+
94
+ const { result, waitForNextUpdate } = renderHook(() =>
95
+ usePersistentCache(key)
96
+ );
97
+
98
+ await act(async () => {
99
+ await waitForNextUpdate();
100
+ });
101
+
102
+ // Set data
103
+ await act(async () => {
104
+ await result.current.setData(data);
105
+ });
106
+
107
+ expect(result.current.data).toEqual(data);
108
+ expect(result.current.isExpired).toBe(false);
109
+
110
+ // Verify storage
111
+ const stored = await AsyncStorage.getItem(key);
112
+ const parsed = JSON.parse(stored!);
113
+ expect(parsed.value).toEqual(data);
114
+ });
115
+
116
+ it('should use custom TTL', async () => {
117
+ const key = 'test-cache';
118
+ const data = { test: 'value' };
119
+ const ttl = 120000; // 2 minutes
120
+
121
+ const { result } = renderHook(() =>
122
+ usePersistentCache(key, { ttl })
123
+ );
124
+
125
+ await act(async () => {
126
+ await result.current.setData(data);
127
+ });
128
+
129
+ // Verify storage
130
+ const stored = await AsyncStorage.getItem(key);
131
+ const parsed = JSON.parse(stored!);
132
+ expect(parsed.expiresAt - parsed.cachedAt).toBe(ttl);
133
+ });
134
+
135
+ it('should use version', async () => {
136
+ const key = 'test-cache';
137
+ const data = { test: 'value' };
138
+ const version = 2;
139
+
140
+ const { result } = renderHook(() =>
141
+ usePersistentCache(key, { version })
142
+ );
143
+
144
+ await act(async () => {
145
+ await result.current.setData(data);
146
+ });
147
+
148
+ // Verify storage
149
+ const stored = await AsyncStorage.getItem(key);
150
+ const parsed = JSON.parse(stored!);
151
+ expect(parsed.version).toBe(version);
152
+ });
153
+
154
+ it('should not set data when disabled', async () => {
155
+ const key = 'test-cache';
156
+ const data = { test: 'value' };
157
+
158
+ const { result } = renderHook(() =>
159
+ usePersistentCache(key, { enabled: false })
160
+ );
161
+
162
+ await act(async () => {
163
+ await result.current.setData(data);
164
+ });
165
+
166
+ // Verify storage
167
+ const stored = await AsyncStorage.getItem(key);
168
+ expect(stored).toBeNull();
169
+ });
170
+ });
171
+
172
+ describe('clearData', () => {
173
+ it('should clear cached data', async () => {
174
+ const key = 'test-cache';
175
+ const data = { test: 'value' };
176
+ const cachedValue = {
177
+ value: data,
178
+ cachedAt: 1000000 - 10000,
179
+ expiresAt: 1000000 + 50000,
180
+ };
181
+
182
+ await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
183
+
184
+ const { result, waitForNextUpdate } = renderHook(() =>
185
+ usePersistentCache(key)
186
+ );
187
+
188
+ await act(async () => {
189
+ await waitForNextUpdate();
190
+ });
191
+
192
+ // Clear data
193
+ await act(async () => {
194
+ await result.current.clearData();
195
+ });
196
+
197
+ expect(result.current.data).toBeNull();
198
+ expect(result.current.isExpired).toBe(true);
199
+
200
+ // Verify storage
201
+ const stored = await AsyncStorage.getItem(key);
202
+ expect(stored).toBeNull();
203
+ });
204
+
205
+ it('should not clear when disabled', async () => {
206
+ const key = 'test-cache';
207
+ const data = { test: 'value' };
208
+ const cachedValue = {
209
+ value: data,
210
+ cachedAt: 1000000 - 10000,
211
+ expiresAt: 1000000 + 50000,
212
+ };
213
+
214
+ await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
215
+
216
+ const { result } = renderHook(() =>
217
+ usePersistentCache(key, { enabled: false })
218
+ );
219
+
220
+ await act(async () => {
221
+ await result.current.clearData();
222
+ });
223
+
224
+ // Storage should not be cleared
225
+ const stored = await AsyncStorage.getItem(key);
226
+ expect(stored).toBe(JSON.stringify(cachedValue));
227
+ });
228
+ });
229
+
230
+ describe('refresh', () => {
231
+ it('should refresh cache from storage', async () => {
232
+ const key = 'test-cache';
233
+ const data1 = { test: 'value1' };
234
+ const data2 = { test: 'value2' };
235
+
236
+ // Set initial cache
237
+ const cachedValue1 = {
238
+ value: data1,
239
+ timestamp: 1000000 - 10000,
240
+ ttl: 60000,
241
+ };
242
+
243
+ await AsyncStorage.setItem(key, JSON.stringify(cachedValue1));
244
+
245
+ const { result, waitForNextUpdate } = renderHook(() =>
246
+ usePersistentCache(key)
247
+ );
248
+
249
+ await act(async () => {
250
+ await waitForNextUpdate();
251
+ });
252
+
253
+ expect(result.current.data).toEqual(data1);
254
+
255
+ // Update storage directly
256
+ const cachedValue2 = {
257
+ value: data2,
258
+ timestamp: 1000000 - 5000,
259
+ ttl: 60000,
260
+ };
261
+
262
+ await AsyncStorage.setItem(key, JSON.stringify(cachedValue2));
263
+
264
+ // Refresh
265
+ await act(async () => {
266
+ await result.current.refresh();
267
+ });
268
+
269
+ expect(result.current.data).toEqual(data2);
270
+ });
271
+ });
272
+
273
+ describe('Performance', () => {
274
+ it('should use singleton CacheStorageOperations', () => {
275
+ const { result: result1 } = renderHook(() =>
276
+ usePersistentCache('key1')
277
+ );
278
+
279
+ const { result: result2 } = renderHook(() =>
280
+ usePersistentCache('key2')
281
+ );
282
+
283
+ // Both hooks should use the same instance
284
+ expect(result1.current.setData).toBe(result2.current.setData);
285
+ });
286
+
287
+ it('should not re-run effect when options change', async () => {
288
+ const key = 'test-cache';
289
+ const data = { test: 'value' };
290
+ const cachedValue = {
291
+ value: data,
292
+ cachedAt: 1000000 - 10000,
293
+ expiresAt: 1000000 + 50000,
294
+ };
295
+
296
+ await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
297
+
298
+ const { result, rerender, waitForNextUpdate } = renderHook(
299
+ ({ options }) => usePersistentCache(key, options),
300
+ {
301
+ initialProps: {
302
+ options: { ttl: 60000, version: 1, enabled: true }
303
+ }
304
+ }
305
+ );
306
+
307
+ await act(async () => {
308
+ await waitForNextUpdate();
309
+ });
310
+
311
+ const getItemCalls = (AsyncStorage.getItem as jest.Mock).mock.calls.length;
312
+
313
+ // Rerender with different options
314
+ rerender({
315
+ options: { ttl: 120000, version: 2, enabled: true }
316
+ });
317
+
318
+ // Should not trigger another storage read
319
+ expect((AsyncStorage.getItem as jest.Mock).mock.calls.length).toBe(getItemCalls);
320
+ });
321
+
322
+ it('should re-run effect when key changes', async () => {
323
+ const key1 = 'key1';
324
+ const key2 = 'key2';
325
+ const data1 = { test: 'value1' };
326
+ const data2 = { test: 'value2' };
327
+
328
+ const cachedValue1 = {
329
+ value: data1,
330
+ timestamp: 1000000 - 10000,
331
+ ttl: 60000,
332
+ };
333
+
334
+ const cachedValue2 = {
335
+ value: data2,
336
+ timestamp: 1000000 - 10000,
337
+ ttl: 60000,
338
+ };
339
+
340
+ await AsyncStorage.setItem(key1, JSON.stringify(cachedValue1));
341
+ await AsyncStorage.setItem(key2, JSON.stringify(cachedValue2));
342
+
343
+ const { result, rerender, waitForNextUpdate } = renderHook(
344
+ ({ key }) => usePersistentCache(key),
345
+ { initialProps: { key: key1 } }
346
+ );
347
+
348
+ await act(async () => {
349
+ await waitForNextUpdate();
350
+ });
351
+
352
+ expect(result.current.data).toEqual(data1);
353
+
354
+ // Change key
355
+ rerender({ key: key2 });
356
+
357
+ await act(async () => {
358
+ await waitForNextUpdate();
359
+ });
360
+
361
+ expect(result.current.data).toEqual(data2);
362
+ });
363
+ });
364
+
365
+ describe('Error Handling', () => {
366
+ it('should handle storage read error', async () => {
367
+ const key = 'test-cache';
368
+
369
+ // Mock storage error
370
+ (AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
371
+
372
+ const { result, waitForNextUpdate } = renderHook(() =>
373
+ usePersistentCache(key)
374
+ );
375
+
376
+ await act(async () => {
377
+ await waitForNextUpdate();
378
+ });
379
+
380
+ expect(result.current.data).toBeNull();
381
+ expect(result.current.isLoading).toBe(false);
382
+ expect(result.current.isExpired).toBe(true);
383
+ });
384
+
385
+ it('should handle invalid cache data', async () => {
386
+ const key = 'test-cache';
387
+
388
+ // Set invalid JSON
389
+ await AsyncStorage.setItem(key, 'invalid-json');
390
+
391
+ const { result, waitForNextUpdate } = renderHook(() =>
392
+ usePersistentCache(key)
393
+ );
394
+
395
+ await act(async () => {
396
+ await waitForNextUpdate();
397
+ });
398
+
399
+ expect(result.current.data).toBeNull();
400
+ expect(result.current.isLoading).toBe(false);
401
+ expect(result.current.isExpired).toBe(true);
402
+ });
403
+ });
404
+ });
@@ -0,0 +1,246 @@
1
+ /**
2
+ * useStorage Hook Tests
3
+ *
4
+ * Unit tests for useStorage hook
5
+ */
6
+
7
+ import { renderHook, act } from '@testing-library/react-hooks';
8
+ import { useStorage } from '../useStorage';
9
+ import { AsyncStorage } from '../../__tests__/mocks/asyncStorage.mock';
10
+
11
+ describe('useStorage Hook', () => {
12
+ beforeEach(() => {
13
+ (AsyncStorage as any).__clear();
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ describe('getItem', () => {
18
+ it('should get item from storage', async () => {
19
+ const key = 'test-key';
20
+ const defaultValue = 'default';
21
+ const expectedValue = 'test-value';
22
+
23
+ await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
24
+
25
+ const { result } = renderHook(() => useStorage());
26
+
27
+ const value = await result.current.getItem(key, defaultValue);
28
+
29
+ expect(value).toBe(expectedValue);
30
+ });
31
+
32
+ it('should return default value for missing key', async () => {
33
+ const key = 'missing-key';
34
+ const defaultValue = 'default';
35
+
36
+ const { result } = renderHook(() => useStorage());
37
+
38
+ const value = await result.current.getItem(key, defaultValue);
39
+
40
+ expect(value).toBe(defaultValue);
41
+ });
42
+
43
+ it('should handle StorageKey enum', async () => {
44
+ const key = '@ui_preferences';
45
+ const defaultValue = 'default';
46
+ const expectedValue = 'test-value';
47
+
48
+ await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
49
+
50
+ const { result } = renderHook(() => useStorage());
51
+
52
+ const value = await result.current.getItem(key as any, defaultValue);
53
+
54
+ expect(value).toBe(expectedValue);
55
+ });
56
+ });
57
+
58
+ describe('setItem', () => {
59
+ it('should set item to storage', async () => {
60
+ const key = 'test-key';
61
+ const value = 'test-value';
62
+
63
+ const { result } = renderHook(() => useStorage());
64
+
65
+ const success = await result.current.setItem(key, value);
66
+
67
+ expect(success).toBe(true);
68
+
69
+ // Verify storage
70
+ const stored = await AsyncStorage.getItem(key);
71
+ expect(JSON.parse(stored!)).toBe(value);
72
+ });
73
+
74
+ it('should return false on failure', async () => {
75
+ const key = 'test-key';
76
+ const value = { circular: {} };
77
+ value.circular = value; // Create circular reference
78
+
79
+ const { result } = renderHook(() => useStorage());
80
+
81
+ const success = await result.current.setItem(key, value);
82
+
83
+ expect(success).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe('getString', () => {
88
+ it('should get string from storage', async () => {
89
+ const key = 'test-key';
90
+ const defaultValue = 'default';
91
+ const expectedValue = 'test-string';
92
+
93
+ await AsyncStorage.setItem(key, expectedValue);
94
+
95
+ const { result } = renderHook(() => useStorage());
96
+
97
+ const value = await result.current.getString(key, defaultValue);
98
+
99
+ expect(value).toBe(expectedValue);
100
+ });
101
+ });
102
+
103
+ describe('setString', () => {
104
+ it('should set string to storage', async () => {
105
+ const key = 'test-key';
106
+ const value = 'test-string';
107
+
108
+ const { result } = renderHook(() => useStorage());
109
+
110
+ const success = await result.current.setString(key, value);
111
+
112
+ expect(success).toBe(true);
113
+
114
+ // Verify storage
115
+ const stored = await AsyncStorage.getItem(key);
116
+ expect(stored).toBe(value);
117
+ });
118
+ });
119
+
120
+ describe('removeItem', () => {
121
+ it('should remove item from storage', async () => {
122
+ const key = 'test-key';
123
+
124
+ // Setup
125
+ await AsyncStorage.setItem(key, 'test-value');
126
+
127
+ const { result } = renderHook(() => useStorage());
128
+
129
+ const success = await result.current.removeItem(key);
130
+
131
+ expect(success).toBe(true);
132
+
133
+ // Verify removal
134
+ const stored = await AsyncStorage.getItem(key);
135
+ expect(stored).toBeNull();
136
+ });
137
+ });
138
+
139
+ describe('hasItem', () => {
140
+ it('should return true for existing item', async () => {
141
+ const key = 'test-key';
142
+
143
+ await AsyncStorage.setItem(key, 'test-value');
144
+
145
+ const { result } = renderHook(() => useStorage());
146
+
147
+ const exists = await result.current.hasItem(key);
148
+
149
+ expect(exists).toBe(true);
150
+ });
151
+
152
+ it('should return false for missing item', async () => {
153
+ const key = 'missing-key';
154
+
155
+ const { result } = renderHook(() => useStorage());
156
+
157
+ const exists = await result.current.hasItem(key);
158
+
159
+ expect(exists).toBe(false);
160
+ });
161
+ });
162
+
163
+ describe('clearAll', () => {
164
+ it('should clear all storage', async () => {
165
+ // Setup
166
+ await AsyncStorage.setItem('key1', 'value1');
167
+ await AsyncStorage.setItem('key2', 'value2');
168
+
169
+ const { result } = renderHook(() => useStorage());
170
+
171
+ const success = await result.current.clearAll();
172
+
173
+ expect(success).toBe(true);
174
+
175
+ // Verify clear
176
+ const keys = await AsyncStorage.getAllKeys();
177
+ expect(keys).toHaveLength(0);
178
+ });
179
+ });
180
+
181
+ describe('getItemWithResult', () => {
182
+ it('should return full result object', async () => {
183
+ const key = 'test-key';
184
+ const defaultValue = 'default';
185
+ const expectedValue = 'test-value';
186
+
187
+ await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
188
+
189
+ const { result } = renderHook(() => useStorage());
190
+
191
+ const storageResult = await result.current.getItemWithResult(key, defaultValue);
192
+
193
+ expect(storageResult.success).toBe(true);
194
+ expect(storageResult.data).toBe(expectedValue);
195
+ expect(storageResult.error).toBeUndefined();
196
+ });
197
+ });
198
+
199
+ describe('Performance', () => {
200
+ it('should memoize return object', () => {
201
+ const { result, rerender } = renderHook(() => useStorage());
202
+
203
+ const firstCall = result.current;
204
+ rerender();
205
+ const secondCall = result.current;
206
+
207
+ // Should be the same reference (memoized)
208
+ expect(firstCall).toBe(secondCall);
209
+ });
210
+
211
+ it('should not recreate functions on re-render', () => {
212
+ const { result, rerender } = renderHook(() => useStorage());
213
+
214
+ const firstFunctions = {
215
+ getItem: result.current.getItem,
216
+ setItem: result.current.setItem,
217
+ getString: result.current.getString,
218
+ setString: result.current.setString,
219
+ removeItem: result.current.removeItem,
220
+ hasItem: result.current.hasItem,
221
+ clearAll: result.current.clearAll,
222
+ getItemWithResult: result.current.getItemWithResult,
223
+ };
224
+
225
+ rerender();
226
+
227
+ const secondFunctions = {
228
+ getItem: result.current.getItem,
229
+ setItem: result.current.setItem,
230
+ getString: result.current.getString,
231
+ setString: result.current.setString,
232
+ removeItem: result.current.removeItem,
233
+ hasItem: result.current.hasItem,
234
+ clearAll: result.current.clearAll,
235
+ getItemWithResult: result.current.getItemWithResult,
236
+ };
237
+
238
+ // All functions should be the same reference
239
+ Object.keys(firstFunctions).forEach(key => {
240
+ expect(firstFunctions[key as keyof typeof firstFunctions]).toBe(
241
+ secondFunctions[key as keyof typeof secondFunctions]
242
+ );
243
+ });
244
+ });
245
+ });
246
+ });