@pushler/js 1.0.3 → 1.2.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 +30 -8
- package/dist/pushler-ru.cjs +433 -35
- package/dist/pushler-ru.cjs.map +1 -1
- package/dist/pushler-ru.esm.js +431 -36
- package/dist/pushler-ru.esm.js.map +1 -1
- package/dist/pushler-ru.js +433 -35
- package/dist/pushler-ru.js.map +1 -1
- package/dist/pushler-ru.min.js +1 -1
- package/dist/pushler-ru.min.js.map +1 -1
- package/package.json +1 -1
- package/src/Channel.js +12 -1
- package/src/PushlerClient.js +400 -31
- package/src/PushlerServer.js +5 -5
- package/src/constants.js +12 -0
- package/src/index.js +16 -2
package/src/PushlerClient.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Channel from './Channel';
|
|
2
|
-
import { ChannelTypes, ConnectionStates, Events, ErrorCodes } from './constants';
|
|
2
|
+
import { ChannelTypes, ConnectionStates, SubscriptionStates, Events, ErrorCodes } from './constants';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Основной класс для подключения к Pushler.ru WebSocket серверу
|
|
@@ -11,6 +11,8 @@ class PushlerClient {
|
|
|
11
11
|
this.wsUrl = options.wsUrl || `wss://ws.pushler.ru/app/${this.appKey}`;
|
|
12
12
|
this.apiUrl = options.apiUrl;
|
|
13
13
|
this.authEndpoint = options.authEndpoint || '/pushler/auth'; // Путь для авторизации каналов
|
|
14
|
+
this.authHeaders = options.authHeaders || null; // Кастомные заголовки для авторизации
|
|
15
|
+
this.getAuthHeaders = options.getAuthHeaders || null; // Функция для динамических заголовков
|
|
14
16
|
this.autoConnect = options.autoConnect !== false;
|
|
15
17
|
this.reconnectDelay = options.reconnectDelay || 1000;
|
|
16
18
|
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
|
@@ -21,10 +23,16 @@ class PushlerClient {
|
|
|
21
23
|
this.channels = new Map(); // Хранит каналы по полному имени (с appKey)
|
|
22
24
|
this.channelAliases = new Map(); // Маппинг оригинальное имя -> полное имя
|
|
23
25
|
this.eventListeners = new Map();
|
|
26
|
+
this.pendingAuthRequests = new Map(); // Отслеживание pending auth запросов для отмены при unsubscribe
|
|
24
27
|
this.reconnectAttempts = 0;
|
|
25
28
|
this.reconnectTimer = null;
|
|
26
29
|
this.authTimeout = null;
|
|
27
30
|
|
|
31
|
+
// Promise для ожидания подключения
|
|
32
|
+
this.connectionPromise = null;
|
|
33
|
+
this.connectionResolve = null;
|
|
34
|
+
this.connectionReject = null;
|
|
35
|
+
|
|
28
36
|
// Автоматическое подключение
|
|
29
37
|
if (this.autoConnect) {
|
|
30
38
|
this.connect();
|
|
@@ -60,13 +68,25 @@ class PushlerClient {
|
|
|
60
68
|
|
|
61
69
|
/**
|
|
62
70
|
* Подключение к WebSocket серверу
|
|
71
|
+
* @returns {Promise} Promise, который резолвится при успешном подключении
|
|
63
72
|
*/
|
|
64
73
|
connect() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return;
|
|
74
|
+
// Если уже подключены - возвращаем резолвленный Promise
|
|
75
|
+
if (this.connectionState === ConnectionStates.CONNECTED) {
|
|
76
|
+
return Promise.resolve({ socketId: this.socketId });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Если уже идёт подключение - возвращаем существующий Promise
|
|
80
|
+
if (this.connectionState === ConnectionStates.CONNECTING && this.connectionPromise) {
|
|
81
|
+
return this.connectionPromise;
|
|
68
82
|
}
|
|
69
83
|
|
|
84
|
+
// Создаём новый Promise для ожидания подключения
|
|
85
|
+
this.connectionPromise = new Promise((resolve, reject) => {
|
|
86
|
+
this.connectionResolve = resolve;
|
|
87
|
+
this.connectionReject = reject;
|
|
88
|
+
});
|
|
89
|
+
|
|
70
90
|
this.connectionState = ConnectionStates.CONNECTING;
|
|
71
91
|
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
72
92
|
|
|
@@ -81,7 +101,66 @@ class PushlerClient {
|
|
|
81
101
|
} catch (error) {
|
|
82
102
|
console.error('PushlerClient: Error creating WebSocket connection:', error);
|
|
83
103
|
this.handleConnectionError(error);
|
|
104
|
+
if (this.connectionReject) {
|
|
105
|
+
this.connectionReject(error);
|
|
106
|
+
this.connectionPromise = null;
|
|
107
|
+
this.connectionResolve = null;
|
|
108
|
+
this.connectionReject = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return this.connectionPromise;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ожидание установления соединения
|
|
117
|
+
* @param {number} timeout - Таймаут в мс (по умолчанию 10000)
|
|
118
|
+
* @returns {Promise} Promise с socketId
|
|
119
|
+
*/
|
|
120
|
+
waitForConnection(timeout = 10000) {
|
|
121
|
+
// Если уже подключены
|
|
122
|
+
if (this.connectionState === ConnectionStates.CONNECTED && this.socketId) {
|
|
123
|
+
return Promise.resolve({ socketId: this.socketId });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Если есть активный Promise подключения
|
|
127
|
+
if (this.connectionPromise) {
|
|
128
|
+
return Promise.race([
|
|
129
|
+
this.connectionPromise,
|
|
130
|
+
new Promise((_, reject) =>
|
|
131
|
+
setTimeout(() => reject(new Error('Connection timeout')), timeout)
|
|
132
|
+
)
|
|
133
|
+
]);
|
|
84
134
|
}
|
|
135
|
+
|
|
136
|
+
// Если не подключаемся - начинаем подключение
|
|
137
|
+
if (this.connectionState === ConnectionStates.DISCONNECTED) {
|
|
138
|
+
return this.connect();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Для других состояний создаём Promise, который ждёт connected события
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const timeoutId = setTimeout(() => {
|
|
144
|
+
this.off(Events.CONNECTED, onConnected);
|
|
145
|
+
this.off(Events.ERROR, onError);
|
|
146
|
+
reject(new Error('Connection timeout'));
|
|
147
|
+
}, timeout);
|
|
148
|
+
|
|
149
|
+
const onConnected = (data) => {
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
this.off(Events.ERROR, onError);
|
|
152
|
+
resolve(data);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const onError = (err) => {
|
|
156
|
+
clearTimeout(timeoutId);
|
|
157
|
+
this.off(Events.CONNECTED, onConnected);
|
|
158
|
+
reject(err);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
this.on(Events.CONNECTED, onConnected);
|
|
162
|
+
this.on(Events.ERROR, onError);
|
|
163
|
+
});
|
|
85
164
|
}
|
|
86
165
|
|
|
87
166
|
/**
|
|
@@ -133,31 +212,98 @@ class PushlerClient {
|
|
|
133
212
|
};
|
|
134
213
|
}
|
|
135
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Разбор склеенных JSON из одного WebSocket фрейма
|
|
217
|
+
* Сервер иногда отправляет несколько JSON объектов подряд в одном фрейме:
|
|
218
|
+
* {"event":"a",...}{"event":"b",...}
|
|
219
|
+
* @param {string} str - Строка с одним или несколькими JSON объектами
|
|
220
|
+
* @returns {string[]} Массив отдельных JSON строк
|
|
221
|
+
*/
|
|
222
|
+
splitConcatenatedJSON(str) {
|
|
223
|
+
const results = [];
|
|
224
|
+
let depth = 0;
|
|
225
|
+
let start = 0;
|
|
226
|
+
let inString = false;
|
|
227
|
+
let escape = false;
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < str.length; i++) {
|
|
230
|
+
const char = str[i];
|
|
231
|
+
|
|
232
|
+
// Обработка escape-символов внутри строк
|
|
233
|
+
if (escape) {
|
|
234
|
+
escape = false;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (char === '\\' && inString) {
|
|
239
|
+
escape = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Отслеживание строковых литералов
|
|
244
|
+
if (char === '"') {
|
|
245
|
+
inString = !inString;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Считаем скобки только вне строк
|
|
250
|
+
if (!inString) {
|
|
251
|
+
if (char === '{') {
|
|
252
|
+
depth++;
|
|
253
|
+
} else if (char === '}') {
|
|
254
|
+
depth--;
|
|
255
|
+
if (depth === 0) {
|
|
256
|
+
results.push(str.slice(start, i + 1));
|
|
257
|
+
start = i + 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
|
|
136
266
|
/**
|
|
137
267
|
* Обработка входящих сообщений
|
|
138
268
|
*/
|
|
139
269
|
handleMessage(event) {
|
|
140
270
|
try {
|
|
141
|
-
|
|
271
|
+
// Пытаемся распарсить как один JSON
|
|
272
|
+
const messages = this.splitConcatenatedJSON(event.data);
|
|
142
273
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.handleAuthSuccess(message);
|
|
152
|
-
break;
|
|
153
|
-
case 'pushler:auth_error':
|
|
154
|
-
this.handleAuthError(message);
|
|
155
|
-
break;
|
|
156
|
-
default:
|
|
157
|
-
this.handleChannelMessage(message);
|
|
274
|
+
// Если несколько JSON объектов, обрабатываем каждый
|
|
275
|
+
for (const jsonStr of messages) {
|
|
276
|
+
try {
|
|
277
|
+
const message = JSON.parse(jsonStr);
|
|
278
|
+
this.processMessage(message);
|
|
279
|
+
} catch (parseError) {
|
|
280
|
+
console.error('Error parsing JSON message:', parseError, 'Raw:', jsonStr);
|
|
281
|
+
}
|
|
158
282
|
}
|
|
159
283
|
} catch (error) {
|
|
160
|
-
console.error('Error
|
|
284
|
+
console.error('Error handling WebSocket message:', error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Обработка распарсенного сообщения
|
|
290
|
+
*/
|
|
291
|
+
processMessage(message) {
|
|
292
|
+
switch (message.event) {
|
|
293
|
+
case 'pushler:connection_established':
|
|
294
|
+
this.handleConnectionEstablished(message.data);
|
|
295
|
+
break;
|
|
296
|
+
case 'pushler:subscription_succeeded':
|
|
297
|
+
this.handleSubscriptionSucceeded(message);
|
|
298
|
+
break;
|
|
299
|
+
case 'pushler:auth_success':
|
|
300
|
+
this.handleAuthSuccess(message);
|
|
301
|
+
break;
|
|
302
|
+
case 'pushler:auth_error':
|
|
303
|
+
this.handleAuthError(message);
|
|
304
|
+
break;
|
|
305
|
+
default:
|
|
306
|
+
this.handleChannelMessage(message);
|
|
161
307
|
}
|
|
162
308
|
}
|
|
163
309
|
|
|
@@ -171,6 +317,14 @@ class PushlerClient {
|
|
|
171
317
|
|
|
172
318
|
console.log('Connection established with socket ID:', this.socketId);
|
|
173
319
|
|
|
320
|
+
// Резолвим Promise подключения
|
|
321
|
+
if (this.connectionResolve) {
|
|
322
|
+
this.connectionResolve({ socketId: this.socketId });
|
|
323
|
+
this.connectionPromise = null;
|
|
324
|
+
this.connectionResolve = null;
|
|
325
|
+
this.connectionReject = null;
|
|
326
|
+
}
|
|
327
|
+
|
|
174
328
|
// Переподписываемся на все каналы после переподключения
|
|
175
329
|
this.resubscribeAllChannels();
|
|
176
330
|
|
|
@@ -210,7 +364,13 @@ class PushlerClient {
|
|
|
210
364
|
const { channel } = message.data;
|
|
211
365
|
const channelInstance = this.channels.get(channel);
|
|
212
366
|
|
|
367
|
+
// Очищаем pending auth запрос
|
|
368
|
+
this.cancelPendingAuth(channel);
|
|
369
|
+
|
|
213
370
|
if (channelInstance) {
|
|
371
|
+
// Устанавливаем состояние subscribed
|
|
372
|
+
channelInstance.subscriptionState = SubscriptionStates.SUBSCRIBED;
|
|
373
|
+
|
|
214
374
|
// На сервере уже происходит подписка после успешной авторизации,
|
|
215
375
|
// поэтому просто помечаем канал как подписанный
|
|
216
376
|
channelInstance.handleAuthSuccess(message.data);
|
|
@@ -223,11 +383,31 @@ class PushlerClient {
|
|
|
223
383
|
* Обработка ошибки аутентификации
|
|
224
384
|
*/
|
|
225
385
|
handleAuthError(message) {
|
|
226
|
-
const { error } = message.data;
|
|
227
|
-
|
|
386
|
+
const { error, channel } = message.data;
|
|
387
|
+
const channelName = channel || null;
|
|
388
|
+
|
|
389
|
+
// Очищаем pending auth запрос
|
|
390
|
+
if (channelName) {
|
|
391
|
+
this.cancelPendingAuth(channelName);
|
|
392
|
+
|
|
393
|
+
const channelInstance = this.channels.get(channelName);
|
|
394
|
+
if (channelInstance) {
|
|
395
|
+
channelInstance.subscriptionState = SubscriptionStates.FAILED;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const originalChannelName = channelName ? this.extractChannelName(channelName) : null;
|
|
400
|
+
|
|
401
|
+
this.emit(Events.CHANNEL_AUTH_ERROR, {
|
|
402
|
+
error,
|
|
403
|
+
channel: originalChannelName,
|
|
404
|
+
socketId: this.socketId
|
|
405
|
+
});
|
|
228
406
|
this.emit(Events.ERROR, {
|
|
229
407
|
code: ErrorCodes.AUTHENTICATION_FAILED,
|
|
230
|
-
message: error
|
|
408
|
+
message: error,
|
|
409
|
+
channel: originalChannelName,
|
|
410
|
+
socketId: this.socketId
|
|
231
411
|
});
|
|
232
412
|
}
|
|
233
413
|
|
|
@@ -285,8 +465,12 @@ class PushlerClient {
|
|
|
285
465
|
// Поддерживаем оба формата имени
|
|
286
466
|
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
287
467
|
|
|
468
|
+
// Отменяем pending auth запросы для этого канала (исправление race condition)
|
|
469
|
+
this.cancelPendingAuth(fullChannelName);
|
|
470
|
+
|
|
288
471
|
const channel = this.channels.get(fullChannelName);
|
|
289
472
|
if (channel) {
|
|
473
|
+
channel.subscriptionState = SubscriptionStates.UNSUBSCRIBED;
|
|
290
474
|
channel.unsubscribe();
|
|
291
475
|
this.channels.delete(fullChannelName);
|
|
292
476
|
this.channelAliases.delete(channel.originalName || channelName);
|
|
@@ -321,32 +505,141 @@ class PushlerClient {
|
|
|
321
505
|
|
|
322
506
|
/**
|
|
323
507
|
* Аутентификация канала
|
|
508
|
+
* С поддержкой отмены при unsubscribe
|
|
324
509
|
*/
|
|
325
510
|
async authenticateChannel(channel) {
|
|
511
|
+
// Создаём AbortController для возможности отмены
|
|
512
|
+
const abortController = new AbortController();
|
|
513
|
+
const channelName = channel.name;
|
|
514
|
+
|
|
515
|
+
// Сохраняем для возможности отмены при unsubscribe
|
|
516
|
+
this.pendingAuthRequests.set(channelName, {
|
|
517
|
+
controller: abortController,
|
|
518
|
+
timeoutId: null
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Устанавливаем состояние канала
|
|
522
|
+
channel.subscriptionState = SubscriptionStates.PENDING_AUTH;
|
|
523
|
+
|
|
326
524
|
try {
|
|
525
|
+
// Проверка отмены перед началом
|
|
526
|
+
if (abortController.signal.aborted) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
327
530
|
const authData = await this.getChannelAuthData(channel);
|
|
328
531
|
|
|
532
|
+
// Проверка отмены после получения данных
|
|
533
|
+
if (abortController.signal.aborted) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
329
537
|
this.sendMessage({
|
|
330
538
|
event: 'pushler:auth',
|
|
331
539
|
data: authData
|
|
332
540
|
});
|
|
333
541
|
|
|
334
542
|
// Устанавливаем таймаут аутентификации
|
|
335
|
-
|
|
543
|
+
const timeoutId = setTimeout(() => {
|
|
544
|
+
// Не отправляем ошибку, если запрос уже отменён
|
|
545
|
+
if (abortController.signal.aborted) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Очищаем pending запрос
|
|
550
|
+
this.pendingAuthRequests.delete(channelName);
|
|
551
|
+
|
|
552
|
+
// Устанавливаем состояние failed
|
|
553
|
+
channel.subscriptionState = SubscriptionStates.FAILED;
|
|
554
|
+
|
|
336
555
|
this.emit(Events.ERROR, {
|
|
337
556
|
code: ErrorCodes.AUTHENTICATION_TIMEOUT,
|
|
338
|
-
message: 'Authentication timeout'
|
|
557
|
+
message: 'Authentication timeout',
|
|
558
|
+
channel: channel.originalName || this.extractChannelName(channelName),
|
|
559
|
+
socketId: this.socketId
|
|
339
560
|
});
|
|
340
561
|
}, 10000);
|
|
341
562
|
|
|
563
|
+
// Сохраняем timeoutId для возможности очистки
|
|
564
|
+
const pending = this.pendingAuthRequests.get(channelName);
|
|
565
|
+
if (pending) {
|
|
566
|
+
pending.timeoutId = timeoutId;
|
|
567
|
+
}
|
|
568
|
+
|
|
342
569
|
} catch (error) {
|
|
570
|
+
// Не отправляем ошибку, если запрос уже отменён
|
|
571
|
+
if (abortController.signal.aborted) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Очищаем pending запрос
|
|
576
|
+
this.pendingAuthRequests.delete(channelName);
|
|
577
|
+
|
|
578
|
+
// Устанавливаем состояние failed
|
|
579
|
+
channel.subscriptionState = SubscriptionStates.FAILED;
|
|
580
|
+
|
|
343
581
|
this.emit(Events.ERROR, {
|
|
344
582
|
code: ErrorCodes.AUTHENTICATION_FAILED,
|
|
345
|
-
message: error.message
|
|
583
|
+
message: error.message,
|
|
584
|
+
channel: channel.originalName || this.extractChannelName(channelName),
|
|
585
|
+
socketId: this.socketId
|
|
346
586
|
});
|
|
347
587
|
}
|
|
348
588
|
}
|
|
349
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Отмена pending auth запроса для канала
|
|
592
|
+
*/
|
|
593
|
+
cancelPendingAuth(channelName) {
|
|
594
|
+
const pending = this.pendingAuthRequests.get(channelName);
|
|
595
|
+
if (pending) {
|
|
596
|
+
// Отменяем запрос
|
|
597
|
+
pending.controller.abort();
|
|
598
|
+
|
|
599
|
+
// Очищаем таймаут
|
|
600
|
+
if (pending.timeoutId) {
|
|
601
|
+
clearTimeout(pending.timeoutId);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Удаляем из карты
|
|
605
|
+
this.pendingAuthRequests.delete(channelName);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Получение заголовков для авторизации
|
|
611
|
+
* Поддерживает как синхронные, так и асинхронные функции getAuthHeaders
|
|
612
|
+
* @returns {Promise<Object>} Заголовки для авторизации
|
|
613
|
+
*/
|
|
614
|
+
async getAuthRequestHeaders() {
|
|
615
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
616
|
+
|
|
617
|
+
// Динамические заголовки имеют приоритет
|
|
618
|
+
if (typeof this.getAuthHeaders === 'function') {
|
|
619
|
+
// Поддержка асинхронных функций (AsyncStorage, SecureStore, Keychain)
|
|
620
|
+
const dynamicHeaders = await Promise.resolve(this.getAuthHeaders());
|
|
621
|
+
Object.assign(headers, dynamicHeaders);
|
|
622
|
+
} else if (this.authHeaders) {
|
|
623
|
+
// Статические заголовки
|
|
624
|
+
Object.assign(headers, this.authHeaders);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return headers;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Установка токена авторизации
|
|
632
|
+
* Удобный метод для установки Bearer токена без создания функции
|
|
633
|
+
* @param {string} token - JWT токен
|
|
634
|
+
*/
|
|
635
|
+
setAuthToken(token) {
|
|
636
|
+
if (token) {
|
|
637
|
+
this.authHeaders = { 'Authorization': `Bearer ${token}` };
|
|
638
|
+
} else {
|
|
639
|
+
this.authHeaders = null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
350
643
|
/**
|
|
351
644
|
* Получение подписи с бэкенда
|
|
352
645
|
*/
|
|
@@ -356,11 +649,16 @@ class PushlerClient {
|
|
|
356
649
|
? this.authEndpoint
|
|
357
650
|
: `${this.apiUrl}${this.authEndpoint}`;
|
|
358
651
|
|
|
652
|
+
// Асинхронное получение заголовков (поддержка AsyncStorage/SecureStore)
|
|
653
|
+
const headers = await this.getAuthRequestHeaders();
|
|
654
|
+
const hasAuthHeaders = this.authHeaders || this.getAuthHeaders;
|
|
655
|
+
|
|
359
656
|
const response = await fetch(authUrl, {
|
|
360
657
|
method: 'POST',
|
|
361
|
-
headers
|
|
362
|
-
|
|
363
|
-
|
|
658
|
+
headers,
|
|
659
|
+
// Если есть authHeaders, не используем credentials (JWT режим)
|
|
660
|
+
// Если нет — используем cookies
|
|
661
|
+
...(hasAuthHeaders ? {} : { credentials: 'include' }),
|
|
364
662
|
body: JSON.stringify({
|
|
365
663
|
channel_name: channelName,
|
|
366
664
|
socket_id: socketId,
|
|
@@ -370,7 +668,7 @@ class PushlerClient {
|
|
|
370
668
|
});
|
|
371
669
|
|
|
372
670
|
if (!response.ok) {
|
|
373
|
-
const error = await response.json();
|
|
671
|
+
const error = await response.json().catch(() => ({ error: 'Auth request failed' }));
|
|
374
672
|
throw new Error(error.error || 'Failed to get channel signature');
|
|
375
673
|
}
|
|
376
674
|
|
|
@@ -503,6 +801,14 @@ class PushlerClient {
|
|
|
503
801
|
|
|
504
802
|
console.error('PushlerClient connection error:', fullErrorMessage);
|
|
505
803
|
|
|
804
|
+
// Реджектим Promise подключения
|
|
805
|
+
if (this.connectionReject) {
|
|
806
|
+
this.connectionReject(new Error(fullErrorMessage));
|
|
807
|
+
this.connectionPromise = null;
|
|
808
|
+
this.connectionResolve = null;
|
|
809
|
+
this.connectionReject = null;
|
|
810
|
+
}
|
|
811
|
+
|
|
506
812
|
this.emit(Events.ERROR, {
|
|
507
813
|
code: ErrorCodes.CONNECTION_FAILED,
|
|
508
814
|
message: fullErrorMessage,
|
|
@@ -601,6 +907,69 @@ class PushlerClient {
|
|
|
601
907
|
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
602
908
|
return this.channels.get(fullChannelName);
|
|
603
909
|
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Проверка подписки на канал
|
|
913
|
+
* @param {string} channelName - Имя канала
|
|
914
|
+
* @returns {boolean} true если подписан
|
|
915
|
+
*/
|
|
916
|
+
isSubscribed(channelName) {
|
|
917
|
+
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
918
|
+
const channel = this.channels.get(fullChannelName);
|
|
919
|
+
return channel ? channel.subscribed : false;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Получение состояния подписки на канал
|
|
924
|
+
* @param {string} channelName - Имя канала
|
|
925
|
+
* @returns {string|null} Состояние подписки или null если канал не найден
|
|
926
|
+
*/
|
|
927
|
+
getSubscriptionState(channelName) {
|
|
928
|
+
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
929
|
+
const channel = this.channels.get(fullChannelName);
|
|
930
|
+
|
|
931
|
+
if (!channel) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return channel.subscriptionState || (channel.subscribed ? SubscriptionStates.SUBSCRIBED : SubscriptionStates.PENDING);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Получение списка подписанных каналов
|
|
940
|
+
* @returns {string[]} Массив имён подписанных каналов
|
|
941
|
+
*/
|
|
942
|
+
getSubscribedChannels() {
|
|
943
|
+
const subscribedChannels = [];
|
|
944
|
+
|
|
945
|
+
for (const [fullName, channel] of this.channels.entries()) {
|
|
946
|
+
if (channel.subscribed) {
|
|
947
|
+
subscribedChannels.push(channel.originalName || this.extractChannelName(fullName));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return subscribedChannels;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Получение всех каналов с их состояниями
|
|
956
|
+
* @returns {Object[]} Массив объектов с информацией о каналах
|
|
957
|
+
*/
|
|
958
|
+
getAllChannelsInfo() {
|
|
959
|
+
const channelsInfo = [];
|
|
960
|
+
|
|
961
|
+
for (const [fullName, channel] of this.channels.entries()) {
|
|
962
|
+
channelsInfo.push({
|
|
963
|
+
name: channel.originalName || this.extractChannelName(fullName),
|
|
964
|
+
fullName: fullName,
|
|
965
|
+
subscribed: channel.subscribed,
|
|
966
|
+
state: channel.subscriptionState || (channel.subscribed ? SubscriptionStates.SUBSCRIBED : SubscriptionStates.PENDING),
|
|
967
|
+
type: channel.getType()
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return channelsInfo;
|
|
972
|
+
}
|
|
604
973
|
}
|
|
605
974
|
|
|
606
975
|
export default PushlerClient;
|
package/src/PushlerServer.js
CHANGED
|
@@ -5,11 +5,12 @@ import { createHmac } from 'crypto';
|
|
|
5
5
|
* Используется на сервере Node.js для отправки событий в каналы
|
|
6
6
|
*/
|
|
7
7
|
class PushlerServer {
|
|
8
|
+
static API_URL = 'https://api.pushler.ru';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @param {Object} options - Параметры клиента
|
|
10
12
|
* @param {string} options.appKey - Ключ приложения (key_xxx)
|
|
11
13
|
* @param {string} options.appSecret - Секрет приложения (secret_xxx)
|
|
12
|
-
* @param {string} [options.apiUrl='http://localhost:8000/api'] - URL API сервера
|
|
13
14
|
* @param {number} [options.timeout=10000] - Таймаут запроса в мс
|
|
14
15
|
*/
|
|
15
16
|
constructor(options = {}) {
|
|
@@ -22,7 +23,6 @@ class PushlerServer {
|
|
|
22
23
|
|
|
23
24
|
this.appKey = options.appKey;
|
|
24
25
|
this.appSecret = options.appSecret;
|
|
25
|
-
this.apiUrl = (options.apiUrl || 'http://localhost:8000/api').replace(/\/$/, '');
|
|
26
26
|
this.timeout = options.timeout || 10000;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -306,7 +306,7 @@ class PushlerServer {
|
|
|
306
306
|
* @returns {Promise<Object>} Результат запроса
|
|
307
307
|
*/
|
|
308
308
|
async makeSignedRequest(method, endpoint, data = null) {
|
|
309
|
-
const url =
|
|
309
|
+
const url = PushlerServer.API_URL + endpoint;
|
|
310
310
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
311
311
|
const body = data ? JSON.stringify(data) : '';
|
|
312
312
|
const signature = this.generateApiSignature(body, timestamp);
|
|
@@ -354,7 +354,7 @@ class PushlerServer {
|
|
|
354
354
|
* @returns {Promise<Object>} Результат запроса
|
|
355
355
|
*/
|
|
356
356
|
async makeHttpRequest(method, endpoint, data = null) {
|
|
357
|
-
const url =
|
|
357
|
+
const url = PushlerServer.API_URL + endpoint;
|
|
358
358
|
|
|
359
359
|
const headers = {
|
|
360
360
|
'Content-Type': 'application/json',
|
|
@@ -395,7 +395,7 @@ class PushlerServer {
|
|
|
395
395
|
* @returns {string}
|
|
396
396
|
*/
|
|
397
397
|
getApiUrl() {
|
|
398
|
-
return
|
|
398
|
+
return PushlerServer.API_URL;
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
|
package/src/constants.js
CHANGED
|
@@ -18,6 +18,18 @@ export const ConnectionStates = {
|
|
|
18
18
|
FAILED: 'failed'
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Состояния подписки на канал
|
|
23
|
+
*/
|
|
24
|
+
export const SubscriptionStates = {
|
|
25
|
+
PENDING: 'pending',
|
|
26
|
+
PENDING_AUTH: 'pending_auth',
|
|
27
|
+
SUBSCRIBING: 'subscribing',
|
|
28
|
+
SUBSCRIBED: 'subscribed',
|
|
29
|
+
FAILED: 'failed',
|
|
30
|
+
UNSUBSCRIBED: 'unsubscribed'
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* События WebSocket
|
|
23
35
|
*/
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import PushlerClient from './PushlerClient';
|
|
2
2
|
import PushlerServer from './PushlerServer';
|
|
3
3
|
import Channel from './Channel';
|
|
4
|
-
import { ChannelTypes, ConnectionStates } from './constants';
|
|
4
|
+
import { ChannelTypes, ConnectionStates, SubscriptionStates, ErrorCodes, Events } from './constants';
|
|
5
5
|
import { generateSocketId } from './utils';
|
|
6
6
|
|
|
7
7
|
// Создаем объект с методами для удобного API
|
|
@@ -17,8 +17,22 @@ const Pushler = {
|
|
|
17
17
|
Channel,
|
|
18
18
|
ChannelTypes,
|
|
19
19
|
ConnectionStates,
|
|
20
|
+
SubscriptionStates,
|
|
21
|
+
ErrorCodes,
|
|
22
|
+
Events,
|
|
20
23
|
generateSocketId
|
|
21
24
|
};
|
|
22
25
|
|
|
23
|
-
export {
|
|
26
|
+
export {
|
|
27
|
+
PushlerClient,
|
|
28
|
+
PushlerServer,
|
|
29
|
+
Channel,
|
|
30
|
+
ChannelTypes,
|
|
31
|
+
ConnectionStates,
|
|
32
|
+
SubscriptionStates,
|
|
33
|
+
ErrorCodes,
|
|
34
|
+
Events,
|
|
35
|
+
generateSocketId,
|
|
36
|
+
Pushler
|
|
37
|
+
};
|
|
24
38
|
export default Pushler;
|