@forcecalendar/core 2.1.65 → 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 +143 -61
- 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';
|
|
@@ -29,6 +29,7 @@ export class EventStore {
|
|
|
29
29
|
/** @type {Map<string, Set<string>>} Status -> Set of event IDs */
|
|
30
30
|
byStatus: new Map()
|
|
31
31
|
};
|
|
32
|
+
this.eventIndexRefs = new Map();
|
|
32
33
|
|
|
33
34
|
// Timezone manager for conversions (use singleton to share cache)
|
|
34
35
|
this.timezoneManager = TimezoneManager.getInstance();
|
|
@@ -39,6 +40,9 @@ export class EventStore {
|
|
|
39
40
|
// Performance optimizer
|
|
40
41
|
this.optimizer = new PerformanceOptimizer(config.performance);
|
|
41
42
|
|
|
43
|
+
// Recurrence expansion engine
|
|
44
|
+
this.recurrenceEngine = config.recurrenceEngine || new RecurrenceEngineV2();
|
|
45
|
+
|
|
42
46
|
// Conflict detector
|
|
43
47
|
this.conflictDetector = new ConflictDetector(this);
|
|
44
48
|
|
|
@@ -334,9 +338,6 @@ export class EventStore {
|
|
|
334
338
|
getEventsForDate(date, timezone = null) {
|
|
335
339
|
timezone = timezone || this.defaultTimezone;
|
|
336
340
|
|
|
337
|
-
// Use local date string for the query date (in the calendar's timezone)
|
|
338
|
-
const dateStr = DateUtils.getLocalDateString(date);
|
|
339
|
-
|
|
340
341
|
// Collect candidate event IDs from indices
|
|
341
342
|
const candidateIds = new Set();
|
|
342
343
|
|
|
@@ -409,23 +410,40 @@ export class EventStore {
|
|
|
409
410
|
|
|
410
411
|
// Collect all events from those dates
|
|
411
412
|
const checkedIds = new Set();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const dateStr = DateUtils.getLocalDateString(date);
|
|
415
|
-
const eventIds = this.indices.byDate.get(dateStr) || new Set();
|
|
413
|
+
const addCandidateIds = eventIds => {
|
|
414
|
+
if (!eventIds) return;
|
|
416
415
|
|
|
417
416
|
eventIds.forEach(id => {
|
|
418
417
|
if (!checkedIds.has(id) && id !== excludeId) {
|
|
419
418
|
checkedIds.add(id);
|
|
420
|
-
const event = this.events.get(id);
|
|
421
|
-
|
|
422
|
-
if (event && event.overlaps({ start, end })) {
|
|
423
|
-
overlapping.push(event);
|
|
424
|
-
}
|
|
425
419
|
}
|
|
426
420
|
});
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
dates.forEach(date => {
|
|
424
|
+
// Use getLocalDateString to match the index key format (YYYY-MM-DD)
|
|
425
|
+
const dateStr = DateUtils.getLocalDateString(date);
|
|
426
|
+
addCandidateIds(this.indices.byDate.get(dateStr));
|
|
427
427
|
});
|
|
428
428
|
|
|
429
|
+
// Lazy-indexed long events may not have every day in byDate. Use month
|
|
430
|
+
// buckets as candidates and rely on precise overlap filtering below.
|
|
431
|
+
const currentMonth = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
|
|
432
|
+
const endMonth = new Date(endDate.getFullYear(), endDate.getMonth(), 1);
|
|
433
|
+
while (currentMonth <= endMonth) {
|
|
434
|
+
const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`;
|
|
435
|
+
addCandidateIds(this.indices.byMonth.get(monthKey));
|
|
436
|
+
currentMonth.setMonth(currentMonth.getMonth() + 1);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const id of checkedIds) {
|
|
440
|
+
const event = this.events.get(id);
|
|
441
|
+
|
|
442
|
+
if (event && event.overlaps({ start, end })) {
|
|
443
|
+
overlapping.push(event);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
429
447
|
return overlapping.sort((a, b) => a.start - b.start);
|
|
430
448
|
}
|
|
431
449
|
|
|
@@ -579,6 +597,20 @@ export class EventStore {
|
|
|
579
597
|
return baseEvents;
|
|
580
598
|
}
|
|
581
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
|
+
|
|
582
614
|
// Expand recurring events
|
|
583
615
|
const expandedEvents = [];
|
|
584
616
|
baseEvents.forEach(event => {
|
|
@@ -615,7 +647,9 @@ export class EventStore {
|
|
|
615
647
|
|
|
616
648
|
// Expand in the event's timezone for accurate recurrence calculation
|
|
617
649
|
const eventTimezone = event.timeZone || timezone;
|
|
618
|
-
const occurrences =
|
|
650
|
+
const occurrences = this.recurrenceEngine.expandEvent(event, rangeStart, rangeEnd, {
|
|
651
|
+
timezone: eventTimezone
|
|
652
|
+
});
|
|
619
653
|
|
|
620
654
|
return occurrences.map((occurrence, index) => {
|
|
621
655
|
// Create a new event instance for each occurrence
|
|
@@ -623,10 +657,11 @@ export class EventStore {
|
|
|
623
657
|
id: `${event.id}_occurrence_${index}`,
|
|
624
658
|
start: occurrence.start,
|
|
625
659
|
end: occurrence.end,
|
|
626
|
-
timeZone: eventTimezone,
|
|
660
|
+
timeZone: occurrence.timezone || eventTimezone,
|
|
627
661
|
metadata: {
|
|
628
662
|
...event.metadata,
|
|
629
663
|
recurringEventId: event.id,
|
|
664
|
+
occurrenceId: occurrence.id,
|
|
630
665
|
occurrenceIndex: index
|
|
631
666
|
}
|
|
632
667
|
});
|
|
@@ -647,6 +682,7 @@ export class EventStore {
|
|
|
647
682
|
this.indices.recurring.clear();
|
|
648
683
|
this.indices.byCategory.clear();
|
|
649
684
|
this.indices.byStatus.clear();
|
|
685
|
+
this.eventIndexRefs.clear();
|
|
650
686
|
|
|
651
687
|
this._notifyChange({
|
|
652
688
|
type: 'clear',
|
|
@@ -687,6 +723,8 @@ export class EventStore {
|
|
|
687
723
|
* @private
|
|
688
724
|
*/
|
|
689
725
|
_indexEvent(event) {
|
|
726
|
+
this._createIndexRefs(event.id);
|
|
727
|
+
|
|
690
728
|
// Check if should use lazy indexing for large date ranges
|
|
691
729
|
if (this.optimizer.shouldUseLazyIndexing(event)) {
|
|
692
730
|
this._indexEventLazy(event);
|
|
@@ -706,26 +744,14 @@ export class EventStore {
|
|
|
706
744
|
|
|
707
745
|
dates.forEach(date => {
|
|
708
746
|
const dateStr = DateUtils.getLocalDateString(date);
|
|
709
|
-
|
|
710
|
-
if (!this.indices.byDate.has(dateStr)) {
|
|
711
|
-
this.indices.byDate.set(dateStr, new Set());
|
|
712
|
-
}
|
|
713
|
-
this.indices.byDate.get(dateStr).add(event.id);
|
|
747
|
+
this._addToKeyedIndex('byDate', dateStr, event.id);
|
|
714
748
|
});
|
|
715
749
|
|
|
716
|
-
// Index by month(s) using UTC
|
|
717
|
-
const startMonth = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}`;
|
|
718
|
-
const endMonth = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}`;
|
|
719
|
-
|
|
720
750
|
// Add to all months the event spans
|
|
721
751
|
const currentMonth = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
|
|
722
752
|
while (currentMonth <= endDate) {
|
|
723
753
|
const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`;
|
|
724
|
-
|
|
725
|
-
if (!this.indices.byMonth.has(monthKey)) {
|
|
726
|
-
this.indices.byMonth.set(monthKey, new Set());
|
|
727
|
-
}
|
|
728
|
-
this.indices.byMonth.get(monthKey).add(event.id);
|
|
754
|
+
this._addToKeyedIndex('byMonth', monthKey, event.id);
|
|
729
755
|
|
|
730
756
|
currentMonth.setMonth(currentMonth.getMonth() + 1);
|
|
731
757
|
}
|
|
@@ -733,24 +759,19 @@ export class EventStore {
|
|
|
733
759
|
// Index by categories
|
|
734
760
|
if (event.categories && event.categories.length > 0) {
|
|
735
761
|
event.categories.forEach(category => {
|
|
736
|
-
|
|
737
|
-
this.indices.byCategory.set(category, new Set());
|
|
738
|
-
}
|
|
739
|
-
this.indices.byCategory.get(category).add(event.id);
|
|
762
|
+
this._addToKeyedIndex('byCategory', category, event.id);
|
|
740
763
|
});
|
|
741
764
|
}
|
|
742
765
|
|
|
743
766
|
// Index by status
|
|
744
767
|
if (event.status) {
|
|
745
|
-
|
|
746
|
-
this.indices.byStatus.set(event.status, new Set());
|
|
747
|
-
}
|
|
748
|
-
this.indices.byStatus.get(event.status).add(event.id);
|
|
768
|
+
this._addToKeyedIndex('byStatus', event.status, event.id);
|
|
749
769
|
}
|
|
750
770
|
|
|
751
771
|
// Index recurring events
|
|
752
772
|
if (event.recurring) {
|
|
753
773
|
this.indices.recurring.add(event.id);
|
|
774
|
+
this.eventIndexRefs.get(event.id).recurring = true;
|
|
754
775
|
}
|
|
755
776
|
}
|
|
756
777
|
|
|
@@ -759,8 +780,7 @@ export class EventStore {
|
|
|
759
780
|
* @private
|
|
760
781
|
*/
|
|
761
782
|
_indexEventLazy(event) {
|
|
762
|
-
|
|
763
|
-
const markers = this.optimizer.createLazyIndexMarkers(event);
|
|
783
|
+
this.optimizer.createLazyIndexMarkers(event);
|
|
764
784
|
|
|
765
785
|
// Index only the boundaries initially (in event's local timezone)
|
|
766
786
|
const eventStartLocal = event.getStartInTimezone(event.timeZone);
|
|
@@ -779,10 +799,7 @@ export class EventStore {
|
|
|
779
799
|
|
|
780
800
|
firstWeekDates.forEach(date => {
|
|
781
801
|
const dateStr = DateUtils.getLocalDateString(date);
|
|
782
|
-
|
|
783
|
-
this.indices.byDate.set(dateStr, new Set());
|
|
784
|
-
}
|
|
785
|
-
this.indices.byDate.get(dateStr).add(event.id);
|
|
802
|
+
this._addToKeyedIndex('byDate', dateStr, event.id);
|
|
786
803
|
});
|
|
787
804
|
|
|
788
805
|
// Index last week if different from first
|
|
@@ -796,10 +813,7 @@ export class EventStore {
|
|
|
796
813
|
|
|
797
814
|
lastWeekDates.forEach(date => {
|
|
798
815
|
const dateStr = DateUtils.getLocalDateString(date);
|
|
799
|
-
|
|
800
|
-
this.indices.byDate.set(dateStr, new Set());
|
|
801
|
-
}
|
|
802
|
-
this.indices.byDate.get(dateStr).add(event.id);
|
|
816
|
+
this._addToKeyedIndex('byDate', dateStr, event.id);
|
|
803
817
|
});
|
|
804
818
|
}
|
|
805
819
|
|
|
@@ -807,33 +821,53 @@ export class EventStore {
|
|
|
807
821
|
const currentMonth = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
|
|
808
822
|
while (currentMonth <= endDate) {
|
|
809
823
|
const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`;
|
|
810
|
-
|
|
811
|
-
this.indices.byMonth.set(monthKey, new Set());
|
|
812
|
-
}
|
|
813
|
-
this.indices.byMonth.get(monthKey).add(event.id);
|
|
824
|
+
this._addToKeyedIndex('byMonth', monthKey, event.id);
|
|
814
825
|
currentMonth.setMonth(currentMonth.getMonth() + 1);
|
|
815
826
|
}
|
|
816
827
|
|
|
817
828
|
// Index other properties normally
|
|
818
829
|
if (event.categories && event.categories.length > 0) {
|
|
819
830
|
event.categories.forEach(category => {
|
|
820
|
-
|
|
821
|
-
this.indices.byCategory.set(category, new Set());
|
|
822
|
-
}
|
|
823
|
-
this.indices.byCategory.get(category).add(event.id);
|
|
831
|
+
this._addToKeyedIndex('byCategory', category, event.id);
|
|
824
832
|
});
|
|
825
833
|
}
|
|
826
834
|
|
|
827
835
|
if (event.status) {
|
|
828
|
-
|
|
829
|
-
this.indices.byStatus.set(event.status, new Set());
|
|
830
|
-
}
|
|
831
|
-
this.indices.byStatus.get(event.status).add(event.id);
|
|
836
|
+
this._addToKeyedIndex('byStatus', event.status, event.id);
|
|
832
837
|
}
|
|
833
838
|
|
|
834
839
|
if (event.recurring) {
|
|
835
840
|
this.indices.recurring.add(event.id);
|
|
841
|
+
this.eventIndexRefs.get(event.id).recurring = true;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Create reverse index references for an event.
|
|
847
|
+
* @private
|
|
848
|
+
*/
|
|
849
|
+
_createIndexRefs(eventId) {
|
|
850
|
+
this.eventIndexRefs.set(eventId, {
|
|
851
|
+
byDate: new Set(),
|
|
852
|
+
byMonth: new Set(),
|
|
853
|
+
byCategory: new Set(),
|
|
854
|
+
byStatus: new Set(),
|
|
855
|
+
recurring: false
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Add an event to a keyed index and record the reverse reference.
|
|
861
|
+
* @private
|
|
862
|
+
*/
|
|
863
|
+
_addToKeyedIndex(indexName, key, eventId) {
|
|
864
|
+
const index = this.indices[indexName];
|
|
865
|
+
if (!index.has(key)) {
|
|
866
|
+
index.set(key, new Set());
|
|
836
867
|
}
|
|
868
|
+
|
|
869
|
+
index.get(key).add(eventId);
|
|
870
|
+
this.eventIndexRefs.get(eventId)?.[indexName].add(key);
|
|
837
871
|
}
|
|
838
872
|
|
|
839
873
|
/**
|
|
@@ -841,6 +875,22 @@ export class EventStore {
|
|
|
841
875
|
* @private
|
|
842
876
|
*/
|
|
843
877
|
_unindexEvent(event) {
|
|
878
|
+
const refs = this.eventIndexRefs.get(event.id);
|
|
879
|
+
|
|
880
|
+
if (refs) {
|
|
881
|
+
this._removeFromReferencedIndex('byDate', refs.byDate, event.id);
|
|
882
|
+
this._removeFromReferencedIndex('byMonth', refs.byMonth, event.id);
|
|
883
|
+
this._removeFromReferencedIndex('byCategory', refs.byCategory, event.id);
|
|
884
|
+
this._removeFromReferencedIndex('byStatus', refs.byStatus, event.id);
|
|
885
|
+
|
|
886
|
+
if (refs.recurring) {
|
|
887
|
+
this.indices.recurring.delete(event.id);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
this.eventIndexRefs.delete(event.id);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
844
894
|
// Remove from date indices
|
|
845
895
|
for (const [dateStr, eventIds] of this.indices.byDate) {
|
|
846
896
|
eventIds.delete(event.id);
|
|
@@ -877,6 +927,24 @@ export class EventStore {
|
|
|
877
927
|
this.indices.recurring.delete(event.id);
|
|
878
928
|
}
|
|
879
929
|
|
|
930
|
+
/**
|
|
931
|
+
* Remove an event from only the keys it was indexed into.
|
|
932
|
+
* @private
|
|
933
|
+
*/
|
|
934
|
+
_removeFromReferencedIndex(indexName, keys, eventId) {
|
|
935
|
+
const index = this.indices[indexName];
|
|
936
|
+
|
|
937
|
+
for (const key of keys) {
|
|
938
|
+
const eventIds = index.get(key);
|
|
939
|
+
if (!eventIds) continue;
|
|
940
|
+
|
|
941
|
+
eventIds.delete(eventId);
|
|
942
|
+
if (eventIds.size === 0) {
|
|
943
|
+
index.delete(key);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
880
948
|
/**
|
|
881
949
|
* Notify listeners of changes
|
|
882
950
|
* @private
|
|
@@ -938,6 +1006,18 @@ export class EventStore {
|
|
|
938
1006
|
Array.from(this.indices.byStatus.entries()).map(([k, v]) => [k, new Set(v)])
|
|
939
1007
|
)
|
|
940
1008
|
},
|
|
1009
|
+
eventIndexRefs: new Map(
|
|
1010
|
+
Array.from(this.eventIndexRefs.entries()).map(([eventId, refs]) => [
|
|
1011
|
+
eventId,
|
|
1012
|
+
{
|
|
1013
|
+
byDate: new Set(refs.byDate),
|
|
1014
|
+
byMonth: new Set(refs.byMonth),
|
|
1015
|
+
byCategory: new Set(refs.byCategory),
|
|
1016
|
+
byStatus: new Set(refs.byStatus),
|
|
1017
|
+
recurring: refs.recurring
|
|
1018
|
+
}
|
|
1019
|
+
])
|
|
1020
|
+
),
|
|
941
1021
|
version: this.version
|
|
942
1022
|
};
|
|
943
1023
|
}
|
|
@@ -981,6 +1061,7 @@ export class EventStore {
|
|
|
981
1061
|
if (this.batchBackup) {
|
|
982
1062
|
this.events = this.batchBackup.events;
|
|
983
1063
|
this.indices = this.batchBackup.indices;
|
|
1064
|
+
this.eventIndexRefs = this.batchBackup.eventIndexRefs;
|
|
984
1065
|
this.version = this.batchBackup.version;
|
|
985
1066
|
this.batchBackup = null;
|
|
986
1067
|
|
|
@@ -1154,7 +1235,6 @@ export class EventStore {
|
|
|
1154
1235
|
cutoffDate.setMonth(cutoffDate.getMonth() - 6); // Default: 6 months ago
|
|
1155
1236
|
}
|
|
1156
1237
|
|
|
1157
|
-
const cutoffStr = cutoffDate.toDateString();
|
|
1158
1238
|
let removed = 0;
|
|
1159
1239
|
|
|
1160
1240
|
// Clean up date indices
|
|
@@ -1172,13 +1252,15 @@ export class EventStore {
|
|
|
1172
1252
|
}
|
|
1173
1253
|
|
|
1174
1254
|
if (!stillNeeded) {
|
|
1255
|
+
for (const eventId of eventIds) {
|
|
1256
|
+
this.eventIndexRefs.get(eventId)?.byDate.delete(dateStr);
|
|
1257
|
+
}
|
|
1175
1258
|
this.indices.byDate.delete(dateStr);
|
|
1176
1259
|
removed++;
|
|
1177
1260
|
}
|
|
1178
1261
|
}
|
|
1179
1262
|
}
|
|
1180
1263
|
|
|
1181
|
-
console.log(`Optimized indices: removed ${removed} old date entries`);
|
|
1182
1264
|
return removed;
|
|
1183
1265
|
}
|
|
1184
1266
|
|
|
@@ -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';
|