@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/dist/pushler-ru.cjs
CHANGED
|
@@ -24,6 +24,18 @@ const ConnectionStates = {
|
|
|
24
24
|
FAILED: 'failed'
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Состояния подписки на канал
|
|
29
|
+
*/
|
|
30
|
+
const SubscriptionStates = {
|
|
31
|
+
PENDING: 'pending',
|
|
32
|
+
PENDING_AUTH: 'pending_auth',
|
|
33
|
+
SUBSCRIBING: 'subscribing',
|
|
34
|
+
SUBSCRIBED: 'subscribed',
|
|
35
|
+
FAILED: 'failed',
|
|
36
|
+
UNSUBSCRIBED: 'unsubscribed'
|
|
37
|
+
};
|
|
38
|
+
|
|
27
39
|
/**
|
|
28
40
|
* События WebSocket
|
|
29
41
|
*/
|
|
@@ -65,6 +77,7 @@ class Channel {
|
|
|
65
77
|
this.name = name;
|
|
66
78
|
this.options = options;
|
|
67
79
|
this.subscribed = false;
|
|
80
|
+
this.subscriptionState = SubscriptionStates.PENDING;
|
|
68
81
|
this.eventListeners = new Map();
|
|
69
82
|
this.presenceData = null;
|
|
70
83
|
}
|
|
@@ -112,6 +125,7 @@ class Channel {
|
|
|
112
125
|
*/
|
|
113
126
|
handleAuthSuccess(data) {
|
|
114
127
|
this.subscribed = true;
|
|
128
|
+
this.subscriptionState = SubscriptionStates.SUBSCRIBED;
|
|
115
129
|
this.emit(Events.CHANNEL_SUBSCRIBED, data);
|
|
116
130
|
|
|
117
131
|
// Для presence каналов сохраняем данные пользователя
|
|
@@ -139,6 +153,7 @@ class Channel {
|
|
|
139
153
|
}
|
|
140
154
|
|
|
141
155
|
this.subscribed = false;
|
|
156
|
+
this.subscriptionState = SubscriptionStates.UNSUBSCRIBED;
|
|
142
157
|
this.emit(Events.CHANNEL_UNSUBSCRIBED);
|
|
143
158
|
}
|
|
144
159
|
|
|
@@ -149,6 +164,14 @@ class Channel {
|
|
|
149
164
|
return this.subscribed;
|
|
150
165
|
}
|
|
151
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Получение состояния подписки
|
|
169
|
+
* @returns {string} Состояние подписки
|
|
170
|
+
*/
|
|
171
|
+
getSubscriptionState() {
|
|
172
|
+
return this.subscriptionState;
|
|
173
|
+
}
|
|
174
|
+
|
|
152
175
|
/**
|
|
153
176
|
* Получение данных присутствия (для presence каналов)
|
|
154
177
|
*/
|
|
@@ -207,6 +230,8 @@ class PushlerClient {
|
|
|
207
230
|
this.wsUrl = options.wsUrl || `wss://ws.pushler.ru/app/${this.appKey}`;
|
|
208
231
|
this.apiUrl = options.apiUrl;
|
|
209
232
|
this.authEndpoint = options.authEndpoint || '/pushler/auth'; // Путь для авторизации каналов
|
|
233
|
+
this.authHeaders = options.authHeaders || null; // Кастомные заголовки для авторизации
|
|
234
|
+
this.getAuthHeaders = options.getAuthHeaders || null; // Функция для динамических заголовков
|
|
210
235
|
this.autoConnect = options.autoConnect !== false;
|
|
211
236
|
this.reconnectDelay = options.reconnectDelay || 1000;
|
|
212
237
|
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
|
@@ -217,10 +242,16 @@ class PushlerClient {
|
|
|
217
242
|
this.channels = new Map(); // Хранит каналы по полному имени (с appKey)
|
|
218
243
|
this.channelAliases = new Map(); // Маппинг оригинальное имя -> полное имя
|
|
219
244
|
this.eventListeners = new Map();
|
|
245
|
+
this.pendingAuthRequests = new Map(); // Отслеживание pending auth запросов для отмены при unsubscribe
|
|
220
246
|
this.reconnectAttempts = 0;
|
|
221
247
|
this.reconnectTimer = null;
|
|
222
248
|
this.authTimeout = null;
|
|
223
249
|
|
|
250
|
+
// Promise для ожидания подключения
|
|
251
|
+
this.connectionPromise = null;
|
|
252
|
+
this.connectionResolve = null;
|
|
253
|
+
this.connectionReject = null;
|
|
254
|
+
|
|
224
255
|
// Автоматическое подключение
|
|
225
256
|
if (this.autoConnect) {
|
|
226
257
|
this.connect();
|
|
@@ -256,13 +287,25 @@ class PushlerClient {
|
|
|
256
287
|
|
|
257
288
|
/**
|
|
258
289
|
* Подключение к WebSocket серверу
|
|
290
|
+
* @returns {Promise} Promise, который резолвится при успешном подключении
|
|
259
291
|
*/
|
|
260
292
|
connect() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return;
|
|
293
|
+
// Если уже подключены - возвращаем резолвленный Promise
|
|
294
|
+
if (this.connectionState === ConnectionStates.CONNECTED) {
|
|
295
|
+
return Promise.resolve({ socketId: this.socketId });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Если уже идёт подключение - возвращаем существующий Promise
|
|
299
|
+
if (this.connectionState === ConnectionStates.CONNECTING && this.connectionPromise) {
|
|
300
|
+
return this.connectionPromise;
|
|
264
301
|
}
|
|
265
302
|
|
|
303
|
+
// Создаём новый Promise для ожидания подключения
|
|
304
|
+
this.connectionPromise = new Promise((resolve, reject) => {
|
|
305
|
+
this.connectionResolve = resolve;
|
|
306
|
+
this.connectionReject = reject;
|
|
307
|
+
});
|
|
308
|
+
|
|
266
309
|
this.connectionState = ConnectionStates.CONNECTING;
|
|
267
310
|
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
268
311
|
|
|
@@ -277,7 +320,66 @@ class PushlerClient {
|
|
|
277
320
|
} catch (error) {
|
|
278
321
|
console.error('PushlerClient: Error creating WebSocket connection:', error);
|
|
279
322
|
this.handleConnectionError(error);
|
|
323
|
+
if (this.connectionReject) {
|
|
324
|
+
this.connectionReject(error);
|
|
325
|
+
this.connectionPromise = null;
|
|
326
|
+
this.connectionResolve = null;
|
|
327
|
+
this.connectionReject = null;
|
|
328
|
+
}
|
|
280
329
|
}
|
|
330
|
+
|
|
331
|
+
return this.connectionPromise;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Ожидание установления соединения
|
|
336
|
+
* @param {number} timeout - Таймаут в мс (по умолчанию 10000)
|
|
337
|
+
* @returns {Promise} Promise с socketId
|
|
338
|
+
*/
|
|
339
|
+
waitForConnection(timeout = 10000) {
|
|
340
|
+
// Если уже подключены
|
|
341
|
+
if (this.connectionState === ConnectionStates.CONNECTED && this.socketId) {
|
|
342
|
+
return Promise.resolve({ socketId: this.socketId });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Если есть активный Promise подключения
|
|
346
|
+
if (this.connectionPromise) {
|
|
347
|
+
return Promise.race([
|
|
348
|
+
this.connectionPromise,
|
|
349
|
+
new Promise((_, reject) =>
|
|
350
|
+
setTimeout(() => reject(new Error('Connection timeout')), timeout)
|
|
351
|
+
)
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Если не подключаемся - начинаем подключение
|
|
356
|
+
if (this.connectionState === ConnectionStates.DISCONNECTED) {
|
|
357
|
+
return this.connect();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Для других состояний создаём Promise, который ждёт connected события
|
|
361
|
+
return new Promise((resolve, reject) => {
|
|
362
|
+
const timeoutId = setTimeout(() => {
|
|
363
|
+
this.off(Events.CONNECTED, onConnected);
|
|
364
|
+
this.off(Events.ERROR, onError);
|
|
365
|
+
reject(new Error('Connection timeout'));
|
|
366
|
+
}, timeout);
|
|
367
|
+
|
|
368
|
+
const onConnected = (data) => {
|
|
369
|
+
clearTimeout(timeoutId);
|
|
370
|
+
this.off(Events.ERROR, onError);
|
|
371
|
+
resolve(data);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const onError = (err) => {
|
|
375
|
+
clearTimeout(timeoutId);
|
|
376
|
+
this.off(Events.CONNECTED, onConnected);
|
|
377
|
+
reject(err);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
this.on(Events.CONNECTED, onConnected);
|
|
381
|
+
this.on(Events.ERROR, onError);
|
|
382
|
+
});
|
|
281
383
|
}
|
|
282
384
|
|
|
283
385
|
/**
|
|
@@ -329,31 +431,98 @@ class PushlerClient {
|
|
|
329
431
|
};
|
|
330
432
|
}
|
|
331
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Разбор склеенных JSON из одного WebSocket фрейма
|
|
436
|
+
* Сервер иногда отправляет несколько JSON объектов подряд в одном фрейме:
|
|
437
|
+
* {"event":"a",...}{"event":"b",...}
|
|
438
|
+
* @param {string} str - Строка с одним или несколькими JSON объектами
|
|
439
|
+
* @returns {string[]} Массив отдельных JSON строк
|
|
440
|
+
*/
|
|
441
|
+
splitConcatenatedJSON(str) {
|
|
442
|
+
const results = [];
|
|
443
|
+
let depth = 0;
|
|
444
|
+
let start = 0;
|
|
445
|
+
let inString = false;
|
|
446
|
+
let escape = false;
|
|
447
|
+
|
|
448
|
+
for (let i = 0; i < str.length; i++) {
|
|
449
|
+
const char = str[i];
|
|
450
|
+
|
|
451
|
+
// Обработка escape-символов внутри строк
|
|
452
|
+
if (escape) {
|
|
453
|
+
escape = false;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (char === '\\' && inString) {
|
|
458
|
+
escape = true;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Отслеживание строковых литералов
|
|
463
|
+
if (char === '"') {
|
|
464
|
+
inString = !inString;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Считаем скобки только вне строк
|
|
469
|
+
if (!inString) {
|
|
470
|
+
if (char === '{') {
|
|
471
|
+
depth++;
|
|
472
|
+
} else if (char === '}') {
|
|
473
|
+
depth--;
|
|
474
|
+
if (depth === 0) {
|
|
475
|
+
results.push(str.slice(start, i + 1));
|
|
476
|
+
start = i + 1;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return results;
|
|
483
|
+
}
|
|
484
|
+
|
|
332
485
|
/**
|
|
333
486
|
* Обработка входящих сообщений
|
|
334
487
|
*/
|
|
335
488
|
handleMessage(event) {
|
|
336
489
|
try {
|
|
337
|
-
|
|
490
|
+
// Пытаемся распарсить как один JSON
|
|
491
|
+
const messages = this.splitConcatenatedJSON(event.data);
|
|
338
492
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.handleAuthSuccess(message);
|
|
348
|
-
break;
|
|
349
|
-
case 'pushler:auth_error':
|
|
350
|
-
this.handleAuthError(message);
|
|
351
|
-
break;
|
|
352
|
-
default:
|
|
353
|
-
this.handleChannelMessage(message);
|
|
493
|
+
// Если несколько JSON объектов, обрабатываем каждый
|
|
494
|
+
for (const jsonStr of messages) {
|
|
495
|
+
try {
|
|
496
|
+
const message = JSON.parse(jsonStr);
|
|
497
|
+
this.processMessage(message);
|
|
498
|
+
} catch (parseError) {
|
|
499
|
+
console.error('Error parsing JSON message:', parseError, 'Raw:', jsonStr);
|
|
500
|
+
}
|
|
354
501
|
}
|
|
355
502
|
} catch (error) {
|
|
356
|
-
console.error('Error
|
|
503
|
+
console.error('Error handling WebSocket message:', error);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Обработка распарсенного сообщения
|
|
509
|
+
*/
|
|
510
|
+
processMessage(message) {
|
|
511
|
+
switch (message.event) {
|
|
512
|
+
case 'pushler:connection_established':
|
|
513
|
+
this.handleConnectionEstablished(message.data);
|
|
514
|
+
break;
|
|
515
|
+
case 'pushler:subscription_succeeded':
|
|
516
|
+
this.handleSubscriptionSucceeded(message);
|
|
517
|
+
break;
|
|
518
|
+
case 'pushler:auth_success':
|
|
519
|
+
this.handleAuthSuccess(message);
|
|
520
|
+
break;
|
|
521
|
+
case 'pushler:auth_error':
|
|
522
|
+
this.handleAuthError(message);
|
|
523
|
+
break;
|
|
524
|
+
default:
|
|
525
|
+
this.handleChannelMessage(message);
|
|
357
526
|
}
|
|
358
527
|
}
|
|
359
528
|
|
|
@@ -367,6 +536,14 @@ class PushlerClient {
|
|
|
367
536
|
|
|
368
537
|
console.log('Connection established with socket ID:', this.socketId);
|
|
369
538
|
|
|
539
|
+
// Резолвим Promise подключения
|
|
540
|
+
if (this.connectionResolve) {
|
|
541
|
+
this.connectionResolve({ socketId: this.socketId });
|
|
542
|
+
this.connectionPromise = null;
|
|
543
|
+
this.connectionResolve = null;
|
|
544
|
+
this.connectionReject = null;
|
|
545
|
+
}
|
|
546
|
+
|
|
370
547
|
// Переподписываемся на все каналы после переподключения
|
|
371
548
|
this.resubscribeAllChannels();
|
|
372
549
|
|
|
@@ -406,7 +583,13 @@ class PushlerClient {
|
|
|
406
583
|
const { channel } = message.data;
|
|
407
584
|
const channelInstance = this.channels.get(channel);
|
|
408
585
|
|
|
586
|
+
// Очищаем pending auth запрос
|
|
587
|
+
this.cancelPendingAuth(channel);
|
|
588
|
+
|
|
409
589
|
if (channelInstance) {
|
|
590
|
+
// Устанавливаем состояние subscribed
|
|
591
|
+
channelInstance.subscriptionState = SubscriptionStates.SUBSCRIBED;
|
|
592
|
+
|
|
410
593
|
// На сервере уже происходит подписка после успешной авторизации,
|
|
411
594
|
// поэтому просто помечаем канал как подписанный
|
|
412
595
|
channelInstance.handleAuthSuccess(message.data);
|
|
@@ -419,11 +602,31 @@ class PushlerClient {
|
|
|
419
602
|
* Обработка ошибки аутентификации
|
|
420
603
|
*/
|
|
421
604
|
handleAuthError(message) {
|
|
422
|
-
const { error } = message.data;
|
|
423
|
-
|
|
605
|
+
const { error, channel } = message.data;
|
|
606
|
+
const channelName = channel || null;
|
|
607
|
+
|
|
608
|
+
// Очищаем pending auth запрос
|
|
609
|
+
if (channelName) {
|
|
610
|
+
this.cancelPendingAuth(channelName);
|
|
611
|
+
|
|
612
|
+
const channelInstance = this.channels.get(channelName);
|
|
613
|
+
if (channelInstance) {
|
|
614
|
+
channelInstance.subscriptionState = SubscriptionStates.FAILED;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const originalChannelName = channelName ? this.extractChannelName(channelName) : null;
|
|
619
|
+
|
|
620
|
+
this.emit(Events.CHANNEL_AUTH_ERROR, {
|
|
621
|
+
error,
|
|
622
|
+
channel: originalChannelName,
|
|
623
|
+
socketId: this.socketId
|
|
624
|
+
});
|
|
424
625
|
this.emit(Events.ERROR, {
|
|
425
626
|
code: ErrorCodes.AUTHENTICATION_FAILED,
|
|
426
|
-
message: error
|
|
627
|
+
message: error,
|
|
628
|
+
channel: originalChannelName,
|
|
629
|
+
socketId: this.socketId
|
|
427
630
|
});
|
|
428
631
|
}
|
|
429
632
|
|
|
@@ -481,8 +684,12 @@ class PushlerClient {
|
|
|
481
684
|
// Поддерживаем оба формата имени
|
|
482
685
|
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
483
686
|
|
|
687
|
+
// Отменяем pending auth запросы для этого канала (исправление race condition)
|
|
688
|
+
this.cancelPendingAuth(fullChannelName);
|
|
689
|
+
|
|
484
690
|
const channel = this.channels.get(fullChannelName);
|
|
485
691
|
if (channel) {
|
|
692
|
+
channel.subscriptionState = SubscriptionStates.UNSUBSCRIBED;
|
|
486
693
|
channel.unsubscribe();
|
|
487
694
|
this.channels.delete(fullChannelName);
|
|
488
695
|
this.channelAliases.delete(channel.originalName || channelName);
|
|
@@ -517,32 +724,141 @@ class PushlerClient {
|
|
|
517
724
|
|
|
518
725
|
/**
|
|
519
726
|
* Аутентификация канала
|
|
727
|
+
* С поддержкой отмены при unsubscribe
|
|
520
728
|
*/
|
|
521
729
|
async authenticateChannel(channel) {
|
|
730
|
+
// Создаём AbortController для возможности отмены
|
|
731
|
+
const abortController = new AbortController();
|
|
732
|
+
const channelName = channel.name;
|
|
733
|
+
|
|
734
|
+
// Сохраняем для возможности отмены при unsubscribe
|
|
735
|
+
this.pendingAuthRequests.set(channelName, {
|
|
736
|
+
controller: abortController,
|
|
737
|
+
timeoutId: null
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Устанавливаем состояние канала
|
|
741
|
+
channel.subscriptionState = SubscriptionStates.PENDING_AUTH;
|
|
742
|
+
|
|
522
743
|
try {
|
|
744
|
+
// Проверка отмены перед началом
|
|
745
|
+
if (abortController.signal.aborted) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
523
749
|
const authData = await this.getChannelAuthData(channel);
|
|
524
750
|
|
|
751
|
+
// Проверка отмены после получения данных
|
|
752
|
+
if (abortController.signal.aborted) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
525
756
|
this.sendMessage({
|
|
526
757
|
event: 'pushler:auth',
|
|
527
758
|
data: authData
|
|
528
759
|
});
|
|
529
760
|
|
|
530
761
|
// Устанавливаем таймаут аутентификации
|
|
531
|
-
|
|
762
|
+
const timeoutId = setTimeout(() => {
|
|
763
|
+
// Не отправляем ошибку, если запрос уже отменён
|
|
764
|
+
if (abortController.signal.aborted) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Очищаем pending запрос
|
|
769
|
+
this.pendingAuthRequests.delete(channelName);
|
|
770
|
+
|
|
771
|
+
// Устанавливаем состояние failed
|
|
772
|
+
channel.subscriptionState = SubscriptionStates.FAILED;
|
|
773
|
+
|
|
532
774
|
this.emit(Events.ERROR, {
|
|
533
775
|
code: ErrorCodes.AUTHENTICATION_TIMEOUT,
|
|
534
|
-
message: 'Authentication timeout'
|
|
776
|
+
message: 'Authentication timeout',
|
|
777
|
+
channel: channel.originalName || this.extractChannelName(channelName),
|
|
778
|
+
socketId: this.socketId
|
|
535
779
|
});
|
|
536
780
|
}, 10000);
|
|
537
781
|
|
|
782
|
+
// Сохраняем timeoutId для возможности очистки
|
|
783
|
+
const pending = this.pendingAuthRequests.get(channelName);
|
|
784
|
+
if (pending) {
|
|
785
|
+
pending.timeoutId = timeoutId;
|
|
786
|
+
}
|
|
787
|
+
|
|
538
788
|
} catch (error) {
|
|
789
|
+
// Не отправляем ошибку, если запрос уже отменён
|
|
790
|
+
if (abortController.signal.aborted) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Очищаем pending запрос
|
|
795
|
+
this.pendingAuthRequests.delete(channelName);
|
|
796
|
+
|
|
797
|
+
// Устанавливаем состояние failed
|
|
798
|
+
channel.subscriptionState = SubscriptionStates.FAILED;
|
|
799
|
+
|
|
539
800
|
this.emit(Events.ERROR, {
|
|
540
801
|
code: ErrorCodes.AUTHENTICATION_FAILED,
|
|
541
|
-
message: error.message
|
|
802
|
+
message: error.message,
|
|
803
|
+
channel: channel.originalName || this.extractChannelName(channelName),
|
|
804
|
+
socketId: this.socketId
|
|
542
805
|
});
|
|
543
806
|
}
|
|
544
807
|
}
|
|
545
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Отмена pending auth запроса для канала
|
|
811
|
+
*/
|
|
812
|
+
cancelPendingAuth(channelName) {
|
|
813
|
+
const pending = this.pendingAuthRequests.get(channelName);
|
|
814
|
+
if (pending) {
|
|
815
|
+
// Отменяем запрос
|
|
816
|
+
pending.controller.abort();
|
|
817
|
+
|
|
818
|
+
// Очищаем таймаут
|
|
819
|
+
if (pending.timeoutId) {
|
|
820
|
+
clearTimeout(pending.timeoutId);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Удаляем из карты
|
|
824
|
+
this.pendingAuthRequests.delete(channelName);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Получение заголовков для авторизации
|
|
830
|
+
* Поддерживает как синхронные, так и асинхронные функции getAuthHeaders
|
|
831
|
+
* @returns {Promise<Object>} Заголовки для авторизации
|
|
832
|
+
*/
|
|
833
|
+
async getAuthRequestHeaders() {
|
|
834
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
835
|
+
|
|
836
|
+
// Динамические заголовки имеют приоритет
|
|
837
|
+
if (typeof this.getAuthHeaders === 'function') {
|
|
838
|
+
// Поддержка асинхронных функций (AsyncStorage, SecureStore, Keychain)
|
|
839
|
+
const dynamicHeaders = await Promise.resolve(this.getAuthHeaders());
|
|
840
|
+
Object.assign(headers, dynamicHeaders);
|
|
841
|
+
} else if (this.authHeaders) {
|
|
842
|
+
// Статические заголовки
|
|
843
|
+
Object.assign(headers, this.authHeaders);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return headers;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Установка токена авторизации
|
|
851
|
+
* Удобный метод для установки Bearer токена без создания функции
|
|
852
|
+
* @param {string} token - JWT токен
|
|
853
|
+
*/
|
|
854
|
+
setAuthToken(token) {
|
|
855
|
+
if (token) {
|
|
856
|
+
this.authHeaders = { 'Authorization': `Bearer ${token}` };
|
|
857
|
+
} else {
|
|
858
|
+
this.authHeaders = null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
546
862
|
/**
|
|
547
863
|
* Получение подписи с бэкенда
|
|
548
864
|
*/
|
|
@@ -552,11 +868,16 @@ class PushlerClient {
|
|
|
552
868
|
? this.authEndpoint
|
|
553
869
|
: `${this.apiUrl}${this.authEndpoint}`;
|
|
554
870
|
|
|
871
|
+
// Асинхронное получение заголовков (поддержка AsyncStorage/SecureStore)
|
|
872
|
+
const headers = await this.getAuthRequestHeaders();
|
|
873
|
+
const hasAuthHeaders = this.authHeaders || this.getAuthHeaders;
|
|
874
|
+
|
|
555
875
|
const response = await fetch(authUrl, {
|
|
556
876
|
method: 'POST',
|
|
557
|
-
headers
|
|
558
|
-
|
|
559
|
-
|
|
877
|
+
headers,
|
|
878
|
+
// Если есть authHeaders, не используем credentials (JWT режим)
|
|
879
|
+
// Если нет — используем cookies
|
|
880
|
+
...(hasAuthHeaders ? {} : { credentials: 'include' }),
|
|
560
881
|
body: JSON.stringify({
|
|
561
882
|
channel_name: channelName,
|
|
562
883
|
socket_id: socketId,
|
|
@@ -566,7 +887,7 @@ class PushlerClient {
|
|
|
566
887
|
});
|
|
567
888
|
|
|
568
889
|
if (!response.ok) {
|
|
569
|
-
const error = await response.json();
|
|
890
|
+
const error = await response.json().catch(() => ({ error: 'Auth request failed' }));
|
|
570
891
|
throw new Error(error.error || 'Failed to get channel signature');
|
|
571
892
|
}
|
|
572
893
|
|
|
@@ -699,6 +1020,14 @@ class PushlerClient {
|
|
|
699
1020
|
|
|
700
1021
|
console.error('PushlerClient connection error:', fullErrorMessage);
|
|
701
1022
|
|
|
1023
|
+
// Реджектим Promise подключения
|
|
1024
|
+
if (this.connectionReject) {
|
|
1025
|
+
this.connectionReject(new Error(fullErrorMessage));
|
|
1026
|
+
this.connectionPromise = null;
|
|
1027
|
+
this.connectionResolve = null;
|
|
1028
|
+
this.connectionReject = null;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
702
1031
|
this.emit(Events.ERROR, {
|
|
703
1032
|
code: ErrorCodes.CONNECTION_FAILED,
|
|
704
1033
|
message: fullErrorMessage,
|
|
@@ -797,6 +1126,69 @@ class PushlerClient {
|
|
|
797
1126
|
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
798
1127
|
return this.channels.get(fullChannelName);
|
|
799
1128
|
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Проверка подписки на канал
|
|
1132
|
+
* @param {string} channelName - Имя канала
|
|
1133
|
+
* @returns {boolean} true если подписан
|
|
1134
|
+
*/
|
|
1135
|
+
isSubscribed(channelName) {
|
|
1136
|
+
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
1137
|
+
const channel = this.channels.get(fullChannelName);
|
|
1138
|
+
return channel ? channel.subscribed : false;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Получение состояния подписки на канал
|
|
1143
|
+
* @param {string} channelName - Имя канала
|
|
1144
|
+
* @returns {string|null} Состояние подписки или null если канал не найден
|
|
1145
|
+
*/
|
|
1146
|
+
getSubscriptionState(channelName) {
|
|
1147
|
+
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
1148
|
+
const channel = this.channels.get(fullChannelName);
|
|
1149
|
+
|
|
1150
|
+
if (!channel) {
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return channel.subscriptionState || (channel.subscribed ? SubscriptionStates.SUBSCRIBED : SubscriptionStates.PENDING);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Получение списка подписанных каналов
|
|
1159
|
+
* @returns {string[]} Массив имён подписанных каналов
|
|
1160
|
+
*/
|
|
1161
|
+
getSubscribedChannels() {
|
|
1162
|
+
const subscribedChannels = [];
|
|
1163
|
+
|
|
1164
|
+
for (const [fullName, channel] of this.channels.entries()) {
|
|
1165
|
+
if (channel.subscribed) {
|
|
1166
|
+
subscribedChannels.push(channel.originalName || this.extractChannelName(fullName));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return subscribedChannels;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Получение всех каналов с их состояниями
|
|
1175
|
+
* @returns {Object[]} Массив объектов с информацией о каналах
|
|
1176
|
+
*/
|
|
1177
|
+
getAllChannelsInfo() {
|
|
1178
|
+
const channelsInfo = [];
|
|
1179
|
+
|
|
1180
|
+
for (const [fullName, channel] of this.channels.entries()) {
|
|
1181
|
+
channelsInfo.push({
|
|
1182
|
+
name: channel.originalName || this.extractChannelName(fullName),
|
|
1183
|
+
fullName: fullName,
|
|
1184
|
+
subscribed: channel.subscribed,
|
|
1185
|
+
state: channel.subscriptionState || (channel.subscribed ? SubscriptionStates.SUBSCRIBED : SubscriptionStates.PENDING),
|
|
1186
|
+
type: channel.getType()
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return channelsInfo;
|
|
1191
|
+
}
|
|
800
1192
|
}
|
|
801
1193
|
|
|
802
1194
|
var PushlerClient$1 = PushlerClient;
|
|
@@ -806,11 +1198,12 @@ var PushlerClient$1 = PushlerClient;
|
|
|
806
1198
|
* Используется на сервере Node.js для отправки событий в каналы
|
|
807
1199
|
*/
|
|
808
1200
|
class PushlerServer {
|
|
1201
|
+
static API_URL = 'https://api.pushler.ru';
|
|
1202
|
+
|
|
809
1203
|
/**
|
|
810
1204
|
* @param {Object} options - Параметры клиента
|
|
811
1205
|
* @param {string} options.appKey - Ключ приложения (key_xxx)
|
|
812
1206
|
* @param {string} options.appSecret - Секрет приложения (secret_xxx)
|
|
813
|
-
* @param {string} [options.apiUrl='http://localhost:8000/api'] - URL API сервера
|
|
814
1207
|
* @param {number} [options.timeout=10000] - Таймаут запроса в мс
|
|
815
1208
|
*/
|
|
816
1209
|
constructor(options = {}) {
|
|
@@ -823,7 +1216,6 @@ class PushlerServer {
|
|
|
823
1216
|
|
|
824
1217
|
this.appKey = options.appKey;
|
|
825
1218
|
this.appSecret = options.appSecret;
|
|
826
|
-
this.apiUrl = (options.apiUrl || 'http://localhost:8000/api').replace(/\/$/, '');
|
|
827
1219
|
this.timeout = options.timeout || 10000;
|
|
828
1220
|
}
|
|
829
1221
|
|
|
@@ -1107,7 +1499,7 @@ class PushlerServer {
|
|
|
1107
1499
|
* @returns {Promise<Object>} Результат запроса
|
|
1108
1500
|
*/
|
|
1109
1501
|
async makeSignedRequest(method, endpoint, data = null) {
|
|
1110
|
-
const url =
|
|
1502
|
+
const url = PushlerServer.API_URL + endpoint;
|
|
1111
1503
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
1112
1504
|
const body = data ? JSON.stringify(data) : '';
|
|
1113
1505
|
const signature = this.generateApiSignature(body, timestamp);
|
|
@@ -1155,7 +1547,7 @@ class PushlerServer {
|
|
|
1155
1547
|
* @returns {Promise<Object>} Результат запроса
|
|
1156
1548
|
*/
|
|
1157
1549
|
async makeHttpRequest(method, endpoint, data = null) {
|
|
1158
|
-
const url =
|
|
1550
|
+
const url = PushlerServer.API_URL + endpoint;
|
|
1159
1551
|
|
|
1160
1552
|
const headers = {
|
|
1161
1553
|
'Content-Type': 'application/json',
|
|
@@ -1196,7 +1588,7 @@ class PushlerServer {
|
|
|
1196
1588
|
* @returns {string}
|
|
1197
1589
|
*/
|
|
1198
1590
|
getApiUrl() {
|
|
1199
|
-
return
|
|
1591
|
+
return PushlerServer.API_URL;
|
|
1200
1592
|
}
|
|
1201
1593
|
}
|
|
1202
1594
|
|
|
@@ -1231,15 +1623,21 @@ const Pushler = {
|
|
|
1231
1623
|
Channel: Channel$1,
|
|
1232
1624
|
ChannelTypes,
|
|
1233
1625
|
ConnectionStates,
|
|
1626
|
+
SubscriptionStates,
|
|
1627
|
+
ErrorCodes,
|
|
1628
|
+
Events,
|
|
1234
1629
|
generateSocketId
|
|
1235
1630
|
};
|
|
1236
1631
|
|
|
1237
1632
|
exports.Channel = Channel$1;
|
|
1238
1633
|
exports.ChannelTypes = ChannelTypes;
|
|
1239
1634
|
exports.ConnectionStates = ConnectionStates;
|
|
1635
|
+
exports.ErrorCodes = ErrorCodes;
|
|
1636
|
+
exports.Events = Events;
|
|
1240
1637
|
exports.Pushler = Pushler;
|
|
1241
1638
|
exports.PushlerClient = PushlerClient$1;
|
|
1242
1639
|
exports.PushlerServer = PushlerServer$1;
|
|
1640
|
+
exports.SubscriptionStates = SubscriptionStates;
|
|
1243
1641
|
exports.default = Pushler;
|
|
1244
1642
|
exports.generateSocketId = generateSocketId;
|
|
1245
1643
|
//# sourceMappingURL=pushler-ru.cjs.map
|