11/* eslint-disable @typescript-eslint/no-explicit-any */
22import type { calendar_v3 } from "@googleapis/calendar" ;
3- import type { Prisma } from "@prisma/client" ;
43import type { GaxiosResponse } from "googleapis-common" ;
54import { RRule } from "rrule" ;
65import { v4 as uuid } from "uuid" ;
76
87import { MeetLocationType } from "@calcom/app-store/locations" ;
9- import dayjs from "@calcom/dayjs" ;
108import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache" ;
119import type { FreeBusyArgs } from "@calcom/features/calendar-cache/calendar-cache.repository.interface" ;
1210import { getTimeMax , getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache" ;
@@ -16,6 +14,7 @@ import logger from "@calcom/lib/logger";
1614import { safeStringify } from "@calcom/lib/safeStringify" ;
1715import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar" ;
1816import prisma from "@calcom/prisma" ;
17+ import type { Prisma } from "@calcom/prisma/client" ;
1918import type {
2019 Calendar ,
2120 CalendarServiceEvent ,
@@ -232,14 +231,13 @@ export default class GoogleCalendarService implements Calendar {
232231 eventId : calEvent . existingRecurringEvent . recurringEventId ,
233232 } ) ;
234233 if ( recurringEventInstances . data . items ) {
235- const calComEventStartTime = dayjs ( calEvent . startTime ) . tz ( calEvent . organizer . timeZone ) . format ( ) ;
234+ // Compare timestamps directly for more reliable and faster matching
235+ const calComEventStartTimeMs = new Date ( calEvent . startTime ) . getTime ( ) ;
236236 for ( let i = 0 ; i < recurringEventInstances . data . items . length ; i ++ ) {
237237 const instance = recurringEventInstances . data . items [ i ] ;
238- const instanceStartTime = dayjs ( instance . start ?. dateTime )
239- . tz ( instance . start ?. timeZone == null ? undefined : instance . start ?. timeZone )
240- . format ( ) ;
238+ const instanceStartTimeMs = new Date ( instance . start ?. dateTime || "" ) . getTime ( ) ;
241239
242- if ( instanceStartTime === calComEventStartTime ) {
240+ if ( instanceStartTimeMs === calComEventStartTimeMs ) {
243241 event = instance ;
244242 break ;
245243 }
@@ -562,10 +560,6 @@ export default class GoogleCalendarService implements Calendar {
562560 try {
563561 const calIdsWithTimeZone = await getCalIdsWithTimeZone ( ) ;
564562 const calIds = calIdsWithTimeZone . map ( ( calIdWithTimeZone ) => ( { id : calIdWithTimeZone . id } ) ) ;
565-
566- const originalStartDate = dayjs ( dateFrom ) ;
567- const originalEndDate = dayjs ( dateTo ) ;
568- const diff = originalEndDate . diff ( originalStartDate , "days" ) ;
569563 const freeBusyData = await this . getCacheOrFetchAvailability ( {
570564 timeMin : dateFrom ,
571565 timeMax : dateTo ,
@@ -593,6 +587,148 @@ export default class GoogleCalendarService implements Calendar {
593587 }
594588 }
595589
590+ /**
591+ * Converts FreeBusy response data to EventBusyDate array
592+ */
593+ private convertFreeBusyToEventBusyDates (
594+ freeBusyResult : calendar_v3 . Schema$FreeBusyResponse
595+ ) : EventBusyDate [ ] {
596+ if ( ! freeBusyResult . calendars ) return [ ] ;
597+
598+ return Object . values ( freeBusyResult . calendars ) . flatMap (
599+ ( calendar ) =>
600+ calendar . busy ?. map ( ( busyTime ) => ( {
601+ start : busyTime . start || "" ,
602+ end : busyTime . end || "" ,
603+ } ) ) || [ ]
604+ ) ;
605+ }
606+
607+ /**
608+ * Attempts to get availability from cache
609+ */
610+ private async tryGetAvailabilityFromCache (
611+ timeMin : string ,
612+ timeMax : string ,
613+ calendarIds : string [ ]
614+ ) : Promise < EventBusyDate [ ] | null > {
615+ try {
616+ const calendarCache = await CalendarCache . init ( null ) ;
617+ const cached = await calendarCache . getCachedAvailability ( {
618+ credentialId : this . credential . id ,
619+ userId : this . credential . userId ,
620+ args : {
621+ // Expand the start date to the start of the month to increase cache hits
622+ timeMin : getTimeMin ( timeMin ) ,
623+ // Expand the end date to the end of the month to increase cache hits
624+ timeMax : getTimeMax ( timeMax ) ,
625+ items : calendarIds . map ( ( id ) => ( { id } ) ) ,
626+ } ,
627+ } ) ;
628+
629+ if ( cached ) {
630+ this . log . debug (
631+ "[Cache Hit] Returning cached availability result" ,
632+ safeStringify ( { timeMin, timeMax, calendarIds } )
633+ ) ;
634+ const freeBusyResult = cached . value as unknown as calendar_v3 . Schema$FreeBusyResponse ;
635+ return this . convertFreeBusyToEventBusyDates ( freeBusyResult ) ;
636+ }
637+
638+ return null ;
639+ } catch ( error ) {
640+ this . log . debug ( "Cache check failed, proceeding with API call" , safeStringify ( error ) ) ;
641+ return null ;
642+ }
643+ }
644+
645+ /**
646+ * Gets calendar IDs for the request, either from selected calendars or fallback logic
647+ */
648+ private async getCalendarIds (
649+ selectedCalendarIds : string [ ] ,
650+ fallbackToPrimary ?: boolean
651+ ) : Promise < string [ ] > {
652+ if ( selectedCalendarIds . length !== 0 ) return selectedCalendarIds ;
653+
654+ const calendar = await this . authedCalendar ( ) ;
655+ const cals = await this . getAllCalendars ( calendar , [ "id" , "primary" ] ) ;
656+ if ( ! cals . length ) return [ ] ;
657+
658+ if ( ! fallbackToPrimary ) {
659+ return this . getValidCalendars ( cals ) . map ( ( cal ) => cal . id ) ;
660+ }
661+
662+ const primaryCalendar = this . filterPrimaryCalendar ( cals ) ;
663+ return primaryCalendar ? [ primaryCalendar . id ] : [ ] ;
664+ }
665+
666+ /**
667+ * Fetches availability data using the cache-or-fetch pattern
668+ */
669+ private async fetchAvailabilityData (
670+ calendarIds : string [ ] ,
671+ dateFrom : string ,
672+ dateTo : string ,
673+ shouldServeCache ?: boolean
674+ ) : Promise < EventBusyDate [ ] > {
675+ // More efficient date difference calculation using native Date objects
676+ // Use Math.floor to match dayjs diff behavior (truncates, doesn't round up)
677+ const fromDate = new Date ( dateFrom ) ;
678+ const toDate = new Date ( dateTo ) ;
679+ const oneDayMs = 1000 * 60 * 60 * 24 ;
680+ const diff = Math . floor ( ( toDate . getTime ( ) - fromDate . getTime ( ) ) / ( oneDayMs ) ) ;
681+
682+ // Google API only allows a date range of 90 days for /freebusy
683+ if ( diff <= 90 ) {
684+ const freeBusyData = await this . getCacheOrFetchAvailability (
685+ {
686+ timeMin : dateFrom ,
687+ timeMax : dateTo ,
688+ items : calendarIds . map ( ( id ) => ( { id } ) ) ,
689+ } ,
690+ shouldServeCache
691+ ) ;
692+
693+ if ( ! freeBusyData ) throw new Error ( "No response from google calendar" ) ;
694+ return freeBusyData . map ( ( freeBusy ) => ( { start : freeBusy . start , end : freeBusy . end } ) ) ;
695+ }
696+
697+ // Handle longer periods by chunking into 90-day periods
698+ const busyData : EventBusyDate [ ] = [ ] ;
699+ const loopsNumber = Math . ceil ( diff / 90 ) ;
700+ let currentStartTime = fromDate . getTime ( ) ;
701+ const originalEndTime = toDate . getTime ( ) ;
702+ const ninetyDaysMs = 90 * 24 * 60 * 60 * 1000 ;
703+ const oneMinuteMs = 60 * 1000 ;
704+
705+ for ( let i = 0 ; i < loopsNumber ; i ++ ) {
706+ let currentEndTime = currentStartTime + ninetyDaysMs ;
707+
708+ // Don't go beyond the original end date
709+ if ( currentEndTime > originalEndTime ) {
710+ currentEndTime = originalEndTime ;
711+ }
712+
713+ const chunkData = await this . getCacheOrFetchAvailability (
714+ {
715+ timeMin : new Date ( currentStartTime ) . toISOString ( ) ,
716+ timeMax : new Date ( currentEndTime ) . toISOString ( ) ,
717+ items : calendarIds . map ( ( id ) => ( { id } ) ) ,
718+ } ,
719+ shouldServeCache
720+ ) ;
721+
722+ if ( chunkData ) {
723+ busyData . push ( ...chunkData . map ( ( freeBusy ) => ( { start : freeBusy . start , end : freeBusy . end } ) ) ) ;
724+ }
725+
726+ currentStartTime = currentEndTime + oneMinuteMs ;
727+ }
728+
729+ return busyData ;
730+ }
731+
596732 async getAvailability (
597733 dateFrom : string ,
598734 dateTo : string ,
@@ -604,71 +740,33 @@ export default class GoogleCalendarService implements Calendar {
604740 fallbackToPrimary ?: boolean
605741 ) : Promise < EventBusyDate [ ] > {
606742 this . log . debug ( "Getting availability" , safeStringify ( { dateFrom, dateTo, selectedCalendars } ) ) ;
607- const calendar = await this . authedCalendar ( ) ;
743+
608744 const selectedCalendarIds = selectedCalendars
609745 . filter ( ( e ) => e . integration === this . integrationName )
610746 . map ( ( e ) => e . externalId ) ;
747+
748+ // Early return if only other integrations are selected
611749 if ( selectedCalendarIds . length === 0 && selectedCalendars . length > 0 ) {
612- // Only calendars of other integrations selected
613750 return [ ] ;
614751 }
615- const getCalIds = async ( ) => {
616- if ( selectedCalendarIds . length !== 0 ) return selectedCalendarIds ;
617- const cals = await this . getAllCalendars ( calendar , [ "id" , "primary" ] ) ;
618- if ( ! cals . length ) return [ ] ;
619- if ( ! fallbackToPrimary ) return this . getValidCalendars ( cals ) . map ( ( cal ) => cal . id ) ;
620-
621- const primaryCalendar = this . filterPrimaryCalendar ( cals ) ;
622- if ( ! primaryCalendar ) return [ ] ;
623- return [ primaryCalendar . id ] ;
624- } ;
625752
626- try {
627- const calsIds = await getCalIds ( ) ;
628- const originalStartDate = dayjs ( dateFrom ) ;
629- const originalEndDate = dayjs ( dateTo ) ;
630- const diff = originalEndDate . diff ( originalStartDate , "days" ) ;
631-
632- // /freebusy from google api only allows a date range of 90 days
633- if ( diff <= 90 ) {
634- const freeBusyData = await this . getCacheOrFetchAvailability (
635- {
636- timeMin : dateFrom ,
637- timeMax : dateTo ,
638- items : calsIds . map ( ( id ) => ( { id } ) ) ,
639- } ,
640- shouldServeCache
641- ) ;
642- if ( ! freeBusyData ) throw new Error ( "No response from google calendar" ) ;
643-
644- return freeBusyData . map ( ( freeBusy ) => ( { start : freeBusy . start , end : freeBusy . end } ) ) ;
645- } else {
646- const busyData = [ ] ;
647-
648- const loopsNumber = Math . ceil ( diff / 90 ) ;
649-
650- let startDate = originalStartDate ;
651- let endDate = originalStartDate . add ( 90 , "days" ) ;
652-
653- for ( let i = 0 ; i < loopsNumber ; i ++ ) {
654- if ( endDate . isAfter ( originalEndDate ) ) endDate = originalEndDate ;
753+ // Try cache first when we have selected calendar IDs
754+ if ( selectedCalendarIds . length > 0 && shouldServeCache !== false ) {
755+ const cachedResult = await this . tryGetAvailabilityFromCache ( dateFrom , dateTo , selectedCalendarIds ) ;
756+ if ( cachedResult ) {
757+ return cachedResult ;
758+ }
759+ }
655760
656- busyData . push (
657- ...( ( await this . getCacheOrFetchAvailability (
658- {
659- timeMin : startDate . format ( ) ,
660- timeMax : endDate . format ( ) ,
661- items : calsIds . map ( ( id ) => ( { id } ) ) ,
662- } ,
663- shouldServeCache
664- ) ) || [ ] )
665- ) ;
761+ // Cache miss - proceed with API calls
762+ this . log . debug (
763+ "[Cache Miss] Proceeding with Google API calls" ,
764+ safeStringify ( { selectedCalendarIds, fallbackToPrimary } )
765+ ) ;
666766
667- startDate = endDate . add ( 1 , "minutes" ) ;
668- endDate = startDate . add ( 90 , "days" ) ;
669- }
670- return busyData . map ( ( freeBusy ) => ( { start : freeBusy . start , end : freeBusy . end } ) ) ;
671- }
767+ try {
768+ const calendarIds = await this . getCalendarIds ( selectedCalendarIds , fallbackToPrimary ) ;
769+ return await this . fetchAvailabilityData ( calendarIds , dateFrom , dateTo , shouldServeCache ) ;
672770 } catch ( error ) {
673771 this . log . error (
674772 "There was an error getting availability from google calendar: " ,
0 commit comments