@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,523 @@
1
+ /**
2
+ * PerformanceOptimizer - Optimizes calendar operations for large datasets
3
+ * Includes caching, lazy loading, and batch processing with adaptive memory management
4
+ */
5
+
6
+ import { LRUCache } from './LRUCache.js';
7
+ import { AdaptiveMemoryManager } from './AdaptiveMemoryManager.js';
8
+
9
+ export class PerformanceOptimizer {
10
+ constructor(config = {}) {
11
+ // Configuration
12
+ this.config = {
13
+ enableCache: true,
14
+ cacheCapacity: 500,
15
+ maxIndexDays: 365,
16
+ batchSize: 100,
17
+ enableMetrics: true,
18
+ cleanupInterval: 3600000, // 1 hour in ms
19
+ maxIndexAge: 30 * 24 * 60 * 60 * 1000, // 30 days in ms
20
+ enableAdaptiveMemory: true, // Enable adaptive memory management
21
+ ...config
22
+ };
23
+
24
+ // Caches with initial capacities
25
+ this.eventCache = new LRUCache(this.config.cacheCapacity);
26
+ this.queryCache = new LRUCache(Math.floor(this.config.cacheCapacity / 2));
27
+ this.dateRangeCache = new LRUCache(Math.floor(this.config.cacheCapacity / 4));
28
+
29
+ // Adaptive memory manager
30
+ if (this.config.enableAdaptiveMemory) {
31
+ this.memoryManager = new AdaptiveMemoryManager({
32
+ checkInterval: 30000,
33
+ memoryThreshold: 0.75,
34
+ criticalThreshold: 0.90
35
+ });
36
+
37
+ // Register caches with memory manager
38
+ this.memoryManager.registerCache('events', this.eventCache, {
39
+ priority: 3, // Highest priority
40
+ initialCapacity: this.config.cacheCapacity,
41
+ minCapacity: 50,
42
+ maxCapacity: 2000
43
+ });
44
+
45
+ this.memoryManager.registerCache('queries', this.queryCache, {
46
+ priority: 2,
47
+ initialCapacity: Math.floor(this.config.cacheCapacity / 2),
48
+ minCapacity: 25,
49
+ maxCapacity: 1000
50
+ });
51
+
52
+ this.memoryManager.registerCache('dateRanges', this.dateRangeCache, {
53
+ priority: 1,
54
+ initialCapacity: Math.floor(this.config.cacheCapacity / 4),
55
+ minCapacity: 10,
56
+ maxCapacity: 500
57
+ });
58
+ }
59
+
60
+ // Lazy loading tracking
61
+ this.lazyIndexes = new Map(); // eventId -> Set of date strings
62
+ this.pendingIndexes = new Map(); // eventId -> Promise
63
+
64
+ // Batch processing
65
+ this.batchQueue = [];
66
+ this.batchTimer = null;
67
+ this.batchCallbacks = [];
68
+
69
+ // Performance metrics
70
+ this.metrics = {
71
+ operations: {},
72
+ averageTimes: {},
73
+ slowQueries: []
74
+ };
75
+
76
+ // Cleanup timer
77
+ this.cleanupTimer = null;
78
+ if (this.config.cleanupInterval > 0) {
79
+ this.startCleanupTimer();
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Measure operation performance
85
+ * @param {string} operation - Operation name
86
+ * @param {Function} fn - Function to measure
87
+ * @returns {*} Function result
88
+ */
89
+ measure(operation, fn) {
90
+ if (!this.config.enableMetrics) {
91
+ return fn();
92
+ }
93
+
94
+ const start = performance.now();
95
+ try {
96
+ const result = fn();
97
+ const duration = performance.now() - start;
98
+ this.recordMetric(operation, duration);
99
+ return result;
100
+ } catch (error) {
101
+ const duration = performance.now() - start;
102
+ this.recordMetric(operation, duration, true);
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Measure async operation performance
109
+ * @param {string} operation - Operation name
110
+ * @param {Function} fn - Async function to measure
111
+ * @returns {Promise<*>} Function result
112
+ */
113
+ async measureAsync(operation, fn) {
114
+ if (!this.config.enableMetrics) {
115
+ return await fn();
116
+ }
117
+
118
+ const start = performance.now();
119
+ try {
120
+ const result = await fn();
121
+ const duration = performance.now() - start;
122
+ this.recordMetric(operation, duration);
123
+ return result;
124
+ } catch (error) {
125
+ const duration = performance.now() - start;
126
+ this.recordMetric(operation, duration, true);
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Record performance metric
133
+ * @private
134
+ */
135
+ recordMetric(operation, duration, isError = false) {
136
+ if (!this.metrics.operations[operation]) {
137
+ this.metrics.operations[operation] = {
138
+ count: 0,
139
+ totalTime: 0,
140
+ errors: 0,
141
+ min: Infinity,
142
+ max: 0
143
+ };
144
+ }
145
+
146
+ const metric = this.metrics.operations[operation];
147
+ metric.count++;
148
+ metric.totalTime += duration;
149
+ metric.min = Math.min(metric.min, duration);
150
+ metric.max = Math.max(metric.max, duration);
151
+
152
+ if (isError) {
153
+ metric.errors++;
154
+ }
155
+
156
+ // Update average
157
+ this.metrics.averageTimes[operation] = metric.totalTime / metric.count;
158
+
159
+ // Track slow queries
160
+ if (duration > 100) {
161
+ this.metrics.slowQueries.push({
162
+ operation,
163
+ duration,
164
+ timestamp: new Date(),
165
+ isError
166
+ });
167
+
168
+ // Keep only last 100 slow queries
169
+ if (this.metrics.slowQueries.length > 100) {
170
+ this.metrics.slowQueries.shift();
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Get performance metrics
177
+ * @returns {Object} Performance metrics
178
+ */
179
+ getMetrics() {
180
+ const summary = {
181
+ cacheStats: {
182
+ event: this.eventCache.getStats(),
183
+ query: this.queryCache.getStats(),
184
+ dateRange: this.dateRangeCache.getStats()
185
+ },
186
+ operations: {},
187
+ slowestOperations: [],
188
+ recentSlowQueries: this.metrics.slowQueries.slice(-10),
189
+ memoryManagement: this.memoryManager ? this.memoryManager.getStats() : null
190
+ };
191
+
192
+ // Process operations
193
+ for (const [op, data] of Object.entries(this.metrics.operations)) {
194
+ summary.operations[op] = {
195
+ count: data.count,
196
+ avgTime: `${(data.totalTime / data.count).toFixed(2)}ms`,
197
+ minTime: `${data.min.toFixed(2)}ms`,
198
+ maxTime: `${data.max.toFixed(2)}ms`,
199
+ totalTime: `${data.totalTime.toFixed(2)}ms`,
200
+ errors: data.errors,
201
+ errorRate: `${((data.errors / data.count) * 100).toFixed(2)}%`
202
+ };
203
+ }
204
+
205
+ // Find slowest operations
206
+ summary.slowestOperations = Object.entries(this.metrics.averageTimes)
207
+ .sort((a, b) => b[1] - a[1])
208
+ .slice(0, 5)
209
+ .map(([op, time]) => ({
210
+ operation: op,
211
+ avgTime: `${time.toFixed(2)}ms`
212
+ }));
213
+
214
+ return summary;
215
+ }
216
+
217
+ /**
218
+ * Check if event should use lazy indexing
219
+ * @param {import('../events/Event.js').Event} event - Event to check
220
+ * @returns {boolean} True if should use lazy indexing
221
+ */
222
+ shouldUseLazyIndexing(event) {
223
+ const daySpan = Math.ceil(
224
+ (event.end - event.start) / (24 * 60 * 60 * 1000)
225
+ );
226
+ return daySpan > this.config.maxIndexDays;
227
+ }
228
+
229
+ /**
230
+ * Create lazy index markers for large events
231
+ * @param {import('../events/Event.js').Event} event - Event to index
232
+ * @returns {Object} Index boundaries
233
+ */
234
+ createLazyIndexMarkers(event) {
235
+ const markers = {
236
+ eventId: event.id,
237
+ start: event.start,
238
+ end: event.end,
239
+ indexed: new Set(),
240
+ pending: false
241
+ };
242
+
243
+ // Index first and last month only initially
244
+ const startMonth = new Date(event.start.getFullYear(), event.start.getMonth(), 1);
245
+ const endMonth = new Date(event.end.getFullYear(), event.end.getMonth(), 1);
246
+
247
+ markers.indexed.add(this.getMonthKey(startMonth));
248
+ if (this.getMonthKey(startMonth) !== this.getMonthKey(endMonth)) {
249
+ markers.indexed.add(this.getMonthKey(endMonth));
250
+ }
251
+
252
+ this.lazyIndexes.set(event.id, markers);
253
+ return markers;
254
+ }
255
+
256
+ /**
257
+ * Expand lazy index for a specific date range
258
+ * @param {string} eventId - Event ID
259
+ * @param {Date} rangeStart - Start of range to index
260
+ * @param {Date} rangeEnd - End of range to index
261
+ * @returns {Promise<Set<string>>} Indexed date strings
262
+ */
263
+ async expandLazyIndex(eventId, rangeStart, rangeEnd) {
264
+ const markers = this.lazyIndexes.get(eventId);
265
+ if (!markers) {
266
+ return new Set();
267
+ }
268
+
269
+ // Check if already pending
270
+ if (markers.pending) {
271
+ return this.pendingIndexes.get(eventId);
272
+ }
273
+
274
+ markers.pending = true;
275
+
276
+ const promise = new Promise((resolve) => {
277
+ // Simulate async indexing (in real app, could be in worker)
278
+ setTimeout(() => {
279
+ const indexed = new Set();
280
+ const current = new Date(rangeStart);
281
+
282
+ while (current <= rangeEnd) {
283
+ const dateStr = current.toDateString();
284
+ if (!markers.indexed.has(dateStr)) {
285
+ indexed.add(dateStr);
286
+ markers.indexed.add(dateStr);
287
+ }
288
+ current.setDate(current.getDate() + 1);
289
+ }
290
+
291
+ markers.pending = false;
292
+ this.pendingIndexes.delete(eventId);
293
+ resolve(indexed);
294
+ }, 0);
295
+ });
296
+
297
+ this.pendingIndexes.set(eventId, promise);
298
+ return promise;
299
+ }
300
+
301
+ /**
302
+ * Get month key for date
303
+ * @private
304
+ */
305
+ getMonthKey(date) {
306
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
307
+ }
308
+
309
+ /**
310
+ * Cache event with TTL
311
+ * @param {string} key - Cache key
312
+ * @param {*} value - Value to cache
313
+ * @param {string} cacheType - Type of cache to use
314
+ */
315
+ cache(key, value, cacheType = 'event') {
316
+ if (!this.config.enableCache) return;
317
+
318
+ let cache;
319
+ let cacheManagerName;
320
+
321
+ switch (cacheType) {
322
+ case 'event':
323
+ cache = this.eventCache;
324
+ cacheManagerName = 'events';
325
+ break;
326
+ case 'query':
327
+ cache = this.queryCache;
328
+ cacheManagerName = 'queries';
329
+ break;
330
+ case 'dateRange':
331
+ cache = this.dateRangeCache;
332
+ cacheManagerName = 'dateRanges';
333
+ break;
334
+ default:
335
+ return;
336
+ }
337
+
338
+ cache.put(key, value);
339
+
340
+ // Update access time in memory manager
341
+ if (this.memoryManager) {
342
+ this.memoryManager.touchCache(cacheManagerName);
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Get from cache
348
+ * @param {string} key - Cache key
349
+ * @param {string} cacheType - Type of cache
350
+ * @returns {*} Cached value or undefined
351
+ */
352
+ getFromCache(key, cacheType = 'event') {
353
+ if (!this.config.enableCache) return undefined;
354
+
355
+ let result;
356
+ let cacheManagerName;
357
+
358
+ switch (cacheType) {
359
+ case 'event':
360
+ result = this.eventCache.get(key);
361
+ cacheManagerName = 'events';
362
+ break;
363
+ case 'query':
364
+ result = this.queryCache.get(key);
365
+ cacheManagerName = 'queries';
366
+ break;
367
+ case 'dateRange':
368
+ result = this.dateRangeCache.get(key);
369
+ cacheManagerName = 'dateRanges';
370
+ break;
371
+ default:
372
+ return undefined;
373
+ }
374
+
375
+ // Update access time on cache hit
376
+ if (result !== undefined && this.memoryManager) {
377
+ this.memoryManager.touchCache(cacheManagerName);
378
+ }
379
+
380
+ return result;
381
+ }
382
+
383
+ /**
384
+ * Invalidate caches for an event
385
+ * @param {string} eventId - Event ID
386
+ */
387
+ invalidateEventCaches(eventId) {
388
+ // Remove from event cache
389
+ this.eventCache.delete(eventId);
390
+
391
+ // Clear query cache (conservative approach)
392
+ // In production, track which queries include this event
393
+ this.queryCache.clear();
394
+ this.dateRangeCache.clear();
395
+ }
396
+
397
+ /**
398
+ * Batch operation for efficiency
399
+ * @param {Function} operation - Operation to batch
400
+ * @returns {Promise} Batch result
401
+ */
402
+ batch(operation) {
403
+ return new Promise((resolve, reject) => {
404
+ this.batchQueue.push(operation);
405
+ this.batchCallbacks.push({ resolve, reject });
406
+
407
+ if (this.batchQueue.length >= this.config.batchSize) {
408
+ this.processBatch();
409
+ } else if (!this.batchTimer) {
410
+ // Process batch after 10ms if not full
411
+ this.batchTimer = setTimeout(() => this.processBatch(), 10);
412
+ }
413
+ });
414
+ }
415
+
416
+ /**
417
+ * Process batched operations
418
+ * @private
419
+ */
420
+ processBatch() {
421
+ if (this.batchTimer) {
422
+ clearTimeout(this.batchTimer);
423
+ this.batchTimer = null;
424
+ }
425
+
426
+ if (this.batchQueue.length === 0) return;
427
+
428
+ const operations = this.batchQueue.splice(0);
429
+ const callbacks = this.batchCallbacks.splice(0);
430
+
431
+ // Process all operations
432
+ const results = [];
433
+ const errors = [];
434
+
435
+ operations.forEach((op, index) => {
436
+ try {
437
+ results[index] = op();
438
+ } catch (error) {
439
+ errors[index] = error;
440
+ }
441
+ });
442
+
443
+ // Resolve callbacks
444
+ callbacks.forEach((callback, index) => {
445
+ if (errors[index]) {
446
+ callback.reject(errors[index]);
447
+ } else {
448
+ callback.resolve(results[index]);
449
+ }
450
+ });
451
+ }
452
+
453
+ /**
454
+ * Start cleanup timer for old indexes
455
+ * @private
456
+ */
457
+ startCleanupTimer() {
458
+ this.cleanupTimer = setInterval(() => {
459
+ this.cleanupOldIndexes();
460
+ }, this.config.cleanupInterval);
461
+ }
462
+
463
+ /**
464
+ * Clean up old indexes
465
+ * @private
466
+ */
467
+ cleanupOldIndexes() {
468
+ const now = Date.now();
469
+ const maxAge = this.config.maxIndexAge;
470
+
471
+ // Clean up lazy indexes for events that are too old
472
+ for (const [eventId, markers] of this.lazyIndexes) {
473
+ if (markers.end.getTime() < now - maxAge) {
474
+ this.lazyIndexes.delete(eventId);
475
+ }
476
+ }
477
+
478
+ // Clean up slow query log
479
+ if (this.metrics.slowQueries.length > 100) {
480
+ this.metrics.slowQueries = this.metrics.slowQueries.slice(-100);
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Optimize query by checking cache first
486
+ * @param {string} queryKey - Unique query identifier
487
+ * @param {Function} queryFn - Function to execute if not cached
488
+ * @returns {*} Query result
489
+ */
490
+ optimizeQuery(queryKey, queryFn) {
491
+ // Check cache first
492
+ const cached = this.getFromCache(queryKey, 'query');
493
+ if (cached !== undefined) {
494
+ return cached;
495
+ }
496
+
497
+ // Execute query and cache result
498
+ const result = this.measure(`query:${queryKey}`, queryFn);
499
+ this.cache(queryKey, result, 'query');
500
+ return result;
501
+ }
502
+
503
+ /**
504
+ * Destroy optimizer and clean up resources
505
+ */
506
+ destroy() {
507
+ if (this.cleanupTimer) {
508
+ clearInterval(this.cleanupTimer);
509
+ this.cleanupTimer = null;
510
+ }
511
+
512
+ if (this.batchTimer) {
513
+ clearTimeout(this.batchTimer);
514
+ this.batchTimer = null;
515
+ }
516
+
517
+ this.eventCache.clear();
518
+ this.queryCache.clear();
519
+ this.dateRangeCache.clear();
520
+ this.lazyIndexes.clear();
521
+ this.pendingIndexes.clear();
522
+ }
523
+ }