@umituz/react-native-storage 2.5.0 → 2.6.1

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 (28) hide show
  1. package/package.json +7 -3
  2. package/src/cache/__tests__/PerformanceAndMemory.test.ts +386 -0
  3. package/src/cache/__tests__/setup.ts +19 -0
  4. package/src/cache/domain/Cache.ts +146 -0
  5. package/src/cache/domain/CacheManager.ts +48 -0
  6. package/src/cache/domain/CacheStatsTracker.ts +49 -0
  7. package/src/cache/domain/ErrorHandler.ts +42 -0
  8. package/src/cache/domain/PatternMatcher.ts +30 -0
  9. package/src/cache/domain/__tests__/Cache.test.ts +292 -0
  10. package/src/cache/domain/__tests__/CacheManager.test.ts +276 -0
  11. package/src/cache/domain/__tests__/ErrorHandler.test.ts +303 -0
  12. package/src/cache/domain/__tests__/PatternMatcher.test.ts +261 -0
  13. package/src/cache/domain/strategies/EvictionStrategy.ts +9 -0
  14. package/src/cache/domain/strategies/FIFOStrategy.ts +12 -0
  15. package/src/cache/domain/strategies/LFUStrategy.ts +22 -0
  16. package/src/cache/domain/strategies/LRUStrategy.ts +22 -0
  17. package/src/cache/domain/strategies/TTLStrategy.ts +23 -0
  18. package/src/cache/domain/strategies/__tests__/EvictionStrategies.test.ts +293 -0
  19. package/src/cache/domain/types/Cache.ts +28 -0
  20. package/src/cache/index.ts +28 -0
  21. package/src/cache/infrastructure/TTLCache.ts +103 -0
  22. package/src/cache/infrastructure/__tests__/TTLCache.test.ts +303 -0
  23. package/src/cache/presentation/__tests__/ReactHooks.test.ts +512 -0
  24. package/src/cache/presentation/useCache.ts +76 -0
  25. package/src/cache/presentation/useCachedValue.ts +88 -0
  26. package/src/cache/types.d.ts +3 -0
  27. package/src/index.ts +28 -0
  28. package/src/types/global.d.ts +2 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Eviction Strategies Tests
3
+ */
4
+
5
+ import { LRUStrategy } from '../LRUStrategy';
6
+ import { LFUStrategy } from '../LFUStrategy';
7
+ import { FIFOStrategy } from '../FIFOStrategy';
8
+ import { TTLStrategy } from '../TTLStrategy';
9
+ import type { CacheEntry } from '../../types/Cache';
10
+
11
+ describe('Eviction Strategies', () => {
12
+ const createMockEntries = (): Map<string, CacheEntry<string>> => {
13
+ const entries = new Map<string, CacheEntry<string>>();
14
+ const now = Date.now();
15
+
16
+ entries.set('key1', {
17
+ value: 'value1',
18
+ timestamp: now - 1000,
19
+ ttl: 5000,
20
+ accessCount: 5,
21
+ lastAccess: now - 500,
22
+ });
23
+
24
+ entries.set('key2', {
25
+ value: 'value2',
26
+ timestamp: now - 2000,
27
+ ttl: 5000,
28
+ accessCount: 3,
29
+ lastAccess: now - 100,
30
+ });
31
+
32
+ entries.set('key3', {
33
+ value: 'value3',
34
+ timestamp: now - 3000,
35
+ ttl: 5000,
36
+ accessCount: 10,
37
+ lastAccess: now - 1500,
38
+ });
39
+
40
+ return entries;
41
+ };
42
+
43
+ describe('LRUStrategy', () => {
44
+ let strategy: LRUStrategy<string>;
45
+
46
+ beforeEach(() => {
47
+ strategy = new LRUStrategy<string>();
48
+ });
49
+
50
+ test('should evict least recently used key', () => {
51
+ const entries = createMockEntries();
52
+ const keyToEvict = strategy.findKeyToEvict(entries);
53
+
54
+ // key3 has the oldest lastAccess time
55
+ expect(keyToEvict).toBe('key3');
56
+ });
57
+
58
+ test('should return undefined for empty entries', () => {
59
+ const entries = new Map<string, CacheEntry<string>>();
60
+ const keyToEvict = strategy.findKeyToEvict(entries);
61
+
62
+ expect(keyToEvict).toBeUndefined();
63
+ });
64
+
65
+ test('should handle single entry', () => {
66
+ const entries = new Map<string, CacheEntry<string>>();
67
+ entries.set('key1', {
68
+ value: 'value1',
69
+ timestamp: Date.now(),
70
+ ttl: 5000,
71
+ accessCount: 1,
72
+ lastAccess: Date.now(),
73
+ });
74
+
75
+ const keyToEvict = strategy.findKeyToEvict(entries);
76
+ expect(keyToEvict).toBe('key1');
77
+ });
78
+ });
79
+
80
+ describe('LFUStrategy', () => {
81
+ let strategy: LFUStrategy<string>;
82
+
83
+ beforeEach(() => {
84
+ strategy = new LFUStrategy<string>();
85
+ });
86
+
87
+ test('should evict least frequently used key', () => {
88
+ const entries = createMockEntries();
89
+ const keyToEvict = strategy.findKeyToEvict(entries);
90
+
91
+ // key2 has the lowest access count (3)
92
+ expect(keyToEvict).toBe('key2');
93
+ });
94
+
95
+ test('should return undefined for empty entries', () => {
96
+ const entries = new Map<string, CacheEntry<string>>();
97
+ const keyToEvict = strategy.findKeyToEvict(entries);
98
+
99
+ expect(keyToEvict).toBeUndefined();
100
+ });
101
+
102
+ test('should handle ties by choosing first encountered', () => {
103
+ const entries = new Map<string, CacheEntry<string>>();
104
+ const now = Date.now();
105
+
106
+ entries.set('key1', {
107
+ value: 'value1',
108
+ timestamp: now,
109
+ ttl: 5000,
110
+ accessCount: 2,
111
+ lastAccess: now,
112
+ });
113
+
114
+ entries.set('key2', {
115
+ value: 'value2',
116
+ timestamp: now,
117
+ ttl: 5000,
118
+ accessCount: 2,
119
+ lastAccess: now,
120
+ });
121
+
122
+ const keyToEvict = strategy.findKeyToEvict(entries);
123
+ // Should return the first key with lowest count
124
+ expect(keyToEvict).toBe('key1');
125
+ });
126
+ });
127
+
128
+ describe('FIFOStrategy', () => {
129
+ let strategy: FIFOStrategy<string>;
130
+
131
+ beforeEach(() => {
132
+ strategy = new FIFOStrategy<string>();
133
+ });
134
+
135
+ test('should evict first inserted key', () => {
136
+ const entries = createMockEntries();
137
+ const keyToEvict = strategy.findKeyToEvict(entries);
138
+
139
+ // Map preserves insertion order, so first key should be evicted
140
+ expect(keyToEvict).toBe('key1');
141
+ });
142
+
143
+ test('should return undefined for empty entries', () => {
144
+ const entries = new Map<string, CacheEntry<string>>();
145
+ const keyToEvict = strategy.findKeyToEvict(entries);
146
+
147
+ expect(keyToEvict).toBeUndefined();
148
+ });
149
+
150
+ test('should handle single entry', () => {
151
+ const entries = new Map<string, CacheEntry<string>>();
152
+ entries.set('onlyKey', {
153
+ value: 'value',
154
+ timestamp: Date.now(),
155
+ ttl: 5000,
156
+ accessCount: 1,
157
+ lastAccess: Date.now(),
158
+ });
159
+
160
+ const keyToEvict = strategy.findKeyToEvict(entries);
161
+ expect(keyToEvict).toBe('onlyKey');
162
+ });
163
+ });
164
+
165
+ describe('TTLStrategy', () => {
166
+ let strategy: TTLStrategy<string>;
167
+
168
+ beforeEach(() => {
169
+ strategy = new TTLStrategy<string>();
170
+ });
171
+
172
+ test('should evict key with nearest expiry', () => {
173
+ const now = Date.now();
174
+ const entries = new Map<string, CacheEntry<string>>();
175
+
176
+ entries.set('key1', {
177
+ value: 'value1',
178
+ timestamp: now - 1000,
179
+ ttl: 2000, // Expires at now + 1000
180
+ accessCount: 1,
181
+ lastAccess: now,
182
+ });
183
+
184
+ entries.set('key2', {
185
+ value: 'value2',
186
+ timestamp: now - 500,
187
+ ttl: 1000, // Expires at now + 500
188
+ accessCount: 1,
189
+ lastAccess: now,
190
+ });
191
+
192
+ entries.set('key3', {
193
+ value: 'value3',
194
+ timestamp: now - 2000,
195
+ ttl: 3000, // Expires at now + 1000
196
+ accessCount: 1,
197
+ lastAccess: now,
198
+ });
199
+
200
+ const keyToEvict = strategy.findKeyToEvict(entries);
201
+
202
+ // key2 expires soonest (now + 500)
203
+ expect(keyToEvict).toBe('key2');
204
+ });
205
+
206
+ test('should return undefined for empty entries', () => {
207
+ const entries = new Map<string, CacheEntry<string>>();
208
+ const keyToEvict = strategy.findKeyToEvict(entries);
209
+
210
+ expect(keyToEvict).toBeUndefined();
211
+ });
212
+
213
+ test('should handle already expired entries', () => {
214
+ const now = Date.now();
215
+ const entries = new Map<string, CacheEntry<string>>();
216
+
217
+ entries.set('key1', {
218
+ value: 'value1',
219
+ timestamp: now - 2000,
220
+ ttl: 1000, // Already expired
221
+ accessCount: 1,
222
+ lastAccess: now,
223
+ });
224
+
225
+ entries.set('key2', {
226
+ value: 'value2',
227
+ timestamp: now - 1000,
228
+ ttl: 2000, // Expires at now + 1000
229
+ accessCount: 1,
230
+ lastAccess: now,
231
+ });
232
+
233
+ const keyToEvict = strategy.findKeyToEvict(entries);
234
+
235
+ // key1 is already expired, should be evicted first
236
+ expect(keyToEvict).toBe('key1');
237
+ });
238
+
239
+ test('should handle ties by choosing first encountered', () => {
240
+ const now = Date.now();
241
+ const entries = new Map<string, CacheEntry<string>>();
242
+
243
+ entries.set('key1', {
244
+ value: 'value1',
245
+ timestamp: now - 1000,
246
+ ttl: 2000, // Both expire at now + 1000
247
+ accessCount: 1,
248
+ lastAccess: now,
249
+ });
250
+
251
+ entries.set('key2', {
252
+ value: 'value2',
253
+ timestamp: now - 1000,
254
+ ttl: 2000, // Both expire at now + 1000
255
+ accessCount: 1,
256
+ lastAccess: now,
257
+ });
258
+
259
+ const keyToEvict = strategy.findKeyToEvict(entries);
260
+
261
+ // Should return the first key with nearest expiry
262
+ expect(keyToEvict).toBe('key1');
263
+ });
264
+ });
265
+
266
+ describe('Strategy Integration', () => {
267
+ test('all strategies should handle different data types', () => {
268
+ const numberStrategy = new LRUStrategy<number>();
269
+ const objectStrategy = new LRUStrategy<{ id: string }>();
270
+
271
+ const numberEntries = new Map<string, CacheEntry<number>>();
272
+ numberEntries.set('num1', {
273
+ value: 42,
274
+ timestamp: Date.now(),
275
+ ttl: 5000,
276
+ accessCount: 1,
277
+ lastAccess: Date.now(),
278
+ });
279
+
280
+ const objectEntries = new Map<string, CacheEntry<{ id: string }>>();
281
+ objectEntries.set('obj1', {
282
+ value: { id: 'test' },
283
+ timestamp: Date.now(),
284
+ ttl: 5000,
285
+ accessCount: 1,
286
+ lastAccess: Date.now(),
287
+ });
288
+
289
+ expect(numberStrategy.findKeyToEvict(numberEntries)).toBe('num1');
290
+ expect(objectStrategy.findKeyToEvict(objectEntries)).toBe('obj1');
291
+ });
292
+ });
293
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Cache Types
3
+ */
4
+
5
+ export interface CacheEntry<T> {
6
+ value: T;
7
+ timestamp: number;
8
+ ttl: number;
9
+ accessCount: number;
10
+ lastAccess: number;
11
+ }
12
+
13
+ export interface CacheConfig {
14
+ maxSize?: number;
15
+ defaultTTL?: number;
16
+ onEvict?: (key: string, entry: CacheEntry<unknown>) => void;
17
+ onExpire?: (key: string, entry: CacheEntry<unknown>) => void;
18
+ }
19
+
20
+ export interface CacheStats {
21
+ size: number;
22
+ hits: number;
23
+ misses: number;
24
+ evictions: number;
25
+ expirations: number;
26
+ }
27
+
28
+ export type EvictionStrategy = 'lru' | 'lfu' | 'fifo' | 'ttl';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @umituz/react-native-cache
3
+ * In-memory caching utilities for React Native
4
+ */
5
+
6
+ export { Cache } from './domain/Cache';
7
+ export { CacheManager, cacheManager } from './domain/CacheManager';
8
+ export { CacheStatsTracker } from './domain/CacheStatsTracker';
9
+ export { PatternMatcher } from './domain/PatternMatcher';
10
+ export { ErrorHandler, CacheError } from './domain/ErrorHandler';
11
+ export { TTLCache } from './infrastructure/TTLCache';
12
+
13
+ export type {
14
+ CacheEntry,
15
+ CacheConfig,
16
+ CacheStats,
17
+ EvictionStrategy,
18
+ } from './domain/types/Cache';
19
+
20
+ export type { EvictionStrategy as IEvictionStrategy } from './domain/strategies/EvictionStrategy';
21
+
22
+ export { LRUStrategy } from './domain/strategies/LRUStrategy';
23
+ export { LFUStrategy } from './domain/strategies/LFUStrategy';
24
+ export { FIFOStrategy } from './domain/strategies/FIFOStrategy';
25
+ export { TTLStrategy as TTLEvictionStrategy } from './domain/strategies/TTLStrategy';
26
+
27
+ export { useCache } from './presentation/useCache';
28
+ export { useCachedValue } from './presentation/useCachedValue';
@@ -0,0 +1,103 @@
1
+ /**
2
+ * TTL Cache
3
+ * Time-to-live cache with automatic cleanup
4
+ */
5
+
6
+ import { Cache } from '../domain/Cache';
7
+ import type { CacheConfig } from '../domain/types/Cache';
8
+
9
+ export class TTLCache<T = unknown> extends Cache<T> {
10
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
11
+ private isDestroyed = false;
12
+ private readonly cleanupIntervalMs: number;
13
+
14
+ constructor(config: CacheConfig & { cleanupIntervalMs?: number } = {}) {
15
+ super(config);
16
+
17
+ this.cleanupIntervalMs = config.cleanupIntervalMs || 60000;
18
+ this.startCleanup();
19
+ }
20
+
21
+ private startCleanup(): void {
22
+ if (this.isDestroyed) return;
23
+
24
+ this.cleanupInterval = setInterval(() => {
25
+ if (!this.isDestroyed) {
26
+ this.cleanup();
27
+ }
28
+ }, this.cleanupIntervalMs);
29
+ }
30
+
31
+ private cleanup(): void {
32
+ if (this.isDestroyed) return;
33
+
34
+ const keys = this.keys();
35
+ let cleanedCount = 0;
36
+ const now = Date.now();
37
+
38
+ for (const key of keys) {
39
+ const entry = (this as any).store.get(key);
40
+ if (entry && (now - entry.timestamp) > entry.ttl) {
41
+ (this as any).store.delete(key);
42
+ cleanedCount++;
43
+ }
44
+ }
45
+
46
+ if (cleanedCount > 0) {
47
+ (this as any).statsTracker.updateSize((this as any).store.size);
48
+ (this as any).statsTracker.recordExpiration();
49
+
50
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
51
+ console.log(`TTLCache: Cleaned up ${cleanedCount} expired entries`);
52
+ }
53
+ }
54
+ }
55
+
56
+ destroy(): void {
57
+ if (this.isDestroyed) return;
58
+
59
+ this.isDestroyed = true;
60
+
61
+ if (this.cleanupInterval) {
62
+ clearInterval(this.cleanupInterval);
63
+ this.cleanupInterval = null;
64
+ }
65
+
66
+ this.clear();
67
+ }
68
+
69
+ override set(key: string, value: T, ttl?: number): void {
70
+ if (this.isDestroyed) {
71
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
72
+ console.warn('TTLCache: Attempted to set value on destroyed cache');
73
+ }
74
+ return;
75
+ }
76
+ super.set(key, value, ttl);
77
+ }
78
+
79
+ override get(key: string): T | undefined {
80
+ if (this.isDestroyed) {
81
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
82
+ console.warn('TTLCache: Attempted to get value from destroyed cache');
83
+ }
84
+ return undefined;
85
+ }
86
+ return super.get(key);
87
+ }
88
+
89
+ override has(key: string): boolean {
90
+ if (this.isDestroyed) return false;
91
+ return super.has(key);
92
+ }
93
+
94
+ override delete(key: string): boolean {
95
+ if (this.isDestroyed) return false;
96
+ return super.delete(key);
97
+ }
98
+
99
+ override clear(): void {
100
+ if (this.isDestroyed) return;
101
+ super.clear();
102
+ }
103
+ }