diff --git a/bun.lockb b/bun.lockb index 6aa74d0..3d135ce 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/projects/backend/src/common/app-utils.ts b/projects/backend/src/common/app.util.ts similarity index 100% rename from projects/backend/src/common/app-utils.ts rename to projects/backend/src/common/app.util.ts diff --git a/projects/backend/src/common/cache.util.ts b/projects/backend/src/common/cache.util.ts new file mode 100644 index 0000000..a59428a --- /dev/null +++ b/projects/backend/src/common/cache.util.ts @@ -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( + cache: SSRCache, + cacheKey: string, + fetchFn: () => Promise +): Promise { + if (cache == undefined) { + throw new InternalServerError(`Cache is not defined`); + } + + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } + + const data = await fetchFn(); + if (data) { + cache.set(cacheKey, data); + } + + return data; +} diff --git a/projects/backend/src/controller/app.controller.ts b/projects/backend/src/controller/app.controller.ts index 174253d..e905056 100644 --- a/projects/backend/src/controller/app.controller.ts +++ b/projects/backend/src/controller/app.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get } from "elysia-decorators"; -import { getAppVersion } from "../common/app-utils"; +import { getAppVersion } from "../common/app.util"; import { AppService } from "../service/app.service"; @Controller() diff --git a/projects/backend/src/service/image.service.tsx b/projects/backend/src/service/image.service.tsx index b5484e0..faffa24 100644 --- a/projects/backend/src/service/image.service.tsx +++ b/projects/backend/src/service/image.service.tsx @@ -5,14 +5,17 @@ import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils import { getDifficultyFromScoreSaberDifficulty } from "@ssr/common/utils/scoresaber-utils"; import { StarIcon } from "../../components/star-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 ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player"; import { Jimp } from "jimp"; import { extractColors } from "extract-colors"; 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 }; export class ImageService { @@ -26,7 +29,7 @@ export class ImageService { public static async getAverageImageColor(src: string): Promise<{ color: string } | undefined> { src = decodeURIComponent(src); - return await this.fetchWithCache<{ color: string }>(`average_color-${src}`, async () => { + return await fetchWithCache<{ color: string }>(cache, `average_color-${src}`, async () => { try { const image = await Jimp.read(src); // Load image using Jimp 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( - cacheKey: string, - fetchFn: () => Promise - ): Promise { - if (cache.has(cacheKey)) { - return cache.get(cacheKey); - } - - const data = await fetchFn(); - if (data) { - cache.set(cacheKey, data); - } - - return data; - } - /** * The base of the OpenGraph image * @@ -120,7 +101,7 @@ export class ImageService { * @param id the player's id */ public static async generatePlayerImage(id: string) { - const player = await this.fetchWithCache(`player-${id}`, async () => { + const player = await fetchWithCache(cache, `player-${id}`, async () => { const token = await scoresaberService.lookupPlayer(id); return token ? await getScoreSaberPlayerFromToken(token, Config.apiUrl) : undefined; }); @@ -194,7 +175,7 @@ export class ImageService { * @param id the leaderboard's id */ public static async generateLeaderboardImage(id: string) { - const leaderboard = await this.fetchWithCache(`leaderboard-${id}`, () => + const leaderboard = await fetchWithCache(cache, `leaderboard-${id}`, () => scoresaberService.lookupLeaderboard(id) ); if (!leaderboard) { diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts index 00d9dcd..e5d60e8 100644 --- a/projects/backend/src/service/score.service.ts +++ b/projects/backend/src/service/score.service.ts @@ -21,6 +21,16 @@ import PlayerScoresResponse from "@ssr/common/response/player-scores-response"; import { DiscordChannels, logToChannel } from "../bot/bot"; import { EmbedBuilder } from "discord.js"; 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 { /** @@ -116,58 +126,64 @@ export class ScoreService { page: number, sort: string, search?: string - ): Promise> { - const scores: PlayerScore[] | undefined = []; - let beatSaverMap: BeatSaverMap | undefined; - let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values + ): Promise | undefined> { + return fetchWithCache( + playerScoresCache, + `player-scores-${leaderboardName}-${id}-${page}-${sort}-${search}`, + async () => { + const scores: PlayerScore[] | undefined = []; + let beatSaverMap: BeatSaverMap | undefined; + let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values - switch (leaderboardName) { - case "scoresaber": { - const leaderboardScores = await scoresaberService.lookupPlayerScores({ - playerId: id, - page: page, - sort: sort as ScoreSort, - search: search, - }); - if (leaderboardScores == undefined) { - break; + switch (leaderboardName) { + case "scoresaber": { + const leaderboardScores = await scoresaberService.lookupPlayerScores({ + playerId: id, + page: page, + sort: sort as ScoreSort, + search: search, + }); + if (leaderboardScores == undefined) { + 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( - 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; + return { + scores: scores, + metadata: metadata, + }; } - default: { - throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); - } - } - - return { - scores: scores, - metadata: metadata, - }; + ); } /** @@ -182,52 +198,54 @@ export class ScoreService { leaderboardName: Leaderboards, id: string, page: number - ): Promise> { - const scores: Score[] = []; - let leaderboard: Leaderboard | undefined; - let beatSaverMap: BeatSaverMap | undefined; - let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values + ): Promise | undefined> { + return fetchWithCache(leaderboardScoresCache, `leaderboard-scores-${leaderboardName}-${id}-${page}`, async () => { + const scores: Score[] = []; + let leaderboard: Leaderboard | undefined; + let beatSaverMap: BeatSaverMap | undefined; + let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values - switch (leaderboardName) { - case "scoresaber": { - const leaderboardResponse = await LeaderboardService.getLeaderboard(leaderboardName, id); - if (leaderboardResponse == undefined) { - throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); - } - leaderboard = leaderboardResponse.leaderboard; - beatSaverMap = leaderboardResponse.beatsaver; + switch (leaderboardName) { + case "scoresaber": { + const leaderboardResponse = await LeaderboardService.getLeaderboard(leaderboardName, id); + if (leaderboardResponse == undefined) { + throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); + } + leaderboard = leaderboardResponse.leaderboard; + beatSaverMap = leaderboardResponse.beatsaver; - const leaderboardScores = await scoresaberService.lookupLeaderboardScores(id, page); - if (leaderboardScores == undefined) { + const leaderboardScores = await scoresaberService.lookupLeaderboardScores(id, page); + 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; } - - for (const token of leaderboardScores.scores) { - const score = getScoreSaberScoreFromToken(token); - if (score == undefined) { - continue; - } - scores.push(score); + default: { + throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); } - - 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 { - scores: scores, - leaderboard: leaderboard, - beatSaver: beatSaverMap, - metadata: metadata, - }; + return { + scores: scores, + leaderboard: leaderboard, + beatSaver: beatSaverMap, + metadata: metadata, + }; + }); } } diff --git a/projects/website/package.json b/projects/website/package.json index bbdd914..1cc532a 100644 --- a/projects/website/package.json +++ b/projects/website/package.json @@ -38,7 +38,6 @@ "next": "15.0.0-rc.1", "next-build-id": "^3.0.0", "next-themes": "^0.3.0", - "node-cache": "^5.1.2", "react": "18.3.1", "react-chartjs-2": "^5.2.0", "react-countup": "^6.5.3", diff --git a/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx b/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx index 1463b3a..a30a0ef 100644 --- a/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx @@ -3,7 +3,6 @@ import { redirect } from "next/navigation"; import { Colors } from "@/common/colors"; import { getAverageColor } from "@/common/image-utils"; import { LeaderboardData } from "@/components/leaderboard/leaderboard-data"; -import NodeCache from "node-cache"; import { Config } from "@ssr/common/config"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; 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 { fetchLeaderboardScores } from "@ssr/common/utils/score-utils"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; +import { SSRCache } from "@ssr/common/cache"; const UNKNOWN_LEADERBOARD = { title: "ScoreSaber Reloaded - Unknown Leaderboard", @@ -32,7 +32,9 @@ type LeaderboardData = { 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 diff --git a/projects/website/src/app/(pages)/player/[...slug]/page.tsx b/projects/website/src/app/(pages)/player/[...slug]/page.tsx index 3162bc2..f5cc071 100644 --- a/projects/website/src/app/(pages)/player/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/player/[...slug]/page.tsx @@ -4,7 +4,6 @@ import { redirect } from "next/navigation"; import { Colors } from "@/common/colors"; import { getAverageColor } from "@/common/image-utils"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; -import NodeCache from "node-cache"; import { getCookieValue } from "@ssr/common/utils/cookie-utils"; import { Config } from "@ssr/common/config"; 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 { fetchPlayerScores } from "@ssr/common/utils/score-utils"; import PlayerScoresResponse from "@ssr/common/response/player-scores-response"; +import { SSRCache } from "@ssr/common/cache"; const UNKNOWN_PLAYER = { title: "ScoreSaber Reloaded - Unknown Player", @@ -36,7 +36,9 @@ type PlayerData = { 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