refactor: replace shouldServeCache with mode parameter for calendar cache control (#26539)

* refactor: replace shouldServeCache with mode parameter for calendar cache control

Replace the boolean shouldServeCache parameter with a new CalendarFetchMode type
that can have values 'slots', 'overlay', and 'booking'. This provides better
control over when to serve cache vs relay on calendar providers.

- 'slots' mode: Check feature flags and use cache when available (for getting
  actual calendar availability)
- 'overlay' mode: Don't use cache (for overlay calendar availability)
- 'booking' mode: Don't use cache (for booking confirmation)
- undefined: Same as 'slots' for backwards compatibility

The cache decision logic is now centralized in getCalendar.ts based on the mode
parameter.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: update CalendarService.test.ts to use mode parameter

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: use shared GetAvailabilityParams type across all calendar services

- Import GetAvailabilityParams and GetAvailabilityWithTimeZonesParams from @calcom/types/Calendar
- Replace inline type definitions with shared types in all calendar service implementations
- Update BaseCalendarService, CalendarCacheWrapper, and CalendarTelemetryWrapper
- Ensures consistent typing and follows DRY principles

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: add missing IntegrationCalendar import and update mock getAvailability signature

- Add IntegrationCalendar back to sendgrid CalendarService imports
- Update bookingScenario mock getAvailability to use typed params object
- Add listCalendars method to mock Calendar object

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: update test files to use typed params object for getAvailability methods

- Update CalendarCacheWrapper.test.ts to call getAvailability/getAvailabilityWithTimeZones with params object
- Update getCalendarsEvents.test.ts toHaveBeenCalledWith assertions to expect params object
- All 32 tests now pass

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: consolidate to single GetAvailabilityParams type for both getAvailability methods

- Remove GetAvailabilityWithTimeZonesParams, use GetAvailabilityParams for both methods
- Add mode parameter to getAvailabilityWithTimeZones calls
- Update wrapper classes to pass mode through to underlying calendar
- Update test files to include mode in getAvailabilityWithTimeZones calls and assertions

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: use EventBusyDate with optional timeZone for getAvailabilityWithTimeZones

- Add optional timeZone field to EventBusyDate type
- Update getAvailabilityWithTimeZones return type to use EventBusyDate[]
- Update Google Calendar service and wrapper classes to use the new type

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: type errors and remove unused calendar watching methods

- Fix type errors in CalendarCacheWrapper.ts (convert null to undefined for timeZone)
- Fix type errors in getCalendarsEvents.ts (ensure timeZone is always present)
- Remove unused watchCalendar/unwatchCalendar from Calendar interface
- Remove unused startWatchingCalendarsInGoogle/stopWatchingCalendarsInGoogle from GoogleCalendarService
- Remove unused imports (uuid, uniqueBy, GOOGLE_WEBHOOK_URL, ONE_MONTH_IN_MS)

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: remove watchCalendar/unwatchCalendar from CalendarTelemetryWrapper

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: remove verbose JSDoc param comments from wrapper classes

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: make mode parameter required in getCalendar

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: add required mode parameter to all getCalendar callers

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: provide default mode value in getBusyCalendarTimes

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: add mode parameter to remaining getCalendar callers

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: add mode parameter to vital and wipemycalother reschedule

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: add mode parameter to credential-sync API endpoint

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor: add 'none' mode to CalendarFetchMode and use as default

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* test: add mode parameter to getCalendarsEvents test calls

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Update packages/app-store/delegationCredential.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Volnei Munhoz
2026-01-08 09:07:41 -03:00
committed by GitHub
co-authored by Volnei Munhoz cubic-dev-ai[bot] Devin AI
parent 64828abad2
commit a62eb2bdac
37 changed files with 356 additions and 369 deletions
@@ -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];
+18 -4
View File
@@ -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<Calendar | null> => {
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);
@@ -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<EventBusyDate[]> {
async getAvailability(_params: GetAvailabilityParams): Promise<EventBusyDate[]> {
return Promise.resolve([]);
}
+1 -1
View File
@@ -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");
@@ -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<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
try {
const externalCalendars: IntegrationCalendar[] = await this.listCalendars();
const calendarsToGetAppointmentsFrom = [];
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
try {
const externalCalendars = await this.listCalendars();
const calendarsToGetAppointmentsFrom = [];
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
const calendars: IntegrationCalendar[] = await this.listCalendars();
const promises: Promise<EventBusyDate[]>[] = calendars
.filter((lcal) => selectedCalendars.some((rcal) => lcal.externalId == rcal.externalId))
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
const selectedCalendarIds = selectedCalendars
.filter((e) => e.integration === this.integrationName)
.map((e) => e.externalId)
@@ -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<calendar_v3.Schema$Event> =>
const isGaxiosResponse= (error: unknown): error is GaxiosResponse<calendar_v3.Schema$Event> =>
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<EventBusyDate[]> {
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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars, fallbackToPrimary } = params;
this.log.debug("Getting availability", safeStringify({ dateFrom, dateTo, selectedCalendars }));
const selectedCalendarIds = selectedCalendars
@@ -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]);
});
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
const startISOString = new Date(dateFrom).toISOString();
const calendars = await this.fetchCalendars();
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
const selectedCalendarIds = selectedCalendars
.filter((e) => e.integration === this.integrationName)
.map((e) => e.externalId)
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
const dateFromParsed = new Date(dateFrom);
const dateToParsed = new Date(dateTo);
@@ -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<EventBusyDate[]> {
async getAvailability(_params: GetAvailabilityParams): Promise<EventBusyDate[]> {
return Promise.resolve([]);
}
+1 -1
View File
@@ -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);
@@ -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);
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
const selectedCalendarIds = selectedCalendars
.filter((e) => e.integration === this.integrationName)
.map((e) => e.externalId);
@@ -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);
@@ -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);
}
}
@@ -687,7 +687,7 @@ export class LuckyUserService implements ILuckyUserService {
getIntervalStartDate({ interval, rrTimestampBasis, meetingStartTime }).toISOString(),
getIntervalEndDate({ interval, rrTimestampBasis, meetingStartTime }).toISOString(),
user.userLevelSelectedCalendars,
true,
"slots",
true
)
)
@@ -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<unknown>,
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: {
@@ -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)
@@ -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)
@@ -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
);
}
}
@@ -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) {
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
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<EventBusyDate[]> {
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<boolean> {
return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false);
}
watchCalendar?(options: {
calendarId: string;
eventTypeIds: SelectedCalendarEventTypeIds;
}): Promise<unknown> {
return this.deps.originalCalendar.watchCalendar?.(options) || Promise.resolve();
}
unwatchCalendar?(options: {
calendarId: string;
eventTypeIds: SelectedCalendarEventTypeIds;
}): Promise<void> {
return this.deps.originalCalendar.unwatchCalendar?.(options) || Promise.resolve();
}
}
@@ -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([]);
});
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
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<EventBusyDate[]> {
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<boolean> {
return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false);
}
watchCalendar?(options: {
calendarId: string;
eventTypeIds: SelectedCalendarEventTypeIds;
}): Promise<unknown> {
return this.deps.originalCalendar.watchCalendar?.(options) || Promise.resolve();
}
unwatchCalendar?(options: {
calendarId: string;
eventTypeIds: SelectedCalendarEventTypeIds;
}): Promise<void> {
return this.deps.originalCalendar.unwatchCalendar?.(options) || Promise.resolve();
}
}
@@ -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<CredentialForCalendarS
.filter((app) => 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<unknown> => {
const calendar = await getCalendar(credential);
const calendar = await getCalendar(credential, "booking");
log.debug(
"Deleting calendar event",
safeStringify({
@@ -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([[]]);
});
});
@@ -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<EventBusyDate[][]> => {
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]'`,
@@ -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();
+3 -5
View File
@@ -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<EventBusyDate[]> {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
const startISOString = new Date(dateFrom).toISOString();
const objects = await this.fetchObjectsWithOptionalExpand({
@@ -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<EventBusyDate[]> => {
getAvailability: async (params: {
dateFrom: string;
dateTo: string;
selectedCalendars: IntegrationCalendar[];
mode: "slots" | "overlay" | "booking";
fallbackToPrimary?: boolean;
}): Promise<EventBusyDate[]> => {
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<IntegrationCalendar[]> {
return Promise.resolve([]);
},
} as Calendar;
}
);
@@ -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) {
@@ -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);
}
+23 -22
View File
@@ -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<Partial<_SelectedCalendar>,
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<unknown>;
getAvailability(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[],
shouldServeCache?: boolean,
fallbackToPrimary?: boolean
): Promise<EventBusyDate[]>;
getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]>;
// 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<EventBusyDate[]>;
fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise<unknown>;
listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]>;
testDelegationCredentialSetup?(): Promise<boolean>;
watchCalendar?(options: {
calendarId: string;
eventTypeIds: SelectedCalendarEventTypeIds;
}): Promise<unknown>;
unwatchCalendar?(options: {
calendarId: string;
eventTypeIds: SelectedCalendarEventTypeIds;
}): Promise<void>;
}
/**