From 3b8d413a087de69e220581a6d84da132e4488dd7 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:42:38 -0400 Subject: [PATCH 1/2] fix(api): save players and sessions to Mongoose as nested documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlayerService.saveOne and SessionService.resetPlayer handed Mongoose the flat serialized document, with keys like "stats.bedwars.overall.wins". Mongoose casts a replacement by building a document and setting each key, and its change tracking rescans every modified path on each set, so a save with K keys cost O(K²): about 100 ms for a player with 3,200 keys. Passing the same document unflattened avoids that: about 18 ms for that player, and 6.5 ms instead of 12 ms for an average one. The stored documents are byte-identical, BSON number types and key order included. Guild saves keep the flat document. Their cost is casting the members array, not the dotted keys, so nesting doesn't change it. --- apps/api/src/player/player.service.ts | 7 ++++++- apps/api/src/session/session.service.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/api/src/player/player.service.ts b/apps/api/src/player/player.service.ts index 0d5ad6218..cb682ed52 100644 --- a/apps/api/src/player/player.service.ts +++ b/apps/api/src/player/player.service.ts @@ -11,6 +11,7 @@ import { type Circular, type Flatten, flatten, + unflatten, } from "@statsify/util"; import { CacheLevel, StatusNotFoundException } from "@statsify/api-client"; import { HypixelService } from "#hypixel"; @@ -167,8 +168,12 @@ export class PlayerService { const serializedPlayer = serialize(Player, flatPlayer); const promises = [ + // Mongoose gets the nested document: with thousands of dotted keys, its change + // tracking makes the replace O(n²) this.playerModel - .replaceOne({ uuid: player.uuid }, serializedPlayer, { upsert: true }) + .replaceOne({ uuid: player.uuid }, unflatten(serializedPlayer), { + upsert: true, + }) .exec(), this.playerLeaderboardService.addLeaderboards( Player, diff --git a/apps/api/src/session/session.service.ts b/apps/api/src/session/session.service.ts index 384e89041..46f5f7621 100644 --- a/apps/api/src/session/session.service.ts +++ b/apps/api/src/session/session.service.ts @@ -11,7 +11,7 @@ import { PlayerNotFoundException, SessionNotFoundException, } from "@statsify/api-client"; -import { type Circular, flatten } from "@statsify/util"; +import { type Circular, flatten, unflatten } from "@statsify/util"; import { DateTime } from "luxon"; import { Inject, @@ -59,7 +59,7 @@ export class SessionService { doc.sessionReset = Math.round(DateTime.now().toMillis() / 1000); - await this.sessionModel.replaceOne({ uuid: doc.uuid }, doc, { upsert: true }).lean().exec(); + await this.sessionModel.replaceOne({ uuid: doc.uuid }, unflatten(doc), { upsert: true }).lean().exec(); return deserialize(Player, flatPlayer); } From ea24bf1a5f3b26c8a49b4911f3089d3a614a036c Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 5 Nov 2026 02:58:08 -0500 Subject: [PATCH 2/2] refactor(api): shorten the nested player save comment --- apps/api/src/player/player.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/api/src/player/player.service.ts b/apps/api/src/player/player.service.ts index 1614e19bf..91059f65c 100644 --- a/apps/api/src/player/player.service.ts +++ b/apps/api/src/player/player.service.ts @@ -177,8 +177,7 @@ export class PlayerService { const serializedPlayer = serialize(Player, flatPlayer); const promises = [ - // Mongoose gets the nested document: with thousands of dotted keys, its change - // tracking makes the replace O(n²) + // Thousands of dotted keys make Mongoose's change tracking O(n²) this.playerModel .replaceOne({ uuid: player.uuid }, unflatten(serializedPlayer), { upsert: true,