@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,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
|
+
});
|