@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
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Performance Tests
3
+ *
4
+ * Performance and memory leak tests
5
+ */
6
+
7
+ import { renderHook, act } from '@testing-library/react-hooks';
8
+ import { useStorage, useStorageState, usePersistentCache } from '../../index';
9
+ import { AsyncStorage } from '../mocks/asyncStorage.mock';
10
+ import { mockPerformance, trackMemoryUsage } from '../setup';
11
+
12
+ describe('Performance Tests', () => {
13
+ beforeEach(() => {
14
+ (AsyncStorage as any).__clear();
15
+ jest.clearAllMocks();
16
+ mockPerformance();
17
+ });
18
+
19
+ describe('Memory Leak Prevention', () => {
20
+ it('should not leak memory with multiple hook instances', () => {
21
+ const memoryTracker = trackMemoryUsage();
22
+
23
+ // Create many hook instances
24
+ const hooks = Array.from({ length: 100 }, (_, i) =>
25
+ renderHook(() => useStorage())
26
+ );
27
+
28
+ // Add listeners
29
+ hooks.forEach(hook => {
30
+ hook.current.getItem(`key-${i}`, 'default');
31
+ });
32
+
33
+ const listenerCount = memoryTracker.getListenerCount();
34
+ expect(listenerCount).toBeLessThan(50); // Should be much less than 100
35
+
36
+ // Cleanup
37
+ hooks.forEach(hook => hook.unmount());
38
+ memoryTracker.cleanup();
39
+ });
40
+
41
+ it('should cleanup on unmount', () => {
42
+ const memoryTracker = trackMemoryUsage();
43
+
44
+ const { result, unmount } = renderHook(() => useStorageState('test', 'default'));
45
+
46
+ // Add some async operations
47
+ const promise = result.current[1]('test-value');
48
+
49
+ // Unmount before promise resolves
50
+ unmount();
51
+
52
+ const listenerCount = memoryTracker.getListenerCount();
53
+ expect(listenerCount).toBe(0);
54
+
55
+ memoryTracker.cleanup();
56
+ return promise;
57
+ });
58
+
59
+ it('should not create multiple CacheStorageOperations instances', () => {
60
+ const { result: hook1 } = renderHook(() =>
61
+ usePersistentCache('key1')
62
+ );
63
+
64
+ const { result: hook2 } = renderHook(() =>
65
+ usePersistentCache('key2')
66
+ );
67
+
68
+ const { result: hook3 } = renderHook(() =>
69
+ usePersistentCache('key3')
70
+ );
71
+
72
+ // All should use the same instance (singleton pattern)
73
+ expect(hook1.current.setData).toBe(hook2.current.setData);
74
+ expect(hook2.current.setData).toBe(hook3.current.setData);
75
+ });
76
+ });
77
+
78
+ describe('Performance Benchmarks', () => {
79
+ it('should handle large datasets efficiently', async () => {
80
+ const { result: storageHook } = renderHook(() => useStorage());
81
+
82
+ // Create large dataset (10,000 items)
83
+ const largeData = Array.from({ length: 10000 }, (_, i) => ({
84
+ id: i,
85
+ name: `Item ${i}`,
86
+ description: `Description for item ${i}`,
87
+ metadata: {
88
+ created: Date.now(),
89
+ tags: [`tag${i % 10}`, `category${i % 5}`],
90
+ },
91
+ }));
92
+
93
+ const startTime = performance.now();
94
+
95
+ // Set large data
96
+ const setSuccess = await storageHook.current.setItem('large-data', largeData);
97
+ expect(setSuccess).toBe(true);
98
+
99
+ // Get large data
100
+ const retrievedData = await storageHook.current.getItem('large-data', []);
101
+ expect(retrievedData).toEqual(largeData);
102
+
103
+ const endTime = performance.now();
104
+ const duration = endTime - startTime;
105
+
106
+ // Should complete within reasonable time (less than 1 second)
107
+ expect(duration).toBeLessThan(1000);
108
+ });
109
+
110
+ it('should handle rapid operations efficiently', async () => {
111
+ const { result: storageHook } = renderHook(() => useStorage());
112
+
113
+ const iterations = 1000;
114
+ const startTime = performance.now();
115
+
116
+ // Rapid set/get operations
117
+ for (let i = 0; i < iterations; i++) {
118
+ const key = `rapid-key-${i}`;
119
+ const value = `rapid-value-${i}`;
120
+
121
+ await storageHook.current.setItem(key, value);
122
+ const retrieved = await storageHook.current.getItem(key, '');
123
+ expect(retrieved).toBe(value);
124
+ }
125
+
126
+ const endTime = performance.now();
127
+ const duration = endTime - startTime;
128
+ const avgTime = duration / iterations;
129
+
130
+ // Average operation should be fast (less than 1ms per operation)
131
+ expect(avgTime).toBeLessThan(1);
132
+ });
133
+
134
+ it('should handle concurrent operations efficiently', async () => {
135
+ const { result: storageHook } = renderHook(() => useStorage());
136
+
137
+ const concurrentOperations = 100;
138
+ const operations = Array.from({ length: concurrentOperations }, (_, i) =>
139
+ storageHook.current.setItem(`concurrent-key-${i}`, `concurrent-value-${i}`)
140
+ );
141
+
142
+ const startTime = performance.now();
143
+
144
+ // Execute all operations concurrently
145
+ const results = await Promise.all(operations);
146
+
147
+ const endTime = performance.now();
148
+ const duration = endTime - startTime;
149
+
150
+ // All operations should succeed
151
+ expect(results.every(success => success)).toBe(true);
152
+
153
+ // Concurrent operations should be faster than sequential
154
+ expect(duration).toBeLessThan(concurrentOperations * 10); // Less than 10ms per operation
155
+ });
156
+ });
157
+
158
+ describe('Cache Performance', () => {
159
+ it('should handle cache operations efficiently', async () => {
160
+ const { result: cacheHook } = renderHook(() =>
161
+ usePersistentCache('performance-cache')
162
+ );
163
+
164
+ const cacheOperations = 1000;
165
+ const startTime = performance.now();
166
+
167
+ // Rapid cache operations
168
+ for (let i = 0; i < cacheOperations; i++) {
169
+ const data = { id: i, value: `cache-value-${i}` };
170
+
171
+ await cacheHook.current.setData(data);
172
+ expect(cacheHook.current.data).toEqual(data);
173
+ }
174
+
175
+ const endTime = performance.now();
176
+ const duration = endTime - startTime;
177
+ const avgTime = duration / cacheOperations;
178
+
179
+ // Cache operations should be very fast
180
+ expect(avgTime).toBeLessThan(0.1); // Less than 0.1ms per operation
181
+ });
182
+
183
+ it('should handle TTL checks efficiently', async () => {
184
+ const { result: cacheHook } = renderHook(() =>
185
+ usePersistentCache('ttl-cache', { ttl: 60000 })
186
+ );
187
+
188
+ const data = { test: 'value' };
189
+ await cacheHook.current.setData(data);
190
+
191
+ const startTime = performance.now();
192
+
193
+ // Check TTL many times (should be cached)
194
+ for (let i = 0; i < 10000; i++) {
195
+ // Access isExpired property
196
+ const isExpired = cacheHook.current.isExpired;
197
+ expect(isExpired).toBe(false);
198
+ }
199
+
200
+ const endTime = performance.now();
201
+ const duration = endTime - startTime;
202
+
203
+ // TTL checks should be extremely fast
204
+ expect(duration).toBeLessThan(10); // Less than 10ms for 10,000 checks
205
+ });
206
+ });
207
+
208
+ describe('State Performance', () => {
209
+ it('should handle state updates efficiently', async () => {
210
+ const { result: stateHook, waitForNextUpdate } = renderHook(() =>
211
+ useStorageState('performance-state', 'initial')
212
+ );
213
+
214
+ await act(async () => {
215
+ await waitForNextUpdate();
216
+ });
217
+
218
+ const stateUpdates = 1000;
219
+ const startTime = performance.now();
220
+
221
+ // Rapid state updates
222
+ for (let i = 0; i < stateUpdates; i++) {
223
+ await act(async () => {
224
+ await stateHook.current[1](`update-${i}`);
225
+ });
226
+ expect(stateHook.current[0]).toBe(`update-${i}`);
227
+ }
228
+
229
+ const endTime = performance.now();
230
+ const duration = endTime - startTime;
231
+ const avgTime = duration / stateUpdates;
232
+
233
+ // State updates should be fast
234
+ expect(avgTime).toBeLessThan(1); // Less than 1ms per update
235
+ });
236
+
237
+ it('should not re-render unnecessarily', async () => {
238
+ let renderCount = 0;
239
+
240
+ const TestComponent = () => {
241
+ renderCount++;
242
+ const [state] = useStorageState('render-test', 'default');
243
+ return null; // Don't render anything
244
+ };
245
+
246
+ const { rerender } = renderHook(() => <TestComponent />);
247
+
248
+ const initialRenders = renderCount;
249
+
250
+ // Rerender with same props
251
+ rerender(<TestComponent />);
252
+
253
+ // Should not cause additional renders
254
+ expect(renderCount).toBe(initialRenders);
255
+ });
256
+ });
257
+
258
+ describe('Memory Usage', () => {
259
+ it('should maintain stable memory usage', async () => {
260
+ const { result: storageHook } = renderHook(() => useStorage());
261
+
262
+ const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
263
+
264
+ // Create and destroy many objects
265
+ for (let i = 0; i < 1000; i++) {
266
+ const largeObject = {
267
+ id: i,
268
+ data: new Array(1000).fill(`data-${i}`),
269
+ nested: {
270
+ level1: { level2: { level3: new Array(100).fill(`nested-${i}`) } },
271
+ },
272
+ };
273
+
274
+ await storageHook.current.setItem(`temp-${i}`, largeObject);
275
+ await storageHook.current.removeItem(`temp-${i}`);
276
+ }
277
+
278
+ // Force garbage collection if available
279
+ if (global.gc) {
280
+ global.gc();
281
+ }
282
+
283
+ const finalMemory = (performance as any).memory?.usedJSHeapSize || 0;
284
+ const memoryIncrease = finalMemory - initialMemory;
285
+
286
+ // Memory increase should be minimal
287
+ expect(memoryIncrease).toBeLessThan(1024 * 1024); // Less than 1MB increase
288
+ });
289
+
290
+ it('should cleanup event listeners', () => {
291
+ const memoryTracker = trackMemoryUsage();
292
+
293
+ // Create and destroy many hooks
294
+ for (let i = 0; i < 100; i++) {
295
+ const { unmount } = renderHook(() => useStorageState(`cleanup-${i}`, 'default'));
296
+ unmount();
297
+ }
298
+
299
+ const listenerCount = memoryTracker.getListenerCount();
300
+ expect(listenerCount).toBe(0);
301
+
302
+ memoryTracker.cleanup();
303
+ });
304
+ });
305
+
306
+ describe('Scalability Tests', () => {
307
+ it('should handle many keys efficiently', async () => {
308
+ const { result: storageHook } = renderHook(() => useStorage());
309
+
310
+ const keyCount = 10000;
311
+ const startTime = performance.now();
312
+
313
+ // Create many keys
314
+ for (let i = 0; i < keyCount; i++) {
315
+ await storageHook.current.setItem(`scale-key-${i}`, `scale-value-${i}`);
316
+ }
317
+
318
+ const endTime = performance.now();
319
+ const duration = endTime - startTime;
320
+
321
+ // Should handle many keys efficiently
322
+ expect(duration).toBeLessThan(keyCount * 0.1); // Less than 0.1ms per key
323
+
324
+ // Verify all keys exist
325
+ for (let i = 0; i < keyCount; i++) {
326
+ const exists = await storageHook.current.hasItem(`scale-key-${i}`);
327
+ expect(exists).toBe(true);
328
+ }
329
+ });
330
+
331
+ it('should handle large values efficiently', async () => {
332
+ const { result: storageHook } = renderHook(() => useStorage());
333
+
334
+ // Create very large value (1MB string)
335
+ const largeValue = 'x'.repeat(1024 * 1024);
336
+ const startTime = performance.now();
337
+
338
+ const setSuccess = await storageHook.current.setItem('large-value', largeValue);
339
+ expect(setSuccess).toBe(true);
340
+
341
+ const retrievedValue = await storageHook.current.getString('large-value', '');
342
+ expect(retrievedValue).toBe(largeValue);
343
+
344
+ const endTime = performance.now();
345
+ const duration = endTime - startTime;
346
+
347
+ // Large values should be handled efficiently
348
+ expect(duration).toBeLessThan(100); // Less than 100ms for 1MB
349
+ });
350
+ });
351
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Test Setup
3
+ *
4
+ * Global test configuration and mocks
5
+ */
6
+
7
+ import 'jest-environment-jsdom';
8
+
9
+ // Mock __DEV__ global
10
+ Object.defineProperty(globalThis, '__DEV__', {
11
+ value: true,
12
+ writable: true,
13
+ });
14
+
15
+ // Mock AsyncStorage
16
+ jest.mock('@react-native-async-storage/async-storage', () => ({
17
+ default: {
18
+ getItem: jest.fn(),
19
+ setItem: jest.fn(),
20
+ removeItem: jest.fn(),
21
+ clear: jest.fn(),
22
+ getAllKeys: jest.fn(),
23
+ multiGet: jest.fn(),
24
+ },
25
+ }));
26
+
27
+ // Mock console for __DEV__ checks
28
+ const originalConsole = { ...console };
29
+
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+ });
33
+
34
+ afterEach(() => {
35
+ // Restore console after each test
36
+ Object.assign(console, originalConsole);
37
+ });
38
+
39
+ // Performance test utilities
40
+ export const mockPerformance = () => {
41
+ const mockPerformance = {
42
+ now: jest.fn(() => Date.now()),
43
+ mark: jest.fn(),
44
+ measure: jest.fn(),
45
+ };
46
+
47
+ globalThis.performance = mockPerformance as any;
48
+ return mockPerformance;
49
+ };
50
+
51
+ // Memory leak test utilities
52
+ export const trackMemoryUsage = () => {
53
+ const listeners = new Set<() => void>();
54
+
55
+ return {
56
+ addListener: (listener: () => void) => {
57
+ listeners.add(listener);
58
+ return () => listeners.delete(listener);
59
+ },
60
+ getListenerCount: () => listeners.size,
61
+ cleanup: () => listeners.clear(),
62
+ };
63
+ };
@@ -3,8 +3,6 @@
3
3
  *
4
4
  * Domain-Driven Design: Application port for storage operations
5
5
  * Infrastructure layer implements this interface
6
- *
7
- * Theme: {{THEME_NAME}} ({{CATEGORY}} category)
8
6
  */
9
7
 
10
8
  import type { StorageResult } from '../../domain/entities/StorageResult';
@@ -59,15 +57,5 @@ export interface IStorageRepository {
59
57
  */
60
58
  getAllKeys(): Promise<StorageResult<string[]>>;
61
59
 
62
- /**
63
- * Get object from storage (alias for getItem for backward compatibility)
64
- * @deprecated Use getItem instead
65
- */
66
- getObject<T>(key: string, defaultValue: T): Promise<StorageResult<T>>;
67
60
 
68
- /**
69
- * Set object in storage (alias for setItem for backward compatibility)
70
- * @deprecated Use setItem instead
71
- */
72
- setObject<T>(key: string, value: T): Promise<StorageResult<T>>;
73
61
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Cache Default Constants
3
+ * Domain layer - Default values for caching
4
+ *
5
+ * General-purpose constants for any app
6
+ */
7
+
8
+ /**
9
+ * Time constants in milliseconds
10
+ */
11
+ export const TIME_MS = {
12
+ SECOND: 1000,
13
+ MINUTE: 60 * 1000,
14
+ HOUR: 60 * 60 * 1000,
15
+ DAY: 24 * 60 * 60 * 1000,
16
+ WEEK: 7 * 24 * 60 * 60 * 1000,
17
+ } as const;
18
+
19
+ /**
20
+ * Default TTL values for different cache types
21
+ */
22
+ export const DEFAULT_TTL = {
23
+ /**
24
+ * Very short cache (1 minute)
25
+ * Use for: Real-time data, live updates
26
+ */
27
+ VERY_SHORT: TIME_MS.MINUTE,
28
+
29
+ /**
30
+ * Short cache (5 minutes)
31
+ * Use for: Frequently changing data
32
+ */
33
+ SHORT: 5 * TIME_MS.MINUTE,
34
+
35
+ /**
36
+ * Medium cache (30 minutes)
37
+ * Use for: Moderately changing data, user-specific content
38
+ */
39
+ MEDIUM: 30 * TIME_MS.MINUTE,
40
+
41
+ /**
42
+ * Long cache (2 hours)
43
+ * Use for: Slowly changing data, public content
44
+ */
45
+ LONG: 2 * TIME_MS.HOUR,
46
+
47
+ /**
48
+ * Very long cache (24 hours)
49
+ * Use for: Rarely changing data, master data
50
+ */
51
+ VERY_LONG: TIME_MS.DAY,
52
+
53
+ /**
54
+ * Permanent cache (7 days)
55
+ * Use for: Static content, app configuration
56
+ */
57
+ PERMANENT: TIME_MS.WEEK,
58
+ } as const;
59
+
60
+ /**
61
+ * Cache version for global invalidation
62
+ * Increment this to invalidate all caches across the app
63
+ */
64
+ export const CACHE_VERSION = 1;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Cached Value Entity
3
+ * Domain layer - Represents a cached value with TTL metadata
4
+ *
5
+ * General-purpose cache entity for any app that needs persistent caching
6
+ */
7
+
8
+ /**
9
+ * Cached value with time-to-live metadata
10
+ * Generic type T can be any serializable data
11
+ */
12
+ export interface CachedValue<T> {
13
+ /**
14
+ * The actual cached data
15
+ */
16
+ value: T;
17
+
18
+ /**
19
+ * Timestamp when the value was cached (milliseconds)
20
+ */
21
+ cachedAt: number;
22
+
23
+ /**
24
+ * Timestamp when the cache expires (milliseconds)
25
+ */
26
+ expiresAt: number;
27
+
28
+ /**
29
+ * Optional version for cache invalidation
30
+ * Increment version to invalidate all caches
31
+ */
32
+ version?: number;
33
+ }
34
+
35
+ /**
36
+ * Create a new cached value with TTL
37
+ * @param value - Data to cache
38
+ * @param ttlMs - Time-to-live in milliseconds
39
+ * @param version - Optional version number
40
+ */
41
+ export function createCachedValue<T>(
42
+ value: T,
43
+ ttlMs: number,
44
+ version?: number,
45
+ ): CachedValue<T> {
46
+ const now = Date.now();
47
+ return {
48
+ value,
49
+ cachedAt: now,
50
+ expiresAt: now + ttlMs,
51
+ version,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Check if cached value is expired
57
+ * @param cached - Cached value to check
58
+ * @param currentVersion - Optional current version to check against
59
+ */
60
+ export function isCacheExpired<T>(
61
+ cached: CachedValue<T>,
62
+ currentVersion?: number,
63
+ ): boolean {
64
+ const now = Date.now();
65
+ const timeExpired = now > cached.expiresAt;
66
+ const versionMismatch = currentVersion !== undefined && cached.version !== currentVersion;
67
+ return timeExpired || versionMismatch;
68
+ }
69
+
70
+ /**
71
+ * Get remaining TTL in milliseconds
72
+ * Returns 0 if expired
73
+ */
74
+ export function getRemainingTTL<T>(cached: CachedValue<T>): number {
75
+ const now = Date.now();
76
+ const remaining = cached.expiresAt - now;
77
+ return Math.max(0, remaining);
78
+ }
79
+
80
+ /**
81
+ * Get cache age in milliseconds
82
+ */
83
+ export function getCacheAge<T>(cached: CachedValue<T>): number {
84
+ const now = Date.now();
85
+ return now - cached.cachedAt;
86
+ }
@@ -3,8 +3,6 @@
3
3
  *
4
4
  * Domain-Driven Design: Entity representing storage operation result
5
5
  * Functional programming pattern for error handling (Result type)
6
- *
7
- * Theme: {{THEME_NAME}} ({{CATEGORY}} category)
8
6
  */
9
7
 
10
8
  import type { StorageError } from '../errors/StorageError';
@@ -68,5 +66,5 @@ export const map = <T, U>(
68
66
  if (isSuccess(result)) {
69
67
  return success(fn(result.data));
70
68
  }
71
- return failure(result.error, result.fallback !== undefined ? fn(result.fallback) : undefined);
69
+ return failure(result.error);
72
70
  };