From 9fbe351470b6a4d0261b58d6a94aa5f702c1099f Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 28 May 2026 23:08:32 +0000 Subject: [PATCH] implemented the leaderboard --- .../leaderboard.refresh.job.spec.ts | 33 +++++++++++++++++++ .../leaderboard/leaderboard.refresh.job.ts | 6 ++-- .../leaderboard/leaderboard.service.ts | 30 +++++++++++------ 3 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 src/modules/leaderboard/leaderboard.refresh.job.spec.ts diff --git a/src/modules/leaderboard/leaderboard.refresh.job.spec.ts b/src/modules/leaderboard/leaderboard.refresh.job.spec.ts new file mode 100644 index 0000000..375915a --- /dev/null +++ b/src/modules/leaderboard/leaderboard.refresh.job.spec.ts @@ -0,0 +1,33 @@ +import { LeaderboardRefreshJob } from './leaderboard.refresh.job'; + +describe('LeaderboardRefreshJob', () => { + it('clears leaderboards and calls bulkUpdateScores with users from prisma', async () => { + const mockLeaderboardService: any = { + clearLeaderboard: jest.fn().mockResolvedValue(undefined), + bulkUpdateScores: jest.fn().mockResolvedValue(undefined), + }; + + const mockPrisma: any = { + user: { + findMany: jest.fn().mockResolvedValue([ + { id: 'u1', reputation: 10 }, + { id: 'u2', reputation: 20 }, + ]), + }, + }; + + const job = new LeaderboardRefreshJob(mockLeaderboardService, mockPrisma); + + // call refresh + await job.refreshLeaderboard(); + + expect(mockLeaderboardService.clearLeaderboard).toHaveBeenCalledWith('global'); + expect(mockLeaderboardService.clearLeaderboard).toHaveBeenCalledWith('weekly'); + expect(mockLeaderboardService.bulkUpdateScores).toHaveBeenCalledWith( + [ + { id: 'u1', reputation: 10 }, + { id: 'u2', reputation: 20 }, + ], + ); + }); +}); diff --git a/src/modules/leaderboard/leaderboard.refresh.job.ts b/src/modules/leaderboard/leaderboard.refresh.job.ts index 77634b7..726626e 100644 --- a/src/modules/leaderboard/leaderboard.refresh.job.ts +++ b/src/modules/leaderboard/leaderboard.refresh.job.ts @@ -28,10 +28,8 @@ export class LeaderboardRefreshJob { await this.leaderboardService.clearLeaderboard('global'); await this.leaderboardService.clearLeaderboard('weekly'); - // Rebuild rankings from real data - for (const user of users) { - await this.leaderboardService.updateScore(user.id, user.reputation); - } + // Rebuild rankings from real data using batched pipelining + await this.leaderboardService.bulkUpdateScores(users); this.logger.log(`Leaderboard refreshed with ${users.length} users.`); } diff --git a/src/modules/leaderboard/leaderboard.service.ts b/src/modules/leaderboard/leaderboard.service.ts index c84b104..a4f1ec9 100644 --- a/src/modules/leaderboard/leaderboard.service.ts +++ b/src/modules/leaderboard/leaderboard.service.ts @@ -14,17 +14,27 @@ export class LeaderboardService { userId: string, score: number, ): Promise { - await this.redis.zadd( - LEADERBOARD_KEYS.GLOBAL, - score, - userId, - ); + // Use a pipeline to reduce round trips for two ZADD operations + const pipeline = this.redis.pipeline(); + pipeline.zadd(LEADERBOARD_KEYS.GLOBAL, score, userId); + pipeline.zadd(LEADERBOARD_KEYS.WEEKLY, score, userId); + await pipeline.exec(); + } - await this.redis.zadd( - LEADERBOARD_KEYS.WEEKLY, - score, - userId, - ); + // Bulk update scores using pipelining in chunks to avoid many roundtrips + async bulkUpdateScores( + users: { id: string; reputation: number }[], + chunkSize = 500, + ): Promise { + for (let i = 0; i < users.length; i += chunkSize) { + const chunk = users.slice(i, i + chunkSize); + const pipeline = this.redis.pipeline(); + for (const u of chunk) { + pipeline.zadd(LEADERBOARD_KEYS.GLOBAL, u.reputation, u.id); + pipeline.zadd(LEADERBOARD_KEYS.WEEKLY, u.reputation, u.id); + } + await pipeline.exec(); + } } // 🔹 Get leaderboard