Files
www/src/server/services/leaderboard.ts
2025-04-18 13:48:15 +02:00

79 lines
2.0 KiB
TypeScript

import { redis } from '../redis'
import { neatqueue_service } from './neatqueue.service'
export class LeaderboardService {
private getZSetKey(channel_id: string) {
return `zset:leaderboard:${channel_id}`
}
private getRawKey(channel_id: string) {
return `raw:leaderboard:${channel_id}`
}
private getUserKey(user_id: string, channel_id: string) {
return `user:${user_id}:${channel_id}`
}
async refreshLeaderboard(channel_id: string) {
try {
const fresh = await neatqueue_service.get_leaderboard(channel_id)
const zsetKey = this.getZSetKey(channel_id)
const rawKey = this.getRawKey(channel_id)
const pipeline = redis.pipeline()
pipeline.setex(rawKey, 180, JSON.stringify(fresh))
pipeline.del(zsetKey)
for (const entry of fresh) {
pipeline.zadd(zsetKey, entry.mmr, entry.id)
pipeline.hset(this.getUserKey(entry.id, channel_id), {
...entry,
channel_id,
})
}
pipeline.expire(zsetKey, 180)
await pipeline.exec()
return fresh
} catch (error) {
console.error('Error refreshing leaderboard:', error)
throw error
}
}
async getLeaderboard(channel_id: string) {
try {
const cached = await redis.get(this.getRawKey(channel_id))
if (cached) return JSON.parse(cached)
return await this.refreshLeaderboard(channel_id)
} catch (error) {
console.error('Error getting leaderboard:', error)
throw error
}
}
async getUserRank(channel_id: string, user_id: string) {
try {
const zsetKey = this.getZSetKey(channel_id)
const rank = await redis.zrevrank(zsetKey, user_id)
if (rank === null) return null
const userData = await redis.hgetall(this.getUserKey(user_id, channel_id))
if (!userData) return null
return {
rank: rank + 1,
...userData,
mmr: Number(userData.mmr),
streak: userData.streak,
}
} catch (error) {
console.error('Error getting user rank:', error)
throw error
}
}
}