perf: getUserAvailability if dateRange is empty, early return (#26000)

* Early return if date range is empty

* test: add tests for empty workingHours early return optimization

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei@cal.com>
This commit is contained in:
Joe Au-Yeung
2026-01-21 02:51:01 -03:00
committed by GitHub
co-authored by joe@cal.com <j.auyeung419@gmail.com> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Volnei Munhoz
parent 546a1510b2
commit ec951cacad
2 changed files with 384 additions and 116 deletions
+253 -4
View File
@@ -24,12 +24,14 @@ import { expect, expectedSlotsForSchedule } from "./getSchedule/expects";
import { setupAndTeardown } from "./getSchedule/setupAndTeardown";
import { timeTravelToTheBeginningOfToday } from "./getSchedule/utils";
/* eslint-disable @typescript-eslint/no-explicit-any */
constantsScenarios.set({
IS_PRODUCTION: true as any,
IS_PRODUCTION: true,
WEBAPP_URL: "http://localhost:3000",
RESERVED_SUBDOMAINS: ["auth", "docs"] as any,
SINGLE_ORG_SLUG: "" as any,
});
RESERVED_SUBDOMAINS: ["auth", "docs"],
SINGLE_ORG_SLUG: "",
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
describe("getSchedule", () => {
const availableSlotsService = getAvailableSlotsService();
@@ -3630,4 +3632,251 @@ describe("getSchedule", () => {
);
});
});
describe("Empty working hours - early return optimization", () => {
test("returns no slots when workingHours is empty", async () => {
vi.setSystemTime("2024-05-21T00:00:13Z");
const plus1DateString = "2024-05-22";
const plus2DateString = "2024-05-23";
await createBookingScenario({
eventTypes: [
{
id: 1,
slotInterval: 60,
length: 60,
users: [
{
id: 101,
},
],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [TestData.schedules.EmptyAvailability],
},
],
bookings: [],
});
const schedule = await availableSlotsService.getAvailableSlots({
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
orgSlug: null,
},
});
expect(schedule).toHaveDateDisabled({
dateString: plus2DateString,
});
});
test("returns slots only for date override when workingHours is empty but date override exists", async () => {
vi.setSystemTime("2024-05-21T00:00:13Z");
const plus1DateString = "2024-05-22";
const plus2DateString = "2024-05-23";
await createBookingScenario({
eventTypes: [
{
id: 1,
slotInterval: 60,
length: 60,
users: [
{
id: 101,
},
],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [
{
name: "Empty schedule with date override",
availability: [
{
days: [],
startTime: new Date("1970-01-01T09:00:00.000Z"),
endTime: new Date("1970-01-01T12:00:00.000Z"),
date: plus2DateString,
},
],
timeZone: Timezones["+5:30"],
},
],
},
],
bookings: [],
});
const schedule = await availableSlotsService.getAvailableSlots({
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
orgSlug: null,
},
});
expect(schedule).toHaveTimeSlots(
[`03:30:00.000Z`, `04:30:00.000Z`, `05:30:00.000Z`],
{
dateString: plus2DateString,
}
);
});
test("returns slots only for date overrides when workingHours is empty but multiple date overrides exist", async () => {
vi.setSystemTime("2024-05-21T00:00:13Z");
const plus1DateString = "2024-05-22";
const plus2DateString = "2024-05-23";
const plus3DateString = "2024-05-24";
await createBookingScenario({
eventTypes: [
{
id: 1,
slotInterval: 60,
length: 60,
users: [
{
id: 101,
},
],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [
{
name: "Empty schedule with multiple date overrides",
availability: [
{
days: [],
startTime: new Date("1970-01-01T09:00:00.000Z"),
endTime: new Date("1970-01-01T11:00:00.000Z"),
date: plus2DateString,
},
{
days: [],
startTime: new Date("1970-01-01T14:00:00.000Z"),
endTime: new Date("1970-01-01T16:00:00.000Z"),
date: plus3DateString,
},
],
timeZone: Timezones["+5:30"],
},
],
},
],
bookings: [],
});
const schedule = await availableSlotsService.getAvailableSlots({
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
orgSlug: null,
},
});
expect(schedule).toHaveTimeSlots([`03:30:00.000Z`, `04:30:00.000Z`], {
dateString: plus2DateString,
});
expect(schedule).toHaveTimeSlots([`08:30:00.000Z`, `09:30:00.000Z`], {
dateString: plus3DateString,
});
});
test("returns no slots for dates without date override when workingHours is empty", async () => {
vi.setSystemTime("2024-05-21T00:00:13Z");
const plus1DateString = "2024-05-22";
const plus2DateString = "2024-05-23";
const plus3DateString = "2024-05-24";
await createBookingScenario({
eventTypes: [
{
id: 1,
slotInterval: 60,
length: 60,
users: [
{
id: 101,
},
],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [
{
name: "Empty schedule with date override only on plus3",
availability: [
{
days: [],
startTime: new Date("1970-01-01T09:00:00.000Z"),
endTime: new Date("1970-01-01T12:00:00.000Z"),
date: plus3DateString,
},
],
timeZone: Timezones["+5:30"],
},
],
},
],
bookings: [],
});
const schedule = await availableSlotsService.getAvailableSlots({
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
orgSlug: null,
},
});
expect(schedule).toHaveDateDisabled({
dateString: plus2DateString,
});
expect(schedule).toHaveTimeSlots(
[`03:30:00.000Z`, `04:30:00.000Z`, `05:30:00.000Z`],
{
dateString: plus3DateString,
}
);
});
});
});
@@ -386,6 +386,132 @@ export class UserAvailabilityService {
user,
});
if (
!(
schedule?.availability ||
(eventType?.availability.length ? eventType.availability : user.availability)
)
) {
throw new HttpError({ statusCode: 400, message: ErrorCode.AvailabilityNotFoundInSchedule });
}
const availability = (
schedule?.availability || (eventType?.availability.length ? eventType.availability : user.availability)
).map((a) => ({
...a,
userId: user.id,
}));
let calendarTimezone: string | null = null;
let finalTimezone: string | null = null;
if (!isTimezoneSet) {
calendarTimezone = await this.getTimezoneFromDelegatedCalendars(user);
if (calendarTimezone) {
finalTimezone = calendarTimezone;
}
}
if (!finalTimezone) {
finalTimezone = schedule.timeZone;
}
const workingHours = getWorkingHours({ timeZone: finalTimezone }, availability);
const dateOverrides: TimeRange[] = [];
// NOTE: getSchedule is currently calling this function for every user in a team event
// but not using these values at all, wasting CPU. Adding this check here temporarily to avoid a larger refactor
// since other callers do using this data.
if (returnDateOverrides) {
const calculateDateOverridesSpan = Sentry.startInactiveSpan({ name: "calculateDateOverrides" });
const availabilityWithDates = availability.filter((availability) => !!availability.date);
for (let i = 0; i < availabilityWithDates.length; i++) {
const override = availabilityWithDates[i];
const startTime = dayjs.utc(override.startTime);
const endTime = dayjs.utc(override.endTime);
const overrideStartDate = dayjs.utc(override.date).hour(startTime.hour()).minute(startTime.minute());
const overrideEndDate = dayjs.utc(override.date).hour(endTime.hour()).minute(endTime.minute());
if (
overrideStartDate.isBetween(dateFrom, dateTo, null, "[]") ||
overrideEndDate.isBetween(dateFrom, dateTo, null, "[]")
) {
dateOverrides.push({
start: overrideStartDate.toDate(),
end: overrideEndDate.toDate(),
});
}
}
calculateDateOverridesSpan.end();
}
const outOfOfficeDays =
initialData?.outOfOfficeDays ??
(await this.dependencies.oooRepo.findUserOOODays({
userId: user.id,
dateFrom: dateFrom.toISOString(),
dateTo: dateTo.toISOString(),
}));
const datesOutOfOffice: IOutOfOfficeData = this.calculateOutOfOfficeRanges(outOfOfficeDays, availability);
const holidayBlockedDates = await this.calculateHolidayBlockedDates(
user.id,
dateFrom.toDate(),
dateTo.toDate(),
availability
);
for (const [date, holidayData] of Object.entries(holidayBlockedDates)) {
if (!datesOutOfOffice[date]) {
datesOutOfOffice[date] = holidayData;
}
}
const travelSchedules: {
startDate: Dayjs;
endDate?: Dayjs;
timeZone: string;
}[] = [];
if (isDefaultSchedule) {
travelSchedules.push(
...user.travelSchedules.map((schedule) => {
let endDate: Dayjs | undefined;
if (schedule.endDate) {
endDate = dayjs(schedule.endDate);
}
return {
startDate: dayjs(schedule.startDate),
endDate,
timeZone: schedule.timeZone,
};
})
);
}
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
dateFrom,
dateTo,
availability,
timeZone: finalTimezone,
travelSchedules,
outOfOffice: datesOutOfOffice,
});
if (dateRanges.length === 0)
return {
busy: [],
timeZone: finalTimezone,
dateRanges: [],
oooExcludedDateRanges: [],
workingHours: [],
dateOverrides: [],
currentSeats: [],
datesOutOfOffice: undefined,
};
const bookingLimits =
eventType?.bookingLimits &&
typeof eventType.bookingLimits === "object" &&
@@ -408,20 +534,6 @@ export class UserAvailabilityService {
? user.allSelectedCalendars.filter((calendar) => calendar.eventTypeId === eventType.id)
: user.userLevelSelectedCalendars;
let calendarTimezone: string | null = null;
let finalTimezone: string | null = null;
if (!isTimezoneSet) {
calendarTimezone = await this.getTimezoneFromDelegatedCalendars(user);
if (calendarTimezone) {
finalTimezone = calendarTimezone;
}
}
if (!finalTimezone) {
finalTimezone = schedule.timeZone;
}
let busyTimesFromLimits: EventBusyDetails[] = [];
if (initialData?.busyTimesFromLimits && initialData?.eventTypeForLimits) {
@@ -514,106 +626,13 @@ export class UserAvailabilityService {
...busyTimesFromTeamLimits,
];
if (
!(
schedule?.availability ||
(eventType?.availability.length ? eventType.availability : user.availability)
)
) {
throw new HttpError({ statusCode: 400, message: ErrorCode.AvailabilityNotFoundInSchedule });
}
const availability = (
schedule?.availability || (eventType?.availability.length ? eventType.availability : user.availability)
).map((a) => ({
...a,
userId: user.id,
}));
const workingHours = getWorkingHours({ timeZone: finalTimezone }, availability);
const dateOverrides: TimeRange[] = [];
// NOTE: getSchedule is currently calling this function for every user in a team event
// but not using these values at all, wasting CPU. Adding this check here temporarily to avoid a larger refactor
// since other callers do using this data.
if (returnDateOverrides) {
const calculateDateOverridesSpan = Sentry.startInactiveSpan({ name: "calculateDateOverrides" });
const availabilityWithDates = availability.filter((availability) => !!availability.date);
for (let i = 0; i < availabilityWithDates.length; i++) {
const override = availabilityWithDates[i];
const startTime = dayjs.utc(override.startTime);
const endTime = dayjs.utc(override.endTime);
const overrideStartDate = dayjs.utc(override.date).hour(startTime.hour()).minute(startTime.minute());
const overrideEndDate = dayjs.utc(override.date).hour(endTime.hour()).minute(endTime.minute());
if (
overrideStartDate.isBetween(dateFrom, dateTo, null, "[]") ||
overrideEndDate.isBetween(dateFrom, dateTo, null, "[]")
) {
dateOverrides.push({
start: overrideStartDate.toDate(),
end: overrideEndDate.toDate(),
});
}
}
calculateDateOverridesSpan.end();
}
const outOfOfficeDays =
initialData?.outOfOfficeDays ??
(await this.dependencies.oooRepo.findUserOOODays({
userId: user.id,
dateFrom: dateFrom.toISOString(),
dateTo: dateTo.toISOString(),
}));
const datesOutOfOffice: IOutOfOfficeData = this.calculateOutOfOfficeRanges(outOfOfficeDays, availability);
const holidayBlockedDates = await this.calculateHolidayBlockedDates(
user.id,
dateFrom.toDate(),
dateTo.toDate(),
availability
log.debug(
`EventType: ${eventTypeId} | User: ${username} (ID: ${userId}) - usingSchedule: ${safeStringify({
chosenSchedule: schedule,
eventTypeSchedule: eventType?.schedule,
})}`
);
for (const [date, holidayData] of Object.entries(holidayBlockedDates)) {
if (!datesOutOfOffice[date]) {
datesOutOfOffice[date] = holidayData;
}
}
const travelSchedules: {
startDate: dayjs.Dayjs;
endDate?: dayjs.Dayjs;
timeZone: string;
}[] = [];
if (isDefaultSchedule) {
travelSchedules.push(
...user.travelSchedules.map((schedule) => {
let endDate: Dayjs | undefined;
if (schedule.endDate) {
endDate = dayjs(schedule.endDate);
}
return {
startDate: dayjs(schedule.startDate),
endDate,
timeZone: schedule.timeZone,
};
})
);
}
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
dateFrom,
dateTo,
availability,
timeZone: finalTimezone,
travelSchedules,
outOfOffice: datesOutOfOffice,
});
const formattedBusyTimes = detailedBusyTimes.map((busy) => ({
start: dayjs(busy.start),
end: dayjs(busy.end),