@forcecalendar/core 2.1.66 → 2.1.67
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/events/EventStore.js +23 -3
- package/core/events/RecurrenceEngineV2.js +48 -17
- package/core/index.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
});
|
|
@@ -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.67';
|
|
32
32
|
|
|
33
33
|
// Default export
|
|
34
34
|
export { Calendar as default } from './calendar/Calendar.js';
|