sanket3280/code-execution
0
1const Challenge = require('../models/Challenge');2const ChallengeLeaderboardService = require('../services/challengeLeaderboard.service');3const logger = require('../utils/logger');4 5// Store for batching updates6const updateBatches = {};7 8/**9 * Initialize challenge leaderboard WebSocket handlers10 * @param {SocketIO.Server} io - Socket.IO server instance11 */12function initializeChallengeLeaderboardSocket(io) {13 io.on('connection', (socket) => {14 logger.debug(`Socket connected: ${socket.id}`);15 16 // Join challenge leaderboard room17 socket.on('join-challenge-leaderboard', async ({ challengeId, userId }) => {18 try {19 logger.debug(`User ${userId} attempting to join leaderboard room: ${challengeId}`);20 21 // Validate challenge exists and is active22 const challenge = await Challenge.findById(challengeId);23 if (!challenge) {24 logger.warn(`Challenge not found: ${challengeId}`);25 return socket.emit('leaderboard-error', { 26 message: 'Challenge not found' 27 });28 }29 30 // Join the room31 const roomName = `challenge:leaderboard:${challengeId}`;32 socket.join(roomName);33 34 // Send initial leaderboard data35 const leaderboardData = await ChallengeLeaderboardService36 .getChallengeLeaderboard(challengeId, { limit: 50 });37 38 // Get user rank if userId provided39 let userRank = null;40 if (userId) {41 try {42 userRank = await ChallengeLeaderboardService.getUserRank(challengeId, userId);43 } catch (rankError) {44 logger.error('Error fetching user rank:', rankError);45 }46 }47 48 // Get recent activity49 let recentActivity = [];50 try {51 recentActivity = await ChallengeLeaderboardService.getRecentActivity(challengeId, { limit: 20 });52 } catch (activityError) {53 logger.error('Error fetching recent activity:', activityError);54 }55 56 socket.emit('leaderboard-initial-data', {57 ...leaderboardData,58 userRank,59 recentActivity,60 timestamp: new Date()61 });62 63 // Track connection64 logger.info(`User ${userId} joined leaderboard room: ${roomName}`);65 } catch (error) {66 logger.error('Error joining leaderboard room:', error);67 socket.emit('leaderboard-error', { 68 message: 'Failed to join leaderboard',69 error: error.message 70 });71 }72 });73 74 // Leave challenge leaderboard room75 socket.on('leave-challenge-leaderboard', ({ challengeId }) => {76 const roomName = `challenge:leaderboard:${challengeId}`;77 socket.leave(roomName);78 logger.debug(`Socket ${socket.id} left leaderboard room: ${roomName}`);79 });80 81 // Manual refresh request82 socket.on('request-leaderboard-update', async ({ challengeId, userId }) => {83 try {84 logger.debug(`Manual refresh requested for challenge: ${challengeId}`);85 86 const leaderboardData = await ChallengeLeaderboardService87 .getChallengeLeaderboard(challengeId, { limit: 50 });88 89 // Get user rank if userId provided90 let userRank = null;91 if (userId) {92 try {93 userRank = await ChallengeLeaderboardService.getUserRank(challengeId, userId);94 } catch (rankError) {95 logger.error('Error fetching user rank:', rankError);96 }97 }98 99 // Get recent activity100 let recentActivity = [];101 try {102 recentActivity = await ChallengeLeaderboardService.getRecentActivity(challengeId, { limit: 20 });103 } catch (activityError) {104 logger.error('Error fetching recent activity:', activityError);105 }106 107 socket.emit('leaderboard-update', {108 ...leaderboardData,109 userRank,110 recentActivity,111 timestamp: new Date()112 });113 } catch (error) {114 logger.error('Error fetching leaderboard:', error);115 socket.emit('leaderboard-error', { 116 message: 'Failed to fetch leaderboard',117 error: error.message 118 });119 }120 });121 122 // Handle disconnection123 socket.on('disconnect', () => {124 logger.debug(`Socket disconnected: ${socket.id}`);125 });126 });127}128 129/**130 * Broadcast leaderboard update to all participants131 * @param {SocketIO.Server} io - Socket.IO server instance132 * @param {string} challengeId - Challenge ID133 * @param {Object} updateData - Leaderboard update data134 */135async function broadcastLeaderboardUpdate(io, challengeId, updateData) {136 const roomName = `challenge:leaderboard:${challengeId}`;137 138 try {139 // Initialize batch if not exists140 if (!updateBatches[challengeId]) {141 updateBatches[challengeId] = {142 updates: [],143 timeout: null144 };145 }146 147 updateBatches[challengeId].updates.push(updateData);148 149 // Clear existing timeout150 if (updateBatches[challengeId].timeout) {151 clearTimeout(updateBatches[challengeId].timeout);152 }153 154 // Set new timeout to batch updates (200ms window)155 updateBatches[challengeId].timeout = setTimeout(async () => {156 const batch = updateBatches[challengeId].updates;157 delete updateBatches[challengeId];158 159 // Merge updates and broadcast160 const mergedUpdate = await mergeBatchUpdates(challengeId, batch);161 162 // Check message size (limit to 50KB)163 const messageSize = JSON.stringify(mergedUpdate).length;164 if (messageSize > 50000) {165 logger.warn(`Leaderboard update message too large: ${messageSize} bytes, truncating...`);166 // Truncate leaderboard to top 20 if too large167 if (mergedUpdate.leaderboard && mergedUpdate.leaderboard.length > 20) {168 mergedUpdate.leaderboard = mergedUpdate.leaderboard.slice(0, 20);169 }170 }171 172 io.to(roomName).emit('leaderboard-update', mergedUpdate);173 174 logger.debug(`Broadcasted batched update to ${roomName} (${batch.length} updates merged)`);175 }, 200);176 } catch (error) {177 logger.error('Error broadcasting leaderboard update:', error);178 }179}180 181/**182 * Merge multiple batch updates into a single update183 * @param {string} challengeId - Challenge ID184 * @param {Array} batch - Array of update objects185 * @returns {Promise<Object>} Merged update data186 */187async function mergeBatchUpdates(challengeId, batch) {188 try {189 // Get fresh leaderboard data190 const leaderboardData = await ChallengeLeaderboardService191 .getChallengeLeaderboard(challengeId, { limit: 50 });192 193 // Get challenge stats194 const stats = await ChallengeLeaderboardService.getChallengeStats(challengeId);195 196 // Get recent activity197 const recentActivity = await ChallengeLeaderboardService.getRecentActivity(challengeId, { limit: 20 });198 199 // Collect all affected user IDs from batch200 const affectedUserIds = [...new Set(batch.map(update => update.userId).filter(Boolean))];201 202 // Get updated ranks for affected users203 const userRanks = {};204 for (const userId of affectedUserIds) {205 try {206 userRanks[userId] = await ChallengeLeaderboardService.getUserRank(challengeId, userId);207 } catch (error) {208 logger.error(`Error getting rank for user ${userId}:`, error);209 }210 }211 212 return {213 ...leaderboardData,214 stats,215 recentActivity,216 userRanks,217 affectedUsers: affectedUserIds,218 timestamp: new Date(),219 batchSize: batch.length220 };221 } catch (error) {222 logger.error('Error merging batch updates:', error);223 // Return minimal update on error224 return {225 challengeId,226 timestamp: new Date(),227 error: 'Failed to merge updates'228 };229 }230}231 232/**233 * Broadcast recent activity event234 * @param {SocketIO.Server} io - Socket.IO server instance235 * @param {string} challengeId - Challenge ID236 * @param {Object} activityData - Activity event data237 */238function broadcastActivity(io, challengeId, activityData) {239 const roomName = `challenge:leaderboard:${challengeId}`;240 241 try {242 io.to(roomName).emit('activity-update', {243 challengeId,244 activity: activityData,245 timestamp: new Date()246 });247 248 logger.debug(`Broadcasted activity to ${roomName}:`, activityData.type);249 } catch (error) {250 logger.error('Error broadcasting activity:', error);251 }252}253 254module.exports = {255 initializeChallengeLeaderboardSocket,256 broadcastLeaderboardUpdate,257 broadcastActivity258};259 