solver-sdk 1.6.3 → 1.6.5

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