diff --git a/apps/api/v1/pages/api/credential-sync/_post.ts b/apps/api/v1/pages/api/credential-sync/_post.ts index bdf1451266..2e755dbe56 100644 --- a/apps/api/v1/pages/api/credential-sync/_post.ts +++ b/apps/api/v1/pages/api/credential-sync/_post.ts @@ -109,7 +109,7 @@ async function handler(req: NextApiRequest) { // ^ Workaround for the select in `create` not working if (createCalendarResources) { - const calendar = await getCalendar({ ...credential, delegatedTo: null }); + const calendar = await getCalendar({ ...credential, delegatedTo: null }, "slots"); if (!calendar) throw new HttpError({ message: "Calendar missing for credential", statusCode: 500 }); const calendars = await calendar.listCalendars(); const calendarToCreate = calendars.find((calendar) => calendar.primary) || calendars[0]; diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 8d2f68e5d4..32b2880fbc 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -7,7 +7,7 @@ import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import logger from "@calcom/lib/logger"; import { isTelemetryEnabled } from "@calcom/lib/sentryWrapper"; import { prisma } from "@calcom/prisma"; -import type { Calendar } from "@calcom/types/Calendar"; +import type { Calendar, CalendarFetchMode } from "@calcom/types/Calendar"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; import { CalendarServiceMap } from "../calendar.services.generated"; @@ -16,7 +16,7 @@ const log = logger.getSubLogger({ prefix: ["CalendarManager"] }); export const getCalendar = async ( credential: CredentialForCalendarService | null, - shouldServeCache?: boolean + mode: CalendarFetchMode = "none" ): Promise => { if (!credential || !credential.key) return null; let { type: calendarType } = credential; @@ -44,8 +44,14 @@ export const getCalendar = async ( log.warn(`calendar of type ${calendarType} is not implemented`); return null; } - // if shouldServeCache is not supplied, determine on the fly. - if (typeof shouldServeCache === "undefined") { + + // Determine if we should use cache based on mode: + // - "slots": Check feature flags and use cache when available (for getting actual calendar availability) + // - "overlay": Don't use cache (for overlay calendar availability) + // - "booking": Don't use cache (for booking confirmation) + // - "none": Don't use cache (for operations that don't use getAvailability, e.g., deleteEvent, listCalendars) + let shouldServeCache = false; + if (mode === "slots") { const featuresRepository = new FeaturesRepository(prisma); const [isCalendarSubscriptionCacheEnabled, isCalendarSubscriptionCacheEnabledForUser] = await Promise.all( [ @@ -62,11 +68,19 @@ export const getCalendar = async ( log.debug("Cache feature flag check", { credentialId: credential.id, userId: credential.userId, + mode, isCalendarSubscriptionCacheEnabled, isCalendarSubscriptionCacheEnabledForUser, shouldServeCache, }); + } else { + log.debug("Cache disabled for mode", { + credentialId: credential.id, + userId: credential.userId, + mode, + }); } + const isCacheSupported = CalendarCacheEventService.isCalendarTypeSupported(calendarType); const originalCalendar = new CalendarService(credential as any); diff --git a/packages/app-store/basecamp3/lib/CalendarService.ts b/packages/app-store/basecamp3/lib/CalendarService.ts index e251ae33db..8879a1645f 100644 --- a/packages/app-store/basecamp3/lib/CalendarService.ts +++ b/packages/app-store/basecamp3/lib/CalendarService.ts @@ -4,6 +4,7 @@ import type { Calendar, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; @@ -247,11 +248,7 @@ export default class BasecampCalendarService implements Calendar { return true; }; - async getAvailability( - _dateFrom: string, - _dateTo: string, - _selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(_params: GetAvailabilityParams): Promise { return Promise.resolve([]); } diff --git a/packages/app-store/delegationCredential.ts b/packages/app-store/delegationCredential.ts index 1ef722e6a7..bc80352c75 100644 --- a/packages/app-store/delegationCredential.ts +++ b/packages/app-store/delegationCredential.ts @@ -321,7 +321,7 @@ export async function checkIfSuccessfullyConfiguredInWorkspace({ user, }); - const calendar = await getCalendar(credential); + const calendar = await getCalendar(credential, "none"); if (!calendar) { throw new Error("Google Calendar App not found"); diff --git a/packages/app-store/exchange2013calendar/lib/CalendarService.ts b/packages/app-store/exchange2013calendar/lib/CalendarService.ts index 13f17e2821..3fce67dc57 100644 --- a/packages/app-store/exchange2013calendar/lib/CalendarService.ts +++ b/packages/app-store/exchange2013calendar/lib/CalendarService.ts @@ -28,6 +28,7 @@ import type { Calendar, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; @@ -142,7 +143,8 @@ export default class ExchangeCalendarService implements Calendar { } } - async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; try { const externalCalendars: IntegrationCalendar[] = await this.listCalendars(); const calendarsToGetAppointmentsFrom = []; diff --git a/packages/app-store/exchange2016calendar/lib/CalendarService.ts b/packages/app-store/exchange2016calendar/lib/CalendarService.ts index 0f97cadc39..a29c766429 100644 --- a/packages/app-store/exchange2016calendar/lib/CalendarService.ts +++ b/packages/app-store/exchange2016calendar/lib/CalendarService.ts @@ -28,6 +28,7 @@ import type { Calendar, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; @@ -143,11 +144,8 @@ export default class ExchangeCalendarService implements Calendar { } } - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; try { const externalCalendars = await this.listCalendars(); const calendarsToGetAppointmentsFrom = []; diff --git a/packages/app-store/exchangecalendar/lib/CalendarService.ts b/packages/app-store/exchangecalendar/lib/CalendarService.ts index b781761e2e..69d9074b72 100644 --- a/packages/app-store/exchangecalendar/lib/CalendarService.ts +++ b/packages/app-store/exchangecalendar/lib/CalendarService.ts @@ -32,6 +32,7 @@ import type { Calendar, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, Person, @@ -133,11 +134,8 @@ export default class ExchangeCalendarService implements Calendar { }); } - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; const calendars: IntegrationCalendar[] = await this.listCalendars(); const promises: Promise[] = calendars .filter((lcal) => selectedCalendars.some((rcal) => lcal.externalId == rcal.externalId)) diff --git a/packages/app-store/feishucalendar/lib/CalendarService.ts b/packages/app-store/feishucalendar/lib/CalendarService.ts index 7ddc601c60..cab60bbe01 100644 --- a/packages/app-store/feishucalendar/lib/CalendarService.ts +++ b/packages/app-store/feishucalendar/lib/CalendarService.ts @@ -7,6 +7,7 @@ import type { CalendarServiceEvent, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; @@ -270,11 +271,8 @@ export default class FeishuCalendarService implements Calendar { } } - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; const selectedCalendarIds = selectedCalendars .filter((e) => e.integration === this.integrationName) .map((e) => e.externalId) diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 41ae0080c3..49a41a5a04 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -2,11 +2,8 @@ import type { calendar_v3 } from "@googleapis/calendar"; import type { GaxiosResponse } from "googleapis-common"; import { RRule } from "rrule"; -import { v4 as uuid } from "uuid"; - import { MeetLocationType } from "@calcom/app-store/constants"; import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; -import { uniqueBy } from "@calcom/lib/array"; import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -17,10 +14,11 @@ import type { CalendarServiceEvent, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, + SelectedCalendarEventTypeIds, } from "@calcom/types/Calendar"; -import type { SelectedCalendarEventTypeIds } from "@calcom/types/Calendar"; import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential"; import { AxiosLikeResponseToFetchResponse } from "../../_utils/oauth/AxiosLikeResponseToFetchResponse"; @@ -34,13 +32,7 @@ interface GoogleCalError extends Error { code?: number; } -const MS_PER_DAY = 24 * 60 * 60 * 1000; -const ONE_MONTH_IN_MS = 30 * MS_PER_DAY; - -const GOOGLE_WEBHOOK_URL_BASE = process.env.GOOGLE_WEBHOOK_URL || process.env.NEXT_PUBLIC_WEBAPP_URL; -const GOOGLE_WEBHOOK_URL = `${GOOGLE_WEBHOOK_URL_BASE}/api/integrations/googlecalendar/webhook`; - -const isGaxiosResponse = (error: unknown): error is GaxiosResponse => +const isGaxiosResponse= (error: unknown): error is GaxiosResponse => typeof error === "object" && !!error && Object.prototype.hasOwnProperty.call(error, "config"); export default class GoogleCalendarService implements Calendar { @@ -120,50 +112,6 @@ export default class GoogleCalendarService implements Calendar { return attendees; }; - private async stopWatchingCalendarsInGoogle( - channels: { googleChannelResourceId: string | null; googleChannelId: string | null }[] - ) { - const calendar = await this.authedCalendar(); - logger.debug(`Unsubscribing from calendars ${channels.map((c) => c.googleChannelId).join(", ")}`); - const uniqueChannels = uniqueBy(channels, ["googleChannelId", "googleChannelResourceId"]); - await Promise.allSettled( - uniqueChannels.map(({ googleChannelResourceId, googleChannelId }) => - calendar.channels - .stop({ - requestBody: { - resourceId: googleChannelResourceId, - id: googleChannelId, - }, - }) - .catch((err) => { - console.warn(JSON.stringify(err)); - }) - ) - ); - } - - private async startWatchingCalendarsInGoogle({ calendarId }: { calendarId: string }) { - const calendar = await this.authedCalendar(); - logger.debug(`Subscribing to calendar ${calendarId}`, safeStringify({ GOOGLE_WEBHOOK_URL })); - - const res = await calendar.events.watch({ - // Calendar identifier. To retrieve calendar IDs call the calendarList.list method. If you want to access the primary calendar of the currently logged in user, use the "primary" keyword. - calendarId, - requestBody: { - // A UUID or similar unique string that identifies this channel. - id: uuid(), - type: "web_hook", - address: GOOGLE_WEBHOOK_URL, - token: process.env.GOOGLE_WEBHOOK_TOKEN, - params: { - // The time-to-live in seconds for the notification channel. Default is 604800 seconds. - ttl: `${Math.round(ONE_MONTH_IN_MS / 1000)}`, - }, - }, - }); - return res.data; - } - async createEvent( calEvent: CalendarServiceEvent, credentialId: number, @@ -491,15 +439,8 @@ export default class GoogleCalendarService implements Calendar { return result; } - async getAvailabilityWithTimeZones( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[], - /** - * If true, we will fallback to the primary calendar if no valid selected calendars are found - */ - fallbackToPrimary?: boolean - ): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]> { + async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars, fallbackToPrimary } = params; const calendar = await this.authedCalendar(); const selectedCalendarIds = selectedCalendars .filter((e) => e.integration === this.integrationName) @@ -665,15 +606,8 @@ export default class GoogleCalendarService implements Calendar { return busyData; } - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[], - /** - * If true, we will fallback to the primary calendar if no valid selected calendars are found - */ - fallbackToPrimary?: boolean - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars, fallbackToPrimary } = params; this.log.debug("Getting availability", safeStringify({ dateFrom, dateTo, selectedCalendars })); const selectedCalendarIds = selectedCalendars diff --git a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts index 318d0ce624..56a6b2b6fa 100644 --- a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts @@ -105,21 +105,23 @@ describe("getAvailability", () => { }; }); - const availabilityWithPrimaryAsFallback = await calendarService.getAvailability( - "2024-01-01", - "2024-01-02", - [], - true - ); + const availabilityWithPrimaryAsFallback = await calendarService.getAvailability({ + dateFrom: "2024-01-01", + dateTo: "2024-01-02", + selectedCalendars: [], + mode: "slots", + fallbackToPrimary: true, + }); expect(availabilityWithPrimaryAsFallback).toEqual(mockedBusyTimes1); - const availabilityWithAllCalendarsAsFallback = await calendarService.getAvailability( - "2024-01-01", - "2024-01-02", - [], - false - ); + const availabilityWithAllCalendarsAsFallback = await calendarService.getAvailability({ + dateFrom: "2024-01-01", + dateTo: "2024-01-02", + selectedCalendars: [], + mode: "slots", + fallbackToPrimary: false, + }); expect(availabilityWithAllCalendarsAsFallback).toEqual([...mockedBusyTimes1, ...mockedBusyTimes2]); }); diff --git a/packages/app-store/ics-feedcalendar/lib/CalendarService.ts b/packages/app-store/ics-feedcalendar/lib/CalendarService.ts index fb10e9206b..b6612c46aa 100644 --- a/packages/app-store/ics-feedcalendar/lib/CalendarService.ts +++ b/packages/app-store/ics-feedcalendar/lib/CalendarService.ts @@ -9,6 +9,7 @@ import type { IntegrationCalendar, EventBusyDate, CalendarEvent, + GetAvailabilityParams, NewCalendarEventType, } from "@calcom/types/Calendar"; import type { CredentialPayload } from "@calcom/types/Credential"; @@ -134,11 +135,8 @@ export default class ICSFeedCalendarService implements Calendar { return selectedCalendars[0].userId || null; }; - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; const startISOString = new Date(dateFrom).toISOString(); const calendars = await this.fetchCalendars(); diff --git a/packages/app-store/larkcalendar/lib/CalendarService.ts b/packages/app-store/larkcalendar/lib/CalendarService.ts index 1ab7cde839..bbccb39394 100644 --- a/packages/app-store/larkcalendar/lib/CalendarService.ts +++ b/packages/app-store/larkcalendar/lib/CalendarService.ts @@ -7,6 +7,7 @@ import type { CalendarServiceEvent, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; @@ -270,11 +271,8 @@ export default class LarkCalendarService implements Calendar { } } - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; const selectedCalendarIds = selectedCalendars .filter((e) => e.integration === this.integrationName) .map((e) => e.externalId) diff --git a/packages/app-store/office365calendar/lib/CalendarService.ts b/packages/app-store/office365calendar/lib/CalendarService.ts index b61095c762..0bff605a46 100644 --- a/packages/app-store/office365calendar/lib/CalendarService.ts +++ b/packages/app-store/office365calendar/lib/CalendarService.ts @@ -17,6 +17,7 @@ import type { Calendar, CalendarServiceEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; @@ -359,11 +360,8 @@ export default class Office365CalendarService implements Calendar { } } - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; const dateFromParsed = new Date(dateFrom); const dateToParsed = new Date(dateTo); diff --git a/packages/app-store/sendgrid/lib/CalendarService.ts b/packages/app-store/sendgrid/lib/CalendarService.ts index fa2480206f..7d2e19d699 100644 --- a/packages/app-store/sendgrid/lib/CalendarService.ts +++ b/packages/app-store/sendgrid/lib/CalendarService.ts @@ -8,6 +8,7 @@ import type { Calendar, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; @@ -87,11 +88,7 @@ export default class CloseComCalendarService implements Calendar { return Promise.resolve(); } - async getAvailability( - _dateFrom: string, - _dateTo: string, - _selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(_params: GetAvailabilityParams): Promise { return Promise.resolve([]); } diff --git a/packages/app-store/vital/lib/reschedule.ts b/packages/app-store/vital/lib/reschedule.ts index 86c7a69f97..5e2c85ef75 100644 --- a/packages/app-store/vital/lib/reschedule.ts +++ b/packages/app-store/vital/lib/reschedule.ts @@ -146,7 +146,7 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => { if (!bookingRef.uid) return; if (bookingRef.type.endsWith("_calendar")) { - const calendar = await getCalendar(credentialsMap.get(bookingRef.type)); + const calendar = await getCalendar(credentialsMap.get(bookingRef.type), "booking"); return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent); } else if (bookingRef.type.endsWith("_video")) { return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid); diff --git a/packages/app-store/wipemycalother/lib/reschedule.ts b/packages/app-store/wipemycalother/lib/reschedule.ts index 9b9fc96eb5..9e826b18cd 100644 --- a/packages/app-store/wipemycalother/lib/reschedule.ts +++ b/packages/app-store/wipemycalother/lib/reschedule.ts @@ -149,7 +149,7 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => { if (!bookingRef.uid) return; if (bookingRef.type.endsWith("_calendar")) { - const calendar = await getCalendar(credentialsMap.get(bookingRef.type)); + const calendar = await getCalendar(credentialsMap.get(bookingRef.type), "booking"); return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent); } else if (bookingRef.type.endsWith("_video")) { return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid); diff --git a/packages/app-store/zohocalendar/lib/CalendarService.ts b/packages/app-store/zohocalendar/lib/CalendarService.ts index cbaca481ef..a51057990d 100644 --- a/packages/app-store/zohocalendar/lib/CalendarService.ts +++ b/packages/app-store/zohocalendar/lib/CalendarService.ts @@ -9,6 +9,7 @@ import type { CalendarServiceEvent, CalendarEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; @@ -302,11 +303,8 @@ export default class ZohoCalendarService implements Calendar { } } - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; const selectedCalendarIds = selectedCalendars .filter((e) => e.integration === this.integrationName) .map((e) => e.externalId); diff --git a/packages/features/availability/lib/getUserAvailability.ts b/packages/features/availability/lib/getUserAvailability.ts index 2ad3acd403..caba2c2ad4 100644 --- a/packages/features/availability/lib/getUserAvailability.ts +++ b/packages/features/availability/lib/getUserAvailability.ts @@ -38,7 +38,7 @@ import type { } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; -import type { EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar"; +import type { CalendarFetchMode, EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar"; import type { TimeRange } from "@calcom/types/schedule"; import { findUsersForAvailabilityCheck } from "./findUsersForAvailabilityCheck"; @@ -58,7 +58,7 @@ const availabilitySchema = z returnDateOverrides: z.boolean(), bypassBusyCalendarTimes: z.boolean().optional(), silentlyHandleCalendarFailures: z.boolean().optional(), - shouldServeCache: z.boolean().optional(), + mode: z.enum(["slots", "overlay", "booking"]).optional(), }) .refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in."); @@ -115,7 +115,7 @@ type GetUserAvailabilityQuery = { returnDateOverrides: boolean; bypassBusyCalendarTimes: boolean; silentlyHandleCalendarFailures?: boolean; - shouldServeCache?: boolean; + mode?: CalendarFetchMode; }; export type CurrentSeats = Awaited< @@ -205,7 +205,7 @@ export class UserAvailabilityService { for (const credential of delegatedCredentials) { try { - const calendar = await getCalendar(credential); + const calendar = await getCalendar(credential, "slots"); if (calendar && "getMainTimeZone" in calendar && typeof calendar.getMainTimeZone === "function") { const timezone = await calendar.getMainTimeZone(); if (timezone && timezone !== "UTC") { @@ -304,7 +304,7 @@ export class UserAvailabilityService { returnDateOverrides, bypassBusyCalendarTimes = false, silentlyHandleCalendarFailures = false, - shouldServeCache, + mode, } = availabilitySchema.parse(query); log.debug( @@ -467,7 +467,7 @@ export class UserAvailabilityService { currentBookings: initialData?.currentBookings, bypassBusyCalendarTimes, silentlyHandleCalendarFailures, - shouldServeCache, + mode, }); } catch (error) { log.error(`Error fetching busy times for user ${username}:`, error); diff --git a/packages/features/bookings/lib/EventManager.ts b/packages/features/bookings/lib/EventManager.ts index e047450eb9..1726fb4572 100644 --- a/packages/features/bookings/lib/EventManager.ts +++ b/packages/features/bookings/lib/EventManager.ts @@ -1160,7 +1160,7 @@ export default class EventManager { const calendarCredential = await CredentialRepository.findCredentialForCalendarServiceById({ id: oldCalendarEvent.credentialId, }); - const calendar = await getCalendar(calendarCredential); + const calendar = await getCalendar(calendarCredential, "booking"); await calendar?.deleteEvent(oldCalendarEvent.uid, event, oldCalendarEvent.externalCalendarId); } } diff --git a/packages/features/bookings/lib/getLuckyUser.ts b/packages/features/bookings/lib/getLuckyUser.ts index 4fcdcbc20e..45d91bd9e4 100644 --- a/packages/features/bookings/lib/getLuckyUser.ts +++ b/packages/features/bookings/lib/getLuckyUser.ts @@ -687,7 +687,7 @@ export class LuckyUserService implements ILuckyUserService { getIntervalStartDate({ interval, rrTimestampBasis, meetingStartTime }).toISOString(), getIntervalEndDate({ interval, rrTimestampBasis, meetingStartTime }).toISOString(), user.userLevelSelectedCalendars, - true, + "slots", true ) ) diff --git a/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts b/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts index a667994f4a..47e628479c 100644 --- a/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts +++ b/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts @@ -13,6 +13,7 @@ import { getPiiFreeUser } from "@calcom/lib/piiFreeData"; import { safeStringify } from "@calcom/lib/safeStringify"; import { withReporting } from "@calcom/lib/sentryWrapper"; import prisma from "@calcom/prisma"; +import type { CalendarFetchMode } from "@calcom/types/Calendar"; import type { getEventTypeResponse } from "./getEventTypesFromDB"; import type { BookingType } from "./originalRescheduledBookingUtils"; @@ -59,7 +60,7 @@ const _ensureAvailableUsers = async ( }, input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType }, loggerWithEventDetails: Logger, - shouldServeCache?: boolean + mode?: CalendarFetchMode // ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists ): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> => { const userAvailabilityService = getUserAvailabilityService(); @@ -102,7 +103,7 @@ const _ensureAvailableUsers = async ( beforeEventBuffer: eventType.beforeEventBuffer, afterEventBuffer: eventType.afterEventBuffer, bypassBusyCalendarTimes: false, - shouldServeCache, + mode, withSource: true, }, initialData: { diff --git a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts index 93f1c14c1d..00cb2f50e1 100644 --- a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts +++ b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts @@ -117,7 +117,7 @@ async function cancelAttendeeSeat( integrationsToUpdate.push(updateMeeting(credential, updatedEvt, reference)); } if (reference.type.includes("_calendar")) { - const calendar = await getCalendar(credential); + const calendar = await getCalendar(credential, "booking"); if (calendar) { integrationsToUpdate.push( calendar?.updateEvent(reference.uid, updatedEvt, reference.externalCalendarId) diff --git a/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts b/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts index b7e68415b0..8da45bb2b3 100644 --- a/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts +++ b/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts @@ -43,7 +43,7 @@ const lastAttendeeDeleteBooking = async ( integrationsToDelete.push(deleteMeeting(credential, reference.uid)); } if (reference.type.includes("_calendar") && originalBookingEvt) { - const calendar = await getCalendar(credential); + const calendar = await getCalendar(credential, "booking"); if (calendar) { integrationsToDelete.push( calendar?.deleteEvent(reference.uid, originalBookingEvt, reference.externalCalendarId) diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 79f608adb1..d4d5cac860 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -795,7 +795,8 @@ async function handler( const isTeamEventType = !!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType); - const shouldServeCache = false; + // Use "booking" mode to bypass cache for booking confirmation + const calendarFetchMode = "booking" as const; tracingLogger.info( `Booking eventType ${eventTypeId} started`, @@ -1002,7 +1003,7 @@ async function handler( originalRescheduledBooking: originalRescheduledBooking ?? null, }, tracingLogger, - shouldServeCache + calendarFetchMode ); } } @@ -1017,7 +1018,7 @@ async function handler( originalRescheduledBooking, }, tracingLogger, - shouldServeCache + calendarFetchMode ); } } @@ -1036,7 +1037,7 @@ async function handler( originalRescheduledBooking, }, tracingLogger, - shouldServeCache + calendarFetchMode ); } else { availableUsers = [...qualifiedRRUsers, ...fixedUsers] as IsFixedAwareUser[]; @@ -1065,7 +1066,7 @@ async function handler( originalRescheduledBooking, }, tracingLogger, - shouldServeCache + calendarFetchMode ); } else { availableUsers = [...additionalFallbackRRUsers, ...fixedUsers] as IsFixedAwareUser[]; @@ -1178,7 +1179,7 @@ async function handler( originalRescheduledBooking, }, tracingLogger, - shouldServeCache + calendarFetchMode ); } } diff --git a/packages/features/busyTimes/services/getBusyTimes.ts b/packages/features/busyTimes/services/getBusyTimes.ts index 04e83de9f5..7609bf9834 100644 --- a/packages/features/busyTimes/services/getBusyTimes.ts +++ b/packages/features/busyTimes/services/getBusyTimes.ts @@ -15,7 +15,7 @@ import type { Booking, EventType } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client"; import type { SelectedCalendar } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; -import type { EventBusyDetails } from "@calcom/types/Calendar"; +import type { CalendarFetchMode, EventBusyDetails } from "@calcom/types/Calendar"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; export interface IBusyTimesService { @@ -52,7 +52,7 @@ export class BusyTimesService { | null; bypassBusyCalendarTimes: boolean; silentlyHandleCalendarFailures?: boolean; - shouldServeCache?: boolean; + mode?: CalendarFetchMode; }) { const { credentials, @@ -70,7 +70,7 @@ export class BusyTimesService { duration, bypassBusyCalendarTimes = false, silentlyHandleCalendarFailures = false, - shouldServeCache, + mode, } = params; logger.silly( @@ -194,7 +194,7 @@ export class BusyTimesService { startTime, endTime, selectedCalendars, - shouldServeCache + mode ); if (!calendarBusyTimesQuery.success) { diff --git a/packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts b/packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts index e9991edaf3..8870c91b68 100644 --- a/packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts +++ b/packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts @@ -6,9 +6,9 @@ import type { CalendarEvent, CalendarServiceEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, - SelectedCalendarEventTypeIds, } from "@calcom/types/Calendar"; const log = logger.getSubLogger({ prefix: ["CachedCalendarWrapper"] }); @@ -56,17 +56,9 @@ export class CalendarCacheWrapper implements Calendar { * - Calendars **with** both `syncToken` and `syncSubscribedAt` → fetched from cache. * - Calendars **without** one of them → fetched directly from the original calendar. * - Results are merged into a single array. - * - * @param dateFrom - Start date (ISO string) - * @param dateTo - End date (ISO string) - * @param selectedCalendars - List of calendars to retrieve availability from - * @returns Combined array of busy date ranges */ - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; return withSpan( { name: "CalendarCacheWrapper.getAvailability", @@ -100,7 +92,12 @@ export class CalendarCacheWrapper implements Calendar { new Date(dateTo) ); const cacheDurationMs = performance.now() - cacheStartTime; - results.push(...cached); + results.push( + ...cached.map((event) => ({ + ...event, + timeZone: event.timeZone ?? undefined, + })) + ); span.setAttribute("cachedCalendarCount", withSync.length); span.setAttribute("cacheFetchDurationMs", cacheDurationMs); @@ -115,7 +112,13 @@ export class CalendarCacheWrapper implements Calendar { // Fetch from original calendar for unsynced ones if (withoutSync.length) { const originalStartTime = performance.now(); - const original = await this.deps.originalCalendar.getAvailability(dateFrom, dateTo, withoutSync); + const original = await this.deps.originalCalendar.getAvailability({ + dateFrom, + dateTo, + selectedCalendars: withoutSync, + mode: params.mode, + fallbackToPrimary: params.fallbackToPrimary, + }); const originalDurationMs = performance.now() - originalStartTime; results.push(...original); @@ -143,17 +146,9 @@ export class CalendarCacheWrapper implements Calendar { * - Calendars **with** both `syncToken` and `syncSubscribedAt` → fetched from cache. * - Calendars **without** one of them → fetched directly from the original calendar. * - Results are merged into a single array with `{ start, end, timeZone }` format. - * - * @param dateFrom - Start date (ISO string) - * @param dateTo - End date (ISO string) - * @param selectedCalendars - List of calendars to retrieve availability from - * @returns Combined array of time-zone-aware availability ranges */ - async getAvailabilityWithTimeZones( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]> { + async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; return withSpan( { name: "CalendarCacheWrapper.getAvailabilityWithTimeZones", @@ -175,7 +170,7 @@ export class CalendarCacheWrapper implements Calendar { const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt); const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt); - const results: { start: Date | string; end: Date | string; timeZone: string }[] = []; + const results: EventBusyDate[] = []; // Fetch from cache for synced calendars if (withSync.length) { @@ -208,11 +203,13 @@ export class CalendarCacheWrapper implements Calendar { // Fetch from original calendar for unsynced ones if (withoutSync.length) { const originalStartTime = performance.now(); - const original = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.( + const original = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.({ dateFrom, dateTo, - withoutSync - ); + selectedCalendars: withoutSync, + mode: params.mode, + fallbackToPrimary: params.fallbackToPrimary, + }); const originalDurationMs = performance.now() - originalStartTime; if (original?.length) results.push(...original); @@ -245,18 +242,4 @@ export class CalendarCacheWrapper implements Calendar { testDelegationCredentialSetup?(): Promise { return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false); } - - watchCalendar?(options: { - calendarId: string; - eventTypeIds: SelectedCalendarEventTypeIds; - }): Promise { - return this.deps.originalCalendar.watchCalendar?.(options) || Promise.resolve(); - } - - unwatchCalendar?(options: { - calendarId: string; - eventTypeIds: SelectedCalendarEventTypeIds; - }): Promise { - return this.deps.originalCalendar.unwatchCalendar?.(options) || Promise.resolve(); - } } diff --git a/packages/features/calendar-subscription/lib/cache/__tests__/CalendarCacheWrapper.test.ts b/packages/features/calendar-subscription/lib/cache/__tests__/CalendarCacheWrapper.test.ts index 19e585a928..94ed39f755 100644 --- a/packages/features/calendar-subscription/lib/cache/__tests__/CalendarCacheWrapper.test.ts +++ b/packages/features/calendar-subscription/lib/cache/__tests__/CalendarCacheWrapper.test.ts @@ -41,7 +41,12 @@ describe("CalendarCacheWrapper", () => { describe("getAvailability", () => { it("should return empty array when no calendars provided", async () => { - const result = await wrapper.getAvailability("2025-01-01", "2025-01-02", []); + const result = await wrapper.getAvailability({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars: [], + mode: "slots", + }); expect(result).toEqual([]); expect(mockRepository.findAllBySelectedCalendarIdsBetween).not.toHaveBeenCalled(); @@ -73,7 +78,12 @@ describe("CalendarCacheWrapper", () => { vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents); - const result = await wrapper.getAvailability("2025-01-01", "2025-01-02", selectedCalendars); + const result = await wrapper.getAvailability({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(result).toEqual(cachedEvents); expect(mockRepository.findAllBySelectedCalendarIdsBetween).toHaveBeenCalledWith( @@ -108,15 +118,22 @@ describe("CalendarCacheWrapper", () => { vi.mocked(mockOriginalCalendar.getAvailability).mockResolvedValue(originalEvents); - const result = await wrapper.getAvailability("2025-01-01", "2025-01-02", selectedCalendars); + const result = await wrapper.getAvailability({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(result).toEqual(originalEvents); expect(mockRepository.findAllBySelectedCalendarIdsBetween).not.toHaveBeenCalled(); - expect(mockOriginalCalendar.getAvailability).toHaveBeenCalledWith( - "2025-01-01", - "2025-01-02", - selectedCalendars - ); + expect(mockOriginalCalendar.getAvailability).toHaveBeenCalledWith({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + fallbackToPrimary: undefined, + }); }); it("should fetch from both cache and original calendar when calendars are mixed", async () => { @@ -148,7 +165,12 @@ describe("CalendarCacheWrapper", () => { vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents); vi.mocked(mockOriginalCalendar.getAvailability).mockResolvedValue(originalEvents); - const result = await wrapper.getAvailability("2025-01-01", "2025-01-02", selectedCalendars); + const result = await wrapper.getAvailability({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(result).toEqual([...cachedEvents, ...originalEvents]); expect(mockRepository.findAllBySelectedCalendarIdsBetween).toHaveBeenCalledWith( @@ -156,9 +178,13 @@ describe("CalendarCacheWrapper", () => { new Date("2025-01-01"), new Date("2025-01-02") ); - expect(mockOriginalCalendar.getAvailability).toHaveBeenCalledWith("2025-01-01", "2025-01-02", [ - selectedCalendars[1], - ]); + expect(mockOriginalCalendar.getAvailability).toHaveBeenCalledWith({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars: [selectedCalendars[1]], + mode: "slots", + fallbackToPrimary: undefined, + }); }); it("should filter out calendars without id when fetching from cache", async () => { @@ -179,7 +205,12 @@ describe("CalendarCacheWrapper", () => { }, ]; - await wrapper.getAvailability("2025-01-01", "2025-01-02", selectedCalendars); + await wrapper.getAvailability({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(mockRepository.findAllBySelectedCalendarIdsBetween).toHaveBeenCalledWith( ["cal-1"], // Only cal-1 has a valid id @@ -191,7 +222,12 @@ describe("CalendarCacheWrapper", () => { describe("getAvailabilityWithTimeZones", () => { it("should return empty array when no calendars provided", async () => { - const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", []); + const result = await wrapper.getAvailabilityWithTimeZones({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars: [], + mode: "slots", + }); expect(result).toEqual([]); expect(mockRepository.findAllBySelectedCalendarIdsBetween).not.toHaveBeenCalled(); @@ -224,7 +260,12 @@ describe("CalendarCacheWrapper", () => { vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents); - const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars); + const result = await wrapper.getAvailabilityWithTimeZones({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(result).toEqual([ { @@ -262,15 +303,22 @@ describe("CalendarCacheWrapper", () => { vi.mocked(mockOriginalCalendar.getAvailabilityWithTimeZones).mockResolvedValue(originalEvents); - const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars); + const result = await wrapper.getAvailabilityWithTimeZones({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(result).toEqual(originalEvents); expect(mockRepository.findAllBySelectedCalendarIdsBetween).not.toHaveBeenCalled(); - expect(mockOriginalCalendar.getAvailabilityWithTimeZones).toHaveBeenCalledWith( - "2025-01-01", - "2025-01-02", - selectedCalendars - ); + expect(mockOriginalCalendar.getAvailabilityWithTimeZones).toHaveBeenCalledWith({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + fallbackToPrimary: undefined, + }); }); it("should fetch from both cache and original calendar when calendars are mixed", async () => { @@ -310,7 +358,12 @@ describe("CalendarCacheWrapper", () => { vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents); vi.mocked(mockOriginalCalendar.getAvailabilityWithTimeZones).mockResolvedValue(originalEvents); - const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars); + const result = await wrapper.getAvailabilityWithTimeZones({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(result).toEqual([ { @@ -351,7 +404,12 @@ describe("CalendarCacheWrapper", () => { vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents); mockOriginalCalendar.getAvailabilityWithTimeZones = undefined; - const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars); + const result = await wrapper.getAvailabilityWithTimeZones({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(result).toEqual([ { @@ -375,7 +433,12 @@ describe("CalendarCacheWrapper", () => { vi.mocked(mockOriginalCalendar.getAvailabilityWithTimeZones).mockResolvedValue([]); - const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars); + const result = await wrapper.getAvailabilityWithTimeZones({ + dateFrom: "2025-01-01", + dateTo: "2025-01-02", + selectedCalendars, + mode: "slots", + }); expect(result).toEqual([]); }); diff --git a/packages/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper.ts b/packages/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper.ts index deb6c7b618..5ab76f80d5 100644 --- a/packages/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper.ts +++ b/packages/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper.ts @@ -5,9 +5,9 @@ import type { CalendarEvent, CalendarServiceEvent, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, - SelectedCalendarEventTypeIds, } from "@calcom/types/Calendar"; const log = logger.getSubLogger({ prefix: ["CalendarTelemetryWrapper"] }); @@ -53,19 +53,8 @@ export class CalendarTelemetryWrapper implements Calendar { return this.deps.originalCalendar.deleteEvent(uid, event, externalCalendarId); } - /** - * Retrieves availability with telemetry tracking when cache is disabled. - * - * @param dateFrom - Start date (ISO string) - * @param dateTo - End date (ISO string) - * @param selectedCalendars - List of calendars to retrieve availability from - * @returns Array of busy date ranges - */ - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; return withSpan( { name: "CalendarTelemetryWrapper.getAvailability", @@ -90,7 +79,13 @@ export class CalendarTelemetryWrapper implements Calendar { if (!selectedCalendars?.length) return []; const startTime = performance.now(); - const results = await this.deps.originalCalendar.getAvailability(dateFrom, dateTo, selectedCalendars); + const results = await this.deps.originalCalendar.getAvailability({ + dateFrom, + dateTo, + selectedCalendars, + mode: params.mode, + fallbackToPrimary: params.fallbackToPrimary, + }); const totalFetchDurationMs = performance.now() - startTime; span.setAttribute("totalFetchDurationMs", totalFetchDurationMs); @@ -109,19 +104,8 @@ export class CalendarTelemetryWrapper implements Calendar { ); } - /** - * Retrieves availability with time zones and telemetry tracking when cache is disabled. - * - * @param dateFrom - Start date (ISO string) - * @param dateTo - End date (ISO string) - * @param selectedCalendars - List of calendars to retrieve availability from - * @returns Array of time-zone-aware availability ranges - */ - async getAvailabilityWithTimeZones( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]> { + async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; // Check if the original calendar supports this method if (!this.deps.originalCalendar.getAvailabilityWithTimeZones) { return []; @@ -151,11 +135,13 @@ export class CalendarTelemetryWrapper implements Calendar { if (!selectedCalendars?.length) return []; const startTime = performance.now(); - const results = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.( + const results = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.({ dateFrom, dateTo, - selectedCalendars - ); + selectedCalendars, + mode: params.mode, + fallbackToPrimary: params.fallbackToPrimary, + }); const totalFetchDurationMs = performance.now() - startTime; span.setAttribute("totalFetchDurationMs", totalFetchDurationMs); @@ -185,18 +171,4 @@ export class CalendarTelemetryWrapper implements Calendar { testDelegationCredentialSetup?(): Promise { return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false); } - - watchCalendar?(options: { - calendarId: string; - eventTypeIds: SelectedCalendarEventTypeIds; - }): Promise { - return this.deps.originalCalendar.watchCalendar?.(options) || Promise.resolve(); - } - - unwatchCalendar?(options: { - calendarId: string; - eventTypeIds: SelectedCalendarEventTypeIds; - }): Promise { - return this.deps.originalCalendar.unwatchCalendar?.(options) || Promise.resolve(); - } } diff --git a/packages/features/calendars/lib/CalendarManager.ts b/packages/features/calendars/lib/CalendarManager.ts index b65ab05f23..7a86d0fe3c 100644 --- a/packages/features/calendars/lib/CalendarManager.ts +++ b/packages/features/calendars/lib/CalendarManager.ts @@ -18,6 +18,7 @@ import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFr import { safeStringify } from "@calcom/lib/safeStringify"; import type { CalendarEvent, + CalendarFetchMode, CalendarServiceEvent, EventBusyDate, IntegrationCalendar, @@ -34,7 +35,7 @@ export const getCalendarCredentials = (credentials: Array app.type.endsWith("_calendar")) .flatMap((app) => { const credentials = app.credentials.flatMap((credential) => { - const calendar = () => getCalendar(credential); + const calendar = () => getCalendar(credential, "slots"); return app.variant === "calendar" ? [{ integration: app, credential, calendar }] : []; }); @@ -236,7 +237,7 @@ export const getBusyCalendarTimes = async ( dateFrom: string, dateTo: string, selectedCalendars: SelectedCalendar[], - shouldServeCache?: boolean, + mode?: CalendarFetchMode, includeTimeZone?: boolean ) => { let results: (EventBusyDate & { timeZone?: string })[][] = []; @@ -274,7 +275,7 @@ export const getBusyCalendarTimes = async ( startDate, endDate, selectedCalendars, - shouldServeCache + mode ?? "slots" ); } } catch (e) { @@ -295,7 +296,7 @@ export const createEvent = async ( // Some calendar libraries may edit the original event so let's clone it const formattedEvent = formatCalEvent(originalEvent); const uid: string = getUid(formattedEvent); - const calendar = await getCalendar(credential); + const calendar = await getCalendar(credential, "booking"); let success = true; let calError: string | undefined = undefined; @@ -389,7 +390,7 @@ export const updateEvent = async ( const formattedEvent = formatCalEvent(rawCalEvent); const calEvent = processEvent(formattedEvent); const uid = getUid(calEvent); - const calendar = await getCalendar(credential); + const calendar = await getCalendar(credential, "booking"); let success = false; let calError: string | undefined = undefined; let calWarnings: string[] | undefined = []; @@ -472,7 +473,7 @@ export const deleteEvent = async ({ event: CalendarEvent; externalCalendarId?: string | null; }): Promise => { - const calendar = await getCalendar(credential); + const calendar = await getCalendar(credential, "booking"); log.debug( "Deleting calendar event", safeStringify({ diff --git a/packages/features/calendars/lib/getCalendarsEvents.test.ts b/packages/features/calendars/lib/getCalendarsEvents.test.ts index bb47d25bbe..5d4dffb4e2 100644 --- a/packages/features/calendars/lib/getCalendarsEvents.test.ts +++ b/packages/features/calendars/lib/getCalendarsEvents.test.ts @@ -190,7 +190,8 @@ describe("getCalendarsEvents", () => { ], "2010-12-01", "2010-12-02", - [] + [], + "slots" ); expect(result).toEqual([]); @@ -206,7 +207,8 @@ describe("getCalendarsEvents", () => { ], "2010-12-01", "2010-12-02", - [] + [], + "slots" ); expect(result).toEqual([[]]); @@ -229,7 +231,8 @@ describe("getCalendarsEvents", () => { ], "2010-12-01", "2010-12-02", - [selectedCalendar] + [selectedCalendar], + "slots" ); expect(result).toEqual([[]]); @@ -267,15 +270,17 @@ describe("getCalendarsEvents", () => { ], "2010-12-01", "2010-12-04", - [selectedCalendar] + [selectedCalendar], + "slots" ); - expect(getAvailabilitySpy).toHaveBeenCalledWith( - "2010-12-01", - "2010-12-04", - [selectedCalendar], - false - ); + expect(getAvailabilitySpy).toHaveBeenCalledWith({ + dateFrom: "2010-12-01", + dateTo: "2010-12-04", + selectedCalendars: [selectedCalendar], + mode: "slots", + fallbackToPrimary: false, + }); expect(result).toEqual([ availability.map((av) => ({ ...av, @@ -337,21 +342,24 @@ describe("getCalendarsEvents", () => { ], "2010-12-01", "2010-12-04", - [selectedGoogleCalendar, selectedOfficeCalendar] + [selectedGoogleCalendar, selectedOfficeCalendar], + "slots" ); - expect(getGoogleAvailabilitySpy).toHaveBeenCalledWith( - "2010-12-01", - "2010-12-04", - [selectedGoogleCalendar], - false - ); - expect(getOfficeAvailabilitySpy).toHaveBeenCalledWith( - "2010-12-01", - "2010-12-04", - [selectedOfficeCalendar], - false - ); + expect(getGoogleAvailabilitySpy).toHaveBeenCalledWith({ + dateFrom: "2010-12-01", + dateTo: "2010-12-04", + selectedCalendars: [selectedGoogleCalendar], + mode: "slots", + fallbackToPrimary: false, + }); + expect(getOfficeAvailabilitySpy).toHaveBeenCalledWith({ + dateFrom: "2010-12-01", + dateTo: "2010-12-04", + selectedCalendars: [selectedOfficeCalendar], + mode: "slots", + fallbackToPrimary: false, + }); expect(result).toEqual([ googleAvailability.map((av) => ({ ...av, @@ -373,7 +381,8 @@ describe("getCalendarsEvents", () => { [buildRegularCredential(credential)], "2010-12-01", "2010-12-02", - [] + [], + "slots" ); expect(getAvailabilitySpy).not.toHaveBeenCalled(); @@ -391,9 +400,15 @@ describe("getCalendarsEvents", () => { .spyOn(GoogleCalendarService.prototype, "getAvailability") .mockReturnValue(Promise.resolve([])); - const result = await getCalendarsEvents(credentials, startDate, endDate, []); + const result = await getCalendarsEvents(credentials, startDate, endDate, [], "slots"); - expect(getAvailabilitySpy).toHaveBeenCalledWith(startDate, endDate, [], true); + expect(getAvailabilitySpy).toHaveBeenCalledWith({ + dateFrom: startDate, + dateTo: endDate, + selectedCalendars: [], + mode: "slots", + fallbackToPrimary: true, + }); expect(result).toEqual([[]]); }); }); @@ -523,12 +538,13 @@ describe("getCalendarsEventsWithTimezones", () => { [selectedCalendar] ); - expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith( - "2010-12-01", - "2010-12-04", - [selectedCalendar], - false - ); + expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith({ + dateFrom: "2010-12-01", + dateTo: "2010-12-04", + selectedCalendars: [selectedCalendar], + mode: "slots", + fallbackToPrimary: false, + }); expect(result).toEqual([ availability.map((av) => ({ ...av, @@ -565,7 +581,13 @@ describe("getCalendarsEventsWithTimezones", () => { const result = await getCalendarsEventsWithTimezones(credentials, startDate, endDate, []); - expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith(startDate, endDate, [], true); + expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith({ + dateFrom: startDate, + dateTo: endDate, + selectedCalendars: [], + mode: "slots", + fallbackToPrimary: true, + }); expect(result).toEqual([[]]); }); }); diff --git a/packages/features/calendars/lib/getCalendarsEvents.ts b/packages/features/calendars/lib/getCalendarsEvents.ts index e5634b3a38..76d023ddfe 100644 --- a/packages/features/calendars/lib/getCalendarsEvents.ts +++ b/packages/features/calendars/lib/getCalendarsEvents.ts @@ -5,7 +5,7 @@ import logger from "@calcom/lib/logger"; import { getPiiFreeSelectedCalendar, getPiiFreeCredential } from "@calcom/lib/piiFreeData"; import { safeStringify } from "@calcom/lib/safeStringify"; import { performance } from "@calcom/lib/server/perfObserver"; -import type { EventBusyDate, SelectedCalendar } from "@calcom/types/Calendar"; +import type { CalendarFetchMode, EventBusyDate, SelectedCalendar } from "@calcom/types/Calendar"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; const log = logger.getSubLogger({ prefix: ["getCalendarsEvents"] }); @@ -25,7 +25,7 @@ export const getCalendarsEventsWithTimezones = async ( const calendarAndCredentialPairs = await Promise.all( calendarCredentials.map(async (credential) => { - const calendar = await getCalendar(credential); + const calendar = await getCalendar(credential, "slots"); return [calendar, credential] as const; }) ); @@ -67,14 +67,18 @@ export const getCalendarsEventsWithTimezones = async ( } /** We extract external Ids so we don't cache too much */ const eventBusyDates = - (await c.getAvailabilityWithTimeZones?.( + (await c.getAvailabilityWithTimeZones?.({ dateFrom, dateTo, - passedSelectedCalendars, - allowFallbackToPrimary - )) || []; + selectedCalendars: passedSelectedCalendars, + mode: "slots", + fallbackToPrimary: allowFallbackToPrimary, + })) || []; - return eventBusyDates; + return eventBusyDates.map((event) => ({ + ...event, + timeZone: event.timeZone || "UTC", + })); }); const awaitedResults = await Promise.all(results); return awaitedResults; @@ -85,7 +89,7 @@ const getCalendarsEvents = async ( dateFrom: string, dateTo: string, selectedCalendars: SelectedCalendar[], - shouldServeCache?: boolean + mode: CalendarFetchMode ): Promise => { const calendarCredentials = withCredentials .filter((credential) => credential.type.endsWith("_calendar")) @@ -94,7 +98,7 @@ const getCalendarsEvents = async ( const calendarAndCredentialPairs = await Promise.all( calendarCredentials.map(async (credential) => { - const calendar = await getCalendar(credential, shouldServeCache); + const calendar = await getCalendar(credential, mode); return [calendar, credential] as const; }) ); @@ -147,12 +151,13 @@ const getCalendarsEvents = async ( selectedCalendars: passedSelectedCalendars.map(getPiiFreeSelectedCalendar), }) ); - const eventBusyDates = await calendarService.getAvailability( + const eventBusyDates = await calendarService.getAvailability({ dateFrom, dateTo, - passedSelectedCalendars, - allowFallbackToPrimary - ); + selectedCalendars: passedSelectedCalendars, + mode, + fallbackToPrimary: allowFallbackToPrimary, + }); performance.mark("eventBusyDatesEnd"); performance.measure( `[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`, diff --git a/packages/features/credentials/handleDeleteCredential.ts b/packages/features/credentials/handleDeleteCredential.ts index 56f5030aed..3122113504 100644 --- a/packages/features/credentials/handleDeleteCredential.ts +++ b/packages/features/credentials/handleDeleteCredential.ts @@ -455,7 +455,7 @@ const handleDeleteCredential = async ({ // If it's a calendar remove it from the SelectedCalendars if (credential.app?.categories.includes(AppCategories.calendar)) { try { - const calendar = await getCalendar(buildNonDelegationCredential(credential)); + const calendar = await getCalendar(buildNonDelegationCredential(credential), "slots"); const calendars = await calendar?.listCalendars(); diff --git a/packages/lib/CalendarService.ts b/packages/lib/CalendarService.ts index 52d4bca2df..6fe8bb7849 100644 --- a/packages/lib/CalendarService.ts +++ b/packages/lib/CalendarService.ts @@ -24,6 +24,7 @@ import type { CalendarEvent, CalendarEventType, EventBusyDate, + GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, TeamMember, @@ -358,11 +359,8 @@ export default abstract class BaseCalendarService implements Calendar { return true; }; - async getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[] - ): Promise { + async getAvailability(params: GetAvailabilityParams): Promise { + const { dateFrom, dateTo, selectedCalendars } = params; const startISOString = new Date(dateFrom).toISOString(); const objects = await this.fetchObjectsWithOptionalExpand({ diff --git a/packages/testing/src/lib/bookingScenario/bookingScenario.ts b/packages/testing/src/lib/bookingScenario/bookingScenario.ts index 841b66840a..6365fff90c 100644 --- a/packages/testing/src/lib/bookingScenario/bookingScenario.ts +++ b/packages/testing/src/lib/bookingScenario/bookingScenario.ts @@ -1740,7 +1740,7 @@ type GetAvailabilityMethodMockCall = CalendarServiceMethodMockCallBase & { dateFrom: string; dateTo: string; selectedCalendars: IntegrationCalendar[]; - shouldServeCache?: boolean; + mode: "slots" | "overlay" | "booking"; }; }; @@ -1941,12 +1941,14 @@ export async function mockCalendar( }, }); }, - getAvailability: async ( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[], - shouldServeCache?: boolean - ): Promise => { + getAvailability: async (params: { + dateFrom: string; + dateTo: string; + selectedCalendars: IntegrationCalendar[]; + mode: "slots" | "overlay" | "booking"; + fallbackToPrimary?: boolean; + }): Promise => { + const { dateFrom, dateTo, selectedCalendars, mode } = params; if (calendarData?.getAvailabilityCrash) { throw new Error("MockCalendarService.getAvailability fake error"); } @@ -1955,7 +1957,7 @@ export async function mockCalendar( dateFrom, dateTo, selectedCalendars, - shouldServeCache, + mode, }, calendarServiceConstructorArgs: { credential, @@ -1965,6 +1967,9 @@ export async function mockCalendar( resolve(calendarData?.busySlots || []); }); }, + listCalendars: async function (): Promise { + return Promise.resolve([]); + }, } as Calendar; } ); diff --git a/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts b/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts index bc15baa538..92946131c8 100644 --- a/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts @@ -83,12 +83,14 @@ export const calendarOverlayHandler = async ({ ctx, input }: ListOptions) => { }; }); - // get all clanedar services + // get all calendar services + // Use "overlay" mode to bypass cache for overlay calendar availability const calendarBusyTimesQuery = await getBusyCalendarTimes( credentials, dateFrom, dateTo, - composedSelectedCalendars + composedSelectedCalendars, + "overlay" ); if (!calendarBusyTimesQuery.success) { diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 92bd6b70e1..ac01a40404 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -52,7 +52,7 @@ import { safeStringify } from "@calcom/lib/safeStringify"; import { withReporting } from "@calcom/lib/sentryWrapper"; import type { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse"; import { PeriodType, SchedulingType } from "@calcom/prisma/enums"; -import type { EventBusyDate, EventBusyDetails } from "@calcom/types/Calendar"; +import type { CalendarFetchMode, EventBusyDate, EventBusyDetails } from "@calcom/types/Calendar"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; import { TRPCError } from "@trpc/server"; import type { Logger } from "tslog"; @@ -786,7 +786,7 @@ export class AvailableSlotsService { endTime, bypassBusyCalendarTimes, silentCalendarFailures, - shouldServeCache, + mode, }: { input: TGetScheduleInputSchema; eventType: Exclude< @@ -803,7 +803,7 @@ export class AvailableSlotsService { endTime: Dayjs; bypassBusyCalendarTimes: boolean; silentCalendarFailures: boolean; - shouldServeCache?: boolean; + mode?: CalendarFetchMode; }) { const usersWithCredentials = this.getUsersWithCredentials({ hosts, @@ -940,7 +940,7 @@ export class AvailableSlotsService { returnDateOverrides: false, bypassBusyCalendarTimes, silentlyHandleCalendarFailures: silentCalendarFailures, - shouldServeCache, + mode, }, initialData: { eventType, @@ -1032,7 +1032,8 @@ export class AvailableSlotsService { throw new TRPCError({ code: "NOT_FOUND" }); } - const shouldServeCache = false; + // Use "slots" mode to enable cache when available for getting calendar availability + const mode: CalendarFetchMode = "slots"; if (isEventTypeLoggingEnabled({ eventTypeId: eventType.id })) { logger.settings.minLevel = 2; } @@ -1141,7 +1142,7 @@ export class AvailableSlotsService { : endTime, bypassBusyCalendarTimes, silentCalendarFailures, - shouldServeCache, + mode, }); let aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType); @@ -1168,7 +1169,7 @@ export class AvailableSlotsService { endTime: twoWeeksFromNow, bypassBusyCalendarTimes, silentCalendarFailures, - shouldServeCache, + mode, }); if ( !getAggregatedAvailability( @@ -1204,7 +1205,7 @@ export class AvailableSlotsService { endTime, bypassBusyCalendarTimes, silentCalendarFailures, - shouldServeCache, + mode, })); aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType); } diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index 589dc63073..8d88ad9679 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -57,6 +57,7 @@ export type EventBusyDate = { start: Date | string; end: Date | string; source?: string | null; + timeZone?: string; }; export type EventBusyDetails = EventBusyDate & { @@ -255,6 +256,26 @@ export interface IntegrationCalendar extends Ensure, integrationTitle?: string; } +/** + * Mode for calendar fetch operations to control caching behavior: + * - "slots": For getting actual calendar availability (uses cache when available) + * - "overlay": For getting overlay calendar availability (does not use cache) + * - "booking": For booking confirmation (does not use cache) + * - "none": For operations that don't use getAvailability (e.g., deleteEvent, listCalendars) + */ +export type CalendarFetchMode = "slots" | "overlay" | "booking" | "none"; + +/** + * Parameters for getAvailability and getAvailabilityWithTimeZones methods + */ +export interface GetAvailabilityParams { + dateFrom: string; + dateTo: string; + selectedCalendars: IntegrationCalendar[]; + mode: CalendarFetchMode; + fallbackToPrimary?: boolean; +} + /** * null is to refer to user-level SelectedCalendar */ @@ -280,36 +301,16 @@ export interface Calendar { deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise; - getAvailability( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[], - shouldServeCache?: boolean, - fallbackToPrimary?: boolean - ): Promise; + getAvailability(params: GetAvailabilityParams): Promise; // for OOO calibration (only google calendar for now) - getAvailabilityWithTimeZones?( - dateFrom: string, - dateTo: string, - selectedCalendars: IntegrationCalendar[], - fallbackToPrimary?: boolean - ): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]>; + getAvailabilityWithTimeZones?(params: GetAvailabilityParams): Promise; fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise; listCalendars(event?: CalendarEvent): Promise; testDelegationCredentialSetup?(): Promise; - - watchCalendar?(options: { - calendarId: string; - eventTypeIds: SelectedCalendarEventTypeIds; - }): Promise; - unwatchCalendar?(options: { - calendarId: string; - eventTypeIds: SelectedCalendarEventTypeIds; - }): Promise; } /**