@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.
@@ -1,6 +1,6 @@
1
1
  import { Event } from './Event.js';
2
2
  import { DateUtils } from '../calendar/DateUtils.js';
3
- import { RecurrenceEngine } from './RecurrenceEngine.js';
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 = RecurrenceEngine.expandEvent(event, rangeStart, rangeEnd);
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
- const dateKey = this.getDateKey(occurrence.start);
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
- state.currentDate = this.getNextDate(state.currentDate, rule, timezone);
94
- state.count++;
95
- continue;
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
- occurrences.push(occurrence);
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, state = {}) {
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, timezone) {
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, timezone) {
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, timezone) {
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.66';
31
+ export const VERSION = '2.1.67';
32
32
 
33
33
  // Default export
34
34
  export { Calendar as default } from './calendar/Calendar.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/core",
3
- "version": "2.1.66",
3
+ "version": "2.1.67",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "A modern, lightweight, framework-agnostic calendar engine optimized for Salesforce",