Merged in fix/destinations-speed-test (pull request #1704)
Feat(destination pages): Performance improvements * fix/destinations: try cache full response * Added more caching * Removed unsed env car * wip * merge master * wip * wip * wip * Renaming Approved-by: Michael Zetterberg
This commit is contained in:
@@ -96,9 +96,10 @@ export const nearbyHotelIdsInput = z.object({
|
||||
hotelId: z.string(),
|
||||
})
|
||||
|
||||
export const getAllHotelsInput = z
|
||||
export const getDestinationsMapDataInput = z
|
||||
.object({
|
||||
lang: z.nativeEnum(Lang),
|
||||
warmup: z.boolean().optional(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
includedSchema,
|
||||
relationshipsSchema as hotelRelationshipsSchema,
|
||||
} from "./schemas/hotel"
|
||||
import { addressSchema } from "./schemas/hotel/address"
|
||||
import { detailedFacilitiesSchema } from "./schemas/hotel/detailedFacility"
|
||||
import { locationSchema } from "./schemas/hotel/location"
|
||||
import { imageSchema } from "./schemas/image"
|
||||
import { locationCitySchema } from "./schemas/location/city"
|
||||
import { locationHotelSchema } from "./schemas/location/hotel"
|
||||
import {
|
||||
@@ -657,3 +661,40 @@ export const roomFeaturesSchema = z
|
||||
.transform((data) => {
|
||||
return data.data.attributes.roomFeatures
|
||||
})
|
||||
|
||||
export const destinationPagesHotelDataSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
location: locationSchema,
|
||||
cityIdentifier: z.string().optional(),
|
||||
tripadvisor: z.number().optional(),
|
||||
detailedFacilities: detailedFacilitiesSchema,
|
||||
galleryImages: z
|
||||
.array(imageSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
|
||||
address: addressSchema,
|
||||
hotelType: z.string(),
|
||||
type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
|
||||
url: z.string().optional(),
|
||||
hotelContent: z
|
||||
.object({
|
||||
texts: z.object({
|
||||
descriptions: z.object({
|
||||
short: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
})
|
||||
.transform(({ data: { ...data } }) => {
|
||||
return {
|
||||
hotel: {
|
||||
...data,
|
||||
},
|
||||
url: data.url ?? "",
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
breakfastPackageInputSchema,
|
||||
cityCoordinatesInputSchema,
|
||||
getAdditionalDataInputSchema,
|
||||
getAllHotelsInput,
|
||||
getDestinationsMapDataInput,
|
||||
getHotelsByCityIdentifierInput,
|
||||
getHotelsByCountryInput,
|
||||
getHotelsByCSFilterInput,
|
||||
@@ -70,7 +70,7 @@ import type { BedTypeSelection } from "@/types/components/hotelReservation/enter
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData, HotelDataWithUrl } from "@/types/hotel"
|
||||
import type {
|
||||
HotelsAvailabilityInputSchema,
|
||||
HotelsByHotelIdsAvailabilityInputSchema,
|
||||
@@ -1292,45 +1292,59 @@ export const hotelQueryRouter = router({
|
||||
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
|
||||
}),
|
||||
}),
|
||||
getAllHotels: router({
|
||||
get: serviceProcedure.input(getAllHotelsInput).query(async function ({
|
||||
input,
|
||||
ctx,
|
||||
}) {
|
||||
getDestinationsMapData: serviceProcedure
|
||||
.input(getDestinationsMapDataInput)
|
||||
.query(async function ({ input, ctx }) {
|
||||
const lang = input?.lang ?? ctx.lang
|
||||
const countries = await getCountries({
|
||||
// Countries need to be in English regardless of incoming lang because
|
||||
// we use the names as input for API endpoints.
|
||||
lang: Lang.en,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
const warmup = input?.warmup ?? false
|
||||
|
||||
if (!countries) {
|
||||
throw new Error("Unable to fetch countries")
|
||||
const fetchHotels = async () => {
|
||||
const countries = await getCountries({
|
||||
// Countries need to be in English regardless of incoming lang because
|
||||
// we use the names as input for API endpoints.
|
||||
lang: Lang.en,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
|
||||
if (!countries) {
|
||||
throw new Error("Unable to fetch countries")
|
||||
}
|
||||
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
const hotelData: DestinationPagesHotelData[] = (
|
||||
await Promise.all(
|
||||
countryNames.map(async (country) => {
|
||||
const hotelIds = await getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
|
||||
const hotels = await getHotelsByHotelIds({
|
||||
hotelIds,
|
||||
lang: lang,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
return hotels
|
||||
})
|
||||
)
|
||||
).flat()
|
||||
|
||||
return hotelData
|
||||
}
|
||||
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
const hotelData: HotelDataWithUrl[] = (
|
||||
await Promise.all(
|
||||
countryNames.map(async (country) => {
|
||||
const hotelIds = await getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
if (warmup) {
|
||||
return await fetchHotels()
|
||||
}
|
||||
|
||||
const hotels = await getHotelsByHotelIds({
|
||||
hotelIds,
|
||||
lang: lang,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
return hotels
|
||||
})
|
||||
)
|
||||
).flat()
|
||||
return hotelData
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${lang}:getDestinationsMapData`,
|
||||
fetchHotels,
|
||||
"max"
|
||||
)
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
nearbyHotelIds: serviceProcedure
|
||||
.input(nearbyHotelIdsInput)
|
||||
.query(async function ({ ctx, input }) {
|
||||
@@ -1341,74 +1355,81 @@ export const hotelQueryRouter = router({
|
||||
const params: Record<string, string | number> = {
|
||||
language: apiLang,
|
||||
}
|
||||
metrics.nearbyHotelIds.counter.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.nearbyHotelIds start",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.nearbyHotelIds.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.nearbyHotelIds error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
const cacheClient = await getCacheClient()
|
||||
return cacheClient.cacheOrGet(
|
||||
`${apiLang}:nearbyHotels:${hotelId}`,
|
||||
async () => {
|
||||
metrics.nearbyHotelIds.counter.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.nearbyHotelIds start",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.nearbyHotelIds.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.nearbyHotelIds error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
|
||||
if (!validateHotelData.success) {
|
||||
metrics.nearbyHotelIds.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateHotelData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.nearbyHotelIds validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validateHotelData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
metrics.nearbyHotelIds.success.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
|
||||
if (!validateHotelData.success) {
|
||||
metrics.nearbyHotelIds.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateHotelData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.nearbyHotelIds validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validateHotelData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
metrics.nearbyHotelIds.success.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.nearbyHotelIds success",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
})
|
||||
)
|
||||
console.info(
|
||||
"api.hotels.nearbyHotelIds success",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
})
|
||||
)
|
||||
|
||||
return validateHotelData.data.map((id: string) => parseInt(id, 10))
|
||||
return validateHotelData.data.map((id: string) => parseInt(id, 10))
|
||||
},
|
||||
env.CACHE_TIME_HOTELS
|
||||
)
|
||||
}),
|
||||
locations: router({
|
||||
get: serviceProcedure.input(getLocationsInput).query(async function ({
|
||||
@@ -1459,22 +1480,29 @@ export const hotelQueryRouter = router({
|
||||
const { city, hotel } = input
|
||||
|
||||
async function fetchCoordinates(address: string) {
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`coordinates:${address}`,
|
||||
async function () {
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== "OK") {
|
||||
console.error(`Geocode error: ${data.status}`)
|
||||
return null
|
||||
}
|
||||
if (data.status !== "OK") {
|
||||
console.error(`Geocode error: ${data.status}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const location = data.results[0]?.geometry?.location
|
||||
if (!location) {
|
||||
console.error("No location found in geocode response")
|
||||
return null
|
||||
}
|
||||
const location = data.results[0]?.geometry?.location
|
||||
if (!location) {
|
||||
console.error("No location found in geocode response")
|
||||
return null
|
||||
}
|
||||
|
||||
return location
|
||||
return location
|
||||
},
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
let location = await fetchCoordinates(city)
|
||||
|
||||
@@ -24,7 +24,7 @@ import { getHotel } from "./query"
|
||||
import type { z } from "zod"
|
||||
|
||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
import type {
|
||||
CitiesGroupedByCountry,
|
||||
CityLocation,
|
||||
@@ -529,19 +529,54 @@ export async function getHotelsByHotelIds({
|
||||
lang: Lang
|
||||
serviceToken: string
|
||||
}) {
|
||||
const hotelPages = await getHotelPageUrls(lang)
|
||||
const hotels = await Promise.all(
|
||||
hotelIds.map(async (hotelId) => {
|
||||
const hotelData = await getHotel(
|
||||
{ hotelId, language: lang, isCardOnlyPayment: false },
|
||||
serviceToken
|
||||
)
|
||||
const hotelPage = hotelPages.find((page) => page.hotelId === hotelId)
|
||||
return hotelData ? { ...hotelData, url: hotelPage?.url ?? null } : null
|
||||
})
|
||||
)
|
||||
const cacheClient = await getCacheClient()
|
||||
const cacheKey = `${lang}:getHotelsByHotelIds:hotels:${hotelIds.sort().join(",")}`
|
||||
|
||||
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
|
||||
return await cacheClient.cacheOrGet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const hotelPages = await getHotelPageUrls(lang)
|
||||
const hotels = await Promise.all(
|
||||
hotelIds.map(async (hotelId) => {
|
||||
const hotelResponse = await getHotel(
|
||||
{ hotelId, language: lang, isCardOnlyPayment: false },
|
||||
serviceToken
|
||||
)
|
||||
|
||||
if (!hotelResponse) {
|
||||
throw new Error(`Hotel not found: ${hotelId}`)
|
||||
}
|
||||
|
||||
const hotelPage = hotelPages.find((page) => page.hotelId === hotelId)
|
||||
const { hotel, cities } = hotelResponse
|
||||
const data: DestinationPagesHotelData = {
|
||||
hotel: {
|
||||
id: hotel.id,
|
||||
galleryImages: hotel.galleryImages?.length
|
||||
? [hotel.galleryImages[0]]
|
||||
: [],
|
||||
name: hotel.name,
|
||||
tripadvisor: hotel.ratings?.tripAdvisor?.rating,
|
||||
detailedFacilities: hotel.detailedFacilities?.slice(0, 3) || [],
|
||||
location: hotel.location,
|
||||
hotelType: hotel.hotelType,
|
||||
type: hotel.type,
|
||||
address: hotel.address,
|
||||
cityIdentifier: cities?.[0]?.cityIdentifier,
|
||||
},
|
||||
url: hotelPage?.url ?? "",
|
||||
} satisfies DestinationPagesHotelData
|
||||
|
||||
return { ...data, url: hotelPage?.url ?? null }
|
||||
})
|
||||
)
|
||||
|
||||
return hotels.filter(
|
||||
(hotel): hotel is DestinationPagesHotelData => !!hotel
|
||||
)
|
||||
},
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
function findProduct(product: Products, rateDefinition: RateDefinition) {
|
||||
@@ -697,10 +732,13 @@ export async function getSelectedRoomAvailability(
|
||||
}
|
||||
|
||||
if (Array.isArray(product)) {
|
||||
const redemptionProduct = userPoints ? product.find(
|
||||
(r) => r.redemption.rateCode === rateDefinition.rateCode &&
|
||||
r.redemption.localPrice.pointsPerStay <= userPoints
|
||||
) : undefined
|
||||
const redemptionProduct = userPoints
|
||||
? product.find(
|
||||
(r) =>
|
||||
r.redemption.rateCode === rateDefinition.rateCode &&
|
||||
r.redemption.localPrice.pointsPerStay <= userPoints
|
||||
)
|
||||
: undefined
|
||||
if (!redemptionProduct) {
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user