import { io } from 'socket.io-client'; const WS_URL = import.meta.env.VITE_WS_URL || 'http://localhost:3000'; class SocketService { constructor() { this.socket = null; this.listeners = new Map(); } connect() { if (this.socket?.connected) { return this.socket; } this.socket = io(WS_URL, { withCredentials: true, transports: ['websocket', 'polling'], }); this.socket.on('connect', () => { console.log('WebSocket connected:', this.socket.id); }); this.socket.on('disconnect', () => { console.log('WebSocket disconnected'); }); this.socket.on('error', (error) => { console.error('WebSocket error:', error); }); return this.socket; } disconnect() { if (this.socket) { this.socket.disconnect(); this.socket = null; } } on(event, callback) { if (!this.socket) { this.connect(); } this.socket.on(event, callback); if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); } off(event, callback) { if (this.socket) { this.socket.off(event, callback); } if (this.listeners.has(event)) { const callbacks = this.listeners.get(event); const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } } emit(event, data) { if (!this.socket) { this.connect(); } this.socket.emit(event, data); } onReconnect(callback) { if (!this.socket) { this.connect(); } // Слушаем событие 'connect' которое происходит при переподключении this.socket.on('connect', callback); } offReconnect(callback) { if (this.socket) { this.socket.off('connect', callback); } } // Game-specific methods joinRoom(roomCode, userId) { this.emit('joinRoom', { roomCode, userId }); } // Присоединение к WebSocket комнате с ожиданием подтверждения joinRoomWithAck(roomCode, userId) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('WebSocket join timeout - room not joined within 5 seconds')); }, 5000); // 5 секунд максимум // Слушаем подтверждение один раз this.socket.once('roomJoined', (data) => { clearTimeout(timeout); console.log('✅ WebSocket room joined:', data); resolve(data); }); // Отправляем запрос на присоединение console.log(`📤 Requesting to join WebSocket room: ${roomCode}`); this.emit('joinRoom', { roomCode, userId }); }); } // Присоединение как участник (создание записи в БД) через WebSocket joinAsParticipant(roomId, roomCode, userId, name, role) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Participant join timeout - not created within 5 seconds')); }, 5000); // Обработчик успешного присоединения const handleSuccess = (data) => { clearTimeout(timeout); this.socket.off('error', handleError); // Убираем обработчик ошибки console.log('✅ Participant joined successfully:', data); resolve(data); }; // Обработчик ошибки const handleError = (error) => { clearTimeout(timeout); this.socket.off('participantJoined', handleSuccess); // Убираем обработчик успеха console.error('❌ Participant join error:', error); reject(new Error(error.message || 'Failed to join as participant')); }; this.socket.once('participantJoined', handleSuccess); this.socket.once('error', handleError); // Отправляем запрос console.log(`📤 Requesting to join as participant: ${name} (${role})`); this.emit('joinAsParticipant', { roomId, roomCode, userId, name, role }); }); } startGame(roomId, roomCode, userId) { this.emit('startGame', { roomId, roomCode, userId }); } endGame(roomId, roomCode, userId) { this.emit('endGame', { roomId, roomCode, userId }); } // Note: Game actions now use 'playerAction' event with UUID // Direct emit is preferred over these helper methods // Example: socketService.emit('playerAction', { action: 'revealAnswer', ... }) updateRoomPack(roomId, roomCode, userId, questions) { this.emit('updateRoomPack', { roomId, roomCode, userId, questions }); } importQuestions(roomId, roomCode, userId, sourcePackId, questionIndices) { this.emit('importQuestions', { roomId, roomCode, userId, sourcePackId, questionIndices, }); } updatePlayerName(roomId, roomCode, userId, participantId, newName) { this.emit('updatePlayerName', { roomId, roomCode, userId, participantId, newName, }); } updatePlayerScore(roomId, roomCode, userId, participantId, newScore) { this.emit('updatePlayerScore', { roomId, roomCode, userId, participantId, newScore, }); } changeRoomTheme(roomId, roomCode, userId, themeId) { this.emit('changeRoomTheme', { roomId, roomCode, userId, themeId, }); } changeParticipantRole(roomId, roomCode, userId, participantId, newRole) { this.emit('changeParticipantRole', { roomId, roomCode, userId, participantId, newRole, }); } toggleParticles(roomId, roomCode, userId, particlesEnabled) { this.emit('toggleParticles', { roomId, roomCode, userId, particlesEnabled, }); } addPlayer(roomId, roomCode, userId, playerName, role = 'PLAYER') { this.emit('addPlayer', { roomId, roomCode, userId, playerName, role, }); } } export default new SocketService();