diff --git a/packages/lib/slots.test.ts b/packages/lib/slots.test.ts index 8a4a4c5adc..04e3482fea 100644 --- a/packages/lib/slots.test.ts +++ b/packages/lib/slots.test.ts @@ -350,3 +350,52 @@ describe("Tests the slot logic with custom env variable", () => { ).toHaveLength(11); }); }); + +describe("Tests the slots function performance", () => { + it("handles hundreds of date ranges efficiently", async () => { + const startTime = process.hrtime(); + + const startDay = dayjs.utc().add(1, "day").startOf("day"); + const dateRanges: DateRange[] = []; + + for (let day = 0; day < 7; day++) { + const currentDay = startDay.add(day, "day"); + + for (let hour = 0; hour < 24; hour++) { + for (let minute = 0; minute < 60; minute += 5) { + if (dateRanges.length >= 2000) break; + + dateRanges.push({ + start: currentDay.hour(hour).minute(minute), + end: currentDay + .hour(hour) + .minute(minute + 4) + .second(59), + }); + } + if (dateRanges.length >= 2000) break; + } + if (dateRanges.length >= 2000) break; + } + + const result = getSlots({ + inviteeDate: startDay, + frequency: 5, + minimumBookingNotice: 0, + dateRanges: dateRanges, + eventLength: 5, + offsetStart: 0, + }); + + expect(result.length).toBeGreaterThan(0); + + const endTime = process.hrtime(startTime); + const executionTimeInMs = endTime[0] * 1000 + endTime[1] / 1000000; + + expect(executionTimeInMs).toBeLessThan(2000); // less than 2 seconds for 2000 date ranges + + console.log( + `Performance test completed in ${executionTimeInMs}ms with ${result.length} slots generated from ${dateRanges.length} date ranges` + ); + }); +}); diff --git a/packages/lib/slots.ts b/packages/lib/slots.ts index 565223395b..6e3f7cf97a 100644 --- a/packages/lib/slots.ts +++ b/packages/lib/slots.ts @@ -40,6 +40,9 @@ function buildSlotsWithDateRanges({ frequency = minimumOfOne(frequency); eventLength = minimumOfOne(eventLength); offsetStart = offsetStart ? minimumOfOne(offsetStart) : 0; + + const orderedDateRanges = dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf()); + // there can only ever be one slot at a given start time, and based on duration also only a single length. const slots = new Map< string, @@ -66,7 +69,8 @@ function buildSlotsWithDateRanges({ const startTimeWithMinNotice = dayjs.utc().add(minimumBookingNotice, "minute"); - const orderedDateRanges = dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf()); + const slotBoundaries = new Map(); + orderedDateRanges.forEach((range) => { const dateYYYYMMDD = range.start.format("YYYY-MM-DD"); @@ -81,32 +85,41 @@ function buildSlotsWithDateRanges({ slotStartTime = slotStartTime.add(offsetStart ?? 0, "minutes").tz(timeZone); - // if the slotStartTime is between an existing slot, we need to adjust to the begin of the existing slot - // but that adjusted startTime must be legal. - const iterator = slots.keys(); - let result = iterator.next(); + // Find the nearest appropriate slot boundary if this time falls within an existing slot + const slotBoundariesArray = Array.from(slotBoundaries.keys()).map((t) => dayjs(t)); + if (slotBoundariesArray.length > 0) { + slotBoundariesArray.sort((a, b) => a.valueOf() - b.valueOf()); - while (!result.done) { - const utcResultValue = dayjs.utc(result.value); - // if the slotStartTime is between an existing slot, we need to adjust to the begin of the existing slot - if ( - utcResultValue.isBefore(slotStartTime) && - utcResultValue.add(frequency + (offsetStart ?? 0), "minutes").isAfter(slotStartTime) - ) { - // however, the slot can now be before the start of this date range. - if (!utcResultValue.isBefore(range.start)) { - // it is between, if possible floor down to the start of the existing slot - slotStartTime = utcResultValue; - } else { - // if not possible to floor, we need to ceil up to the next slot. - slotStartTime = utcResultValue.add(frequency + (offsetStart ?? 0), "minutes"); + let prevBoundary = null; + for (let i = slotBoundariesArray.length - 1; i >= 0; i--) { + if (slotBoundariesArray[i].isBefore(slotStartTime)) { + prevBoundary = slotBoundariesArray[i]; + break; + } + } + + if (prevBoundary) { + const prevBoundaryEnd = prevBoundary.add(frequency + (offsetStart ?? 0), "minutes"); + if (prevBoundaryEnd.isAfter(slotStartTime)) { + if (!prevBoundary.isBefore(range.start)) { + slotStartTime = prevBoundary; + } else { + slotStartTime = prevBoundaryEnd; + } + slotStartTime = slotStartTime.tz(timeZone); } - // and then convert to the correct timezone - UTC mode is just for performance. - slotStartTime = slotStartTime.tz(timeZone); } - result = iterator.next(); } + while (!slotStartTime.add(eventLength, "minutes").subtract(1, "second").utc().isAfter(range.end)) { + const slotKey = slotStartTime.toISOString(); + if (slots.has(slotKey)) { + slotStartTime = slotStartTime.add(frequency + (offsetStart ?? 0), "minutes"); + continue; + } + + slotBoundaries.set(slotKey, true); + const dateOutOfOfficeExists = datesOutOfOffice?.[dateYYYYMMDD]; let slotData: { time: Dayjs; @@ -133,7 +146,7 @@ function buildSlotsWithDateRanges({ }; } - slots.set(slotData.time.toISOString(), slotData); + slots.set(slotKey, slotData); slotStartTime = slotStartTime.add(frequency + (offsetStart ?? 0), "minutes"); } });