From c4897a0ab601b56f220501cae801723a591db058 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:05:59 -0400 Subject: [PATCH 1/7] fix(api): look up guild member names in one query A guild refresh looked up each expired member name through PlayerService.get, one findOne and a full deserialize per member. Fetch them all with one find and a projection; members it doesn't find still go through PlayerService. --- apps/api/src/guild/guild.service.ts | 58 ++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/apps/api/src/guild/guild.service.ts b/apps/api/src/guild/guild.service.ts index e81e74686..190b3774a 100644 --- a/apps/api/src/guild/guild.service.ts +++ b/apps/api/src/guild/guild.service.ts @@ -22,6 +22,11 @@ import { PlayerService } from "#player"; import { flatten } from "@statsify/util"; import type { ReturnModelType } from "@typegoose/typegoose"; +type MemberName = Pick; + +const isNameCached = (cachedMember?: GuildMember): cachedMember is GuildMember => + !!cachedMember && Date.now() < cachedMember.expiresAt; + @Injectable() export class GuildService { private readonly logger = new Logger("GuildService"); @@ -70,10 +75,17 @@ export class GuildService { const guildExpHistory: Record = {}; const requireGuildId: string[] = []; + // Look up every expired name in one query instead of one per member + const memberNames = await this.getMemberNames( + guild.members + .filter((member) => !isNameCached(memberMap[member.uuid])) + .map((member) => member.uuid) + ); + const fetchMembers = guild.members.map(async (member) => { const cacheMember = memberMap[member.uuid]; - await this.getMemberName(member, cacheMember); + await this.getMemberName(member, cacheMember, memberNames.get(member.uuid)); // These members will need their player document updated with the correct guild id if (member.guildId !== guild.id) { @@ -208,8 +220,31 @@ export class GuildService { return await this.guildModel.deleteOne({ id: cachedGuild.id }).lean().exec(); } - private async getMemberName(member: GuildMember, cachedMember?: GuildMember) { - if (cachedMember && Date.now() < cachedMember.expiresAt) { + private async getMemberNames(uuids: string[]): Promise> { + if (uuids.length === 0) return new Map(); + + const players = await this.playerModel + .find({ uuid: { $in: uuids } }) + .select({ uuid: true, username: true, displayName: true, guildId: true }) + .lean() + .exec() + .catch(() => []); + + // Missing fields default to "", the same as deserialize + return new Map( + players.map(({ uuid, username = "", displayName = "", guildId = "" }) => [ + uuid, + { username, displayName, guildId }, + ]) + ); + } + + private async getMemberName( + member: GuildMember, + cachedMember?: GuildMember, + memberName?: MemberName + ) { + if (isNameCached(cachedMember)) { member.displayName = cachedMember.displayName; member.username = cachedMember.username; member.expiresAt = cachedMember.expiresAt; @@ -217,13 +252,16 @@ export class GuildService { return; } - const player = await this.playerService - .get(member.uuid, CacheLevel.CACHE_ONLY, { - username: true, - displayName: true, - guildId: true, - }) - .catch(() => null); + // Members the query missed go through PlayerService, which fetches unknown players from Hypixel + const player = + memberName ?? + (await this.playerService + .get(member.uuid, CacheLevel.CACHE_ONLY, { + username: true, + displayName: true, + guildId: true, + }) + .catch(() => null)); if (player) { member.username = player.username; From 9ba829e8195128c97fcffe0b86e94ebf0a3b64a9 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:23:49 -0400 Subject: [PATCH 2/7] refactor(api): drop name defaults the guild refresh never needs --- apps/api/src/guild/guild.service.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/api/src/guild/guild.service.ts b/apps/api/src/guild/guild.service.ts index 190b3774a..1e169d973 100644 --- a/apps/api/src/guild/guild.service.ts +++ b/apps/api/src/guild/guild.service.ts @@ -230,13 +230,7 @@ export class GuildService { .exec() .catch(() => []); - // Missing fields default to "", the same as deserialize - return new Map( - players.map(({ uuid, username = "", displayName = "", guildId = "" }) => [ - uuid, - { username, displayName, guildId }, - ]) - ); + return new Map(players.map((player) => [player.uuid, player])); } private async getMemberName( From 0ccfc4f6b25ecb8ca6de2b4825f9cb815ac560fd Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:23:53 -0400 Subject: [PATCH 3/7] refactor(api): drop guild member names comment about the old code --- apps/api/src/guild/guild.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/api/src/guild/guild.service.ts b/apps/api/src/guild/guild.service.ts index 1e169d973..3bb2e951b 100644 --- a/apps/api/src/guild/guild.service.ts +++ b/apps/api/src/guild/guild.service.ts @@ -75,7 +75,6 @@ export class GuildService { const guildExpHistory: Record = {}; const requireGuildId: string[] = []; - // Look up every expired name in one query instead of one per member const memberNames = await this.getMemberNames( guild.members .filter((member) => !isNameCached(memberMap[member.uuid])) From 37aee17fc368ebb7f2a617c40fc33208c5af6772 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:53:19 -0400 Subject: [PATCH 4/7] fix(api): error when the guild member name query fails --- apps/api/src/guild/guild.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/api/src/guild/guild.service.ts b/apps/api/src/guild/guild.service.ts index 3bb2e951b..62b974927 100644 --- a/apps/api/src/guild/guild.service.ts +++ b/apps/api/src/guild/guild.service.ts @@ -226,8 +226,7 @@ export class GuildService { .find({ uuid: { $in: uuids } }) .select({ uuid: true, username: true, displayName: true, guildId: true }) .lean() - .exec() - .catch(() => []); + .exec(); return new Map(players.map((player) => [player.uuid, player])); } From 1c123cff0335b1008ecd0be572071284328f917b Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:35:32 -0500 Subject: [PATCH 5/7] fix(api): resolve missing guild members sequentially --- apps/api/src/guild/guild.service.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/api/src/guild/guild.service.ts b/apps/api/src/guild/guild.service.ts index 2a6e77e82..5d74061a2 100644 --- a/apps/api/src/guild/guild.service.ts +++ b/apps/api/src/guild/guild.service.ts @@ -81,9 +81,10 @@ export class GuildService { .map((member) => member.uuid) ); - const fetchMembers = guild.members.map(async (member) => { + for (const member of guild.members) { const cacheMember = memberMap[member.uuid]; + // eslint-disable-next-line no-await-in-loop -- Missing players must be fetched from Hypixel one at a time. await this.getMemberName(member, cacheMember, memberNames.get(member.uuid)); // These members will need their player document updated with the correct guild id @@ -112,11 +113,7 @@ export class GuildService { } guild.questParticipation = guild.questParticipation + member.questParticipation; - - return member; - }); - - guild.members = await Promise.all(fetchMembers); + } await this.playerModel .updateMany({ uuid: { $in: requireGuildId } }, { guildId: guild.id }) From f3c465de501016da65b30f400152c4b76179d99c Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:43:31 -0500 Subject: [PATCH 6/7] fix(api): preserve concurrent guild member resolution --- apps/api/src/guild/guild.service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/api/src/guild/guild.service.ts b/apps/api/src/guild/guild.service.ts index 5d74061a2..2a6e77e82 100644 --- a/apps/api/src/guild/guild.service.ts +++ b/apps/api/src/guild/guild.service.ts @@ -81,10 +81,9 @@ export class GuildService { .map((member) => member.uuid) ); - for (const member of guild.members) { + const fetchMembers = guild.members.map(async (member) => { const cacheMember = memberMap[member.uuid]; - // eslint-disable-next-line no-await-in-loop -- Missing players must be fetched from Hypixel one at a time. await this.getMemberName(member, cacheMember, memberNames.get(member.uuid)); // These members will need their player document updated with the correct guild id @@ -113,7 +112,11 @@ export class GuildService { } guild.questParticipation = guild.questParticipation + member.questParticipation; - } + + return member; + }); + + guild.members = await Promise.all(fetchMembers); await this.playerModel .updateMany({ uuid: { $in: requireGuildId } }, { guildId: guild.id }) From 206aa006cf8b506f60ebb3925341a12550ccba7a Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 5 Nov 2026 02:58:23 -0500 Subject: [PATCH 7/7] refactor(api): cut the guild member fallback comment to the CACHE_ONLY quirk --- apps/api/src/guild/guild.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/guild/guild.service.ts b/apps/api/src/guild/guild.service.ts index 2a6e77e82..22eac880f 100644 --- a/apps/api/src/guild/guild.service.ts +++ b/apps/api/src/guild/guild.service.ts @@ -244,7 +244,7 @@ export class GuildService { return; } - // Members the query missed go through PlayerService, which fetches unknown players from Hypixel + // CACHE_ONLY falls back to Hypixel for players Mongo doesn't have const player = memberName ?? (await this.playerService