@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.
- package/core/calendar/Calendar.js +715 -0
- package/core/calendar/DateUtils.js +553 -0
- package/core/conflicts/ConflictDetector.js +517 -0
- package/core/events/Event.js +914 -0
- package/core/events/EventStore.js +1198 -0
- package/core/events/RRuleParser.js +420 -0
- package/core/events/RecurrenceEngine.js +382 -0
- package/core/ics/ICSHandler.js +389 -0
- package/core/ics/ICSParser.js +475 -0
- package/core/performance/AdaptiveMemoryManager.js +333 -0
- package/core/performance/LRUCache.js +118 -0
- package/core/performance/PerformanceOptimizer.js +523 -0
- package/core/search/EventSearch.js +476 -0
- package/core/state/StateManager.js +546 -0
- package/core/timezone/TimezoneDatabase.js +294 -0
- package/core/timezone/TimezoneManager.js +419 -0
- package/core/types.js +366 -0
- package/package.json +11 -9
|
@@ -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
|
+
}
|