@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.
Files changed (34) hide show
  1. package/package.json +23 -7
  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
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@umituz/react-native-storage",
3
- "version": "2.0.0",
3
+ "version": "2.3.2",
4
4
  "description": "Zustand state management with AsyncStorage persistence and type-safe storage operations for React Native",
5
- "main": "./src/index.ts",
6
- "types": "./src/index.ts",
5
+ "main": "./lib/index.js",
6
+ "types": "./lib/index.d.ts",
7
7
  "scripts": {
8
+ "build": "tsc",
8
9
  "typecheck": "tsc --noEmit",
9
- "lint": "tsc --noEmit"
10
+ "lint": "tsc --noEmit",
11
+ "test": "jest",
12
+ "test:watch": "jest --watch",
13
+ "test:coverage": "jest --coverage",
14
+ "test:ci": "jest --ci --coverage --watchAll=false"
10
15
  },
11
16
  "keywords": [
12
17
  "react-native",
@@ -27,16 +32,27 @@
27
32
  },
28
33
  "peerDependencies": {
29
34
  "@react-native-async-storage/async-storage": "^1.21.0",
30
- "zustand": "^5.0.0",
31
35
  "react": ">=18.2.0",
32
- "react-native": ">=0.74.0"
36
+ "react-native": ">=0.74.0",
37
+ "zustand": "^5.0.0"
33
38
  },
34
39
  "devDependencies": {
35
40
  "@react-native-async-storage/async-storage": "^1.24.0",
41
+ "@testing-library/jest-native": "^5.4.3",
42
+ "@testing-library/react": "^13.4.0",
43
+ "@testing-library/react-hooks": "^8.0.1",
44
+ "@types/jest": "^29.5.8",
36
45
  "@types/react": "^18.2.45",
37
46
  "@types/react-native": "^0.73.0",
47
+ "find-up": "^8.0.0",
48
+ "jest": "^29.7.0",
49
+ "jest-environment-jsdom": "^29.7.0",
50
+ "p-limit": "^7.2.0",
51
+ "p-try": "^3.0.0",
52
+ "path-exists": "^5.0.0",
38
53
  "react": "^18.2.0",
39
54
  "react-native": "^0.74.0",
55
+ "ts-jest": "^29.4.6",
40
56
  "typescript": "^5.3.3",
41
57
  "zustand": "^5.0.0"
42
58
  },
@@ -44,9 +60,9 @@
44
60
  "access": "public"
45
61
  },
46
62
  "files": [
63
+ "lib",
47
64
  "src",
48
65
  "README.md",
49
66
  "LICENSE"
50
67
  ]
51
68
  }
52
-
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Integration Tests
3
+ *
4
+ * End-to-end tests for the storage system
5
+ */
6
+
7
+ import { renderHook, act } from '@testing-library/react-hooks';
8
+ import { useStorage, useStorageState, usePersistentCache } from '../../index';
9
+ import { StorageKey } from '../../domain/value-objects/StorageKey';
10
+ import { AsyncStorage } from '../mocks/asyncStorage.mock';
11
+
12
+ describe('Integration Tests', () => {
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('Storage Operations Integration', () => {
24
+ it('should work with complex data types', async () => {
25
+ const { result: storageHook } = renderHook(() => useStorage());
26
+
27
+ // Test with complex object
28
+ const complexData = {
29
+ user: {
30
+ id: 1,
31
+ name: 'John Doe',
32
+ preferences: {
33
+ theme: 'dark',
34
+ notifications: true,
35
+ },
36
+ },
37
+ metadata: {
38
+ version: '1.0.0',
39
+ timestamp: Date.now(),
40
+ },
41
+ };
42
+
43
+ // Set complex data
44
+ const setSuccess = await storageHook.current.setItem('complex-data', complexData);
45
+ expect(setSuccess).toBe(true);
46
+
47
+ // Get complex data
48
+ const retrievedData = await storageHook.current.getItem('complex-data', {});
49
+ expect(retrievedData).toEqual(complexData);
50
+ });
51
+
52
+ it('should work with arrays', async () => {
53
+ const { result: storageHook } = renderHook(() => useStorage());
54
+
55
+ const arrayData = [
56
+ { id: 1, name: 'Item 1' },
57
+ { id: 2, name: 'Item 2' },
58
+ { id: 3, name: 'Item 3' },
59
+ ];
60
+
61
+ // Set array
62
+ const setSuccess = await storageHook.current.setItem('array-data', arrayData);
63
+ expect(setSuccess).toBe(true);
64
+
65
+ // Get array
66
+ const retrievedData = await storageHook.current.getItem('array-data', []);
67
+ expect(retrievedData).toEqual(arrayData);
68
+ });
69
+
70
+ it('should work with StorageKey enum', async () => {
71
+ const { result: storageHook } = renderHook(() => useStorage());
72
+
73
+ const preferences = {
74
+ theme: 'light',
75
+ language: 'en',
76
+ };
77
+
78
+ // Set with enum key
79
+ const setSuccess = await storageHook.current.setItem(
80
+ StorageKey.USER_PREFERENCES,
81
+ preferences
82
+ );
83
+ expect(setSuccess).toBe(true);
84
+
85
+ // Get with enum key
86
+ const retrievedData = await storageHook.current.getItem(
87
+ StorageKey.USER_PREFERENCES,
88
+ {}
89
+ );
90
+ expect(retrievedData).toEqual(preferences);
91
+ });
92
+ });
93
+
94
+ describe('State Management Integration', () => {
95
+ it('should sync state with storage', async () => {
96
+ const key = 'sync-test';
97
+ const initialValue = 'initial';
98
+ const updatedValue = 'updated';
99
+
100
+ // Set initial value in storage
101
+ await AsyncStorage.setItem(key, JSON.stringify(initialValue));
102
+
103
+ const { result: stateHook, waitForNextUpdate } = renderHook(() =>
104
+ useStorageState(key, initialValue)
105
+ );
106
+
107
+ // Wait for initial load
108
+ await act(async () => {
109
+ await waitForNextUpdate();
110
+ });
111
+
112
+ expect(stateHook.current[0]).toBe(initialValue);
113
+
114
+ // Update state
115
+ await act(async () => {
116
+ await stateHook.current[1](updatedValue);
117
+ });
118
+
119
+ expect(stateHook.current[0]).toBe(updatedValue);
120
+
121
+ // Verify storage
122
+ const stored = await AsyncStorage.getItem(key);
123
+ expect(JSON.parse(stored!)).toBe(updatedValue);
124
+ });
125
+
126
+ it('should handle multiple state hooks', async () => {
127
+ const { result: hook1, waitForNextUpdate: waitFor1 } = renderHook(() =>
128
+ useStorageState('key1', 'value1')
129
+ );
130
+
131
+ const { result: hook2, waitForNextUpdate: waitFor2 } = renderHook(() =>
132
+ useStorageState('key2', 'value2')
133
+ );
134
+
135
+ // Wait for both to load
136
+ await act(async () => {
137
+ await Promise.all([waitFor1(), waitFor2()]);
138
+ });
139
+
140
+ expect(hook1.current[0]).toBe('value1');
141
+ expect(hook2.current[0]).toBe('value2');
142
+
143
+ // Update first hook
144
+ await act(async () => {
145
+ await hook1.current[1]('updated1');
146
+ });
147
+
148
+ expect(hook1.current[0]).toBe('updated1');
149
+ expect(hook2.current[0]).toBe('value2'); // Should not affect second hook
150
+ });
151
+ });
152
+
153
+ describe('Cache Integration', () => {
154
+ it('should cache and retrieve data with TTL', async () => {
155
+ const key = 'cache-integration';
156
+ const data = { posts: [{ id: 1, title: 'Test Post' }] };
157
+ const ttl = 60000; // 1 minute
158
+
159
+ const { result: cacheHook, waitForNextUpdate } = renderHook(() =>
160
+ usePersistentCache(key, { ttl })
161
+ );
162
+
163
+ await act(async () => {
164
+ await waitForNextUpdate();
165
+ });
166
+
167
+ // Should be empty initially
168
+ expect(cacheHook.current.data).toBeNull();
169
+ expect(cacheHook.current.isExpired).toBe(true);
170
+
171
+ // Set data
172
+ await act(async () => {
173
+ await cacheHook.current.setData(data);
174
+ });
175
+
176
+ expect(cacheHook.current.data).toEqual(data);
177
+ expect(cacheHook.current.isExpired).toBe(false);
178
+
179
+ // Create new hook instance to test persistence
180
+ const { result: newCacheHook, waitForNextUpdate: waitForNew } = renderHook(() =>
181
+ usePersistentCache(key, { ttl })
182
+ );
183
+
184
+ await act(async () => {
185
+ await waitForNew();
186
+ });
187
+
188
+ expect(newCacheHook.current.data).toEqual(data);
189
+ expect(newCacheHook.current.isExpired).toBe(false);
190
+ });
191
+
192
+ it('should handle cache expiration', async () => {
193
+ const key = 'cache-expiration';
194
+ const data = { test: 'value' };
195
+ const ttl = 1000; // 1 second
196
+
197
+ const { result: cacheHook, waitForNextUpdate } = renderHook(() =>
198
+ usePersistentCache(key, { ttl })
199
+ );
200
+
201
+ // Set data
202
+ await act(async () => {
203
+ await waitForNextUpdate();
204
+ await cacheHook.current.setData(data);
205
+ });
206
+
207
+ expect(cacheHook.current.isExpired).toBe(false);
208
+
209
+ // Simulate time passage
210
+ jest.spyOn(Date, 'now').mockReturnValue(1000000 + 2000); // 2 seconds later
211
+
212
+ // Create new hook to test expiration
213
+ const { result: newCacheHook, waitForNextUpdate: waitForNew } = renderHook(() =>
214
+ usePersistentCache(key, { ttl })
215
+ );
216
+
217
+ await act(async () => {
218
+ await waitForNew();
219
+ });
220
+
221
+ expect(newCacheHook.current.data).toEqual(data);
222
+ expect(newCacheHook.current.isExpired).toBe(true);
223
+ });
224
+
225
+ it('should handle cache versioning', async () => {
226
+ const key = 'cache-versioning';
227
+ const dataV1 = { version: 1, data: 'old' };
228
+ const dataV2 = { version: 2, data: 'new' };
229
+
230
+ // Set cache with version 1
231
+ await AsyncStorage.setItem(key, JSON.stringify({
232
+ value: dataV1,
233
+ timestamp: 1000000 - 10000,
234
+ ttl: 60000,
235
+ version: 1,
236
+ }));
237
+
238
+ // Load with version 1
239
+ const { result: cacheHook1, waitForNextUpdate: waitFor1 } = renderHook(() =>
240
+ usePersistentCache(key, { version: 1 })
241
+ );
242
+
243
+ await act(async () => {
244
+ await waitFor1();
245
+ });
246
+
247
+ expect(cacheHook1.current.data).toEqual(dataV1);
248
+ expect(cacheHook1.current.isExpired).toBe(false);
249
+
250
+ // Load with version 2 (should be expired)
251
+ const { result: cacheHook2, waitForNextUpdate: waitFor2 } = renderHook(() =>
252
+ usePersistentCache(key, { version: 2 })
253
+ );
254
+
255
+ await act(async () => {
256
+ await waitFor2();
257
+ });
258
+
259
+ expect(cacheHook2.current.data).toEqual(dataV1);
260
+ expect(cacheHook2.current.isExpired).toBe(true);
261
+
262
+ // Update with version 2
263
+ await act(async () => {
264
+ await cacheHook2.current.setData(dataV2);
265
+ });
266
+
267
+ expect(cacheHook2.current.data).toEqual(dataV2);
268
+ expect(cacheHook2.current.isExpired).toBe(false);
269
+ });
270
+ });
271
+
272
+ describe('Error Recovery Integration', () => {
273
+ it('should recover from storage errors', async () => {
274
+ const key = 'error-recovery';
275
+ const defaultValue = 'default';
276
+
277
+ // Mock storage error for first call
278
+ let callCount = 0;
279
+ (AsyncStorage.getItem as jest.Mock).mockImplementation(() => {
280
+ callCount++;
281
+ if (callCount === 1) {
282
+ return Promise.reject(new Error('Storage error'));
283
+ }
284
+ return Promise.resolve(JSON.stringify('recovered-value'));
285
+ });
286
+
287
+ const { result: stateHook, waitForNextUpdate } = renderHook(() =>
288
+ useStorageState(key, defaultValue)
289
+ );
290
+
291
+ await act(async () => {
292
+ await waitForNextUpdate();
293
+ });
294
+
295
+ // Should use default value on error
296
+ expect(stateHook.current[0]).toBe(defaultValue);
297
+
298
+ // Retry should work
299
+ await act(async () => {
300
+ await stateHook.current[1]('retry-value');
301
+ });
302
+
303
+ expect(stateHook.current[0]).toBe('retry-value');
304
+ });
305
+
306
+ it('should handle corrupted data gracefully', async () => {
307
+ const key = 'corrupted-data';
308
+ const defaultValue = { safe: 'default' };
309
+
310
+ // Set corrupted JSON
311
+ await AsyncStorage.setItem(key, '{"invalid": json}');
312
+
313
+ const { result: stateHook, waitForNextUpdate } = renderHook(() =>
314
+ useStorageState(key, defaultValue)
315
+ );
316
+
317
+ await act(async () => {
318
+ await waitForNextUpdate();
319
+ });
320
+
321
+ // Should use default value
322
+ expect(stateHook.current[0]).toEqual(defaultValue);
323
+
324
+ // Should be able to set new data
325
+ const validData = { valid: 'data' };
326
+ await act(async () => {
327
+ await stateHook.current[1](validData);
328
+ });
329
+
330
+ expect(stateHook.current[0]).toEqual(validData);
331
+ });
332
+ });
333
+
334
+ describe('Performance Integration', () => {
335
+ it('should handle rapid state changes', async () => {
336
+ const key = 'rapid-changes';
337
+ const { result: stateHook, waitForNextUpdate } = renderHook(() =>
338
+ useStorageState(key, 'initial')
339
+ );
340
+
341
+ await act(async () => {
342
+ await waitForNextUpdate();
343
+ });
344
+
345
+ // Rapid changes
346
+ const changes = ['change1', 'change2', 'change3'];
347
+
348
+ for (const change of changes) {
349
+ await act(async () => {
350
+ await stateHook.current[1](change);
351
+ });
352
+ expect(stateHook.current[0]).toBe(change);
353
+ }
354
+
355
+ // Verify final state in storage
356
+ const stored = await AsyncStorage.getItem(key);
357
+ expect(JSON.parse(stored!)).toBe('change3');
358
+ });
359
+
360
+ it('should not interfere with different keys', async () => {
361
+ const { result: hook1, waitForNextUpdate: waitFor1 } = renderHook(() =>
362
+ useStorageState('key1', 'value1')
363
+ );
364
+
365
+ const { result: hook2, waitForNextUpdate: waitFor2 } = renderHook(() =>
366
+ useStorageState('key2', 'value2')
367
+ );
368
+
369
+ await act(async () => {
370
+ await Promise.all([waitFor1(), waitFor2()]);
371
+ });
372
+
373
+ // Update both hooks rapidly
374
+ await act(async () => {
375
+ await Promise.all([
376
+ hook1.current[1]('updated1'),
377
+ hook2.current[1]('updated2'),
378
+ ]);
379
+ });
380
+
381
+ expect(hook1.current[0]).toBe('updated1');
382
+ expect(hook2.current[0]).toBe('updated2');
383
+
384
+ // Verify storage
385
+ const stored1 = await AsyncStorage.getItem('key1');
386
+ const stored2 = await AsyncStorage.getItem('key2');
387
+ expect(JSON.parse(stored1!)).toBe('updated1');
388
+ expect(JSON.parse(stored2!)).toBe('updated2');
389
+ });
390
+ });
391
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * AsyncStorage Mock
3
+ *
4
+ * Mock implementation for testing
5
+ */
6
+
7
+ export class AsyncStorageMock {
8
+ private storage: Map<string, string> = new Map();
9
+
10
+ async getItem(key: string): Promise<string | null> {
11
+ return this.storage.get(key) || null;
12
+ }
13
+
14
+ async setItem(key: string, value: string): Promise<void> {
15
+ this.storage.set(key, value);
16
+ }
17
+
18
+ async removeItem(key: string): Promise<void> {
19
+ this.storage.delete(key);
20
+ }
21
+
22
+ async clear(): Promise<void> {
23
+ this.storage.clear();
24
+ }
25
+
26
+ async getAllKeys(): Promise<readonly string[]> {
27
+ return Array.from(this.storage.keys());
28
+ }
29
+
30
+ async multiGet(keys: readonly string[]): Promise<readonly (readonly [string, string | null])[]> {
31
+ return keys.map(key => [key, this.storage.get(key) || null]);
32
+ }
33
+
34
+ // Test utilities
35
+ __clear() {
36
+ this.storage.clear();
37
+ }
38
+
39
+ __size() {
40
+ return this.storage.size;
41
+ }
42
+
43
+ __has(key: string) {
44
+ return this.storage.has(key);
45
+ }
46
+
47
+ __get(key: string) {
48
+ return this.storage.get(key);
49
+ }
50
+ }
51
+
52
+ export const AsyncStorage = new AsyncStorageMock();