diff --git a/packages/lib/getBusyTimes.ts b/packages/lib/getBusyTimes.ts index 62107ab264..95050b8204 100644 --- a/packages/lib/getBusyTimes.ts +++ b/packages/lib/getBusyTimes.ts @@ -20,6 +20,72 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential"; import { getDefinedBufferTimes } from "../features/eventtypes/lib/getDefinedBufferTimes"; import { BookingRepository as BookingRepo } from "./server/repository/booking"; +const processBookingsToBusyTimes = ( + bookings: (Pick & { + eventType: Pick | null; + _count?: { + seatsReferences: number; + }; + })[], + rescheduleUid: string | null | undefined, + eventTypeId: number | undefined, + beforeEventBuffer: number | undefined, + afterEventBuffer: number | undefined +) => { + const bookingSeatCountMap: { [x: string]: number } = {}; + return { + busyTimes: bookings.reduce((aggregate: EventBusyDetails[], booking) => { + const { id, startTime, endTime, eventType, title, ...rest } = booking; + + const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0); + const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0); + + if (rest._count?.seatsReferences) { + const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`; + bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0; + bookingSeatCountMap[bookedAt]++; + // Seat references on the current event are non-blocking until the event is fully booked. + if ( + // there are still seats available. + bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) && + // and this is the seated event, other event types should be blocked. + eventTypeId === eventType?.id + ) { + // then we ONLY add the before/after buffer times as busy times. + if (minutesToBlockBeforeEvent) { + aggregate.push({ + start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(), + end: dayjs(startTime).toDate(), // The event starts after the buffer + }); + } + if (minutesToBlockAfterEvent) { + aggregate.push({ + start: dayjs(endTime).toDate(), // The event ends before the buffer + end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(), + }); + } + return aggregate; + } + // if it does get blocked at this point; we remove the bookingSeatCountMap entry + // doing this allows using the map later to remove the ranges from calendar busy times. + delete bookingSeatCountMap[bookedAt]; + } + // rescheduling the same booking to the same time should be possible. Why? + if (rest.uid === rescheduleUid) { + return aggregate; + } + aggregate.push({ + start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(), + end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(), + title, + source: `eventType-${eventType?.id}-booking-${id}`, + }); + return aggregate; + }, []), + bookingSeatCountMap, + }; +}; + const _getBusyTimes = async (params: { credentials: CredentialForCalendarService[]; userId: number; @@ -107,65 +173,45 @@ const _getBusyTimes = async (params: { // to avoid potential side effects. let bookings = params.currentBookings; + const promises = []; + let bookingsPromise; + if (!bookings) { - bookings = await BookingRepo.findAllExistingBookingsForEventTypeBetween({ + bookingsPromise = BookingRepo.findAllExistingBookingsForEventTypeBetween({ userIdAndEmailMap: new Map([[userId, userEmail]]), eventTypeId, startDate: startTimeAdjustedWithMaxBuffer, endDate: endTimeAdjustedWithMaxBuffer, seatedEvent, }); + promises.push(bookingsPromise); } - const bookingSeatCountMap: { [x: string]: number } = {}; - const busyTimes = bookings.reduce((aggregate: EventBusyDetails[], booking) => { - const { id, startTime, endTime, eventType, title, ...rest } = booking; + let calendarBusyTimesPromise; + if (credentials?.length > 0 && !bypassBusyCalendarTimes) { + calendarBusyTimesPromise = getBusyCalendarTimes( + credentials, + startTime, + endTime, + selectedCalendars, + shouldServeCache + ); + promises.push(calendarBusyTimesPromise); + } - const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0); - const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0); + await Promise.all(promises); - if (rest._count?.seatsReferences) { - const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`; - bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0; - bookingSeatCountMap[bookedAt]++; - // Seat references on the current event are non-blocking until the event is fully booked. - if ( - // there are still seats available. - bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) && - // and this is the seated event, other event types should be blocked. - eventTypeId === eventType?.id - ) { - // then we ONLY add the before/after buffer times as busy times. - if (minutesToBlockBeforeEvent) { - aggregate.push({ - start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(), - end: dayjs(startTime).toDate(), // The event starts after the buffer - }); - } - if (minutesToBlockAfterEvent) { - aggregate.push({ - start: dayjs(endTime).toDate(), // The event ends before the buffer - end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(), - }); - } - return aggregate; - } - // if it does get blocked at this point; we remove the bookingSeatCountMap entry - // doing this allows using the map later to remove the ranges from calendar busy times. - delete bookingSeatCountMap[bookedAt]; - } - // rescheduling the same booking to the same time should be possible. Why? - if (rest.uid === rescheduleUid) { - return aggregate; - } - aggregate.push({ - start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(), - end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(), - title, - source: `eventType-${eventType?.id}-booking-${id}`, - }); - return aggregate; - }, []); + if (bookingsPromise) { + bookings = await bookingsPromise; + } + + const { busyTimes, bookingSeatCountMap } = processBookingsToBusyTimes( + bookings || [], + rescheduleUid, + eventTypeId, + beforeEventBuffer, + afterEventBuffer + ); logger.debug( `Busy Time from Cal Bookings ${JSON.stringify({ @@ -176,15 +222,10 @@ const _getBusyTimes = async (params: { ); performance.mark("prismaBookingGetEnd"); performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd"); - if (credentials?.length > 0 && !bypassBusyCalendarTimes) { + + if (credentials?.length > 0 && !bypassBusyCalendarTimes && calendarBusyTimesPromise) { const startConnectedCalendarsGet = performance.now(); - const calendarBusyTimes = await getBusyCalendarTimes( - credentials, - startTime, - endTime, - selectedCalendars, - shouldServeCache - ); + const calendarBusyTimes = await calendarBusyTimesPromise; const endConnectedCalendarsGet = performance.now(); logger.debug( `Connected Calendars get took ${ @@ -204,7 +245,7 @@ const _getBusyTimes = async (params: { }); if (rescheduleUid) { - const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid); + const originalRescheduleBooking = bookings?.find((booking) => booking.uid === rescheduleUid); // calendar busy time from original rescheduled booking should not be blocked if (originalRescheduleBooking) { openSeatsDateRanges.push({ diff --git a/packages/lib/getUserAvailability.ts b/packages/lib/getUserAvailability.ts index 2e5076ed45..6b065c4488 100644 --- a/packages/lib/getUserAvailability.ts +++ b/packages/lib/getUserAvailability.ts @@ -349,12 +349,15 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const durationLimits = parseDurationLimit(eventType?.durationLimits); let busyTimesFromLimits: EventBusyDetails[] = []; + let busyTimesFromTeamLimits: EventBusyDetails[] = []; + + const busyTimesPromises = []; if (initialData?.busyTimesFromLimits && initialData?.eventTypeForLimits) { busyTimesFromLimits = initialData.busyTimesFromLimits.get(user.id) || []; } else if (eventType && (bookingLimits || durationLimits)) { // Fall back to individual query if not available in initialData - busyTimesFromLimits = await getBusyTimesFromLimits( + const busyTimesFromLimitsPromise = getBusyTimesFromLimits( bookingLimits, durationLimits, dateFrom.tz(timeZone), @@ -364,7 +367,10 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA initialData?.busyTimesFromLimitsBookings ?? [], timeZone, initialData?.rescheduleUid ?? undefined - ); + ).then((result) => { + busyTimesFromLimits = result; + }); + busyTimesPromises.push(busyTimesFromLimitsPromise); } const teamForBookingLimits = @@ -374,13 +380,11 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits); - let busyTimesFromTeamLimits: EventBusyDetails[] = []; - if (initialData?.teamBookingLimits && teamForBookingLimits) { busyTimesFromTeamLimits = initialData.teamBookingLimits.get(user.id) || []; } else if (teamForBookingLimits && teamBookingLimits) { // Fall back to individual query if not available in initialData - busyTimesFromTeamLimits = await getBusyTimesFromTeamLimits( + const busyTimesFromTeamLimitsPromise = getBusyTimesFromTeamLimits( user, teamBookingLimits, dateFrom.tz(timeZone), @@ -389,7 +393,14 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA teamForBookingLimits.includeManagedEventsInLimits, timeZone, initialData?.rescheduleUid ?? undefined - ); + ).then((result) => { + busyTimesFromTeamLimits = result; + }); + busyTimesPromises.push(busyTimesFromTeamLimitsPromise); + } + + if (busyTimesPromises.length > 0) { + await Promise.all(busyTimesPromises); } // TODO: only query what we need after applying limits (shrink date range) diff --git a/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts b/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts index f87261f34d..85ba9f98eb 100644 --- a/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts +++ b/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts @@ -31,41 +31,51 @@ const _getBusyTimesFromLimits = async ( // shared amongst limiters to prevent processing known busy periods const limitManager = new LimitManager(); + const limitChecks = []; + // run this first, as counting bookings should always run faster.. if (bookingLimits) { performance.mark("bookingLimitsStart"); - await getBusyTimesFromBookingLimits({ - bookings, - bookingLimits, - dateFrom, - dateTo, - eventTypeId: eventType.id, - limitManager, - rescheduleUid, - timeZone, - }); - performance.mark("bookingLimitsEnd"); - performance.measure(`checking booking limits took $1'`, "bookingLimitsStart", "bookingLimitsEnd"); + limitChecks.push( + getBusyTimesFromBookingLimits({ + bookings, + bookingLimits, + dateFrom, + dateTo, + eventTypeId: eventType.id, + limitManager, + rescheduleUid, + timeZone, + }).then(() => { + performance.mark("bookingLimitsEnd"); + performance.measure(`checking booking limits took $1'`, "bookingLimitsStart", "bookingLimitsEnd"); + }) + ); } // ..than adding up durations (especially for the whole year) if (durationLimits) { performance.mark("durationLimitsStart"); - await getBusyTimesFromDurationLimits( - bookings, - durationLimits, - dateFrom, - dateTo, - duration, - eventType, - limitManager, - timeZone, - rescheduleUid + limitChecks.push( + getBusyTimesFromDurationLimits( + bookings, + durationLimits, + dateFrom, + dateTo, + duration, + eventType, + limitManager, + timeZone, + rescheduleUid + ).then(() => { + performance.mark("durationLimitsEnd"); + performance.measure(`checking duration limits took $1'`, "durationLimitsStart", "durationLimitsEnd"); + }) ); - performance.mark("durationLimitsEnd"); - performance.measure(`checking duration limits took $1'`, "durationLimitsStart", "durationLimitsEnd"); } + await Promise.all(limitChecks); + performance.mark("limitsEnd"); performance.measure(`checking all limits took $1'`, "limitsStart", "limitsEnd");