cleanup caching
All checks were successful
Deploy Backend / deploy (push) Successful in 4m0s
Deploy Website / deploy (push) Successful in 7m9s

This commit is contained in:
Lee 2024-10-19 04:10:44 +01:00
parent 238ec6e254
commit 7421c47959
9 changed files with 153 additions and 121 deletions

BIN
bun.lockb

Binary file not shown.

@ -0,0 +1,30 @@
import { SSRCache } from "@ssr/common/cache";
import { InternalServerError } from "../error/internal-server-error";
/**
* Fetches data with caching.
*
* @param cache the cache to fetch from
* @param cacheKey The key used for caching.
* @param fetchFn The function to fetch data if it's not in cache.
*/
export async function fetchWithCache<T>(
cache: SSRCache,
cacheKey: string,
fetchFn: () => Promise<T | undefined>
): Promise<T | undefined> {
if (cache == undefined) {
throw new InternalServerError(`Cache is not defined`);
}
if (cache.has(cacheKey)) {
return cache.get<T>(cacheKey);
}
const data = await fetchFn();
if (data) {
cache.set(cacheKey, data);
}
return data;
}

@ -1,5 +1,5 @@
import { Controller, Get } from "elysia-decorators"; import { Controller, Get } from "elysia-decorators";
import { getAppVersion } from "../common/app-utils"; import { getAppVersion } from "../common/app.util";
import { AppService } from "../service/app.service"; import { AppService } from "../service/app.service";
@Controller() @Controller()

@ -5,14 +5,17 @@ import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils
import { getDifficultyFromScoreSaberDifficulty } from "@ssr/common/utils/scoresaber-utils"; import { getDifficultyFromScoreSaberDifficulty } from "@ssr/common/utils/scoresaber-utils";
import { StarIcon } from "../../components/star-icon"; import { StarIcon } from "../../components/star-icon";
import { GlobeIcon } from "../../components/globe-icon"; import { GlobeIcon } from "../../components/globe-icon";
import NodeCache from "node-cache";
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player"; import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player";
import { Jimp } from "jimp"; import { Jimp } from "jimp";
import { extractColors } from "extract-colors"; import { extractColors } from "extract-colors";
import { Config } from "@ssr/common/config"; import { Config } from "@ssr/common/config";
import { fetchWithCache } from "../common/cache.util";
import { SSRCache } from "@ssr/common/cache";
const cache = new NodeCache({ stdTTL: 60 * 60, checkperiod: 120 }); const cache = new SSRCache({
ttl: 1000 * 60 * 60, // 1 hour
});
const imageOptions = { width: 1200, height: 630 }; const imageOptions = { width: 1200, height: 630 };
export class ImageService { export class ImageService {
@ -26,7 +29,7 @@ export class ImageService {
public static async getAverageImageColor(src: string): Promise<{ color: string } | undefined> { public static async getAverageImageColor(src: string): Promise<{ color: string } | undefined> {
src = decodeURIComponent(src); src = decodeURIComponent(src);
return await this.fetchWithCache<{ color: string }>(`average_color-${src}`, async () => { return await fetchWithCache<{ color: string }>(cache, `average_color-${src}`, async () => {
try { try {
const image = await Jimp.read(src); // Load image using Jimp const image = await Jimp.read(src); // Load image using Jimp
const { width, height, data } = image.bitmap; // Access image dimensions and pixel data const { width, height, data } = image.bitmap; // Access image dimensions and pixel data
@ -54,28 +57,6 @@ export class ImageService {
}); });
} }
/**
* Fetches data with caching.
*
* @param cacheKey The key used for caching.
* @param fetchFn The function to fetch data if it's not in cache.
*/
private static async fetchWithCache<T>(
cacheKey: string,
fetchFn: () => Promise<T | undefined>
): Promise<T | undefined> {
if (cache.has(cacheKey)) {
return cache.get<T>(cacheKey);
}
const data = await fetchFn();
if (data) {
cache.set(cacheKey, data);
}
return data;
}
/** /**
* The base of the OpenGraph image * The base of the OpenGraph image
* *
@ -120,7 +101,7 @@ export class ImageService {
* @param id the player's id * @param id the player's id
*/ */
public static async generatePlayerImage(id: string) { public static async generatePlayerImage(id: string) {
const player = await this.fetchWithCache<ScoreSaberPlayer>(`player-${id}`, async () => { const player = await fetchWithCache<ScoreSaberPlayer>(cache, `player-${id}`, async () => {
const token = await scoresaberService.lookupPlayer(id); const token = await scoresaberService.lookupPlayer(id);
return token ? await getScoreSaberPlayerFromToken(token, Config.apiUrl) : undefined; return token ? await getScoreSaberPlayerFromToken(token, Config.apiUrl) : undefined;
}); });
@ -194,7 +175,7 @@ export class ImageService {
* @param id the leaderboard's id * @param id the leaderboard's id
*/ */
public static async generateLeaderboardImage(id: string) { public static async generateLeaderboardImage(id: string) {
const leaderboard = await this.fetchWithCache<ScoreSaberLeaderboardToken>(`leaderboard-${id}`, () => const leaderboard = await fetchWithCache<ScoreSaberLeaderboardToken>(cache, `leaderboard-${id}`, () =>
scoresaberService.lookupLeaderboard(id) scoresaberService.lookupLeaderboard(id)
); );
if (!leaderboard) { if (!leaderboard) {

@ -21,6 +21,16 @@ import PlayerScoresResponse from "@ssr/common/response/player-scores-response";
import { DiscordChannels, logToChannel } from "../bot/bot"; import { DiscordChannels, logToChannel } from "../bot/bot";
import { EmbedBuilder } from "discord.js"; import { EmbedBuilder } from "discord.js";
import { Config } from "@ssr/common/config"; import { Config } from "@ssr/common/config";
import { SSRCache } from "@ssr/common/cache";
import { fetchWithCache } from "../common/cache.util";
const playerScoresCache = new SSRCache({
ttl: 1000 * 60, // 1 minute
});
const leaderboardScoresCache = new SSRCache({
ttl: 1000 * 60, // 1 minute
});
export class ScoreService { export class ScoreService {
/** /**
@ -116,58 +126,64 @@ export class ScoreService {
page: number, page: number,
sort: string, sort: string,
search?: string search?: string
): Promise<PlayerScoresResponse<unknown, unknown>> { ): Promise<PlayerScoresResponse<unknown, unknown> | undefined> {
const scores: PlayerScore<unknown, unknown>[] | undefined = []; return fetchWithCache(
let beatSaverMap: BeatSaverMap | undefined; playerScoresCache,
let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values `player-scores-${leaderboardName}-${id}-${page}-${sort}-${search}`,
async () => {
const scores: PlayerScore<unknown, unknown>[] | undefined = [];
let beatSaverMap: BeatSaverMap | undefined;
let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values
switch (leaderboardName) { switch (leaderboardName) {
case "scoresaber": { case "scoresaber": {
const leaderboardScores = await scoresaberService.lookupPlayerScores({ const leaderboardScores = await scoresaberService.lookupPlayerScores({
playerId: id, playerId: id,
page: page, page: page,
sort: sort as ScoreSort, sort: sort as ScoreSort,
search: search, search: search,
}); });
if (leaderboardScores == undefined) { if (leaderboardScores == undefined) {
break; break;
}
metadata = new Metadata(
Math.ceil(leaderboardScores.metadata.total / leaderboardScores.metadata.itemsPerPage),
leaderboardScores.metadata.total,
leaderboardScores.metadata.page,
leaderboardScores.metadata.itemsPerPage
);
for (const token of leaderboardScores.playerScores) {
const score = getScoreSaberScoreFromToken(token.score);
if (score == undefined) {
continue;
}
const tokenLeaderboard = getScoreSaberLeaderboardFromToken(token.leaderboard);
if (tokenLeaderboard == undefined) {
continue;
}
beatSaverMap = await BeatSaverService.getMap(tokenLeaderboard.songHash);
scores.push({
score: score,
leaderboard: tokenLeaderboard,
beatSaver: beatSaverMap,
});
}
break;
}
default: {
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
}
} }
metadata = new Metadata( return {
Math.ceil(leaderboardScores.metadata.total / leaderboardScores.metadata.itemsPerPage), scores: scores,
leaderboardScores.metadata.total, metadata: metadata,
leaderboardScores.metadata.page, };
leaderboardScores.metadata.itemsPerPage
);
for (const token of leaderboardScores.playerScores) {
const score = getScoreSaberScoreFromToken(token.score);
if (score == undefined) {
continue;
}
const tokenLeaderboard = getScoreSaberLeaderboardFromToken(token.leaderboard);
if (tokenLeaderboard == undefined) {
continue;
}
beatSaverMap = await BeatSaverService.getMap(tokenLeaderboard.songHash);
scores.push({
score: score,
leaderboard: tokenLeaderboard,
beatSaver: beatSaverMap,
});
}
break;
} }
default: { );
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
}
}
return {
scores: scores,
metadata: metadata,
};
} }
/** /**
@ -182,52 +198,54 @@ export class ScoreService {
leaderboardName: Leaderboards, leaderboardName: Leaderboards,
id: string, id: string,
page: number page: number
): Promise<LeaderboardScoresResponse<unknown, unknown>> { ): Promise<LeaderboardScoresResponse<unknown, unknown> | undefined> {
const scores: Score[] = []; return fetchWithCache(leaderboardScoresCache, `leaderboard-scores-${leaderboardName}-${id}-${page}`, async () => {
let leaderboard: Leaderboard | undefined; const scores: Score[] = [];
let beatSaverMap: BeatSaverMap | undefined; let leaderboard: Leaderboard | undefined;
let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values let beatSaverMap: BeatSaverMap | undefined;
let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values
switch (leaderboardName) { switch (leaderboardName) {
case "scoresaber": { case "scoresaber": {
const leaderboardResponse = await LeaderboardService.getLeaderboard(leaderboardName, id); const leaderboardResponse = await LeaderboardService.getLeaderboard(leaderboardName, id);
if (leaderboardResponse == undefined) { if (leaderboardResponse == undefined) {
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
} }
leaderboard = leaderboardResponse.leaderboard; leaderboard = leaderboardResponse.leaderboard;
beatSaverMap = leaderboardResponse.beatsaver; beatSaverMap = leaderboardResponse.beatsaver;
const leaderboardScores = await scoresaberService.lookupLeaderboardScores(id, page); const leaderboardScores = await scoresaberService.lookupLeaderboardScores(id, page);
if (leaderboardScores == undefined) { if (leaderboardScores == undefined) {
break;
}
for (const token of leaderboardScores.scores) {
const score = getScoreSaberScoreFromToken(token);
if (score == undefined) {
continue;
}
scores.push(score);
}
metadata = new Metadata(
Math.ceil(leaderboardScores.metadata.total / leaderboardScores.metadata.itemsPerPage),
leaderboardScores.metadata.total,
leaderboardScores.metadata.page,
leaderboardScores.metadata.itemsPerPage
);
break; break;
} }
default: {
for (const token of leaderboardScores.scores) { throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
const score = getScoreSaberScoreFromToken(token);
if (score == undefined) {
continue;
}
scores.push(score);
} }
metadata = new Metadata(
Math.ceil(leaderboardScores.metadata.total / leaderboardScores.metadata.itemsPerPage),
leaderboardScores.metadata.total,
leaderboardScores.metadata.page,
leaderboardScores.metadata.itemsPerPage
);
break;
} }
default: {
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
}
}
return { return {
scores: scores, scores: scores,
leaderboard: leaderboard, leaderboard: leaderboard,
beatSaver: beatSaverMap, beatSaver: beatSaverMap,
metadata: metadata, metadata: metadata,
}; };
});
} }
} }

@ -38,7 +38,6 @@
"next": "15.0.0-rc.1", "next": "15.0.0-rc.1",
"next-build-id": "^3.0.0", "next-build-id": "^3.0.0",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"node-cache": "^5.1.2",
"react": "18.3.1", "react": "18.3.1",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-countup": "^6.5.3", "react-countup": "^6.5.3",

@ -3,7 +3,6 @@ import { redirect } from "next/navigation";
import { Colors } from "@/common/colors"; import { Colors } from "@/common/colors";
import { getAverageColor } from "@/common/image-utils"; import { getAverageColor } from "@/common/image-utils";
import { LeaderboardData } from "@/components/leaderboard/leaderboard-data"; import { LeaderboardData } from "@/components/leaderboard/leaderboard-data";
import NodeCache from "node-cache";
import { Config } from "@ssr/common/config"; import { Config } from "@ssr/common/config";
import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response"; import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response";
@ -11,6 +10,7 @@ import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leade
import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util"; import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util";
import { fetchLeaderboardScores } from "@ssr/common/utils/score-utils"; import { fetchLeaderboardScores } from "@ssr/common/utils/score-utils";
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
import { SSRCache } from "@ssr/common/cache";
const UNKNOWN_LEADERBOARD = { const UNKNOWN_LEADERBOARD = {
title: "ScoreSaber Reloaded - Unknown Leaderboard", title: "ScoreSaber Reloaded - Unknown Leaderboard",
@ -32,7 +32,9 @@ type LeaderboardData = {
page: number; page: number;
}; };
const leaderboardCache = new NodeCache({ stdTTL: 60, checkperiod: 120 }); const leaderboardCache = new SSRCache({
ttl: 1000 * 60, // 1 minute
});
/** /**
* Gets the leaderboard data and scores * Gets the leaderboard data and scores

@ -4,7 +4,6 @@ import { redirect } from "next/navigation";
import { Colors } from "@/common/colors"; import { Colors } from "@/common/colors";
import { getAverageColor } from "@/common/image-utils"; import { getAverageColor } from "@/common/image-utils";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import NodeCache from "node-cache";
import { getCookieValue } from "@ssr/common/utils/cookie-utils"; import { getCookieValue } from "@ssr/common/utils/cookie-utils";
import { Config } from "@ssr/common/config"; import { Config } from "@ssr/common/config";
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player"; import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player";
@ -13,6 +12,7 @@ import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
import { fetchPlayerScores } from "@ssr/common/utils/score-utils"; import { fetchPlayerScores } from "@ssr/common/utils/score-utils";
import PlayerScoresResponse from "@ssr/common/response/player-scores-response"; import PlayerScoresResponse from "@ssr/common/response/player-scores-response";
import { SSRCache } from "@ssr/common/cache";
const UNKNOWN_PLAYER = { const UNKNOWN_PLAYER = {
title: "ScoreSaber Reloaded - Unknown Player", title: "ScoreSaber Reloaded - Unknown Player",
@ -36,7 +36,9 @@ type PlayerData = {
search: string; search: string;
}; };
const playerCache = new NodeCache({ stdTTL: 60, checkperiod: 120 }); const playerCache = new SSRCache({
ttl: 1000 * 60, // 1 minute
});
/** /**
* Gets the player data and scores * Gets the player data and scores