solver-sdk 10.4.7 → 10.4.9

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.
@@ -10,9 +10,31 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
10
10
  if (k2 === undefined) k2 = k;
11
11
  o[k2] = m[k];
12
12
  }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
13
18
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
19
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
20
  };
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
16
38
  Object.defineProperty(exports, "__esModule", { value: true });
17
39
  exports.ChatApi = void 0;
18
40
  const stream_utils_1 = require("./stream-utils");
@@ -25,6 +47,103 @@ function generateId(length = 10) {
25
47
  // Экспортируем все типы и интерфейсы для внешнего использования
26
48
  __exportStar(require("./models"), exports);
27
49
  __exportStar(require("./interfaces"), exports);
50
+ /**
51
+ * 🔧 WorkAI: Internal Persistent Connection Implementation
52
+ */
53
+ class PersistentSSEConnectionImpl {
54
+ constructor(id, sessionId, closeCallback) {
55
+ this.isConnected = false;
56
+ this.lastPing = 0;
57
+ this.createdAt = Date.now();
58
+ this.eventHandlers = new Map();
59
+ this.reconnectAttempts = 0;
60
+ this.maxReconnectAttempts = 3;
61
+ this.id = id;
62
+ this.sessionId = sessionId;
63
+ this.closeCallback = closeCallback;
64
+ this.readyPromise = new Promise((resolve, reject) => {
65
+ this.resolveReady = resolve;
66
+ this.rejectReady = reject;
67
+ });
68
+ }
69
+ async waitForReady() {
70
+ return this.readyPromise;
71
+ }
72
+ async close(reason = 'client_request') {
73
+ if (!this.isConnected) {
74
+ return;
75
+ }
76
+ this.isConnected = false;
77
+ this.emit('close', { reason });
78
+ if (this.closeCallback) {
79
+ await this.closeCallback(reason);
80
+ }
81
+ }
82
+ on(event, callback) {
83
+ if (!this.eventHandlers.has(event)) {
84
+ this.eventHandlers.set(event, new Set());
85
+ }
86
+ this.eventHandlers.get(event).add(callback);
87
+ }
88
+ off(event, callback) {
89
+ const handlers = this.eventHandlers.get(event);
90
+ if (handlers) {
91
+ handlers.delete(callback);
92
+ }
93
+ }
94
+ // Internal methods
95
+ markReady() {
96
+ this.isConnected = true;
97
+ this.reconnectAttempts = 0; // Reset on successful connection
98
+ this.resolveReady();
99
+ }
100
+ markFailed(error) {
101
+ this.isConnected = false;
102
+ this.rejectReady(error);
103
+ // Auto-reconnect with exponential backoff
104
+ this.attemptReconnect();
105
+ }
106
+ updatePing(timestamp) {
107
+ this.lastPing = timestamp;
108
+ this.emit('ping', { timestamp });
109
+ }
110
+ setReconnectCallback(callback) {
111
+ this.reconnectCallback = callback;
112
+ }
113
+ async attemptReconnect() {
114
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
115
+ console.error(`❌ Max reconnect attempts (${this.maxReconnectAttempts}) reached`);
116
+ return;
117
+ }
118
+ this.reconnectAttempts++;
119
+ const backoffDelay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 4000);
120
+ console.log(`🔄 Reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} after ${backoffDelay}ms`);
121
+ this.emit('reconnecting', { attempt: this.reconnectAttempts });
122
+ await new Promise(resolve => setTimeout(resolve, backoffDelay));
123
+ if (this.reconnectCallback) {
124
+ try {
125
+ await this.reconnectCallback();
126
+ }
127
+ catch (error) {
128
+ console.error(`❌ Reconnect failed:`, error);
129
+ // Will retry again on next error
130
+ }
131
+ }
132
+ }
133
+ emit(event, data) {
134
+ const handlers = this.eventHandlers.get(event);
135
+ if (handlers) {
136
+ handlers.forEach(handler => {
137
+ try {
138
+ handler(data);
139
+ }
140
+ catch (error) {
141
+ console.error(`Error in event handler for ${event}:`, error);
142
+ }
143
+ });
144
+ }
145
+ }
146
+ }
28
147
  /**
29
148
  * API для работы с чатом с поддержкой Anthropic Extended Thinking
30
149
  */
@@ -82,10 +201,62 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
82
201
  if (debugLevel !== 'silent') {
83
202
  console.error(`❌ ${message}`);
84
203
  }
204
+ },
205
+ debug: (message) => {
206
+ const debugLevel = this.options.debug;
207
+ if (debugLevel === 'verbose' || debugLevel === 'debug') {
208
+ console.log(`🔍 ${message}`);
209
+ }
85
210
  }
86
211
  };
87
212
  this.options = options;
88
213
  }
214
+ /**
215
+ * 🔧 WorkAI: Выполняет streaming GET запрос используя нативный fetch
216
+ * Используется для persistent SSE connections, где axios не работает
217
+ *
218
+ * @param url - Полный URL для запроса
219
+ * @param headers - HTTP заголовки
220
+ * @returns Promise<Response> с ReadableStream body
221
+ *
222
+ * Best practice from undici docs: Node 18+ has global fetch
223
+ */
224
+ async fetchStream(url, headers) {
225
+ console.log(`[SDK] 🔌 fetchStream: Attempting fetch to ${url}`);
226
+ console.log(`[SDK] 🔌 fetchStream: globalThis.fetch available: ${typeof globalThis.fetch !== 'undefined'}`);
227
+ // Node 18+ has native fetch support
228
+ if (typeof globalThis.fetch !== 'undefined') {
229
+ console.log(`[SDK] 🔌 fetchStream: Using globalThis.fetch`);
230
+ try {
231
+ const response = await globalThis.fetch(url, {
232
+ method: 'GET',
233
+ headers
234
+ });
235
+ console.log(`[SDK] ✅ fetchStream: Response received, status: ${response.status}, hasBody: ${!!response.body}`);
236
+ return response;
237
+ }
238
+ catch (error) {
239
+ console.error(`[SDK] ❌ fetchStream: globalThis.fetch failed:`, error.message);
240
+ throw error;
241
+ }
242
+ }
243
+ // Fallback для Node 16 через dynamic import
244
+ console.log(`[SDK] 🔌 fetchStream: globalThis.fetch not available, trying node-fetch`);
245
+ try {
246
+ const { default: nodeFetch } = await Promise.resolve().then(() => __importStar(require('node-fetch')));
247
+ console.log(`[SDK] ✅ fetchStream: node-fetch imported`);
248
+ const response = await nodeFetch(url, {
249
+ method: 'GET',
250
+ headers
251
+ });
252
+ console.log(`[SDK] ✅ fetchStream: node-fetch response received, status: ${response.status}`);
253
+ return response;
254
+ }
255
+ catch (error) {
256
+ console.error(`[SDK] ❌ fetchStream: node-fetch failed:`, error.message);
257
+ throw new Error(`Fetch API unavailable: ${error.message}. Please use Node.js 18+ or install node-fetch`);
258
+ }
259
+ }
89
260
  /**
90
261
  * Отправляет сообщение в чат и получает ответ от модели
91
262
  * @param {ChatMessage[]} messages Массив сообщений для отправки
@@ -367,6 +538,35 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
367
538
  const messages = [{ role: 'user', content: prompt }];
368
539
  yield* this.streamChat(messages, options);
369
540
  }
541
+ /**
542
+ * 🔧 WorkAI: Send continuation via persistent SSE connection
543
+ * Uses existing connection instead of creating new one
544
+ */
545
+ async *sendContinuationViaPersistent(connection, messages, options) {
546
+ if (!connection.isConnected) {
547
+ throw new Error('Persistent SSE connection is not active');
548
+ }
549
+ this.logger.info(`🔌 [CONTINUATION_VIA_PERSISTENT] Using connection: ${connection.id}`);
550
+ try {
551
+ const params = this.buildRequestParams(messages, {
552
+ ...options,
553
+ stream: true,
554
+ clientRequestId: connection.id,
555
+ });
556
+ params.beta = 'interleaved-thinking-2025-05-14';
557
+ const response = await this.httpClient.post('/api/v1/chat/continue', params);
558
+ this.logger.info(`✅ [CONTINUATION_VIA_PERSISTENT] Request sent, waiting for events via persistent SSE`);
559
+ // Events will arrive via the persistent SSE connection
560
+ // We need to yield them from connection, but connection doesn't expose events yet
561
+ // For now, return empty - будет реализовано когда connection будет слушать continuation события
562
+ // TODO: Implement event listening on PersistentSSEConnection
563
+ this.logger.warn('⚠️ [CONTINUATION_VIA_PERSISTENT] Event listening not yet implemented');
564
+ }
565
+ catch (error) {
566
+ this.logger.error(`❌ [CONTINUATION_VIA_PERSISTENT] Error: ${error.message}`);
567
+ throw error;
568
+ }
569
+ }
370
570
  /**
371
571
  * Отправляет continuation запрос для interleaved thinking
372
572
  */
@@ -794,97 +994,251 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
794
994
  if (!clientRequestId) {
795
995
  throw new Error('clientRequestId is required for subscribeToResponse');
796
996
  }
997
+ // 🔧 WorkAI: If persistent connection provided, use it instead
998
+ if (options.persistentConnection && options.persistentConnection.isConnected) {
999
+ this.logger.info(`🔌 [PRE_SUBSCRIBE] Using persistent connection: ${clientRequestId}`);
1000
+ // TODO: Yield events from persistent connection
1001
+ // For now, fallback to direct subscription
1002
+ this.logger.warn(`⚠️ [PRE_SUBSCRIBE] Persistent connection event streaming not yet implemented`);
1003
+ }
797
1004
  const endpoint = `/api/v1/chat/subscribe?clientRequestId=${encodeURIComponent(clientRequestId)}`;
798
1005
  this.logger.info(`🔌 [PRE_SUBSCRIBE] Connecting to SSE: ${clientRequestId}`);
799
- try {
800
- // Используем request с responseType='stream' для GET запроса с SSE
801
- const response = await this.httpClient.request({
802
- method: 'GET',
803
- url: endpoint,
804
- headers: {
805
- 'X-Project-ID': options.projectId,
806
- 'X-Client-Ready': 'true',
807
- },
808
- responseType: 'stream',
809
- });
810
- if (!response.body) {
811
- throw new Error('No response body from subscribe endpoint');
1006
+ // 🔧 WorkAI: Retry logic for "No response body" errors
1007
+ let lastError = null;
1008
+ const maxRetries = 1;
1009
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1010
+ if (attempt > 0) {
1011
+ this.logger.warn(`🔄 [RETRY_SUBSCRIBE] Attempt ${attempt + 1}/${maxRetries + 1} after 500ms`);
1012
+ await new Promise(resolve => setTimeout(resolve, 500));
812
1013
  }
813
- const reader = response.body.getReader();
814
- const decoder = new TextDecoder();
815
- let subscribed = false;
816
- // 🔧 FIX: Буфер для накопления неполных SSE строк
817
- let lineBuffer = '';
818
1014
  try {
819
- while (true) {
820
- const { done, value } = await reader.read();
821
- if (done) {
822
- this.logger.info(`🔌 [PRE_SUBSCRIBE] SSE stream ended: ${clientRequestId}`);
823
- break;
824
- }
825
- const chunk = decoder.decode(value, { stream: true });
826
- // 🔧 FIX: Добавляем к буферу, а не парсим сразу
827
- lineBuffer += chunk;
828
- // Ищем полные строки (заканчиваются на \n)
829
- const lines = lineBuffer.split('\n');
830
- // 🔧 FIX: Последняя "строка" может быть неполной - сохраняем в буфер
831
- lineBuffer = lines.pop() || '';
832
- for (const line of lines) {
833
- if (!line.trim() || !line.startsWith('data: '))
834
- continue;
835
- const data = line.slice(6).trim();
836
- if (!data)
837
- continue;
838
- try {
839
- const event = JSON.parse(data);
840
- // Callback when subscribed
841
- if (event.type === 'subscribed' && !subscribed) {
842
- subscribed = true;
843
- this.logger.info(`🔌 [PRE_SUBSCRIBE] Subscribed successfully: ${clientRequestId}`);
844
- if (options.onSubscribed) {
845
- options.onSubscribed();
846
- }
847
- }
848
- // Skip heartbeat events
849
- if (event.type === 'heartbeat') {
1015
+ // Используем request с responseType='stream' для GET запроса с SSE
1016
+ const response = await this.httpClient.request({
1017
+ method: 'GET',
1018
+ url: endpoint,
1019
+ headers: {
1020
+ 'X-Project-ID': options.projectId,
1021
+ 'X-Client-Ready': 'true',
1022
+ },
1023
+ responseType: 'stream',
1024
+ });
1025
+ // 🔍 DEBUG: Log response details
1026
+ this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response status: ${response.status}`);
1027
+ this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response headers: ${JSON.stringify(response.headers)}`);
1028
+ this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response body present: ${!!response.body}`);
1029
+ if (!response.body) {
1030
+ this.logger.error(`❌ [PRE_SUBSCRIBE] No response body! Full response: ${JSON.stringify({
1031
+ status: response.status,
1032
+ statusText: response.statusText,
1033
+ headers: response.headers,
1034
+ bodyType: typeof response.body,
1035
+ })}`);
1036
+ throw new Error('No response body from subscribe endpoint');
1037
+ }
1038
+ const reader = response.body.getReader();
1039
+ const decoder = new TextDecoder();
1040
+ let subscribed = false;
1041
+ // 🔧 FIX: Буфер для накопления неполных SSE строк
1042
+ let lineBuffer = '';
1043
+ try {
1044
+ while (true) {
1045
+ const { done, value } = await reader.read();
1046
+ if (done) {
1047
+ this.logger.info(`🔌 [PRE_SUBSCRIBE] SSE stream ended: ${clientRequestId}`);
1048
+ break;
1049
+ }
1050
+ const chunk = decoder.decode(value, { stream: true });
1051
+ // 🔧 FIX: Добавляем к буферу, а не парсим сразу
1052
+ lineBuffer += chunk;
1053
+ // Ищем полные строки (заканчиваются на \n)
1054
+ const lines = lineBuffer.split('\n');
1055
+ // 🔧 FIX: Последняя "строка" может быть неполной - сохраняем в буфер
1056
+ lineBuffer = lines.pop() || '';
1057
+ for (const line of lines) {
1058
+ if (!line.trim() || !line.startsWith('data: '))
850
1059
  continue;
1060
+ const data = line.slice(6).trim();
1061
+ if (!data)
1062
+ continue;
1063
+ try {
1064
+ const event = JSON.parse(data);
1065
+ // Callback when subscribed
1066
+ if (event.type === 'subscribed' && !subscribed) {
1067
+ subscribed = true;
1068
+ this.logger.info(`🔌 [PRE_SUBSCRIBE] Subscribed successfully: ${clientRequestId}`);
1069
+ if (options.onSubscribed) {
1070
+ options.onSubscribed();
1071
+ }
1072
+ }
1073
+ // Skip heartbeat events
1074
+ if (event.type === 'heartbeat') {
1075
+ continue;
1076
+ }
1077
+ // Timeout event
1078
+ if (event.type === 'timeout') {
1079
+ this.logger.warn(`🔌 [PRE_SUBSCRIBE] Timeout: ${clientRequestId}`);
1080
+ break;
1081
+ }
1082
+ // Convert to ChatStreamChunk and yield
1083
+ const streamChunk = {
1084
+ type: event.type,
1085
+ ...event,
1086
+ };
1087
+ yield streamChunk;
1088
+ // End on message_stop
1089
+ if (event.type === 'message_stop') {
1090
+ this.logger.info(`🔌 [PRE_SUBSCRIBE] Message stop received: ${clientRequestId}`);
1091
+ break;
1092
+ }
851
1093
  }
852
- // Timeout event
853
- if (event.type === 'timeout') {
854
- this.logger.warn(`🔌 [PRE_SUBSCRIBE] Timeout: ${clientRequestId}`);
855
- break;
856
- }
857
- // Convert to ChatStreamChunk and yield
858
- const streamChunk = {
859
- type: event.type,
860
- ...event,
861
- };
862
- yield streamChunk;
863
- // End on message_stop
864
- if (event.type === 'message_stop') {
865
- this.logger.info(`🔌 [PRE_SUBSCRIBE] Message stop received: ${clientRequestId}`);
866
- break;
1094
+ catch (parseError) {
1095
+ this.logger.warn(`🔌 [PRE_SUBSCRIBE] Parse error: ${parseError}`);
867
1096
  }
868
1097
  }
869
- catch (parseError) {
870
- this.logger.warn(`🔌 [PRE_SUBSCRIBE] Parse error: ${parseError}`);
871
- }
872
1098
  }
873
1099
  }
1100
+ finally {
1101
+ reader.releaseLock();
1102
+ }
1103
+ // Success - break retry loop
1104
+ return;
874
1105
  }
875
- finally {
876
- reader.releaseLock();
1106
+ catch (error) {
1107
+ lastError = error;
1108
+ // 🔌 WorkAI: Graceful handling для stream abort
1109
+ if (error.name === 'StreamAbortedError' ||
1110
+ (error.message && error.message.includes('aborted'))) {
1111
+ this.logger.info(`🔄 [PRE_SUBSCRIBE] SSE stream aborted (likely replaced): ${clientRequestId}`);
1112
+ // НЕ выбрасываем ошибку - это нормальное завершение
1113
+ return;
1114
+ }
1115
+ // 🔄 WorkAI: Retry for "No response body" errors
1116
+ if (error.message && error.message.includes('No response body')) {
1117
+ if (attempt < maxRetries) {
1118
+ this.logger.warn(`⚠️ [PRE_SUBSCRIBE] No response body, will retry: ${clientRequestId}`);
1119
+ continue; // Try again
1120
+ }
1121
+ }
1122
+ this.logger.error(`🔌 [PRE_SUBSCRIBE] Error: ${error.message}`);
1123
+ throw error;
877
1124
  }
878
1125
  }
879
- catch (error) {
880
- // 🔌 WorkAI: Graceful handling для stream abort
881
- if (error.name === 'StreamAbortedError' ||
882
- (error.message && error.message.includes('aborted'))) {
883
- this.logger.info(`🔄 [PRE_SUBSCRIBE] SSE stream aborted (likely replaced): ${clientRequestId}`);
884
- // НЕ выбрасываем ошибку - это нормальное завершение
885
- return;
1126
+ // If we exhausted all retries
1127
+ if (lastError) {
1128
+ this.logger.error(`❌ [PRE_SUBSCRIBE] All ${maxRetries + 1} attempts failed: ${lastError.message}`);
1129
+ throw lastError;
1130
+ }
1131
+ }
1132
+ /**
1133
+ * 🔧 WorkAI: Open persistent SSE connection for chat session
1134
+ */
1135
+ async openPersistentSSE(sessionId, clientRequestId, options) {
1136
+ const endpoint = `/api/v1/chat/subscribe/persistent?sessionId=${encodeURIComponent(sessionId)}&clientRequestId=${encodeURIComponent(clientRequestId)}`;
1137
+ this.logger.info(`🔌 [PERSISTENT_SSE] Opening: ${clientRequestId}`);
1138
+ const connection = new PersistentSSEConnectionImpl(clientRequestId, sessionId, async (reason) => {
1139
+ try {
1140
+ await this.httpClient.post('/api/v1/chat/subscribe/close', { sessionId, reason });
1141
+ }
1142
+ catch (error) {
1143
+ this.logger.error(`❌ [PERSISTENT_SSE] Close error: ${error.message}`);
1144
+ }
1145
+ });
1146
+ try {
1147
+ // 🔧 WorkAI: Получаем auth token динамически для 401 retry support
1148
+ console.log(`[SDK] 🔌 openPersistentSSE: Getting auth token...`);
1149
+ let authToken = null;
1150
+ if (typeof this.httpClient.options?.getAuthToken === 'function') {
1151
+ try {
1152
+ authToken = await Promise.resolve(this.httpClient.options.getAuthToken());
1153
+ console.log(`[SDK] ✅ openPersistentSSE: Auth token obtained`);
1154
+ }
1155
+ catch (error) {
1156
+ console.log(`[SDK] ⚠️ openPersistentSSE: Failed to get auth token: ${error.message}`);
1157
+ this.logger.warn(`⚠️ [PERSISTENT_SSE] Failed to get auth token: ${error.message}`);
1158
+ }
886
1159
  }
887
- this.logger.error(`🔌 [PRE_SUBSCRIBE] Error: ${error.message}`);
1160
+ // Формируем headers с auth token
1161
+ const headers = {
1162
+ 'X-Project-ID': options.projectId,
1163
+ };
1164
+ if (authToken) {
1165
+ headers['Authorization'] = `Bearer ${authToken}`;
1166
+ }
1167
+ // Формируем полный URL (baseURL + endpoint)
1168
+ const baseURL = this.httpClient.getBaseURL?.() || 'http://localhost:3000';
1169
+ const fullUrl = `${baseURL}${endpoint}`;
1170
+ console.log(`[SDK] 🔌 openPersistentSSE: Calling fetchStream to ${fullUrl}`);
1171
+ // 🔧 WorkAI: Используем нативный fetch вместо axios для streaming
1172
+ // Best practice: fetch нативно возвращает Response с ReadableStream body
1173
+ const response = await this.fetchStream(fullUrl, headers);
1174
+ // Проверка статуса (401 retry будет добавлен отдельно если потребуется)
1175
+ console.log(`[SDK] 📡 openPersistentSSE: Response status: ${response.status}`);
1176
+ if (!response.ok) {
1177
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1178
+ }
1179
+ if (!response.body) {
1180
+ console.error(`[SDK] ❌ openPersistentSSE: No response body!`);
1181
+ throw new Error('No response body from persistent SSE');
1182
+ }
1183
+ console.log(`[SDK] ✅ openPersistentSSE: Response body available, starting SSE parsing...`);
1184
+ // 🔧 SSE parsing согласно best practices (ofetch + undici)
1185
+ const reader = response.body.getReader();
1186
+ const decoder = new TextDecoder();
1187
+ let buffer = '';
1188
+ // Асинхронно читаем stream
1189
+ (async () => {
1190
+ try {
1191
+ while (true) {
1192
+ const { done, value } = await reader.read();
1193
+ if (done)
1194
+ break;
1195
+ // Декодируем с stream: true для поддержки multi-byte символов
1196
+ buffer += decoder.decode(value, { stream: true });
1197
+ const lines = buffer.split('\n');
1198
+ // Оставляем неполную строку в buffer (best practice)
1199
+ buffer = lines.pop() || '';
1200
+ for (const line of lines) {
1201
+ if (line.startsWith('data: ')) {
1202
+ try {
1203
+ const event = JSON.parse(line.slice(6));
1204
+ if (event.type === 'persistent_ready') {
1205
+ connection.markReady();
1206
+ }
1207
+ else if (event.type === 'ping') {
1208
+ connection.updatePing(event.timestamp);
1209
+ }
1210
+ else if (event.type === 'connection_closed') {
1211
+ connection.isConnected = false;
1212
+ break;
1213
+ }
1214
+ }
1215
+ catch (e) {
1216
+ // Ignore JSON parse errors for non-JSON SSE events
1217
+ }
1218
+ }
1219
+ }
1220
+ }
1221
+ }
1222
+ catch (error) {
1223
+ // Ignore abort errors (normal when connection closes)
1224
+ if (!error.message?.includes('aborted')) {
1225
+ connection.emit('error', error);
1226
+ connection.markFailed(error);
1227
+ }
1228
+ }
1229
+ finally {
1230
+ reader.releaseLock();
1231
+ }
1232
+ })();
1233
+ // Ждем persistent_ready event с timeout
1234
+ await Promise.race([
1235
+ connection.waitForReady(),
1236
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Persistent SSE timeout after 10s')), 10000)),
1237
+ ]);
1238
+ return connection;
1239
+ }
1240
+ catch (error) {
1241
+ connection.markFailed(error);
888
1242
  throw error;
889
1243
  }
890
1244
  }