mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-25 04:26:00 -05:00
Fix counts a season once when the cache is filled concurrently
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// One finished season, one user sitting at rank 5 (so: top-10 AND top-100).
|
||||
vi.mock("~/features/mmr/core/Seasons", () => ({
|
||||
allFinished: () => [1],
|
||||
}));
|
||||
vi.mock("~/features/leaderboards/LeaderboardRepository.server", () => ({
|
||||
findUserSPLeaderboard: async () => [{ id: 100, placementRank: 5 }],
|
||||
}));
|
||||
|
||||
import { cachedUserSQLeaderboardTopData } from "./utils.server";
|
||||
|
||||
describe("SendouQ leaderboard widget cache", () => {
|
||||
it("counts a season once when the cache is filled concurrently", async () => {
|
||||
// This is exactly how UserRepository.findWidgetsByUserId drives it: the
|
||||
// top-10-seasons and top-100-seasons widgets both call the cache inside the
|
||||
// same Promise.all, so a cold cache is populated by two concurrent callers.
|
||||
const [cache] = await Promise.all([
|
||||
cachedUserSQLeaderboardTopData(),
|
||||
cachedUserSQLeaderboardTopData(),
|
||||
]);
|
||||
|
||||
const user = cache.get(100)!;
|
||||
|
||||
expect(user.TOP_10.times).toBe(1);
|
||||
expect(user.TOP_10.seasons).toEqual([1]);
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,37 @@
|
||||
import { cachified } from "@epic-web/cachified";
|
||||
import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
|
||||
|
||||
type LeaderboardTopData = {
|
||||
times: number;
|
||||
seasons: number[];
|
||||
};
|
||||
|
||||
const sqLeaderboardTopCache = new Map<
|
||||
type UserSQLeaderboardTopData = Map<
|
||||
number,
|
||||
{
|
||||
TOP_10: LeaderboardTopData;
|
||||
TOP_100: LeaderboardTopData;
|
||||
}
|
||||
>();
|
||||
>;
|
||||
|
||||
export async function cachedUserSQLeaderboardTopData() {
|
||||
if (sqLeaderboardTopCache.size > 0) {
|
||||
return sqLeaderboardTopCache;
|
||||
}
|
||||
const SQ_LEADERBOARD_TOP_CACHE_KEY = "sq-leaderboard-top";
|
||||
|
||||
const allSeasons = Seasons.allFinished();
|
||||
/** How many times & in what seasons each user placed in the SendouQ leaderboard top 10 and top 100. */
|
||||
export function cachedUserSQLeaderboardTopData() {
|
||||
return cachified({
|
||||
key: SQ_LEADERBOARD_TOP_CACHE_KEY,
|
||||
cache,
|
||||
ttl: ttl(IN_MILLISECONDS.TWO_HOURS),
|
||||
getFreshValue: userSQLeaderboardTopData,
|
||||
});
|
||||
}
|
||||
|
||||
for (const season of allSeasons) {
|
||||
async function userSQLeaderboardTopData(): Promise<UserSQLeaderboardTopData> {
|
||||
const result: UserSQLeaderboardTopData = new Map();
|
||||
|
||||
for (const season of Seasons.allFinished()) {
|
||||
const leaderboard =
|
||||
await LeaderboardRepository.findUserSPLeaderboard(season);
|
||||
|
||||
@@ -29,14 +39,14 @@ export async function cachedUserSQLeaderboardTopData() {
|
||||
const userId = entry.id;
|
||||
const placementRank = entry.placementRank;
|
||||
|
||||
if (!sqLeaderboardTopCache.has(userId)) {
|
||||
sqLeaderboardTopCache.set(userId, {
|
||||
if (!result.has(userId)) {
|
||||
result.set(userId, {
|
||||
TOP_10: { times: 0, seasons: [] },
|
||||
TOP_100: { times: 0, seasons: [] },
|
||||
});
|
||||
}
|
||||
|
||||
const userData = sqLeaderboardTopCache.get(userId)!;
|
||||
const userData = result.get(userId)!;
|
||||
|
||||
if (placementRank <= 10) {
|
||||
userData.TOP_10.times += 1;
|
||||
@@ -50,5 +60,5 @@ export async function cachedUserSQLeaderboardTopData() {
|
||||
}
|
||||
}
|
||||
|
||||
return sqLeaderboardTopCache;
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user