fix: add args as keyParts for unstable_cache
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
|
||||
@@ -38,7 +37,6 @@ import {
|
||||
getCitiesByCountry,
|
||||
getCountries,
|
||||
getLocations,
|
||||
locationsAffix,
|
||||
TWENTYFOUR_HOURS,
|
||||
} from "./utils"
|
||||
|
||||
@@ -657,36 +655,19 @@ export const hotelQueryRouter = router({
|
||||
},
|
||||
}
|
||||
|
||||
const getCachedCountries = unstable_cache(
|
||||
getCountries,
|
||||
[`${ctx.lang}:${locationsAffix}:countries`],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)
|
||||
|
||||
const countries = await getCachedCountries(options, searchParams)
|
||||
|
||||
const getCachedCitiesByCountry = unstable_cache(
|
||||
getCitiesByCountry,
|
||||
[`${ctx.lang}:${locationsAffix}:cities-by-country`],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)
|
||||
const countries = await getCountries(options, searchParams, ctx.lang)
|
||||
|
||||
let citiesByCountry = null
|
||||
if (countries) {
|
||||
citiesByCountry = await getCachedCitiesByCountry(
|
||||
citiesByCountry = await getCitiesByCountry(
|
||||
countries,
|
||||
options,
|
||||
searchParams
|
||||
searchParams,
|
||||
ctx.lang
|
||||
)
|
||||
}
|
||||
|
||||
const getCachedLocations = unstable_cache(
|
||||
getLocations,
|
||||
[`${ctx.lang}:${locationsAffix}`],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)
|
||||
|
||||
const locations = await getCachedLocations(
|
||||
const locations = await getLocations(
|
||||
ctx.lang,
|
||||
options,
|
||||
searchParams,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
PointOfInterestCategoryNameEnum,
|
||||
PointOfInterestGroupEnum,
|
||||
} from "@/types/hotel"
|
||||
import { HotelLocation } from "@/types/trpc/routers/hotel/locations"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
|
||||
@@ -54,89 +55,119 @@ export const locationsAffix = "locations"
|
||||
export const TWENTYFOUR_HOURS = 60 * 60 * 24
|
||||
export async function getCity(
|
||||
cityUrl: string,
|
||||
options: RequestOptionsWithOutBody
|
||||
options: RequestOptionsWithOutBody,
|
||||
lang: Lang,
|
||||
relationshipCity: HotelLocation["relationships"]["city"]
|
||||
) {
|
||||
const url = new URL(cityUrl)
|
||||
const cityResponse = await api.get(
|
||||
url.pathname as Endpoint,
|
||||
options,
|
||||
url.searchParams
|
||||
)
|
||||
return unstable_cache(
|
||||
async function (locationCityUrl: string) {
|
||||
const url = new URL(locationCityUrl)
|
||||
const cityResponse = await api.get(
|
||||
url.pathname as Endpoint,
|
||||
options,
|
||||
url.searchParams
|
||||
)
|
||||
|
||||
if (!cityResponse.ok) {
|
||||
return null
|
||||
}
|
||||
if (!cityResponse.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const cityJson = await cityResponse.json()
|
||||
const city = apiCitySchema.safeParse(cityJson)
|
||||
if (!city.success) {
|
||||
console.info(`Validation of city failed`)
|
||||
console.info(`cityUrl: ${cityUrl}`)
|
||||
console.error(city.error)
|
||||
return null
|
||||
}
|
||||
const cityJson = await cityResponse.json()
|
||||
const city = apiCitySchema.safeParse(cityJson)
|
||||
if (!city.success) {
|
||||
console.info(`Validation of city failed`)
|
||||
console.info(`cityUrl: ${locationCityUrl}`)
|
||||
console.error(city.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return city.data
|
||||
return city.data
|
||||
},
|
||||
[cityUrl, `${lang}:${relationshipCity}`],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(cityUrl)
|
||||
}
|
||||
|
||||
export async function getCountries(
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams
|
||||
params: URLSearchParams,
|
||||
lang: Lang
|
||||
) {
|
||||
const countryResponse = await api.get(
|
||||
api.endpoints.v1.countries,
|
||||
options,
|
||||
params
|
||||
)
|
||||
|
||||
if (!countryResponse.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const countriesJson = await countryResponse.json()
|
||||
const countries = apiCountriesSchema.safeParse(countriesJson)
|
||||
if (!countries.success) {
|
||||
console.info(`Validation for countries failed`)
|
||||
console.error(countries.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return countries.data
|
||||
}
|
||||
|
||||
export async function getCitiesByCountry(
|
||||
countries: Countries,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams
|
||||
) {
|
||||
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
|
||||
|
||||
await Promise.all(
|
||||
countries.data.map(async (country) => {
|
||||
return unstable_cache(
|
||||
async function (searchParams) {
|
||||
const countryResponse = await api.get(
|
||||
`${api.endpoints.v1.citiesCountry}/${country.name}`,
|
||||
api.endpoints.v1.countries,
|
||||
options,
|
||||
params
|
||||
searchParams
|
||||
)
|
||||
|
||||
if (!countryResponse.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const countryJson = await countryResponse.json()
|
||||
const citiesByCountry = apiCitiesByCountrySchema.safeParse(countryJson)
|
||||
if (!citiesByCountry.success) {
|
||||
console.info(`Failed to validate Cities by Country payload`)
|
||||
console.error(citiesByCountry.error)
|
||||
const countriesJson = await countryResponse.json()
|
||||
const countries = apiCountriesSchema.safeParse(countriesJson)
|
||||
if (!countries.success) {
|
||||
console.info(`Validation for countries failed`)
|
||||
console.error(countries.error)
|
||||
return null
|
||||
}
|
||||
|
||||
citiesGroupedByCountry[country.name] = citiesByCountry.data.data
|
||||
return true
|
||||
})
|
||||
)
|
||||
return countries.data
|
||||
},
|
||||
[`${lang}:${locationsAffix}:countries`, params.toString()],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params)
|
||||
}
|
||||
|
||||
return citiesGroupedByCountry
|
||||
export async function getCitiesByCountry(
|
||||
countries: Countries,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
lang: Lang
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (
|
||||
searchParams: URLSearchParams,
|
||||
searchedCountries: Countries
|
||||
) {
|
||||
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
|
||||
|
||||
await Promise.all(
|
||||
searchedCountries.data.map(async (country) => {
|
||||
const countryResponse = await api.get(
|
||||
`${api.endpoints.v1.citiesCountry}/${country.name}`,
|
||||
options,
|
||||
searchParams
|
||||
)
|
||||
|
||||
if (!countryResponse.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const countryJson = await countryResponse.json()
|
||||
const citiesByCountry =
|
||||
apiCitiesByCountrySchema.safeParse(countryJson)
|
||||
if (!citiesByCountry.success) {
|
||||
console.info(`Failed to validate Cities by Country payload`)
|
||||
console.error(citiesByCountry.error)
|
||||
return null
|
||||
}
|
||||
|
||||
citiesGroupedByCountry[country.name] = citiesByCountry.data.data
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
return citiesGroupedByCountry
|
||||
},
|
||||
[
|
||||
`${lang}:${locationsAffix}:cities-by-country`,
|
||||
params.toString(),
|
||||
JSON.stringify(countries),
|
||||
],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params, countries)
|
||||
}
|
||||
|
||||
export async function getLocations(
|
||||
@@ -145,72 +176,89 @@ export async function getLocations(
|
||||
params: URLSearchParams,
|
||||
citiesByCountry: CitiesGroupedByCountry | null
|
||||
) {
|
||||
const apiResponse = await api.get(api.endpoints.v1.locations, options, params)
|
||||
return unstable_cache(
|
||||
async function (
|
||||
searchParams: URLSearchParams,
|
||||
groupedCitiesByCountry: CitiesGroupedByCountry | null
|
||||
) {
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.locations,
|
||||
options,
|
||||
searchParams
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
if (apiResponse.status === 401) {
|
||||
return { error: true, cause: "unauthorized" } as const
|
||||
} else if (apiResponse.status === 403) {
|
||||
return { error: true, cause: "forbidden" } as const
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedLocations = apiLocationsSchema.safeParse(apiJson)
|
||||
if (!verifiedLocations.success) {
|
||||
console.info(`Locations Verification Failed`)
|
||||
console.error(verifiedLocations.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return await Promise.all(
|
||||
verifiedLocations.data.data.map(async (location) => {
|
||||
if (location.type === "cities") {
|
||||
if (citiesByCountry) {
|
||||
const country = Object.keys(citiesByCountry).find((country) => {
|
||||
if (
|
||||
citiesByCountry[country].find((loc) => loc.name === location.name)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if (country) {
|
||||
return {
|
||||
...location,
|
||||
country,
|
||||
}
|
||||
} else {
|
||||
console.info(
|
||||
`Location cannot be found in any of the countries cities`
|
||||
)
|
||||
console.info(location)
|
||||
}
|
||||
}
|
||||
} else if (location.type === "hotels") {
|
||||
if (location.relationships.city?.url) {
|
||||
const getCachedCity = unstable_cache(
|
||||
getCity,
|
||||
[`${lang}:${location.relationships.city}`],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)
|
||||
|
||||
const city = await getCachedCity(
|
||||
location.relationships.city.url,
|
||||
options
|
||||
)
|
||||
if (city) {
|
||||
return deepmerge(location, {
|
||||
relationships: {
|
||||
city,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!apiResponse.ok) {
|
||||
if (apiResponse.status === 401) {
|
||||
return { error: true, cause: "unauthorized" } as const
|
||||
} else if (apiResponse.status === 403) {
|
||||
return { error: true, cause: "forbidden" } as const
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return location
|
||||
})
|
||||
)
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedLocations = apiLocationsSchema.safeParse(apiJson)
|
||||
if (!verifiedLocations.success) {
|
||||
console.info(`Locations Verification Failed`)
|
||||
console.error(verifiedLocations.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return await Promise.all(
|
||||
verifiedLocations.data.data.map(async (location) => {
|
||||
if (location.type === "cities") {
|
||||
if (groupedCitiesByCountry) {
|
||||
const country = Object.keys(groupedCitiesByCountry).find(
|
||||
(country) => {
|
||||
if (
|
||||
groupedCitiesByCountry[country].find(
|
||||
(loc) => loc.name === location.name
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
)
|
||||
if (country) {
|
||||
return {
|
||||
...location,
|
||||
country,
|
||||
}
|
||||
} else {
|
||||
console.info(
|
||||
`Location cannot be found in any of the countries cities`
|
||||
)
|
||||
console.info(location)
|
||||
}
|
||||
}
|
||||
} else if (location.type === "hotels") {
|
||||
if (location.relationships.city?.url) {
|
||||
const city = await getCity(
|
||||
location.relationships.city.url,
|
||||
options,
|
||||
lang,
|
||||
location.relationships.city
|
||||
)
|
||||
if (city) {
|
||||
return deepmerge(location, {
|
||||
relationships: {
|
||||
city,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return location
|
||||
})
|
||||
)
|
||||
},
|
||||
[
|
||||
`${lang}:${locationsAffix}`,
|
||||
params.toString(),
|
||||
JSON.stringify(citiesByCountry),
|
||||
],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params, citiesByCountry)
|
||||
}
|
||||
|
||||
@@ -5,24 +5,22 @@ import { env } from "@/env/server"
|
||||
|
||||
import { generateServiceTokenTag } from "@/utils/generateTag"
|
||||
|
||||
import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken"
|
||||
import { ServiceTokenResponse } from "@/types/tokens"
|
||||
|
||||
// OpenTelemetry metrics: Service token
|
||||
const meter = metrics.getMeter("trpc.context.serviceToken")
|
||||
const getServiceTokenCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.get-new-token"
|
||||
const fetchServiceTokenCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-new-token"
|
||||
)
|
||||
const getTempServiceTokenCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.get-temporary"
|
||||
const fetchTempServiceTokenCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-temporary"
|
||||
)
|
||||
const getServiceTokenFailCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.get-fail"
|
||||
const fetchServiceTokenFailCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-fail"
|
||||
)
|
||||
|
||||
async function getServiceToken() {
|
||||
getServiceTokenCounter.add(1)
|
||||
const scopes = Object.keys(ServiceTokenScopeEnum)
|
||||
async function fetchServiceToken(scopes: string[]) {
|
||||
fetchServiceTokenCounter.add(1)
|
||||
const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -38,7 +36,7 @@ async function getServiceToken() {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
getServiceTokenFailCounter.add(1, {
|
||||
fetchServiceTokenFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: response.status,
|
||||
@@ -51,21 +49,22 @@ async function getServiceToken() {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function fetchServiceToken(): Promise<ServiceTokenResponse> {
|
||||
export async function getServiceToken(): Promise<ServiceTokenResponse> {
|
||||
try {
|
||||
const tag = generateServiceTokenTag()
|
||||
const scopes = ["profile", "hotel", "booking"]
|
||||
const tag = generateServiceTokenTag(scopes)
|
||||
const getCachedJwt = unstable_cache(
|
||||
async () => {
|
||||
const jwt = await getServiceToken()
|
||||
async (scopes) => {
|
||||
const jwt = await fetchServiceToken(scopes)
|
||||
|
||||
const expiresAt = Date.now() + jwt.expires_in * 1000
|
||||
return { expiresAt, jwt }
|
||||
},
|
||||
[],
|
||||
[tag],
|
||||
{ tags: [tag] }
|
||||
)
|
||||
|
||||
const cachedJwt = await getCachedJwt()
|
||||
const cachedJwt = await getCachedJwt(scopes)
|
||||
if (cachedJwt.expiresAt < Date.now()) {
|
||||
console.log(
|
||||
"trpc.context.serviceToken: Service token expired, revalidating tag"
|
||||
@@ -75,8 +74,8 @@ export async function fetchServiceToken(): Promise<ServiceTokenResponse> {
|
||||
console.log(
|
||||
"trpc.context.serviceToken: Fetching new temporary service token."
|
||||
)
|
||||
getTempServiceTokenCounter.add(1)
|
||||
const newToken = await getServiceToken()
|
||||
fetchTempServiceTokenCounter.add(1)
|
||||
const newToken = await fetchServiceToken(scopes)
|
||||
return newToken
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,12 @@ import {
|
||||
unauthorizedError,
|
||||
} from "./errors/trpc"
|
||||
import { type Context, createContext } from "./context"
|
||||
import { fetchServiceToken } from "./tokenManager"
|
||||
import { getServiceToken } from "./tokenManager"
|
||||
import { transformer } from "./transformer"
|
||||
import { langInput } from "./utils"
|
||||
|
||||
import type { Session } from "next-auth"
|
||||
|
||||
import {
|
||||
ServiceTokenScope,
|
||||
ServiceTokenScopeEnum,
|
||||
} from "@/types/enums/serviceToken"
|
||||
import type { Meta } from "@/types/trpc/meta"
|
||||
|
||||
const t = initTRPC
|
||||
@@ -126,7 +122,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) {
|
||||
})
|
||||
|
||||
export const serviceProcedure = t.procedure.use(async (opts) => {
|
||||
const { access_token } = await fetchServiceToken()
|
||||
const { access_token } = await getServiceToken()
|
||||
if (!access_token) {
|
||||
throw internalServerError(`Failed to obtain service token`)
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export enum ServiceTokenScopeEnum {
|
||||
profile = "profile",
|
||||
hotel = "hotel",
|
||||
booking = "booking",
|
||||
}
|
||||
|
||||
export type ServiceTokenScope = keyof typeof ServiceTokenScopeEnum
|
||||
@@ -6,3 +6,6 @@ export interface LocationSchema extends z.output<typeof apiLocationsSchema> {}
|
||||
|
||||
export type Locations = LocationSchema["data"]
|
||||
export type Location = Locations[number]
|
||||
|
||||
export type CityLocation = Location & { type: "cities" }
|
||||
export type HotelLocation = Location & { type: "hotels" }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken"
|
||||
import { System } from "@/types/requests/system"
|
||||
import type { Edges } from "@/types/requests/utils/edges"
|
||||
import type { NodeRefs } from "@/types/requests/utils/refs"
|
||||
@@ -107,7 +106,6 @@ export function generateLoyaltyConfigTag(
|
||||
* @param serviceTokenScope scope of service token
|
||||
* @returns string
|
||||
*/
|
||||
export function generateServiceTokenTag() {
|
||||
const scopes = Object.keys(ServiceTokenScopeEnum).join("-")
|
||||
return `service_token:${scopes}`
|
||||
export function generateServiceTokenTag(scopes: string[]) {
|
||||
return `service_token:${scopes.join("-")}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user