Merged in feature/warmup (pull request #1887)

* unified warmup function

Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-04-29 06:18:14 +00:00
parent bbbd665a32
commit c1505ce50e
33 changed files with 886 additions and 185 deletions
+5 -1
View File
@@ -1,3 +1,5 @@
import type { CacheOrGetOptions } from "./cacheOrGetOptions"
const ONE_HOUR_IN_SECONDS = 3_600 as const
const ONE_DAY_IN_SECONDS = 86_400 as const
@@ -60,6 +62,7 @@ export type DataCache = {
* @param key The cache key
* @param getDataFromSource An async function that provides a value to cache
* @param ttl Time to live, either a named cache time or a number of seconds
* @param opts Options to control cache behavior when retrieving or storing data.
* @returns The cached value or the result from the callback
*/
cacheOrGet: <T>(
@@ -67,7 +70,8 @@ export type DataCache = {
getDataFromSource: (
overrideTTL?: (cacheTime: CacheTime) => void
) => Promise<T>,
ttl: CacheTime
ttl: CacheTime,
opts?: CacheOrGetOptions
) => Promise<T>
/**
@@ -1,5 +1,9 @@
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
import {
type CacheOrGetOptions,
shouldGetFromCache,
} from "../cacheOrGetOptions"
import { cacheLogger } from "../logger"
import { generateCacheKey } from "./generateCacheKey"
import { get } from "./get"
@@ -8,10 +12,15 @@ import { set } from "./set"
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
key: string | string[],
callback: (overrideTTL: (cacheTime: CacheTime) => void) => Promise<T>,
ttl: CacheTime
ttl: CacheTime,
opts?: CacheOrGetOptions
) => {
const cacheKey = generateCacheKey(key)
const cachedValue = await get<T>(cacheKey)
let cachedValue: Awaited<T> | undefined = undefined
if (shouldGetFromCache(opts)) {
cachedValue = await get<T>(cacheKey)
}
let realTTL = ttl
@@ -1,13 +1,18 @@
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
import { cacheLogger } from "@/services/dataCache/logger"
import {
type CacheOrGetOptions,
shouldGetFromCache,
} from "../../cacheOrGetOptions"
import { get } from "./get"
import { set } from "./set"
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
key: string | string[],
callback: (overrideTTL?: (cacheTime: CacheTime) => void) => Promise<T>,
ttl: CacheTime
ttl: CacheTime,
opts?: CacheOrGetOptions
): Promise<T> => {
if (Array.isArray(key)) {
key = key.join("-")
@@ -18,14 +23,16 @@ export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
realTTL = cacheTime
}
const cached = await get(key)
let cached: Awaited<T> | undefined = undefined
if (shouldGetFromCache(opts)) {
cached = await get(key)
if (cached) {
return cached
}
if (cached) {
return cached as T
cacheLogger.debug(`Miss for key '${key}'`)
}
cacheLogger.debug(`Miss for key '${key}'`)
try {
const data = await callback(overrideTTL)
await set(key, data, realTTL)
@@ -1,4 +1,4 @@
import { unstable_cache } from "next/cache"
import { revalidateTag, unstable_cache } from "next/cache"
import {
type CacheTime,
@@ -6,18 +6,26 @@ import {
getCacheTimeInSeconds,
} from "@/services/dataCache/Cache"
import {
type CacheOrGetOptions,
shouldGetFromCache,
} from "../../cacheOrGetOptions"
import { cacheLogger } from "../../logger"
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
key: string | string[],
callback: () => Promise<T>,
ttl: CacheTime
ttl: CacheTime,
opts?: CacheOrGetOptions
): Promise<T> => {
if (!Array.isArray(key)) {
key = [key]
}
const perf = performance.now()
if (!shouldGetFromCache(opts)) {
revalidateTag(key[0])
}
const res = await unstable_cache(callback, key, {
revalidate: getCacheTimeInSeconds(ttl),
@@ -0,0 +1,27 @@
/**
* Options to control cache behavior when retrieving or storing data.
*
* - "cache-first": Default behaviour, check if the needed data is available in the cache first. If the data is found, it is returned immediately. Otherwise, the data is fetched and then cached.
* - "fetch-then-cache": Always fetch the data first, and then update the cache with the freshly fetched data.
*/
export type CacheStrategy = "cache-first" | "fetch-then-cache"
export type CacheOrGetOptions = {
cacheStrategy?: CacheStrategy
}
export function defaultCacheOrGetOptions(
opts: CacheOrGetOptions = {}
): CacheOrGetOptions {
return {
cacheStrategy: "cache-first",
...opts,
}
}
export function shouldGetFromCache(
opts: CacheOrGetOptions | undefined
): boolean {
opts = defaultCacheOrGetOptions(opts)
return opts.cacheStrategy === "cache-first"
}
+1 -2
View File
@@ -3,11 +3,10 @@ import { env } from "@/env/server"
import { isEdge } from "@/utils/isEdge"
import { createMemoryCache } from "./MemoryCache/createMemoryCache"
import { type DataCache } from "./Cache"
import { createDistributedCache } from "./DistributedCache"
import { cacheLogger } from "./logger"
import type { DataCache } from "./Cache"
export type { CacheTime, DataCache } from "./Cache"
export async function getCacheClient(): Promise<DataCache> {
+50
View File
@@ -0,0 +1,50 @@
import { Lang } from "@/constants/languages"
import { warmupCountry } from "./warmupCountries"
import { warmupHotelData } from "./warmupHotelData"
import { warmupHotelIdsByCountry } from "./warmupHotelIdsByCountry"
import type { WarmupFunctionsKey } from "./warmupKeys"
export type WarmupFunction = () => Promise<WarmupResult>
type BaseWarmup = {
status: "skipped" | "completed"
}
type FailedWarmup = {
status: "error"
error: Error
}
export type WarmupResult = BaseWarmup | FailedWarmup
export const warmupFunctions: Record<WarmupFunctionsKey, WarmupFunction> = {
countries_en: warmupCountry(Lang.en),
countries_da: warmupCountry(Lang.da),
countries_de: warmupCountry(Lang.de),
countries_fi: warmupCountry(Lang.fi),
countries_sv: warmupCountry(Lang.sv),
countries_no: warmupCountry(Lang.no),
hotelsByCountry: warmupHotelIdsByCountry(),
hotelData_en: warmupHotelData(Lang.en),
hotelData_da: warmupHotelData(Lang.da),
hotelData_de: warmupHotelData(Lang.de),
hotelData_fi: warmupHotelData(Lang.fi),
hotelData_sv: warmupHotelData(Lang.sv),
hotelData_no: warmupHotelData(Lang.no),
}
export async function warmup(key: WarmupFunctionsKey): Promise<WarmupResult> {
const func = warmupFunctions[key]
if (!func) {
return {
status: "error",
error: new Error(`Warmup function ${key} not found`),
}
}
return func()
}
@@ -0,0 +1,28 @@
import { getCountries } from "@/server/routers/hotels/utils"
import { getServiceToken } from "@/server/tokenManager"
import type { Lang } from "@/constants/languages"
import type { WarmupFunction, WarmupResult } from "."
export const warmupCountry =
(lang: Lang): WarmupFunction =>
async (): Promise<WarmupResult> => {
try {
const serviceToken = await getServiceToken()
await getCountries({
lang: lang,
serviceToken: serviceToken.access_token,
warmup: true,
})
} catch (error) {
return {
status: "error",
error: error as Error,
}
}
return {
status: "completed",
}
}
@@ -0,0 +1,31 @@
import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
import type { Lang } from "@/constants/languages"
import type { WarmupFunction, WarmupResult } from "."
export const warmupHotelData =
(lang: Lang): WarmupFunction =>
async (): Promise<WarmupResult> => {
if (!env.ENABLE_WARMUP_HOTEL) {
return {
status: "skipped",
}
}
try {
await serverClient().hotel.hotels.getDestinationsMapData({
lang,
warmup: true,
})
} catch (error) {
return {
status: "error",
error: error as Error,
}
}
return {
status: "completed",
}
}
@@ -0,0 +1,60 @@
import { Lang } from "@/constants/languages"
import {
getCountries,
getHotelIdsByCountry,
} from "@/server/routers/hotels/utils"
import { getServiceToken } from "@/server/tokenManager"
import { safeTry } from "@/utils/safeTry"
import type { WarmupFunction, WarmupResult } from "."
export const warmupHotelIdsByCountry =
(): WarmupFunction => async (): Promise<WarmupResult> => {
try {
let serviceToken = await getServiceToken()
const [countries, countriesError] = await safeTry(
getCountries({
lang: Lang.en,
serviceToken: serviceToken.access_token,
warmup: true,
})
)
if (!countries || countriesError) {
return {
status: "error",
error: new Error("Unable to get countries"),
}
}
const countryNames = countries.data.map((country) => country.name)
for (const countryName of countryNames) {
serviceToken = await getServiceToken()
const [_, error] = await safeTry(
getHotelIdsByCountry({
country: countryName,
serviceToken: serviceToken.access_token,
})
)
if (error) {
console.error(
`[Warmup]: Error fetching hotel IDs for ${countryName}:`,
error
)
continue
}
}
return {
status: "completed",
}
} catch (error) {
return {
status: "error",
error: error as Error,
}
}
}
@@ -0,0 +1,20 @@
import { Lang } from "@/constants/languages"
const langs = Object.keys(Lang) as Lang[]
/*
* Keys for warmup functions, the order of the keys is the order in which they will be executed
*/
export const warmupKeys = [
...langs.map((lang) => `countries_${lang}` as const),
"hotelsByCountry",
...langs.map((lang) => `hotelData_${lang}` as const),
] as const
export type WarmupFunctionsKey = (typeof warmupKeys)[number]
export function isWarmupKey(key: unknown): key is WarmupFunctionsKey {
return (
typeof key === "string" && warmupKeys.includes(key as WarmupFunctionsKey)
)
}