ciyidogan commited on
Commit
5e664a7
Β·
verified Β·
1 Parent(s): 94f7a1e

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.errorSubject.next('WebSocket bağlantı hatası');
82
  reject(error);
83
  };
84
 
85
- this.socket.onclose = () => {
86
- console.log('πŸ”Œ WebSocket disconnected');
 
 
 
 
 
 
87
  this.connectionSubject.next(false);
88
  this.stopKeepAlive();
89
 
90
- // Attempt reconnection
91
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
 
 
 
 
 
 
 
 
 
 
 
 
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
- this.stopKeepAlive();
105
-
106
- if (this.socket) {
107
- this.socket.close();
108
- this.socket = null;
 
 
 
 
 
 
 
 
 
109
  }
110
-
111
- this.connectionSubject.next(false);
112
  }
113
 
114
  send(message: WebSocketMessage): void {
115
- if (this.socket && this.socket.readyState === WebSocket.OPEN) {
116
- this.socket.send(JSON.stringify(message));
117
- } else {
118
- console.warn('WebSocket is not connected');
119
- this.errorSubject.next('WebSocket bağlantısı yok');
 
 
 
 
 
 
 
 
 
 
 
120
  }
121
  }
122
 
123
  sendAudioChunk(audioData: string): void {
124
- this.send({
125
- type: 'audio_chunk',
126
- data: audioData,
127
- timestamp: Date.now()
128
- });
 
 
 
 
 
129
  }
130
 
131
  sendControl(action: string, config?: any): void {
132
- this.send({
133
- type: 'control',
134
- action: action,
135
- config: config
136
- });
 
 
 
 
 
 
137
  }
138
 
139
  private handleMessage(message: WebSocketMessage): void {
140
- // Emit to general message stream
141
- this.messageSubject.next(message);
142
-
143
- // Handle specific message types
144
- switch (message.type) {
145
- case 'transcription':
146
- this.transcriptionSubject.next({
147
- text: message['text'],
148
- is_final: message['is_final'],
149
- confidence: message['confidence']
150
- });
151
- break;
152
-
153
- case 'state_change':
154
- this.stateChangeSubject.next({
155
- from: message['from'],
156
- to: message['to']
157
- });
158
- break;
159
-
160
- case 'error':
161
- this.errorSubject.next(message['message']);
162
- break;
163
-
164
- case 'tts_audio':
165
- // Handle TTS audio chunks
166
- this.messageSubject.next(message);
167
- break;
168
-
169
- case 'assistant_response':
170
- // Handle assistant text response
171
- this.messageSubject.next(message);
172
- break;
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
  }
175
 
176
  private attemptReconnect(sessionId: string): void {
177
  this.reconnectAttempts++;
178
- const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
 
 
 
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
- if (this.socket && this.socket.readyState === WebSocket.OPEN) {
195
- this.send({ type: 'ping' });
 
 
 
 
 
 
 
 
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
  }