From 6598823d607376041f9f78480de20ffbece52b95 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 21 Apr 2022 12:24:13 +0200 Subject: [PATCH] Converted leaderboards DAO to typescript (#2843) * added type for leaderboard entry * converted leaderboards to ts * fixed return type * renamed occurences to DAL * removed as any * removed as any * removed type casting --- backend/api/controllers/leaderboard.ts | 19 +-- backend/dao/leaderboards.js | 148 ---------------------- backend/dao/leaderboards.ts | 167 +++++++++++++++++++++++++ backend/jobs/update-leaderboards.ts | 13 +- backend/types/types.d.ts | 16 +++ 5 files changed, 202 insertions(+), 161 deletions(-) delete mode 100644 backend/dao/leaderboards.js create mode 100644 backend/dao/leaderboards.ts diff --git a/backend/api/controllers/leaderboard.ts b/backend/api/controllers/leaderboard.ts index ac431d9cb..2d154b8a0 100644 --- a/backend/api/controllers/leaderboard.ts +++ b/backend/api/controllers/leaderboard.ts @@ -1,6 +1,6 @@ import _ from "lodash"; import { MonkeyResponse } from "../../utils/monkey-response"; -import LeaderboardsDAO from "../../dao/leaderboards"; +import * as LeaderboardsDAL from "../../dao/leaderboards"; export async function getLeaderboard( req: MonkeyTypes.Request @@ -10,10 +10,10 @@ export async function getLeaderboard( const queryLimit = Math.min(parseInt(limit as string, 10), 50); - const leaderboard = await LeaderboardsDAO.get( - mode, - mode2, - language, + const leaderboard = await LeaderboardsDAL.get( + mode as string, + mode2 as string, + language as string, parseInt(skip as string, 10), queryLimit ); @@ -26,7 +26,7 @@ export async function getLeaderboard( ); } - const normalizedLeaderboard = _.map(leaderboard as any[], (entry) => { + const normalizedLeaderboard = _.map(leaderboard, (entry) => { return uid && entry.uid === uid ? entry : _.omit(entry, ["discordId", "uid", "difficulty", "language"]); @@ -41,7 +41,12 @@ export async function getRankFromLeaderboard( const { language, mode, mode2 } = req.query; const { uid } = req.ctx.decodedToken; - const data = await LeaderboardsDAO.getRank(mode, mode2, language, uid); + const data = await LeaderboardsDAL.getRank( + mode as string, + mode2 as string, + language as string, + uid + ); if (data === false) { return new MonkeyResponse( "Leaderboard is currently updating. Please try again in a few seconds.", diff --git a/backend/dao/leaderboards.js b/backend/dao/leaderboards.js deleted file mode 100644 index 7e7e88d5d..000000000 --- a/backend/dao/leaderboards.js +++ /dev/null @@ -1,148 +0,0 @@ -import db from "../init/db"; -import Logger from "../utils/logger"; -import { performance } from "perf_hooks"; -import { setLeaderboard } from "../utils/prometheus"; - -const leaderboardUpdating = {}; - -class LeaderboardsDAO { - static async get(mode, mode2, language, skip, limit = 50) { - if (leaderboardUpdating[`${language}_${mode}_${mode2}`]) return false; - if (limit > 50 || limit <= 0) limit = 50; - if (skip < 0) skip = 0; - const preset = await db - .collection(`leaderboards.${language}.${mode}.${mode2}`) - .find() - .sort({ rank: 1 }) - .skip(parseInt(skip)) - .limit(parseInt(limit.toString())) - .toArray(); - return preset; - } - - static async getRank(mode, mode2, language, uid) { - if (leaderboardUpdating[`${language}_${mode}_${mode2}`]) return false; - const res = await db - .collection(`leaderboards.${language}.${mode}.${mode2}`) - .findOne({ uid }); - if (res) { - res.count = await db - .collection(`leaderboards.${language}.${mode}.${mode2}`) - .estimatedDocumentCount(); - } - return res; - } - - static async update(mode, mode2, language, uid = undefined) { - let str = `lbPersonalBests.${mode}.${mode2}.${language}`; - let start1 = performance.now(); - let lb = await db - .collection("users") - .aggregate( - [ - { - $match: { - [str + ".wpm"]: { - $exists: true, - }, - [str + ".acc"]: { - $exists: true, - }, - [str + ".timestamp"]: { - $exists: true, - }, - banned: { $exists: false }, - }, - }, - { - $set: { - [str + ".uid"]: "$uid", - [str + ".name"]: "$name", - [str + ".discordId"]: "$discordId", - }, - }, - { - $replaceRoot: { - newRoot: "$" + str, - }, - }, - { - $sort: { - wpm: -1, - acc: -1, - timestamp: -1, - }, - }, - ], - { allowDiskUse: true } - ) - .toArray(); - let end1 = performance.now(); - - let start2 = performance.now(); - let retval = undefined; - lb.forEach((lbEntry, index) => { - lbEntry.rank = index + 1; - if (uid && lbEntry.uid === uid) { - retval = index + 1; - } - }); - let end2 = performance.now(); - let start3 = performance.now(); - leaderboardUpdating[`${language}_${mode}_${mode2}`] = true; - try { - await db.collection(`leaderboards.${language}.${mode}.${mode2}`).drop(); - } catch (e) {} - if (lb && lb.length !== 0) { - await db - .collection(`leaderboards.${language}.${mode}.${mode2}`) - .insertMany(lb); - } - let end3 = performance.now(); - - let start4 = performance.now(); - await db - .collection(`leaderboards.${language}.${mode}.${mode2}`) - .createIndex({ - uid: -1, - }); - await db - .collection(`leaderboards.${language}.${mode}.${mode2}`) - .createIndex({ - rank: 1, - }); - leaderboardUpdating[`${language}_${mode}_${mode2}`] = false; - let end4 = performance.now(); - - let timeToRunAggregate = (end1 - start1) / 1000; - let timeToRunLoop = (end2 - start2) / 1000; - let timeToRunInsert = (end3 - start3) / 1000; - let timeToRunIndex = (end4 - start4) / 1000; - - Logger.logToDb( - `system_lb_update_${language}_${mode}_${mode2}`, - `Aggregate ${timeToRunAggregate}s, loop ${timeToRunLoop}s, insert ${timeToRunInsert}s, index ${timeToRunIndex}s`, - uid - ); - - setLeaderboard(language, mode, mode2, [ - timeToRunAggregate, - timeToRunLoop, - timeToRunInsert, - timeToRunIndex, - ]); - - if (retval) { - return { - message: "Successfully updated leaderboard", - rank: retval, - }; - } else { - return { - message: "Successfully updated leaderboard", - }; - } - } -} - -export default LeaderboardsDAO; diff --git a/backend/dao/leaderboards.ts b/backend/dao/leaderboards.ts new file mode 100644 index 000000000..10e5f8210 --- /dev/null +++ b/backend/dao/leaderboards.ts @@ -0,0 +1,167 @@ +import db from "../init/db"; +import Logger from "../utils/logger"; +import { performance } from "perf_hooks"; +import { setLeaderboard } from "../utils/prometheus"; + +const leaderboardUpdating: { [key: string]: boolean } = {}; + +export async function get( + mode: string, + mode2: string, + language: string, + skip: number, + limit = 50 +): Promise { + if (leaderboardUpdating[`${language}_${mode}_${mode2}`]) return false; + if (limit > 50 || limit <= 0) limit = 50; + if (skip < 0) skip = 0; + const preset = await db + .collection( + `leaderboards.${language}.${mode}.${mode2}` + ) + .find() + .sort({ rank: 1 }) + .skip(skip) + .limit(limit) + .toArray(); + return preset; +} + +interface RankReturn extends MonkeyTypes.LeaderboardEntry { + count?: number; +} + +export async function getRank( + mode: string, + mode2: string, + language: string, + uid: string +): Promise { + if (leaderboardUpdating[`${language}_${mode}_${mode2}`]) return false; + const res: RankReturn | null = await db + .collection( + `leaderboards.${language}.${mode}.${mode2}` + ) + .findOne({ uid }); + if (res) { + res.count = await db + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .estimatedDocumentCount(); + return res; + } +} + +export async function update( + mode: string, + mode2: string, + language: string, + uid?: string +): Promise<{ + message: string; + rank?: number; +}> { + const str = `lbPersonalBests.${mode}.${mode2}.${language}`; + const start1 = performance.now(); + const lb = await db + .collection("users") + .aggregate( + [ + { + $match: { + [str + ".wpm"]: { + $exists: true, + }, + [str + ".acc"]: { + $exists: true, + }, + [str + ".timestamp"]: { + $exists: true, + }, + banned: { $exists: false }, + }, + }, + { + $set: { + [str + ".uid"]: "$uid", + [str + ".name"]: "$name", + [str + ".discordId"]: "$discordId", + }, + }, + { + $replaceRoot: { + newRoot: "$" + str, + }, + }, + { + $sort: { + wpm: -1, + acc: -1, + timestamp: -1, + }, + }, + ], + { allowDiskUse: true } + ) + .toArray(); + const end1 = performance.now(); + + const start2 = performance.now(); + let retval: number | undefined = undefined; + lb.forEach((lbEntry, index) => { + lbEntry.rank = index + 1; + if (uid && lbEntry.uid === uid) { + retval = index + 1; + } + }); + const end2 = performance.now(); + const start3 = performance.now(); + leaderboardUpdating[`${language}_${mode}_${mode2}`] = true; + try { + await db.collection(`leaderboards.${language}.${mode}.${mode2}`).drop(); + } catch (e) {} + if (lb && lb.length !== 0) { + await db + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .insertMany(lb); + } + const end3 = performance.now(); + + const start4 = performance.now(); + await db.collection(`leaderboards.${language}.${mode}.${mode2}`).createIndex({ + uid: -1, + }); + await db.collection(`leaderboards.${language}.${mode}.${mode2}`).createIndex({ + rank: 1, + }); + leaderboardUpdating[`${language}_${mode}_${mode2}`] = false; + const end4 = performance.now(); + + const timeToRunAggregate = (end1 - start1) / 1000; + const timeToRunLoop = (end2 - start2) / 1000; + const timeToRunInsert = (end3 - start3) / 1000; + const timeToRunIndex = (end4 - start4) / 1000; + + Logger.logToDb( + `system_lb_update_${language}_${mode}_${mode2}`, + `Aggregate ${timeToRunAggregate}s, loop ${timeToRunLoop}s, insert ${timeToRunInsert}s, index ${timeToRunIndex}s`, + uid + ); + + setLeaderboard(language, mode, mode2, [ + timeToRunAggregate, + timeToRunLoop, + timeToRunInsert, + timeToRunIndex, + ]); + + if (retval) { + return { + message: "Successfully updated leaderboard", + rank: retval, + }; + } else { + return { + message: "Successfully updated leaderboard", + }; + } +} diff --git a/backend/jobs/update-leaderboards.ts b/backend/jobs/update-leaderboards.ts index 068b316c5..746218433 100644 --- a/backend/jobs/update-leaderboards.ts +++ b/backend/jobs/update-leaderboards.ts @@ -1,22 +1,23 @@ import { CronJob } from "cron"; import BotDAO from "../dao/bot"; import George from "../tasks/george"; -import { Document, WithId } from "mongodb"; -import LeaderboardsDAO from "../dao/leaderboards"; +import * as LeaderboardsDAL from "../dao/leaderboards"; import { getCachedConfiguration } from "../init/configuration"; const CRON_SCHEDULE = "30 14/15 * * * *"; const RECENT_AGE_MINUTES = 10; const RECENT_AGE_MILLISECONDS = RECENT_AGE_MINUTES * 60 * 1000; -async function getTop10(leaderboardTime: string): Promise[]> { - return (await LeaderboardsDAO.get( +async function getTop10( + leaderboardTime: string +): Promise { + return (await LeaderboardsDAL.get( "time", leaderboardTime, "english", 0, 10 - )) as any[]; + )) as MonkeyTypes.LeaderboardEntry[]; //can do that because gettop10 will not be called during an update } async function updateLeaderboardAndNotifyChanges( @@ -30,7 +31,7 @@ async function updateLeaderboardAndNotifyChanges( }) ); - await LeaderboardsDAO.update("time", leaderboardTime, "english"); + await LeaderboardsDAL.update("time", leaderboardTime, "english"); const top10AfterUpdate = await getTop10(leaderboardTime); diff --git a/backend/types/types.d.ts b/backend/types/types.d.ts index 2a9aef72e..54ed5ee87 100644 --- a/backend/types/types.d.ts +++ b/backend/types/types.d.ts @@ -93,6 +93,22 @@ declare namespace MonkeyTypes { personalBests?: PersonalBests; } + interface LeaderboardEntry { + _id: ObjectId; + acc: number; + consistency: number; + difficulty: Difficulty; + lazyMode: boolean; + language: string; + punctuation: boolean; + raw: number; + wpm: number; + timestamp: number; + uid: string; + name: string; + rank: number; + } + interface CustomTheme { _id: ObjectId; name: string;