@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
@@ -3,39 +3,13 @@
3
3
  *
4
4
  * Domain-Driven Design: Value Object for storage keys
5
5
  * Ensures type safety and prevents invalid key usage
6
- *
7
- * Theme: {{THEME_NAME}} ({{CATEGORY}} category)
8
- */
9
-
10
- /**
11
- * Storage Key Type
12
- * All valid storage keys must be defined here
13
6
  */
14
- export enum StorageKey {
15
- // Onboarding
16
- ONBOARDING_COMPLETED = '@onboarding_completed',
17
-
18
- // Auth
19
- AUTH_SHOW_REGISTER = '@auth_show_register',
20
-
21
- // Localization
22
- LANGUAGE = '@app_language',
23
-
24
- // Theme
25
- THEME_MODE = '@app_theme_mode',
26
-
27
- // Settings (requires userId suffix)
28
- SETTINGS = 'app_settings',
29
-
30
- // Query Cache (requires app name prefix)
31
- QUERY_CACHE = '{{APP_NAME}}_query_cache',
32
- }
33
7
 
34
8
  /**
35
9
  * Storage key with dynamic suffix
36
10
  */
37
11
  export type DynamicStorageKey = {
38
- base: StorageKey;
12
+ base: string;
39
13
  suffix: string;
40
14
  };
41
15
 
@@ -46,7 +20,7 @@ export type DynamicStorageKey = {
46
20
  * @param userId User identifier
47
21
  * @returns User-specific key
48
22
  */
49
- export const createUserKey = (baseKey: StorageKey, userId: string): string => {
23
+ export const createUserKey = (baseKey: string, userId: string): string => {
50
24
  return `${baseKey}_${userId}`;
51
25
  };
52
26
 
@@ -57,6 +31,30 @@ export const createUserKey = (baseKey: StorageKey, userId: string): string => {
57
31
  * @param appName App identifier
58
32
  * @returns App-specific key
59
33
  */
60
- export const createAppKey = (baseKey: StorageKey, appName: string): string => {
34
+ export const createAppKey = (baseKey: string, appName: string): string => {
61
35
  return `${appName}_${baseKey}`;
62
36
  };
37
+
38
+ /**
39
+ * Helper to create namespaced storage key
40
+ *
41
+ * @param namespace Key namespace
42
+ * @param key Specific key
43
+ * @returns Namespaced key
44
+ */
45
+ export const createNamespacedKey = (namespace: string, key: string): string => {
46
+ return `${namespace}:${key}`;
47
+ };
48
+
49
+ /**
50
+ * Legacy StorageKey enum for backward compatibility
51
+ * @deprecated Use createNamespacedKey instead
52
+ */
53
+ export enum StorageKey {
54
+ USER_PREFERENCES = '@user_preferences',
55
+ APP_SETTINGS = '@app_settings',
56
+ LANGUAGE = '@language',
57
+ UI_PREFERENCES = '@ui_preferences',
58
+ QUERY_CACHE = '@query_cache',
59
+ DATA_CACHE = '@data_cache',
60
+ }
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * - presentation: Hooks (React integration)
14
14
  *
15
15
  * Usage:
16
- * import { useStorage, useStorageState, StorageKey } from '@umituz/react-native-storage';
16
+ * import { useStorage, useStorageState, createStore } from '@umituz/react-native-storage';
17
17
  */
18
18
 
19
19
  // =============================================================================
@@ -48,6 +48,54 @@ export {
48
48
  isFailure,
49
49
  } from './domain/entities/StorageResult';
50
50
 
51
+ // =============================================================================
52
+ // DOMAIN LAYER - Cached Value Entity
53
+ // =============================================================================
54
+
55
+ export type { CachedValue } from './domain/entities/CachedValue';
56
+
57
+ export {
58
+ createCachedValue,
59
+ isCacheExpired,
60
+ getRemainingTTL,
61
+ getCacheAge,
62
+ } from './domain/entities/CachedValue';
63
+
64
+ // =============================================================================
65
+ // DOMAIN LAYER - Cache Utilities
66
+ // =============================================================================
67
+
68
+ export {
69
+ generateCacheKey,
70
+ generateListCacheKey,
71
+ parseCacheKey,
72
+ isCacheKey,
73
+ } from './domain/utils/CacheKeyGenerator';
74
+
75
+ // =============================================================================
76
+ // DOMAIN LAYER - Cache Constants
77
+ // =============================================================================
78
+
79
+ export { TIME_MS, DEFAULT_TTL, CACHE_VERSION } from './domain/constants/CacheDefaults';
80
+
81
+ // =============================================================================
82
+ // DOMAIN LAYER - Development Utilities
83
+ // =============================================================================
84
+
85
+ export { isDev, devWarn, devError, devLog } from './domain/utils/devUtils';
86
+
87
+ // =============================================================================
88
+ // DOMAIN LAYER - Store Types
89
+ // =============================================================================
90
+
91
+ export type { StoreConfig, PersistedState } from './domain/types/Store';
92
+
93
+ // =============================================================================
94
+ // DOMAIN LAYER - Store Factory
95
+ // =============================================================================
96
+
97
+ export { createStore } from './domain/factories/StoreFactory';
98
+
51
99
  // =============================================================================
52
100
  // APPLICATION LAYER - Ports
53
101
  // =============================================================================
@@ -74,3 +122,13 @@ export {
74
122
 
75
123
  export { useStorage } from './presentation/hooks/useStorage';
76
124
  export { useStorageState } from './presentation/hooks/useStorageState';
125
+ export { useStore } from './presentation/hooks/useStore';
126
+
127
+ export {
128
+ usePersistentCache,
129
+ type PersistentCacheOptions,
130
+ type PersistentCacheResult,
131
+ } from './presentation/hooks/usePersistentCache';
132
+
133
+ export { useCacheState } from './presentation/hooks/useCacheState';
134
+ export { CacheStorageOperations } from './presentation/hooks/CacheStorageOperations';
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import AsyncStorage from '@react-native-async-storage/async-storage';
9
+ import { devWarn } from '../../domain/utils/devUtils';
9
10
 
10
11
  /**
11
12
  * StateStorage interface for Zustand persist middleware
@@ -18,13 +19,14 @@ export interface StateStorage {
18
19
 
19
20
  /**
20
21
  * Storage service for Zustand persist middleware
21
- * Direct AsyncStorage implementation
22
+ * Direct AsyncStorage implementation with proper error handling
22
23
  */
23
24
  export const storageService: StateStorage = {
24
25
  getItem: async (name: string): Promise<string | null> => {
25
26
  try {
26
27
  return await AsyncStorage.getItem(name);
27
- } catch {
28
+ } catch (error) {
29
+ devWarn(`StorageService: Failed to get item "${name}"`, error);
28
30
  return null;
29
31
  }
30
32
  },
@@ -32,16 +34,16 @@ export const storageService: StateStorage = {
32
34
  setItem: async (name: string, value: string): Promise<void> => {
33
35
  try {
34
36
  await AsyncStorage.setItem(name, value);
35
- } catch {
36
- // Silent fail
37
+ } catch (error) {
38
+ devWarn(`StorageService: Failed to set item "${name}"`, error);
37
39
  }
38
40
  },
39
41
 
40
42
  removeItem: async (name: string): Promise<void> => {
41
43
  try {
42
44
  await AsyncStorage.removeItem(name);
43
- } catch {
44
- // Silent fail
45
+ } catch (error) {
46
+ devWarn(`StorageService: Failed to remove item "${name}"`, error);
45
47
  }
46
48
  },
47
49
  };
@@ -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
+ }