diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts index 4e7cacdaaf2b..bfaf6285a7b1 100644 --- a/backend/src/api/controllers/result.ts +++ b/backend/src/api/controllers/result.ts @@ -64,6 +64,7 @@ import { MonkeyRequest } from "../types"; import { getFunbox, checkCompatibility } from "@monkeytype/funbox"; import { tryCatch } from "@monkeytype/util/trycatch"; import { getCachedConfiguration } from "../../init/configuration"; +import { allTimeLeaderboardCache } from "../../utils/all-time-leaderboard-cache"; try { if (!anticheatImplemented()) throw new Error("undefined"); @@ -534,6 +535,9 @@ export async function addResult( }, dailyLeaderboardsConfig, ); + + allTimeLeaderboardCache.clear(); + if ( dailyLeaderboardRank >= 1 && dailyLeaderboardRank <= 10 && diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 964602852f10..9437c85c3476 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -4,6 +4,7 @@ import MonkeyError, { isFirebaseError, } from "../../utils/error"; import { MonkeyResponse } from "../../utils/monkey-response"; +import { getCached, setCached, invalidateUserCache } from "../../utils/cache"; import * as DiscordUtils from "../../utils/discord"; import { buildAgentLog, @@ -914,6 +915,10 @@ export async function getProfile( ): Promise { const { uidOrName } = req.params; + const cacheKey = `user:profile:${uidOrName}`; + const cached = await getCached(cacheKey); + if (cached !== null) return cached; + const user = req.query.isUid ? await UserDAL.getUser(uidOrName, "get user profile") : await UserDAL.getUserByName(uidOrName, "get user profile"); @@ -987,7 +992,11 @@ export async function getProfile( }; if (banned) { - return new MonkeyResponse("Profile retrived: banned user", baseProfile); + await setCached( + cacheKey, + new MonkeyResponse("Profile retrieved: banned user", baseProfile), + ); + return new MonkeyResponse("Profile retrieved: banned user", baseProfile); } const allTimeLbs = await getAllTimeLbs(user.uid); @@ -1005,6 +1014,10 @@ export async function getProfile( } else { delete profileData.testActivity; } + await setCached( + cacheKey, + new MonkeyResponse("Profile retrieved", profileData), + ); return new MonkeyResponse("Profile retrieved", profileData); } @@ -1050,7 +1063,7 @@ export async function updateProfile( }; await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory); - + await invalidateUserCache(uid); return new MonkeyResponse("Profile updated", profileDetailsUpdates); } diff --git a/backend/src/dal/leaderboards.ts b/backend/src/dal/leaderboards.ts index f2a48658f6af..decc677004f5 100644 --- a/backend/src/dal/leaderboards.ts +++ b/backend/src/dal/leaderboards.ts @@ -393,8 +393,7 @@ async function createIndex( Logger.warning(`Index ${key} not matching, dropping and recreating...`); const existingIndex = (await getUsersCollection().listIndexes().toArray()) - // oxlint-disable-next-line no-unsafe-member-access - .map((it) => it.name as string) + .map((it: unknown) => (it as { name: string }).name) .find((it) => it.startsWith(key)); if (existingIndex !== undefined && existingIndex !== null) { diff --git a/backend/src/utils/all-time-leaderboard-cache.ts b/backend/src/utils/all-time-leaderboard-cache.ts new file mode 100644 index 000000000000..aa822f187153 --- /dev/null +++ b/backend/src/utils/all-time-leaderboard-cache.ts @@ -0,0 +1,34 @@ +type AllTimeCacheKey = { + mode: string; + language: string; + mode2: string; +}; + +type CacheEntry = { + data: unknown[]; + count: number; +}; + +class AllTimeLeaderboardCache { + private cache = new Map(); + + private getKey({ mode, language, mode2 }: AllTimeCacheKey): string { + return `alltime-lb:${mode}:${language}:${mode2}`; + } + + get(key: AllTimeCacheKey): CacheEntry | null { + const cacheKey = this.getKey(key); + const entry = this.cache.get(cacheKey); + return entry ?? null; + } + + set(key: AllTimeCacheKey, data: unknown[], count: number): void { + this.cache.set(this.getKey(key), { data, count }); + } + + clear(): void { + this.cache.clear(); + } +} + +export const allTimeLeaderboardCache = new AllTimeLeaderboardCache(); \ No newline at end of file diff --git a/backend/src/utils/cache.ts b/backend/src/utils/cache.ts new file mode 100644 index 000000000000..268c96a87d5a --- /dev/null +++ b/backend/src/utils/cache.ts @@ -0,0 +1,42 @@ +import { getConnection } from "../init/redis"; + +const CACHE_PREFIX = "cache:"; +const TTL = 300; // == 5 minutes + +export async function getCached(key: string): Promise { + const redis = getConnection(); + if (!redis) return null; + + try { + const data = await redis.get(`${CACHE_PREFIX}${key}`); + if (data === null || data === undefined || data === "") return null; + return JSON.parse(data) as T; + } catch { + return null; + } +} + +export async function setCached(key: string, data: T): Promise { + const redis = getConnection(); + if (!redis) return; + + try { + await redis.setex(`${CACHE_PREFIX}${key}`, TTL, JSON.stringify(data)); + } catch (err) { + console.error("Cache set failed:", err); + } +} + +export async function invalidateUserCache(userId: string): Promise { + const redis = getConnection(); + if (!redis) return; + + try { + const keys = await redis.keys(`${CACHE_PREFIX}user:profile:${userId}*`); + if (keys.length > 0) { + await redis.del(keys); + } + } catch (err) { + console.error("Cache invalidation failed:", err); + } +} \ No newline at end of file