11import User from '../models/User.js' ;
22import UserStats from '../models/UserStats.js' ;
33
4- // Production-scale caching for 2M+ users
5- const CACHE_DURATION = 300000 ; // 5 minute cache for production performance
6- const DAILY_CACHE_DURATION = 180000 ; // 3 minute cache for daily leaderboards (more frequent updates)
4+ // Improved caching with separate keys for different modes
5+ const CACHE_DURATION = 60000 ; // 1 minute cache
76const cache = new Map ( ) ;
87
98function getCacheKey ( mode , pastDay ) {
@@ -12,8 +11,7 @@ function getCacheKey(mode, pastDay) {
1211
1312function getCachedData ( key ) {
1413 const cached = cache . get ( key ) ;
15- const cacheDuration = key . includes ( 'daily' ) ? DAILY_CACHE_DURATION : CACHE_DURATION ;
16- if ( cached && Date . now ( ) - cached . timestamp < cacheDuration ) {
14+ if ( cached && Date . now ( ) - cached . timestamp < CACHE_DURATION ) {
1715 return cached . data ;
1816 }
1917 return null ;
@@ -37,194 +35,152 @@ function sendableUser(user) {
3735 } ;
3836}
3937
38+ // Improved 24h leaderboard calculation using UserStats model methods
4039async function getDailyLeaderboard ( isXp = true ) {
4140 const scoreField = isXp ? 'totalXp' : 'elo' ;
4241 const now = new Date ( ) ;
4342 const dayAgo = new Date ( now . getTime ( ) - ( 24 * 60 * 60 * 1000 ) ) ;
4443
45- console . time ( `getDailyLeaderboard_${ isXp ? 'xp' : 'elo' } ` ) ;
46-
47- // PRODUCTION OPTIMIZATION: Single aggregation with user data included
48- const leaderboardData = await UserStats . aggregate ( [
49- // STEP 1: Filter to last 24h (uses timestamp index)
44+ // Get all users who have stats in the last 24h with their deltas
45+ const userDeltas = await UserStats . aggregate ( [
46+ // Match users active in last 24h
5047 {
5148 $match : {
5249 timestamp : { $gte : dayAgo }
5350 }
5451 } ,
55- // STEP 2: Sort for efficient grouping (uses compound index)
52+ // Sort by userId and timestamp to get latest first
5653 {
5754 $sort : { userId : 1 , timestamp : - 1 }
5855 } ,
59- // STEP 3: Get first/last stats per user in 24h window
56+ // Group by userId to get latest and earliest in 24h period
6057 {
6158 $group : {
6259 _id : '$userId' ,
6360 latestStat : { $first : '$$ROOT' } ,
6461 earliestStat : { $last : '$$ROOT' }
6562 }
6663 } ,
67- // STEP 4: Calculate deltas and add user lookup in single stage
68- {
69- $lookup : {
70- from : 'users' ,
71- localField : '_id' ,
72- foreignField : '_id' ,
73- pipeline : [
74- { $match : { banned : false } } ,
75- { $project : { username : 1 , elo : 1 , totalXp : 1 , created_at : 1 , games : 1 } }
76- ] ,
77- as : 'user'
78- }
79- } ,
80- // STEP 5: Filter out banned users and calculate deltas
81- {
82- $match : {
83- 'user.0' : { $exists : true } ,
84- 'user.0.username' : { $exists : true }
85- }
86- } ,
64+ // Calculate the actual 24h change
8765 {
88- $addFields : {
89- user : { $arrayElemAt : [ '$user' , 0 ] } ,
66+ $project : {
67+ userId : '$_id' ,
68+ latestScore : `$latestStat.${ scoreField } ` ,
69+ earliestScore : `$earliestStat.${ scoreField } ` ,
9070 delta : {
9171 $subtract : [ `$latestStat.${ scoreField } ` , `$earliestStat.${ scoreField } ` ]
92- }
72+ } ,
73+ latestTimestamp : '$latestStat.timestamp' ,
74+ earliestTimestamp : '$earliestStat.timestamp'
9375 }
9476 } ,
95- // STEP 6: Filter meaningful changes only
77+ // Only include users with meaningful changes (positive for XP, any change for ELO)
9678 {
9779 $match : isXp ? { delta : { $gt : 0 } } : { delta : { $ne : 0 } }
9880 } ,
99- // STEP 7: Sort by delta and limit (most expensive operation at the end)
81+ // Sort by delta descending
10082 {
10183 $sort : { delta : - 1 }
10284 } ,
85+ // Limit to top 100
10386 {
10487 $limit : 100
105- } ,
106- // STEP 8: Format final output
107- {
108- $project : {
109- username : '$user.username' ,
110- totalXp : isXp ? '$delta' : '$user.totalXp' ,
111- createdAt : '$user.created_at' ,
112- gamesLen : { $size : { $ifNull : [ '$user.games' , [ ] ] } } ,
113- elo : isXp ? '$user.elo' : '$delta' ,
114- eloToday : '$delta' ,
115- rank : { $add : [ { $indexOfArray : [ [ ] , '$_id' ] } , 1 ] } // Will be set after
116- }
11788 }
11889 ] ) ;
11990
120- // Add rank numbers efficiently
121- const leaderboard = leaderboardData . map ( ( user , index ) => ( {
122- ...user ,
123- rank : index + 1
124- } ) ) ;
91+ // Get user details for all users in the leaderboard
92+ const userIds = userDeltas . map ( delta => delta . userId ) ;
93+ const users = await User . find ( {
94+ _id : { $in : userIds } ,
95+ banned : false
96+ } ) . select ( '_id username elo totalXp created_at games' ) . lean ( ) ;
97+
98+ const userMap = new Map ( users . map ( u => [ u . _id . toString ( ) , u ] ) ) ;
12599
126- console . timeEnd ( `getDailyLeaderboard_${ isXp ? 'xp' : 'elo' } ` ) ;
127- console . log ( `Daily leaderboard generated: ${ leaderboard . length } users` ) ;
100+ // Build leaderboard with proper data
101+ const leaderboard = userDeltas . map ( ( delta , index ) => {
102+ const user = userMap . get ( delta . userId ) ;
103+ if ( ! user || ! user . username ) return null ;
128104
129- return { leaderboard, userDeltas : leaderboardData } ;
105+ return {
106+ username : user . username ,
107+ totalXp : isXp ? delta . delta : user . totalXp , // For XP show delta, for ELO show current total XP
108+ createdAt : user . created_at ,
109+ gamesLen : user . games ?. length || 0 ,
110+ elo : isXp ? user . elo : delta . delta , // For ELO mode show delta in elo field
111+ eloToday : delta . delta , // Always show the 24h change
112+ rank : index + 1
113+ } ;
114+ } ) . filter ( user => user !== null ) ;
115+
116+ return { leaderboard, userDeltas } ;
130117}
131118
132- // Production-optimized user ranking for daily leaderboard
119+ // Get user's position in daily leaderboard
133120async function getUserDailyRank ( username , isXp = true ) {
134- const user = await User . findOne ( { username : username } ) . lean ( ) ;
121+ const user = await User . findOne ( { username : username } ) ;
135122 if ( ! user ) return { rank : null , delta : null } ;
136123
137124 const scoreField = isXp ? 'totalXp' : 'elo' ;
138- const dayAgo = new Date ( Date . now ( ) - ( 24 * 60 * 60 * 1000 ) ) ;
125+ const now = new Date ( ) ;
126+ const dayAgo = new Date ( now . getTime ( ) - ( 24 * 60 * 60 * 1000 ) ) ;
139127
140- console . time ( `getUserDailyRank_${ username } ` ) ;
128+ // Get user's 24h change
129+ const userStats = await UserStats . find ( {
130+ userId : user . _id . toString ( ) ,
131+ timestamp : { $gte : dayAgo }
132+ } ) . sort ( { timestamp : - 1 } ) . limit ( 1 ) ;
141133
142- // OPTIMIZED: Get user's stats and rank in one efficient query
143- const result = await UserStats . aggregate ( [
144- // Step 1: Get user's own delta first
134+ const oldestUserStats = await UserStats . find ( {
135+ userId : user . _id . toString ( ) ,
136+ timestamp : { $gte : dayAgo }
137+ } ) . sort ( { timestamp : 1 } ) . limit ( 1 ) ;
138+
139+ if ( ! userStats [ 0 ] || ! oldestUserStats [ 0 ] ) {
140+ return { rank : null , delta : 0 } ;
141+ }
142+
143+ const userDelta = userStats [ 0 ] [ scoreField ] - oldestUserStats [ 0 ] [ scoreField ] ;
144+
145+ // Count how many users have better deltas
146+ const betterUsersCount = await UserStats . aggregate ( [
147+ // Match users active in last 24h
145148 {
146- $facet : {
147- userDelta : [
148- {
149- $match : {
150- userId : user . _id . toString ( ) ,
151- timestamp : { $gte : dayAgo }
152- }
153- } ,
154- { $sort : { timestamp : - 1 } } ,
155- {
156- $group : {
157- _id : '$userId' ,
158- latest : { $first : `$${ scoreField } ` } ,
159- earliest : { $last : `$${ scoreField } ` }
160- }
161- } ,
162- {
163- $project : {
164- delta : { $subtract : [ '$latest' , '$earliest' ] }
165- }
166- }
167- ] ,
168- betterUsers : [
169- {
170- $match : {
171- timestamp : { $gte : dayAgo }
172- }
173- } ,
174- { $sort : { userId : 1 , timestamp : - 1 } } ,
175- {
176- $group : {
177- _id : '$userId' ,
178- delta : {
179- $subtract : [
180- { $first : `$${ scoreField } ` } ,
181- { $last : `$${ scoreField } ` }
182- ]
183- }
184- }
185- } ,
186- {
187- $match : isXp ? { delta : { $gt : 0 } } : { delta : { $ne : 0 } }
188- }
189- ]
149+ $match : {
150+ timestamp : { $gte : dayAgo }
151+ }
152+ } ,
153+ // Group by userId to get their 24h deltas
154+ {
155+ $sort : { userId : 1 , timestamp : - 1 }
156+ } ,
157+ {
158+ $group : {
159+ _id : '$userId' ,
160+ latestStat : { $first : '$$ROOT' } ,
161+ earliestStat : { $last : '$$ROOT' }
190162 }
191163 } ,
192- // Step 2: Count users with better deltas
193164 {
194165 $project : {
195- userDelta : { $arrayElemAt : [ '$userDelta.delta' , 0 ] } ,
196- rank : {
197- $add : [
198- {
199- $size : {
200- $filter : {
201- input : '$betterUsers' ,
202- cond : {
203- $gt : [
204- '$$this.delta' ,
205- { $arrayElemAt : [ '$userDelta.delta' , 0 ] }
206- ]
207- }
208- }
209- }
210- } ,
211- 1
212- ]
166+ delta : {
167+ $subtract : [ `$latestStat.${ scoreField } ` , `$earliestStat.${ scoreField } ` ]
213168 }
214169 }
170+ } ,
171+ // Count users with better deltas
172+ {
173+ $match : {
174+ delta : { $gt : userDelta }
175+ }
176+ } ,
177+ {
178+ $count : "count"
215179 }
216180 ] ) ;
217181
218- let rank = null ;
219- let delta = 0 ;
220-
221- if ( result . length > 0 && result [ 0 ] . userDelta !== null ) {
222- rank = result [ 0 ] . rank ;
223- delta = result [ 0 ] . userDelta ;
224- }
225-
226- console . timeEnd ( `getUserDailyRank_${ username } ` ) ;
227- return { rank, delta } ;
182+ const rank = betterUsersCount . length > 0 ? betterUsersCount [ 0 ] . count + 1 : 1 ;
183+ return { rank, delta : userDelta } ;
228184}
229185
230186export default async function handler ( req , res ) {
@@ -300,4 +256,4 @@ export default async function handler(req, res) {
300256 error : error . message
301257 } ) ;
302258 }
303- }
259+ }
0 commit comments