From 0cecacbd8041d87e19abba497cc06b1454f8249d Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Sun, 27 Jul 2025 20:36:27 +0100 Subject: [PATCH] Centralize Redis Client Usage and Add Caching in LeaderboardService --- package-lock.json | 88 ++++++++++++++++++++++++-- package.json | 3 +- src/app.module.ts | 7 +- src/leaderboard/leaderboard.service.ts | 31 +++++++-- src/redis/redis.constants.ts | 2 + src/redis/redis.module.ts | 23 +++++++ src/redis/redis.provider.ts | 23 +++++++ 7 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 src/redis/redis.constants.ts create mode 100644 src/redis/redis.module.ts create mode 100644 src/redis/redis.provider.ts diff --git a/package-lock.json b/package-lock.json index 46c97bc..e58a455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@nestjs/axios": "^4.0.0", "@nestjs/common": "^11.0.12", - "@nestjs/config": "^4.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.12", "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "^11.0.0", @@ -27,6 +27,7 @@ "class-validator": "^0.14.1", "fast-csv": "^5.0.2", "google-auth-library": "^9.15.1", + "ioredis": "^5.6.1", "oauth2client": "^1.0.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -1341,6 +1342,11 @@ } } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2413,10 +2419,9 @@ } }, "node_modules/@nestjs/config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.1.tgz", - "integrity": "sha512-0hr6lKS//Wf8A6VcV69ts8uD0fke6jtmmmXSxzvwAzOM/HEXEKYEp21nRU+cpYxlYqm7Khb0oTOoVuDGk+AWUw==", - "license": "MIT", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", "dependencies": { "dotenv": "16.4.7", "dotenv-expand": "12.0.1", @@ -5532,6 +5537,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5950,6 +5963,14 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8108,6 +8129,29 @@ "kind-of": "^6.0.2" } }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -9367,6 +9411,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -9385,6 +9434,11 @@ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -11488,6 +11542,25 @@ "esprima": "~4.0.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -12347,6 +12420,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/starknet": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/starknet/-/starknet-6.24.1.tgz", diff --git a/package.json b/package.json index dbf1044..82611b2 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "@nestjs/axios": "^4.0.0", "@nestjs/common": "^11.0.12", - "@nestjs/config": "^4.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.12", "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "^11.0.0", @@ -39,6 +39,7 @@ "class-validator": "^0.14.1", "fast-csv": "^5.0.2", "google-auth-library": "^9.15.1", + "ioredis": "^5.6.1", "oauth2client": "^1.0.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index 4e0f253..813e7ce 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,7 +3,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { EventEmitterModule } from '@nestjs/event-emitter'; - +import { RedisModule } from './redis/redis.module'; import { AuthModule } from './auth/auth.module'; import appConfig from './config/app.config'; import databaseConfig from './config/database.config'; @@ -18,7 +18,7 @@ import { PuzzleModule } from './puzzle/puzzle.module'; import { AppService } from './app.service'; import { AppController } from './app.controller'; import { GamificationModule } from './gamification/gamification.module'; -import { AchievementModule } from './achievement/achievement.module'; +import { AchievementModule } from './achievement/achievement.module'; // const ENV = process.env.NODE_ENV; // console.log('NODE_ENV:', process.env.NODE_ENV); @@ -50,13 +50,14 @@ import { AchievementModule } from './achievement/achievement.module'; UsersModule, LeaderboardModule, CommonModule, + RedisModule, BlockchainModule, BadgeModule, TimeFilterModule, IQAssessmentModule, PuzzleModule, GamificationModule, - AchievementModule + AchievementModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/leaderboard/leaderboard.service.ts b/src/leaderboard/leaderboard.service.ts index c72c5cb..eafa116 100644 --- a/src/leaderboard/leaderboard.service.ts +++ b/src/leaderboard/leaderboard.service.ts @@ -1,4 +1,7 @@ -import { Injectable } from '@nestjs/common'; +/* eslint-disable prettier/prettier */ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +import { REDIS_CLIENT } from 'src/redis/redis.constants'; import { LeaderboardQueryDto, SortBy, @@ -15,10 +18,30 @@ export class LeaderboardService { private readonly getLeaderboardService: GetLeaderboardProvider, private readonly updatePlayerStatsService: UpdatePlayerStatsProvider, private readonly getUserRankService: GetUserRankProvider, + @Inject(REDIS_CLIENT) private readonly redis: Redis, ) {} - getLeaderboard(query: LeaderboardQueryDto) { - return this.getLeaderboardService.execute(query); + async getLeaderboard(query: LeaderboardQueryDto): Promise { + const cacheKey = `leaderboard:${query.sort}:${query.period}:${query.limit}:${query.offset}`; + + try { + const cached = await this.redis.get(cacheKey); + if (cached) { + return JSON.parse(cached) as any[]; + } + } catch (err) { + console.warn('Redis read error:', err); + } + + const result = await this.getLeaderboardService.execute(query); + + try { + await this.redis.set(cacheKey, JSON.stringify(result), 'EX', 60); + } catch (err) { + console.warn('Redis write error:', err); + } + + return result; } updatePlayerStats(userId: string, dto: UpdateLeaderboardDto) { @@ -37,4 +60,4 @@ export class LeaderboardService { offset: 0, }); } -} \ No newline at end of file +} diff --git a/src/redis/redis.constants.ts b/src/redis/redis.constants.ts new file mode 100644 index 0000000..0905d76 --- /dev/null +++ b/src/redis/redis.constants.ts @@ -0,0 +1,2 @@ +/* eslint-disable prettier/prettier */ +export const REDIS_CLIENT = 'REDIS_CLIENT'; diff --git a/src/redis/redis.module.ts b/src/redis/redis.module.ts new file mode 100644 index 0000000..bc3d021 --- /dev/null +++ b/src/redis/redis.module.ts @@ -0,0 +1,23 @@ +/* eslint-disable prettier/prettier */ +import { Global, Module, OnModuleDestroy, Inject } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { redisProvider } from './redis.provider'; +import { REDIS_CLIENT } from './redis.constants'; +import Redis from 'ioredis'; + +@Global() +@Module({ + imports: [ConfigModule], + providers: [redisProvider], + exports: [redisProvider], +}) +export class RedisModule implements OnModuleDestroy { + constructor(@Inject(REDIS_CLIENT) private readonly redis: Redis) {} + + async onModuleDestroy() { + if (this.redis) { + await this.redis.quit(); + console.log('🧹 Redis disconnected gracefully'); + } + } +} diff --git a/src/redis/redis.provider.ts b/src/redis/redis.provider.ts new file mode 100644 index 0000000..1857e13 --- /dev/null +++ b/src/redis/redis.provider.ts @@ -0,0 +1,23 @@ +/* eslint-disable prettier/prettier */ +import { Provider } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Redis from 'ioredis'; +import { REDIS_CLIENT } from './redis.constants'; + +export const redisProvider: Provider = { + provide: REDIS_CLIENT, + useFactory: (configService: ConfigService) => { + const redisUrl = configService.get('REDIS_URL'); + if (!redisUrl) { + throw new Error('REDIS_URL not defined in environment variables'); + } + + const client = new Redis(redisUrl); + + client.on('connect', () => console.log('Redis connected')); + client.on('error', (err) => console.error('Redis error:', err)); + + return client; + }, + inject: [ConfigService], +};