@forcecalendar/core 0.2.0 → 0.2.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.
@@ -0,0 +1,333 @@
1
+ /**
2
+ * AdaptiveMemoryManager - Dynamically manages cache sizes based on memory pressure
3
+ * Monitors memory usage and adjusts cache capacity to prevent memory issues
4
+ */
5
+
6
+ export class AdaptiveMemoryManager {
7
+ constructor(config = {}) {
8
+ this.config = {
9
+ checkInterval: 30000, // Check memory every 30 seconds
10
+ memoryThreshold: 0.8, // Start reducing cache at 80% memory usage
11
+ criticalThreshold: 0.95, // Emergency clear at 95% memory usage
12
+ minCacheSize: 10, // Minimum cache size to maintain
13
+ maxCacheSize: 10000, // Maximum cache size allowed
14
+ adaptiveScaling: true, // Enable/disable adaptive scaling
15
+ ...config
16
+ };
17
+
18
+ // Cache references
19
+ this.caches = new Map();
20
+
21
+ // Memory statistics
22
+ this.stats = {
23
+ adjustments: 0,
24
+ emergencyClears: 0,
25
+ lastMemoryUsage: 0,
26
+ lastCheckTime: null,
27
+ cacheResizes: []
28
+ };
29
+
30
+ // Start monitoring if enabled
31
+ this.monitoringInterval = null;
32
+ if (this.config.adaptiveScaling) {
33
+ this.startMonitoring();
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Register a cache for management
39
+ * @param {string} name - Cache identifier
40
+ * @param {Object} cache - Cache instance with size/clear methods
41
+ * @param {Object} [options] - Cache-specific options
42
+ */
43
+ registerCache(name, cache, options = {}) {
44
+ this.caches.set(name, {
45
+ cache,
46
+ priority: options.priority || 1, // Higher priority = less likely to be reduced
47
+ currentCapacity: options.initialCapacity || 100,
48
+ minCapacity: options.minCapacity || this.config.minCacheSize,
49
+ maxCapacity: options.maxCapacity || this.config.maxCacheSize,
50
+ scaleFactor: options.scaleFactor || 0.5, // How much to reduce on pressure
51
+ lastAccess: Date.now()
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Unregister a cache
57
+ * @param {string} name - Cache identifier
58
+ */
59
+ unregisterCache(name) {
60
+ this.caches.delete(name);
61
+ }
62
+
63
+ /**
64
+ * Start memory monitoring
65
+ */
66
+ startMonitoring() {
67
+ if (this.monitoringInterval) {
68
+ return;
69
+ }
70
+
71
+ this.monitoringInterval = setInterval(() => {
72
+ this.checkMemoryPressure();
73
+ }, this.config.checkInterval);
74
+
75
+ // Initial check
76
+ this.checkMemoryPressure();
77
+ }
78
+
79
+ /**
80
+ * Stop memory monitoring
81
+ */
82
+ stopMonitoring() {
83
+ if (this.monitoringInterval) {
84
+ clearInterval(this.monitoringInterval);
85
+ this.monitoringInterval = null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Check memory pressure and adjust caches
91
+ */
92
+ async checkMemoryPressure() {
93
+ const memoryUsage = await this.getMemoryUsage();
94
+ this.stats.lastMemoryUsage = memoryUsage;
95
+ this.stats.lastCheckTime = new Date();
96
+
97
+ if (memoryUsage > this.config.criticalThreshold) {
98
+ // Emergency clear - clear all caches
99
+ this.emergencyClear();
100
+ } else if (memoryUsage > this.config.memoryThreshold) {
101
+ // Memory pressure - reduce cache sizes
102
+ this.reduceCacheSizes(memoryUsage);
103
+ } else if (memoryUsage < this.config.memoryThreshold - 0.2) {
104
+ // Memory available - can increase cache sizes
105
+ this.increaseCacheSizes();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get current memory usage percentage
111
+ * @returns {Promise<number>} Memory usage as percentage (0-1)
112
+ */
113
+ async getMemoryUsage() {
114
+ // Browser environment
115
+ if (typeof performance !== 'undefined' && performance.memory) {
116
+ const memInfo = performance.memory;
117
+ if (memInfo.jsHeapSizeLimit && memInfo.usedJSHeapSize) {
118
+ return memInfo.usedJSHeapSize / memInfo.jsHeapSizeLimit;
119
+ }
120
+ }
121
+
122
+ // Node.js environment
123
+ if (typeof process !== 'undefined' && process.memoryUsage) {
124
+ const usage = process.memoryUsage();
125
+ // Use heap total as the limit in Node.js
126
+ return usage.heapUsed / usage.heapTotal;
127
+ }
128
+
129
+ // Fallback - estimate based on cache sizes
130
+ return this.estimateMemoryUsage();
131
+ }
132
+
133
+ /**
134
+ * Estimate memory usage based on cache sizes
135
+ * @private
136
+ */
137
+ estimateMemoryUsage() {
138
+ let totalItems = 0;
139
+ let maxItems = 0;
140
+
141
+ for (const [_, cacheInfo] of this.caches) {
142
+ if (cacheInfo.cache.size !== undefined) {
143
+ totalItems += cacheInfo.cache.size;
144
+ maxItems += cacheInfo.maxCapacity;
145
+ }
146
+ }
147
+
148
+ return maxItems > 0 ? totalItems / maxItems : 0.5;
149
+ }
150
+
151
+ /**
152
+ * Reduce cache sizes based on memory pressure
153
+ * @param {number} memoryUsage - Current memory usage percentage
154
+ */
155
+ reduceCacheSizes(memoryUsage) {
156
+ const pressureLevel = (memoryUsage - this.config.memoryThreshold) /
157
+ (this.config.criticalThreshold - this.config.memoryThreshold);
158
+
159
+ // Sort caches by priority (lower priority first)
160
+ const sortedCaches = Array.from(this.caches.entries())
161
+ .sort((a, b) => a[1].priority - b[1].priority);
162
+
163
+ for (const [name, cacheInfo] of sortedCaches) {
164
+ const reduction = Math.floor(cacheInfo.currentCapacity * cacheInfo.scaleFactor * pressureLevel);
165
+ const newCapacity = Math.max(
166
+ cacheInfo.minCapacity,
167
+ cacheInfo.currentCapacity - reduction
168
+ );
169
+
170
+ if (newCapacity < cacheInfo.currentCapacity) {
171
+ this.resizeCache(name, cacheInfo, newCapacity);
172
+ }
173
+ }
174
+
175
+ this.stats.adjustments++;
176
+ }
177
+
178
+ /**
179
+ * Increase cache sizes when memory is available
180
+ */
181
+ increaseCacheSizes() {
182
+ for (const [name, cacheInfo] of this.caches) {
183
+ // Only increase if cache is being actively used
184
+ const timeSinceAccess = Date.now() - cacheInfo.lastAccess;
185
+ if (timeSinceAccess < 60000) { // Used in last minute
186
+ const increase = Math.floor(cacheInfo.currentCapacity * 0.2);
187
+ const newCapacity = Math.min(
188
+ cacheInfo.maxCapacity,
189
+ cacheInfo.currentCapacity + increase
190
+ );
191
+
192
+ if (newCapacity > cacheInfo.currentCapacity) {
193
+ this.resizeCache(name, cacheInfo, newCapacity);
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Resize a cache
201
+ * @private
202
+ */
203
+ resizeCache(name, cacheInfo, newCapacity) {
204
+ const oldCapacity = cacheInfo.currentCapacity;
205
+ cacheInfo.currentCapacity = newCapacity;
206
+
207
+ // If cache has a capacity property, update it
208
+ if (cacheInfo.cache.capacity !== undefined) {
209
+ cacheInfo.cache.capacity = newCapacity;
210
+ }
211
+
212
+ // If cache is now over capacity, evict excess items
213
+ if (cacheInfo.cache.size > newCapacity) {
214
+ this.evictExcessItems(cacheInfo.cache, newCapacity);
215
+ }
216
+
217
+ // Record resize event
218
+ this.stats.cacheResizes.push({
219
+ cache: name,
220
+ timestamp: new Date(),
221
+ oldCapacity,
222
+ newCapacity,
223
+ reason: newCapacity < oldCapacity ? 'pressure' : 'available'
224
+ });
225
+
226
+ // Keep only last 100 resize events
227
+ if (this.stats.cacheResizes.length > 100) {
228
+ this.stats.cacheResizes.shift();
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Evict excess items from cache
234
+ * @private
235
+ */
236
+ evictExcessItems(cache, targetSize) {
237
+ if (cache.size <= targetSize) {
238
+ return;
239
+ }
240
+
241
+ const itemsToRemove = cache.size - targetSize;
242
+
243
+ // If cache is a Map or has keys method
244
+ if (cache.keys) {
245
+ const keys = Array.from(cache.keys());
246
+ for (let i = 0; i < itemsToRemove; i++) {
247
+ cache.delete(keys[i]);
248
+ }
249
+ } else if (cache.clear) {
250
+ // Last resort - clear the cache
251
+ cache.clear();
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Emergency clear all caches
257
+ */
258
+ emergencyClear() {
259
+ for (const [name, cacheInfo] of this.caches) {
260
+ if (cacheInfo.cache.clear) {
261
+ cacheInfo.cache.clear();
262
+ }
263
+ // Reset to minimum capacity
264
+ cacheInfo.currentCapacity = cacheInfo.minCapacity;
265
+ }
266
+
267
+ this.stats.emergencyClears++;
268
+ console.warn('AdaptiveMemoryManager: Emergency cache clear triggered');
269
+ }
270
+
271
+ /**
272
+ * Update cache access time
273
+ * @param {string} name - Cache name
274
+ */
275
+ touchCache(name) {
276
+ const cacheInfo = this.caches.get(name);
277
+ if (cacheInfo) {
278
+ cacheInfo.lastAccess = Date.now();
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Get memory management statistics
284
+ * @returns {Object} Statistics object
285
+ */
286
+ getStats() {
287
+ const cacheStats = {};
288
+ for (const [name, cacheInfo] of this.caches) {
289
+ cacheStats[name] = {
290
+ size: cacheInfo.cache.size || 0,
291
+ capacity: cacheInfo.currentCapacity,
292
+ priority: cacheInfo.priority,
293
+ lastAccess: new Date(cacheInfo.lastAccess)
294
+ };
295
+ }
296
+
297
+ return {
298
+ ...this.stats,
299
+ memoryUsagePercent: `${(this.stats.lastMemoryUsage * 100).toFixed(2)}%`,
300
+ totalCaches: this.caches.size,
301
+ cacheStats,
302
+ monitoring: this.monitoringInterval !== null
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Manual trigger for memory pressure check
308
+ */
309
+ async checkNow() {
310
+ await this.checkMemoryPressure();
311
+ }
312
+
313
+ /**
314
+ * Set memory thresholds
315
+ * @param {Object} thresholds - New threshold values
316
+ */
317
+ setThresholds(thresholds) {
318
+ if (thresholds.memoryThreshold !== undefined) {
319
+ this.config.memoryThreshold = Math.max(0.5, Math.min(0.95, thresholds.memoryThreshold));
320
+ }
321
+ if (thresholds.criticalThreshold !== undefined) {
322
+ this.config.criticalThreshold = Math.max(this.config.memoryThreshold + 0.05, Math.min(1.0, thresholds.criticalThreshold));
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Destroy manager and clean up
328
+ */
329
+ destroy() {
330
+ this.stopMonitoring();
331
+ this.caches.clear();
332
+ }
333
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * LRU (Least Recently Used) Cache implementation
3
+ * Provides O(1) get and put operations
4
+ */
5
+ export class LRUCache {
6
+ /**
7
+ * Create a new LRU Cache
8
+ * @param {number} capacity - Maximum number of items in cache
9
+ */
10
+ constructor(capacity = 100) {
11
+ this.capacity = capacity;
12
+ this.cache = new Map();
13
+ this.hits = 0;
14
+ this.misses = 0;
15
+ this.evictions = 0;
16
+ }
17
+
18
+ /**
19
+ * Get a value from the cache
20
+ * @param {string} key - Cache key
21
+ * @returns {*} Cached value or undefined
22
+ */
23
+ get(key) {
24
+ if (!this.cache.has(key)) {
25
+ this.misses++;
26
+ return undefined;
27
+ }
28
+
29
+ // Move to end (most recently used)
30
+ const value = this.cache.get(key);
31
+ this.cache.delete(key);
32
+ this.cache.set(key, value);
33
+ this.hits++;
34
+ return value;
35
+ }
36
+
37
+ /**
38
+ * Put a value in the cache
39
+ * @param {string} key - Cache key
40
+ * @param {*} value - Value to cache
41
+ */
42
+ put(key, value) {
43
+ // Remove if exists to update position
44
+ if (this.cache.has(key)) {
45
+ this.cache.delete(key);
46
+ } else if (this.cache.size >= this.capacity) {
47
+ // Remove least recently used (first item)
48
+ const firstKey = this.cache.keys().next().value;
49
+ this.cache.delete(firstKey);
50
+ this.evictions++;
51
+ }
52
+
53
+ this.cache.set(key, value);
54
+ }
55
+
56
+ /**
57
+ * Check if key exists in cache
58
+ * @param {string} key - Cache key
59
+ * @returns {boolean} True if key exists
60
+ */
61
+ has(key) {
62
+ return this.cache.has(key);
63
+ }
64
+
65
+ /**
66
+ * Remove a key from the cache
67
+ * @param {string} key - Cache key
68
+ * @returns {boolean} True if key was removed
69
+ */
70
+ delete(key) {
71
+ return this.cache.delete(key);
72
+ }
73
+
74
+ /**
75
+ * Clear all cached items
76
+ */
77
+ clear() {
78
+ this.cache.clear();
79
+ this.hits = 0;
80
+ this.misses = 0;
81
+ this.evictions = 0;
82
+ }
83
+
84
+ /**
85
+ * Get cache statistics
86
+ * @returns {Object} Cache stats
87
+ */
88
+ getStats() {
89
+ const hitRate = this.hits + this.misses > 0
90
+ ? (this.hits / (this.hits + this.misses) * 100).toFixed(2)
91
+ : 0;
92
+
93
+ return {
94
+ size: this.cache.size,
95
+ capacity: this.capacity,
96
+ hits: this.hits,
97
+ misses: this.misses,
98
+ evictions: this.evictions,
99
+ hitRate: `${hitRate}%`
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Get all keys in order (least to most recently used)
105
+ * @returns {string[]} Array of keys
106
+ */
107
+ keys() {
108
+ return Array.from(this.cache.keys());
109
+ }
110
+
111
+ /**
112
+ * Get cache size
113
+ * @returns {number} Number of items in cache
114
+ */
115
+ get size() {
116
+ return this.cache.size;
117
+ }
118
+ }