/
bytevalue
/
optimal
Обзор
Документация
Войти
/
bytevalue
/
optimal
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/main/resources/static/js/websocket.js
435 строк
15 KB
bytevalue
super puper mega work
05 дек 2025, 18:16
05 дек 2025, 18:16
d5605cd
Код
Авторство
О чём код?
class WebSocketService { constructor() { this.client = null; this.stompClient = null; this.isConnected = false; this.reconnectAttempts = 0; this.subscriptions = new Map(); this.pendingSubscriptions = new Set(); this.reconnectTimer = null; this.activeChatSubscriptions = new Set(); } /** * Connect to WebSocket */ connect() { if (this.isConnected) { console.log('⚠️ WebSocket already connected'); return; } if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } try { console.log('🔌 Connecting to WebSocket...'); const wsUrl = '/ws'; this.client = new SockJS(wsUrl); this.stompClient = Stomp.over(this.client); this.stompClient.debug = null; this.stompClient.connect( {}, (frame) => this.onConnected(frame), (error) => this.onError(error) ); } catch (error) { console.error('❌ WebSocket connection failed:', error); this.scheduleReconnect(); } } /** * On connected callback */ onConnected(frame) { console.log('✅ WebSocket connected'); this.isConnected = true; this.reconnectAttempts = 0; stateService.setState('isConnected', true); eventBus.publish(CONFIG.EVENTS.CONNECTION_ESTABLISHED); // Подписываемся на активный чат const activeChatId = stateService.getState('activeChatId'); if (activeChatId) { this.subscribeToChat(activeChatId); } // Обрабатываем ожидающие подписки this.pendingSubscriptions.forEach(topic => { this.subscribeToTopic(topic); }); this.pendingSubscriptions.clear(); } /** * Subscribe to chat updates */ subscribeToChat(chatId) { if (this.activeChatSubscriptions.has(chatId)) { console.log(`⚠️ Already subscribed to chat ${chatId}`); return; } const topic = `/topic/chat/${chatId}`; this.subscribeToTopic(topic); this.activeChatSubscriptions.add(chatId); console.log(`✅ Subscribed to chat ${chatId}`); } /** * Unsubscribe from chat */ unsubscribeFromChat(chatId) { const topic = `/topic/chat/${chatId}`; this.unsubscribeFromTopic(topic); this.activeChatSubscriptions.delete(chatId); console.log(`✅ Unsubscribed from chat ${chatId}`); } /** * Subscribe to topic */ subscribeToTopic(topic) { if (!this.isConnected || !this.stompClient) { console.log(`📝 Queueing subscription to ${topic}`); this.pendingSubscriptions.add(topic); return; } if (this.subscriptions.has(topic)) { console.log(`⚠️ Already subscribed to ${topic}`); return; } console.log(`📨 Subscribing to ${topic}...`); try { const subscription = this.stompClient.subscribe(topic, (message) => { this.handleMessage(topic, message); }); this.subscriptions.set(topic, subscription); console.log(`✅ Subscribed to ${topic}`); } catch (error) { console.error(`❌ Failed to subscribe to ${topic}:`, error); } } /** * Handle incoming message */ handleMessage(topic, message) { try { const data = JSON.parse(message.body); console.log(`📨 Received WebSocket message on ${topic}:`, data); // Обрабатываем разные типы сообщений switch (data.type) { case 'PIPELINE_UPDATE': this.handlePipelineUpdate(data); break; case 'PIPELINE_COMPLETED': this.handlePipelineCompleted(data); break; case 'PIPELINE_ERROR': this.handlePipelineError(data); break; case 'PIPELINE_CANCELLED': this.handlePipelineCancelled(data); break; case 'ASSISTANT_MESSAGE_CREATED': this.handleAssistantMessageCreated(data); break; default: console.log('📨 Received unknown message type:', data.type); eventBus.publish('websocket:message', { topic, data }); } } catch (error) { console.error('❌ Failed to parse WebSocket message:', error, message.body); } } /** * Handle pipeline update - этапы выполнения */ handlePipelineUpdate(data) { console.log('🔄 Pipeline update:', data); const { chatId, messageId: wsMessageId, stage } = data; // Находим реальный ID сообщения в состоянии const realMessageId = this.findOrCreateMessageIdMapping(chatId, wsMessageId); // Сохраняем этап в состоянии с реальным ID stateService.setPipelineStage(realMessageId, stage); // Публикуем событие для UI с реальным ID eventBus.publish(CONFIG.EVENTS.PIPELINE_STAGE_UPDATE, { chatId, messageId: realMessageId, stage }); } /** * Handle pipeline completed - завершение пайплайна */ async handlePipelineCompleted(data) { console.log('✅ Pipeline completed:', data); const { chatId, messageId: wsMessageId, result } = data; // Находим реальный ID сообщения в состоянии const realMessageId = this.findOrCreateMessageIdMapping(chatId, wsMessageId); // Удаляем этап из состояния stateService.removePipelineStage(realMessageId); // СНАЧАЛА обновляем локально из result if (realMessageId && result !== undefined) { console.log(`🔄 Updating message ${realMessageId} locally with result`); stateService.updateMessageContent(chatId, realMessageId, result, 'COMPLETED'); } // ЗАТЕМ запрашиваем с сервера для синхронизации try { // Ждем немного, чтобы сервер успел обновить БД await new Promise(resolve => setTimeout(resolve, 500)); // Инвалидируем кэш чата apiService.invalidateChatCache(chatId); // Запрашиваем актуальные сообщения с сервера const timestamp = Date.now(); const messagesResponse = await apiService.getMessagesWithCacheBust(chatId, timestamp); if (messagesResponse && messagesResponse.messages) { console.log(`📥 Server returned ${messagesResponse.messages.length} messages for chat ${chatId}`); // Ищем сообщение ассистента по реальному ID const serverAssistantMessage = messagesResponse.messages.find( msg => msg.id === realMessageId ); if (!serverAssistantMessage) { // Если не нашли по ID, ищем любое сообщение ассистента const anyAssistantMessage = messagesResponse.messages.find( msg => msg.role === 'ASSISTANT' ); if (anyAssistantMessage && anyAssistantMessage.content && anyAssistantMessage.content.trim() !== '') { console.log(`✅ Found assistant message with content, ID: ${anyAssistantMessage.id}`); stateService.updateMessageContent(chatId, anyAssistantMessage.id, anyAssistantMessage.content, 'COMPLETED'); } } else if (serverAssistantMessage.content && serverAssistantMessage.content.trim() !== '') { // Если сервер вернул непустое сообщение, используем его console.log(`✅ Server has non-empty content for message ${realMessageId}`); stateService.updateMessageContent(chatId, realMessageId, serverAssistantMessage.content, 'COMPLETED'); } else { // Иначе оставляем локальное обновление из result console.log(`⚠️ Server returned empty assistant message, keeping local update`); } // ВСЕГДА обновляем весь список сообщений из сервера для синхронизации stateService.setMessages(chatId, messagesResponse.messages); } } catch (error) { console.error(`❌ Failed to update messages for chat ${chatId}:`, error); // Если не удалось получить сообщения, оставляем локальное обновление } // Публикуем событие для UI eventBus.publish(CONFIG.EVENTS.PIPELINE_COMPLETED, { chatId, messageId: realMessageId, content: result }); } /** * Handle assistant message created - создание сообщения ассистента */ handleAssistantMessageCreated(data) { console.log('🤖 Assistant message created:', data); const { chatId, messageId, createdAt } = data; // Создаем сообщение ассистента с пустым телом const assistantMessage = { id: messageId, chatId: chatId, role: 'ASSISTANT', content: '', status: 'PROCESSING', createdAt: createdAt || new Date().toISOString() }; // Добавляем в состояние stateService.addMessage(chatId, assistantMessage); // Публикуем событие eventBus.publish(CONFIG.EVENTS.ASSISTANT_MESSAGE_CREATED, { chatId, messageId, message: assistantMessage }); } /** * Handle pipeline error */ handlePipelineError(data) { console.error('❌ Pipeline error:', data); const { chatId, messageId, error } = data; // Удаляем этап stateService.removePipelineStage(messageId); // Обновляем статус сообщения if (messageId) { stateService.updateMessageContent(chatId, messageId, `Ошибка: ${error}`, 'FAILED'); } // Инвалидируем кэш apiService.invalidateChatCache(chatId); eventBus.publish('pipeline:error', { chatId, messageId, error }); } /** * Handle pipeline cancelled */ handlePipelineCancelled(data) { console.log('⏹️ Pipeline cancelled:', data); const { chatId, messageId } = data; stateService.removePipelineStage(messageId); if (messageId) { stateService.updateMessageContent(chatId, messageId, 'Генерация отменена', 'CANCELLED'); } eventBus.publish('pipeline:cancelled', data); } findOrCreateMessageIdMapping(chatId, wsMessageId) { // Сначала ищем сообщение ассистента с пустым телом const emptyAssistantMsgId = stateService.findEmptyAssistantMessageId(chatId); if (emptyAssistantMsgId) { console.log(`🔗 Mapping WebSocket message ${wsMessageId} to existing assistant message ${emptyAssistantMsgId}`); return emptyAssistantMsgId; } // Если не нашли, ищем последнее сообщение ассистента const lastAssistantMsgId = stateService.findLastAssistantMessageId(chatId); if (lastAssistantMsgId) { console.log(`🔗 Mapping WebSocket message ${wsMessageId} to last assistant message ${lastAssistantMsgId}`); return lastAssistantMsgId; } // Если совсем не нашли, используем wsMessageId console.log(`⚠️ No assistant message found in chat ${chatId}, using WebSocket messageId`); return wsMessageId; } /** * Unsubscribe from topic */ unsubscribeFromTopic(topic) { const subscription = this.subscriptions.get(topic); if (subscription) { subscription.unsubscribe(); this.subscriptions.delete(topic); console.log(`📨 Unsubscribed from ${topic}`); } } /** * On error callback */ onError(error) { console.error('❌ WebSocket error:', error); this.isConnected = false; stateService.setState('isConnected', false); eventBus.publish(CONFIG.EVENTS.CONNECTION_LOST); this.scheduleReconnect(); } /** * Disconnect */ disconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.stompClient && this.isConnected) { try { this.stompClient.disconnect(() => { console.log('🔌 WebSocket disconnected'); }); } catch (error) { console.log('WebSocket already disconnected'); } } this.isConnected = false; this.subscriptions.clear(); this.pendingSubscriptions.clear(); this.activeChatSubscriptions.clear(); stateService.setState('isConnected', false); } /** * Schedule reconnect */ scheduleReconnect() { if (this.reconnectTimer) { return; } if (this.reconnectAttempts >= CONFIG.WEBSOCKET.MAX_RECONNECT_ATTEMPTS) { console.error('❌ Max reconnect attempts reached'); eventBus.publish(CONFIG.EVENTS.ERROR, { message: 'Connection lost. Please refresh the page.', }); return; } this.reconnectAttempts++; const delay = CONFIG.WEBSOCKET.RECONNECT_INTERVAL * this.reconnectAttempts; console.log( `⏳ Attempting to reconnect in ${delay}ms... (${this.reconnectAttempts}/${CONFIG.WEBSOCKET.MAX_RECONNECT_ATTEMPTS})` ); this.reconnectTimer = setTimeout(() => { this.connect(); }, delay); } /** * Check connection status */ getStatus() { return { isConnected: this.isConnected, reconnectAttempts: this.reconnectAttempts, subscriptions: this.subscriptions.size }; } } const webSocketService = new WebSocketService(); console.log('🔗 WebSocket service ready'); window.addEventListener('beforeunload', () => { webSocketService.disconnect(); });