@umituz/react-native-storage 2.0.0 → 2.3.3

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 (34) hide show
  1. package/package.json +21 -5
  2. package/src/__tests__/integration.test.ts +391 -0
  3. package/src/__tests__/mocks/asyncStorage.mock.ts +52 -0
  4. package/src/__tests__/performance.test.ts +351 -0
  5. package/src/__tests__/setup.ts +63 -0
  6. package/src/application/ports/IStorageRepository.ts +0 -12
  7. package/src/domain/entities/StorageResult.ts +1 -3
  8. package/src/domain/entities/__tests__/CachedValue.test.ts +149 -0
  9. package/src/domain/entities/__tests__/StorageResult.test.ts +122 -0
  10. package/src/domain/errors/StorageError.ts +0 -2
  11. package/src/domain/errors/__tests__/StorageError.test.ts +127 -0
  12. package/src/domain/utils/__tests__/devUtils.test.ts +97 -0
  13. package/src/domain/utils/devUtils.ts +37 -0
  14. package/src/domain/value-objects/StorageKey.ts +27 -29
  15. package/src/index.ts +9 -1
  16. package/src/infrastructure/adapters/StorageService.ts +8 -6
  17. package/src/infrastructure/repositories/AsyncStorageRepository.ts +27 -108
  18. package/src/infrastructure/repositories/BaseStorageOperations.ts +101 -0
  19. package/src/infrastructure/repositories/BatchStorageOperations.ts +42 -0
  20. package/src/infrastructure/repositories/StringStorageOperations.ts +44 -0
  21. package/src/infrastructure/repositories/__tests__/AsyncStorageRepository.test.ts +169 -0
  22. package/src/infrastructure/repositories/__tests__/BaseStorageOperations.test.ts +200 -0
  23. package/src/presentation/hooks/CacheStorageOperations.ts +95 -0
  24. package/src/presentation/hooks/__tests__/usePersistentCache.test.ts +404 -0
  25. package/src/presentation/hooks/__tests__/useStorage.test.ts +246 -0
  26. package/src/presentation/hooks/__tests__/useStorageState.test.ts +292 -0
  27. package/src/presentation/hooks/useCacheState.ts +55 -0
  28. package/src/presentation/hooks/usePersistentCache.ts +30 -39
  29. package/src/presentation/hooks/useStorage.ts +4 -3
  30. package/src/presentation/hooks/useStorageState.ts +24 -8
  31. package/src/presentation/hooks/useStore.ts +3 -1
  32. package/src/types/global.d.ts +40 -0
  33. package/LICENSE +0 -22
  34. package/src/presentation/hooks/usePersistedState.ts +0 -34
@@ -0,0 +1,200 @@
1
+ /**
2
+ * BaseStorageOperations Tests
3
+ *
4
+ * Unit tests for BaseStorageOperations
5
+ */
6
+
7
+ import { BaseStorageOperations } from '../BaseStorageOperations';
8
+ import { AsyncStorage } from '../../__tests__/mocks/asyncStorage.mock';
9
+ import { StorageReadError, StorageWriteError, StorageDeleteError } from '../../../domain/errors/StorageError';
10
+
11
+ describe('BaseStorageOperations', () => {
12
+ let baseOps: BaseStorageOperations;
13
+
14
+ beforeEach(() => {
15
+ baseOps = new BaseStorageOperations();
16
+ (AsyncStorage as any).__clear();
17
+ });
18
+
19
+ describe('getItem', () => {
20
+ it('should get item successfully', async () => {
21
+ const key = 'test-key';
22
+ const value = { test: 'value' };
23
+ const defaultValue = { default: true };
24
+
25
+ // Setup
26
+ await AsyncStorage.setItem(key, JSON.stringify(value));
27
+
28
+ // Test
29
+ const result = await baseOps.getItem(key, defaultValue);
30
+
31
+ expect(result.success).toBe(true);
32
+ expect(result.data).toEqual(value);
33
+ });
34
+
35
+ it('should return default value for missing key', async () => {
36
+ const key = 'missing-key';
37
+ const defaultValue = { default: true };
38
+
39
+ const result = await baseOps.getItem(key, defaultValue);
40
+
41
+ expect(result.success).toBe(true);
42
+ expect(result.data).toBe(defaultValue);
43
+ });
44
+
45
+ it('should handle deserialization error', async () => {
46
+ const key = 'invalid-json-key';
47
+ const defaultValue = { default: true };
48
+
49
+ // Setup invalid JSON
50
+ await AsyncStorage.setItem(key, 'invalid-json');
51
+
52
+ const result = await baseOps.getItem(key, defaultValue);
53
+
54
+ expect(result.success).toBe(false);
55
+ expect(result.data).toBe(defaultValue);
56
+ expect(result.error).toBeInstanceOf(StorageReadError);
57
+ });
58
+
59
+ it('should handle storage read error', async () => {
60
+ const key = 'test-key';
61
+ const defaultValue = { default: true };
62
+
63
+ // Mock storage error
64
+ (AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
65
+
66
+ const result = await baseOps.getItem(key, defaultValue);
67
+
68
+ expect(result.success).toBe(false);
69
+ expect(result.data).toBe(defaultValue);
70
+ expect(result.error).toBeInstanceOf(StorageReadError);
71
+ });
72
+ });
73
+
74
+ describe('setItem', () => {
75
+ it('should set item successfully', async () => {
76
+ const key = 'test-key';
77
+ const value = { test: 'value' };
78
+
79
+ const result = await baseOps.setItem(key, value);
80
+
81
+ expect(result.success).toBe(true);
82
+ expect(result.data).toEqual(value);
83
+
84
+ // Verify storage
85
+ const stored = await AsyncStorage.getItem(key);
86
+ expect(JSON.parse(stored!)).toEqual(value);
87
+ });
88
+
89
+ it('should handle serialization error', async () => {
90
+ const key = 'test-key';
91
+ const value = { circular: {} };
92
+ value.circular = value; // Create circular reference
93
+
94
+ const result = await baseOps.setItem(key, value);
95
+
96
+ expect(result.success).toBe(false);
97
+ expect(result.error).toBeInstanceOf(StorageWriteError);
98
+ });
99
+
100
+ it('should handle storage write error', async () => {
101
+ const key = 'test-key';
102
+ const value = { test: 'value' };
103
+
104
+ // Mock storage error
105
+ (AsyncStorage.setItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
106
+
107
+ const result = await baseOps.setItem(key, value);
108
+
109
+ expect(result.success).toBe(false);
110
+ expect(result.error).toBeInstanceOf(StorageWriteError);
111
+ });
112
+ });
113
+
114
+ describe('removeItem', () => {
115
+ it('should remove item successfully', async () => {
116
+ const key = 'test-key';
117
+
118
+ // Setup
119
+ await AsyncStorage.setItem(key, 'test-value');
120
+
121
+ const result = await baseOps.removeItem(key);
122
+
123
+ expect(result.success).toBe(true);
124
+
125
+ // Verify removal
126
+ const stored = await AsyncStorage.getItem(key);
127
+ expect(stored).toBeNull();
128
+ });
129
+
130
+ it('should handle storage delete error', async () => {
131
+ const key = 'test-key';
132
+
133
+ // Mock storage error
134
+ (AsyncStorage.removeItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
135
+
136
+ const result = await baseOps.removeItem(key);
137
+
138
+ expect(result.success).toBe(false);
139
+ expect(result.error).toBeInstanceOf(StorageDeleteError);
140
+ });
141
+ });
142
+
143
+ describe('hasItem', () => {
144
+ it('should return true for existing item', async () => {
145
+ const key = 'test-key';
146
+
147
+ // Setup
148
+ await AsyncStorage.setItem(key, 'test-value');
149
+
150
+ const exists = await baseOps.hasItem(key);
151
+
152
+ expect(exists).toBe(true);
153
+ });
154
+
155
+ it('should return false for missing item', async () => {
156
+ const key = 'missing-key';
157
+
158
+ const exists = await baseOps.hasItem(key);
159
+
160
+ expect(exists).toBe(false);
161
+ });
162
+
163
+ it('should return false on storage error', async () => {
164
+ const key = 'test-key';
165
+
166
+ // Mock storage error
167
+ (AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
168
+
169
+ const exists = await baseOps.hasItem(key);
170
+
171
+ expect(exists).toBe(false);
172
+ });
173
+ });
174
+
175
+ describe('clearAll', () => {
176
+ it('should clear all storage successfully', async () => {
177
+ // Setup
178
+ await AsyncStorage.setItem('key1', 'value1');
179
+ await AsyncStorage.setItem('key2', 'value2');
180
+
181
+ const result = await baseOps.clearAll();
182
+
183
+ expect(result.success).toBe(true);
184
+
185
+ // Verify clear
186
+ const keys = await AsyncStorage.getAllKeys();
187
+ expect(keys).toHaveLength(0);
188
+ });
189
+
190
+ it('should handle storage clear error', async () => {
191
+ // Mock storage error
192
+ (AsyncStorage.clear as jest.Mock).mockRejectedValue(new Error('Storage error'));
193
+
194
+ const result = await baseOps.clearAll();
195
+
196
+ expect(result.success).toBe(false);
197
+ expect(result.error).toBeInstanceOf(StorageDeleteError);
198
+ });
199
+ });
200
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Cache Storage Operations
3
+ *
4
+ * Handles cache storage operations following Single Responsibility Principle
5
+ */
6
+
7
+ import { storageRepository } from '../../infrastructure/repositories/AsyncStorageRepository';
8
+ import type { CachedValue } from '../../domain/entities/CachedValue';
9
+ import { createCachedValue } from '../../domain/entities/CachedValue';
10
+ import { devWarn } from '../../domain/utils/devUtils';
11
+
12
+ export interface CacheStorageOptions {
13
+ ttl?: number;
14
+ version?: number;
15
+ enabled?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Handles cache storage operations with proper error handling and memory management
20
+ */
21
+ export class CacheStorageOperations {
22
+ private static instance: CacheStorageOperations | null = null;
23
+
24
+ /**
25
+ * Singleton pattern to prevent memory leaks
26
+ */
27
+ static getInstance(): CacheStorageOperations {
28
+ if (!CacheStorageOperations.instance) {
29
+ CacheStorageOperations.instance = new CacheStorageOperations();
30
+ }
31
+ return CacheStorageOperations.instance;
32
+ }
33
+
34
+ /**
35
+ * Reset singleton instance (useful for testing)
36
+ */
37
+ static resetInstance(): void {
38
+ CacheStorageOperations.instance = null;
39
+ }
40
+
41
+ /**
42
+ * Load cached data from storage
43
+ */
44
+ async loadFromStorage<T>(
45
+ key: string,
46
+ version?: number
47
+ ): Promise<CachedValue<T> | null> {
48
+ try {
49
+ const result = await storageRepository.getString(key, '');
50
+
51
+ if (result.success && result.data) {
52
+ const cached = JSON.parse(result.data) as CachedValue<T>;
53
+ return cached;
54
+ }
55
+
56
+ return null;
57
+ } catch (error) {
58
+ devWarn(`CacheStorageOperations: Failed to load cache for key "${key}"`, error);
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Save data to cache storage
65
+ */
66
+ async saveToStorage<T>(
67
+ key: string,
68
+ value: T,
69
+ options: CacheStorageOptions = {}
70
+ ): Promise<void> {
71
+ const { ttl, version, enabled = true } = options;
72
+
73
+ if (!enabled) return;
74
+
75
+ try {
76
+ const cached = createCachedValue(value, ttl || 0, version);
77
+ await storageRepository.setString(key, JSON.stringify(cached));
78
+ } catch (error) {
79
+ devWarn(`CacheStorageOperations: Failed to save cache for key "${key}"`, error);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Clear cached data from storage
85
+ */
86
+ async clearFromStorage(key: string, enabled = true): Promise<void> {
87
+ if (!enabled) return;
88
+
89
+ try {
90
+ await storageRepository.removeItem(key);
91
+ } catch (error) {
92
+ devWarn(`CacheStorageOperations: Failed to clear cache for key "${key}"`, error);
93
+ }
94
+ }
95
+ }
@@ -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
+ });