sanket3280/code-execution
0
1const SpectatorSession = require('../models/SpectatorSession');2const Challenge = require('../models/Challenge');3const redis = require('../utils/redis');4 5/**6 * Initialize spectator WebSocket handlers7 * @param {SocketIO.Server} io - Socket.IO server instance8 */9function initializeSpectatorSocket(io) {10 io.on('connection', (socket) => {11 // Spectator joins a challenge room12 socket.on('spectator:join', async (data) => {13 try {14 const { challengeId, spectatorId, role } = data;15 16 // Validate challenge exists and is active17 const challenge = await Challenge.findById(challengeId);18 if (!challenge) {19 socket.emit('error:challengeNotFound', {20 error: 'CHALLENGE_NOT_FOUND',21 message: 'Challenge not found'22 });23 return;24 }25 26 // Check if challenge has started27 if (new Date() < new Date(challenge.startDate)) {28 socket.emit('error:challengeNotStarted', {29 error: 'CHALLENGE_NOT_STARTED',30 message: 'This challenge has not started yet',31 startDate: challenge.startDate32 });33 return;34 }35 36 // Join challenge room37 const roomName = `challenge:${challengeId}:spectators`;38 socket.join(roomName);39 40 // Get client IP41 const ipAddress = socket.handshake.headers['x-forwarded-for'] || 42 socket.handshake.address || 43 'unknown';44 const userAgent = socket.handshake.headers['user-agent'] || 'unknown';45 46 // Create spectator session47 await SpectatorSession.create({48 challengeId,49 spectatorId,50 role,51 socketId: socket.id,52 ipAddress,53 userAgent,54 watchingParticipants: [],55 isActive: true56 });57 58 // Get participant count59 const participantCount = challenge.participants.length;60 const activeParticipants = challenge.participants.filter(p => {61 // Consider active if last activity within 5 minutes62 return p.lastSubmission && 63 (Date.now() - new Date(p.lastSubmission).getTime() < 5 * 60 * 1000);64 }).length;65 66 // Confirm join67 socket.emit('spectator:joined', {68 success: true,69 spectatorId,70 participantCount,71 activeParticipants,72 permissions: {73 viewAllParticipants: true,74 flagActivities: role === 'creator'75 }76 });77 } catch (error) {78 console.error('Error in spectator:join:', error);79 socket.emit('error:unauthorized', {80 error: 'UNAUTHORIZED',81 message: 'Failed to join spectator room'82 });83 }84 });85 86 // Spectator watches a specific participant87 socket.on('spectator:watchParticipant', async (data) => {88 try {89 const { challengeId, spectatorId, participantId, problemId } = data;90 91 // Leave all previous participant rooms first92 const rooms = Array.from(socket.rooms);93 rooms.forEach(room => {94 if (room.startsWith('participant:') && room !== socket.id) {95 socket.leave(room);96 }97 });98 99 // Update spectator session100 await SpectatorSession.findOneAndUpdate(101 { socketId: socket.id, spectatorId },102 {103 $set: {104 watchingParticipants: [{105 participantId,106 startedAt: new Date()107 }]108 },109 lastActivity: new Date()110 }111 );112 113 // Join participant-specific room for updates114 const participantRoom = `participant:${participantId}:${problemId}`;115 socket.join(participantRoom);116 console.log('๐๏ธ Spectator joined room:', {117 spectatorSocketId: socket.id,118 room: participantRoom,119 participantId,120 problemId121 });122 123 // Get current code from Redis or database124 try {125 const cacheKey = `code:${participantId}:${problemId}`;126 const cached = await redis.get(cacheKey);127 128 if (cached) {129 socket.emit('spectator:codeSnapshot', {130 participantId,131 problemId,132 code: cached.code,133 language: cached.language,134 lastUpdate: cached.lastUpdate,135 linesOfCode: cached.code.split('\n').length,136 participantInfo: {137 username: cached.username || 'Unknown',138 isActive: true139 }140 });141 }142 } catch (err) {143 console.log('Redis error, will fetch from database:', err.message);144 }145 } catch (error) {146 console.error('Error in spectator:watchParticipant:', error);147 }148 });149 150 // Spectator stops watching a participant151 socket.on('spectator:stopWatching', async (data) => {152 try {153 const { spectatorId, participantId } = data;154 155 // Leave participant room156 const rooms = Array.from(socket.rooms);157 rooms.forEach(room => {158 if (room.includes(`participant:${participantId}`)) {159 socket.leave(room);160 }161 });162 163 // Update spectator session164 await SpectatorSession.findOneAndUpdate(165 { socketId: socket.id, spectatorId },166 {167 $pull: {168 watchingParticipants: { participantId }169 },170 lastActivity: new Date()171 }172 );173 } catch (error) {174 console.error('Error in spectator:stopWatching:', error);175 }176 });177 178 // Spectator requests code snapshot179 socket.on('spectator:requestSnapshot', async (data) => {180 try {181 const { participantId, problemId } = data;182 183 // Get code from Redis184 const cacheKey = `code:${participantId}:${problemId}`;185 const cached = await redis.get(cacheKey);186 187 if (cached) {188 socket.emit('spectator:codeSnapshot', {189 participantId,190 problemId,191 code: cached.code,192 language: cached.language,193 lastUpdate: cached.lastUpdate,194 linesOfCode: cached.code.split('\n').length195 });196 }197 } catch (error) {198 console.error('Error in spectator:requestSnapshot:', error);199 }200 });201 202 // Spectator leaves203 socket.on('spectator:leave', async (data) => {204 try {205 const { challengeId, spectatorId } = data;206 207 // Mark session as inactive208 await SpectatorSession.findOneAndUpdate(209 { socketId: socket.id, spectatorId },210 { isActive: false }211 );212 213 // Leave all rooms214 const roomName = `challenge:${challengeId}:spectators`;215 socket.leave(roomName);216 } catch (error) {217 console.error('Error in spectator:leave:', error);218 }219 });220 221 // Participant code update (emitted by participants)222 socket.on('participant:codeUpdate', async (data) => {223 try {224 const { challengeId, participantId, problemId, code, changeType, delta, language } = data;225 console.log('๐ก SERVER RECEIVED participant:codeUpdate', {226 socketId: socket.id,227 participantId,228 problemId,229 codeLength: code?.length,230 timestamp: new Date().toISOString()231 });232 233 // IMMEDIATELY broadcast to spectators watching this participant (NO CACHING)234 const participantRoom = `participant:${participantId}:${problemId}`;235 236 // Get all sockets in the room237 const socketsInRoom = await io.in(participantRoom).fetchSockets();238 console.log('๐ข Broadcasting to room:', participantRoom, {239 socketsInRoom: socketsInRoom.length,240 socketIds: socketsInRoom.map(s => s.id)241 });242 243 io.to(participantRoom).emit('spectator:codeUpdate', {244 participantId,245 problemId,246 code,247 changeType: changeType || 'typing',248 timestamp: Date.now(),249 delta: delta || { linesAdded: 0, charactersAdded: 0 },250 language: language || 'javascript'251 });252 253 // Broadcast activity to spectators254 const challengeRoom = `challenge:${challengeId}:spectators`;255 io.to(challengeRoom).emit('spectator:participantActivity', {256 participantId,257 activity: 'typing',258 problemId,259 timestamp: Date.now()260 });261 } catch (error) {262 console.error('Error in participant:codeUpdate:', error);263 }264 });265 266 // Participant activity update267 socket.on('participant:activityUpdate', async (data) => {268 try {269 const { challengeId, participantId, activity, problemId } = data;270 271 // Update activity in Redis272 const activityKey = `participant:${participantId}:${challengeId}:activity`;273 await redis.set(activityKey, {274 currentProblem: { problemId },275 lastActivity: Date.now(),276 activity277 }, 60 * 60);278 279 // Broadcast to spectators280 const challengeRoom = `challenge:${challengeId}:spectators`;281 io.to(challengeRoom).emit('spectator:participantActivity', {282 participantId,283 activity,284 problemId,285 timestamp: Date.now()286 });287 } catch (error) {288 console.error('Error in participant:activityUpdate:', error);289 }290 });291 292 // Participant joins challenge293 socket.on('participant:join', async (data) => {294 try {295 const { challengeId, participantId, problemId } = data;296 297 // Broadcast to spectators298 const challengeRoom = `challenge:${challengeId}:spectators`;299 io.to(challengeRoom).emit('spectator:participantListUpdate', {300 action: 'joined',301 participant: {302 userId: participantId,303 currentProblem: problemId,304 isActive: true305 },306 timestamp: Date.now()307 });308 } catch (error) {309 console.error('Error in participant:join:', error);310 }311 });312 313 // Participant leaves314 socket.on('participant:leave', async (data) => {315 try {316 const { challengeId, participantId } = data;317 318 // Broadcast to spectators319 const challengeRoom = `challenge:${challengeId}:spectators`;320 io.to(challengeRoom).emit('spectator:participantListUpdate', {321 action: 'left',322 participant: {323 userId: participantId,324 isActive: false325 },326 timestamp: Date.now()327 });328 } catch (error) {329 console.error('Error in participant:leave:', error);330 }331 });332 333 // Handle disconnect334 socket.on('disconnect', async () => {335 try {336 // Mark all sessions with this socket as inactive337 await SpectatorSession.updateMany(338 { socketId: socket.id },339 { isActive: false }340 );341 } catch (error) {342 console.error('Error in disconnect:', error);343 }344 });345 });346}347 348module.exports = { initializeSpectatorSocket };349 