From 49f6a0e68d1615d19a049b566725602afdcca495 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:45:42 -0400 Subject: [PATCH 1/3] perf(api): batch leaderboard page lookups into one $in query getAdditionalStats ran one findOne per row, ten per page, for both player and guild leaderboards. It now runs a single find with $in and puts the rows back in Redis order. A row whose player or guild document is missing from Mongo used to come back with no name. It is now left out of the page, the other rows keep their Redis positions, and the API logs how many rows each page dropped. --- .../leaderboards/guild-leaderboard.service.ts | 35 ++++++++++-------- .../src/leaderboards/leaderboard.service.ts | 37 ++++++++++++++----- .../player-leaderboard.service.ts | 35 ++++++++++-------- 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts b/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts index 430f00294..f1f11babd 100644 --- a/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts +++ b/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts @@ -49,29 +49,34 @@ export class GuildLeaderboardService extends LeaderboardService { protected async getAdditionalStats( ids: string[], fields: string[] - ): Promise { + ): Promise<(LeaderboardAdditionalStats | null)[]> { const selector = fields.reduce((acc, key) => { acc[key] = true; return acc; }, {} as Record); + selector.id = true; selector.nameFormatted = true; - return await Promise.all( - ids.map(async (id) => { - const guild = await this.guildModel - .findOne() - .where("id") - .equals(id) - .select(selector) - .lean() - .exec(); + const guilds = await this.guildModel + .find({ id: { $in: ids } }) + .select(selector) + .lean() + .exec(); - const additionalStats = flatten(guild) as LeaderboardAdditionalStats; - additionalStats.name = additionalStats.nameFormatted; + const guildsById = new Map(guilds.map((guild) => [guild.id, guild])); - return additionalStats; - }) - ); + return ids.map((id) => { + const guild = guildsById.get(id); + + if (!guild) { + return null; + } + + const additionalStats = flatten(guild) as LeaderboardAdditionalStats; + additionalStats.name = additionalStats.nameFormatted; + + return additionalStats; + }); } } diff --git a/apps/api/src/leaderboards/leaderboard.service.ts b/apps/api/src/leaderboards/leaderboard.service.ts index 58a15af98..d08ed7e04 100644 --- a/apps/api/src/leaderboards/leaderboard.service.ts +++ b/apps/api/src/leaderboards/leaderboard.service.ts @@ -13,6 +13,7 @@ import { InjectRedis } from "#redis"; import { Injectable, InternalServerErrorException } from "@nestjs/common"; import { type LeaderboardEnabledMetadata, getLeaderboardField, getLeaderboardFields } from "@statsify/schemas"; import { LeaderboardQuery } from "@statsify/api-client"; +import { Logger } from "@statsify/logger"; import { Redis } from "ioredis"; const DAYS_IN_WEEK = { @@ -29,6 +30,8 @@ export type LeaderboardAdditionalStats = Record & { name: string }; @Injectable() export abstract class LeaderboardService { + private readonly logger = new Logger("LeaderboardService"); + public constructor(@InjectRedis() protected readonly redis: Redis) {} public async addLeaderboards( @@ -135,17 +138,31 @@ export abstract class LeaderboardService { getLeaderboardField(constructor, extraDisplay, false) : undefined; - const additionalStats = await this.getAdditionalStats( - leaderboard.map(({ id }) => id), - [ - ...additionalFields.filter((k) => k !== field), - ...(extraDisplay ? [extraDisplay] : []), - ] - ); - - const data = leaderboard.map((doc, index) => { + const additionalStats = leaderboard.length > 0 ? + await this.getAdditionalStats( + leaderboard.map(({ id }) => id), + [ + ...additionalFields.filter((k) => k !== field), + ...(extraDisplay ? [extraDisplay] : []), + ] + ) : + []; + + // Rows whose document is missing from Mongo are left out; the rest keep their Redis position + const rows = leaderboard.flatMap((doc, index) => { const stats = additionalStats[index]; + return stats ? [{ doc, stats }] : []; + }); + + const missing = leaderboard.length - rows.length; + + if (missing) { + this.logger.warn( + `${missing} of ${leaderboard.length} rows on ${constructor.name} ${field} page ${top / PAGE_SIZE} have no document` + ); + } + const data = rows.map(({ doc, stats }) => { if (extraDisplay) { const extraDisplayValue = stats[extraDisplay] ?? extraDisplayMetadata?.default; // Only add the extra display if it exists @@ -265,7 +282,7 @@ export abstract class LeaderboardService { protected abstract getAdditionalStats( ids: string[], fields: string[] - ): Promise; + ): Promise<(LeaderboardAdditionalStats | null)[]>; private async getLeaderboardFromRedis( constructor: Constructor, diff --git a/apps/api/src/player/leaderboards/player-leaderboard.service.ts b/apps/api/src/player/leaderboards/player-leaderboard.service.ts index 5877de527..0beac0367 100644 --- a/apps/api/src/player/leaderboards/player-leaderboard.service.ts +++ b/apps/api/src/player/leaderboards/player-leaderboard.service.ts @@ -48,29 +48,34 @@ export class PlayerLeaderboardService extends LeaderboardService { protected async getAdditionalStats( ids: string[], fields: string[] - ): Promise { + ): Promise<(LeaderboardAdditionalStats | null)[]> { const selector = fields.reduce((acc, key) => { acc[key] = true; return acc; }, {} as Record); selector.displayName = true; + selector.uuid = true; - return await Promise.all( - ids.map(async (id) => { - const player = await this.playerModel - .findOne() - .where("uuid") - .equals(id) - .select(selector) - .lean() - .exec(); + const players = await this.playerModel + .find({ uuid: { $in: ids } }) + .select(selector) + .lean() + .exec(); - const additionalStats = flatten(player) as LeaderboardAdditionalStats; - additionalStats.name = additionalStats.displayName; + const playersById = new Map(players.map((player) => [player.uuid, player])); - return additionalStats; - }) - ); + return ids.map((id) => { + const player = playersById.get(id); + + if (!player) { + return null; + } + + const additionalStats = flatten(player) as LeaderboardAdditionalStats; + additionalStats.name = additionalStats.displayName; + + return additionalStats; + }); } } From 5309fc247e3750b005db8baa78d049be6773ccd3 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:30:01 -0400 Subject: [PATCH 2/3] refactor(api): key leaderboard additional stats by id --- .../leaderboards/guild-leaderboard.service.ts | 22 +++++-------- .../src/leaderboards/leaderboard.service.ts | 33 +++++++++---------- .../player-leaderboard.service.ts | 22 +++++-------- 3 files changed, 31 insertions(+), 46 deletions(-) diff --git a/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts b/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts index f1f11babd..8fc11aec9 100644 --- a/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts +++ b/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts @@ -49,7 +49,7 @@ export class GuildLeaderboardService extends LeaderboardService { protected async getAdditionalStats( ids: string[], fields: string[] - ): Promise<(LeaderboardAdditionalStats | null)[]> { + ): Promise> { const selector = fields.reduce((acc, key) => { acc[key] = true; return acc; @@ -64,19 +64,13 @@ export class GuildLeaderboardService extends LeaderboardService { .lean() .exec(); - const guildsById = new Map(guilds.map((guild) => [guild.id, guild])); + return new Map( + guilds.map((guild) => { + const additionalStats = flatten(guild) as LeaderboardAdditionalStats; + additionalStats.name = additionalStats.nameFormatted; - return ids.map((id) => { - const guild = guildsById.get(id); - - if (!guild) { - return null; - } - - const additionalStats = flatten(guild) as LeaderboardAdditionalStats; - additionalStats.name = additionalStats.nameFormatted; - - return additionalStats; - }); + return [guild.id, additionalStats]; + }) + ); } } diff --git a/apps/api/src/leaderboards/leaderboard.service.ts b/apps/api/src/leaderboards/leaderboard.service.ts index d08ed7e04..d086fe462 100644 --- a/apps/api/src/leaderboards/leaderboard.service.ts +++ b/apps/api/src/leaderboards/leaderboard.service.ts @@ -146,23 +146,12 @@ export abstract class LeaderboardService { ...(extraDisplay ? [extraDisplay] : []), ] ) : - []; + new Map(); - // Rows whose document is missing from Mongo are left out; the rest keep their Redis position - const rows = leaderboard.flatMap((doc, index) => { - const stats = additionalStats[index]; - return stats ? [{ doc, stats }] : []; - }); - - const missing = leaderboard.length - rows.length; - - if (missing) { - this.logger.warn( - `${missing} of ${leaderboard.length} rows on ${constructor.name} ${field} page ${top / PAGE_SIZE} have no document` - ); - } + const data = leaderboard.flatMap((doc) => { + const stats = additionalStats.get(doc.id); + if (!stats) return []; - const data = rows.map(({ doc, stats }) => { if (extraDisplay) { const extraDisplayValue = stats[extraDisplay] ?? extraDisplayMetadata?.default; // Only add the extra display if it exists @@ -185,15 +174,23 @@ export abstract class LeaderboardService { if (!hidden) fields.push(field); fields.push(...additionalValues); - return { + return [{ id: doc.id, fields, name: stats.name, position: doc.index + 1, highlight: doc.index === highlight, - }; + }]; }); + const missing = leaderboard.length - data.length; + + if (missing) { + this.logger.warn( + `${missing} of ${leaderboard.length} rows on ${constructor.name} ${field} page ${top / PAGE_SIZE} have no document` + ); + } + const fields = []; if (!hidden) fields.push(fieldName); fields.push(...additionalFieldMetadata.map(({ fieldName }) => fieldName)); @@ -282,7 +279,7 @@ export abstract class LeaderboardService { protected abstract getAdditionalStats( ids: string[], fields: string[] - ): Promise<(LeaderboardAdditionalStats | null)[]>; + ): Promise>; private async getLeaderboardFromRedis( constructor: Constructor, diff --git a/apps/api/src/player/leaderboards/player-leaderboard.service.ts b/apps/api/src/player/leaderboards/player-leaderboard.service.ts index 0beac0367..1ba9e365d 100644 --- a/apps/api/src/player/leaderboards/player-leaderboard.service.ts +++ b/apps/api/src/player/leaderboards/player-leaderboard.service.ts @@ -48,7 +48,7 @@ export class PlayerLeaderboardService extends LeaderboardService { protected async getAdditionalStats( ids: string[], fields: string[] - ): Promise<(LeaderboardAdditionalStats | null)[]> { + ): Promise> { const selector = fields.reduce((acc, key) => { acc[key] = true; return acc; @@ -63,19 +63,13 @@ export class PlayerLeaderboardService extends LeaderboardService { .lean() .exec(); - const playersById = new Map(players.map((player) => [player.uuid, player])); + return new Map( + players.map((player) => { + const additionalStats = flatten(player) as LeaderboardAdditionalStats; + additionalStats.name = additionalStats.displayName; - return ids.map((id) => { - const player = playersById.get(id); - - if (!player) { - return null; - } - - const additionalStats = flatten(player) as LeaderboardAdditionalStats; - additionalStats.name = additionalStats.displayName; - - return additionalStats; - }); + return [player.uuid, additionalStats]; + }) + ); } } From ab9375e19112211d527be1db5ad81f30e20a57ce Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:30:06 -0400 Subject: [PATCH 3/3] refactor(api): drop the empty leaderboard page guard --- apps/api/src/leaderboards/leaderboard.service.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/api/src/leaderboards/leaderboard.service.ts b/apps/api/src/leaderboards/leaderboard.service.ts index d086fe462..2ab33356d 100644 --- a/apps/api/src/leaderboards/leaderboard.service.ts +++ b/apps/api/src/leaderboards/leaderboard.service.ts @@ -138,15 +138,13 @@ export abstract class LeaderboardService { getLeaderboardField(constructor, extraDisplay, false) : undefined; - const additionalStats = leaderboard.length > 0 ? - await this.getAdditionalStats( - leaderboard.map(({ id }) => id), - [ - ...additionalFields.filter((k) => k !== field), - ...(extraDisplay ? [extraDisplay] : []), - ] - ) : - new Map(); + const additionalStats = await this.getAdditionalStats( + leaderboard.map(({ id }) => id), + [ + ...additionalFields.filter((k) => k !== field), + ...(extraDisplay ? [extraDisplay] : []), + ] + ); const data = leaderboard.flatMap((doc) => { const stats = additionalStats.get(doc.id);