Skip to content

Commit 5b4ef42

Browse files
committed
dfg
1 parent b16d721 commit 5b4ef42

File tree

1 file changed

+94
-138
lines changed

1 file changed

+94
-138
lines changed

api/leaderboard.js

Lines changed: 94 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import User from '../models/User.js';
22
import 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
76
const cache = new Map();
87

98
function getCacheKey(mode, pastDay) {
@@ -12,8 +11,7 @@ function getCacheKey(mode, pastDay) {
1211

1312
function 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
4039
async 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
133120
async 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

230186
export 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

Comments
 (0)