From 7e4bbfb3e6ea10e0a1afe5efc26d44bc48e04e88 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Tue, 5 Nov 2024 12:53:57 +0100 Subject: [PATCH] feat: performance improvements --- .../(standard)/[step]/page.tsx | 1 - .../(standard)/select-rate/page.tsx | 1 - components/ContentType/HotelPage/index.tsx | 4 +- components/Current/Footer/index.tsx | 6 +- lib/trpc/memoizedRequests/index.ts | 9 +- .../contentstack/loyaltyLevel/query.ts | 97 ++-- server/routers/hotels/input.ts | 11 +- server/routers/hotels/query.ts | 461 +++++++++--------- 8 files changed, 290 insertions(+), 300 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 1743d07b2..e4ace7273 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -69,7 +69,6 @@ export default async function StepPage({ const hotelData = await getHotelData({ hotelId, language: lang, - include: [HotelIncludeEnum.RoomCategories], }) const roomAvailability = await getSelectedRoomAvailability({ hotelId, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 5a7c8617c..7655dee10 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -61,7 +61,6 @@ export default async function SelectRatePage({ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, language: params.lang, - include: [HotelIncludeEnum.RoomCategories], }), serverClient().hotel.availability.rooms({ hotelId: parseInt(searchParams.hotel, 10), diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index d3ee721a8..0b13baa2a 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -31,9 +31,7 @@ export default async function HotelPage() { const lang = getLang() const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID - const hotelData = await serverClient().hotel.get({ - include: ["RoomCategories"], - }) + const hotelData = await serverClient().hotel.get() if (!hotelData) { return null } diff --git a/components/Current/Footer/index.tsx b/components/Current/Footer/index.tsx index 83860cae7..683c26011 100644 --- a/components/Current/Footer/index.tsx +++ b/components/Current/Footer/index.tsx @@ -1,4 +1,4 @@ -import { serverClient } from "@/lib/trpc/server" +import { getCurrentFooter } from "@/lib/trpc/memoizedRequests" import Image from "@/components/Image" import { getLang } from "@/i18n/serverContext" @@ -8,9 +8,7 @@ import Navigation from "./Navigation" import styles from "./footer.module.css" export default async function Footer() { - const footerData = await serverClient().contentstack.base.currentFooter({ - lang: getLang(), - }) + const footerData = await getCurrentFooter(getLang()) if (!footerData) { return null } diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index d54339801..9f8c701b0 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -61,18 +61,15 @@ export const getHotelData = cache(async function getMemoizedHotelData({ hotelId, language, isCardOnlyPayment, - include, }: { hotelId: string language: string isCardOnlyPayment?: boolean - include?: HotelIncludeEnum[] }) { return serverClient().hotel.hotelData.get({ hotelId, language, isCardOnlyPayment, - include, }) }) @@ -120,6 +117,12 @@ export const getCurrentHeader = cache(async function getMemoizedCurrentHeader( return serverClient().contentstack.base.currentHeader({ lang }) }) +export const getCurrentFooter = cache(async function getMemoizedCurrentFooter( + lang: Lang +) { + return serverClient().contentstack.base.currentFooter({ lang }) +}) + export const getMyPagesNavigation = cache( async function getMemoizedMyPagesNavigation() { return serverClient().contentstack.myPages.navigation.get() diff --git a/server/routers/contentstack/loyaltyLevel/query.ts b/server/routers/contentstack/loyaltyLevel/query.ts index c531fbebb..3b8cfa6b4 100644 --- a/server/routers/contentstack/loyaltyLevel/query.ts +++ b/server/routers/contentstack/loyaltyLevel/query.ts @@ -1,4 +1,5 @@ import { metrics } from "@opentelemetry/api" +import { cache } from "react" import { MembershipLevel, @@ -42,7 +43,7 @@ const getByLevelLoyaltyLevelFailCounter = meter.createCounter( "trpc.contentstack.loyaltyLevel.byLevel-fail" ) -export async function getAllLoyaltyLevels(ctx: Context) { +export const getAllLoyaltyLevels = cache(async (ctx: Context) => { getAllLoyaltyLevelCounter.add(1) // Ideally we should fetch all available tiers from API, but since they @@ -95,58 +96,60 @@ export async function getAllLoyaltyLevels(ctx: Context) { getAllLoyaltyLevelSuccessCounter.add(1) return validatedLoyaltyLevels.data -} +}) -export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) { - getByLevelLoyaltyLevelCounter.add(1, { - query: JSON.stringify({ lang: ctx.lang, level_id }), - }) +export const getLoyaltyLevel = cache( + async (ctx: Context, level_id: MembershipLevel) => { + getByLevelLoyaltyLevelCounter.add(1, { + query: JSON.stringify({ lang: ctx.lang, level_id }), + }) - const loyaltyLevelsConfigResponse = await request( - GetLoyaltyLevel, - { lang: ctx.lang, level_id }, - { - next: { - tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)], - }, - cache: "force-cache", + const loyaltyLevelsConfigResponse = await request( + GetLoyaltyLevel, + { lang: ctx.lang, level_id }, + { + next: { + tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)], + }, + cache: "force-cache", + } + ) + if ( + !loyaltyLevelsConfigResponse.data || + !loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length + ) { + getByLevelLoyaltyLevelFailCounter.add(1) + const notFoundError = notFound(loyaltyLevelsConfigResponse) + console.error( + "contentstack.loyaltyLevel not found error", + JSON.stringify({ + query: { lang: ctx.lang, level_id }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError } - ) - if ( - !loyaltyLevelsConfigResponse.data || - !loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length - ) { - getByLevelLoyaltyLevelFailCounter.add(1) - const notFoundError = notFound(loyaltyLevelsConfigResponse) - console.error( - "contentstack.loyaltyLevel not found error", - JSON.stringify({ - query: { lang: ctx.lang, level_id }, - error: { code: notFoundError.code }, - }) - ) - throw notFoundError - } - const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse( - loyaltyLevelsConfigResponse.data - ) - if (!validatedLoyaltyLevels.success) { - getByLevelLoyaltyLevelFailCounter.add(1) - console.error(validatedLoyaltyLevels.error) - console.error( - "contentstack.loyaltyLevel validation error", - JSON.stringify({ - query: { lang: ctx.lang, level_id }, - error: validatedLoyaltyLevels.error, - }) + const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse( + loyaltyLevelsConfigResponse.data ) - return null - } + if (!validatedLoyaltyLevels.success) { + getByLevelLoyaltyLevelFailCounter.add(1) + console.error(validatedLoyaltyLevels.error) + console.error( + "contentstack.loyaltyLevel validation error", + JSON.stringify({ + query: { lang: ctx.lang, level_id }, + error: validatedLoyaltyLevels.error, + }) + ) + return null + } - getByLevelLoyaltyLevelSuccessCounter.add(1) - return validatedLoyaltyLevels.data[0] -} + getByLevelLoyaltyLevelSuccessCounter.add(1) + return validatedLoyaltyLevels.data[0] + } +) export const loyaltyLevelQueryRouter = router({ byLevel: contentstackBaseProcedure diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 16518bc9f..50ec5af4b 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -1,11 +1,5 @@ import { z } from "zod" -export const getHotelInputSchema = z.object({ - include: z - .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) - .optional(), -}) - export const getHotelsAvailabilityInputSchema = z.object({ cityId: z.string(), roomStayStartDate: z.string(), @@ -54,18 +48,17 @@ export const getRatesInputSchema = z.object({ hotelId: z.string(), }) -export enum HotelIncludeEnum { +export const HotelIncludeEnum = z.enum([ "RoomCategories", "NearbyHotels", "Restaurants", "City", -} +]) export const getHotelDataInputSchema = z.object({ hotelId: z.string(), language: z.string(), isCardOnlyPayment: z.boolean().optional(), - include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(), }) export type HotelDataInput = z.input diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index ae27a8498..87db78640 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1,4 +1,5 @@ import { metrics } from "@opentelemetry/api" +import { cache } from "react" import { Lang } from "@/constants/languages" import * as api from "@/lib/api" @@ -34,7 +35,6 @@ import { import { getBreakfastPackageInputSchema, getHotelDataInputSchema, - getHotelInputSchema, getHotelsAvailabilityInputSchema, getRatesInputSchema, getRoomsAvailabilityInputSchema, @@ -165,251 +165,249 @@ async function getContentstackData(lang: Lang, uid?: string | null) { return hotelPageData.data.hotel_page } -export async function getHotelData( - input: HotelDataInput, - serviceToken: string -) { - const { hotelId, language, include, isCardOnlyPayment } = input +export const getHotelData = cache( + async (input: HotelDataInput, serviceToken: string) => { + const { hotelId, language, isCardOnlyPayment } = input - const params: Record = { - hotelId, - language, - } - - if (include) { - params.include = include.join(",") - } - - getHotelCounter.add(1, { - hotelId, - language, - include, - }) - console.info( - "api.hotels.hotelData start", - JSON.stringify({ query: { hotelId, params } }) - ) - - const apiResponse = await api.get( - api.endpoints.v1.Hotel.Hotels.hotel(hotelId), - { - headers: { - Authorization: `Bearer ${serviceToken}`, - }, - }, - params - ) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - getHotelFailCounter.add(1, { + const params: Record = { + hotelId, + language, + } + + params.include = HotelIncludeEnum.options.join(",") + + getHotelCounter.add(1, { hotelId, language, - include, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }), }) - console.error( - "api.hotels.hotelData error", - JSON.stringify({ - query: { hotelId, params }, - error: { + console.info( + "api.hotels.hotelData start", + JSON.stringify({ query: { hotelId, params } }) + ) + + const apiResponse = await api.get( + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), + { + headers: { + Authorization: `Bearer ${serviceToken}`, + }, + // needs to clear default option as only + // cache or next.revalidate is permitted + cache: undefined, + next: { + revalidate: 60 * 30, // 30 minutes + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + getHotelFailCounter.add(1, { + hotelId, + language, + error_type: "http_error", + error: JSON.stringify({ status: apiResponse.status, statusText: apiResponse.statusText, text, - }, + }), }) - ) - return null - } - - const apiJson = await apiResponse.json() - const validateHotelData = getHotelDataSchema.safeParse(apiJson) - - if (!validateHotelData.success) { - getHotelFailCounter.add(1, { - hotelId, - language, - include, - error_type: "validation_error", - error: JSON.stringify(validateHotelData.error), - }) - - console.error( - "api.hotels.hotelData validation error", - JSON.stringify({ - query: { hotelId, params }, - error: validateHotelData.error, - }) - ) - throw badRequestError() - } - - getHotelSuccessCounter.add(1, { - hotelId, - language, - include, - }) - console.info( - "api.hotels.hotelData success", - JSON.stringify({ - query: { hotelId, params: params }, - }) - ) - - if (isCardOnlyPayment) { - validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions = - [] - } - - return validateHotelData.data -} - -export const hotelQueryRouter = router({ - get: contentStackUidWithServiceProcedure - .input(getHotelInputSchema) - .query(async ({ ctx, input }) => { - const { lang, uid } = ctx - const { include } = input - - const contentstackData = await getContentstackData(lang, uid) - const hotelId = contentstackData?.hotel_page_id - - if (!hotelId) { - throw notFound(`Hotel not found for uid: ${uid}`) - } - - const apiLang = toApiLang(lang) - const params: Record = { - hotelId, - language: apiLang, - } - - if (include) { - params.include = include.join(",") - } - - getHotelCounter.add(1, { hotelId, lang, include }) - console.info( - "api.hotels.hotel start", + console.error( + "api.hotels.hotelData error", JSON.stringify({ query: { hotelId, params }, - }) - ) - const apiResponse = await api.get( - api.endpoints.v1.Hotel.Hotels.hotel(hotelId), - { - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - }, - params - ) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - getHotelFailCounter.add(1, { - hotelId, - lang, - include, - error_type: "http_error", - error: JSON.stringify({ + error: { status: apiResponse.status, statusText: apiResponse.statusText, text, - }), - }) - console.error( - "api.hotels.hotel error", - JSON.stringify({ - query: { hotelId, params }, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }, - }) - ) - throw serverErrorByStatus(apiResponse.status, apiResponse) - } - const apiJson = await apiResponse.json() - const validatedHotelData = getHotelDataSchema.safeParse(apiJson) - - if (!validatedHotelData.success) { - getHotelFailCounter.add(1, { - hotelId, - lang, - include, - error_type: "validation_error", - error: JSON.stringify(validatedHotelData.error), - }) - - console.error( - "api.hotels.hotel validation error", - JSON.stringify({ - query: { hotelId, params }, - error: validatedHotelData.error, - }) - ) - throw badRequestError() - } - - const included = validatedHotelData.data.included || [] - - const hotelAttributes = validatedHotelData.data.data.attributes - const images = hotelAttributes.gallery?.smallerImages - const hotelAlerts = hotelAttributes.meta?.specialAlerts || [] - - const roomCategories = included - ? included.filter((item) => item.type === "roomcategories") - : [] - - const activities = contentstackData?.content - ? contentstackData?.content[0] - : null - - const facilities: Facility[] = [ - { - ...apiJson.data.attributes.restaurantImages, - id: FacilityCardTypeEnum.restaurant, - }, - { - ...apiJson.data.attributes.conferencesAndMeetings, - id: FacilityCardTypeEnum.conference, - }, - { - ...apiJson.data.attributes.healthAndWellness, - id: FacilityCardTypeEnum.wellness, - }, - ] - - getHotelSuccessCounter.add(1, { hotelId, lang, include }) - console.info( - "api.hotels.hotel success", - JSON.stringify({ - query: { hotelId, params: params }, + }, }) ) - return { - hotelName: hotelAttributes.name, - hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short, - hotelLocation: hotelAttributes.location, - hotelAddress: hotelAttributes.address, - hotelRatings: hotelAttributes.ratings, - hotelDetailedFacilities: hotelAttributes.detailedFacilities, - hotelImages: images, - pointsOfInterest: hotelAttributes.pointsOfInterest, - roomCategories, - activitiesCard: activities?.upcoming_activities_card, - facilities, - alerts: hotelAlerts, - faq: contentstackData?.faq, - } - }), + return null + } + + const apiJson = await apiResponse.json() + const validateHotelData = getHotelDataSchema.safeParse(apiJson) + + if (!validateHotelData.success) { + getHotelFailCounter.add(1, { + hotelId, + language, + error_type: "validation_error", + error: JSON.stringify(validateHotelData.error), + }) + + console.error( + "api.hotels.hotelData validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validateHotelData.error, + }) + ) + throw badRequestError() + } + + getHotelSuccessCounter.add(1, { + hotelId, + language, + }) + console.info( + "api.hotels.hotelData success", + JSON.stringify({ + query: { hotelId, params: params }, + }) + ) + + if (isCardOnlyPayment) { + validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions = + [] + } + + return validateHotelData.data + } +) + +export const hotelQueryRouter = router({ + get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => { + const { lang, uid } = ctx + + const contentstackData = await getContentstackData(lang, uid) + const hotelId = contentstackData?.hotel_page_id + + if (!hotelId) { + throw notFound(`Hotel not found for uid: ${uid}`) + } + + const apiLang = toApiLang(lang) + const params: Record = { + hotelId, + language: apiLang, + } + + params.include = HotelIncludeEnum.options.join(",") + + getHotelCounter.add(1, { hotelId, lang }) + console.info( + "api.hotels.hotel start", + JSON.stringify({ + query: { hotelId, params }, + }) + ) + const apiResponse = await api.get( + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + // needs to clear default option as only + // cache or next.revalidate is permitted + cache: undefined, + next: { + revalidate: 60 * 30, // 30 minutes + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + getHotelFailCounter.add(1, { + hotelId, + lang, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.hotels.hotel error", + JSON.stringify({ + query: { hotelId, params }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + const apiJson = await apiResponse.json() + const validatedHotelData = getHotelDataSchema.safeParse(apiJson) + + if (!validatedHotelData.success) { + getHotelFailCounter.add(1, { + hotelId, + lang, + error_type: "validation_error", + error: JSON.stringify(validatedHotelData.error), + }) + + console.error( + "api.hotels.hotel validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validatedHotelData.error, + }) + ) + throw badRequestError() + } + + const included = validatedHotelData.data.included || [] + + const hotelAttributes = validatedHotelData.data.data.attributes + const images = hotelAttributes.gallery?.smallerImages + const hotelAlerts = hotelAttributes.meta?.specialAlerts || [] + + const roomCategories = included + ? included.filter((item) => item.type === "roomcategories") + : [] + + const activities = contentstackData?.content + ? contentstackData?.content[0] + : null + + const facilities: Facility[] = [ + { + ...apiJson.data.attributes.restaurantImages, + id: FacilityCardTypeEnum.restaurant, + }, + { + ...apiJson.data.attributes.conferencesAndMeetings, + id: FacilityCardTypeEnum.conference, + }, + { + ...apiJson.data.attributes.healthAndWellness, + id: FacilityCardTypeEnum.wellness, + }, + ] + + getHotelSuccessCounter.add(1, { hotelId, lang }) + console.info( + "api.hotels.hotel success", + JSON.stringify({ + query: { hotelId, params: params }, + }) + ) + return { + hotelName: hotelAttributes.name, + hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short, + hotelLocation: hotelAttributes.location, + hotelAddress: hotelAttributes.address, + hotelRatings: hotelAttributes.ratings, + hotelDetailedFacilities: hotelAttributes.detailedFacilities, + hotelImages: images, + pointsOfInterest: hotelAttributes.pointsOfInterest, + roomCategories, + activitiesCard: activities?.upcoming_activities_card, + facilities, + alerts: hotelAlerts, + faq: contentstackData?.faq, + } + }), availability: router({ hotels: serviceProcedure .input(getHotelsAvailabilityInputSchema) @@ -773,7 +771,6 @@ export const hotelQueryRouter = router({ { hotelId, language: ctx.lang, - include: [HotelIncludeEnum.RoomCategories], }, ctx.serviceToken )