@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.
- package/package.json +7 -3
- package/src/cache/__tests__/PerformanceAndMemory.test.ts +386 -0
- package/src/cache/__tests__/setup.ts +19 -0
- package/src/cache/domain/Cache.ts +146 -0
- package/src/cache/domain/CacheManager.ts +48 -0
- package/src/cache/domain/CacheStatsTracker.ts +49 -0
- package/src/cache/domain/ErrorHandler.ts +42 -0
- package/src/cache/domain/PatternMatcher.ts +30 -0
- package/src/cache/domain/__tests__/Cache.test.ts +292 -0
- package/src/cache/domain/__tests__/CacheManager.test.ts +276 -0
- package/src/cache/domain/__tests__/ErrorHandler.test.ts +303 -0
- package/src/cache/domain/__tests__/PatternMatcher.test.ts +261 -0
- package/src/cache/domain/strategies/EvictionStrategy.ts +9 -0
- package/src/cache/domain/strategies/FIFOStrategy.ts +12 -0
- package/src/cache/domain/strategies/LFUStrategy.ts +22 -0
- package/src/cache/domain/strategies/LRUStrategy.ts +22 -0
- package/src/cache/domain/strategies/TTLStrategy.ts +23 -0
- package/src/cache/domain/strategies/__tests__/EvictionStrategies.test.ts +293 -0
- package/src/cache/domain/types/Cache.ts +28 -0
- package/src/cache/index.ts +28 -0
- package/src/cache/infrastructure/TTLCache.ts +103 -0
- package/src/cache/infrastructure/__tests__/TTLCache.test.ts +303 -0
- package/src/cache/presentation/__tests__/ReactHooks.test.ts +512 -0
- package/src/cache/presentation/useCache.ts +76 -0
- package/src/cache/presentation/useCachedValue.ts +88 -0
- package/src/cache/types.d.ts +3 -0
- package/src/index.ts +28 -0
- 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
|
+
}
|