From 5efc7f3856940ca7a78630f38f84f6e7b5442928 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 18:40:18 -0400 Subject: [PATCH] =?UTF-8?q?perf:=20optimize=20O(n=C2=B2)=20algorithms=20in?= =?UTF-8?q?=20slot=20generation=20to=20O(n)=20or=20O(n=20log=20n)=20(#2137?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: keith@cal.com --- .../trpc/server/routers/viewer/slots/util.ts | 86 +++++++++++-------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 81cf2acb9b..b9b4a6a8ec 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -1,5 +1,4 @@ // eslint-disable-next-line no-restricted-imports -import { countBy } from "lodash"; import type { Logger } from "tslog"; import { v4 as uuid } from "uuid"; @@ -248,14 +247,21 @@ export const getRegularOrDynamicEventType = withReporting( ); function applyOccupiedSeatsToCurrentSeats(currentSeats: CurrentSeats, occupiedSeats: SelectedSlots[]) { - const occupiedSeatsCount = countBy(occupiedSeats, (item) => item.slotUtcStartDate.toISOString()); - Object.keys(occupiedSeatsCount).forEach((date) => { + const occupiedSeatsMap = new Map(); + + occupiedSeats.forEach((item) => { + const dateKey = item.slotUtcStartDate.toISOString(); + occupiedSeatsMap.set(dateKey, (occupiedSeatsMap.get(dateKey) || 0) + 1); + }); + + occupiedSeatsMap.forEach((count, date) => { currentSeats.push({ uid: uuid(), startTime: dayjs(date).toDate(), - _count: { attendees: occupiedSeatsCount[date] }, + _count: { attendees: count }, }); }); + return currentSeats; } @@ -542,15 +548,15 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise((r, c) => { + if (!c.isSeat) { + r.push({ start: c.slotUtcStartDate, end: c.slotUtcEndDate }); + } + return r; + }, []); + availableTimeSlots = availableTimeSlots .map((slot) => { - const busySlotsFromReservedSlots = reservedSlots.reduce((r, c) => { - if (!c.isSeat) { - r.push({ start: c.slotUtcStartDate, end: c.slotUtcEndDate }); - } - return r; - }, []); - if ( !checkForConflicts({ time: slot.time, @@ -588,34 +594,41 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise 0) { + currentSeats.forEach((booking) => { + const timeKey = booking.startTime.toISOString(); + currentSeatsMap.set(timeKey, { + attendees: booking._count.attendees, + uid: booking.uid, + }); + }); + } + return availableTimeSlots.reduce( ( r: Record, { time, ...passThroughProps } ) => { - // TODO: Adds unit tests to prevent regressions in getSchedule (try multiple timezones) - // This used to be _time.tz(input.timeZone) but Dayjs tz() is slow. // toLocaleDateString slugish, using Intl.DateTimeFormat we get the desired speed results. const dateString = formatter.format(time.toDate()); + const timeISO = time.toISOString(); r[dateString] = r[dateString] || []; if (eventType?.onlyShowFirstAvailableSlot && r[dateString].length > 0) { return r; } + + const existingBooking = currentSeatsMap.get(timeISO); + r[dateString].push({ ...passThroughProps, - time: time.toISOString(), - // Conditionally add the attendees and booking id to slots object if there is already a booking during that time - ...(currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString()) && { - attendees: - currentSeats[ - currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) - ]._count.attendees, - bookingUid: - currentSeats[ - currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) - ].uid, + time: timeISO, + ...(existingBooking && { + attendees: existingBooking.attendees, + bookingUid: existingBooking.uid, }), }); return r; @@ -654,20 +667,25 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise { - // Computation Optimization: If a future limit violation has been found, we just consider all slots to be out of bounds beyond that slot. - // We can't do the same for periodType=RANGE because it can start from a day other than today and today will hit the violation then. - if (foundAFutureLimitViolation && doesRangeStartFromToday(eventType.periodType)) { - return withinBoundsSlotsMappedToDate; + + const withinBoundsSlotsMappedToDate = {} as typeof slotsMappedToDate; + const doesStartFromToday = doesRangeStartFromToday(eventType.periodType); + + for (const [date, slots] of Object.entries(slotsMappedToDate)) { + if (foundAFutureLimitViolation && doesStartFromToday) { + break; // Instead of continuing the loop, we can break since all future dates will be skipped } + const filteredSlots = slots.filter((slot) => { const isFutureLimitViolationForTheSlot = isTimeViolatingFutureLimit({ time: slot.time, periodLimits, }); + if (isFutureLimitViolationForTheSlot) { foundAFutureLimitViolation = true; } + return ( !isFutureLimitViolationForTheSlot && // TODO: Perf Optimization: Slots calculation logic already seems to consider the minimum booking notice and past booking time and thus there shouldn't be need to filter out slots here. @@ -675,14 +693,12 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise