@pushler/js 1.1.1 → 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.
@@ -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
  */
@@ -219,6 +242,7 @@ class PushlerClient {
219
242
  this.channels = new Map(); // Хранит каналы по полному имени (с appKey)
220
243
  this.channelAliases = new Map(); // Маппинг оригинальное имя -> полное имя
221
244
  this.eventListeners = new Map();
245
+ this.pendingAuthRequests = new Map(); // Отслеживание pending auth запросов для отмены при unsubscribe
222
246
  this.reconnectAttempts = 0;
223
247
  this.reconnectTimer = null;
224
248
  this.authTimeout = null;
@@ -407,31 +431,98 @@ class PushlerClient {
407
431
  };
408
432
  }
409
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
+
410
485
  /**
411
486
  * Обработка входящих сообщений
412
487
  */
413
488
  handleMessage(event) {
414
489
  try {
415
- const message = JSON.parse(event.data);
490
+ // Пытаемся распарсить как один JSON
491
+ const messages = this.splitConcatenatedJSON(event.data);
416
492
 
417
- switch (message.event) {
418
- case 'pushler:connection_established':
419
- this.handleConnectionEstablished(message.data);
420
- break;
421
- case 'pushler:subscription_succeeded':
422
- this.handleSubscriptionSucceeded(message);
423
- break;
424
- case 'pushler:auth_success':
425
- this.handleAuthSuccess(message);
426
- break;
427
- case 'pushler:auth_error':
428
- this.handleAuthError(message);
429
- break;
430
- default:
431
- 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
+ }
432
501
  }
433
502
  } catch (error) {
434
- console.error('Error parsing WebSocket message:', 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);
435
526
  }
436
527
  }
437
528
 
@@ -492,7 +583,13 @@ class PushlerClient {
492
583
  const { channel } = message.data;
493
584
  const channelInstance = this.channels.get(channel);
494
585
 
586
+ // Очищаем pending auth запрос
587
+ this.cancelPendingAuth(channel);
588
+
495
589
  if (channelInstance) {
590
+ // Устанавливаем состояние subscribed
591
+ channelInstance.subscriptionState = SubscriptionStates.SUBSCRIBED;
592
+
496
593
  // На сервере уже происходит подписка после успешной авторизации,
497
594
  // поэтому просто помечаем канал как подписанный
498
595
  channelInstance.handleAuthSuccess(message.data);
@@ -505,11 +602,31 @@ class PushlerClient {
505
602
  * Обработка ошибки аутентификации
506
603
  */
507
604
  handleAuthError(message) {
508
- const { error } = message.data;
509
- this.emit(Events.CHANNEL_AUTH_ERROR, error);
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
+ });
510
625
  this.emit(Events.ERROR, {
511
626
  code: ErrorCodes.AUTHENTICATION_FAILED,
512
- message: error
627
+ message: error,
628
+ channel: originalChannelName,
629
+ socketId: this.socketId
513
630
  });
514
631
  }
515
632
 
@@ -567,8 +684,12 @@ class PushlerClient {
567
684
  // Поддерживаем оба формата имени
568
685
  const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
569
686
 
687
+ // Отменяем pending auth запросы для этого канала (исправление race condition)
688
+ this.cancelPendingAuth(fullChannelName);
689
+
570
690
  const channel = this.channels.get(fullChannelName);
571
691
  if (channel) {
692
+ channel.subscriptionState = SubscriptionStates.UNSUBSCRIBED;
572
693
  channel.unsubscribe();
573
694
  this.channels.delete(fullChannelName);
574
695
  this.channelAliases.delete(channel.originalName || channelName);
@@ -603,41 +724,119 @@ class PushlerClient {
603
724
 
604
725
  /**
605
726
  * Аутентификация канала
727
+ * С поддержкой отмены при unsubscribe
606
728
  */
607
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
+
608
743
  try {
744
+ // Проверка отмены перед началом
745
+ if (abortController.signal.aborted) {
746
+ return;
747
+ }
748
+
609
749
  const authData = await this.getChannelAuthData(channel);
610
750
 
751
+ // Проверка отмены после получения данных
752
+ if (abortController.signal.aborted) {
753
+ return;
754
+ }
755
+
611
756
  this.sendMessage({
612
757
  event: 'pushler:auth',
613
758
  data: authData
614
759
  });
615
760
 
616
761
  // Устанавливаем таймаут аутентификации
617
- this.authTimeout = setTimeout(() => {
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
+
618
774
  this.emit(Events.ERROR, {
619
775
  code: ErrorCodes.AUTHENTICATION_TIMEOUT,
620
- message: 'Authentication timeout'
776
+ message: 'Authentication timeout',
777
+ channel: channel.originalName || this.extractChannelName(channelName),
778
+ socketId: this.socketId
621
779
  });
622
780
  }, 10000);
623
781
 
782
+ // Сохраняем timeoutId для возможности очистки
783
+ const pending = this.pendingAuthRequests.get(channelName);
784
+ if (pending) {
785
+ pending.timeoutId = timeoutId;
786
+ }
787
+
624
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
+
625
800
  this.emit(Events.ERROR, {
626
801
  code: ErrorCodes.AUTHENTICATION_FAILED,
627
- message: error.message
802
+ message: error.message,
803
+ channel: channel.originalName || this.extractChannelName(channelName),
804
+ socketId: this.socketId
628
805
  });
629
806
  }
630
807
  }
631
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
+
632
828
  /**
633
829
  * Получение заголовков для авторизации
830
+ * Поддерживает как синхронные, так и асинхронные функции getAuthHeaders
831
+ * @returns {Promise<Object>} Заголовки для авторизации
634
832
  */
635
- getAuthRequestHeaders() {
833
+ async getAuthRequestHeaders() {
636
834
  const headers = { 'Content-Type': 'application/json' };
637
835
 
638
836
  // Динамические заголовки имеют приоритет
639
837
  if (typeof this.getAuthHeaders === 'function') {
640
- const dynamicHeaders = this.getAuthHeaders();
838
+ // Поддержка асинхронных функций (AsyncStorage, SecureStore, Keychain)
839
+ const dynamicHeaders = await Promise.resolve(this.getAuthHeaders());
641
840
  Object.assign(headers, dynamicHeaders);
642
841
  } else if (this.authHeaders) {
643
842
  // Статические заголовки
@@ -647,6 +846,19 @@ class PushlerClient {
647
846
  return headers;
648
847
  }
649
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
+
650
862
  /**
651
863
  * Получение подписи с бэкенда
652
864
  */
@@ -656,7 +868,8 @@ class PushlerClient {
656
868
  ? this.authEndpoint
657
869
  : `${this.apiUrl}${this.authEndpoint}`;
658
870
 
659
- const headers = this.getAuthRequestHeaders();
871
+ // Асинхронное получение заголовков (поддержка AsyncStorage/SecureStore)
872
+ const headers = await this.getAuthRequestHeaders();
660
873
  const hasAuthHeaders = this.authHeaders || this.getAuthHeaders;
661
874
 
662
875
  const response = await fetch(authUrl, {
@@ -913,6 +1126,69 @@ class PushlerClient {
913
1126
  const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
914
1127
  return this.channels.get(fullChannelName);
915
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
+ }
916
1192
  }
917
1193
 
918
1194
  var PushlerClient$1 = PushlerClient;
@@ -1347,15 +1623,21 @@ const Pushler = {
1347
1623
  Channel: Channel$1,
1348
1624
  ChannelTypes,
1349
1625
  ConnectionStates,
1626
+ SubscriptionStates,
1627
+ ErrorCodes,
1628
+ Events,
1350
1629
  generateSocketId
1351
1630
  };
1352
1631
 
1353
1632
  exports.Channel = Channel$1;
1354
1633
  exports.ChannelTypes = ChannelTypes;
1355
1634
  exports.ConnectionStates = ConnectionStates;
1635
+ exports.ErrorCodes = ErrorCodes;
1636
+ exports.Events = Events;
1356
1637
  exports.Pushler = Pushler;
1357
1638
  exports.PushlerClient = PushlerClient$1;
1358
1639
  exports.PushlerServer = PushlerServer$1;
1640
+ exports.SubscriptionStates = SubscriptionStates;
1359
1641
  exports.default = Pushler;
1360
1642
  exports.generateSocketId = generateSocketId;
1361
1643
  //# sourceMappingURL=pushler-ru.cjs.map