@forcecalendar/core 2.1.66 → 2.1.68
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/DateUtils.js +1 -13
- package/core/conflicts/ConflictDetector.js +0 -3
- package/core/events/Event.js +2 -4
- package/core/events/EventStore.js +23 -3
- package/core/events/RecurrenceEngine.js +2 -2
- package/core/events/RecurrenceEngineV2.js +48 -17
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +0 -13
- package/core/performance/AdaptiveMemoryManager.js +2 -2
- package/core/search/EventSearch.js +1 -1
- package/package.json +1 -1
|
@@ -507,7 +507,7 @@ export class DateUtils {
|
|
|
507
507
|
* @param {string} timeZone - IANA timezone string
|
|
508
508
|
* @returns {Date}
|
|
509
509
|
*/
|
|
510
|
-
static addHoursWithDST(date, hours,
|
|
510
|
+
static addHoursWithDST(date, hours, _timeZone) {
|
|
511
511
|
const result = new Date(date);
|
|
512
512
|
|
|
513
513
|
// UTC millisecond arithmetic is inherently DST-agnostic.
|
|
@@ -534,18 +534,6 @@ export class DateUtils {
|
|
|
534
534
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
535
535
|
const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
|
|
536
536
|
|
|
537
|
-
// Use Intl API to get the UTC time for this local time in the timezone
|
|
538
|
-
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
539
|
-
timeZone,
|
|
540
|
-
year: 'numeric',
|
|
541
|
-
month: '2-digit',
|
|
542
|
-
day: '2-digit',
|
|
543
|
-
hour: '2-digit',
|
|
544
|
-
minute: '2-digit',
|
|
545
|
-
second: '2-digit',
|
|
546
|
-
hour12: false
|
|
547
|
-
});
|
|
548
|
-
|
|
549
537
|
// Parse the local date in the target timezone
|
|
550
538
|
const localDate = new Date(`${dateStr}T${timeStr}`);
|
|
551
539
|
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
* ConflictDetector - Detects scheduling conflicts between events
|
|
3
3
|
* Checks for time overlaps, attendee conflicts, and resource conflicts
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
import { DateUtils } from '../calendar/DateUtils.js';
|
|
7
|
-
|
|
8
5
|
export class ConflictDetector {
|
|
9
6
|
/**
|
|
10
7
|
* Create a new ConflictDetector
|
package/core/events/Event.js
CHANGED
|
@@ -366,8 +366,6 @@ export class Event {
|
|
|
366
366
|
* @param {string} [timezone] - Timezone for the new dates
|
|
367
367
|
*/
|
|
368
368
|
updateTimes(start, end, timezone) {
|
|
369
|
-
const tz = timezone || this.timeZone;
|
|
370
|
-
|
|
371
369
|
this.start = start instanceof Date ? start : new Date(start);
|
|
372
370
|
this.end = end instanceof Date ? end : new Date(end);
|
|
373
371
|
|
|
@@ -477,9 +475,9 @@ export class Event {
|
|
|
477
475
|
throw new Error('Parameter must be an Event instance or have start/end properties');
|
|
478
476
|
}
|
|
479
477
|
|
|
480
|
-
|
|
478
|
+
const thisStart = this.start;
|
|
481
479
|
let thisEnd = this.end;
|
|
482
|
-
|
|
480
|
+
const otherStart = otherEvent.start;
|
|
483
481
|
let otherEnd = otherEvent.end;
|
|
484
482
|
|
|
485
483
|
// Normalize all-day event boundaries for consistent comparison.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Event } from './Event.js';
|
|
2
2
|
import { DateUtils } from '../calendar/DateUtils.js';
|
|
3
|
-
import {
|
|
3
|
+
import { RecurrenceEngineV2 } from './RecurrenceEngineV2.js';
|
|
4
4
|
import { PerformanceOptimizer } from '../performance/PerformanceOptimizer.js';
|
|
5
5
|
import { ConflictDetector } from '../conflicts/ConflictDetector.js';
|
|
6
6
|
import { TimezoneManager } from '../timezone/TimezoneManager.js';
|
|
@@ -40,6 +40,9 @@ export class EventStore {
|
|
|
40
40
|
// Performance optimizer
|
|
41
41
|
this.optimizer = new PerformanceOptimizer(config.performance);
|
|
42
42
|
|
|
43
|
+
// Recurrence expansion engine
|
|
44
|
+
this.recurrenceEngine = config.recurrenceEngine || new RecurrenceEngineV2();
|
|
45
|
+
|
|
43
46
|
// Conflict detector
|
|
44
47
|
this.conflictDetector = new ConflictDetector(this);
|
|
45
48
|
|
|
@@ -594,6 +597,20 @@ export class EventStore {
|
|
|
594
597
|
return baseEvents;
|
|
595
598
|
}
|
|
596
599
|
|
|
600
|
+
// Recurring series can start before the requested range but still have
|
|
601
|
+
// occurrences inside it, so include all recurring series as expansion
|
|
602
|
+
// candidates and let the recurrence engine filter by range.
|
|
603
|
+
const baseEventIds = new Set(baseEvents.map(event => event.id));
|
|
604
|
+
for (const eventId of this.indices.recurring) {
|
|
605
|
+
if (!baseEventIds.has(eventId)) {
|
|
606
|
+
const event = this.events.get(eventId);
|
|
607
|
+
if (event) {
|
|
608
|
+
baseEvents.push(event);
|
|
609
|
+
baseEventIds.add(eventId);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
597
614
|
// Expand recurring events
|
|
598
615
|
const expandedEvents = [];
|
|
599
616
|
baseEvents.forEach(event => {
|
|
@@ -630,7 +647,9 @@ export class EventStore {
|
|
|
630
647
|
|
|
631
648
|
// Expand in the event's timezone for accurate recurrence calculation
|
|
632
649
|
const eventTimezone = event.timeZone || timezone;
|
|
633
|
-
const occurrences =
|
|
650
|
+
const occurrences = this.recurrenceEngine.expandEvent(event, rangeStart, rangeEnd, {
|
|
651
|
+
timezone: eventTimezone
|
|
652
|
+
});
|
|
634
653
|
|
|
635
654
|
return occurrences.map((occurrence, index) => {
|
|
636
655
|
// Create a new event instance for each occurrence
|
|
@@ -638,10 +657,11 @@ export class EventStore {
|
|
|
638
657
|
id: `${event.id}_occurrence_${index}`,
|
|
639
658
|
start: occurrence.start,
|
|
640
659
|
end: occurrence.end,
|
|
641
|
-
timeZone: eventTimezone,
|
|
660
|
+
timeZone: occurrence.timezone || eventTimezone,
|
|
642
661
|
metadata: {
|
|
643
662
|
...event.metadata,
|
|
644
663
|
recurringEventId: event.id,
|
|
664
|
+
occurrenceId: occurrence.id,
|
|
645
665
|
occurrenceIndex: index
|
|
646
666
|
}
|
|
647
667
|
});
|
|
@@ -165,7 +165,7 @@ export class RecurrenceEngine {
|
|
|
165
165
|
* @param {string} [timezone] - Timezone for calculation
|
|
166
166
|
* @returns {Date} Next occurrence date
|
|
167
167
|
*/
|
|
168
|
-
static getNextOccurrence(currentDate, rule,
|
|
168
|
+
static getNextOccurrence(currentDate, rule, _timezone = 'UTC') {
|
|
169
169
|
const next = new Date(currentDate);
|
|
170
170
|
|
|
171
171
|
switch (rule.freq) {
|
|
@@ -331,7 +331,7 @@ export class RecurrenceEngine {
|
|
|
331
331
|
* @param {string} [eventId] - Event ID for better exception tracking
|
|
332
332
|
* @returns {boolean}
|
|
333
333
|
*/
|
|
334
|
-
static isException(date, rule,
|
|
334
|
+
static isException(date, rule, _eventId = null) {
|
|
335
335
|
if (!rule.exceptions || rule.exceptions.length === 0) {
|
|
336
336
|
return false;
|
|
337
337
|
}
|
|
@@ -48,11 +48,11 @@ export class RecurrenceEngineV2 {
|
|
|
48
48
|
// Check cache
|
|
49
49
|
const cacheKey = this.getCacheKey(event.id, rangeStart, rangeEnd, options);
|
|
50
50
|
if (this.occurrenceCache.has(cacheKey)) {
|
|
51
|
-
return this.occurrenceCache.get(cacheKey);
|
|
51
|
+
return this.cloneOccurrences(this.occurrenceCache.get(cacheKey));
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (!event.recurring || !event.recurrenceRule) {
|
|
55
|
-
return [this.createOccurrence(event, event.start, event.end)];
|
|
55
|
+
return this.cloneOccurrences([this.createOccurrence(event, event.start, event.end)]);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const rule = RRuleParser.parse(event.recurrenceRule);
|
|
@@ -64,7 +64,8 @@ export class RecurrenceEngineV2 {
|
|
|
64
64
|
currentDate: new Date(event.start),
|
|
65
65
|
count: 0,
|
|
66
66
|
tzOffsets: new Map(),
|
|
67
|
-
dstTransitions: []
|
|
67
|
+
dstTransitions: [],
|
|
68
|
+
stuckIterations: 0
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
// Pre-calculate DST transitions in range
|
|
@@ -85,21 +86,20 @@ export class RecurrenceEngineV2 {
|
|
|
85
86
|
|
|
86
87
|
// Check exceptions and modifications
|
|
87
88
|
if (occurrence) {
|
|
88
|
-
|
|
89
|
+
let shouldInclude = true;
|
|
89
90
|
|
|
90
91
|
// Skip if exception
|
|
91
92
|
if (this.isException(event.id, occurrence.start, rule)) {
|
|
92
93
|
if (!includeCancelled) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
shouldInclude = false;
|
|
95
|
+
} else {
|
|
96
|
+
occurrence.status = 'cancelled';
|
|
97
|
+
occurrence.cancellationReason = this.getExceptionReason(event.id, occurrence.start);
|
|
96
98
|
}
|
|
97
|
-
occurrence.status = 'cancelled';
|
|
98
|
-
occurrence.cancellationReason = this.getExceptionReason(event.id, occurrence.start);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Apply modifications if any
|
|
102
|
-
if (includeModified) {
|
|
102
|
+
if (shouldInclude && includeModified) {
|
|
103
103
|
const modified = this.getModifiedInstance(event.id, occurrence.start);
|
|
104
104
|
if (modified) {
|
|
105
105
|
Object.assign(occurrence, modified);
|
|
@@ -107,14 +107,26 @@ export class RecurrenceEngineV2 {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
if (shouldInclude) {
|
|
111
|
+
occurrences.push(occurrence);
|
|
112
|
+
}
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
// Get next occurrence date
|
|
117
|
+
const previousTimestamp = state.currentDate.getTime();
|
|
115
118
|
state.currentDate = this.getNextDate(state.currentDate, rule, timezone, state);
|
|
116
119
|
state.count++;
|
|
117
120
|
|
|
121
|
+
if (state.currentDate.getTime() <= previousTimestamp) {
|
|
122
|
+
state.stuckIterations++;
|
|
123
|
+
if (state.stuckIterations >= 3) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
state.stuckIterations = 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
// Check COUNT limit
|
|
119
131
|
if (rule.count && state.count >= rule.count) {
|
|
120
132
|
break;
|
|
@@ -129,7 +141,7 @@ export class RecurrenceEngineV2 {
|
|
|
129
141
|
// Cache results
|
|
130
142
|
this.cacheOccurrences(cacheKey, occurrences);
|
|
131
143
|
|
|
132
|
-
return occurrences;
|
|
144
|
+
return this.cloneOccurrences(occurrences);
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
/**
|
|
@@ -169,7 +181,7 @@ export class RecurrenceEngineV2 {
|
|
|
169
181
|
/**
|
|
170
182
|
* Get next occurrence date with complex pattern support
|
|
171
183
|
*/
|
|
172
|
-
getNextDate(currentDate, rule, timezone,
|
|
184
|
+
getNextDate(currentDate, rule, timezone, _state = {}) {
|
|
173
185
|
const next = new Date(currentDate);
|
|
174
186
|
|
|
175
187
|
switch (rule.freq) {
|
|
@@ -226,7 +238,7 @@ export class RecurrenceEngineV2 {
|
|
|
226
238
|
/**
|
|
227
239
|
* Get next weekly occurrence with BYDAY support
|
|
228
240
|
*/
|
|
229
|
-
getNextWeekly(date, rule,
|
|
241
|
+
getNextWeekly(date, rule, _timezone) {
|
|
230
242
|
const next = new Date(date);
|
|
231
243
|
|
|
232
244
|
if (rule.byDay && rule.byDay.length > 0) {
|
|
@@ -276,7 +288,7 @@ export class RecurrenceEngineV2 {
|
|
|
276
288
|
/**
|
|
277
289
|
* Get next monthly occurrence with complex patterns
|
|
278
290
|
*/
|
|
279
|
-
getNextMonthly(date, rule,
|
|
291
|
+
getNextMonthly(date, rule, _timezone) {
|
|
280
292
|
const next = new Date(date);
|
|
281
293
|
|
|
282
294
|
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
@@ -372,7 +384,7 @@ export class RecurrenceEngineV2 {
|
|
|
372
384
|
/**
|
|
373
385
|
* Get next yearly occurrence
|
|
374
386
|
*/
|
|
375
|
-
getNextYearly(date, rule,
|
|
387
|
+
getNextYearly(date, rule, _timezone) {
|
|
376
388
|
const next = new Date(date);
|
|
377
389
|
|
|
378
390
|
if (rule.byMonth && rule.byMonth.length > 0) {
|
|
@@ -617,7 +629,7 @@ export class RecurrenceEngineV2 {
|
|
|
617
629
|
* Cache occurrences
|
|
618
630
|
*/
|
|
619
631
|
cacheOccurrences(key, occurrences) {
|
|
620
|
-
this.occurrenceCache.set(key, occurrences);
|
|
632
|
+
this.occurrenceCache.set(key, this.cloneOccurrences(occurrences));
|
|
621
633
|
|
|
622
634
|
// LRU eviction
|
|
623
635
|
if (this.occurrenceCache.size > this.cacheSize) {
|
|
@@ -626,6 +638,25 @@ export class RecurrenceEngineV2 {
|
|
|
626
638
|
}
|
|
627
639
|
}
|
|
628
640
|
|
|
641
|
+
/**
|
|
642
|
+
* Clone occurrence results before returning or caching.
|
|
643
|
+
*/
|
|
644
|
+
cloneOccurrences(occurrences) {
|
|
645
|
+
return occurrences.map(occurrence => ({
|
|
646
|
+
...occurrence,
|
|
647
|
+
start: occurrence.start ? new Date(occurrence.start) : occurrence.start,
|
|
648
|
+
end: occurrence.end ? new Date(occurrence.end) : occurrence.end,
|
|
649
|
+
startUTC: occurrence.startUTC ? new Date(occurrence.startUTC) : occurrence.startUTC,
|
|
650
|
+
endUTC: occurrence.endUTC ? new Date(occurrence.endUTC) : occurrence.endUTC,
|
|
651
|
+
originalStart: occurrence.originalStart
|
|
652
|
+
? new Date(occurrence.originalStart)
|
|
653
|
+
: occurrence.originalStart,
|
|
654
|
+
categories: Array.isArray(occurrence.categories)
|
|
655
|
+
? [...occurrence.categories]
|
|
656
|
+
: occurrence.categories
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
|
|
629
660
|
/**
|
|
630
661
|
* Clear cache for specific event
|
|
631
662
|
*/
|
package/core/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export { RRuleParser } from './events/RRuleParser.js';
|
|
|
28
28
|
export { EnhancedCalendar } from './integration/EnhancedCalendar.js';
|
|
29
29
|
|
|
30
30
|
// Version — keep in sync with package.json
|
|
31
|
-
export const VERSION = '2.1.
|
|
31
|
+
export const VERSION = '2.1.68';
|
|
32
32
|
|
|
33
33
|
// Default export
|
|
34
34
|
export { Calendar as default } from './calendar/Calendar.js';
|
|
@@ -411,19 +411,6 @@ export function createEnhancedCalendar(config) {
|
|
|
411
411
|
calendar.cancelOccurrence('meeting-1', new Date('2024-01-15T10:00:00'), 'Public Holiday');
|
|
412
412
|
|
|
413
413
|
// Example: Advanced search
|
|
414
|
-
calendar
|
|
415
|
-
.advancedSearch('standup', {
|
|
416
|
-
dateRange: {
|
|
417
|
-
start: new Date('2024-01-01'),
|
|
418
|
-
end: new Date('2024-01-31')
|
|
419
|
-
},
|
|
420
|
-
categories: ['meetings'],
|
|
421
|
-
modifiedOnly: false
|
|
422
|
-
})
|
|
423
|
-
.then(results => {
|
|
424
|
-
console.log('Search results:', results);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
414
|
return calendar;
|
|
428
415
|
}
|
|
429
416
|
|
|
@@ -187,7 +187,7 @@ export class AdaptiveMemoryManager {
|
|
|
187
187
|
* Increase cache sizes when memory is available
|
|
188
188
|
*/
|
|
189
189
|
increaseCacheSizes() {
|
|
190
|
-
for (const
|
|
190
|
+
for (const cacheInfo of this.caches.values()) {
|
|
191
191
|
// Only increase if cache is being actively used
|
|
192
192
|
const timeSinceAccess = Date.now() - cacheInfo.lastAccess;
|
|
193
193
|
if (timeSinceAccess < 60000) {
|
|
@@ -262,7 +262,7 @@ export class AdaptiveMemoryManager {
|
|
|
262
262
|
* Emergency clear all caches
|
|
263
263
|
*/
|
|
264
264
|
emergencyClear() {
|
|
265
|
-
for (const
|
|
265
|
+
for (const cacheInfo of this.caches.values()) {
|
|
266
266
|
if (cacheInfo.cache.clear) {
|
|
267
267
|
cacheInfo.cache.clear();
|
|
268
268
|
}
|