@pushler/js 1.0.0
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.
- package/README.md +243 -0
- package/dist/pushler-ru.esm.js +1233 -0
- package/dist/pushler-ru.esm.js.map +1 -0
- package/dist/pushler-ru.js +1253 -0
- package/dist/pushler-ru.js.map +1 -0
- package/dist/pushler-ru.min.js +2 -0
- package/dist/pushler-ru.min.js.map +1 -0
- package/package.json +48 -0
- package/src/Channel.js +142 -0
- package/src/PushlerClient.js +605 -0
- package/src/PushlerServer.js +403 -0
- package/src/constants.js +51 -0
- package/src/index.js +24 -0
- package/src/utils.js +74 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import Channel from './Channel';
|
|
2
|
+
import { ChannelTypes, ConnectionStates, Events, ErrorCodes } from './constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Основной класс для подключения к Pushler.ru WebSocket серверу
|
|
6
|
+
*/
|
|
7
|
+
class PushlerClient {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.appKey = options.appKey;
|
|
10
|
+
this.wsUrl = options.wsUrl || 'ws://pushler.ru:8080/ws';
|
|
11
|
+
this.apiUrl = options.apiUrl;
|
|
12
|
+
this.authEndpoint = options.authEndpoint || '/pushler/auth'; // Путь для авторизации каналов
|
|
13
|
+
this.autoConnect = options.autoConnect !== false;
|
|
14
|
+
this.reconnectDelay = options.reconnectDelay || 1000;
|
|
15
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
|
16
|
+
|
|
17
|
+
this.connectionState = ConnectionStates.DISCONNECTED;
|
|
18
|
+
this.socket = null;
|
|
19
|
+
this.socketId = null;
|
|
20
|
+
this.channels = new Map(); // Хранит каналы по полному имени (с appKey)
|
|
21
|
+
this.channelAliases = new Map(); // Маппинг оригинальное имя -> полное имя
|
|
22
|
+
this.eventListeners = new Map();
|
|
23
|
+
this.reconnectAttempts = 0;
|
|
24
|
+
this.reconnectTimer = null;
|
|
25
|
+
this.authTimeout = null;
|
|
26
|
+
|
|
27
|
+
// Автоматическое подключение
|
|
28
|
+
if (this.autoConnect) {
|
|
29
|
+
this.connect();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Формирование полного имени канала с префиксом appKey
|
|
35
|
+
* Формат: {appKey}:{channelName}
|
|
36
|
+
* @param {string} channelName - Оригинальное имя канала
|
|
37
|
+
* @returns {string} Полное имя канала
|
|
38
|
+
*/
|
|
39
|
+
formatChannelName(channelName) {
|
|
40
|
+
// Если канал уже содержит appKey, возвращаем как есть
|
|
41
|
+
if (channelName.startsWith(this.appKey + ':')) {
|
|
42
|
+
return channelName;
|
|
43
|
+
}
|
|
44
|
+
return `${this.appKey}:${channelName}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Извлечение оригинального имени канала (без appKey)
|
|
49
|
+
* @param {string} fullChannelName - Полное имя канала
|
|
50
|
+
* @returns {string} Оригинальное имя канала
|
|
51
|
+
*/
|
|
52
|
+
extractChannelName(fullChannelName) {
|
|
53
|
+
const prefix = this.appKey + ':';
|
|
54
|
+
if (fullChannelName.startsWith(prefix)) {
|
|
55
|
+
return fullChannelName.substring(prefix.length);
|
|
56
|
+
}
|
|
57
|
+
return fullChannelName;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Подключение к WebSocket серверу
|
|
62
|
+
*/
|
|
63
|
+
connect() {
|
|
64
|
+
if (this.connectionState === ConnectionStates.CONNECTED ||
|
|
65
|
+
this.connectionState === ConnectionStates.CONNECTING) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.connectionState = ConnectionStates.CONNECTING;
|
|
70
|
+
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (!this.wsUrl) {
|
|
74
|
+
throw new Error('WebSocket URL is not configured');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`PushlerClient: Connecting to ${this.wsUrl}`);
|
|
78
|
+
this.socket = new WebSocket(this.wsUrl);
|
|
79
|
+
this.setupWebSocketHandlers();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('PushlerClient: Error creating WebSocket connection:', error);
|
|
82
|
+
this.handleConnectionError(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Настройка обработчиков WebSocket
|
|
88
|
+
*/
|
|
89
|
+
setupWebSocketHandlers() {
|
|
90
|
+
this.socket.onopen = () => {
|
|
91
|
+
// Не устанавливаем CONNECTED здесь - ждем pushler:connection_established
|
|
92
|
+
// Это предотвращает двойные события и проблемы с переподключением
|
|
93
|
+
this.connectionState = ConnectionStates.CONNECTING;
|
|
94
|
+
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
95
|
+
console.log('WebSocket connection opened, waiting for connection_established...');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
this.socket.onmessage = (event) => {
|
|
99
|
+
this.handleMessage(event);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.socket.onclose = (event) => {
|
|
103
|
+
this.connectionState = ConnectionStates.DISCONNECTED;
|
|
104
|
+
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
105
|
+
this.emit(Events.DISCONNECTED, event);
|
|
106
|
+
|
|
107
|
+
// Логируем причину закрытия для отладки
|
|
108
|
+
if (event.code !== 1000) { // 1000 = нормальное закрытие
|
|
109
|
+
console.warn('WebSocket closed unexpectedly:', {
|
|
110
|
+
code: event.code,
|
|
111
|
+
reason: event.reason || 'No reason provided',
|
|
112
|
+
wasClean: event.wasClean,
|
|
113
|
+
url: this.wsUrl
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Автоматическое переподключение
|
|
118
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
119
|
+
this.scheduleReconnect();
|
|
120
|
+
} else {
|
|
121
|
+
// Если превышено количество попыток, отправляем ошибку
|
|
122
|
+
this.emit(Events.ERROR, {
|
|
123
|
+
code: ErrorCodes.CONNECTION_FAILED,
|
|
124
|
+
message: `Failed to connect after ${this.maxReconnectAttempts} attempts. Please check if the WebSocket server is running at ${this.wsUrl}`,
|
|
125
|
+
url: this.wsUrl
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
this.socket.onerror = (error) => {
|
|
131
|
+
this.handleConnectionError(error);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Обработка входящих сообщений
|
|
137
|
+
*/
|
|
138
|
+
handleMessage(event) {
|
|
139
|
+
try {
|
|
140
|
+
const message = JSON.parse(event.data);
|
|
141
|
+
|
|
142
|
+
switch (message.event) {
|
|
143
|
+
case 'pushler:connection_established':
|
|
144
|
+
this.handleConnectionEstablished(message.data);
|
|
145
|
+
break;
|
|
146
|
+
case 'pushler:subscription_succeeded':
|
|
147
|
+
this.handleSubscriptionSucceeded(message);
|
|
148
|
+
break;
|
|
149
|
+
case 'pushler:auth_success':
|
|
150
|
+
this.handleAuthSuccess(message);
|
|
151
|
+
break;
|
|
152
|
+
case 'pushler:auth_error':
|
|
153
|
+
this.handleAuthError(message);
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
this.handleChannelMessage(message);
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('Error parsing WebSocket message:', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Обработка установления соединения
|
|
165
|
+
*/
|
|
166
|
+
handleConnectionEstablished(data) {
|
|
167
|
+
this.socketId = data.socket_id;
|
|
168
|
+
this.connectionState = ConnectionStates.CONNECTED;
|
|
169
|
+
this.reconnectAttempts = 0;
|
|
170
|
+
|
|
171
|
+
console.log('Connection established with socket ID:', this.socketId);
|
|
172
|
+
|
|
173
|
+
// Переподписываемся на все каналы после переподключения
|
|
174
|
+
this.resubscribeAllChannels();
|
|
175
|
+
|
|
176
|
+
this.emit(Events.CONNECTED, { socketId: this.socketId });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Переподписка на все каналы после переподключения
|
|
181
|
+
*/
|
|
182
|
+
resubscribeAllChannels() {
|
|
183
|
+
for (const [channelName, channel] of this.channels.entries()) {
|
|
184
|
+
// Сбрасываем флаг подписки
|
|
185
|
+
channel.subscribed = false;
|
|
186
|
+
// Выполняем подписку заново
|
|
187
|
+
this.performChannelSubscription(channel);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Обработка успешной подписки на канал
|
|
193
|
+
*/
|
|
194
|
+
handleSubscriptionSucceeded(message) {
|
|
195
|
+
const channel = message.channel;
|
|
196
|
+
const channelInstance = this.channels.get(channel);
|
|
197
|
+
|
|
198
|
+
if (channelInstance) {
|
|
199
|
+
channelInstance.subscribed = true;
|
|
200
|
+
channelInstance.emit(Events.CHANNEL_SUBSCRIBED, message.data || {});
|
|
201
|
+
console.log(`Channel ${channel} subscribed successfully`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Обработка успешной аутентификации
|
|
207
|
+
*/
|
|
208
|
+
handleAuthSuccess(message) {
|
|
209
|
+
const { channel } = message.data;
|
|
210
|
+
const channelInstance = this.channels.get(channel);
|
|
211
|
+
|
|
212
|
+
if (channelInstance) {
|
|
213
|
+
// На сервере уже происходит подписка после успешной авторизации,
|
|
214
|
+
// поэтому просто помечаем канал как подписанный
|
|
215
|
+
channelInstance.handleAuthSuccess(message.data);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.emit(Events.CHANNEL_AUTH_SUCCESS, message.data);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Обработка ошибки аутентификации
|
|
223
|
+
*/
|
|
224
|
+
handleAuthError(message) {
|
|
225
|
+
const { error } = message.data;
|
|
226
|
+
this.emit(Events.CHANNEL_AUTH_ERROR, error);
|
|
227
|
+
this.emit(Events.ERROR, {
|
|
228
|
+
code: ErrorCodes.AUTHENTICATION_FAILED,
|
|
229
|
+
message: error
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Обработка сообщений каналов
|
|
235
|
+
*/
|
|
236
|
+
handleChannelMessage(message) {
|
|
237
|
+
const { channel, event, data } = message;
|
|
238
|
+
const channelInstance = this.channels.get(channel);
|
|
239
|
+
|
|
240
|
+
if (channelInstance) {
|
|
241
|
+
channelInstance.handleMessage(event, data);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Для события возвращаем оригинальное имя канала (без appKey)
|
|
245
|
+
const originalChannelName = this.extractChannelName(channel);
|
|
246
|
+
this.emit(Events.MESSAGE_RECEIVED, { channel: originalChannelName, event, data });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Подписка на канал
|
|
251
|
+
* @param {string} channelName - Имя канала (без appKey, будет добавлен автоматически)
|
|
252
|
+
* @param {Object} options - Опции подписки
|
|
253
|
+
* @returns {Channel} Объект канала
|
|
254
|
+
*/
|
|
255
|
+
subscribe(channelName, options = {}) {
|
|
256
|
+
// Формируем полное имя канала с appKey
|
|
257
|
+
const fullChannelName = this.formatChannelName(channelName);
|
|
258
|
+
|
|
259
|
+
if (this.channels.has(fullChannelName)) {
|
|
260
|
+
return this.channels.get(fullChannelName);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Создаем канал с полным именем
|
|
264
|
+
const channel = new Channel(this, fullChannelName, options);
|
|
265
|
+
// Сохраняем оригинальное имя для удобства
|
|
266
|
+
channel.originalName = channelName;
|
|
267
|
+
|
|
268
|
+
this.channels.set(fullChannelName, channel);
|
|
269
|
+
this.channelAliases.set(channelName, fullChannelName);
|
|
270
|
+
|
|
271
|
+
// Если подключены, сразу подписываемся
|
|
272
|
+
if (this.connectionState === ConnectionStates.CONNECTED) {
|
|
273
|
+
this.performChannelSubscription(channel);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return channel;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Отписка от канала
|
|
281
|
+
* @param {string} channelName - Имя канала (можно использовать оригинальное или полное)
|
|
282
|
+
*/
|
|
283
|
+
unsubscribe(channelName) {
|
|
284
|
+
// Поддерживаем оба формата имени
|
|
285
|
+
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
286
|
+
|
|
287
|
+
const channel = this.channels.get(fullChannelName);
|
|
288
|
+
if (channel) {
|
|
289
|
+
channel.unsubscribe();
|
|
290
|
+
this.channels.delete(fullChannelName);
|
|
291
|
+
this.channelAliases.delete(channel.originalName || channelName);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Выполнение подписки на канал
|
|
297
|
+
*/
|
|
298
|
+
performChannelSubscription(channel) {
|
|
299
|
+
if (this.connectionState !== ConnectionStates.CONNECTED) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const channelType = this.getChannelType(channel.name);
|
|
304
|
+
|
|
305
|
+
// Для публичных каналов подписываемся сразу
|
|
306
|
+
if (channelType === ChannelTypes.PUBLIC) {
|
|
307
|
+
this.sendMessage({
|
|
308
|
+
event: 'pushler:subscribe',
|
|
309
|
+
data: {
|
|
310
|
+
channel: channel.name,
|
|
311
|
+
app_key: this.appKey
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Для приватных и presence каналов нужна аутентификация
|
|
318
|
+
this.authenticateChannel(channel);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Аутентификация канала
|
|
323
|
+
*/
|
|
324
|
+
async authenticateChannel(channel) {
|
|
325
|
+
try {
|
|
326
|
+
const authData = await this.getChannelAuthData(channel);
|
|
327
|
+
|
|
328
|
+
this.sendMessage({
|
|
329
|
+
event: 'pushler:auth',
|
|
330
|
+
data: authData
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Устанавливаем таймаут аутентификации
|
|
334
|
+
this.authTimeout = setTimeout(() => {
|
|
335
|
+
this.emit(Events.ERROR, {
|
|
336
|
+
code: ErrorCodes.AUTHENTICATION_TIMEOUT,
|
|
337
|
+
message: 'Authentication timeout'
|
|
338
|
+
});
|
|
339
|
+
}, 10000);
|
|
340
|
+
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this.emit(Events.ERROR, {
|
|
343
|
+
code: ErrorCodes.AUTHENTICATION_FAILED,
|
|
344
|
+
message: error.message
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Получение подписи с бэкенда
|
|
351
|
+
*/
|
|
352
|
+
async getChannelSignature(channelName, socketId, userData = null) {
|
|
353
|
+
// Формируем URL: если authEndpoint абсолютный — используем его, иначе добавляем к apiUrl
|
|
354
|
+
const authUrl = this.authEndpoint.startsWith('http')
|
|
355
|
+
? this.authEndpoint
|
|
356
|
+
: `${this.apiUrl}${this.authEndpoint}`;
|
|
357
|
+
|
|
358
|
+
const response = await fetch(authUrl, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
headers: {
|
|
361
|
+
'Content-Type': 'application/json',
|
|
362
|
+
},
|
|
363
|
+
body: JSON.stringify({
|
|
364
|
+
channel_name: channelName,
|
|
365
|
+
socket_id: socketId,
|
|
366
|
+
app_key: this.appKey,
|
|
367
|
+
user_data: userData
|
|
368
|
+
})
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
const error = await response.json();
|
|
373
|
+
throw new Error(error.error || 'Failed to get channel signature');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const data = await response.json();
|
|
377
|
+
return data.signature;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Получение данных для аутентификации канала
|
|
382
|
+
*/
|
|
383
|
+
async getChannelAuthData(channel) {
|
|
384
|
+
if (!this.socketId) {
|
|
385
|
+
throw new Error('Socket ID not available. Connection not established.');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const channelType = this.getChannelType(channel.name);
|
|
389
|
+
|
|
390
|
+
const authData = {
|
|
391
|
+
app_key: this.appKey,
|
|
392
|
+
channel: channel.name,
|
|
393
|
+
socket_id: this.socketId
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Для приватных и presence каналов нужна подпись
|
|
397
|
+
if (channelType === ChannelTypes.PRIVATE || channelType === ChannelTypes.PRESENCE) {
|
|
398
|
+
// Если подпись уже предоставлена, используем её
|
|
399
|
+
if (channel.options.signature) {
|
|
400
|
+
authData.signature = channel.options.signature;
|
|
401
|
+
console.log('Using provided signature:', {
|
|
402
|
+
channel: channel.name,
|
|
403
|
+
socketId: this.socketId,
|
|
404
|
+
signatureLength: channel.options.signature.length,
|
|
405
|
+
signature: channel.options.signature.substring(0, 20) + '...'
|
|
406
|
+
});
|
|
407
|
+
} else {
|
|
408
|
+
// Иначе получаем подпись с бэкенда
|
|
409
|
+
try {
|
|
410
|
+
authData.signature = await this.getChannelSignature(
|
|
411
|
+
channel.name,
|
|
412
|
+
this.socketId,
|
|
413
|
+
channel.options.user
|
|
414
|
+
);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
throw new Error(`Failed to get channel signature: ${error.message}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Для presence каналов нужны данные пользователя
|
|
422
|
+
if (channelType === ChannelTypes.PRESENCE) {
|
|
423
|
+
if (!channel.options.user) {
|
|
424
|
+
throw new Error('User data is required for presence channels');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
authData.user = channel.options.user;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return authData;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Определение типа канала
|
|
435
|
+
* Формат канала: {appKey}:{type-}channelName
|
|
436
|
+
*/
|
|
437
|
+
getChannelType(channelName) {
|
|
438
|
+
// Извлекаем имя канала без appKey
|
|
439
|
+
const baseName = this.extractChannelName(channelName);
|
|
440
|
+
|
|
441
|
+
if (baseName.startsWith('private-')) {
|
|
442
|
+
return ChannelTypes.PRIVATE;
|
|
443
|
+
}
|
|
444
|
+
if (baseName.startsWith('presence-')) {
|
|
445
|
+
return ChannelTypes.PRESENCE;
|
|
446
|
+
}
|
|
447
|
+
return ChannelTypes.PUBLIC;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Отправка сообщения через WebSocket
|
|
452
|
+
*/
|
|
453
|
+
sendMessage(message) {
|
|
454
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
455
|
+
this.socket.send(JSON.stringify(message));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Генерация уникального ID сокета
|
|
461
|
+
*/
|
|
462
|
+
generateSocketId() {
|
|
463
|
+
return Math.random().toString(36).substr(2, 9) + '.' + Math.floor(Math.random() * 1000000);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Планирование переподключения
|
|
468
|
+
*/
|
|
469
|
+
scheduleReconnect() {
|
|
470
|
+
if (this.reconnectTimer) {
|
|
471
|
+
clearTimeout(this.reconnectTimer);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
this.reconnectAttempts++;
|
|
475
|
+
this.connectionState = ConnectionStates.RECONNECTING;
|
|
476
|
+
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
477
|
+
|
|
478
|
+
this.reconnectTimer = setTimeout(() => {
|
|
479
|
+
this.connect();
|
|
480
|
+
}, this.reconnectDelay * this.reconnectAttempts);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Обработка ошибки подключения
|
|
485
|
+
*/
|
|
486
|
+
handleConnectionError(error) {
|
|
487
|
+
this.connectionState = ConnectionStates.FAILED;
|
|
488
|
+
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
489
|
+
|
|
490
|
+
// Формируем более информативное сообщение об ошибке
|
|
491
|
+
let errorMessage = 'Connection failed';
|
|
492
|
+
if (error && error.message) {
|
|
493
|
+
errorMessage = error.message;
|
|
494
|
+
} else if (error && typeof error === 'object') {
|
|
495
|
+
errorMessage = JSON.stringify(error);
|
|
496
|
+
} else if (typeof error === 'string') {
|
|
497
|
+
errorMessage = error;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Добавляем информацию о URL для отладки
|
|
501
|
+
const fullErrorMessage = `WebSocket connection to '${this.wsUrl}' failed: ${errorMessage}`;
|
|
502
|
+
|
|
503
|
+
console.error('PushlerClient connection error:', fullErrorMessage);
|
|
504
|
+
|
|
505
|
+
this.emit(Events.ERROR, {
|
|
506
|
+
code: ErrorCodes.CONNECTION_FAILED,
|
|
507
|
+
message: fullErrorMessage,
|
|
508
|
+
url: this.wsUrl,
|
|
509
|
+
originalError: error
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Отключение от WebSocket сервера
|
|
515
|
+
*/
|
|
516
|
+
disconnect() {
|
|
517
|
+
if (this.reconnectTimer) {
|
|
518
|
+
clearTimeout(this.reconnectTimer);
|
|
519
|
+
this.reconnectTimer = null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (this.authTimeout) {
|
|
523
|
+
clearTimeout(this.authTimeout);
|
|
524
|
+
this.authTimeout = null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (this.socket) {
|
|
528
|
+
this.socket.close();
|
|
529
|
+
this.socket = null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Очищаем каналы и алиасы
|
|
533
|
+
this.channels.clear();
|
|
534
|
+
this.channelAliases.clear();
|
|
535
|
+
|
|
536
|
+
this.connectionState = ConnectionStates.DISCONNECTED;
|
|
537
|
+
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Подписка на событие
|
|
542
|
+
*/
|
|
543
|
+
on(event, callback) {
|
|
544
|
+
if (!this.eventListeners.has(event)) {
|
|
545
|
+
this.eventListeners.set(event, []);
|
|
546
|
+
}
|
|
547
|
+
this.eventListeners.get(event).push(callback);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Отписка от события
|
|
552
|
+
*/
|
|
553
|
+
off(event, callback) {
|
|
554
|
+
if (this.eventListeners.has(event)) {
|
|
555
|
+
const listeners = this.eventListeners.get(event);
|
|
556
|
+
const index = listeners.indexOf(callback);
|
|
557
|
+
if (index > -1) {
|
|
558
|
+
listeners.splice(index, 1);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Вызов события
|
|
565
|
+
*/
|
|
566
|
+
emit(event, data) {
|
|
567
|
+
if (this.eventListeners.has(event)) {
|
|
568
|
+
this.eventListeners.get(event).forEach(callback => {
|
|
569
|
+
try {
|
|
570
|
+
callback(data);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.error('Error in event listener:', error);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Получение состояния подключения
|
|
580
|
+
*/
|
|
581
|
+
getConnectionState() {
|
|
582
|
+
return this.connectionState;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Получение всех каналов (возвращает оригинальные имена без appKey)
|
|
587
|
+
*/
|
|
588
|
+
getChannels() {
|
|
589
|
+
return Array.from(this.channels.values()).map(channel =>
|
|
590
|
+
channel.originalName || this.extractChannelName(channel.name)
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Получение канала по имени
|
|
596
|
+
* @param {string} channelName - Имя канала (можно использовать оригинальное или полное)
|
|
597
|
+
* @returns {Channel|undefined} Объект канала
|
|
598
|
+
*/
|
|
599
|
+
channel(channelName) {
|
|
600
|
+
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
601
|
+
return this.channels.get(fullChannelName);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export default PushlerClient;
|