@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.esm.js
CHANGED
|
@@ -20,6 +20,18 @@ const ConnectionStates = {
|
|
|
20
20
|
FAILED: 'failed'
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Состояния подписки на канал
|
|
25
|
+
*/
|
|
26
|
+
const SubscriptionStates = {
|
|
27
|
+
PENDING: 'pending',
|
|
28
|
+
PENDING_AUTH: 'pending_auth',
|
|
29
|
+
SUBSCRIBING: 'subscribing',
|
|
30
|
+
SUBSCRIBED: 'subscribed',
|
|
31
|
+
FAILED: 'failed',
|
|
32
|
+
UNSUBSCRIBED: 'unsubscribed'
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
/**
|
|
24
36
|
* События WebSocket
|
|
25
37
|
*/
|
|
@@ -61,6 +73,7 @@ class Channel {
|
|
|
61
73
|
this.name = name;
|
|
62
74
|
this.options = options;
|
|
63
75
|
this.subscribed = false;
|
|
76
|
+
this.subscriptionState = SubscriptionStates.PENDING;
|
|
64
77
|
this.eventListeners = new Map();
|
|
65
78
|
this.presenceData = null;
|
|
66
79
|
}
|
|
@@ -108,6 +121,7 @@ class Channel {
|
|
|
108
121
|
*/
|
|
109
122
|
handleAuthSuccess(data) {
|
|
110
123
|
this.subscribed = true;
|
|
124
|
+
this.subscriptionState = SubscriptionStates.SUBSCRIBED;
|
|
111
125
|
this.emit(Events.CHANNEL_SUBSCRIBED, data);
|
|
112
126
|
|
|
113
127
|
// Для presence каналов сохраняем данные пользователя
|
|
@@ -135,6 +149,7 @@ class Channel {
|
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
this.subscribed = false;
|
|
152
|
+
this.subscriptionState = SubscriptionStates.UNSUBSCRIBED;
|
|
138
153
|
this.emit(Events.CHANNEL_UNSUBSCRIBED);
|
|
139
154
|
}
|
|
140
155
|
|
|
@@ -145,6 +160,14 @@ class Channel {
|
|
|
145
160
|
return this.subscribed;
|
|
146
161
|
}
|
|
147
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Получение состояния подписки
|
|
165
|
+
* @returns {string} Состояние подписки
|
|
166
|
+
*/
|
|
167
|
+
getSubscriptionState() {
|
|
168
|
+
return this.subscriptionState;
|
|
169
|
+
}
|
|
170
|
+
|
|
148
171
|
/**
|
|
149
172
|
* Получение данных присутствия (для presence каналов)
|
|
150
173
|
*/
|
|
@@ -203,6 +226,8 @@ class PushlerClient {
|
|
|
203
226
|
this.wsUrl = options.wsUrl || `wss://ws.pushler.ru/app/${this.appKey}`;
|
|
204
227
|
this.apiUrl = options.apiUrl;
|
|
205
228
|
this.authEndpoint = options.authEndpoint || '/pushler/auth'; // Путь для авторизации каналов
|
|
229
|
+
this.authHeaders = options.authHeaders || null; // Кастомные заголовки для авторизации
|
|
230
|
+
this.getAuthHeaders = options.getAuthHeaders || null; // Функция для динамических заголовков
|
|
206
231
|
this.autoConnect = options.autoConnect !== false;
|
|
207
232
|
this.reconnectDelay = options.reconnectDelay || 1000;
|
|
208
233
|
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
|
@@ -213,10 +238,16 @@ class PushlerClient {
|
|
|
213
238
|
this.channels = new Map(); // Хранит каналы по полному имени (с appKey)
|
|
214
239
|
this.channelAliases = new Map(); // Маппинг оригинальное имя -> полное имя
|
|
215
240
|
this.eventListeners = new Map();
|
|
241
|
+
this.pendingAuthRequests = new Map(); // Отслеживание pending auth запросов для отмены при unsubscribe
|
|
216
242
|
this.reconnectAttempts = 0;
|
|
217
243
|
this.reconnectTimer = null;
|
|
218
244
|
this.authTimeout = null;
|
|
219
245
|
|
|
246
|
+
// Promise для ожидания подключения
|
|
247
|
+
this.connectionPromise = null;
|
|
248
|
+
this.connectionResolve = null;
|
|
249
|
+
this.connectionReject = null;
|
|
250
|
+
|
|
220
251
|
// Автоматическое подключение
|
|
221
252
|
if (this.autoConnect) {
|
|
222
253
|
this.connect();
|
|
@@ -252,13 +283,25 @@ class PushlerClient {
|
|
|
252
283
|
|
|
253
284
|
/**
|
|
254
285
|
* Подключение к WebSocket серверу
|
|
286
|
+
* @returns {Promise} Promise, который резолвится при успешном подключении
|
|
255
287
|
*/
|
|
256
288
|
connect() {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return;
|
|
289
|
+
// Если уже подключены - возвращаем резолвленный Promise
|
|
290
|
+
if (this.connectionState === ConnectionStates.CONNECTED) {
|
|
291
|
+
return Promise.resolve({ socketId: this.socketId });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Если уже идёт подключение - возвращаем существующий Promise
|
|
295
|
+
if (this.connectionState === ConnectionStates.CONNECTING && this.connectionPromise) {
|
|
296
|
+
return this.connectionPromise;
|
|
260
297
|
}
|
|
261
298
|
|
|
299
|
+
// Создаём новый Promise для ожидания подключения
|
|
300
|
+
this.connectionPromise = new Promise((resolve, reject) => {
|
|
301
|
+
this.connectionResolve = resolve;
|
|
302
|
+
this.connectionReject = reject;
|
|
303
|
+
});
|
|
304
|
+
|
|
262
305
|
this.connectionState = ConnectionStates.CONNECTING;
|
|
263
306
|
this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
|
|
264
307
|
|
|
@@ -273,7 +316,66 @@ class PushlerClient {
|
|
|
273
316
|
} catch (error) {
|
|
274
317
|
console.error('PushlerClient: Error creating WebSocket connection:', error);
|
|
275
318
|
this.handleConnectionError(error);
|
|
319
|
+
if (this.connectionReject) {
|
|
320
|
+
this.connectionReject(error);
|
|
321
|
+
this.connectionPromise = null;
|
|
322
|
+
this.connectionResolve = null;
|
|
323
|
+
this.connectionReject = null;
|
|
324
|
+
}
|
|
276
325
|
}
|
|
326
|
+
|
|
327
|
+
return this.connectionPromise;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Ожидание установления соединения
|
|
332
|
+
* @param {number} timeout - Таймаут в мс (по умолчанию 10000)
|
|
333
|
+
* @returns {Promise} Promise с socketId
|
|
334
|
+
*/
|
|
335
|
+
waitForConnection(timeout = 10000) {
|
|
336
|
+
// Если уже подключены
|
|
337
|
+
if (this.connectionState === ConnectionStates.CONNECTED && this.socketId) {
|
|
338
|
+
return Promise.resolve({ socketId: this.socketId });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Если есть активный Promise подключения
|
|
342
|
+
if (this.connectionPromise) {
|
|
343
|
+
return Promise.race([
|
|
344
|
+
this.connectionPromise,
|
|
345
|
+
new Promise((_, reject) =>
|
|
346
|
+
setTimeout(() => reject(new Error('Connection timeout')), timeout)
|
|
347
|
+
)
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Если не подключаемся - начинаем подключение
|
|
352
|
+
if (this.connectionState === ConnectionStates.DISCONNECTED) {
|
|
353
|
+
return this.connect();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Для других состояний создаём Promise, который ждёт connected события
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
const timeoutId = setTimeout(() => {
|
|
359
|
+
this.off(Events.CONNECTED, onConnected);
|
|
360
|
+
this.off(Events.ERROR, onError);
|
|
361
|
+
reject(new Error('Connection timeout'));
|
|
362
|
+
}, timeout);
|
|
363
|
+
|
|
364
|
+
const onConnected = (data) => {
|
|
365
|
+
clearTimeout(timeoutId);
|
|
366
|
+
this.off(Events.ERROR, onError);
|
|
367
|
+
resolve(data);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const onError = (err) => {
|
|
371
|
+
clearTimeout(timeoutId);
|
|
372
|
+
this.off(Events.CONNECTED, onConnected);
|
|
373
|
+
reject(err);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
this.on(Events.CONNECTED, onConnected);
|
|
377
|
+
this.on(Events.ERROR, onError);
|
|
378
|
+
});
|
|
277
379
|
}
|
|
278
380
|
|
|
279
381
|
/**
|
|
@@ -325,31 +427,98 @@ class PushlerClient {
|
|
|
325
427
|
};
|
|
326
428
|
}
|
|
327
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Разбор склеенных JSON из одного WebSocket фрейма
|
|
432
|
+
* Сервер иногда отправляет несколько JSON объектов подряд в одном фрейме:
|
|
433
|
+
* {"event":"a",...}{"event":"b",...}
|
|
434
|
+
* @param {string} str - Строка с одним или несколькими JSON объектами
|
|
435
|
+
* @returns {string[]} Массив отдельных JSON строк
|
|
436
|
+
*/
|
|
437
|
+
splitConcatenatedJSON(str) {
|
|
438
|
+
const results = [];
|
|
439
|
+
let depth = 0;
|
|
440
|
+
let start = 0;
|
|
441
|
+
let inString = false;
|
|
442
|
+
let escape = false;
|
|
443
|
+
|
|
444
|
+
for (let i = 0; i < str.length; i++) {
|
|
445
|
+
const char = str[i];
|
|
446
|
+
|
|
447
|
+
// Обработка escape-символов внутри строк
|
|
448
|
+
if (escape) {
|
|
449
|
+
escape = false;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (char === '\\' && inString) {
|
|
454
|
+
escape = true;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Отслеживание строковых литералов
|
|
459
|
+
if (char === '"') {
|
|
460
|
+
inString = !inString;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Считаем скобки только вне строк
|
|
465
|
+
if (!inString) {
|
|
466
|
+
if (char === '{') {
|
|
467
|
+
depth++;
|
|
468
|
+
} else if (char === '}') {
|
|
469
|
+
depth--;
|
|
470
|
+
if (depth === 0) {
|
|
471
|
+
results.push(str.slice(start, i + 1));
|
|
472
|
+
start = i + 1;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return results;
|
|
479
|
+
}
|
|
480
|
+
|
|
328
481
|
/**
|
|
329
482
|
* Обработка входящих сообщений
|
|
330
483
|
*/
|
|
331
484
|
handleMessage(event) {
|
|
332
485
|
try {
|
|
333
|
-
|
|
486
|
+
// Пытаемся распарсить как один JSON
|
|
487
|
+
const messages = this.splitConcatenatedJSON(event.data);
|
|
334
488
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.handleAuthSuccess(message);
|
|
344
|
-
break;
|
|
345
|
-
case 'pushler:auth_error':
|
|
346
|
-
this.handleAuthError(message);
|
|
347
|
-
break;
|
|
348
|
-
default:
|
|
349
|
-
this.handleChannelMessage(message);
|
|
489
|
+
// Если несколько JSON объектов, обрабатываем каждый
|
|
490
|
+
for (const jsonStr of messages) {
|
|
491
|
+
try {
|
|
492
|
+
const message = JSON.parse(jsonStr);
|
|
493
|
+
this.processMessage(message);
|
|
494
|
+
} catch (parseError) {
|
|
495
|
+
console.error('Error parsing JSON message:', parseError, 'Raw:', jsonStr);
|
|
496
|
+
}
|
|
350
497
|
}
|
|
351
498
|
} catch (error) {
|
|
352
|
-
console.error('Error
|
|
499
|
+
console.error('Error handling WebSocket message:', error);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Обработка распарсенного сообщения
|
|
505
|
+
*/
|
|
506
|
+
processMessage(message) {
|
|
507
|
+
switch (message.event) {
|
|
508
|
+
case 'pushler:connection_established':
|
|
509
|
+
this.handleConnectionEstablished(message.data);
|
|
510
|
+
break;
|
|
511
|
+
case 'pushler:subscription_succeeded':
|
|
512
|
+
this.handleSubscriptionSucceeded(message);
|
|
513
|
+
break;
|
|
514
|
+
case 'pushler:auth_success':
|
|
515
|
+
this.handleAuthSuccess(message);
|
|
516
|
+
break;
|
|
517
|
+
case 'pushler:auth_error':
|
|
518
|
+
this.handleAuthError(message);
|
|
519
|
+
break;
|
|
520
|
+
default:
|
|
521
|
+
this.handleChannelMessage(message);
|
|
353
522
|
}
|
|
354
523
|
}
|
|
355
524
|
|
|
@@ -363,6 +532,14 @@ class PushlerClient {
|
|
|
363
532
|
|
|
364
533
|
console.log('Connection established with socket ID:', this.socketId);
|
|
365
534
|
|
|
535
|
+
// Резолвим Promise подключения
|
|
536
|
+
if (this.connectionResolve) {
|
|
537
|
+
this.connectionResolve({ socketId: this.socketId });
|
|
538
|
+
this.connectionPromise = null;
|
|
539
|
+
this.connectionResolve = null;
|
|
540
|
+
this.connectionReject = null;
|
|
541
|
+
}
|
|
542
|
+
|
|
366
543
|
// Переподписываемся на все каналы после переподключения
|
|
367
544
|
this.resubscribeAllChannels();
|
|
368
545
|
|
|
@@ -402,7 +579,13 @@ class PushlerClient {
|
|
|
402
579
|
const { channel } = message.data;
|
|
403
580
|
const channelInstance = this.channels.get(channel);
|
|
404
581
|
|
|
582
|
+
// Очищаем pending auth запрос
|
|
583
|
+
this.cancelPendingAuth(channel);
|
|
584
|
+
|
|
405
585
|
if (channelInstance) {
|
|
586
|
+
// Устанавливаем состояние subscribed
|
|
587
|
+
channelInstance.subscriptionState = SubscriptionStates.SUBSCRIBED;
|
|
588
|
+
|
|
406
589
|
// На сервере уже происходит подписка после успешной авторизации,
|
|
407
590
|
// поэтому просто помечаем канал как подписанный
|
|
408
591
|
channelInstance.handleAuthSuccess(message.data);
|
|
@@ -415,11 +598,31 @@ class PushlerClient {
|
|
|
415
598
|
* Обработка ошибки аутентификации
|
|
416
599
|
*/
|
|
417
600
|
handleAuthError(message) {
|
|
418
|
-
const { error } = message.data;
|
|
419
|
-
|
|
601
|
+
const { error, channel } = message.data;
|
|
602
|
+
const channelName = channel || null;
|
|
603
|
+
|
|
604
|
+
// Очищаем pending auth запрос
|
|
605
|
+
if (channelName) {
|
|
606
|
+
this.cancelPendingAuth(channelName);
|
|
607
|
+
|
|
608
|
+
const channelInstance = this.channels.get(channelName);
|
|
609
|
+
if (channelInstance) {
|
|
610
|
+
channelInstance.subscriptionState = SubscriptionStates.FAILED;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const originalChannelName = channelName ? this.extractChannelName(channelName) : null;
|
|
615
|
+
|
|
616
|
+
this.emit(Events.CHANNEL_AUTH_ERROR, {
|
|
617
|
+
error,
|
|
618
|
+
channel: originalChannelName,
|
|
619
|
+
socketId: this.socketId
|
|
620
|
+
});
|
|
420
621
|
this.emit(Events.ERROR, {
|
|
421
622
|
code: ErrorCodes.AUTHENTICATION_FAILED,
|
|
422
|
-
message: error
|
|
623
|
+
message: error,
|
|
624
|
+
channel: originalChannelName,
|
|
625
|
+
socketId: this.socketId
|
|
423
626
|
});
|
|
424
627
|
}
|
|
425
628
|
|
|
@@ -477,8 +680,12 @@ class PushlerClient {
|
|
|
477
680
|
// Поддерживаем оба формата имени
|
|
478
681
|
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
479
682
|
|
|
683
|
+
// Отменяем pending auth запросы для этого канала (исправление race condition)
|
|
684
|
+
this.cancelPendingAuth(fullChannelName);
|
|
685
|
+
|
|
480
686
|
const channel = this.channels.get(fullChannelName);
|
|
481
687
|
if (channel) {
|
|
688
|
+
channel.subscriptionState = SubscriptionStates.UNSUBSCRIBED;
|
|
482
689
|
channel.unsubscribe();
|
|
483
690
|
this.channels.delete(fullChannelName);
|
|
484
691
|
this.channelAliases.delete(channel.originalName || channelName);
|
|
@@ -513,32 +720,141 @@ class PushlerClient {
|
|
|
513
720
|
|
|
514
721
|
/**
|
|
515
722
|
* Аутентификация канала
|
|
723
|
+
* С поддержкой отмены при unsubscribe
|
|
516
724
|
*/
|
|
517
725
|
async authenticateChannel(channel) {
|
|
726
|
+
// Создаём AbortController для возможности отмены
|
|
727
|
+
const abortController = new AbortController();
|
|
728
|
+
const channelName = channel.name;
|
|
729
|
+
|
|
730
|
+
// Сохраняем для возможности отмены при unsubscribe
|
|
731
|
+
this.pendingAuthRequests.set(channelName, {
|
|
732
|
+
controller: abortController,
|
|
733
|
+
timeoutId: null
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Устанавливаем состояние канала
|
|
737
|
+
channel.subscriptionState = SubscriptionStates.PENDING_AUTH;
|
|
738
|
+
|
|
518
739
|
try {
|
|
740
|
+
// Проверка отмены перед началом
|
|
741
|
+
if (abortController.signal.aborted) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
519
745
|
const authData = await this.getChannelAuthData(channel);
|
|
520
746
|
|
|
747
|
+
// Проверка отмены после получения данных
|
|
748
|
+
if (abortController.signal.aborted) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
521
752
|
this.sendMessage({
|
|
522
753
|
event: 'pushler:auth',
|
|
523
754
|
data: authData
|
|
524
755
|
});
|
|
525
756
|
|
|
526
757
|
// Устанавливаем таймаут аутентификации
|
|
527
|
-
|
|
758
|
+
const timeoutId = setTimeout(() => {
|
|
759
|
+
// Не отправляем ошибку, если запрос уже отменён
|
|
760
|
+
if (abortController.signal.aborted) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Очищаем pending запрос
|
|
765
|
+
this.pendingAuthRequests.delete(channelName);
|
|
766
|
+
|
|
767
|
+
// Устанавливаем состояние failed
|
|
768
|
+
channel.subscriptionState = SubscriptionStates.FAILED;
|
|
769
|
+
|
|
528
770
|
this.emit(Events.ERROR, {
|
|
529
771
|
code: ErrorCodes.AUTHENTICATION_TIMEOUT,
|
|
530
|
-
message: 'Authentication timeout'
|
|
772
|
+
message: 'Authentication timeout',
|
|
773
|
+
channel: channel.originalName || this.extractChannelName(channelName),
|
|
774
|
+
socketId: this.socketId
|
|
531
775
|
});
|
|
532
776
|
}, 10000);
|
|
533
777
|
|
|
778
|
+
// Сохраняем timeoutId для возможности очистки
|
|
779
|
+
const pending = this.pendingAuthRequests.get(channelName);
|
|
780
|
+
if (pending) {
|
|
781
|
+
pending.timeoutId = timeoutId;
|
|
782
|
+
}
|
|
783
|
+
|
|
534
784
|
} catch (error) {
|
|
785
|
+
// Не отправляем ошибку, если запрос уже отменён
|
|
786
|
+
if (abortController.signal.aborted) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Очищаем pending запрос
|
|
791
|
+
this.pendingAuthRequests.delete(channelName);
|
|
792
|
+
|
|
793
|
+
// Устанавливаем состояние failed
|
|
794
|
+
channel.subscriptionState = SubscriptionStates.FAILED;
|
|
795
|
+
|
|
535
796
|
this.emit(Events.ERROR, {
|
|
536
797
|
code: ErrorCodes.AUTHENTICATION_FAILED,
|
|
537
|
-
message: error.message
|
|
798
|
+
message: error.message,
|
|
799
|
+
channel: channel.originalName || this.extractChannelName(channelName),
|
|
800
|
+
socketId: this.socketId
|
|
538
801
|
});
|
|
539
802
|
}
|
|
540
803
|
}
|
|
541
804
|
|
|
805
|
+
/**
|
|
806
|
+
* Отмена pending auth запроса для канала
|
|
807
|
+
*/
|
|
808
|
+
cancelPendingAuth(channelName) {
|
|
809
|
+
const pending = this.pendingAuthRequests.get(channelName);
|
|
810
|
+
if (pending) {
|
|
811
|
+
// Отменяем запрос
|
|
812
|
+
pending.controller.abort();
|
|
813
|
+
|
|
814
|
+
// Очищаем таймаут
|
|
815
|
+
if (pending.timeoutId) {
|
|
816
|
+
clearTimeout(pending.timeoutId);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Удаляем из карты
|
|
820
|
+
this.pendingAuthRequests.delete(channelName);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Получение заголовков для авторизации
|
|
826
|
+
* Поддерживает как синхронные, так и асинхронные функции getAuthHeaders
|
|
827
|
+
* @returns {Promise<Object>} Заголовки для авторизации
|
|
828
|
+
*/
|
|
829
|
+
async getAuthRequestHeaders() {
|
|
830
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
831
|
+
|
|
832
|
+
// Динамические заголовки имеют приоритет
|
|
833
|
+
if (typeof this.getAuthHeaders === 'function') {
|
|
834
|
+
// Поддержка асинхронных функций (AsyncStorage, SecureStore, Keychain)
|
|
835
|
+
const dynamicHeaders = await Promise.resolve(this.getAuthHeaders());
|
|
836
|
+
Object.assign(headers, dynamicHeaders);
|
|
837
|
+
} else if (this.authHeaders) {
|
|
838
|
+
// Статические заголовки
|
|
839
|
+
Object.assign(headers, this.authHeaders);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return headers;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Установка токена авторизации
|
|
847
|
+
* Удобный метод для установки Bearer токена без создания функции
|
|
848
|
+
* @param {string} token - JWT токен
|
|
849
|
+
*/
|
|
850
|
+
setAuthToken(token) {
|
|
851
|
+
if (token) {
|
|
852
|
+
this.authHeaders = { 'Authorization': `Bearer ${token}` };
|
|
853
|
+
} else {
|
|
854
|
+
this.authHeaders = null;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
542
858
|
/**
|
|
543
859
|
* Получение подписи с бэкенда
|
|
544
860
|
*/
|
|
@@ -548,11 +864,16 @@ class PushlerClient {
|
|
|
548
864
|
? this.authEndpoint
|
|
549
865
|
: `${this.apiUrl}${this.authEndpoint}`;
|
|
550
866
|
|
|
867
|
+
// Асинхронное получение заголовков (поддержка AsyncStorage/SecureStore)
|
|
868
|
+
const headers = await this.getAuthRequestHeaders();
|
|
869
|
+
const hasAuthHeaders = this.authHeaders || this.getAuthHeaders;
|
|
870
|
+
|
|
551
871
|
const response = await fetch(authUrl, {
|
|
552
872
|
method: 'POST',
|
|
553
|
-
headers
|
|
554
|
-
|
|
555
|
-
|
|
873
|
+
headers,
|
|
874
|
+
// Если есть authHeaders, не используем credentials (JWT режим)
|
|
875
|
+
// Если нет — используем cookies
|
|
876
|
+
...(hasAuthHeaders ? {} : { credentials: 'include' }),
|
|
556
877
|
body: JSON.stringify({
|
|
557
878
|
channel_name: channelName,
|
|
558
879
|
socket_id: socketId,
|
|
@@ -562,7 +883,7 @@ class PushlerClient {
|
|
|
562
883
|
});
|
|
563
884
|
|
|
564
885
|
if (!response.ok) {
|
|
565
|
-
const error = await response.json();
|
|
886
|
+
const error = await response.json().catch(() => ({ error: 'Auth request failed' }));
|
|
566
887
|
throw new Error(error.error || 'Failed to get channel signature');
|
|
567
888
|
}
|
|
568
889
|
|
|
@@ -695,6 +1016,14 @@ class PushlerClient {
|
|
|
695
1016
|
|
|
696
1017
|
console.error('PushlerClient connection error:', fullErrorMessage);
|
|
697
1018
|
|
|
1019
|
+
// Реджектим Promise подключения
|
|
1020
|
+
if (this.connectionReject) {
|
|
1021
|
+
this.connectionReject(new Error(fullErrorMessage));
|
|
1022
|
+
this.connectionPromise = null;
|
|
1023
|
+
this.connectionResolve = null;
|
|
1024
|
+
this.connectionReject = null;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
698
1027
|
this.emit(Events.ERROR, {
|
|
699
1028
|
code: ErrorCodes.CONNECTION_FAILED,
|
|
700
1029
|
message: fullErrorMessage,
|
|
@@ -793,6 +1122,69 @@ class PushlerClient {
|
|
|
793
1122
|
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
794
1123
|
return this.channels.get(fullChannelName);
|
|
795
1124
|
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Проверка подписки на канал
|
|
1128
|
+
* @param {string} channelName - Имя канала
|
|
1129
|
+
* @returns {boolean} true если подписан
|
|
1130
|
+
*/
|
|
1131
|
+
isSubscribed(channelName) {
|
|
1132
|
+
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
1133
|
+
const channel = this.channels.get(fullChannelName);
|
|
1134
|
+
return channel ? channel.subscribed : false;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Получение состояния подписки на канал
|
|
1139
|
+
* @param {string} channelName - Имя канала
|
|
1140
|
+
* @returns {string|null} Состояние подписки или null если канал не найден
|
|
1141
|
+
*/
|
|
1142
|
+
getSubscriptionState(channelName) {
|
|
1143
|
+
const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
|
|
1144
|
+
const channel = this.channels.get(fullChannelName);
|
|
1145
|
+
|
|
1146
|
+
if (!channel) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return channel.subscriptionState || (channel.subscribed ? SubscriptionStates.SUBSCRIBED : SubscriptionStates.PENDING);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Получение списка подписанных каналов
|
|
1155
|
+
* @returns {string[]} Массив имён подписанных каналов
|
|
1156
|
+
*/
|
|
1157
|
+
getSubscribedChannels() {
|
|
1158
|
+
const subscribedChannels = [];
|
|
1159
|
+
|
|
1160
|
+
for (const [fullName, channel] of this.channels.entries()) {
|
|
1161
|
+
if (channel.subscribed) {
|
|
1162
|
+
subscribedChannels.push(channel.originalName || this.extractChannelName(fullName));
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return subscribedChannels;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Получение всех каналов с их состояниями
|
|
1171
|
+
* @returns {Object[]} Массив объектов с информацией о каналах
|
|
1172
|
+
*/
|
|
1173
|
+
getAllChannelsInfo() {
|
|
1174
|
+
const channelsInfo = [];
|
|
1175
|
+
|
|
1176
|
+
for (const [fullName, channel] of this.channels.entries()) {
|
|
1177
|
+
channelsInfo.push({
|
|
1178
|
+
name: channel.originalName || this.extractChannelName(fullName),
|
|
1179
|
+
fullName: fullName,
|
|
1180
|
+
subscribed: channel.subscribed,
|
|
1181
|
+
state: channel.subscriptionState || (channel.subscribed ? SubscriptionStates.SUBSCRIBED : SubscriptionStates.PENDING),
|
|
1182
|
+
type: channel.getType()
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
return channelsInfo;
|
|
1187
|
+
}
|
|
796
1188
|
}
|
|
797
1189
|
|
|
798
1190
|
var PushlerClient$1 = PushlerClient;
|
|
@@ -802,11 +1194,12 @@ var PushlerClient$1 = PushlerClient;
|
|
|
802
1194
|
* Используется на сервере Node.js для отправки событий в каналы
|
|
803
1195
|
*/
|
|
804
1196
|
class PushlerServer {
|
|
1197
|
+
static API_URL = 'https://api.pushler.ru';
|
|
1198
|
+
|
|
805
1199
|
/**
|
|
806
1200
|
* @param {Object} options - Параметры клиента
|
|
807
1201
|
* @param {string} options.appKey - Ключ приложения (key_xxx)
|
|
808
1202
|
* @param {string} options.appSecret - Секрет приложения (secret_xxx)
|
|
809
|
-
* @param {string} [options.apiUrl='http://localhost:8000/api'] - URL API сервера
|
|
810
1203
|
* @param {number} [options.timeout=10000] - Таймаут запроса в мс
|
|
811
1204
|
*/
|
|
812
1205
|
constructor(options = {}) {
|
|
@@ -819,7 +1212,6 @@ class PushlerServer {
|
|
|
819
1212
|
|
|
820
1213
|
this.appKey = options.appKey;
|
|
821
1214
|
this.appSecret = options.appSecret;
|
|
822
|
-
this.apiUrl = (options.apiUrl || 'http://localhost:8000/api').replace(/\/$/, '');
|
|
823
1215
|
this.timeout = options.timeout || 10000;
|
|
824
1216
|
}
|
|
825
1217
|
|
|
@@ -1103,7 +1495,7 @@ class PushlerServer {
|
|
|
1103
1495
|
* @returns {Promise<Object>} Результат запроса
|
|
1104
1496
|
*/
|
|
1105
1497
|
async makeSignedRequest(method, endpoint, data = null) {
|
|
1106
|
-
const url =
|
|
1498
|
+
const url = PushlerServer.API_URL + endpoint;
|
|
1107
1499
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
1108
1500
|
const body = data ? JSON.stringify(data) : '';
|
|
1109
1501
|
const signature = this.generateApiSignature(body, timestamp);
|
|
@@ -1151,7 +1543,7 @@ class PushlerServer {
|
|
|
1151
1543
|
* @returns {Promise<Object>} Результат запроса
|
|
1152
1544
|
*/
|
|
1153
1545
|
async makeHttpRequest(method, endpoint, data = null) {
|
|
1154
|
-
const url =
|
|
1546
|
+
const url = PushlerServer.API_URL + endpoint;
|
|
1155
1547
|
|
|
1156
1548
|
const headers = {
|
|
1157
1549
|
'Content-Type': 'application/json',
|
|
@@ -1192,7 +1584,7 @@ class PushlerServer {
|
|
|
1192
1584
|
* @returns {string}
|
|
1193
1585
|
*/
|
|
1194
1586
|
getApiUrl() {
|
|
1195
|
-
return
|
|
1587
|
+
return PushlerServer.API_URL;
|
|
1196
1588
|
}
|
|
1197
1589
|
}
|
|
1198
1590
|
|
|
@@ -1227,8 +1619,11 @@ const Pushler = {
|
|
|
1227
1619
|
Channel: Channel$1,
|
|
1228
1620
|
ChannelTypes,
|
|
1229
1621
|
ConnectionStates,
|
|
1622
|
+
SubscriptionStates,
|
|
1623
|
+
ErrorCodes,
|
|
1624
|
+
Events,
|
|
1230
1625
|
generateSocketId
|
|
1231
1626
|
};
|
|
1232
1627
|
|
|
1233
|
-
export { Channel$1 as Channel, ChannelTypes, ConnectionStates, Pushler, PushlerClient$1 as PushlerClient, PushlerServer$1 as PushlerServer, Pushler as default, generateSocketId };
|
|
1628
|
+
export { Channel$1 as Channel, ChannelTypes, ConnectionStates, ErrorCodes, Events, Pushler, PushlerClient$1 as PushlerClient, PushlerServer$1 as PushlerServer, SubscriptionStates, Pushler as default, generateSocketId };
|
|
1234
1629
|
//# sourceMappingURL=pushler-ru.esm.js.map
|