@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.
@@ -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 ? 6 : Math.ceil((lastDay.getDate() + DateUtils.getDayOfWeek(firstDay, weekStartsOn)) / 7);
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: (events) => this.eventStore.calculateEventPositions(events)
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((change) => {
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 date1.getFullYear() === date2.getFullYear() &&
213
- date1.getMonth() === date2.getMonth() &&
214
- date1.getDate() === date2.getDate();
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) || (year % 400 === 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() + (hours * 60 * 60 * 1000));
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() + (offset * 60000);
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.getEventsInRange(searchStart, searchEnd, false)
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 event.attendees && event.attendees.some(attendee =>
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 = opts.attendeeEmails.length > 0
177
- ? this.getBusyPeriods(opts.attendeeEmails, start, end)
178
- : this._getAllBusyPeriods(start, end);
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 (!opts.businessHoursOnly || this._isWithinBusinessHours(currentTime, busy.start, opts)) {
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.getEventsInRange(start, end, false)
490
+ const events = this.eventStore
491
+ .getEventsInRange(start, end, false)
494
492
  .filter(e => e.status !== 'cancelled');
495
493
 
496
- return events.map(event => ({
497
- start: event.start,
498
- end: event.end,
499
- eventIds: [event.id]
500
- })).sort((a, b) => a.start - b.start);
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
+ }
@@ -179,12 +179,12 @@ export class Event {
179
179
  organizer = null,
180
180
  attendees = [],
181
181
  reminders = [],
182
- category, // Support singular category (no default)
183
- categories, // Support plural categories (no default)
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 // Capture any extra properties
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, // Pass category to normalize
212
- categories, // Pass categories to normalize
211
+ category, // Pass category to normalize
212
+ categories, // Pass categories to normalize
213
213
  attachments,
214
214
  conferenceData,
215
215
  metadata,
216
- ...rest // Pass any extra properties
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
- // Single day event: check if it's on the same day
422
- return startString === dateString;
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
- throw new Error('Parameter must be an Event instance or have start/end properties');
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 => filters.hasAttendees ? event.hasAttendees : !event.hasAttendees);
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(event =>
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 (b.end - b.start) - (a.end - a.start);
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(startDate,
721
- firstWeekEnd < endDate ? firstWeekEnd : endDate);
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(Array.from(this.indices.byDate.entries()).map(([k, v]) => [k, new Set(v)])),
855
- byMonth: new Map(Array.from(this.indices.byMonth.entries()).map(([k, v]) => [k, new Set(v)])),
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(Array.from(this.indices.byCategory.entries()).map(([k, v]) => [k, new Set(v)])),
858
- byStatus: new Map(Array.from(this.indices.byStatus.entries()).map(([k, v]) => [k, new Set(v)]))
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
- this.startBatch(enableRollback);
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
- const result = await operation();
925
- this.commitBatch();
926
- return result;
927
- } catch (error) {
928
- if (enableRollback) {
929
- this.rollbackBatch();
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
+ }