Spaces:
Paused
Paused
| import { Injectable } from '@angular/core'; | |
| import { Subject, Observable, timer } from 'rxjs'; | |
| import { retry, tap } from 'rxjs/operators'; | |
| export interface WebSocketMessage { | |
| type: string; | |
| [key: string]: any; | |
| } | |
| export interface TranscriptionResult { | |
| text: string; | |
| is_final: boolean; | |
| confidence: number; | |
| } | |
| export interface StateChangeMessage { | |
| from: string; | |
| to: string; | |
| } | |
| ({ | |
| providedIn: 'root' | |
| }) | |
| export class WebSocketService { | |
| private socket: WebSocket | null = null; | |
| private url: string = ''; | |
| private reconnectAttempts = 0; | |
| private maxReconnectAttempts = 5; | |
| private reconnectDelay = 1000; | |
| // Subjects for different message types | |
| private messageSubject = new Subject<WebSocketMessage>(); | |
| private transcriptionSubject = new Subject<TranscriptionResult>(); | |
| private stateChangeSubject = new Subject<StateChangeMessage>(); | |
| private errorSubject = new Subject<string>(); | |
| private connectionSubject = new Subject<boolean>(); | |
| // Public observables | |
| public message$ = this.messageSubject.asObservable(); | |
| public transcription$ = this.transcriptionSubject.asObservable(); | |
| public stateChange$ = this.stateChangeSubject.asObservable(); | |
| public error$ = this.errorSubject.asObservable(); | |
| public connection$ = this.connectionSubject.asObservable(); | |
| constructor() {} | |
| connect(sessionId: string): Promise<void> { | |
| return new Promise((resolve, reject) => { | |
| try { | |
| // Construct WebSocket URL | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const host = window.location.host; | |
| this.url = `${protocol}//${host}/ws/conversation/${sessionId}`; | |
| console.log(`🔌 Connecting to WebSocket: ${this.url}`); | |
| this.socket = new WebSocket(this.url); | |
| this.socket.onopen = () => { | |
| console.log('✅ WebSocket connected'); | |
| this.reconnectAttempts = 0; | |
| this.connectionSubject.next(true); | |
| // Start keep-alive ping | |
| this.startKeepAlive(); | |
| resolve(); | |
| }; | |
| this.socket.onmessage = (event) => { | |
| try { | |
| const message: WebSocketMessage = JSON.parse(event.data); | |
| this.handleMessage(message); | |
| } catch (error) { | |
| console.error('Failed to parse WebSocket message:', error); | |
| } | |
| }; | |
| this.socket.onerror = (error) => { | |
| console.error('❌ WebSocket error:', error); | |
| this.errorSubject.next('WebSocket bağlantı hatası'); | |
| reject(error); | |
| }; | |
| this.socket.onclose = () => { | |
| console.log('🔌 WebSocket disconnected'); | |
| this.connectionSubject.next(false); | |
| this.stopKeepAlive(); | |
| // Attempt reconnection | |
| if (this.reconnectAttempts < this.maxReconnectAttempts) { | |
| this.attemptReconnect(sessionId); | |
| } | |
| }; | |
| } catch (error) { | |
| console.error('Failed to create WebSocket:', error); | |
| reject(error); | |
| } | |
| }); | |
| } | |
| disconnect(): void { | |
| this.stopKeepAlive(); | |
| if (this.socket) { | |
| this.socket.close(); | |
| this.socket = null; | |
| } | |
| this.connectionSubject.next(false); | |
| } | |
| send(message: WebSocketMessage): void { | |
| if (this.socket && this.socket.readyState === WebSocket.OPEN) { | |
| this.socket.send(JSON.stringify(message)); | |
| } else { | |
| console.warn('WebSocket is not connected'); | |
| this.errorSubject.next('WebSocket bağlantısı yok'); | |
| } | |
| } | |
| sendAudioChunk(audioData: string): void { | |
| this.send({ | |
| type: 'audio_chunk', | |
| data: audioData, | |
| timestamp: Date.now() | |
| }); | |
| } | |
| sendControl(action: string, config?: any): void { | |
| this.send({ | |
| type: 'control', | |
| action: action, | |
| config: config | |
| }); | |
| } | |
| private handleMessage(message: WebSocketMessage): void { | |
| // Emit to general message stream | |
| this.messageSubject.next(message); | |
| // Handle specific message types | |
| switch (message.type) { | |
| case 'transcription': | |
| this.transcriptionSubject.next({ | |
| text: message['text'], | |
| is_final: message['is_final'], | |
| confidence: message['confidence'] | |
| }); | |
| break; | |
| case 'state_change': | |
| this.stateChangeSubject.next({ | |
| from: message['from'], | |
| to: message['to'] | |
| }); | |
| break; | |
| case 'error': | |
| this.errorSubject.next(message['message']); | |
| break; | |
| case 'tts_audio': | |
| // Handle TTS audio chunks | |
| this.messageSubject.next(message); | |
| break; | |
| case 'assistant_response': | |
| // Handle assistant text response | |
| this.messageSubject.next(message); | |
| break; | |
| } | |
| } | |
| private attemptReconnect(sessionId: string): void { | |
| this.reconnectAttempts++; | |
| const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); | |
| console.log(`🔄 Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`); | |
| setTimeout(() => { | |
| this.connect(sessionId).catch(error => { | |
| console.error('Reconnection failed:', error); | |
| }); | |
| }, delay); | |
| } | |
| // Keep-alive mechanism | |
| private keepAliveInterval: any; | |
| private startKeepAlive(): void { | |
| this.keepAliveInterval = setInterval(() => { | |
| if (this.socket && this.socket.readyState === WebSocket.OPEN) { | |
| this.send({ type: 'ping' }); | |
| } | |
| }, 30000); // Ping every 30 seconds | |
| } | |
| private stopKeepAlive(): void { | |
| if (this.keepAliveInterval) { | |
| clearInterval(this.keepAliveInterval); | |
| this.keepAliveInterval = null; | |
| } | |
| } | |
| isConnected(): boolean { | |
| return this.socket !== null && this.socket.readyState === WebSocket.OPEN; | |
| } | |
| } |