solver-sdk 1.6.3 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
+ // Импортируем Socket.IO клиент
2
+ import { io } from 'socket.io-client';
1
3
  /**
2
- * Базовый класс для WebSocket клиентов
4
+ * Базовый класс для WebSocket клиентов, реализованный на базе Socket.IO
3
5
  */
4
6
  export class WebSocketClient {
5
7
  /**
@@ -8,8 +10,8 @@ export class WebSocketClient {
8
10
  * @param {WebSocketClientOptions} [options] Опции клиента
9
11
  */
10
12
  constructor(url, options = {}) {
11
- /** Экземпляр WebSocket */
12
- this.webSocket = null;
13
+ /** Экземпляр Socket.IO */
14
+ this.socket = null;
13
15
  /** Счетчик попыток переподключения */
14
16
  this.retryCount = 0;
15
17
  /** Флаг, указывающий, что соединение было закрыто намеренно */
@@ -30,6 +32,11 @@ export class WebSocketClient {
30
32
  this.socketId = null;
31
33
  /** Хранилище ожидающих callback-функций */
32
34
  this._pendingCallbacks = new Map();
35
+ /**
36
+ * Тип для хранения отложенных обработчиков событий
37
+ * @private
38
+ */
39
+ this._pendingCallbackHandlers = new Map();
33
40
  this.url = url;
34
41
  this.options = {
35
42
  headers: options.headers || {},
@@ -79,12 +86,12 @@ export class WebSocketClient {
79
86
  });
80
87
  }
81
88
  /**
82
- * Подключается к WebSocket серверу
89
+ * Подключается к WebSocket серверу используя Socket.IO клиент
83
90
  * @returns {Promise<void>}
84
91
  */
85
92
  connect() {
86
93
  // Если соединение уже установлено, возвращаем Promise.resolve
87
- if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
94
+ if (this.socket && this.socket.connected) {
88
95
  this.logger('info', 'Соединение уже установлено');
89
96
  return Promise.resolve();
90
97
  }
@@ -92,366 +99,216 @@ export class WebSocketClient {
92
99
  this.intentionallyClosed = false;
93
100
  return new Promise((resolve, reject) => {
94
101
  try {
95
- // Проверяем, содержит ли URL параметры Socket.IO
102
+ // Формируем корректный URL с namespace для Socket.IO
96
103
  let wsUrl = this.url;
97
- // Проверяем, является ли это Socket.IO подключением
98
- if (wsUrl.includes('/socket.io/') || wsUrl.includes('?EIO=')) {
99
- // Убедимся, что в URL есть параметры для Socket.IO версии 4
100
- if (!wsUrl.includes('EIO=')) {
101
- // Добавляем разделитель: ? или &
102
- const separator = wsUrl.includes('?') ? '&' : '?';
103
- wsUrl += `${separator}EIO=4&transport=websocket`;
104
- }
105
- // Добавляем timestamp для предотвращения кеширования
106
- if (!wsUrl.includes('t=')) {
107
- const separator = wsUrl.includes('?') ? '&' : '?';
108
- wsUrl += `${separator}t=${Date.now()}`;
109
- }
110
- }
111
- this.logger('info', 'Выполняется подключение', { url: wsUrl });
112
- // Создаем новый экземпляр WebSocket
113
- if (this.isBrowser) {
114
- // Браузерное окружение
115
- this.webSocket = new WebSocket(wsUrl, this.options.protocols);
116
- }
117
- else {
118
- // Node.js окружение
119
- try {
120
- // Динамически импортируем ws модуль в Node.js
121
- const WebSocketImpl = require('ws');
122
- this.webSocket = new WebSocketImpl(wsUrl, {
123
- headers: this.options.headers,
124
- protocol: this.options.protocols,
125
- rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0" ? false : (this.options.rejectUnauthorized === false ? false : true)
126
- });
127
- }
128
- catch (error) {
129
- const errorMsg = `Не удалось загрузить модуль ws в Node.js: ${error.message}`;
130
- this.logger('error', errorMsg);
131
- reject(new Error(errorMsg));
132
- return;
133
- }
104
+ let namespaceStr = this.namespace;
105
+ // Для Socket.IO не нужно добавлять параметры в URL, они будут добавлены библиотекой
106
+ this.logger('info', 'Выполняется подключение', { url: wsUrl, namespace: namespaceStr });
107
+ // Настройки для Socket.IO клиента
108
+ const socketOptions = {
109
+ transports: ['websocket'], // Используем только WebSocket транспорт
110
+ reconnection: this.options.autoReconnect, // Автоматическое переподключение
111
+ reconnectionAttempts: this.options.maxRetries, // Количество попыток
112
+ reconnectionDelay: this.options.retryDelay, // Задержка между попытками
113
+ timeout: this.options.connectionTimeout, // Таймаут соединения
114
+ forceNew: true, // Создавать новое соединение
115
+ extraHeaders: this.options.headers, // HTTP заголовки
116
+ rejectUnauthorized: this.options.rejectUnauthorized // Проверка сертификатов
117
+ };
118
+ // Если указан API ключ, добавляем его в query и auth
119
+ if (this.options.apiKey) {
120
+ socketOptions.auth = { token: this.options.apiKey };
121
+ socketOptions.query = { token: this.options.apiKey };
134
122
  }
123
+ // Создаем Socket.IO клиент с namespace
124
+ this.socket = io(wsUrl + namespaceStr, socketOptions);
135
125
  // Устанавливаем таймаут соединения
136
126
  this.connectionTimeoutTimer = setTimeout(() => {
137
- if (this.webSocket && this.webSocket.readyState !== WebSocket.OPEN) {
138
- reject(new Error('Таймаут подключения WebSocket'));
127
+ if (this.socket && !this.socket.connected) {
128
+ const error = new Error('Таймаут подключения WebSocket');
129
+ this.logger('error', 'Превышен таймаут подключения', { timeout: this.options.connectionTimeout });
130
+ reject(error);
139
131
  this.close();
140
132
  }
141
133
  }, this.options.connectionTimeout);
142
- // Обработчик открытия соединения
143
- this.webSocket.onopen = () => {
134
+ // Обработчик успешного подключения
135
+ this.socket.on('connect', () => {
144
136
  clearTimeout(this.connectionTimeoutTimer);
145
137
  this.retryCount = 0;
146
138
  this.connected = true;
147
- this.logger('info', 'WebSocket соединение установлено');
139
+ this.socketId = this.socket?.id || null;
140
+ this.logger('info', 'WebSocket соединение установлено', { socketId: this.socketId });
148
141
  // Отправляем сообщения из очереди
149
142
  while (this.messageQueue.length > 0) {
150
143
  const message = this.messageQueue.shift();
151
- if (message && this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
152
- this.webSocket.send(message);
153
- }
154
- }
155
- // Запускаем таймер для ping в Socket.IO
156
- this.setupPingPongTimer();
157
- resolve();
158
- this.dispatchEvent('open', {});
159
- };
160
- // Обработчик сообщений
161
- this.webSocket.onmessage = (event) => {
162
- try {
163
- // Пытаемся распарсить сообщение как JSON
164
- let data = event.data;
165
- // Обработка сообщений Socket.IO
166
- if (typeof data === 'string') {
167
- // Обработка Engine.IO - Ping/Pong (2/3)
168
- if (data === '2') {
169
- // Engine.IO ping, отправляем понг
170
- if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
171
- this.webSocket.send('3');
172
- }
173
- this.dispatchEvent('ping', {});
174
- return;
175
- }
176
- else if (data === '3') {
177
- // Engine.IO pong
178
- this.dispatchEvent('pong', {});
179
- return;
180
- }
181
- // Обработка открытия соединения Socket.IO
182
- if (data.startsWith('0')) {
183
- try {
184
- const authData = JSON.parse(data.substring(1));
185
- this.dispatchEvent('socket.io_open', authData);
186
- // Вызываем обработчик открытия соединения Socket.IO
187
- this.handleSocketIOOpen(authData);
188
- return;
189
- }
190
- catch (e) {
191
- // Игнорируем ошибки парсинга
192
- }
193
- }
194
- // Обработка Socket.IO событий (42)
195
- if (data.startsWith('42')) {
196
- // Формат: 42/namespace,[event,data]
197
- let namespace = '';
198
- let eventData = data.substring(2);
199
- // Извлекаем namespace, если есть
200
- if (eventData.startsWith('/')) {
201
- const namespaceEnd = eventData.indexOf(',');
202
- if (namespaceEnd !== -1) {
203
- namespace = eventData.substring(0, namespaceEnd);
204
- eventData = eventData.substring(namespaceEnd + 1);
205
- }
206
- }
207
- try {
208
- const parsedData = JSON.parse(eventData);
209
- if (Array.isArray(parsedData) && parsedData.length >= 1) {
210
- const event = parsedData[0];
211
- const eventPayload = parsedData.length > 1 ? parsedData[1] : null;
212
- // Запускаем обработчик для данного события
213
- this.dispatchEvent(event, eventPayload);
214
- // Также отправляем общее событие socket.io с информацией о полученном событии
215
- this.dispatchEvent('socket.io_event', {
216
- event,
217
- data: eventPayload,
218
- namespace
219
- });
220
- // Обработка специальных событий для стриминга
221
- if (event === 'text_delta' && eventPayload?.delta?.text) {
222
- this.dispatchEvent('streaming_delta', { type: 'text', content: eventPayload.delta.text });
223
- }
224
- else if (event === 'thinking_delta' && eventPayload?.delta?.thinking) {
225
- this.dispatchEvent('streaming_delta', { type: 'thinking', content: eventPayload.delta.thinking });
226
- }
227
- else if (event === 'message_stop') {
228
- this.dispatchEvent('streaming_complete', {});
229
- }
230
- }
231
- }
232
- catch (e) {
233
- this.dispatchEvent('error', { message: `Ошибка парсинга Socket.IO данных: ${e instanceof Error ? e.message : 'Неизвестная ошибка'}` });
234
- }
235
- return;
236
- }
237
- // Определяем тип пакета Socket.IO по первому символу
238
- const packetType = data.charAt(0);
239
- // Если это пакет данных (тип 0-6)
240
- if (packetType >= '0' && packetType <= '6') {
241
- // Отправляем особое событие для Socket.IO пакетов
242
- this.dispatchEvent('socket.io_raw', {
243
- type: packetType,
244
- data: data.substring(1)
245
- });
246
- // Пытаемся обработать содержимое
247
- if (data.length > 1) {
248
- try {
249
- // Обработка пакета с namespace
250
- let jsonStart = 1;
251
- let namespace = '';
252
- // Если есть namespace, извлекаем его
253
- if (data.charAt(1) === '/') {
254
- const namespaceEnd = data.indexOf('{', 1);
255
- if (namespaceEnd !== -1) {
256
- namespace = data.substring(1, namespaceEnd);
257
- jsonStart = namespaceEnd;
258
- }
259
- }
260
- // Пытаемся преобразовать оставшуюся часть в JSON
261
- const jsonStr = data.substring(jsonStart);
262
- if (jsonStr) {
263
- try {
264
- const parsedData = JSON.parse(jsonStr);
265
- this.dispatchEvent('message', parsedData);
266
- }
267
- catch (e) {
268
- // Если не можем преобразовать в JSON, отправляем как есть
269
- this.dispatchEvent('message', jsonStr);
270
- }
271
- }
272
- }
273
- catch (e) {
274
- // Игнорируем ошибки парсинга
275
- }
276
- }
144
+ if (message && this.socket && this.socket.connected) {
145
+ if (typeof message === 'object' && message.event) {
146
+ this.socket.emit(message.event, message.data);
277
147
  }
278
148
  else {
279
- // Если это не Socket.IO пакет, пытаемся обработать как обычное сообщение
280
- try {
281
- const parsedData = JSON.parse(data);
282
- this.dispatchEvent('message', parsedData);
283
- }
284
- catch (e) {
285
- // Если не можем преобразовать в JSON, отправляем как есть
286
- this.dispatchEvent('message', data);
287
- }
149
+ // Поддержка старого формата сообщений
150
+ this.socket.send(message);
288
151
  }
289
152
  }
290
- else {
291
- // Если данные не строка (например, ArrayBuffer), отправляем как есть
292
- this.dispatchEvent('message', data);
293
- }
294
153
  }
295
- catch (e) {
296
- console.error('Ошибка при обработке сообщения WebSocket:', e);
297
- this.dispatchEvent('error', { message: `Ошибка при обработке сообщения: ${e instanceof Error ? e.message : 'Неизвестная ошибка'}` });
154
+ resolve();
155
+ this.dispatchEvent('open', {});
156
+ });
157
+ // Обработчик ошибок соединения
158
+ this.socket.on('connect_error', (error) => {
159
+ clearTimeout(this.connectionTimeoutTimer);
160
+ this.logger('error', 'Ошибка соединения WebSocket', {
161
+ message: error.message,
162
+ name: error.name,
163
+ stack: error.stack
164
+ });
165
+ this.dispatchEvent('error', error);
166
+ if (!this.connected) {
167
+ reject(new Error('Ошибка подключения WebSocket'));
298
168
  }
299
- };
169
+ });
300
170
  // Обработчик закрытия соединения
301
- this.webSocket.onclose = (event) => {
171
+ this.socket.on('disconnect', (reason) => {
302
172
  clearTimeout(this.connectionTimeoutTimer);
303
173
  this.connected = false;
304
- this.dispatchEvent('close', { code: event.code, reason: event.reason });
174
+ this.logger('info', `WebSocket соединение закрыто: ${reason}`);
175
+ // Формируем объект события для совместимости с WebSocket API
176
+ const closeEvent = {
177
+ code: this.getCloseCodeFromReason(reason),
178
+ reason: reason
179
+ };
180
+ this.dispatchEvent('close', closeEvent);
305
181
  // Если соединение было закрыто намеренно, не пытаемся переподключиться
306
182
  if (this.intentionallyClosed) {
307
183
  return;
308
184
  }
309
- // Если включено автоматическое переподключение, пытаемся переподключиться
310
- if (this.options.autoReconnect) {
311
- this.reconnect();
312
- }
313
- };
314
- // Обработчик ошибок
315
- this.webSocket.onerror = (error) => {
316
- this.dispatchEvent('error', error);
317
- // Если соединение не установлено и это первая попытка, отклоняем Promise
318
- if (!this.connected && this.retryCount === 0) {
319
- clearTimeout(this.connectionTimeoutTimer);
320
- reject(new Error('Ошибка подключения WebSocket'));
321
- }
322
- };
185
+ });
186
+ // Обработчик всех сообщений, используем 'message' для совместимости
187
+ this.socket.onAny((eventName, ...args) => {
188
+ // Отправляем в обработчик события по имени события
189
+ this.dispatchEvent(eventName, args.length === 1 ? args[0] : args);
190
+ // Также отправляем событие message для совместимости
191
+ this.dispatchEvent('message', {
192
+ event: eventName,
193
+ data: args.length === 1 ? args[0] : args
194
+ });
195
+ });
323
196
  }
324
197
  catch (error) {
198
+ clearTimeout(this.connectionTimeoutTimer);
199
+ this.logger('error', 'Ошибка при создании Socket.IO клиента', error);
325
200
  reject(error);
326
201
  }
327
202
  });
328
203
  }
329
204
  /**
330
- * Настраивает таймер для ping/pong сообщений Socket.IO
331
- */
332
- setupPingPongTimer() {
333
- // Если URL содержит параметры Socket.IO, настраиваем автоматический ping/pong
334
- if (this.url.includes('EIO=4') && this.url.includes('transport=websocket')) {
335
- // Типичный интервал ping для Socket.IO - 25 секунд
336
- const pingInterval = 25000;
337
- // Периодически отправляем ping
338
- const pingTimer = setInterval(() => {
339
- if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
340
- // Socket.IO ping - это просто строка "2"
341
- this.webSocket.send('2');
342
- }
343
- else {
344
- // Если соединение закрыто, останавливаем таймер
345
- clearInterval(pingTimer);
346
- }
347
- }, pingInterval);
348
- // Останавливаем таймер при закрытии соединения
349
- this.on('close', () => clearInterval(pingTimer));
350
- }
351
- }
352
- /**
353
- * Переподключается к WebSocket серверу
205
+ * Получает код закрытия WebSocket из строки причины Socket.IO
206
+ * @param {string} reason Причина закрытия Socket.IO
207
+ * @returns {number} Код закрытия WebSocket
354
208
  * @private
355
209
  */
356
- reconnect() {
357
- // Увеличиваем счетчик попыток
358
- this.retryCount++;
359
- // Если превышено максимальное количество попыток, не пытаемся переподключиться
360
- if (this.retryCount > (this.options.maxRetries || 5)) {
361
- this.dispatchEvent('maxRetries', { retries: this.retryCount });
362
- return;
210
+ getCloseCodeFromReason(reason) {
211
+ switch (reason) {
212
+ case 'io server disconnect':
213
+ return 1000; // Нормальное закрытие соединения сервером
214
+ case 'io client disconnect':
215
+ return 1000; // Нормальное закрытие соединения клиентом
216
+ case 'ping timeout':
217
+ return 1001; // Выход из соединения по таймауту
218
+ case 'transport close':
219
+ return 1006; // Аномальное закрытие соединения
220
+ case 'transport error':
221
+ return 1002; // Протокольная ошибка
222
+ default:
223
+ return 1000; // По умолчанию - нормальное закрытие
363
224
  }
364
- // Вычисляем задержку перед переподключением с экспоненциальным ростом
365
- const delay = Math.min((this.options.retryDelay || 1000) * Math.pow(2, this.retryCount - 1), this.options.maxRetryDelay || 30000);
366
- // Пытаемся переподключиться после задержки
367
- this.reconnectTimer = setTimeout(() => {
368
- this.dispatchEvent('reconnect', { attempt: this.retryCount });
369
- this.connect().catch(() => { });
370
- }, delay);
371
225
  }
372
226
  /**
373
- * Закрывает WebSocket соединение
227
+ * Закрывает соединение WebSocket
374
228
  * @param {number} [code=1000] Код закрытия
375
- * @param {string} [reason] Причина закрытия
229
+ * @param {string} [reason='Closed by client'] Причина закрытия
376
230
  */
377
- close(code = 1000, reason) {
231
+ close(code = 1000, reason = 'Closed by client') {
378
232
  this.intentionallyClosed = true;
379
- // Очищаем таймеры
380
233
  clearTimeout(this.reconnectTimer);
381
234
  clearTimeout(this.connectionTimeoutTimer);
382
- // Очищаем очередь сообщений
383
- this.messageQueue = [];
384
- // Закрываем соединение
385
- if (this.webSocket) {
386
- if (this.webSocket.readyState === WebSocket.OPEN) {
387
- this.webSocket.close(code, reason);
388
- }
389
- this.webSocket = null;
235
+ if (this.socket) {
236
+ this.logger('info', 'Закрытие WebSocket соединения', { code, reason });
237
+ // Отключаем Socket.IO клиент
238
+ this.socket.disconnect();
239
+ this.socket = null;
390
240
  }
391
241
  this.connected = false;
392
242
  }
393
243
  /**
394
- * Отправляет сообщение через WebSocket
395
- * @param {string | object} message Сообщение для отправки
244
+ * Отправляет сообщение в WebSocket
245
+ * @param {any} data Данные для отправки
396
246
  * @returns {boolean} Успешно ли отправлено сообщение
397
247
  */
398
- send(message) {
399
- // Если это объект с type='2' (Socket.IO EVENT), обрабатываем как Socket.IO сообщение
400
- if (typeof message === 'object' && message.type === '2') {
401
- const socketIOMessage = message;
402
- let packet;
403
- // Формат сообщения для Socket.IO протокола 4
404
- if (socketIOMessage.event && socketIOMessage.data) {
405
- // Format: 42namespace,[event,data]
406
- const namespace = socketIOMessage.nsp ? socketIOMessage.nsp : '';
407
- const namespaceStr = namespace && !namespace.startsWith('/') ? '/' + namespace : namespace;
408
- const eventData = [socketIOMessage.event];
409
- if (socketIOMessage.data !== undefined) {
410
- eventData.push(socketIOMessage.data);
411
- }
412
- // 4 - Engine.IO message type
413
- // 2 - Socket.IO EVENT type
414
- packet = `42${namespaceStr}${namespaceStr ? ',' : ''}${JSON.stringify(eventData)}`;
415
- }
416
- else {
417
- // Обратная совместимость со старым форматом
418
- packet = `${socketIOMessage.type}${socketIOMessage.nsp ? socketIOMessage.nsp : ''}${JSON.stringify(socketIOMessage.data)}`;
419
- }
420
- if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
421
- this.webSocket.send(packet);
248
+ send(data) {
249
+ try {
250
+ // Если соединение еще не установлено, добавляем сообщение в очередь
251
+ if (!this.socket || !this.socket.connected) {
252
+ this.messageQueue.push(data);
422
253
  return true;
423
254
  }
424
- else {
425
- // Если соединение не установлено, добавляем сообщение в очередь
426
- this.messageQueue.push(packet);
427
- if ((!this.webSocket || this.webSocket.readyState === WebSocket.CLOSED) && !this.intentionallyClosed) {
428
- this.connect().catch(() => { });
255
+ // Обработка разных типов сообщений для совместимости
256
+ if (typeof data === 'object') {
257
+ if (data.event) {
258
+ // Формат { event: 'event_name', data: {} }
259
+ this.socket.emit(data.event, data.data);
260
+ }
261
+ else if (data.type && data.type === '2' && data.data && Array.isArray(data.data)) {
262
+ // Socket.IO тип пакета '2' - событие с данными
263
+ // Формат { type: '2', nsp: '/namespace', data: ['event_name', {}] }
264
+ const eventName = data.data[0];
265
+ const eventData = data.data.length > 1 ? data.data[1] : null;
266
+ this.socket.emit(eventName, eventData);
267
+ }
268
+ else {
269
+ // Обычные объекты отправляем через 'message'
270
+ this.socket.emit('message', data);
429
271
  }
430
- return false;
431
272
  }
432
- }
433
- // Стандартная отправка
434
- const data = typeof message === 'string' ? message : JSON.stringify(message);
435
- if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
436
- this.webSocket.send(data);
273
+ else {
274
+ // Строки, бинарные данные и т.д.
275
+ this.socket.send(data);
276
+ }
437
277
  return true;
438
278
  }
439
- else {
440
- // Если соединение не установлено, добавляем сообщение в очередь
441
- this.messageQueue.push(data);
442
- // Если соединение не установлено и не закрыто намеренно, пытаемся подключиться
443
- if ((!this.webSocket || this.webSocket.readyState === WebSocket.CLOSED) && !this.intentionallyClosed) {
444
- this.connect().catch(() => { });
445
- }
279
+ catch (error) {
280
+ this.logger('error', 'Ошибка при отправке сообщения', error);
446
281
  return false;
447
282
  }
448
283
  }
449
284
  /**
450
- * Проверяет, установлено ли соединение
451
- * @returns {boolean} Установлено ли соединение
285
+ * Отправляет событие с данными и ожидает ответа с помощью Promise
286
+ * @param {string} event Название события
287
+ * @param {any} data Данные события
288
+ * @param {number} [timeout=5000] Таймаут ожидания ответа в мс
289
+ * @returns {Promise<any>} Promise с ответом
452
290
  */
453
- isConnected() {
454
- return this.webSocket !== null && this.webSocket.readyState === WebSocket.OPEN;
291
+ emitWithAck(event, data, timeout = 5000) {
292
+ return new Promise((resolve, reject) => {
293
+ if (!this.socket || !this.socket.connected) {
294
+ reject(new Error('WebSocket не подключен'));
295
+ return;
296
+ }
297
+ try {
298
+ // Используем встроенный механизм acknowledgements в Socket.IO
299
+ this.socket.timeout(timeout).emit(event, data, (err, response) => {
300
+ if (err) {
301
+ reject(err);
302
+ }
303
+ else {
304
+ resolve(response);
305
+ }
306
+ });
307
+ }
308
+ catch (error) {
309
+ reject(error);
310
+ }
311
+ });
455
312
  }
456
313
  /**
457
314
  * Добавляет обработчик события
@@ -463,6 +320,12 @@ export class WebSocketClient {
463
320
  this.eventHandlers[eventType] = [];
464
321
  }
465
322
  this.eventHandlers[eventType].push(handler);
323
+ // Если соединение уже установлено, добавляем обработчик для Socket.IO
324
+ if (this.socket && this.socket.connected && eventType !== 'open' && eventType !== 'close') {
325
+ // Не добавляем обработчики для 'open' и 'close', так как они обрабатываются
326
+ // через 'connect' и 'disconnect' в методе connect()
327
+ this.socket.on(eventType, handler);
328
+ }
466
329
  }
467
330
  /**
468
331
  * Удаляет обработчик события
@@ -473,23 +336,31 @@ export class WebSocketClient {
473
336
  if (!this.eventHandlers[eventType]) {
474
337
  return;
475
338
  }
476
- if (!handler) {
477
- // Если обработчик не указан, удаляем все обработчики для данного события
478
- delete this.eventHandlers[eventType];
339
+ if (handler) {
340
+ // Удаляем конкретный обработчик
341
+ const index = this.eventHandlers[eventType].indexOf(handler);
342
+ if (index !== -1) {
343
+ this.eventHandlers[eventType].splice(index, 1);
344
+ }
345
+ // Также удаляем обработчик из Socket.IO, если соединение установлено
346
+ if (this.socket && this.socket.connected) {
347
+ this.socket.off(eventType, handler);
348
+ }
479
349
  }
480
350
  else {
481
- // Если обработчик указан, удаляем только его
482
- this.eventHandlers[eventType] = this.eventHandlers[eventType].filter(h => h !== handler);
483
- // Если обработчиков больше нет, удаляем массив
484
- if (this.eventHandlers[eventType].length === 0) {
485
- delete this.eventHandlers[eventType];
351
+ // Удаляем все обработчики для данного типа события
352
+ delete this.eventHandlers[eventType];
353
+ // Также удаляем все обработчики из Socket.IO, если соединение установлено
354
+ if (this.socket && this.socket.connected) {
355
+ this.socket.off(eventType);
486
356
  }
487
357
  }
488
358
  }
489
359
  /**
490
- * Вызывает обработчики для указанного события
360
+ * Отправляет событие в обработчики
491
361
  * @param {string} eventType Тип события
492
362
  * @param {any} data Данные события
363
+ * @private
493
364
  */
494
365
  dispatchEvent(eventType, data) {
495
366
  if (!this.eventHandlers[eventType]) {
@@ -499,250 +370,54 @@ export class WebSocketClient {
499
370
  try {
500
371
  handler(data);
501
372
  }
502
- catch (e) {
503
- console.error(`Ошибка в обработчике события ${eventType}:`, e);
504
- }
505
- }
506
- }
507
- /**
508
- * Обработка полученного сообщения от сервера
509
- * @param message Сообщение от сервера
510
- */
511
- handleMessage(message) {
512
- try {
513
- if (!message) {
514
- this.dispatchEvent('error', { message: 'Получено пустое сообщение' });
515
- return;
516
- }
517
- // Проверяем, является ли сообщение пингом Socket.IO (2)
518
- if (message === '2') {
519
- // Отправляем понг (3)
520
- this.send('3');
521
- this.dispatchEvent('debug', 'Получен ping, отправлен pong');
522
- return;
523
- }
524
- // Если это сообщение Socket.IO с данными
525
- if (message.startsWith('42')) {
526
- // Формат: 42[event,data]
527
- const eventDataStr = message.substring(2);
528
- try {
529
- const [event, data] = JSON.parse(eventDataStr);
530
- // Проверяем, есть ли это ack-ответ для callback
531
- if (event.endsWith('_ack') || event.endsWith('_response') ||
532
- event.endsWith('_success') || event.endsWith('_error')) {
533
- const baseEvent = event.split('_')[0]; // Получаем базовое имя события
534
- // Проверяем, есть ли у нас отложенный callback для этого события
535
- const callbackEntries = Array.from(this._pendingCallbacks.entries())
536
- .filter(([key]) => key.startsWith(`${baseEvent}_ack_`));
537
- if (callbackEntries.length > 0) {
538
- const [callbackId, callback] = callbackEntries[0];
539
- this._pendingCallbacks.delete(callbackId);
540
- // Вызываем callback с полученными данными
541
- try {
542
- callback(data);
543
- }
544
- catch (callbackError) {
545
- this.logger('error', `Ошибка при вызове callback для ${baseEvent}: ${callbackError instanceof Error ? callbackError.message : String(callbackError)}`);
546
- }
547
- // Не продолжаем обработку, так как это ack-сообщение уже обработано
548
- return;
549
- }
550
- }
551
- // Вызываем соответствующий обработчик события
552
- this.dispatchEvent(event, data);
553
- // Добавляем специальную обработку для событий стриминга
554
- if (event === 'text_delta' && data.delta?.text) {
555
- this.dispatchEvent('streaming_delta', { type: 'text', content: data.delta.text });
556
- }
557
- else if (event === 'thinking_delta' && data.delta?.thinking) {
558
- this.dispatchEvent('streaming_delta', { type: 'thinking', content: data.delta.thinking });
559
- }
560
- else if (event === 'message_stop') {
561
- this.dispatchEvent('streaming_complete', {});
562
- }
563
- }
564
- catch (error) {
565
- const errorMessage = error instanceof Error ? error.message : 'Неизвестная ошибка';
566
- this.dispatchEvent('error', { message: `Ошибка разбора данных события: ${errorMessage}` });
567
- }
568
- return;
569
- }
570
- // Пытаемся преобразовать сообщение в JSON
571
- try {
572
- const parsedMessage = JSON.parse(message);
573
- // Если это объект с типом event и данными, это может быть событие
574
- if (parsedMessage.event && parsedMessage.data) {
575
- const { event, data } = parsedMessage;
576
- this.dispatchEvent(event, data);
577
- }
578
- else {
579
- this.dispatchEvent('message', parsedMessage);
580
- }
581
- }
582
373
  catch (error) {
583
- // Если не удалось преобразовать в JSON, обрабатываем как текстовое сообщение
584
- this.dispatchEvent('message', message);
374
+ this.logger('error', `Ошибка в обработчике события '${eventType}'`, error);
585
375
  }
586
376
  }
587
- catch (error) {
588
- const errorMessage = error instanceof Error ? error.message : 'Неизвестная ошибка';
589
- this.dispatchEvent('error', { message: `Ошибка при обработке сообщения: ${errorMessage}` });
590
- }
591
377
  }
592
378
  /**
593
- * Отправляет событие Socket.IO через WebSocket соединение
594
- * @param {string} event Имя события
595
- * @param {any} data Данные события
596
- * @param {(response: any) => void} [callback] Функция обратного вызова для получения ответа
597
- * @param {string} [namespace=''] Namespace для Socket.IO
598
- * @returns {boolean} Успешно ли отправлено сообщение
379
+ * Возвращает текущий статус соединения
380
+ * @returns {boolean} Подключен ли клиент
599
381
  */
600
- sendSocketIOEvent(event, data, callback, namespace = '') {
601
- // Проверяем наличие callback-функции
602
- const hasCallback = typeof callback === 'function';
603
- this.logger('debug', `Отправка Socket.IO события ${event}`, {
604
- hasData: !!data,
605
- namespace,
606
- hasCallback
607
- });
608
- // Формируем объект для отправки через метод send в стандартном формате Socket.IO
609
- if (hasCallback) {
610
- // Socket.IO ожидает callback как последний аргумент emit, поэтому
611
- // мы должны отправить особое сообщение, указывающее, что нужно использовать callback
612
- // Это важно: не добавляйте callback в data!
613
- try {
614
- // Для Socket.IO клиента (веб-браузер)
615
- if (this.isBrowser && this.webSocket) {
616
- // @ts-ignore - вызываем нативный метод emit у Socket.IO, если он доступен
617
- if (this.webSocket.emit) {
618
- const namespacedEvent = namespace ? `${namespace}#${event}` : event;
619
- // @ts-ignore
620
- return this.webSocket.emit(namespacedEvent, data, callback);
621
- }
622
- }
623
- // Формат данных для Socket.IO: [event, data, ack] в формате пакета типа 2 (EVENT)
624
- const socketIOPacket = {
625
- type: '2', // Socket.IO EVENT
626
- event,
627
- data,
628
- namespace,
629
- useCallback: true // Специальный флаг для внутренней обработки
630
- };
631
- // Сохраняем callback для последующего использования
632
- const callbackId = `${event}_ack_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
633
- this._pendingCallbacks.set(callbackId, callback);
634
- // Устанавливаем таймаут для автоматического удаления callback
635
- setTimeout(() => {
636
- if (this._pendingCallbacks.has(callbackId)) {
637
- const cb = this._pendingCallbacks.get(callbackId);
638
- this._pendingCallbacks.delete(callbackId);
639
- if (typeof cb === 'function') {
640
- cb({
641
- success: false,
642
- error: `Таймаут ожидания ответа на событие ${event}`,
643
- timeout: true
644
- });
645
- }
646
- }
647
- }, 10000); // 10-секундный таймаут
648
- // Отправляем объект с данными и callbackId
649
- return this.send({
650
- ...socketIOPacket,
651
- callbackId
652
- });
653
- }
654
- catch (error) {
655
- this.logger('error', `Ошибка при отправке Socket.IO события ${event} с callback: ${error instanceof Error ? error.message : String(error)}`);
656
- return false;
657
- }
658
- }
659
- else {
660
- // Стандартная отправка без callback
661
- const socketIOMessage = {
662
- type: '2',
663
- event,
664
- data,
665
- nsp: namespace
666
- };
667
- return this.send(socketIOMessage);
668
- }
382
+ isConnected() {
383
+ return this.socket !== null && this.socket.connected;
669
384
  }
670
385
  /**
671
- * Обработка подключения к пространству имен Socket.IO
386
+ * Выполняет принудительное переподключение
387
+ * @returns {Promise<void>} Promise без результата
672
388
  */
673
- handleNamespaceConnection() {
674
- if (!this.namespace) {
675
- return;
676
- }
677
- // Отправляем запрос на подключение к namespace
678
- this.logger('info', `Подключение к пространству имен ${this.namespace}`);
679
- const connectMessage = `40${this.namespace}`;
680
- if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
681
- this.webSocket.send(connectMessage);
682
- }
683
- else {
684
- this.messageQueue.push(connectMessage);
389
+ async reconnect() {
390
+ // Если соединение уже установлено, сначала закрываем его
391
+ if (this.socket && this.socket.connected) {
392
+ this.close();
685
393
  }
394
+ // Сбрасываем флаг намеренного закрытия для возможности переподключения
395
+ this.intentionallyClosed = false;
396
+ // Устанавливаем новое соединение
397
+ return this.connect();
686
398
  }
687
399
  /**
688
- * Отправка аутентификационного сообщения
400
+ * Отправляет событие (алиас для более удобного использования)
401
+ * @param {string} eventName Название события
402
+ * @param {any} data Данные события
403
+ * @returns {boolean} Успешно ли отправлено событие
689
404
  */
690
- authenticate() {
691
- if (!this.options.apiKey) {
692
- this.logger('warn', 'API ключ не предоставлен, аутентификация пропущена');
693
- return;
694
- }
695
- const apiKeySafe = this.options.apiKey.length > 8
696
- ? `${this.options.apiKey.substring(0, 4)}...${this.options.apiKey.substring(this.options.apiKey.length - 4)}`
697
- : '[короткий ключ]';
698
- this.logger('info', 'Отправка аутентификационного сообщения', {
699
- namespace: this.namespace,
700
- apiKey: apiKeySafe,
701
- authenticated: this.authenticated,
702
- socketState: this.webSocket ? this.webSocket.readyState : 'нет соединения'
703
- });
704
- // Отправляем событие authenticate с API ключом
705
- this.sendSocketIOEvent('authenticate', { apiKey: this.options.apiKey }, undefined, this.namespace);
405
+ emit(eventName, data) {
406
+ return this.send({ event: eventName, data });
706
407
  }
707
408
  /**
708
- * Обработка открытия соединения Socket.IO
709
- * @param data Данные открытия соединения
409
+ * Возвращает ID сокета, если соединение установлено
410
+ * @returns {string|null} ID сокета или null, если соединение не установлено
710
411
  */
711
- handleSocketIOOpen(data) {
712
- this.logger('info', 'Socket.IO соединение открыто', {
713
- pingInterval: data.pingInterval,
714
- pingTimeout: data.pingTimeout,
715
- sid: data.sid
716
- });
717
- // Если указано пространство имен, подключаемся к нему
718
- if (this.namespace) {
719
- this.handleNamespaceConnection();
720
- }
721
- // Добавляем обработчик успешного подключения к пространству имен
722
- this.on('socket.io_event', (eventData) => {
723
- // Если это событие connect и мы еще не аутентифицированы
724
- if (!this.authenticated && eventData.event === 'connect') {
725
- this.logger('info', `Подключено к пространству имен ${eventData.namespace || '/'}`, {
726
- namespace: eventData.namespace || '/',
727
- event: eventData.event,
728
- data: eventData.data
729
- });
730
- // Аутентифицируем соединение
731
- this.authenticate();
732
- this.authenticated = true;
733
- }
734
- });
735
- // Сохраняем ID сокета, если он пришел
736
- if (data && data.sid) {
737
- this.socketId = data.sid;
738
- }
412
+ getSocketId() {
413
+ return this.socket?.id || null;
739
414
  }
740
415
  /**
741
- * Получает ID текущего сокета
742
- * @returns {string|null} ID сокета или null, если соединение не установлено
416
+ * Устанавливает функцию логирования
417
+ * @param {Function} loggerFn Функция для логирования
743
418
  */
744
- getSocketId() {
745
- return this.socketId;
419
+ setLogger(loggerFn) {
420
+ this.logger = loggerFn;
746
421
  }
747
422
  /**
748
423
  * Регистрирует обработчик события, который будет вызван один раз и удален
@@ -751,6 +426,11 @@ export class WebSocketClient {
751
426
  * @returns {void}
752
427
  */
753
428
  once(event, handler) {
429
+ // Если есть нативная реализация в Socket.IO, используем её
430
+ if (this.socket && this.socket.connected) {
431
+ this.socket.once(event, handler);
432
+ return;
433
+ }
754
434
  // Создаем обертку, которая удалит обработчик после первого вызова
755
435
  const wrapperHandler = (data) => {
756
436
  // Удаляем обработчик
@@ -761,5 +441,45 @@ export class WebSocketClient {
761
441
  // Регистрируем обертку
762
442
  this.on(event, wrapperHandler);
763
443
  }
444
+ /**
445
+ * Отправляет событие Socket.IO через WebSocket соединение
446
+ * @param {string} event Имя события
447
+ * @param {any} data Данные события
448
+ * @param {(response: any) => void} [callback] Функция обратного вызова для получения ответа
449
+ * @param {string} [namespace=''] Namespace для Socket.IO
450
+ * @returns {boolean} Успешно ли отправлено сообщение
451
+ */
452
+ sendSocketIOEvent(event, data, callback, namespace = '') {
453
+ // Если нет соединения, сразу возвращаем false
454
+ if (!this.socket || !this.socket.connected) {
455
+ this.logger('error', 'Нельзя отправить событие: WebSocket не подключен');
456
+ return false;
457
+ }
458
+ try {
459
+ // Проверяем, нужно ли использовать другой namespace
460
+ let targetSocket = this.socket;
461
+ // Если указан другой namespace, используем его
462
+ if (namespace && namespace !== this.namespace) {
463
+ const nsSocket = io(this.url + namespace, {
464
+ forceNew: false,
465
+ auth: { token: this.options.apiKey }
466
+ });
467
+ targetSocket = nsSocket;
468
+ }
469
+ // Отправляем событие с callback, если он указан
470
+ if (callback) {
471
+ targetSocket.emit(event, data, callback);
472
+ }
473
+ else {
474
+ targetSocket.emit(event, data);
475
+ }
476
+ this.logger('debug', `Отправлено Socket.IO событие ${event}`, { hasData: !!data, namespace });
477
+ return true;
478
+ }
479
+ catch (error) {
480
+ this.logger('error', `Ошибка при отправке Socket.IO события ${event}`, error);
481
+ return false;
482
+ }
483
+ }
764
484
  }
765
485
  //# sourceMappingURL=websocket-client.js.map