Spaces:
Running
Running
Update flare-ui/src/app/services/websocket.service.ts
Browse files
flare-ui/src/app/services/websocket.service.ts
CHANGED
@@ -1,6 +1,9 @@
|
|
|
|
|
|
|
|
1 |
import { Injectable } from '@angular/core';
|
2 |
-
import { Subject, Observable, timer } from 'rxjs';
|
3 |
-
import { retry, tap } from 'rxjs/operators';
|
4 |
|
5 |
export interface WebSocketMessage {
|
6 |
type: string;
|
@@ -27,6 +30,9 @@ export class WebSocketService {
|
|
27 |
private reconnectAttempts = 0;
|
28 |
private maxReconnectAttempts = 5;
|
29 |
private reconnectDelay = 1000;
|
|
|
|
|
|
|
30 |
|
31 |
// Subjects for different message types
|
32 |
private messageSubject = new Subject<WebSocketMessage>();
|
@@ -47,6 +53,17 @@ export class WebSocketService {
|
|
47 |
connect(sessionId: string): Promise<void> {
|
48 |
return new Promise((resolve, reject) => {
|
49 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
// Construct WebSocket URL
|
51 |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
52 |
const host = window.location.host;
|
@@ -54,9 +71,18 @@ export class WebSocketService {
|
|
54 |
|
55 |
console.log(`π Connecting to WebSocket: ${this.url}`);
|
56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
this.socket = new WebSocket(this.url);
|
58 |
|
59 |
this.socket.onopen = () => {
|
|
|
60 |
console.log('β
WebSocket connected');
|
61 |
this.reconnectAttempts = 0;
|
62 |
this.connectionSubject.next(true);
|
@@ -73,126 +99,217 @@ export class WebSocketService {
|
|
73 |
this.handleMessage(message);
|
74 |
} catch (error) {
|
75 |
console.error('Failed to parse WebSocket message:', error);
|
|
|
76 |
}
|
77 |
};
|
78 |
|
79 |
this.socket.onerror = (error) => {
|
|
|
80 |
console.error('β WebSocket error:', error);
|
81 |
-
this.
|
82 |
reject(error);
|
83 |
};
|
84 |
|
85 |
-
this.socket.onclose = () => {
|
86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
this.connectionSubject.next(false);
|
88 |
this.stopKeepAlive();
|
89 |
|
90 |
-
//
|
91 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
this.attemptReconnect(sessionId);
|
93 |
}
|
94 |
};
|
95 |
|
96 |
} catch (error) {
|
97 |
console.error('Failed to create WebSocket:', error);
|
|
|
98 |
reject(error);
|
99 |
}
|
100 |
});
|
101 |
}
|
102 |
|
103 |
disconnect(): void {
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
this.socket
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
}
|
110 |
-
|
111 |
-
this.connectionSubject.next(false);
|
112 |
}
|
113 |
|
114 |
send(message: WebSocketMessage): void {
|
115 |
-
|
116 |
-
this.socket.
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
}
|
121 |
}
|
122 |
|
123 |
sendAudioChunk(audioData: string): void {
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
|
|
|
|
|
|
|
|
|
|
129 |
}
|
130 |
|
131 |
sendControl(action: string, config?: any): void {
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
}
|
138 |
|
139 |
private handleMessage(message: WebSocketMessage): void {
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
}
|
174 |
}
|
175 |
|
176 |
private attemptReconnect(sessionId: string): void {
|
177 |
this.reconnectAttempts++;
|
178 |
-
const delay =
|
|
|
|
|
|
|
179 |
|
180 |
console.log(`π Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
181 |
|
182 |
setTimeout(() => {
|
183 |
this.connect(sessionId).catch(error => {
|
184 |
console.error('Reconnection failed:', error);
|
|
|
|
|
|
|
|
|
185 |
});
|
186 |
}, delay);
|
187 |
}
|
188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
189 |
// Keep-alive mechanism
|
190 |
-
private keepAliveInterval: any;
|
191 |
-
|
192 |
private startKeepAlive(): void {
|
|
|
|
|
193 |
this.keepAliveInterval = setInterval(() => {
|
194 |
-
|
195 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
}
|
197 |
}, 30000); // Ping every 30 seconds
|
198 |
}
|
@@ -207,4 +324,37 @@ export class WebSocketService {
|
|
207 |
isConnected(): boolean {
|
208 |
return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
|
209 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
}
|
|
|
1 |
+
// websocket.service.ts
|
2 |
+
// Path: /flare-ui/src/app/services/websocket.service.ts
|
3 |
+
|
4 |
import { Injectable } from '@angular/core';
|
5 |
+
import { Subject, Observable, timer, throwError } from 'rxjs';
|
6 |
+
import { retry, tap, catchError } from 'rxjs/operators';
|
7 |
|
8 |
export interface WebSocketMessage {
|
9 |
type: string;
|
|
|
30 |
private reconnectAttempts = 0;
|
31 |
private maxReconnectAttempts = 5;
|
32 |
private reconnectDelay = 1000;
|
33 |
+
private keepAliveInterval: any;
|
34 |
+
private connectionTimeout: any;
|
35 |
+
private readonly CONNECTION_TIMEOUT = 30000; // 30 seconds
|
36 |
|
37 |
// Subjects for different message types
|
38 |
private messageSubject = new Subject<WebSocketMessage>();
|
|
|
53 |
connect(sessionId: string): Promise<void> {
|
54 |
return new Promise((resolve, reject) => {
|
55 |
try {
|
56 |
+
if (!sessionId) {
|
57 |
+
const error = new Error('Session ID is required');
|
58 |
+
reject(error);
|
59 |
+
return;
|
60 |
+
}
|
61 |
+
|
62 |
+
// Close existing connection if any
|
63 |
+
if (this.socket) {
|
64 |
+
this.disconnect();
|
65 |
+
}
|
66 |
+
|
67 |
// Construct WebSocket URL
|
68 |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
69 |
const host = window.location.host;
|
|
|
71 |
|
72 |
console.log(`π Connecting to WebSocket: ${this.url}`);
|
73 |
|
74 |
+
// Set connection timeout
|
75 |
+
this.connectionTimeout = setTimeout(() => {
|
76 |
+
const error = new Error('WebSocket connection timeout');
|
77 |
+
console.error('β WebSocket connection timeout');
|
78 |
+
this.handleConnectionError(error);
|
79 |
+
reject(error);
|
80 |
+
}, this.CONNECTION_TIMEOUT);
|
81 |
+
|
82 |
this.socket = new WebSocket(this.url);
|
83 |
|
84 |
this.socket.onopen = () => {
|
85 |
+
clearTimeout(this.connectionTimeout);
|
86 |
console.log('β
WebSocket connected');
|
87 |
this.reconnectAttempts = 0;
|
88 |
this.connectionSubject.next(true);
|
|
|
99 |
this.handleMessage(message);
|
100 |
} catch (error) {
|
101 |
console.error('Failed to parse WebSocket message:', error);
|
102 |
+
this.errorSubject.next('Invalid message format received');
|
103 |
}
|
104 |
};
|
105 |
|
106 |
this.socket.onerror = (error) => {
|
107 |
+
clearTimeout(this.connectionTimeout);
|
108 |
console.error('β WebSocket error:', error);
|
109 |
+
this.handleConnectionError(error);
|
110 |
reject(error);
|
111 |
};
|
112 |
|
113 |
+
this.socket.onclose = (event) => {
|
114 |
+
clearTimeout(this.connectionTimeout);
|
115 |
+
console.log('π WebSocket disconnected', {
|
116 |
+
code: event.code,
|
117 |
+
reason: event.reason,
|
118 |
+
wasClean: event.wasClean
|
119 |
+
});
|
120 |
+
|
121 |
this.connectionSubject.next(false);
|
122 |
this.stopKeepAlive();
|
123 |
|
124 |
+
// Handle different close codes
|
125 |
+
if (event.code === 1006) {
|
126 |
+
// Abnormal closure
|
127 |
+
this.errorSubject.next('WebSocket connection lost unexpectedly');
|
128 |
+
} else if (event.code === 1000) {
|
129 |
+
// Normal closure
|
130 |
+
console.log('WebSocket closed normally');
|
131 |
+
} else {
|
132 |
+
// Other closure codes
|
133 |
+
this.errorSubject.next(`WebSocket closed: ${event.reason || 'Unknown reason'}`);
|
134 |
+
}
|
135 |
+
|
136 |
+
// Attempt reconnection for non-normal closures
|
137 |
+
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
|
138 |
this.attemptReconnect(sessionId);
|
139 |
}
|
140 |
};
|
141 |
|
142 |
} catch (error) {
|
143 |
console.error('Failed to create WebSocket:', error);
|
144 |
+
this.handleConnectionError(error);
|
145 |
reject(error);
|
146 |
}
|
147 |
});
|
148 |
}
|
149 |
|
150 |
disconnect(): void {
|
151 |
+
try {
|
152 |
+
this.stopKeepAlive();
|
153 |
+
clearTimeout(this.connectionTimeout);
|
154 |
+
|
155 |
+
if (this.socket) {
|
156 |
+
// Close with normal closure code
|
157 |
+
this.socket.close(1000, 'Client disconnect');
|
158 |
+
this.socket = null;
|
159 |
+
}
|
160 |
+
|
161 |
+
this.connectionSubject.next(false);
|
162 |
+
this.reconnectAttempts = 0;
|
163 |
+
} catch (error) {
|
164 |
+
console.error('Error during disconnect:', error);
|
165 |
}
|
|
|
|
|
166 |
}
|
167 |
|
168 |
send(message: WebSocketMessage): void {
|
169 |
+
try {
|
170 |
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
171 |
+
const messageStr = JSON.stringify(message);
|
172 |
+
this.socket.send(messageStr);
|
173 |
+
} else {
|
174 |
+
const errorMsg = 'WebSocket is not connected';
|
175 |
+
console.warn(errorMsg);
|
176 |
+
this.errorSubject.next(errorMsg);
|
177 |
+
|
178 |
+
// Optionally throw error for caller to handle
|
179 |
+
throw new Error(errorMsg);
|
180 |
+
}
|
181 |
+
} catch (error) {
|
182 |
+
console.error('Error sending message:', error);
|
183 |
+
this.errorSubject.next('Failed to send message');
|
184 |
+
throw error;
|
185 |
}
|
186 |
}
|
187 |
|
188 |
sendAudioChunk(audioData: string): void {
|
189 |
+
try {
|
190 |
+
this.send({
|
191 |
+
type: 'audio_chunk',
|
192 |
+
data: audioData,
|
193 |
+
timestamp: Date.now()
|
194 |
+
});
|
195 |
+
} catch (error) {
|
196 |
+
console.error('Failed to send audio chunk:', error);
|
197 |
+
// Don't re-throw for audio chunks to avoid interrupting stream
|
198 |
+
}
|
199 |
}
|
200 |
|
201 |
sendControl(action: string, config?: any): void {
|
202 |
+
try {
|
203 |
+
this.send({
|
204 |
+
type: 'control',
|
205 |
+
action: action,
|
206 |
+
config: config,
|
207 |
+
timestamp: Date.now()
|
208 |
+
});
|
209 |
+
} catch (error) {
|
210 |
+
console.error(`Failed to send control action '${action}':`, error);
|
211 |
+
throw error;
|
212 |
+
}
|
213 |
}
|
214 |
|
215 |
private handleMessage(message: WebSocketMessage): void {
|
216 |
+
try {
|
217 |
+
// Emit to general message stream
|
218 |
+
this.messageSubject.next(message);
|
219 |
+
|
220 |
+
// Handle specific message types
|
221 |
+
switch (message.type) {
|
222 |
+
case 'transcription':
|
223 |
+
this.transcriptionSubject.next({
|
224 |
+
text: message['text'] || '',
|
225 |
+
is_final: message['is_final'] || false,
|
226 |
+
confidence: message['confidence'] || 0
|
227 |
+
});
|
228 |
+
break;
|
229 |
+
|
230 |
+
case 'state_change':
|
231 |
+
this.stateChangeSubject.next({
|
232 |
+
from: message['from'] || 'unknown',
|
233 |
+
to: message['to'] || 'unknown'
|
234 |
+
});
|
235 |
+
break;
|
236 |
+
|
237 |
+
case 'error':
|
238 |
+
const errorMessage = message['message'] || 'Unknown error';
|
239 |
+
this.errorSubject.next(errorMessage);
|
240 |
+
|
241 |
+
// Handle specific error types
|
242 |
+
if (message['error_type'] === 'race_condition') {
|
243 |
+
console.warn('Race condition detected in WebSocket message');
|
244 |
+
}
|
245 |
+
break;
|
246 |
+
|
247 |
+
case 'tts_audio':
|
248 |
+
case 'assistant_response':
|
249 |
+
// These are handled by general message stream
|
250 |
+
break;
|
251 |
+
|
252 |
+
case 'pong':
|
253 |
+
// Keep-alive response
|
254 |
+
break;
|
255 |
+
|
256 |
+
default:
|
257 |
+
console.warn('Unknown message type:', message.type);
|
258 |
+
}
|
259 |
+
} catch (error) {
|
260 |
+
console.error('Error handling message:', error);
|
261 |
+
this.errorSubject.next('Error processing message');
|
262 |
}
|
263 |
}
|
264 |
|
265 |
private attemptReconnect(sessionId: string): void {
|
266 |
this.reconnectAttempts++;
|
267 |
+
const delay = Math.min(
|
268 |
+
this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
269 |
+
30000 // Max 30 seconds
|
270 |
+
);
|
271 |
|
272 |
console.log(`π Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
273 |
|
274 |
setTimeout(() => {
|
275 |
this.connect(sessionId).catch(error => {
|
276 |
console.error('Reconnection failed:', error);
|
277 |
+
|
278 |
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
279 |
+
this.errorSubject.next('Maximum reconnection attempts reached');
|
280 |
+
}
|
281 |
});
|
282 |
}, delay);
|
283 |
}
|
284 |
|
285 |
+
private handleConnectionError(error: any): void {
|
286 |
+
const errorMessage = error?.message || 'WebSocket connection error';
|
287 |
+
this.errorSubject.next(errorMessage);
|
288 |
+
|
289 |
+
// Log additional error details
|
290 |
+
console.error('WebSocket connection error details:', {
|
291 |
+
message: errorMessage,
|
292 |
+
type: error?.type,
|
293 |
+
target: error?.target,
|
294 |
+
timestamp: new Date().toISOString()
|
295 |
+
});
|
296 |
+
}
|
297 |
+
|
298 |
// Keep-alive mechanism
|
|
|
|
|
299 |
private startKeepAlive(): void {
|
300 |
+
this.stopKeepAlive(); // Clear any existing interval
|
301 |
+
|
302 |
this.keepAliveInterval = setInterval(() => {
|
303 |
+
try {
|
304 |
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
305 |
+
this.send({ type: 'ping', timestamp: Date.now() });
|
306 |
+
} else {
|
307 |
+
console.warn('Keep-alive: WebSocket not open');
|
308 |
+
this.stopKeepAlive();
|
309 |
+
}
|
310 |
+
} catch (error) {
|
311 |
+
console.error('Keep-alive error:', error);
|
312 |
+
this.stopKeepAlive();
|
313 |
}
|
314 |
}, 30000); // Ping every 30 seconds
|
315 |
}
|
|
|
324 |
isConnected(): boolean {
|
325 |
return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
|
326 |
}
|
327 |
+
|
328 |
+
getConnectionState(): string {
|
329 |
+
if (!this.socket) return 'DISCONNECTED';
|
330 |
+
|
331 |
+
switch (this.socket.readyState) {
|
332 |
+
case WebSocket.CONNECTING:
|
333 |
+
return 'CONNECTING';
|
334 |
+
case WebSocket.OPEN:
|
335 |
+
return 'CONNECTED';
|
336 |
+
case WebSocket.CLOSING:
|
337 |
+
return 'CLOSING';
|
338 |
+
case WebSocket.CLOSED:
|
339 |
+
return 'CLOSED';
|
340 |
+
default:
|
341 |
+
return 'UNKNOWN';
|
342 |
+
}
|
343 |
+
}
|
344 |
+
|
345 |
+
// Get reconnection status
|
346 |
+
getReconnectionInfo(): { attempts: number, maxAttempts: number, isReconnecting: boolean } {
|
347 |
+
return {
|
348 |
+
attempts: this.reconnectAttempts,
|
349 |
+
maxAttempts: this.maxReconnectAttempts,
|
350 |
+
isReconnecting: this.reconnectAttempts > 0 && this.reconnectAttempts < this.maxReconnectAttempts
|
351 |
+
};
|
352 |
+
}
|
353 |
+
|
354 |
+
// Force reconnect
|
355 |
+
forceReconnect(sessionId: string): Promise<void> {
|
356 |
+
this.reconnectAttempts = 0;
|
357 |
+
this.disconnect();
|
358 |
+
return this.connect(sessionId);
|
359 |
+
}
|
360 |
}
|