import type { ICalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository.interface"; import logger from "@calcom/lib/logger"; import { withSpan } from "@calcom/lib/sentryWrapper"; import type { Calendar, CalendarEvent, CalendarServiceEvent, EventBusyDate, GetAvailabilityParams, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; const log = logger.getSubLogger({ prefix: ["CachedCalendarWrapper"] }); /** * A wrapper to load cache from database and cache it. * * @see Calendar */ export class CalendarCacheWrapper implements Calendar { constructor( private deps: { originalCalendar: Calendar; calendarCacheEventRepository: ICalendarCacheEventRepository; } ) {} getCredentialId?(): number { return this.deps.originalCalendar.getCredentialId ? this.deps.originalCalendar.getCredentialId() : -1; } createEvent( event: CalendarServiceEvent, credentialId: number, externalCalendarId?: string ): Promise { return this.deps.originalCalendar.createEvent(event, credentialId, externalCalendarId); } updateEvent( uid: string, event: CalendarServiceEvent, externalCalendarId?: string | null ): Promise { return this.deps.originalCalendar.updateEvent(uid, event, externalCalendarId); } deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise { return this.deps.originalCalendar.deleteEvent(uid, event, externalCalendarId); } /** * Retrieves availability combining cache and live sources. * * - 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. */ async getAvailability(params: GetAvailabilityParams): Promise { const { dateFrom, dateTo, selectedCalendars } = params; return withSpan( { name: "CalendarCacheWrapper.getAvailability", op: "calendar.cache.internal.getAvailability", attributes: { calendarCount: selectedCalendars?.length ?? 0, }, }, async (span) => { log.debug("getAvailability (mixed cache + original)", { dateFrom, dateTo, calendarIds: selectedCalendars.map((c) => c.id), calendarCount: selectedCalendars.length, }); if (!selectedCalendars?.length) return []; const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt); const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt); const results: EventBusyDate[] = []; // Fetch from cache for synced calendars if (withSync.length) { const cacheStartTime = performance.now(); const ids = withSync.map((c) => c.id).filter((id): id is string => Boolean(id)); const cached = await this.deps.calendarCacheEventRepository.findAllBySelectedCalendarIdsBetween( ids, new Date(dateFrom), new Date(dateTo) ); const cacheDurationMs = performance.now() - cacheStartTime; results.push( ...cached.map((event) => ({ ...event, timeZone: event.timeZone ?? undefined, })) ); span.setAttribute("cachedCalendarCount", withSync.length); span.setAttribute("cacheFetchDurationMs", cacheDurationMs); span.setAttribute("cachedEventsCount", cached.length); log.info("Calendar cache fetch completed", { cachedCalendarCount: withSync.length, cacheFetchDurationMs: cacheDurationMs, cachedEventsCount: cached.length, }); } // Fetch from original calendar for unsynced ones if (withoutSync.length) { const originalStartTime = performance.now(); 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); span.setAttribute("originalCalendarCount", withoutSync.length); span.setAttribute("originalFetchDurationMs", originalDurationMs); span.setAttribute("originalEventsCount", original.length); log.info("Original calendar fetch completed", { originalCalendarCount: withoutSync.length, originalFetchDurationMs: originalDurationMs, originalEventsCount: original.length, }); } span.setAttribute("cacheUsed", withSync.length > 0); span.setAttribute("totalEventsCount", results.length); return results; } ); } /** * Retrieves availability with time zones, combining cache and live data. * * - 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. */ async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise { const { dateFrom, dateTo, selectedCalendars } = params; return withSpan( { name: "CalendarCacheWrapper.getAvailabilityWithTimeZones", op: "calendar.cache.internal.getAvailabilityWithTimeZones", attributes: { calendarCount: selectedCalendars?.length ?? 0, }, }, async (span) => { log.debug("getAvailabilityWithTimeZones (mixed cache + original)", { dateFrom, dateTo, calendarIds: selectedCalendars.map((c) => c.id), calendarCount: selectedCalendars.length, }); if (!selectedCalendars?.length) return []; const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt); const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt); const results: EventBusyDate[] = []; // Fetch from cache for synced calendars if (withSync.length) { const cacheStartTime = performance.now(); const ids = withSync.map((c) => c.id).filter((id): id is string => Boolean(id)); const cached = await this.deps.calendarCacheEventRepository.findAllBySelectedCalendarIdsBetween( ids, new Date(dateFrom), new Date(dateTo) ); const cacheDurationMs = performance.now() - cacheStartTime; results.push( ...cached.map(({ start, end, timeZone }) => ({ start, end, timeZone: timeZone || "UTC", })) ); span.setAttribute("cachedCalendarCount", withSync.length); span.setAttribute("cacheFetchDurationMs", cacheDurationMs); span.setAttribute("cachedEventsCount", cached.length); log.info("Calendar cache fetch with timezones completed", { cachedCalendarCount: withSync.length, cacheFetchDurationMs: cacheDurationMs, cachedEventsCount: cached.length, }); } // Fetch from original calendar for unsynced ones if (withoutSync.length) { const originalStartTime = performance.now(); const original = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.({ dateFrom, dateTo, selectedCalendars: withoutSync, mode: params.mode, fallbackToPrimary: params.fallbackToPrimary, }); const originalDurationMs = performance.now() - originalStartTime; if (original?.length) results.push(...original); span.setAttribute("originalCalendarCount", withoutSync.length); span.setAttribute("originalFetchDurationMs", originalDurationMs); span.setAttribute("originalEventsCount", original?.length ?? 0); log.info("Original calendar fetch with timezones completed", { originalCalendarCount: withoutSync.length, originalFetchDurationMs: originalDurationMs, originalEventsCount: original?.length ?? 0, }); } span.setAttribute("cacheUsed", withSync.length > 0); span.setAttribute("totalEventsCount", results.length); return results; } ); } fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise { return this.deps.originalCalendar.fetchAvailabilityAndSetCache?.(selectedCalendars) || Promise.resolve(); } listCalendars(event?: CalendarEvent): Promise { return this.deps.originalCalendar.listCalendars(event); } testDelegationCredentialSetup?(): Promise { return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false); } }