Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/src/api/controllers/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -534,6 +535,9 @@ export async function addResult(
},
dailyLeaderboardsConfig,
);

allTimeLeaderboardCache.clear();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not the correct place, this is the dailyLeaderboard.


if (
dailyLeaderboardRank >= 1 &&
dailyLeaderboardRank <= 10 &&
Expand Down
17 changes: 15 additions & 2 deletions backend/src/api/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -914,6 +915,10 @@ export async function getProfile(
): Promise<GetProfileResponse> {
const { uidOrName } = req.params;

const cacheKey = `user:profile:${uidOrName}`;
const cached = await getCached<GetProfileResponse>(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");
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down Expand Up @@ -1050,7 +1063,7 @@ export async function updateProfile(
};

await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory);

await invalidateUserCache(uid);
return new MonkeyResponse("Profile updated", profileDetailsUpdates);
}

Expand Down
3 changes: 1 addition & 2 deletions backend/src/dal/leaderboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 34 additions & 0 deletions backend/src/utils/all-time-leaderboard-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
type AllTimeCacheKey = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add mode2 to the cache key

mode: string;
language: string;
mode2: string;
};

type CacheEntry = {
data: unknown[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep the type

count: number;
};

class AllTimeLeaderboardCache {
private cache = new Map<string, CacheEntry>();

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();
42 changes: 42 additions & 0 deletions backend/src/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getConnection } from "../init/redis";

const CACHE_PREFIX = "cache:";
const TTL = 300; // == 5 minutes

export async function getCached<T>(key: string): Promise<T | null> {
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<T>(key: string, data: T): Promise<void> {
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<void> {
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);
}
}
Loading