perf: no wait for session when calling getschedule 10552 cal 2311 (#10607)

* No batching on getting session

* Fix usePublicPage hook to use new router search params

* Move things so getSchedule data can be load as soon as we are rendering BookerComponent

* pre fetch session

* Removed custom code in favour of useTimePreferences

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
alannnc 2023-08-09 01:55:27 -07:00 committed by GitHub
parent bee5011ec1
commit fcd892bfa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 129 additions and 32 deletions

View File

@ -18,7 +18,7 @@ import { useFlags } from "@calcom/features/flags/hooks";
import { trpc } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui";
import usePublicPage from "@lib/hooks/usePublicPage";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
import type { WithNonceProps } from "@lib/withNonce";
import { useViewerI18n } from "@components/I18nLanguageHandler";
@ -247,7 +247,7 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) {
const AppProviders = (props: AppPropsWithChildren) => {
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const isBookingPage = useIsBookingPage();
const { pageProps, ...rest } = props;
const { _nonce, ...restPageProps } = pageProps;
const propsWithoutNonce = {
@ -267,7 +267,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
themeBasis={props.pageProps.themeBasis}
nonce={props.pageProps.nonce}
isThemeSupported={props.Component.isThemeSupported}
isBookingPage={props.Component.isBookingPage}
isBookingPage={props.Component.isBookingPage || isBookingPage}
router={props.router}>
<FeatureFlagsProvider>
<OrgBrandProvider>
@ -281,7 +281,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
</EventCollectionProvider>
);
if (isPublicPage) {
if (isBookingPage) {
return RemainingProviders;
}

View File

@ -0,0 +1,12 @@
import { usePathname, useSearchParams } from "next/navigation";
export default function useIsBookingPage() {
const pathname = usePathname();
const isBookingPage = ["/booking", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
const searchParams = useSearchParams();
const userParam = searchParams.get("user");
const teamParam = searchParams.get("team");
return !!(isBookingPage || userParam || teamParam);
}

View File

@ -1,9 +0,0 @@
import { usePathname } from "next/navigation";
export default function usePublicPage() {
const pathname = usePathname();
const isPublicPage = ["/[user]", "/booking", "/cancel", "/reschedule"].find((route) =>
pathname?.startsWith(route)
);
return isPublicPage;
}

View File

@ -316,8 +316,6 @@ async function runTestStepsCommonForTeamAndUserEventType(
await test.step("Do a reschedule and notice that we can't book without giving a value for rescheduleReason", async () => {
const page = previewTabPage;
await rescheduleFromTheLinkOnPage({ page });
// eslint-disable-next-line playwright/no-page-pause
await page.pause();
await expectErrorToBeThereFor({ page, name: "rescheduleReason" });
});
}

View File

@ -31,5 +31,7 @@ export async function ssrInit(context: GetServerSidePropsContext) {
// Provides a better UX to the users who have already upgraded.
await ssr.viewer.teams.hasTeamPlan.prefetch();
await ssr.viewer.public.session.prefetch();
return ssr;
}

View File

@ -21,7 +21,7 @@ import { Away, NotFound } from "./components/Unavailable";
import { extraDaysConfig, fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
import { useBookerStore, useInitializeBookerStore } from "./store";
import type { BookerLayout, BookerProps } from "./types";
import { useEvent } from "./utils/event";
import { useEvent, useScheduleForEvent } from "./utils/event";
import { validateLayout } from "./utils/layout";
import { getQueryParam } from "./utils/query-param";
import { useBrandColors } from "./utils/use-brand-colors";
@ -41,6 +41,17 @@ const BookerComponent = ({
isTeamEvent,
entity,
}: BookerProps) => {
/**
* Prioritize dateSchedule load
* Component will render but use data already fetched from here, and no duplicate requests will be made
* */
useScheduleForEvent({
prefetchNextMonth: false,
username,
eventSlug,
month,
duration: undefined,
});
const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(max-width: 1024px)");
const timeslotsRef = useRef<HTMLDivElement>(null);

View File

@ -27,7 +27,8 @@ export const useEvent = () => {
/**
* Gets schedule for the current event and current month.
* Gets all values from the booker store.
* Gets all values right away and not the store because it increases network timing, only for the first render.
* We can read from the store if we want to get the latest values.
*
* Using this hook means you only need to use one hook, instead
* of combining multiple conditional hooks.
@ -36,21 +37,35 @@ export const useEvent = () => {
* useful when the user is viewing dates near the end of the month,
* this way the multi day view will show data of both months.
*/
export const useScheduleForEvent = ({ prefetchNextMonth }: { prefetchNextMonth?: boolean } = {}) => {
export const useScheduleForEvent = ({
prefetchNextMonth,
username,
eventSlug,
eventId,
month,
duration,
}: {
prefetchNextMonth?: boolean;
username?: string | null;
eventSlug?: string | null;
eventId?: number | null;
month?: string | null;
duration?: number | null;
} = {}) => {
const { timezone } = useTimePreferences();
const event = useEvent();
const [username, eventSlug, month, duration] = useBookerStore(
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore(
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
shallow
);
return useSchedule({
username,
eventSlug,
eventId: event.data?.id,
month,
username: usernameFromStore ?? username,
eventSlug: eventSlugFromStore ?? eventSlug,
eventId: event.data?.id ?? eventId,
timezone,
prefetchNextMonth,
duration,
month: monthFromStore ?? month,
duration: durationFromStore ?? duration,
});
};

View File

@ -29,24 +29,31 @@ export const useSchedule = ({
return trpc.viewer.public.slots.getSchedule.useQuery(
{
usernameList: getUsernameList(username ?? ""),
eventTypeSlug: eventSlug!,
// Prioritize slug over id, since slug is the first value we get available.
// If we have a slug, we don't need to fetch the id.
// TODO: are queries using eventTypeId faster? Even tho we lost time fetching the id with the slug.
...(eventSlug ? { eventTypeSlug: eventSlug } : { eventTypeId: eventId ?? 0 }),
// @TODO: Old code fetched 2 days ago if we were fetching the current month.
// Do we want / need to keep that behavior?
startTime: monthDayjs.startOf("month").toISOString(),
// if `prefetchNextMonth` is true, two months are fetched at once.
endTime: (prefetchNextMonth ? nextMonthDayjs : monthDayjs).endOf("month").toISOString(),
timeZone: timezone!,
eventTypeId: eventId!,
duration: duration ? `${duration}` : undefined,
},
{
trpc: {
context: {
skipBatch: true,
},
},
refetchOnWindowFocus: false,
enabled:
Boolean(username) &&
Boolean(eventSlug) &&
(Boolean(eventId) || eventId === 0) &&
Boolean(month) &&
Boolean(timezone),
Boolean(timezone) &&
// Should only wait for one or the other, not both.
(Boolean(eventSlug) || Boolean(eventId) || eventId === 0),
}
);
};

View File

@ -70,9 +70,46 @@ export const checkIfIsAvailable = ({
};
export async function getEventType(input: TGetScheduleInputSchema) {
const { eventTypeSlug, usernameList } = input;
let eventTypeId = input.eventTypeId;
if (eventTypeId === undefined && eventTypeSlug && usernameList && usernameList.length === 1) {
// If we only have the slug and usernameList, we need to get the id first
const username = usernameList[0];
const userId = await getUserIdFromUsername(username);
let teamId;
if (!userId) {
teamId = await getTeamIdFromSlug(username);
if (!teamId) {
throw new TRPCError({
message: "User or team not found",
code: "NOT_FOUND",
});
}
}
const eventType = await prisma.eventType.findFirst({
where: {
slug: eventTypeSlug,
...(teamId ? { teamId } : {}),
...(userId ? { userId } : {}),
},
select: {
id: true,
},
});
if (!eventType) {
throw new TRPCError({ code: "NOT_FOUND" });
}
eventTypeId = eventType.id;
}
const eventType = await prisma.eventType.findUnique({
where: {
id: input.eventTypeId,
id: eventTypeId,
},
select: {
id: true,
@ -175,7 +212,7 @@ export async function getDynamicEventType(input: TGetScheduleInputSchema) {
}
export function getRegularOrDynamicEventType(input: TGetScheduleInputSchema) {
const isDynamicBooking = !input.eventTypeId;
const isDynamicBooking = input.usernameList && input.usernameList.length > 1;
return isDynamicBooking ? getDynamicEventType(input) : getEventType(input);
}
@ -237,7 +274,7 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
username: currentUser.username || "",
dateFrom: startTime.format(),
dateTo: endTime.format(),
eventTypeId: input.eventTypeId,
eventTypeId: eventType.id,
afterEventBuffer: eventType.afterEventBuffer,
beforeEventBuffer: eventType.beforeEventBuffer,
duration: input.duration || 0,
@ -446,3 +483,27 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
slots: computedAvailableSlots,
};
}
async function getUserIdFromUsername(username: string) {
const user = await prisma.user.findFirst({
where: {
username,
},
select: {
id: true,
},
});
return user?.id;
}
async function getTeamIdFromSlug(slug: string) {
const team = await prisma.team.findFirst({
where: {
slug,
},
select: {
id: true,
},
});
return team?.id;
}