mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 05:19:23 +00:00
leaderboard error handling
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -131,22 +132,27 @@ export function LeaderboardPage() {
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
|
||||
|
||||
// Fetch leaderboard data
|
||||
const [rankedLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery({
|
||||
channel_id: RANKED_CHANNEL,
|
||||
})
|
||||
const [rankedLeaderboardResult] =
|
||||
api.leaderboard.get_leaderboard.useSuspenseQuery({
|
||||
channel_id: RANKED_CHANNEL,
|
||||
})
|
||||
|
||||
const [vanillaLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery(
|
||||
{
|
||||
const [vanillaLeaderboardResult] =
|
||||
api.leaderboard.get_leaderboard.useSuspenseQuery({
|
||||
channel_id: VANILLA_CHANNEL,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Get the current leaderboard based on selected tab
|
||||
const currentLeaderboard = useMemo(
|
||||
const currentLeaderboardResult = useMemo(
|
||||
() =>
|
||||
leaderboardType === 'ranked' ? rankedLeaderboard : vanillaLeaderboard,
|
||||
[leaderboardType, rankedLeaderboard, vanillaLeaderboard]
|
||||
leaderboardType === 'ranked'
|
||||
? rankedLeaderboardResult
|
||||
: vanillaLeaderboardResult,
|
||||
[leaderboardType, rankedLeaderboardResult, vanillaLeaderboardResult]
|
||||
)
|
||||
|
||||
const currentLeaderboard = currentLeaderboardResult.data
|
||||
|
||||
const filteredLeaderboard = useMemo(
|
||||
() =>
|
||||
currentLeaderboard.filter((entry) =>
|
||||
@@ -238,6 +244,16 @@ export function LeaderboardPage() {
|
||||
<div className='flex flex-1 flex-col overflow-hidden'>
|
||||
<div className='mx-auto flex w-[calc(100%-1rem)] max-w-fd-container flex-1 flex-col'>
|
||||
<div className='flex flex-1 flex-col overflow-hidden border-none'>
|
||||
{currentLeaderboardResult.isStale && (
|
||||
<Alert className='my-4 border-amber-500 bg-amber-50 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300'>
|
||||
<AlertTitle>Stale Data</AlertTitle>
|
||||
<AlertDescription>
|
||||
The leaderboard data is currently stale due to issues with the
|
||||
neatqueue service. We're showing you the latest available data.
|
||||
Please check back later.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Tabs
|
||||
defaultValue={leaderboardType}
|
||||
value={leaderboardType}
|
||||
|
||||
@@ -12,9 +12,11 @@ export const leaderboard_router = createTRPCRouter({
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return (await service.getLeaderboard(
|
||||
input.channel_id
|
||||
)) as LeaderboardEntry[]
|
||||
const result = await service.getLeaderboard(input.channel_id)
|
||||
return {
|
||||
data: result.data as LeaderboardEntry[],
|
||||
isStale: result.isStale
|
||||
}
|
||||
}),
|
||||
get_user_rank: publicProcedure
|
||||
.input(
|
||||
@@ -24,6 +26,11 @@ export const leaderboard_router = createTRPCRouter({
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await service.getUserRank(input.channel_id, input.user_id)
|
||||
const result = await service.getUserRank(input.channel_id, input.user_id)
|
||||
if (!result) return null
|
||||
return {
|
||||
data: result.data,
|
||||
isStale: result.isStale
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { redis } from '../redis'
|
||||
import { type LeaderboardEntry, neatqueue_service } from './neatqueue.service'
|
||||
import { db } from '@/server/db'
|
||||
import { metadata } from '@/server/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
export type LeaderboardResponse = {
|
||||
data: LeaderboardEntry[]
|
||||
isStale: boolean
|
||||
}
|
||||
|
||||
export type UserRankResponse = {
|
||||
data: LeaderboardEntry
|
||||
isStale: boolean
|
||||
} | null
|
||||
|
||||
export class LeaderboardService {
|
||||
private getZSetKey(channel_id: string) {
|
||||
@@ -14,11 +27,16 @@ export class LeaderboardService {
|
||||
return `user:${user_id}:${channel_id}`
|
||||
}
|
||||
|
||||
async refreshLeaderboard(channel_id: string) {
|
||||
private getBackupKey(channel_id: string) {
|
||||
return `backup_leaderboard_${channel_id}`
|
||||
}
|
||||
|
||||
async refreshLeaderboard(channel_id: string): Promise<LeaderboardResponse> {
|
||||
try {
|
||||
const fresh = await neatqueue_service.get_leaderboard(channel_id)
|
||||
const zsetKey = this.getZSetKey(channel_id)
|
||||
const rawKey = this.getRawKey(channel_id)
|
||||
const backupKey = this.getBackupKey(channel_id)
|
||||
|
||||
const pipeline = redis.pipeline()
|
||||
pipeline.setex(rawKey, 180, JSON.stringify(fresh))
|
||||
@@ -35,38 +53,134 @@ export class LeaderboardService {
|
||||
pipeline.expire(zsetKey, 180)
|
||||
await pipeline.exec()
|
||||
|
||||
return fresh
|
||||
// Store the latest successful leaderboard data in the database
|
||||
await db
|
||||
.insert(metadata)
|
||||
.values({
|
||||
key: backupKey,
|
||||
value: JSON.stringify({
|
||||
data: fresh,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: metadata.key,
|
||||
set: {
|
||||
value: JSON.stringify({
|
||||
data: fresh,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
return { data: fresh, isStale: false }
|
||||
} catch (error) {
|
||||
console.error('Error refreshing leaderboard:', error)
|
||||
throw error
|
||||
|
||||
// If neatqueue fails, try to get the latest backup from the database
|
||||
const backupKey = this.getBackupKey(channel_id)
|
||||
const backup = await db
|
||||
.select()
|
||||
.from(metadata)
|
||||
.where(eq(metadata.key, backupKey))
|
||||
.limit(1)
|
||||
.then((res) => res[0])
|
||||
|
||||
if (backup) {
|
||||
const parsedBackup = JSON.parse(backup.value)
|
||||
console.log(`Using backup leaderboard data from ${parsedBackup.timestamp} in refreshLeaderboard`)
|
||||
return { data: parsedBackup.data as LeaderboardEntry[], isStale: true }
|
||||
}
|
||||
|
||||
// If no backup exists, return an empty array with isStale flag
|
||||
console.log('No backup leaderboard data available for refreshLeaderboard, returning empty array')
|
||||
return { data: [], isStale: true }
|
||||
}
|
||||
}
|
||||
|
||||
async getLeaderboard(channel_id: string) {
|
||||
async getLeaderboard(channel_id: string): Promise<LeaderboardResponse> {
|
||||
try {
|
||||
// Try to get from Redis cache first
|
||||
const cached = await redis.get(this.getRawKey(channel_id))
|
||||
if (cached) return JSON.parse(cached)
|
||||
if (cached) return { data: JSON.parse(cached) as LeaderboardEntry[], isStale: false }
|
||||
|
||||
// If not in cache, try to refresh from neatqueue
|
||||
return await this.refreshLeaderboard(channel_id)
|
||||
} catch (error) {
|
||||
console.error('Error getting leaderboard:', error)
|
||||
throw error
|
||||
console.error('Error getting leaderboard from neatqueue:', error)
|
||||
|
||||
// If neatqueue fails, try to get the latest backup from the database
|
||||
const backupKey = this.getBackupKey(channel_id)
|
||||
const backup = await db
|
||||
.select()
|
||||
.from(metadata)
|
||||
.where(eq(metadata.key, backupKey))
|
||||
.limit(1)
|
||||
.then((res) => res[0])
|
||||
|
||||
if (backup) {
|
||||
const parsedBackup = JSON.parse(backup.value)
|
||||
console.log(`Using backup leaderboard data from ${parsedBackup.timestamp}`)
|
||||
return { data: parsedBackup.data as LeaderboardEntry[], isStale: true }
|
||||
}
|
||||
|
||||
// If no backup exists, return an empty array with isStale flag
|
||||
console.log('No backup leaderboard data available for getLeaderboard, returning empty array')
|
||||
return { data: [], isStale: true }
|
||||
}
|
||||
}
|
||||
|
||||
async getUserRank(channel_id: string, user_id: string) {
|
||||
async getUserRank(channel_id: string, user_id: string): Promise<UserRankResponse> {
|
||||
try {
|
||||
// Try to get user data from Redis first
|
||||
const userData = await redis.hgetall(this.getUserKey(user_id, channel_id))
|
||||
if (!userData) return null
|
||||
if (userData) {
|
||||
return {
|
||||
data: {
|
||||
...userData,
|
||||
mmr: Number(userData.mmr),
|
||||
streak: userData.streak,
|
||||
} as unknown as LeaderboardEntry,
|
||||
isStale: false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...userData,
|
||||
mmr: Number(userData.mmr),
|
||||
streak: userData.streak,
|
||||
} as unknown as LeaderboardEntry
|
||||
// If not found in Redis, try to refresh the leaderboard
|
||||
try {
|
||||
const { data: freshLeaderboard } = await this.refreshLeaderboard(channel_id)
|
||||
const userEntry = freshLeaderboard.find(entry => entry.id === user_id)
|
||||
if (userEntry) {
|
||||
return { data: userEntry, isStale: false }
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error('Error refreshing leaderboard for user rank:', refreshError)
|
||||
// Continue to backup if refresh fails
|
||||
}
|
||||
|
||||
// If not found in fresh data or refresh failed, try to get from backup
|
||||
const backupKey = this.getBackupKey(channel_id)
|
||||
const backup = await db
|
||||
.select()
|
||||
.from(metadata)
|
||||
.where(eq(metadata.key, backupKey))
|
||||
.limit(1)
|
||||
.then((res) => res[0])
|
||||
|
||||
if (backup) {
|
||||
const parsedBackup = JSON.parse(backup.value)
|
||||
const userEntry = parsedBackup.data.find((entry: any) => entry.id === user_id)
|
||||
if (userEntry) {
|
||||
console.log(`Using backup leaderboard data for user ${user_id} from ${parsedBackup.timestamp}`)
|
||||
return { data: userEntry as LeaderboardEntry, isStale: true }
|
||||
}
|
||||
}
|
||||
|
||||
// If user not found anywhere
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('Error getting user rank:', error)
|
||||
throw error
|
||||
// Return null instead of rethrowing the error to prevent the page from breaking
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user