@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.
@@ -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
  */
@@ -215,6 +238,7 @@ class PushlerClient {
215
238
  this.channels = new Map(); // Хранит каналы по полному имени (с appKey)
216
239
  this.channelAliases = new Map(); // Маппинг оригинальное имя -> полное имя
217
240
  this.eventListeners = new Map();
241
+ this.pendingAuthRequests = new Map(); // Отслеживание pending auth запросов для отмены при unsubscribe
218
242
  this.reconnectAttempts = 0;
219
243
  this.reconnectTimer = null;
220
244
  this.authTimeout = null;
@@ -403,31 +427,98 @@ class PushlerClient {
403
427
  };
404
428
  }
405
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
+
406
481
  /**
407
482
  * Обработка входящих сообщений
408
483
  */
409
484
  handleMessage(event) {
410
485
  try {
411
- const message = JSON.parse(event.data);
486
+ // Пытаемся распарсить как один JSON
487
+ const messages = this.splitConcatenatedJSON(event.data);
412
488
 
413
- switch (message.event) {
414
- case 'pushler:connection_established':
415
- this.handleConnectionEstablished(message.data);
416
- break;
417
- case 'pushler:subscription_succeeded':
418
- this.handleSubscriptionSucceeded(message);
419
- break;
420
- case 'pushler:auth_success':
421
- this.handleAuthSuccess(message);
422
- break;
423
- case 'pushler:auth_error':
424
- this.handleAuthError(message);
425
- break;
426
- default:
427
- 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
+ }
428
497
  }
429
498
  } catch (error) {
430
- console.error('Error parsing WebSocket message:', 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);
431
522
  }
432
523
  }
433
524
 
@@ -488,7 +579,13 @@ class PushlerClient {
488
579
  const { channel } = message.data;
489
580
  const channelInstance = this.channels.get(channel);
490
581
 
582
+ // Очищаем pending auth запрос
583
+ this.cancelPendingAuth(channel);
584
+
491
585
  if (channelInstance) {
586
+ // Устанавливаем состояние subscribed
587
+ channelInstance.subscriptionState = SubscriptionStates.SUBSCRIBED;
588
+
492
589
  // На сервере уже происходит подписка после успешной авторизации,
493
590
  // поэтому просто помечаем канал как подписанный
494
591
  channelInstance.handleAuthSuccess(message.data);
@@ -501,11 +598,31 @@ class PushlerClient {
501
598
  * Обработка ошибки аутентификации
502
599
  */
503
600
  handleAuthError(message) {
504
- const { error } = message.data;
505
- this.emit(Events.CHANNEL_AUTH_ERROR, error);
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
+ });
506
621
  this.emit(Events.ERROR, {
507
622
  code: ErrorCodes.AUTHENTICATION_FAILED,
508
- message: error
623
+ message: error,
624
+ channel: originalChannelName,
625
+ socketId: this.socketId
509
626
  });
510
627
  }
511
628
 
@@ -563,8 +680,12 @@ class PushlerClient {
563
680
  // Поддерживаем оба формата имени
564
681
  const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
565
682
 
683
+ // Отменяем pending auth запросы для этого канала (исправление race condition)
684
+ this.cancelPendingAuth(fullChannelName);
685
+
566
686
  const channel = this.channels.get(fullChannelName);
567
687
  if (channel) {
688
+ channel.subscriptionState = SubscriptionStates.UNSUBSCRIBED;
568
689
  channel.unsubscribe();
569
690
  this.channels.delete(fullChannelName);
570
691
  this.channelAliases.delete(channel.originalName || channelName);
@@ -599,41 +720,119 @@ class PushlerClient {
599
720
 
600
721
  /**
601
722
  * Аутентификация канала
723
+ * С поддержкой отмены при unsubscribe
602
724
  */
603
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
+
604
739
  try {
740
+ // Проверка отмены перед началом
741
+ if (abortController.signal.aborted) {
742
+ return;
743
+ }
744
+
605
745
  const authData = await this.getChannelAuthData(channel);
606
746
 
747
+ // Проверка отмены после получения данных
748
+ if (abortController.signal.aborted) {
749
+ return;
750
+ }
751
+
607
752
  this.sendMessage({
608
753
  event: 'pushler:auth',
609
754
  data: authData
610
755
  });
611
756
 
612
757
  // Устанавливаем таймаут аутентификации
613
- this.authTimeout = setTimeout(() => {
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
+
614
770
  this.emit(Events.ERROR, {
615
771
  code: ErrorCodes.AUTHENTICATION_TIMEOUT,
616
- message: 'Authentication timeout'
772
+ message: 'Authentication timeout',
773
+ channel: channel.originalName || this.extractChannelName(channelName),
774
+ socketId: this.socketId
617
775
  });
618
776
  }, 10000);
619
777
 
778
+ // Сохраняем timeoutId для возможности очистки
779
+ const pending = this.pendingAuthRequests.get(channelName);
780
+ if (pending) {
781
+ pending.timeoutId = timeoutId;
782
+ }
783
+
620
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
+
621
796
  this.emit(Events.ERROR, {
622
797
  code: ErrorCodes.AUTHENTICATION_FAILED,
623
- message: error.message
798
+ message: error.message,
799
+ channel: channel.originalName || this.extractChannelName(channelName),
800
+ socketId: this.socketId
624
801
  });
625
802
  }
626
803
  }
627
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
+
628
824
  /**
629
825
  * Получение заголовков для авторизации
826
+ * Поддерживает как синхронные, так и асинхронные функции getAuthHeaders
827
+ * @returns {Promise<Object>} Заголовки для авторизации
630
828
  */
631
- getAuthRequestHeaders() {
829
+ async getAuthRequestHeaders() {
632
830
  const headers = { 'Content-Type': 'application/json' };
633
831
 
634
832
  // Динамические заголовки имеют приоритет
635
833
  if (typeof this.getAuthHeaders === 'function') {
636
- const dynamicHeaders = this.getAuthHeaders();
834
+ // Поддержка асинхронных функций (AsyncStorage, SecureStore, Keychain)
835
+ const dynamicHeaders = await Promise.resolve(this.getAuthHeaders());
637
836
  Object.assign(headers, dynamicHeaders);
638
837
  } else if (this.authHeaders) {
639
838
  // Статические заголовки
@@ -643,6 +842,19 @@ class PushlerClient {
643
842
  return headers;
644
843
  }
645
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
+
646
858
  /**
647
859
  * Получение подписи с бэкенда
648
860
  */
@@ -652,7 +864,8 @@ class PushlerClient {
652
864
  ? this.authEndpoint
653
865
  : `${this.apiUrl}${this.authEndpoint}`;
654
866
 
655
- const headers = this.getAuthRequestHeaders();
867
+ // Асинхронное получение заголовков (поддержка AsyncStorage/SecureStore)
868
+ const headers = await this.getAuthRequestHeaders();
656
869
  const hasAuthHeaders = this.authHeaders || this.getAuthHeaders;
657
870
 
658
871
  const response = await fetch(authUrl, {
@@ -909,6 +1122,69 @@ class PushlerClient {
909
1122
  const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
910
1123
  return this.channels.get(fullChannelName);
911
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
+ }
912
1188
  }
913
1189
 
914
1190
  var PushlerClient$1 = PushlerClient;
@@ -1343,8 +1619,11 @@ const Pushler = {
1343
1619
  Channel: Channel$1,
1344
1620
  ChannelTypes,
1345
1621
  ConnectionStates,
1622
+ SubscriptionStates,
1623
+ ErrorCodes,
1624
+ Events,
1346
1625
  generateSocketId
1347
1626
  };
1348
1627
 
1349
- 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 };
1350
1629
  //# sourceMappingURL=pushler-ru.esm.js.map