@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
@@ -2,178 +2,97 @@
2
2
  * AsyncStorage Repository
3
3
  *
4
4
  * Domain-Driven Design: Infrastructure implementation of IStorageRepository
5
- * Adapts React Native AsyncStorage to domain interface
5
+ * Adapts React Native AsyncStorage to domain interface using composition
6
6
  */
7
7
 
8
- import AsyncStorage from '@react-native-async-storage/async-storage';
9
8
  import type { IStorageRepository } from '../../application/ports/IStorageRepository';
10
9
  import type { StorageResult } from '../../domain/entities/StorageResult';
11
- import { success, failure } from '../../domain/entities/StorageResult';
12
- import {
13
- StorageReadError,
14
- StorageWriteError,
15
- StorageDeleteError,
16
- StorageSerializationError,
17
- StorageDeserializationError,
18
- } from '../../domain/errors/StorageError';
10
+ import { BaseStorageOperations } from './BaseStorageOperations';
11
+ import { StringStorageOperations } from './StringStorageOperations';
12
+ import { BatchStorageOperations } from './BatchStorageOperations';
19
13
 
20
14
  /**
21
15
  * AsyncStorage Repository Implementation
16
+ * Uses composition to follow Single Responsibility Principle
22
17
  */
23
18
  export class AsyncStorageRepository implements IStorageRepository {
19
+ private baseOps: BaseStorageOperations;
20
+ private stringOps: StringStorageOperations;
21
+ private batchOps: BatchStorageOperations;
22
+
23
+ constructor() {
24
+ this.baseOps = new BaseStorageOperations();
25
+ this.stringOps = new StringStorageOperations();
26
+ this.batchOps = new BatchStorageOperations();
27
+ }
28
+
24
29
  /**
25
30
  * Get item from AsyncStorage with type safety
26
31
  */
27
32
  async getItem<T>(key: string, defaultValue: T): Promise<StorageResult<T>> {
28
- try {
29
- const value = await AsyncStorage.getItem(key);
30
-
31
- if (value === null) {
32
- // Missing keys on first app launch are NORMAL, not errors
33
- return success(defaultValue);
34
- }
35
-
36
- try {
37
- const parsed = JSON.parse(value) as T;
38
- return success(parsed);
39
- } catch (parseError) {
40
- return failure(new StorageDeserializationError(key, parseError), defaultValue);
41
- }
42
- } catch (error) {
43
- return failure(new StorageReadError(key, error), defaultValue);
44
- }
33
+ return this.baseOps.getItem(key, defaultValue);
45
34
  }
46
35
 
47
36
  /**
48
37
  * Set item in AsyncStorage with automatic JSON serialization
49
38
  */
50
39
  async setItem<T>(key: string, value: T): Promise<StorageResult<T>> {
51
- try {
52
- let serialized: string;
53
- try {
54
- serialized = JSON.stringify(value);
55
- } catch (serializeError) {
56
- return failure(new StorageSerializationError(key, serializeError));
57
- }
58
-
59
- await AsyncStorage.setItem(key, serialized);
60
- return success(value);
61
- } catch (error) {
62
- return failure(new StorageWriteError(key, error));
63
- }
40
+ return this.baseOps.setItem(key, value);
64
41
  }
65
42
 
66
43
  /**
67
44
  * Get string value (no JSON parsing)
68
45
  */
69
46
  async getString(key: string, defaultValue: string): Promise<StorageResult<string>> {
70
- try {
71
- const value = await AsyncStorage.getItem(key);
72
-
73
- if (value === null) {
74
- // Missing keys on first app launch are NORMAL, not errors
75
- return success(defaultValue);
76
- }
77
-
78
- return success(value);
79
- } catch (error) {
80
- return failure(new StorageReadError(key, error), defaultValue);
81
- }
47
+ return this.stringOps.getString(key, defaultValue);
82
48
  }
83
49
 
84
50
  /**
85
51
  * Set string value (no JSON serialization)
86
52
  */
87
53
  async setString(key: string, value: string): Promise<StorageResult<string>> {
88
- try {
89
- await AsyncStorage.setItem(key, value);
90
- return success(value);
91
- } catch (error) {
92
- return failure(new StorageWriteError(key, error));
93
- }
54
+ return this.stringOps.setString(key, value);
94
55
  }
95
56
 
96
57
  /**
97
58
  * Remove item from AsyncStorage
98
59
  */
99
60
  async removeItem(key: string): Promise<StorageResult<void>> {
100
- try {
101
- await AsyncStorage.removeItem(key);
102
- return success(undefined);
103
- } catch (error) {
104
- return failure(new StorageDeleteError(key, error));
105
- }
61
+ return this.baseOps.removeItem(key);
106
62
  }
107
63
 
108
64
  /**
109
65
  * Check if key exists in storage
110
66
  */
111
67
  async hasItem(key: string): Promise<boolean> {
112
- try {
113
- const value = await AsyncStorage.getItem(key);
114
- return value !== null;
115
- } catch {
116
- return false;
117
- }
68
+ return this.baseOps.hasItem(key);
118
69
  }
119
70
 
120
71
  /**
121
- * Clear all AsyncStorage data (use with caution!)
72
+ * Clear all AsyncStorage data
122
73
  */
123
74
  async clearAll(): Promise<StorageResult<void>> {
124
- try {
125
- await AsyncStorage.clear();
126
- return success(undefined);
127
- } catch (error) {
128
- return failure(new StorageDeleteError('ALL_KEYS', error));
129
- }
75
+ return this.baseOps.clearAll();
130
76
  }
131
77
 
132
78
  /**
133
- * Get multiple items at once (more efficient than multiple getItem calls)
79
+ * Get multiple items at once
134
80
  */
135
81
  async getMultiple(
136
82
  keys: string[]
137
83
  ): Promise<StorageResult<Record<string, string | null>>> {
138
- try {
139
- const pairs = await AsyncStorage.multiGet(keys);
140
- const result = Object.fromEntries(pairs);
141
- return success(result);
142
- } catch (error) {
143
- return failure(new StorageReadError('MULTIPLE_KEYS', error));
144
- }
84
+ return this.batchOps.getMultiple(keys);
145
85
  }
146
86
 
147
87
  /**
148
88
  * Get all keys from storage
149
89
  */
150
90
  async getAllKeys(): Promise<StorageResult<string[]>> {
151
- try {
152
- const keys = await AsyncStorage.getAllKeys();
153
- return success([...keys]);
154
- } catch (error) {
155
- return failure(new StorageReadError('ALL_KEYS', error));
156
- }
157
- }
158
-
159
- /**
160
- * Get object from storage (alias for getItem for backward compatibility)
161
- * @deprecated Use getItem instead
162
- */
163
- async getObject<T>(key: string, defaultValue: T): Promise<StorageResult<T>> {
164
- return this.getItem(key, defaultValue);
165
- }
166
-
167
- /**
168
- * Set object in storage (alias for setItem for backward compatibility)
169
- * @deprecated Use setItem instead
170
- */
171
- async setObject<T>(key: string, value: T): Promise<StorageResult<T>> {
172
- return this.setItem(key, value);
91
+ return this.batchOps.getAllKeys();
173
92
  }
174
93
  }
175
94
 
176
95
  /**
177
96
  * Singleton instance
178
97
  */
179
- export const storageRepository = new AsyncStorageRepository();
98
+ export const storageRepository = new AsyncStorageRepository();
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Base Storage Operations
3
+ *
4
+ * Core storage operations following Single Responsibility Principle
5
+ */
6
+
7
+ import AsyncStorage from '@react-native-async-storage/async-storage';
8
+ import type { IStorageRepository } from '../../application/ports/IStorageRepository';
9
+ import type { StorageResult } from '../../domain/entities/StorageResult';
10
+ import { success, failure } from '../../domain/entities/StorageResult';
11
+ import {
12
+ StorageReadError,
13
+ StorageWriteError,
14
+ StorageDeleteError,
15
+ StorageSerializationError,
16
+ StorageDeserializationError,
17
+ } from '../../domain/errors/StorageError';
18
+ import { devWarn } from '../../domain/utils/devUtils';
19
+
20
+ /**
21
+ * Base storage operations implementation
22
+ */
23
+ export class BaseStorageOperations {
24
+ /**
25
+ * Get item from AsyncStorage with type safety
26
+ */
27
+ async getItem<T>(key: string, defaultValue: T): Promise<StorageResult<T>> {
28
+ try {
29
+ const value = await AsyncStorage.getItem(key);
30
+
31
+ if (value === null) {
32
+ return success(defaultValue);
33
+ }
34
+
35
+ try {
36
+ const parsed = JSON.parse(value) as T;
37
+ return success(parsed);
38
+ } catch (parseError) {
39
+ return failure(new StorageDeserializationError(key, parseError), defaultValue);
40
+ }
41
+ } catch (error) {
42
+ return failure(new StorageReadError(key, error), defaultValue);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Set item in AsyncStorage with automatic JSON serialization
48
+ */
49
+ async setItem<T>(key: string, value: T): Promise<StorageResult<T>> {
50
+ try {
51
+ let serialized: string;
52
+ try {
53
+ serialized = JSON.stringify(value);
54
+ } catch (serializeError) {
55
+ return failure(new StorageSerializationError(key, serializeError));
56
+ }
57
+
58
+ await AsyncStorage.setItem(key, serialized);
59
+ return success(value);
60
+ } catch (error) {
61
+ return failure(new StorageWriteError(key, error));
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Remove item from AsyncStorage
67
+ */
68
+ async removeItem(key: string): Promise<StorageResult<void>> {
69
+ try {
70
+ await AsyncStorage.removeItem(key);
71
+ return success(undefined);
72
+ } catch (error) {
73
+ return failure(new StorageDeleteError(key, error));
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Check if key exists in storage
79
+ */
80
+ async hasItem(key: string): Promise<boolean> {
81
+ try {
82
+ const value = await AsyncStorage.getItem(key);
83
+ return value !== null;
84
+ } catch (error) {
85
+ devWarn(`BaseStorageOperations: Failed to check if key "${key}" exists`, error);
86
+ return false;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Clear all AsyncStorage data
92
+ */
93
+ async clearAll(): Promise<StorageResult<void>> {
94
+ try {
95
+ await AsyncStorage.clear();
96
+ return success(undefined);
97
+ } catch (error) {
98
+ return failure(new StorageDeleteError('ALL_KEYS', error));
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Batch Storage Operations
3
+ *
4
+ * Batch operations following Single Responsibility Principle
5
+ */
6
+
7
+ import AsyncStorage from '@react-native-async-storage/async-storage';
8
+ import type { StorageResult } from '../../domain/entities/StorageResult';
9
+ import { success, failure } from '../../domain/entities/StorageResult';
10
+ import { StorageReadError } from '../../domain/errors/StorageError';
11
+
12
+ /**
13
+ * Batch storage operations for efficiency
14
+ */
15
+ export class BatchStorageOperations {
16
+ /**
17
+ * Get multiple items at once
18
+ */
19
+ async getMultiple(
20
+ keys: string[]
21
+ ): Promise<StorageResult<Record<string, string | null>>> {
22
+ try {
23
+ const pairs = await AsyncStorage.multiGet(keys);
24
+ const result = Object.fromEntries(pairs);
25
+ return success(result);
26
+ } catch (error) {
27
+ return failure(new StorageReadError('MULTIPLE_KEYS', error));
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Get all keys from storage
33
+ */
34
+ async getAllKeys(): Promise<StorageResult<string[]>> {
35
+ try {
36
+ const keys = await AsyncStorage.getAllKeys();
37
+ return success([...keys]);
38
+ } catch (error) {
39
+ return failure(new StorageReadError('ALL_KEYS', error));
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * String Storage Operations
3
+ *
4
+ * Specialized string operations following Single Responsibility Principle
5
+ */
6
+
7
+ import AsyncStorage from '@react-native-async-storage/async-storage';
8
+ import type { StorageResult } from '../../domain/entities/StorageResult';
9
+ import { success, failure } from '../../domain/entities/StorageResult';
10
+ import { StorageReadError, StorageWriteError } from '../../domain/errors/StorageError';
11
+
12
+ /**
13
+ * String-specific storage operations
14
+ */
15
+ export class StringStorageOperations {
16
+ /**
17
+ * Get string value (no JSON parsing)
18
+ */
19
+ async getString(key: string, defaultValue: string): Promise<StorageResult<string>> {
20
+ try {
21
+ const value = await AsyncStorage.getItem(key);
22
+
23
+ if (value === null) {
24
+ return success(defaultValue);
25
+ }
26
+
27
+ return success(value);
28
+ } catch (error) {
29
+ return failure(new StorageReadError(key, error), defaultValue);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Set string value (no JSON serialization)
35
+ */
36
+ async setString(key: string, value: string): Promise<StorageResult<string>> {
37
+ try {
38
+ await AsyncStorage.setItem(key, value);
39
+ return success(value);
40
+ } catch (error) {
41
+ return failure(new StorageWriteError(key, error));
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * AsyncStorageRepository Tests
3
+ *
4
+ * Unit tests for AsyncStorageRepository
5
+ */
6
+
7
+ import { AsyncStorageRepository } from '../AsyncStorageRepository';
8
+ import { AsyncStorage } from '../../__tests__/mocks/asyncStorage.mock';
9
+
10
+ describe('AsyncStorageRepository', () => {
11
+ let repository: AsyncStorageRepository;
12
+
13
+ beforeEach(() => {
14
+ repository = new AsyncStorageRepository();
15
+ (AsyncStorage as any).__clear();
16
+ });
17
+
18
+ describe('Composition', () => {
19
+ it('should use composition pattern', () => {
20
+ expect(repository).toBeInstanceOf(AsyncStorageRepository);
21
+
22
+ // Test that all methods are available
23
+ expect(typeof repository.getItem).toBe('function');
24
+ expect(typeof repository.setItem).toBe('function');
25
+ expect(typeof repository.getString).toBe('function');
26
+ expect(typeof repository.setString).toBe('function');
27
+ expect(typeof repository.removeItem).toBe('function');
28
+ expect(typeof repository.hasItem).toBe('function');
29
+ expect(typeof repository.clearAll).toBe('function');
30
+ expect(typeof repository.getMultiple).toBe('function');
31
+ expect(typeof repository.getAllKeys).toBe('function');
32
+ });
33
+ });
34
+
35
+ describe('Delegation', () => {
36
+ it('should delegate getItem to BaseStorageOperations', async () => {
37
+ const key = 'test-key';
38
+ const defaultValue = 'default';
39
+ const expectedValue = 'test-value';
40
+
41
+ await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
42
+
43
+ const result = await repository.getItem(key, defaultValue);
44
+
45
+ expect(result.success).toBe(true);
46
+ expect(result.data).toBe(expectedValue);
47
+ });
48
+
49
+ it('should delegate setItem to BaseStorageOperations', async () => {
50
+ const key = 'test-key';
51
+ const value = 'test-value';
52
+
53
+ const result = await repository.setItem(key, value);
54
+
55
+ expect(result.success).toBe(true);
56
+ expect(result.data).toBe(value);
57
+ });
58
+
59
+ it('should delegate getString to StringStorageOperations', async () => {
60
+ const key = 'test-key';
61
+ const defaultValue = 'default';
62
+ const expectedValue = 'test-string';
63
+
64
+ await AsyncStorage.setItem(key, expectedValue);
65
+
66
+ const result = await repository.getString(key, defaultValue);
67
+
68
+ expect(result.success).toBe(true);
69
+ expect(result.data).toBe(expectedValue);
70
+ });
71
+
72
+ it('should delegate setString to StringStorageOperations', async () => {
73
+ const key = 'test-key';
74
+ const value = 'test-string';
75
+
76
+ const result = await repository.setString(key, value);
77
+
78
+ expect(result.success).toBe(true);
79
+ expect(result.data).toBe(value);
80
+ });
81
+
82
+ it('should delegate removeItem to BaseStorageOperations', async () => {
83
+ const key = 'test-key';
84
+
85
+ await AsyncStorage.setItem(key, 'test-value');
86
+
87
+ const result = await repository.removeItem(key);
88
+
89
+ expect(result.success).toBe(true);
90
+ });
91
+
92
+ it('should delegate hasItem to BaseStorageOperations', async () => {
93
+ const key = 'test-key';
94
+
95
+ await AsyncStorage.setItem(key, 'test-value');
96
+
97
+ const exists = await repository.hasItem(key);
98
+
99
+ expect(exists).toBe(true);
100
+ });
101
+
102
+ it('should delegate clearAll to BaseStorageOperations', async () => {
103
+ await AsyncStorage.setItem('key1', 'value1');
104
+ await AsyncStorage.setItem('key2', 'value2');
105
+
106
+ const result = await repository.clearAll();
107
+
108
+ expect(result.success).toBe(true);
109
+ });
110
+
111
+ it('should delegate getMultiple to BatchStorageOperations', async () => {
112
+ const keys = ['key1', 'key2'];
113
+
114
+ await AsyncStorage.setItem('key1', 'value1');
115
+ await AsyncStorage.setItem('key2', 'value2');
116
+
117
+ const result = await repository.getMultiple(keys);
118
+
119
+ expect(result.success).toBe(true);
120
+ expect(result.data).toEqual({
121
+ key1: 'value1',
122
+ key2: 'value2'
123
+ });
124
+ });
125
+
126
+ it('should delegate getAllKeys to BatchStorageOperations', async () => {
127
+ await AsyncStorage.setItem('key1', 'value1');
128
+ await AsyncStorage.setItem('key2', 'value2');
129
+
130
+ const result = await repository.getAllKeys();
131
+
132
+ expect(result.success).toBe(true);
133
+ expect(result.data).toEqual(expect.arrayContaining(['key1', 'key2']));
134
+ });
135
+ });
136
+
137
+ describe('Error Handling', () => {
138
+ it('should handle errors from delegated operations', async () => {
139
+ const key = 'test-key';
140
+
141
+ // Mock error in AsyncStorage
142
+ (AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
143
+
144
+ const result = await repository.getItem(key, 'default');
145
+
146
+ expect(result.success).toBe(false);
147
+ expect(result.error).toBeDefined();
148
+ });
149
+ });
150
+
151
+ describe('Type Safety', () => {
152
+ it('should maintain type safety for generic methods', async () => {
153
+ interface TestData {
154
+ id: number;
155
+ name: string;
156
+ }
157
+
158
+ const key = 'typed-key';
159
+ const data: TestData = { id: 1, name: 'test' };
160
+
161
+ const setResult = await repository.setItem<TestData>(key, data);
162
+ const getResult = await repository.getItem<TestData>(key, { id: 0, name: '' });
163
+
164
+ expect(setResult.success).toBe(true);
165
+ expect(getResult.success).toBe(true);
166
+ expect(getResult.data).toEqual(data);
167
+ });
168
+ });
169
+ });