cleanup caching
This commit is contained in:
parent
238ec6e254
commit
7421c47959
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
30
projects/backend/src/common/cache.util.ts
Normal file
30
projects/backend/src/common/cache.util.ts
Normal file
@ -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
|
||||||
|
Reference in New Issue
Block a user