@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.
@@ -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, timeZone) {
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
@@ -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
- let thisStart = this.start;
478
+ const thisStart = this.start;
481
479
  let thisEnd = this.end;
482
- let otherStart = otherEvent.start;
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 { 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
  });
@@ -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, timezone = 'UTC') {
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, eventId = null) {
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
- 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.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 [name, cacheInfo] of this.caches) {
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 [name, cacheInfo] of this.caches) {
265
+ for (const cacheInfo of this.caches.values()) {
266
266
  if (cacheInfo.cache.clear) {
267
267
  cacheInfo.cache.clear();
268
268
  }
@@ -322,7 +322,7 @@ export class EventSearch {
322
322
 
323
323
  // Sort events within groups if requested
324
324
  if (sortEvents) {
325
- for (const [key, eventList] of groups) {
325
+ for (const eventList of groups.values()) {
326
326
  eventList.sort((a, b) => a.start - b.start);
327
327
  }
328
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/core",
3
- "version": "2.1.66",
3
+ "version": "2.1.68",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "A modern, lightweight, framework-agnostic calendar engine optimized for Salesforce",