adjust styling and store users profiles according to the leaderboard

This commit is contained in:
2025-04-04 15:29:09 +02:00
parent 04b3e68214
commit 5944a5418d
3 changed files with 168 additions and 182 deletions

View File

@@ -1,5 +1,6 @@
import { redis } from '../redis'
import { neatqueue_service } from './neatqueue.service'
export class LeaderboardService {
private getZSetKey(channel_id: string) {
return `zset:leaderboard:${channel_id}`
@@ -9,79 +10,68 @@ export class LeaderboardService {
return `raw:leaderboard:${channel_id}`
}
private getUserKey(user_id: string, channel_id: string) {
return `user:${user_id}:${channel_id}`
}
async refreshLeaderboard(channel_id: string) {
const fresh = await neatqueue_service.get_leaderboard(channel_id)
const zsetKey = this.getZSetKey(channel_id)
const rawKey = this.getRawKey(channel_id)
try {
const fresh = await neatqueue_service.get_leaderboard(channel_id)
const zsetKey = this.getZSetKey(channel_id)
const rawKey = this.getRawKey(channel_id)
// store raw data for full queries
await redis.setex(rawKey, 180, JSON.stringify(fresh))
const pipeline = redis.pipeline()
pipeline.setex(rawKey, 180, JSON.stringify(fresh))
pipeline.del(zsetKey)
// store sorted set for rank queries
const pipeline = redis.pipeline()
pipeline.del(zsetKey) // clear existing
for (const entry of fresh) {
pipeline.zadd(zsetKey, entry.mmr, entry.id)
pipeline.hset(this.getUserKey(entry.id, channel_id), {
...entry,
channel_id,
})
}
for (const entry of fresh) {
// store by mmr for ranking
pipeline.zadd(zsetKey, entry.rank, entry.id)
pipeline.expire(zsetKey, 180)
await pipeline.exec()
// store user data separately for quick lookups
pipeline.hset(`user:${entry.id}`, entry)
return fresh
} catch (error) {
console.error('Error refreshing leaderboard:', error)
throw error
}
pipeline.expire(zsetKey, 180)
await pipeline.exec()
}
async getLeaderboard(channel_id: string) {
const cached = await redis.get(this.getRawKey(channel_id))
if (cached) return JSON.parse(cached)
try {
const cached = await redis.get(this.getRawKey(channel_id))
if (cached) return JSON.parse(cached)
// if not cached, refresh and return
await this.refreshLeaderboard(channel_id)
// @ts-ignore
return redis.get(this.getRawKey(channel_id)).then(JSON.parse)
}
async getUserRank(channel_id: string, user_id: string) {
const zsetKey = this.getZSetKey(channel_id)
// zrevrank because higher mmr = better rank
const rank = await redis.zrevrank(zsetKey, user_id)
if (rank === null) return null
// get user data
const userData = await redis.hgetall(`user:${user_id}`)
if (!userData) return null
return {
rank: rank + 1, // zero-based -> one-based
...userData,
return await this.refreshLeaderboard(channel_id)
} catch (error) {
console.error('Error getting leaderboard:', error)
throw error
}
}
// get users around a specific rank
async getRankRange(channel_id: string, rank: number, range = 5) {
const zsetKey = this.getZSetKey(channel_id)
async getUserRank(channel_id: string, user_id: string) {
try {
const zsetKey = this.getZSetKey(channel_id)
const rank = await redis.zrevrank(zsetKey, user_id)
// get ids
const ids = await redis.zrevrange(
zsetKey,
Math.max(0, rank - range),
rank + range
)
if (rank === null) return null
// get data for each id
const pipeline = redis.pipeline()
// biome-ignore lint/complexity/noForEach: <explanation>
ids.forEach((id) => pipeline.hgetall(`user:${id}`))
const userData = await redis.hgetall(this.getUserKey(user_id, channel_id))
if (!userData) return null
const results = await pipeline.exec()
return ids.map((id, i) => ({
id,
rank: rank - range + i + 1,
// @ts-ignore
...results[i][1],
}))
return {
rank: rank + 1,
...userData,
mmr: Number(userData.mmr),
}
} catch (error) {
console.error('Error getting user rank:', error)
throw error
}
}
}