Skip to content

Commit

Permalink
perf: improve checkIsAvailable in getAvailable slots (#18352)
Browse files Browse the repository at this point in the history
* refactor: Update booking query to use userId and optimize availability check logic

- Changed the booking query to use userId instead of nested user object for filtering active bookings.
- Refactored availability check logic to improve readability and performance by using valueOf() for time comparisons and consolidating multiple checks into a single return statement.

* fix: update tests

* chore: use toDate()

* tests: add checkavailable unit tests
  • Loading branch information
Udit-takkar authored Dec 27, 2024
1 parent d2930bc commit e2f45bd
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 14 deletions.
163 changes: 163 additions & 0 deletions packages/trpc/server/routers/viewer/slots/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";

import type { GetAvailabilityUser } from "@calcom/core/getUserAvailability";
import dayjs from "@calcom/dayjs";
import type { EventBusyDate } from "@calcom/types/Calendar";

import { checkIfIsAvailable } from "./util";
import { getUsersWithCredentialsConsideringContactOwner } from "./util";

describe("getUsersWithCredentialsConsideringContactOwner", () => {
Expand Down Expand Up @@ -88,3 +91,163 @@ describe("getUsersWithCredentialsConsideringContactOwner", () => {
]);
});
});

describe("checkIfIsAvailable", () => {
const createTestData = (time: string) => ({
time: dayjs(time),
eventLength: 30,
busy: [] as EventBusyDate[],
});

describe("currentSeats handling", () => {
it("should return true if slot exists in currentSeats", () => {
const currentSeats: CurrentSeats = [
{
uid: "123",
startTime: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
_count: { attendees: 1 },
},
];

const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:00:00Z"),
currentSeats,
});

expect(result).toBe(true);
});
});

describe("busy time overlap scenarios", () => {
it("should return true when no busy periods", () => {
const result = checkIfIsAvailable(createTestData("2023-01-01T09:00:00Z"));
expect(result).toBe(true);
});

it("should return true when busy period ends before slot starts", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:00:00Z"),
busy: [
{
start: dayjs.utc("2023-01-01T08:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T08:30:00Z").toDate(),
},
],
});
expect(result).toBe(true);
});

it("should return true when busy period starts after slot ends", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:00:00Z"),
busy: [
{
start: dayjs.utc("2023-01-01T10:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T10:30:00Z").toDate(),
},
],
});
expect(result).toBe(true);
});

it("should return false when slot start falls within busy period", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:15:00Z"),
busy: [
{
start: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T09:30:00Z").toDate(),
},
],
});
expect(result).toBe(false);
});

it("should return false when slot end falls within busy period", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T08:45:00Z"),
busy: [
{
start: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T09:30:00Z").toDate(),
},
],
});
expect(result).toBe(false);
});

it("should return false when busy period is completely contained within slot", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:00:00Z"),
eventLength: 60,
busy: [
{
start: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
end: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
},
],
});
expect(result).toBe(false);
});

it("should return false when slot is completely contained within busy period", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:15:00Z"),
busy: [
{
start: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T10:00:00Z").toDate(),
},
],
});
expect(result).toBe(false);
});

it("should return true when multiple non-overlapping busy periods", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:30:00Z"),
busy: [
{
start: dayjs.utc("2023-01-01T08:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
},
{
start: dayjs.utc("2023-01-01T10:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T11:00:00Z").toDate(),
},
],
});
expect(result).toBe(true);
});

it("should return false if any busy period overlaps", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:30:00Z"),
busy: [
{
start: dayjs.utc("2023-01-01T08:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
},
{
start: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
end: dayjs.utc("2023-01-01T10:00:00Z").toDate(),
},
],
});
expect(result).toBe(false);
});

it("should handle exact boundary conditions", () => {
const result = checkIfIsAvailable({
...createTestData("2023-01-01T09:30:00Z"),
busy: [
{
start: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
end: dayjs.utc("2023-01-01T09:30:00Z").toDate(),
},
],
});
expect(result).toBe(true);
});
});
});
33 changes: 19 additions & 14 deletions packages/trpc/server/routers/viewer/slots/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,33 +62,38 @@ export const checkIfIsAvailable = ({
return true;
}

const slotEndTime = time.add(eventLength, "minutes").utc();
const slotStartTime = time.utc();
const slotStartDate = time.utc().toDate();
const slotEndDate = time.add(eventLength, "minutes").utc().toDate();

return busy.every((busyTime) => {
const startTime = dayjs.utc(busyTime.start).utc();
const endTime = dayjs.utc(busyTime.end);
const busyStartDate = dayjs.utc(busyTime.start).toDate();
const busyEndDate = dayjs.utc(busyTime.end).toDate();

if (endTime.isBefore(slotStartTime) || startTime.isAfter(slotEndTime)) {
// First check if there's any overlap at all
// If busy period ends before slot starts or starts after slot ends, there's no overlap
if (busyEndDate <= slotStartDate || busyStartDate >= slotEndDate) {
return true;
}

if (slotStartTime.isBetween(startTime, endTime, null, "[)")) {
return false;
} else if (slotEndTime.isBetween(startTime, endTime, null, "(]")) {
// Now check all possible overlap scenarios:

// 1. Slot start falls within busy period (inclusive start, exclusive end)
if (slotStartDate >= busyStartDate && slotStartDate < busyEndDate) {
return false;
}

// Check if start times are the same
if (time.utc().isBetween(startTime, endTime, null, "[)")) {
// 2. Slot end falls within busy period (exclusive start, inclusive end)
if (slotEndDate > busyStartDate && slotEndDate <= busyEndDate) {
return false;
}
// Check if slot end time is between start and end time
else if (slotEndTime.isBetween(startTime, endTime)) {

// 3. Busy period completely contained within slot
if (busyStartDate >= slotStartDate && busyEndDate <= slotEndDate) {
return false;
}
// Check if startTime is between slot
else if (startTime.isBetween(time, slotEndTime)) {

// 4. Slot completely contained within busy period
if (busyStartDate <= slotStartDate && busyEndDate >= slotEndDate) {
return false;
}

Expand Down

0 comments on commit e2f45bd

Please sign in to comment.