@forcecalendar/core 2.1.0 → 2.1.2
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/Calendar.js +7 -9
- package/core/calendar/DateUtils.js +10 -9
- package/core/conflicts/ConflictDetector.js +24 -24
- package/core/events/Event.js +14 -20
- package/core/events/EventStore.js +70 -19
- package/core/events/RRuleParser.js +423 -394
- package/core/events/RecurrenceEngine.js +33 -21
- package/core/events/RecurrenceEngineV2.js +536 -562
- package/core/ics/ICSHandler.js +348 -348
- package/core/ics/ICSParser.js +433 -435
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +363 -398
- package/core/performance/AdaptiveMemoryManager.js +310 -308
- package/core/performance/LRUCache.js +3 -4
- package/core/performance/PerformanceOptimizer.js +4 -6
- package/core/search/EventSearch.js +409 -417
- package/core/search/SearchWorkerManager.js +338 -338
- package/core/state/StateManager.js +4 -2
- package/core/timezone/TimezoneDatabase.js +574 -271
- package/core/timezone/TimezoneManager.js +422 -402
- package/core/types.js +1 -1
- package/package.json +1 -1
|
@@ -357,11 +357,7 @@ export class Calendar {
|
|
|
357
357
|
* @returns {string} Formatted date string
|
|
358
358
|
*/
|
|
359
359
|
formatInTimezone(date, timezone = null, options = {}) {
|
|
360
|
-
return this.timezoneManager.formatInTimezone(
|
|
361
|
-
date,
|
|
362
|
-
timezone || this.config.timeZone,
|
|
363
|
-
options
|
|
364
|
-
);
|
|
360
|
+
return this.timezoneManager.formatInTimezone(date, timezone || this.config.timeZone, options);
|
|
365
361
|
}
|
|
366
362
|
|
|
367
363
|
/**
|
|
@@ -437,7 +433,9 @@ export class Calendar {
|
|
|
437
433
|
let currentDate = new Date(startDate);
|
|
438
434
|
|
|
439
435
|
// Generate weeks
|
|
440
|
-
const maxWeeks = fixedWeekCount
|
|
436
|
+
const maxWeeks = fixedWeekCount
|
|
437
|
+
? 6
|
|
438
|
+
: Math.ceil((lastDay.getDate() + DateUtils.getDayOfWeek(firstDay, weekStartsOn)) / 7);
|
|
441
439
|
|
|
442
440
|
for (let weekIndex = 0; weekIndex < maxWeeks; weekIndex++) {
|
|
443
441
|
const week = {
|
|
@@ -502,7 +500,7 @@ export class Calendar {
|
|
|
502
500
|
events: this.getEventsForDate(dayDate),
|
|
503
501
|
// Add overlap groups for positioning overlapping events
|
|
504
502
|
overlapGroups: this.eventStore.getOverlapGroups(dayDate, true),
|
|
505
|
-
getEventPositions:
|
|
503
|
+
getEventPositions: events => this.eventStore.calculateEventPositions(events)
|
|
506
504
|
});
|
|
507
505
|
// Move to next day
|
|
508
506
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
@@ -706,7 +704,7 @@ export class Calendar {
|
|
|
706
704
|
});
|
|
707
705
|
|
|
708
706
|
// Listen to event store changes
|
|
709
|
-
this.eventStore.subscribe(
|
|
707
|
+
this.eventStore.subscribe(change => {
|
|
710
708
|
this._emit('eventStoreChange', change);
|
|
711
709
|
});
|
|
712
710
|
}
|
|
@@ -752,4 +750,4 @@ export class Calendar {
|
|
|
752
750
|
|
|
753
751
|
this._emit('destroy');
|
|
754
752
|
}
|
|
755
|
-
}// Test workflow
|
|
753
|
+
} // Test workflow
|
|
@@ -209,9 +209,11 @@ export class DateUtils {
|
|
|
209
209
|
* @returns {boolean}
|
|
210
210
|
*/
|
|
211
211
|
static isSameDay(date1, date2) {
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
return (
|
|
213
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
214
|
+
date1.getMonth() === date2.getMonth() &&
|
|
215
|
+
date1.getDate() === date2.getDate()
|
|
216
|
+
);
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
/**
|
|
@@ -234,8 +236,7 @@ export class DateUtils {
|
|
|
234
236
|
* @returns {boolean}
|
|
235
237
|
*/
|
|
236
238
|
static isSameMonth(date1, date2) {
|
|
237
|
-
return date1.getFullYear() === date2.getFullYear() &&
|
|
238
|
-
date1.getMonth() === date2.getMonth();
|
|
239
|
+
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth();
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
/**
|
|
@@ -389,7 +390,7 @@ export class DateUtils {
|
|
|
389
390
|
* @returns {boolean}
|
|
390
391
|
*/
|
|
391
392
|
static isLeapYear(year) {
|
|
392
|
-
return (year % 4 === 0 && year % 100 !== 0) ||
|
|
393
|
+
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
393
394
|
}
|
|
394
395
|
|
|
395
396
|
/**
|
|
@@ -504,7 +505,7 @@ export class DateUtils {
|
|
|
504
505
|
const originalOffset = DateUtils.getTimezoneOffset(date, timeZone);
|
|
505
506
|
|
|
506
507
|
// Add hours
|
|
507
|
-
result.setTime(result.getTime() +
|
|
508
|
+
result.setTime(result.getTime() + hours * 60 * 60 * 1000);
|
|
508
509
|
|
|
509
510
|
// Check if DST transition occurred
|
|
510
511
|
const newOffset = DateUtils.getTimezoneOffset(result, timeZone);
|
|
@@ -550,8 +551,8 @@ export class DateUtils {
|
|
|
550
551
|
|
|
551
552
|
// Get offset and adjust
|
|
552
553
|
const offset = DateUtils.getTimezoneOffset(localDate, timeZone);
|
|
553
|
-
const utcTime = localDate.getTime() +
|
|
554
|
+
const utcTime = localDate.getTime() + offset * 60000;
|
|
554
555
|
|
|
555
556
|
return new Date(utcTime);
|
|
556
557
|
}
|
|
557
|
-
}
|
|
558
|
+
}
|
|
@@ -47,7 +47,8 @@ export class ConflictDetector {
|
|
|
47
47
|
const searchStart = new Date(event.start.getTime() - opts.bufferMinutes * 60000);
|
|
48
48
|
const searchEnd = new Date(event.end.getTime() + opts.bufferMinutes * 60000);
|
|
49
49
|
|
|
50
|
-
const potentialConflicts = this.eventStore
|
|
50
|
+
const potentialConflicts = this.eventStore
|
|
51
|
+
.getEventsInRange(searchStart, searchEnd, false)
|
|
51
52
|
.filter(e => {
|
|
52
53
|
// Exclude self
|
|
53
54
|
if (e.id === event.id) return false;
|
|
@@ -64,11 +65,7 @@ export class ConflictDetector {
|
|
|
64
65
|
|
|
65
66
|
// Check each potential conflict
|
|
66
67
|
for (const conflictingEvent of potentialConflicts) {
|
|
67
|
-
const eventConflicts = this._detectEventConflicts(
|
|
68
|
-
event,
|
|
69
|
-
conflictingEvent,
|
|
70
|
-
opts
|
|
71
|
-
);
|
|
68
|
+
const eventConflicts = this._detectEventConflicts(event, conflictingEvent, opts);
|
|
72
69
|
|
|
73
70
|
if (eventConflicts.length > 0) {
|
|
74
71
|
conflicts.push(...eventConflicts);
|
|
@@ -131,8 +128,8 @@ export class ConflictDetector {
|
|
|
131
128
|
if (!opts.includeStatuses.includes(event.status)) return false;
|
|
132
129
|
if (event.status === 'cancelled') return false;
|
|
133
130
|
|
|
134
|
-
return
|
|
135
|
-
attendeeEmails.includes(attendee.email)
|
|
131
|
+
return (
|
|
132
|
+
event.attendees && event.attendees.some(attendee => attendeeEmails.includes(attendee.email))
|
|
136
133
|
);
|
|
137
134
|
});
|
|
138
135
|
|
|
@@ -173,9 +170,10 @@ export class ConflictDetector {
|
|
|
173
170
|
const freePeriods = [];
|
|
174
171
|
|
|
175
172
|
// Get busy periods
|
|
176
|
-
const busyPeriods =
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
const busyPeriods =
|
|
174
|
+
opts.attendeeEmails.length > 0
|
|
175
|
+
? this.getBusyPeriods(opts.attendeeEmails, start, end)
|
|
176
|
+
: this._getAllBusyPeriods(start, end);
|
|
179
177
|
|
|
180
178
|
// Find gaps between busy periods
|
|
181
179
|
let currentTime = new Date(start);
|
|
@@ -186,7 +184,10 @@ export class ConflictDetector {
|
|
|
186
184
|
const gapDuration = (busy.start - currentTime) / 60000; // minutes
|
|
187
185
|
if (gapDuration >= duration) {
|
|
188
186
|
// Check if within business hours if required
|
|
189
|
-
if (
|
|
187
|
+
if (
|
|
188
|
+
!opts.businessHoursOnly ||
|
|
189
|
+
this._isWithinBusinessHours(currentTime, busy.start, opts)
|
|
190
|
+
) {
|
|
190
191
|
freePeriods.push({
|
|
191
192
|
start: new Date(currentTime),
|
|
192
193
|
end: new Date(busy.start)
|
|
@@ -221,11 +222,7 @@ export class ConflictDetector {
|
|
|
221
222
|
const conflicts = [];
|
|
222
223
|
|
|
223
224
|
// Check time overlap with buffer
|
|
224
|
-
const hasTimeOverlap = this._checkTimeOverlap(
|
|
225
|
-
event1,
|
|
226
|
-
event2,
|
|
227
|
-
options.bufferMinutes
|
|
228
|
-
);
|
|
225
|
+
const hasTimeOverlap = this._checkTimeOverlap(event1, event2, options.bufferMinutes);
|
|
229
226
|
|
|
230
227
|
if (hasTimeOverlap) {
|
|
231
228
|
// Time conflict
|
|
@@ -490,14 +487,17 @@ export class ConflictDetector {
|
|
|
490
487
|
* @private
|
|
491
488
|
*/
|
|
492
489
|
_getAllBusyPeriods(start, end) {
|
|
493
|
-
const events = this.eventStore
|
|
490
|
+
const events = this.eventStore
|
|
491
|
+
.getEventsInRange(start, end, false)
|
|
494
492
|
.filter(e => e.status !== 'cancelled');
|
|
495
493
|
|
|
496
|
-
return events
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
494
|
+
return events
|
|
495
|
+
.map(event => ({
|
|
496
|
+
start: event.start,
|
|
497
|
+
end: event.end,
|
|
498
|
+
eventIds: [event.id]
|
|
499
|
+
}))
|
|
500
|
+
.sort((a, b) => a.start - b.start);
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
/**
|
|
@@ -514,4 +514,4 @@ export class ConflictDetector {
|
|
|
514
514
|
|
|
515
515
|
return startHour >= businessStart && endHour <= businessEnd;
|
|
516
516
|
}
|
|
517
|
-
}
|
|
517
|
+
}
|
package/core/events/Event.js
CHANGED
|
@@ -179,12 +179,12 @@ export class Event {
|
|
|
179
179
|
organizer = null,
|
|
180
180
|
attendees = [],
|
|
181
181
|
reminders = [],
|
|
182
|
-
category,
|
|
183
|
-
categories,
|
|
182
|
+
category, // Support singular category (no default)
|
|
183
|
+
categories, // Support plural categories (no default)
|
|
184
184
|
attachments = [],
|
|
185
185
|
conferenceData = null,
|
|
186
186
|
metadata = {},
|
|
187
|
-
...rest
|
|
187
|
+
...rest // Capture any extra properties
|
|
188
188
|
}) {
|
|
189
189
|
// Normalize and validate input
|
|
190
190
|
const normalized = Event.normalize({
|
|
@@ -208,12 +208,12 @@ export class Event {
|
|
|
208
208
|
organizer,
|
|
209
209
|
attendees,
|
|
210
210
|
reminders,
|
|
211
|
-
category,
|
|
212
|
-
categories,
|
|
211
|
+
category, // Pass category to normalize
|
|
212
|
+
categories, // Pass categories to normalize
|
|
213
213
|
attachments,
|
|
214
214
|
conferenceData,
|
|
215
215
|
metadata,
|
|
216
|
-
...rest
|
|
216
|
+
...rest // Pass any extra properties
|
|
217
217
|
});
|
|
218
218
|
|
|
219
219
|
// Validate normalized data
|
|
@@ -417,10 +417,9 @@ export class Event {
|
|
|
417
417
|
dayEnd.setHours(23, 59, 59, 999);
|
|
418
418
|
|
|
419
419
|
return this.start <= dayEnd && this.end >= dayStart;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
420
|
+
}
|
|
421
|
+
// Single day event: check if it's on the same day
|
|
422
|
+
return startString === dateString;
|
|
424
423
|
}
|
|
425
424
|
|
|
426
425
|
/**
|
|
@@ -436,9 +435,8 @@ export class Event {
|
|
|
436
435
|
} else if (otherEvent && otherEvent.start && otherEvent.end) {
|
|
437
436
|
// Allow checking against time ranges
|
|
438
437
|
return !(this.end <= otherEvent.start || this.start >= otherEvent.end);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
438
|
+
}
|
|
439
|
+
throw new Error('Parameter must be an Event instance or have start/end properties');
|
|
442
440
|
}
|
|
443
441
|
|
|
444
442
|
/**
|
|
@@ -586,9 +584,7 @@ export class Event {
|
|
|
586
584
|
* @returns {boolean} True if attendee was removed
|
|
587
585
|
*/
|
|
588
586
|
removeAttendee(emailOrId) {
|
|
589
|
-
const index = this.attendees.findIndex(
|
|
590
|
-
a => a.email === emailOrId || a.id === emailOrId
|
|
591
|
-
);
|
|
587
|
+
const index = this.attendees.findIndex(a => a.email === emailOrId || a.id === emailOrId);
|
|
592
588
|
|
|
593
589
|
if (index !== -1) {
|
|
594
590
|
this.attendees.splice(index, 1);
|
|
@@ -747,9 +743,7 @@ export class Event {
|
|
|
747
743
|
*/
|
|
748
744
|
removeCategory(category) {
|
|
749
745
|
const normalizedCategory = category.trim().toLowerCase();
|
|
750
|
-
const index = this.categories.findIndex(
|
|
751
|
-
c => c.toLowerCase() === normalizedCategory
|
|
752
|
-
);
|
|
746
|
+
const index = this.categories.findIndex(c => c.toLowerCase() === normalizedCategory);
|
|
753
747
|
|
|
754
748
|
if (index !== -1) {
|
|
755
749
|
this.categories.splice(index, 1);
|
|
@@ -917,4 +911,4 @@ export class Event {
|
|
|
917
911
|
get isVirtual() {
|
|
918
912
|
return this.conferenceData !== null;
|
|
919
913
|
}
|
|
920
|
-
}
|
|
914
|
+
}
|
|
@@ -46,6 +46,8 @@ export class EventStore {
|
|
|
46
46
|
this.isBatchMode = false;
|
|
47
47
|
this.batchNotifications = [];
|
|
48
48
|
this.batchBackup = null; // For rollback support
|
|
49
|
+
this._batchLock = null; // Lock to prevent concurrent batch operations
|
|
50
|
+
this._batchLockResolve = null; // Resolver for the batch lock
|
|
49
51
|
|
|
50
52
|
// Change tracking
|
|
51
53
|
/** @type {number} */
|
|
@@ -251,13 +253,15 @@ export class EventStore {
|
|
|
251
253
|
|
|
252
254
|
// Filter by having attendees
|
|
253
255
|
if (Object.prototype.hasOwnProperty.call(filters, 'hasAttendees')) {
|
|
254
|
-
results = results.filter(event =>
|
|
256
|
+
results = results.filter(event =>
|
|
257
|
+
filters.hasAttendees ? event.hasAttendees : !event.hasAttendees
|
|
258
|
+
);
|
|
255
259
|
}
|
|
256
260
|
|
|
257
261
|
// Filter by organizer email
|
|
258
262
|
if (filters.organizerEmail) {
|
|
259
|
-
results = results.filter(
|
|
260
|
-
event.organizer && event.organizer.email === filters.organizerEmail
|
|
263
|
+
results = results.filter(
|
|
264
|
+
event => event.organizer && event.organizer.email === filters.organizerEmail
|
|
261
265
|
);
|
|
262
266
|
}
|
|
263
267
|
|
|
@@ -456,7 +460,7 @@ export class EventStore {
|
|
|
456
460
|
events.sort((a, b) => {
|
|
457
461
|
const startDiff = a.start - b.start;
|
|
458
462
|
if (startDiff !== 0) return startDiff;
|
|
459
|
-
return
|
|
463
|
+
return b.end - b.start - (a.end - a.start);
|
|
460
464
|
});
|
|
461
465
|
|
|
462
466
|
// Track which columns are occupied at each time
|
|
@@ -717,8 +721,10 @@ export class EventStore {
|
|
|
717
721
|
// Index first week
|
|
718
722
|
const firstWeekEnd = new Date(startDate);
|
|
719
723
|
firstWeekEnd.setDate(firstWeekEnd.getDate() + 7);
|
|
720
|
-
const firstWeekDates = DateUtils.getDateRange(
|
|
721
|
-
|
|
724
|
+
const firstWeekDates = DateUtils.getDateRange(
|
|
725
|
+
startDate,
|
|
726
|
+
firstWeekEnd < endDate ? firstWeekEnd : endDate
|
|
727
|
+
);
|
|
722
728
|
|
|
723
729
|
firstWeekDates.forEach(date => {
|
|
724
730
|
const dateStr = DateUtils.getLocalDateString(date);
|
|
@@ -800,6 +806,22 @@ export class EventStore {
|
|
|
800
806
|
}
|
|
801
807
|
}
|
|
802
808
|
|
|
809
|
+
// Remove from category indices
|
|
810
|
+
for (const [category, eventIds] of this.indices.byCategory) {
|
|
811
|
+
eventIds.delete(event.id);
|
|
812
|
+
if (eventIds.size === 0) {
|
|
813
|
+
this.indices.byCategory.delete(category);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Remove from status indices
|
|
818
|
+
for (const [status, eventIds] of this.indices.byStatus) {
|
|
819
|
+
eventIds.delete(event.id);
|
|
820
|
+
if (eventIds.size === 0) {
|
|
821
|
+
this.indices.byStatus.delete(status);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
803
825
|
// Remove from recurring index
|
|
804
826
|
this.indices.recurring.delete(event.id);
|
|
805
827
|
}
|
|
@@ -851,11 +873,19 @@ export class EventStore {
|
|
|
851
873
|
this.batchBackup = {
|
|
852
874
|
events: new Map(this.events),
|
|
853
875
|
indices: {
|
|
854
|
-
byDate: new Map(
|
|
855
|
-
|
|
876
|
+
byDate: new Map(
|
|
877
|
+
Array.from(this.indices.byDate.entries()).map(([k, v]) => [k, new Set(v)])
|
|
878
|
+
),
|
|
879
|
+
byMonth: new Map(
|
|
880
|
+
Array.from(this.indices.byMonth.entries()).map(([k, v]) => [k, new Set(v)])
|
|
881
|
+
),
|
|
856
882
|
recurring: new Set(this.indices.recurring),
|
|
857
|
-
byCategory: new Map(
|
|
858
|
-
|
|
883
|
+
byCategory: new Map(
|
|
884
|
+
Array.from(this.indices.byCategory.entries()).map(([k, v]) => [k, new Set(v)])
|
|
885
|
+
),
|
|
886
|
+
byStatus: new Map(
|
|
887
|
+
Array.from(this.indices.byStatus.entries()).map(([k, v]) => [k, new Set(v)])
|
|
888
|
+
)
|
|
859
889
|
},
|
|
860
890
|
version: this.version
|
|
861
891
|
};
|
|
@@ -912,23 +942,44 @@ export class EventStore {
|
|
|
912
942
|
|
|
913
943
|
/**
|
|
914
944
|
* Execute batch operation with automatic rollback on error
|
|
945
|
+
* Uses a lock to prevent concurrent batch operations from corrupting state
|
|
915
946
|
* @param {Function} operation - Operation to execute
|
|
916
947
|
* @param {boolean} [enableRollback=true] - Enable automatic rollback on error
|
|
917
948
|
* @returns {*} Result of operation
|
|
918
949
|
* @throws {Error} If operation fails
|
|
919
950
|
*/
|
|
920
951
|
async executeBatch(operation, enableRollback = true) {
|
|
921
|
-
|
|
952
|
+
// Wait for any existing batch operation to complete
|
|
953
|
+
while (this._batchLock) {
|
|
954
|
+
await this._batchLock;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Acquire the lock
|
|
958
|
+
this._batchLock = new Promise(resolve => {
|
|
959
|
+
this._batchLockResolve = resolve;
|
|
960
|
+
});
|
|
922
961
|
|
|
923
962
|
try {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
963
|
+
this.startBatch(enableRollback);
|
|
964
|
+
|
|
965
|
+
try {
|
|
966
|
+
const result = await operation();
|
|
967
|
+
this.commitBatch();
|
|
968
|
+
return result;
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (enableRollback) {
|
|
971
|
+
this.rollbackBatch();
|
|
972
|
+
}
|
|
973
|
+
throw error;
|
|
974
|
+
}
|
|
975
|
+
} finally {
|
|
976
|
+
// Release the lock
|
|
977
|
+
const resolve = this._batchLockResolve;
|
|
978
|
+
this._batchLock = null;
|
|
979
|
+
this._batchLockResolve = null;
|
|
980
|
+
if (resolve) {
|
|
981
|
+
resolve();
|
|
930
982
|
}
|
|
931
|
-
throw error;
|
|
932
983
|
}
|
|
933
984
|
}
|
|
934
985
|
|
|
@@ -1209,4 +1260,4 @@ export class EventStore {
|
|
|
1209
1260
|
|
|
1210
1261
|
return eventsWithConflicts;
|
|
1211
1262
|
}
|
|
1212
|
-
}
|
|
1263
|
+
}
|