@fjell/cache 4.7.0 → 4.7.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.
- package/CACHE_EVENTS.md +306 -0
- package/CACHE_IMPLEMENTATIONS.md +117 -0
- package/MEMORY_LEAK_FIXES.md +270 -0
- package/README.md +7 -0
- package/dist/Aggregator.d.ts +27 -16
- package/dist/Cache.d.ts +55 -0
- package/dist/CacheContext.d.ts +13 -5
- package/dist/CacheMap.d.ts +59 -16
- package/dist/CacheStats.d.ts +51 -0
- package/dist/Operations.d.ts +20 -17
- package/dist/Options.d.ts +1 -3
- package/dist/browser/AsyncIndexDBCacheMap.d.ts +1 -1
- package/dist/browser/IndexDBCacheMap.d.ts +26 -11
- package/dist/browser/LocalStorageCacheMap.d.ts +30 -14
- package/dist/browser/SessionStorageCacheMap.d.ts +27 -11
- package/dist/events/CacheEventEmitter.d.ts +82 -0
- package/dist/events/CacheEventFactory.d.ts +121 -0
- package/dist/events/CacheEventTypes.d.ts +122 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/eviction/EvictionManager.d.ts +57 -0
- package/dist/eviction/EvictionStrategy.d.ts +102 -10
- package/dist/eviction/index.d.ts +2 -1
- package/dist/eviction/strategies/ARCEvictionStrategy.d.ts +10 -5
- package/dist/eviction/strategies/FIFOEvictionStrategy.d.ts +6 -5
- package/dist/eviction/strategies/LFUEvictionStrategy.d.ts +6 -5
- package/dist/eviction/strategies/LRUEvictionStrategy.d.ts +6 -5
- package/dist/eviction/strategies/MRUEvictionStrategy.d.ts +6 -5
- package/dist/eviction/strategies/RandomEvictionStrategy.d.ts +6 -5
- package/dist/eviction/strategies/TwoQueueEvictionStrategy.d.ts +6 -5
- package/dist/index.d.ts +9 -3
- package/dist/index.js +4739 -2843
- package/dist/index.js.map +4 -4
- package/dist/memory/EnhancedMemoryCacheMap.d.ts +28 -22
- package/dist/memory/MemoryCacheMap.d.ts +26 -11
- package/dist/normalization.d.ts +1 -2
- package/dist/ops/get.d.ts +1 -0
- package/dist/ttl/TTLManager.d.ts +100 -0
- package/dist/ttl/index.d.ts +2 -0
- package/dist/utils/CacheSize.d.ts +0 -7
- package/fix-async-tests.js +116 -0
- package/package.json +16 -13
- package/debug_test2.js +0 -0
- package/debug_test3.js +0 -0
package/CACHE_EVENTS.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Cache Event System
|
|
2
|
+
|
|
3
|
+
The fjell-cache event system provides real-time notifications when cache operations occur. This enables reactive patterns where components can subscribe to cache changes and update automatically.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The event system is built into the core Cache interface and provides:
|
|
8
|
+
|
|
9
|
+
- **Event emission** for all cache operations (create, update, remove, query, etc.)
|
|
10
|
+
- **Subscription management** with filtering options
|
|
11
|
+
- **Event types** covering all cache state changes
|
|
12
|
+
- **Debouncing** support for high-frequency updates
|
|
13
|
+
- **Type safety** with full TypeScript support
|
|
14
|
+
|
|
15
|
+
## Basic Usage
|
|
16
|
+
|
|
17
|
+
### Subscribing to Events
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { createCache, CacheEventListener } from '@fjell/cache';
|
|
21
|
+
|
|
22
|
+
// Create cache
|
|
23
|
+
const cache = createCache(api, coordinate, registry);
|
|
24
|
+
|
|
25
|
+
// Subscribe to all events
|
|
26
|
+
const listener: CacheEventListener<MyItem, 'myType'> = (event) => {
|
|
27
|
+
console.log('Cache event:', event.type, event);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const subscription = cache.subscribe(listener);
|
|
31
|
+
|
|
32
|
+
// Later: unsubscribe
|
|
33
|
+
subscription.unsubscribe();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Filtering Events
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Subscribe only to item creation events
|
|
40
|
+
const subscription = cache.subscribe(listener, {
|
|
41
|
+
eventTypes: ['item_created', 'item_updated']
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Subscribe only to events for specific keys
|
|
45
|
+
const subscription = cache.subscribe(listener, {
|
|
46
|
+
keys: [{ pk: 'user-123' }]
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Subscribe only to events in specific locations
|
|
50
|
+
const subscription = cache.subscribe(listener, {
|
|
51
|
+
locations: [{ lk: 'container-1' }]
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Subscribe with debouncing (useful for UI updates)
|
|
55
|
+
const subscription = cache.subscribe(listener, {
|
|
56
|
+
debounceMs: 100 // Debounce events by 100ms
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Event Types
|
|
61
|
+
|
|
62
|
+
### Item Events
|
|
63
|
+
|
|
64
|
+
These events are emitted when individual items are affected:
|
|
65
|
+
|
|
66
|
+
- **`item_created`** - Item was created via API and cached
|
|
67
|
+
- **`item_updated`** - Item was updated via API and cache updated
|
|
68
|
+
- **`item_removed`** - Item was removed via API and from cache
|
|
69
|
+
- **`item_retrieved`** - Item was retrieved from API and cached
|
|
70
|
+
- **`item_set`** - Item was set directly in cache (no API call)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
interface ItemEvent<V, S, L1, L2, L3, L4, L5> {
|
|
74
|
+
type: 'item_created' | 'item_updated' | 'item_removed' | 'item_retrieved' | 'item_set';
|
|
75
|
+
key: ComKey<S, L1, L2, L3, L4, L5> | PriKey<S>;
|
|
76
|
+
item: V | null; // null for removed items
|
|
77
|
+
previousItem?: V | null; // Previous state before change
|
|
78
|
+
affectedLocations?: LocKeyArray<L1, L2, L3, L4, L5> | [];
|
|
79
|
+
timestamp: number;
|
|
80
|
+
source: 'api' | 'cache' | 'operation';
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Query Events
|
|
85
|
+
|
|
86
|
+
These events are emitted when multiple items are queried:
|
|
87
|
+
|
|
88
|
+
- **`items_queried`** - Multiple items were queried and cached
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
interface QueryEvent<V, S, L1, L2, L3, L4, L5> {
|
|
92
|
+
type: 'items_queried';
|
|
93
|
+
query: ItemQuery;
|
|
94
|
+
locations: LocKeyArray<L1, L2, L3, L4, L5> | [];
|
|
95
|
+
items: V[];
|
|
96
|
+
affectedKeys: (ComKey<S, L1, L2, L3, L4, L5> | PriKey<S>)[];
|
|
97
|
+
timestamp: number;
|
|
98
|
+
source: 'api' | 'cache' | 'operation';
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Cache Management Events
|
|
103
|
+
|
|
104
|
+
These events are emitted for cache-wide operations:
|
|
105
|
+
|
|
106
|
+
- **`cache_cleared`** - Entire cache was cleared
|
|
107
|
+
- **`location_invalidated`** - Specific location(s) were invalidated
|
|
108
|
+
- **`query_invalidated`** - Cached query results were invalidated
|
|
109
|
+
|
|
110
|
+
## Advanced Usage
|
|
111
|
+
|
|
112
|
+
### React Integration Example
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { useEffect, useState } from 'react';
|
|
116
|
+
import { Cache, CacheSubscription } from '@fjell/cache';
|
|
117
|
+
|
|
118
|
+
function useItem<T>(cache: Cache<T, any>, key: any): T | null {
|
|
119
|
+
const [item, setItem] = useState<T | null>(() =>
|
|
120
|
+
cache.cacheMap.get(key)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const subscription = cache.subscribe((event) => {
|
|
125
|
+
if (event.type === 'item_changed' &&
|
|
126
|
+
JSON.stringify(event.key) === JSON.stringify(key)) {
|
|
127
|
+
setItem(event.item);
|
|
128
|
+
} else if (event.type === 'item_removed' &&
|
|
129
|
+
JSON.stringify(event.key) === JSON.stringify(key)) {
|
|
130
|
+
setItem(null);
|
|
131
|
+
}
|
|
132
|
+
}, {
|
|
133
|
+
keys: [key],
|
|
134
|
+
eventTypes: ['item_updated', 'item_removed', 'item_set']
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return () => subscription.unsubscribe();
|
|
138
|
+
}, [cache, key]);
|
|
139
|
+
|
|
140
|
+
return item;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Listening to Multiple Caches
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
class CacheEventHub {
|
|
148
|
+
private subscriptions: CacheSubscription[] = [];
|
|
149
|
+
|
|
150
|
+
subscribeTo<T>(cache: Cache<T, any>, listener: CacheEventListener<T, any>) {
|
|
151
|
+
const subscription = cache.subscribe(listener);
|
|
152
|
+
this.subscriptions.push(subscription);
|
|
153
|
+
return subscription;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
unsubscribeAll() {
|
|
157
|
+
this.subscriptions.forEach(sub => sub.unsubscribe());
|
|
158
|
+
this.subscriptions = [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const hub = new CacheEventHub();
|
|
163
|
+
|
|
164
|
+
// Subscribe to multiple caches
|
|
165
|
+
hub.subscribeTo(userCache, (event) => console.log('User event:', event));
|
|
166
|
+
hub.subscribeTo(orderCache, (event) => console.log('Order event:', event));
|
|
167
|
+
|
|
168
|
+
// Later: clean up all subscriptions
|
|
169
|
+
hub.unsubscribeAll();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Event Logging and Debugging
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Log all cache events for debugging
|
|
176
|
+
const debugSubscription = cache.subscribe((event) => {
|
|
177
|
+
console.log(`[${new Date(event.timestamp).toISOString()}] ${event.type}:`, {
|
|
178
|
+
source: event.source,
|
|
179
|
+
...('key' in event ? { key: event.key } : {}),
|
|
180
|
+
...('query' in event ? { query: event.query } : {}),
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Performance monitoring
|
|
185
|
+
let eventCounts = new Map<string, number>();
|
|
186
|
+
|
|
187
|
+
cache.subscribe((event) => {
|
|
188
|
+
const count = eventCounts.get(event.type) || 0;
|
|
189
|
+
eventCounts.set(event.type, count + 1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
setInterval(() => {
|
|
193
|
+
console.log('Event counts:', Object.fromEntries(eventCounts));
|
|
194
|
+
eventCounts.clear();
|
|
195
|
+
}, 10000);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Best Practices
|
|
199
|
+
|
|
200
|
+
### 1. Use Specific Event Filters
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// Good: Filter by specific event types and keys
|
|
204
|
+
cache.subscribe(listener, {
|
|
205
|
+
eventTypes: ['item_updated'],
|
|
206
|
+
keys: [userKey]
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Avoid: Subscribing to all events when you only need specific ones
|
|
210
|
+
cache.subscribe(listener); // Too broad
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 2. Debounce High-Frequency Updates
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Good: Debounce UI updates
|
|
217
|
+
cache.subscribe(updateUI, {
|
|
218
|
+
debounceMs: 100
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Good: Don't debounce critical business logic
|
|
222
|
+
cache.subscribe(saveToDisk, {
|
|
223
|
+
// No debouncing for critical operations
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 3. Clean Up Subscriptions
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const subscription = cache.subscribe(listener);
|
|
232
|
+
|
|
233
|
+
// Always clean up
|
|
234
|
+
return () => subscription.unsubscribe();
|
|
235
|
+
}, []);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 4. Handle Errors in Listeners
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
cache.subscribe((event) => {
|
|
242
|
+
try {
|
|
243
|
+
// Your event handling logic
|
|
244
|
+
handleEvent(event);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('Error handling cache event:', error);
|
|
247
|
+
// Don't let errors break other listeners
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Event Sources
|
|
253
|
+
|
|
254
|
+
Events include a `source` field indicating where they originated:
|
|
255
|
+
|
|
256
|
+
- **`api`** - Event from API operation (create, update, remove, etc.)
|
|
257
|
+
- **`cache`** - Event from direct cache operation (set, invalidate)
|
|
258
|
+
- **`operation`** - Event from internal cache operation
|
|
259
|
+
|
|
260
|
+
## Performance Considerations
|
|
261
|
+
|
|
262
|
+
- **Subscription count**: Each subscription has minimal overhead, but avoid creating unnecessary subscriptions
|
|
263
|
+
- **Event frequency**: Use debouncing for high-frequency UI updates
|
|
264
|
+
- **Memory leaks**: Always unsubscribe when components unmount
|
|
265
|
+
- **Event filtering**: Use specific filters to reduce unnecessary event processing
|
|
266
|
+
|
|
267
|
+
## Error Handling
|
|
268
|
+
|
|
269
|
+
The event system is designed to be resilient:
|
|
270
|
+
|
|
271
|
+
- Listener errors don't affect other listeners or cache operations
|
|
272
|
+
- Failed subscriptions are automatically cleaned up
|
|
273
|
+
- Event emission continues even if some listeners fail
|
|
274
|
+
|
|
275
|
+
## Migration from Manual State Management
|
|
276
|
+
|
|
277
|
+
If you're currently using manual state management patterns:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// Before: Manual state management
|
|
281
|
+
const [items, setItems] = useState([]);
|
|
282
|
+
|
|
283
|
+
const addItem = async (item) => {
|
|
284
|
+
const newItem = await cache.operations.create(item);
|
|
285
|
+
setItems(prev => [...prev, newItem]); // Manual update
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// After: Event-driven updates
|
|
289
|
+
const [items, setItems] = useState([]);
|
|
290
|
+
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
const subscription = cache.subscribe((event) => {
|
|
293
|
+
if (event.type === 'item_created') {
|
|
294
|
+
setItems(prev => [...prev, event.item]);
|
|
295
|
+
}
|
|
296
|
+
}, { eventTypes: ['item_created'] });
|
|
297
|
+
|
|
298
|
+
return () => subscription.unsubscribe();
|
|
299
|
+
}, []);
|
|
300
|
+
|
|
301
|
+
const addItem = async (item) => {
|
|
302
|
+
await cache.operations.create(item); // Event automatically updates state
|
|
303
|
+
};
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
This approach eliminates the need for manual state synchronization and ensures your UI always reflects the current cache state.
|
package/CACHE_IMPLEMENTATIONS.md
CHANGED
|
@@ -151,6 +151,123 @@ const cache = new MemoryCacheMap(keyTypeArray);
|
|
|
151
151
|
- **Small data, persistent**: `LocalStorageCacheMap`
|
|
152
152
|
- **Large data, complex queries**: `AsyncIndexDBCacheMap`
|
|
153
153
|
|
|
154
|
+
## Cache Information and Capabilities
|
|
155
|
+
|
|
156
|
+
All cache instances expose metadata about their configuration and capabilities through the `getCacheInfo()` method at the Cache level. This provides client applications with visibility into the cache's behavior and limitations.
|
|
157
|
+
|
|
158
|
+
### CacheInfo Interface
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface CacheInfo {
|
|
162
|
+
/** The implementation type in format "<category>/<implementation>" */
|
|
163
|
+
implementationType: string;
|
|
164
|
+
/** The eviction policy being used (if any) */
|
|
165
|
+
evictionPolicy?: string;
|
|
166
|
+
/** Default TTL in milliseconds (if configured) */
|
|
167
|
+
defaultTTL?: number;
|
|
168
|
+
/** Whether TTL is supported by this implementation */
|
|
169
|
+
supportsTTL: boolean;
|
|
170
|
+
/** Whether eviction is supported by this implementation */
|
|
171
|
+
supportsEviction: boolean;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Getting Cache Information
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { MemoryCacheMap, EnhancedMemoryCacheMap } from '@fjell/cache';
|
|
179
|
+
|
|
180
|
+
// Create cache instance
|
|
181
|
+
const cache = createCache(api, createCoordinate('item'), registry);
|
|
182
|
+
const cacheInfo = cache.getCacheInfo();
|
|
183
|
+
console.log(cacheInfo);
|
|
184
|
+
// Output: {
|
|
185
|
+
// implementationType: "memory/memory",
|
|
186
|
+
// supportsTTL: true,
|
|
187
|
+
// supportsEviction: false
|
|
188
|
+
// }
|
|
189
|
+
|
|
190
|
+
// Enhanced memory cache with eviction
|
|
191
|
+
const enhancedCache = createCache(api, createCoordinate('item'), registry, {
|
|
192
|
+
cacheType: 'memory',
|
|
193
|
+
memoryConfig: {
|
|
194
|
+
size: {
|
|
195
|
+
maxItems: 1000,
|
|
196
|
+
evictionPolicy: 'lru'
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const enhancedInfo = enhancedCache.getCacheInfo();
|
|
201
|
+
console.log(enhancedInfo);
|
|
202
|
+
// Output: {
|
|
203
|
+
// implementationType: "memory/enhanced",
|
|
204
|
+
// evictionPolicy: "lru",
|
|
205
|
+
// supportsTTL: true,
|
|
206
|
+
// supportsEviction: true
|
|
207
|
+
// }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Implementation Types
|
|
211
|
+
|
|
212
|
+
Each cache implementation has a unique type identifier:
|
|
213
|
+
|
|
214
|
+
- `memory/memory` - Basic MemoryCacheMap
|
|
215
|
+
- `memory/enhanced` - EnhancedMemoryCacheMap with eviction support
|
|
216
|
+
- `browser/localStorage` - LocalStorageCacheMap
|
|
217
|
+
- `browser/sessionStorage` - SessionStorageCacheMap
|
|
218
|
+
- `browser/indexedDB` - IndexDBCacheMap
|
|
219
|
+
|
|
220
|
+
### Capability Detection
|
|
221
|
+
|
|
222
|
+
Use the capability flags to adapt your application behavior:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
function configureCache<V extends Item<S>, S extends string>(
|
|
226
|
+
cache: Cache<V, S>
|
|
227
|
+
) {
|
|
228
|
+
const info = cache.getCacheInfo();
|
|
229
|
+
|
|
230
|
+
// Adjust TTL strategy based on support
|
|
231
|
+
if (info.supportsTTL) {
|
|
232
|
+
console.log(`TTL operations supported for ${info.implementationType}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Show eviction policy if supported
|
|
236
|
+
if (info.supportsEviction && info.evictionPolicy) {
|
|
237
|
+
console.log(`Using ${info.evictionPolicy} eviction policy`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Log implementation details
|
|
241
|
+
console.log(`Cache implementation: ${info.implementationType}`);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Display Information for Debugging
|
|
246
|
+
|
|
247
|
+
The cache info is particularly useful for debugging and monitoring:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
function debugCacheStatus<V extends Item<S>, S extends string>(
|
|
251
|
+
cacheName: string,
|
|
252
|
+
cache: Cache<V, S>
|
|
253
|
+
) {
|
|
254
|
+
const info = cache.getCacheInfo();
|
|
255
|
+
|
|
256
|
+
console.log(`[${cacheName}] Cache Configuration:`);
|
|
257
|
+
console.log(` Type: ${info.implementationType}`);
|
|
258
|
+
console.log(` TTL Support: ${info.supportsTTL ? 'Yes' : 'No'}`);
|
|
259
|
+
console.log(` Eviction Support: ${info.supportsEviction ? 'Yes' : 'No'}`);
|
|
260
|
+
|
|
261
|
+
if (info.evictionPolicy) {
|
|
262
|
+
console.log(` Eviction Policy: ${info.evictionPolicy}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (info.defaultTTL) {
|
|
266
|
+
console.log(` Default TTL: ${info.defaultTTL}ms`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
154
271
|
## Common Patterns
|
|
155
272
|
|
|
156
273
|
### Factory Pattern
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Memory Leak Prevention Fixes in @fjell/cache
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document outlines the comprehensive memory leak prevention features implemented in the fjell-cache event system. These fixes address critical memory management issues that could cause applications to accumulate memory over time without proper cleanup.
|
|
6
|
+
|
|
7
|
+
## Fixed Memory Leak Issues
|
|
8
|
+
|
|
9
|
+
### 1. Static Timestamp State in CacheEventFactory ✅
|
|
10
|
+
|
|
11
|
+
**Problem**: The `CacheEventFactory` class used a static `lastTimestamp` variable that accumulated without cleanup, potentially causing memory issues in long-running applications.
|
|
12
|
+
|
|
13
|
+
**Solution**:
|
|
14
|
+
- Added automatic cleanup mechanisms with instance counting
|
|
15
|
+
- Implemented periodic cleanup that resets stale timestamp state
|
|
16
|
+
- Added `destroyInstance()` method to properly manage static state lifecycle
|
|
17
|
+
- Cleanup timer runs every 60 seconds and doesn't keep the process alive
|
|
18
|
+
|
|
19
|
+
**Files Modified**:
|
|
20
|
+
- `src/events/CacheEventFactory.ts`
|
|
21
|
+
|
|
22
|
+
**Key Features**:
|
|
23
|
+
```typescript
|
|
24
|
+
// Automatic cleanup when instances are destroyed
|
|
25
|
+
CacheEventFactory.destroyInstance();
|
|
26
|
+
|
|
27
|
+
// Periodic cleanup of stale state
|
|
28
|
+
private static performCleanup(): void {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
if (now - this.lastTimestamp > this.MAX_TIMESTAMP_AGE_MS) {
|
|
31
|
+
this.lastTimestamp = 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Weak References for Event Handlers ✅
|
|
37
|
+
|
|
38
|
+
**Problem**: Event handlers held strong references to listener functions, preventing garbage collection even when the listeners were no longer needed.
|
|
39
|
+
|
|
40
|
+
**Solution**:
|
|
41
|
+
- Implemented optional weak references for event listeners
|
|
42
|
+
- Automatic detection and cleanup of garbage-collected listeners
|
|
43
|
+
- Backwards compatible with fallback to strong references when WeakRef is not available
|
|
44
|
+
|
|
45
|
+
**Files Modified**:
|
|
46
|
+
- `src/events/CacheEventEmitter.ts`
|
|
47
|
+
- `src/events/CacheEventTypes.ts`
|
|
48
|
+
|
|
49
|
+
**Key Features**:
|
|
50
|
+
```typescript
|
|
51
|
+
// Weak reference support
|
|
52
|
+
listenerRef?: WeakRef<CacheEventListener<V, S, L1, L2, L3, L4, L5>>;
|
|
53
|
+
|
|
54
|
+
// Automatic cleanup
|
|
55
|
+
if (this.WEAK_REF_ENABLED && subscription.listenerRef) {
|
|
56
|
+
const listener = subscription.listenerRef.deref();
|
|
57
|
+
if (!listener) {
|
|
58
|
+
subscription.isActive = false;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Configuration option
|
|
64
|
+
{ useWeakRef: true }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Automatic Subscription Cleanup with Timeouts ✅
|
|
68
|
+
|
|
69
|
+
**Problem**: Event subscriptions could remain active indefinitely without access, accumulating memory over time.
|
|
70
|
+
|
|
71
|
+
**Solution**:
|
|
72
|
+
- Added periodic cleanup of inactive subscriptions
|
|
73
|
+
- Track last access time for each subscription
|
|
74
|
+
- Automatic removal of subscriptions inactive for more than 5 minutes
|
|
75
|
+
- Cleanup runs every 30 seconds
|
|
76
|
+
|
|
77
|
+
**Files Modified**:
|
|
78
|
+
- `src/events/CacheEventEmitter.ts`
|
|
79
|
+
|
|
80
|
+
**Key Features**:
|
|
81
|
+
```typescript
|
|
82
|
+
// Track access times
|
|
83
|
+
createdAt: number;
|
|
84
|
+
lastAccessTime: number;
|
|
85
|
+
|
|
86
|
+
// Periodic cleanup
|
|
87
|
+
private performPeriodicCleanup(): void {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (now - subscription.lastAccessTime > this.MAX_INACTIVE_TIME_MS) {
|
|
90
|
+
toRemove.push(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. Cache Destruction Mechanisms ✅
|
|
96
|
+
|
|
97
|
+
**Problem**: Cache instances lacked proper destruction methods, making it difficult to clean up resources when caches were no longer needed.
|
|
98
|
+
|
|
99
|
+
**Solution**:
|
|
100
|
+
- Added `destroy()` method to Cache interface and implementation
|
|
101
|
+
- Comprehensive cleanup of all associated resources
|
|
102
|
+
- Proper timer cleanup to prevent resource leaks
|
|
103
|
+
- Integration with CacheEventFactory instance counting
|
|
104
|
+
|
|
105
|
+
**Files Modified**:
|
|
106
|
+
- `src/Cache.ts`
|
|
107
|
+
- `src/Aggregator.ts`
|
|
108
|
+
|
|
109
|
+
**Key Features**:
|
|
110
|
+
```typescript
|
|
111
|
+
destroy(): void {
|
|
112
|
+
// Clean up event emitter
|
|
113
|
+
eventEmitter.destroy();
|
|
114
|
+
|
|
115
|
+
// Clean up TTL manager
|
|
116
|
+
if (ttlManager && typeof ttlManager.destroy === 'function') {
|
|
117
|
+
ttlManager.destroy();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Clean up cache map
|
|
121
|
+
if (cacheMap && typeof (cacheMap as any).destroy === 'function') {
|
|
122
|
+
(cacheMap as any).destroy();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Notify CacheEventFactory
|
|
126
|
+
CacheEventFactory.destroyInstance();
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 5. Enhanced Timer Management ✅
|
|
131
|
+
|
|
132
|
+
**Problem**: Debounce timers and cleanup intervals could accumulate without proper cleanup.
|
|
133
|
+
|
|
134
|
+
**Solution**:
|
|
135
|
+
- Comprehensive timer cleanup in all destruction paths
|
|
136
|
+
- Use of `timer.unref()` to prevent keeping Node.js process alive
|
|
137
|
+
- Proper cleanup of debounce timers when subscriptions are removed
|
|
138
|
+
|
|
139
|
+
**Files Modified**:
|
|
140
|
+
- `src/events/CacheEventEmitter.ts`
|
|
141
|
+
- `src/events/CacheEventFactory.ts`
|
|
142
|
+
|
|
143
|
+
**Key Features**:
|
|
144
|
+
```typescript
|
|
145
|
+
// Timer cleanup
|
|
146
|
+
if (subscription.debounceTimer) {
|
|
147
|
+
clearTimeout(subscription.debounceTimer);
|
|
148
|
+
subscription.debounceTimer = null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Non-blocking timers
|
|
152
|
+
if (this.cleanupInterval.unref) {
|
|
153
|
+
this.cleanupInterval.unref();
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Configuration Options
|
|
158
|
+
|
|
159
|
+
### Event Subscription Options
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
interface CacheSubscriptionOptions {
|
|
163
|
+
/** Use weak references for the listener (default: true if WeakRef is available) */
|
|
164
|
+
useWeakRef?: boolean;
|
|
165
|
+
|
|
166
|
+
/** Debounce events by this many milliseconds */
|
|
167
|
+
debounceMs?: number;
|
|
168
|
+
|
|
169
|
+
/** Filter by event types */
|
|
170
|
+
eventTypes?: CacheEventType[];
|
|
171
|
+
|
|
172
|
+
/** Optional error handler for listener errors */
|
|
173
|
+
onError?: (error: Error, event: any) => void;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Usage Examples
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Create cache with automatic cleanup
|
|
181
|
+
const cache = createCache(api, coordinate, registry, {
|
|
182
|
+
cacheType: 'memory'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Subscribe with weak references (default)
|
|
186
|
+
const subscription = cache.subscribe(
|
|
187
|
+
(event) => console.log(event.type),
|
|
188
|
+
{
|
|
189
|
+
useWeakRef: true, // Optional: defaults to true
|
|
190
|
+
eventTypes: ['item_created', 'item_updated']
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Proper cleanup when done
|
|
195
|
+
cache.destroy();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Testing
|
|
199
|
+
|
|
200
|
+
Comprehensive integration tests have been added to verify the memory leak prevention features:
|
|
201
|
+
|
|
202
|
+
- `tests/examples/memory-leak-prevention.integration.test.ts`
|
|
203
|
+
- `examples/memory-leak-prevention-example.ts`
|
|
204
|
+
|
|
205
|
+
The tests verify:
|
|
206
|
+
- Static state cleanup in CacheEventFactory
|
|
207
|
+
- Weak reference functionality
|
|
208
|
+
- Subscription timeout cleanup
|
|
209
|
+
- Cache destruction mechanisms
|
|
210
|
+
- Timer cleanup
|
|
211
|
+
|
|
212
|
+
## Monitoring and Debugging
|
|
213
|
+
|
|
214
|
+
### Memory Usage Monitoring
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Check active subscriptions
|
|
218
|
+
console.log(`Active subscriptions: ${cache.eventEmitter.getSubscriptionCount()}`);
|
|
219
|
+
|
|
220
|
+
// Get subscription details
|
|
221
|
+
const subscriptions = cache.eventEmitter.getSubscriptions();
|
|
222
|
+
console.log('Subscription details:', subscriptions);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Performance Impact
|
|
226
|
+
|
|
227
|
+
- Cleanup operations run in the background and don't block application logic
|
|
228
|
+
- Timers are configured to not keep the Node.js process alive
|
|
229
|
+
- Weak references provide automatic memory management with zero performance overhead
|
|
230
|
+
- Periodic cleanup intervals are optimized for balance between memory usage and performance
|
|
231
|
+
|
|
232
|
+
## Migration Guide
|
|
233
|
+
|
|
234
|
+
### For Existing Applications
|
|
235
|
+
|
|
236
|
+
1. **No Breaking Changes**: All existing code will continue to work without modifications
|
|
237
|
+
2. **Opt-in Features**: Weak references and enhanced cleanup are enabled by default but can be disabled
|
|
238
|
+
3. **Recommended Actions**:
|
|
239
|
+
- Add `cache.destroy()` calls when caches are no longer needed
|
|
240
|
+
- Consider enabling weak references for better memory management
|
|
241
|
+
- Monitor subscription counts in long-running applications
|
|
242
|
+
|
|
243
|
+
### Best Practices
|
|
244
|
+
|
|
245
|
+
1. **Always destroy caches**: Call `cache.destroy()` when done with a cache instance
|
|
246
|
+
2. **Use weak references**: Enable weak references for better automatic cleanup
|
|
247
|
+
3. **Monitor subscriptions**: Regularly check subscription counts in production
|
|
248
|
+
4. **Handle errors**: Provide error handlers for event listeners
|
|
249
|
+
5. **Avoid long-lived subscriptions**: Clean up subscriptions that are no longer needed
|
|
250
|
+
|
|
251
|
+
## Future Considerations
|
|
252
|
+
|
|
253
|
+
- Memory usage metrics and monitoring hooks
|
|
254
|
+
- Configurable cleanup intervals
|
|
255
|
+
- Enhanced debugging tools for memory leak detection
|
|
256
|
+
- Integration with application performance monitoring (APM) tools
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Summary
|
|
261
|
+
|
|
262
|
+
The memory leak prevention features provide comprehensive protection against common memory issues in event-driven cache systems:
|
|
263
|
+
|
|
264
|
+
- ✅ **Static state cleanup** prevents accumulation of factory state
|
|
265
|
+
- ✅ **Weak references** enable automatic garbage collection of listeners
|
|
266
|
+
- ✅ **Periodic cleanup** removes inactive subscriptions automatically
|
|
267
|
+
- ✅ **Resource destruction** provides comprehensive cleanup mechanisms
|
|
268
|
+
- ✅ **Timer management** prevents resource leaks from background timers
|
|
269
|
+
|
|
270
|
+
These features ensure that fjell-cache applications can run for extended periods without memory degradation, making the library suitable for production environments with high availability requirements.
|