fix: add args as keyParts for unstable_cache

This commit is contained in:
Christel Westerberg
2024-10-09 11:27:16 +02:00
parent 71b03143ce
commit 95ff011bdb
7 changed files with 204 additions and 186 deletions

View File

@@ -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,

View File

@@ -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,9 +55,13 @@ 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)
return unstable_cache(
async function (locationCityUrl: string) {
const url = new URL(locationCityUrl)
const cityResponse = await api.get(
url.pathname as Endpoint,
options,
@@ -71,22 +76,29 @@ export async function getCity(
const city = apiCitySchema.safeParse(cityJson)
if (!city.success) {
console.info(`Validation of city failed`)
console.info(`cityUrl: ${cityUrl}`)
console.info(`cityUrl: ${locationCityUrl}`)
console.error(city.error)
return null
}
return city.data
},
[cityUrl, `${lang}:${relationshipCity}`],
{ revalidate: TWENTYFOUR_HOURS }
)(cityUrl)
}
export async function getCountries(
options: RequestOptionsWithOutBody,
params: URLSearchParams
params: URLSearchParams,
lang: Lang
) {
return unstable_cache(
async function (searchParams) {
const countryResponse = await api.get(
api.endpoints.v1.countries,
options,
params
searchParams
)
if (!countryResponse.ok) {
@@ -102,21 +114,31 @@ export async function getCountries(
}
return countries.data
},
[`${lang}:${locationsAffix}:countries`, params.toString()],
{ revalidate: TWENTYFOUR_HOURS }
)(params)
}
export async function getCitiesByCountry(
countries: Countries,
options: RequestOptionsWithOutBody,
params: URLSearchParams
params: URLSearchParams,
lang: Lang
) {
return unstable_cache(
async function (
searchParams: URLSearchParams,
searchedCountries: Countries
) {
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
await Promise.all(
countries.data.map(async (country) => {
searchedCountries.data.map(async (country) => {
const countryResponse = await api.get(
`${api.endpoints.v1.citiesCountry}/${country.name}`,
options,
params
searchParams
)
if (!countryResponse.ok) {
@@ -124,7 +146,8 @@ export async function getCitiesByCountry(
}
const countryJson = await countryResponse.json()
const citiesByCountry = apiCitiesByCountrySchema.safeParse(countryJson)
const citiesByCountry =
apiCitiesByCountrySchema.safeParse(countryJson)
if (!citiesByCountry.success) {
console.info(`Failed to validate Cities by Country payload`)
console.error(citiesByCountry.error)
@@ -137,6 +160,14 @@ export async function getCitiesByCountry(
)
return citiesGroupedByCountry
},
[
`${lang}:${locationsAffix}:cities-by-country`,
params.toString(),
JSON.stringify(countries),
],
{ revalidate: TWENTYFOUR_HOURS }
)(params, countries)
}
export async function getLocations(
@@ -145,7 +176,16 @@ 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) {
@@ -167,15 +207,19 @@ export async function getLocations(
return await Promise.all(
verifiedLocations.data.data.map(async (location) => {
if (location.type === "cities") {
if (citiesByCountry) {
const country = Object.keys(citiesByCountry).find((country) => {
if (groupedCitiesByCountry) {
const country = Object.keys(groupedCitiesByCountry).find(
(country) => {
if (
citiesByCountry[country].find((loc) => loc.name === location.name)
groupedCitiesByCountry[country].find(
(loc) => loc.name === location.name
)
) {
return true
}
return false
})
}
)
if (country) {
return {
...location,
@@ -190,15 +234,11 @@ export async function getLocations(
}
} 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(
const city = await getCity(
location.relationships.city.url,
options
options,
lang,
location.relationships.city
)
if (city) {
return deepmerge(location, {
@@ -213,4 +253,12 @@ export async function getLocations(
return location
})
)
},
[
`${lang}:${locationsAffix}`,
params.toString(),
JSON.stringify(citiesByCountry),
],
{ revalidate: TWENTYFOUR_HOURS }
)(params, citiesByCountry)
}

View File

@@ -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
}

View File

@@ -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`)
}

View File

@@ -1,7 +0,0 @@
export enum ServiceTokenScopeEnum {
profile = "profile",
hotel = "hotel",
booking = "booking",
}
export type ServiceTokenScope = keyof typeof ServiceTokenScopeEnum

View File

@@ -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" }

View File

@@ -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("-")}`
}