Skip to content

Commit 7f59a1f

Browse files
committed
Revert "perf: (googlecalendar): batch freebusy calls by delegation credential (#24331)"
This reverts commit 253c33d.
1 parent 5322d96 commit 7f59a1f

File tree

8 files changed

+87
-1699
lines changed

8 files changed

+87
-1699
lines changed

packages/app-store/_utils/__tests__/getCalendar.test.ts

Lines changed: 0 additions & 408 deletions
This file was deleted.

packages/app-store/_utils/getCalendar.ts

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { CalendarBatchService } from "@calcom/features/calendar-batch/lib/CalendarBatchService";
2-
import { CalendarBatchWrapper } from "@calcom/features/calendar-batch/lib/CalendarBatchWrapper";
31
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
42
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
53
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
@@ -44,20 +42,6 @@ export const getCalendar = async (
4442
log.warn(`calendar of type ${calendarType} is not implemented`);
4543
return null;
4644
}
47-
48-
// eslint-disable-next-line
49-
const originalCalendar = new CalendarService(credential as any);
50-
return resolveCalendarServeStrategy(originalCalendar, credential, shouldServeCache);
51-
};
52-
53-
/**
54-
* Resolve best calendar strategy for current calendar and credential
55-
*/
56-
const resolveCalendarServeStrategy = async (
57-
originalCalendar: Calendar,
58-
credential: CredentialForCalendarService,
59-
shouldServeCache?: boolean
60-
): Promise<Calendar> => {
6145
// if shouldServeCache is not supplied, determine on the fly.
6246
if (typeof shouldServeCache === "undefined") {
6347
const featuresRepository = new FeaturesRepository(prisma);
@@ -74,26 +58,20 @@ const resolveCalendarServeStrategy = async (
7458
);
7559
shouldServeCache = isCalendarSubscriptionCacheEnabled && isCalendarSubscriptionCacheEnabledForUser;
7660
}
77-
if (CalendarCacheEventService.isCalendarTypeSupported(credential.type) && shouldServeCache) {
78-
log.info("Calendar Cache is enabled, using CalendarCacheService for credential", {
79-
credentialId: credential.id,
80-
});
81-
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
82-
return new CalendarCacheWrapper({
83-
originalCalendar: originalCalendar as unknown as Calendar,
84-
calendarCacheEventRepository,
85-
});
86-
} else if (CalendarBatchService.isSupported(credential)) {
87-
// If calendar cache isn't supported, we try calendar batch as the second layer of optimization
88-
log.info("Calendar Batch is supported, using CalendarBatchService for credential", {
89-
credentialId: credential.id,
90-
});
91-
return new CalendarBatchWrapper({ originalCalendar: originalCalendar as unknown as Calendar });
61+
if (CalendarCacheEventService.isCalendarTypeSupported(calendarType) && shouldServeCache) {
62+
log.info(`Calendar Cache is enabled, using CalendarCacheService for credential ${credential.id}`);
63+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
64+
const originalCalendar = new CalendarService(credential as any);
65+
if (originalCalendar) {
66+
// return cacheable calendar
67+
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
68+
return new CalendarCacheWrapper({
69+
originalCalendar,
70+
calendarCacheEventRepository,
71+
});
72+
}
9273
}
9374

94-
// Ended up returning unoptimized original calendar
95-
log.info("Calendar Cache and Batch aren't supported, serving regular calendar for credential", {
96-
credentialId: credential.id,
97-
});
98-
return originalCalendar;
75+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
76+
return new CalendarService(credential as any);
9977
};

packages/app-store/googlecalendar/lib/CalendarService.ts

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
import type { calendar_v3 } from "@googleapis/calendar";
33
import type { GaxiosResponse } from "googleapis-common";
44
import { RRule } from "rrule";
5+
import { v4 as uuid } from "uuid";
56

67
import { MeetLocationType } from "@calcom/app-store/constants";
78
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
9+
import { uniqueBy } from "@calcom/lib/array";
810
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
911
import logger from "@calcom/lib/logger";
1012
import { safeStringify } from "@calcom/lib/safeStringify";
@@ -32,6 +34,12 @@ interface GoogleCalError extends Error {
3234
code?: number;
3335
}
3436

37+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
38+
const ONE_MONTH_IN_MS = 30 * MS_PER_DAY;
39+
40+
const GOOGLE_WEBHOOK_URL_BASE = process.env.GOOGLE_WEBHOOK_URL || process.env.NEXT_PUBLIC_WEBAPP_URL;
41+
const GOOGLE_WEBHOOK_URL = `${GOOGLE_WEBHOOK_URL_BASE}/api/integrations/googlecalendar/webhook`;
42+
3543
const isGaxiosResponse = (error: unknown): error is GaxiosResponse<calendar_v3.Schema$Event> =>
3644
typeof error === "object" && !!error && Object.prototype.hasOwnProperty.call(error, "config");
3745

@@ -112,6 +120,50 @@ export default class GoogleCalendarService implements Calendar {
112120
return attendees;
113121
};
114122

123+
private async stopWatchingCalendarsInGoogle(
124+
channels: { googleChannelResourceId: string | null; googleChannelId: string | null }[]
125+
) {
126+
const calendar = await this.authedCalendar();
127+
logger.debug(`Unsubscribing from calendars ${channels.map((c) => c.googleChannelId).join(", ")}`);
128+
const uniqueChannels = uniqueBy(channels, ["googleChannelId", "googleChannelResourceId"]);
129+
await Promise.allSettled(
130+
uniqueChannels.map(({ googleChannelResourceId, googleChannelId }) =>
131+
calendar.channels
132+
.stop({
133+
requestBody: {
134+
resourceId: googleChannelResourceId,
135+
id: googleChannelId,
136+
},
137+
})
138+
.catch((err) => {
139+
console.warn(JSON.stringify(err));
140+
})
141+
)
142+
);
143+
}
144+
145+
private async startWatchingCalendarsInGoogle({ calendarId }: { calendarId: string }) {
146+
const calendar = await this.authedCalendar();
147+
logger.debug(`Subscribing to calendar ${calendarId}`, safeStringify({ GOOGLE_WEBHOOK_URL }));
148+
149+
const res = await calendar.events.watch({
150+
// 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.
151+
calendarId,
152+
requestBody: {
153+
// A UUID or similar unique string that identifies this channel.
154+
id: uuid(),
155+
type: "web_hook",
156+
address: GOOGLE_WEBHOOK_URL,
157+
token: process.env.GOOGLE_WEBHOOK_TOKEN,
158+
params: {
159+
// The time-to-live in seconds for the notification channel. Default is 604800 seconds.
160+
ttl: `${Math.round(ONE_MONTH_IN_MS / 1000)}`,
161+
},
162+
},
163+
});
164+
return res.data;
165+
}
166+
115167
async createEvent(
116168
calEvent: CalendarServiceEvent,
117169
credentialId: number,
@@ -400,7 +452,9 @@ export default class GoogleCalendarService implements Calendar {
400452
return apiResponse.json;
401453
}
402454

403-
async getFreeBusyResult(args: FreeBusyArgs): Promise<calendar_v3.Schema$FreeBusyResponse> {
455+
async getFreeBusyResult(
456+
args: FreeBusyArgs,
457+
): Promise<calendar_v3.Schema$FreeBusyResponse> {
404458
return await this.fetchAvailability(args);
405459
}
406460

@@ -417,7 +471,9 @@ export default class GoogleCalendarService implements Calendar {
417471
return validCals[0];
418472
}
419473

420-
async getFreeBusyData(args: FreeBusyArgs): Promise<(EventBusyDate & { id: string })[] | null> {
474+
async getFreeBusyData(
475+
args: FreeBusyArgs,
476+
): Promise<(EventBusyDate & { id: string })[] | null> {
421477
const freeBusyResult = await this.getFreeBusyResult(args);
422478
if (!freeBusyResult.calendars) return null;
423479

@@ -548,12 +604,11 @@ export default class GoogleCalendarService implements Calendar {
548604

549605
/**
550606
* Fetches availability data using the cache-or-fetch pattern
551-
*
552607
*/
553608
private async fetchAvailabilityData(
554609
calendarIds: string[],
555610
dateFrom: string,
556-
dateTo: string
611+
dateTo: string,
557612
): Promise<EventBusyDate[]> {
558613
// More efficient date difference calculation using native Date objects
559614
// Use Math.floor to match dayjs diff behavior (truncates, doesn't round up)
@@ -564,11 +619,13 @@ export default class GoogleCalendarService implements Calendar {
564619

565620
// Google API only allows a date range of 90 days for /freebusy
566621
if (diff <= 90) {
567-
const freeBusyData = await this.getFreeBusyData({
568-
timeMin: dateFrom,
569-
timeMax: dateTo,
570-
items: calendarIds.map((id) => ({ id })),
571-
});
622+
const freeBusyData = await this.getFreeBusyData(
623+
{
624+
timeMin: dateFrom,
625+
timeMax: dateTo,
626+
items: calendarIds.map((id) => ({ id })),
627+
}
628+
);
572629

573630
if (!freeBusyData) throw new Error("No response from google calendar");
574631
return freeBusyData.map((freeBusy) => ({ start: freeBusy.start, end: freeBusy.end }));
@@ -590,11 +647,13 @@ export default class GoogleCalendarService implements Calendar {
590647
currentEndTime = originalEndTime;
591648
}
592649

593-
const chunkData = await this.getFreeBusyData({
594-
timeMin: new Date(currentStartTime).toISOString(),
595-
timeMax: new Date(currentEndTime).toISOString(),
596-
items: calendarIds.map((id) => ({ id })),
597-
});
650+
const chunkData = await this.getFreeBusyData(
651+
{
652+
timeMin: new Date(currentStartTime).toISOString(),
653+
timeMax: new Date(currentEndTime).toISOString(),
654+
items: calendarIds.map((id) => ({ id })),
655+
}
656+
);
598657

599658
if (chunkData) {
600659
busyData.push(...chunkData.map((freeBusy) => ({ start: freeBusy.start, end: freeBusy.end })));

0 commit comments

Comments
 (0)