@pushler/js 1.0.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.
@@ -0,0 +1,1253 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('crypto')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'crypto'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Pushler = {}, global.crypto));
5
+ })(this, (function (exports, crypto) { 'use strict';
6
+
7
+ /**
8
+ * Типы каналов
9
+ */
10
+ const ChannelTypes = {
11
+ PUBLIC: 'public',
12
+ PRIVATE: 'private',
13
+ PRESENCE: 'presence'
14
+ };
15
+
16
+ /**
17
+ * Состояния подключения
18
+ */
19
+ const ConnectionStates = {
20
+ CONNECTING: 'connecting',
21
+ CONNECTED: 'connected',
22
+ DISCONNECTED: 'disconnected',
23
+ RECONNECTING: 'reconnecting',
24
+ FAILED: 'failed'
25
+ };
26
+
27
+ /**
28
+ * События WebSocket
29
+ */
30
+ const Events = {
31
+ // События подключения
32
+ CONNECTION_STATE_CHANGED: 'connection_state_changed',
33
+ CONNECTED: 'connected',
34
+ DISCONNECTED: 'disconnected',
35
+ ERROR: 'error',
36
+
37
+ // События каналов
38
+ CHANNEL_SUBSCRIBED: 'channel_subscribed',
39
+ CHANNEL_UNSUBSCRIBED: 'channel_unsubscribed',
40
+ CHANNEL_AUTH_SUCCESS: 'pushler:auth_success',
41
+ CHANNEL_AUTH_ERROR: 'pushler:auth_error',
42
+
43
+ // События сообщений
44
+ MESSAGE_RECEIVED: 'message_received'
45
+ };
46
+
47
+ /**
48
+ * Коды ошибок
49
+ */
50
+ const ErrorCodes = {
51
+ INVALID_APP_KEY: 'INVALID_APP_KEY',
52
+ INVALID_SIGNATURE: 'INVALID_SIGNATURE',
53
+ AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED',
54
+ CHANNEL_ACCESS_DENIED: 'CHANNEL_ACCESS_DENIED',
55
+ CONNECTION_FAILED: 'CONNECTION_FAILED',
56
+ AUTHENTICATION_TIMEOUT: 'AUTHENTICATION_TIMEOUT'
57
+ };
58
+
59
+ /**
60
+ * Класс для управления каналом
61
+ */
62
+ class Channel {
63
+ constructor(client, name, options = {}) {
64
+ this.client = client;
65
+ this.name = name;
66
+ this.options = options;
67
+ this.subscribed = false;
68
+ this.eventListeners = new Map();
69
+ this.presenceData = null;
70
+ }
71
+
72
+ /**
73
+ * Подписка на событие канала
74
+ */
75
+ on(event, callback) {
76
+ if (!this.eventListeners.has(event)) {
77
+ this.eventListeners.set(event, []);
78
+ }
79
+ this.eventListeners.get(event).push(callback);
80
+ }
81
+
82
+ /**
83
+ * Отписка от события канала
84
+ */
85
+ off(event, callback) {
86
+ if (this.eventListeners.has(event)) {
87
+ const listeners = this.eventListeners.get(event);
88
+ const index = listeners.indexOf(callback);
89
+ if (index > -1) {
90
+ listeners.splice(index, 1);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Вызов события канала
97
+ */
98
+ emit(event, data) {
99
+ if (this.eventListeners.has(event)) {
100
+ this.eventListeners.get(event).forEach(callback => {
101
+ try {
102
+ callback(data);
103
+ } catch (error) {
104
+ console.error('Error in channel event listener:', error);
105
+ }
106
+ });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Обработка успешной аутентификации
112
+ */
113
+ handleAuthSuccess(data) {
114
+ this.subscribed = true;
115
+ this.emit(Events.CHANNEL_SUBSCRIBED, data);
116
+
117
+ // Для presence каналов сохраняем данные пользователя
118
+ if (data.user) {
119
+ this.presenceData = data.user;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Обработка входящих сообщений
125
+ */
126
+ handleMessage(event, data) {
127
+ this.emit(event, data);
128
+ }
129
+
130
+ /**
131
+ * Отписка от канала
132
+ */
133
+ unsubscribe() {
134
+ if (this.subscribed && this.client.connectionState === 'connected') {
135
+ this.client.sendMessage({
136
+ event: 'pushler:unsubscribe',
137
+ data: { channel: this.name }
138
+ });
139
+ }
140
+
141
+ this.subscribed = false;
142
+ this.emit(Events.CHANNEL_UNSUBSCRIBED);
143
+ }
144
+
145
+ /**
146
+ * Проверка подписки на канал
147
+ */
148
+ isSubscribed() {
149
+ return this.subscribed;
150
+ }
151
+
152
+ /**
153
+ * Получение данных присутствия (для presence каналов)
154
+ */
155
+ getPresenceData() {
156
+ return this.presenceData;
157
+ }
158
+
159
+ /**
160
+ * Получение типа канала
161
+ * Учитывает, что имя канала может иметь формат {appKey}:{channelName}
162
+ */
163
+ getType() {
164
+ // Извлекаем базовое имя канала (без appKey)
165
+ const baseName = this.getBaseName();
166
+
167
+ if (baseName.startsWith('private-')) {
168
+ return ChannelTypes.PRIVATE;
169
+ }
170
+ if (baseName.startsWith('presence-')) {
171
+ return ChannelTypes.PRESENCE;
172
+ }
173
+ return ChannelTypes.PUBLIC;
174
+ }
175
+
176
+ /**
177
+ * Получение базового имени канала (без appKey префикса)
178
+ * @returns {string} Базовое имя канала
179
+ */
180
+ getBaseName() {
181
+ // Имя канала может быть в формате {appKey}:{channelName}
182
+ const colonIndex = this.name.indexOf(':');
183
+ if (colonIndex !== -1) {
184
+ return this.name.substring(colonIndex + 1);
185
+ }
186
+ return this.name;
187
+ }
188
+
189
+ /**
190
+ * Получение полного имени канала (с appKey)
191
+ * @returns {string} Полное имя канала
192
+ */
193
+ getFullName() {
194
+ return this.name;
195
+ }
196
+ }
197
+
198
+ var Channel$1 = Channel;
199
+
200
+ /**
201
+ * Основной класс для подключения к Pushler.ru WebSocket серверу
202
+ */
203
+ class PushlerClient {
204
+ constructor(options = {}) {
205
+ this.appKey = options.appKey;
206
+ this.wsUrl = options.wsUrl || 'ws://pushler.ru:8080/ws';
207
+ this.apiUrl = options.apiUrl;
208
+ this.authEndpoint = options.authEndpoint || '/pushler/auth'; // Путь для авторизации каналов
209
+ this.autoConnect = options.autoConnect !== false;
210
+ this.reconnectDelay = options.reconnectDelay || 1000;
211
+ this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
212
+
213
+ this.connectionState = ConnectionStates.DISCONNECTED;
214
+ this.socket = null;
215
+ this.socketId = null;
216
+ this.channels = new Map(); // Хранит каналы по полному имени (с appKey)
217
+ this.channelAliases = new Map(); // Маппинг оригинальное имя -> полное имя
218
+ this.eventListeners = new Map();
219
+ this.reconnectAttempts = 0;
220
+ this.reconnectTimer = null;
221
+ this.authTimeout = null;
222
+
223
+ // Автоматическое подключение
224
+ if (this.autoConnect) {
225
+ this.connect();
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Формирование полного имени канала с префиксом appKey
231
+ * Формат: {appKey}:{channelName}
232
+ * @param {string} channelName - Оригинальное имя канала
233
+ * @returns {string} Полное имя канала
234
+ */
235
+ formatChannelName(channelName) {
236
+ // Если канал уже содержит appKey, возвращаем как есть
237
+ if (channelName.startsWith(this.appKey + ':')) {
238
+ return channelName;
239
+ }
240
+ return `${this.appKey}:${channelName}`;
241
+ }
242
+
243
+ /**
244
+ * Извлечение оригинального имени канала (без appKey)
245
+ * @param {string} fullChannelName - Полное имя канала
246
+ * @returns {string} Оригинальное имя канала
247
+ */
248
+ extractChannelName(fullChannelName) {
249
+ const prefix = this.appKey + ':';
250
+ if (fullChannelName.startsWith(prefix)) {
251
+ return fullChannelName.substring(prefix.length);
252
+ }
253
+ return fullChannelName;
254
+ }
255
+
256
+ /**
257
+ * Подключение к WebSocket серверу
258
+ */
259
+ connect() {
260
+ if (this.connectionState === ConnectionStates.CONNECTED ||
261
+ this.connectionState === ConnectionStates.CONNECTING) {
262
+ return;
263
+ }
264
+
265
+ this.connectionState = ConnectionStates.CONNECTING;
266
+ this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
267
+
268
+ try {
269
+ if (!this.wsUrl) {
270
+ throw new Error('WebSocket URL is not configured');
271
+ }
272
+
273
+ console.log(`PushlerClient: Connecting to ${this.wsUrl}`);
274
+ this.socket = new WebSocket(this.wsUrl);
275
+ this.setupWebSocketHandlers();
276
+ } catch (error) {
277
+ console.error('PushlerClient: Error creating WebSocket connection:', error);
278
+ this.handleConnectionError(error);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Настройка обработчиков WebSocket
284
+ */
285
+ setupWebSocketHandlers() {
286
+ this.socket.onopen = () => {
287
+ // Не устанавливаем CONNECTED здесь - ждем pushler:connection_established
288
+ // Это предотвращает двойные события и проблемы с переподключением
289
+ this.connectionState = ConnectionStates.CONNECTING;
290
+ this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
291
+ console.log('WebSocket connection opened, waiting for connection_established...');
292
+ };
293
+
294
+ this.socket.onmessage = (event) => {
295
+ this.handleMessage(event);
296
+ };
297
+
298
+ this.socket.onclose = (event) => {
299
+ this.connectionState = ConnectionStates.DISCONNECTED;
300
+ this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
301
+ this.emit(Events.DISCONNECTED, event);
302
+
303
+ // Логируем причину закрытия для отладки
304
+ if (event.code !== 1000) { // 1000 = нормальное закрытие
305
+ console.warn('WebSocket closed unexpectedly:', {
306
+ code: event.code,
307
+ reason: event.reason || 'No reason provided',
308
+ wasClean: event.wasClean,
309
+ url: this.wsUrl
310
+ });
311
+ }
312
+
313
+ // Автоматическое переподключение
314
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
315
+ this.scheduleReconnect();
316
+ } else {
317
+ // Если превышено количество попыток, отправляем ошибку
318
+ this.emit(Events.ERROR, {
319
+ code: ErrorCodes.CONNECTION_FAILED,
320
+ message: `Failed to connect after ${this.maxReconnectAttempts} attempts. Please check if the WebSocket server is running at ${this.wsUrl}`,
321
+ url: this.wsUrl
322
+ });
323
+ }
324
+ };
325
+
326
+ this.socket.onerror = (error) => {
327
+ this.handleConnectionError(error);
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Обработка входящих сообщений
333
+ */
334
+ handleMessage(event) {
335
+ try {
336
+ const message = JSON.parse(event.data);
337
+
338
+ switch (message.event) {
339
+ case 'pushler:connection_established':
340
+ this.handleConnectionEstablished(message.data);
341
+ break;
342
+ case 'pushler:subscription_succeeded':
343
+ this.handleSubscriptionSucceeded(message);
344
+ break;
345
+ case 'pushler:auth_success':
346
+ this.handleAuthSuccess(message);
347
+ break;
348
+ case 'pushler:auth_error':
349
+ this.handleAuthError(message);
350
+ break;
351
+ default:
352
+ this.handleChannelMessage(message);
353
+ }
354
+ } catch (error) {
355
+ console.error('Error parsing WebSocket message:', error);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Обработка установления соединения
361
+ */
362
+ handleConnectionEstablished(data) {
363
+ this.socketId = data.socket_id;
364
+ this.connectionState = ConnectionStates.CONNECTED;
365
+ this.reconnectAttempts = 0;
366
+
367
+ console.log('Connection established with socket ID:', this.socketId);
368
+
369
+ // Переподписываемся на все каналы после переподключения
370
+ this.resubscribeAllChannels();
371
+
372
+ this.emit(Events.CONNECTED, { socketId: this.socketId });
373
+ }
374
+
375
+ /**
376
+ * Переподписка на все каналы после переподключения
377
+ */
378
+ resubscribeAllChannels() {
379
+ for (const [channelName, channel] of this.channels.entries()) {
380
+ // Сбрасываем флаг подписки
381
+ channel.subscribed = false;
382
+ // Выполняем подписку заново
383
+ this.performChannelSubscription(channel);
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Обработка успешной подписки на канал
389
+ */
390
+ handleSubscriptionSucceeded(message) {
391
+ const channel = message.channel;
392
+ const channelInstance = this.channels.get(channel);
393
+
394
+ if (channelInstance) {
395
+ channelInstance.subscribed = true;
396
+ channelInstance.emit(Events.CHANNEL_SUBSCRIBED, message.data || {});
397
+ console.log(`Channel ${channel} subscribed successfully`);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Обработка успешной аутентификации
403
+ */
404
+ handleAuthSuccess(message) {
405
+ const { channel } = message.data;
406
+ const channelInstance = this.channels.get(channel);
407
+
408
+ if (channelInstance) {
409
+ // На сервере уже происходит подписка после успешной авторизации,
410
+ // поэтому просто помечаем канал как подписанный
411
+ channelInstance.handleAuthSuccess(message.data);
412
+ }
413
+
414
+ this.emit(Events.CHANNEL_AUTH_SUCCESS, message.data);
415
+ }
416
+
417
+ /**
418
+ * Обработка ошибки аутентификации
419
+ */
420
+ handleAuthError(message) {
421
+ const { error } = message.data;
422
+ this.emit(Events.CHANNEL_AUTH_ERROR, error);
423
+ this.emit(Events.ERROR, {
424
+ code: ErrorCodes.AUTHENTICATION_FAILED,
425
+ message: error
426
+ });
427
+ }
428
+
429
+ /**
430
+ * Обработка сообщений каналов
431
+ */
432
+ handleChannelMessage(message) {
433
+ const { channel, event, data } = message;
434
+ const channelInstance = this.channels.get(channel);
435
+
436
+ if (channelInstance) {
437
+ channelInstance.handleMessage(event, data);
438
+ }
439
+
440
+ // Для события возвращаем оригинальное имя канала (без appKey)
441
+ const originalChannelName = this.extractChannelName(channel);
442
+ this.emit(Events.MESSAGE_RECEIVED, { channel: originalChannelName, event, data });
443
+ }
444
+
445
+ /**
446
+ * Подписка на канал
447
+ * @param {string} channelName - Имя канала (без appKey, будет добавлен автоматически)
448
+ * @param {Object} options - Опции подписки
449
+ * @returns {Channel} Объект канала
450
+ */
451
+ subscribe(channelName, options = {}) {
452
+ // Формируем полное имя канала с appKey
453
+ const fullChannelName = this.formatChannelName(channelName);
454
+
455
+ if (this.channels.has(fullChannelName)) {
456
+ return this.channels.get(fullChannelName);
457
+ }
458
+
459
+ // Создаем канал с полным именем
460
+ const channel = new Channel$1(this, fullChannelName, options);
461
+ // Сохраняем оригинальное имя для удобства
462
+ channel.originalName = channelName;
463
+
464
+ this.channels.set(fullChannelName, channel);
465
+ this.channelAliases.set(channelName, fullChannelName);
466
+
467
+ // Если подключены, сразу подписываемся
468
+ if (this.connectionState === ConnectionStates.CONNECTED) {
469
+ this.performChannelSubscription(channel);
470
+ }
471
+
472
+ return channel;
473
+ }
474
+
475
+ /**
476
+ * Отписка от канала
477
+ * @param {string} channelName - Имя канала (можно использовать оригинальное или полное)
478
+ */
479
+ unsubscribe(channelName) {
480
+ // Поддерживаем оба формата имени
481
+ const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
482
+
483
+ const channel = this.channels.get(fullChannelName);
484
+ if (channel) {
485
+ channel.unsubscribe();
486
+ this.channels.delete(fullChannelName);
487
+ this.channelAliases.delete(channel.originalName || channelName);
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Выполнение подписки на канал
493
+ */
494
+ performChannelSubscription(channel) {
495
+ if (this.connectionState !== ConnectionStates.CONNECTED) {
496
+ return;
497
+ }
498
+
499
+ const channelType = this.getChannelType(channel.name);
500
+
501
+ // Для публичных каналов подписываемся сразу
502
+ if (channelType === ChannelTypes.PUBLIC) {
503
+ this.sendMessage({
504
+ event: 'pushler:subscribe',
505
+ data: {
506
+ channel: channel.name,
507
+ app_key: this.appKey
508
+ }
509
+ });
510
+ return;
511
+ }
512
+
513
+ // Для приватных и presence каналов нужна аутентификация
514
+ this.authenticateChannel(channel);
515
+ }
516
+
517
+ /**
518
+ * Аутентификация канала
519
+ */
520
+ async authenticateChannel(channel) {
521
+ try {
522
+ const authData = await this.getChannelAuthData(channel);
523
+
524
+ this.sendMessage({
525
+ event: 'pushler:auth',
526
+ data: authData
527
+ });
528
+
529
+ // Устанавливаем таймаут аутентификации
530
+ this.authTimeout = setTimeout(() => {
531
+ this.emit(Events.ERROR, {
532
+ code: ErrorCodes.AUTHENTICATION_TIMEOUT,
533
+ message: 'Authentication timeout'
534
+ });
535
+ }, 10000);
536
+
537
+ } catch (error) {
538
+ this.emit(Events.ERROR, {
539
+ code: ErrorCodes.AUTHENTICATION_FAILED,
540
+ message: error.message
541
+ });
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Получение подписи с бэкенда
547
+ */
548
+ async getChannelSignature(channelName, socketId, userData = null) {
549
+ // Формируем URL: если authEndpoint абсолютный — используем его, иначе добавляем к apiUrl
550
+ const authUrl = this.authEndpoint.startsWith('http')
551
+ ? this.authEndpoint
552
+ : `${this.apiUrl}${this.authEndpoint}`;
553
+
554
+ const response = await fetch(authUrl, {
555
+ method: 'POST',
556
+ headers: {
557
+ 'Content-Type': 'application/json',
558
+ },
559
+ body: JSON.stringify({
560
+ channel_name: channelName,
561
+ socket_id: socketId,
562
+ app_key: this.appKey,
563
+ user_data: userData
564
+ })
565
+ });
566
+
567
+ if (!response.ok) {
568
+ const error = await response.json();
569
+ throw new Error(error.error || 'Failed to get channel signature');
570
+ }
571
+
572
+ const data = await response.json();
573
+ return data.signature;
574
+ }
575
+
576
+ /**
577
+ * Получение данных для аутентификации канала
578
+ */
579
+ async getChannelAuthData(channel) {
580
+ if (!this.socketId) {
581
+ throw new Error('Socket ID not available. Connection not established.');
582
+ }
583
+
584
+ const channelType = this.getChannelType(channel.name);
585
+
586
+ const authData = {
587
+ app_key: this.appKey,
588
+ channel: channel.name,
589
+ socket_id: this.socketId
590
+ };
591
+
592
+ // Для приватных и presence каналов нужна подпись
593
+ if (channelType === ChannelTypes.PRIVATE || channelType === ChannelTypes.PRESENCE) {
594
+ // Если подпись уже предоставлена, используем её
595
+ if (channel.options.signature) {
596
+ authData.signature = channel.options.signature;
597
+ console.log('Using provided signature:', {
598
+ channel: channel.name,
599
+ socketId: this.socketId,
600
+ signatureLength: channel.options.signature.length,
601
+ signature: channel.options.signature.substring(0, 20) + '...'
602
+ });
603
+ } else {
604
+ // Иначе получаем подпись с бэкенда
605
+ try {
606
+ authData.signature = await this.getChannelSignature(
607
+ channel.name,
608
+ this.socketId,
609
+ channel.options.user
610
+ );
611
+ } catch (error) {
612
+ throw new Error(`Failed to get channel signature: ${error.message}`);
613
+ }
614
+ }
615
+ }
616
+
617
+ // Для presence каналов нужны данные пользователя
618
+ if (channelType === ChannelTypes.PRESENCE) {
619
+ if (!channel.options.user) {
620
+ throw new Error('User data is required for presence channels');
621
+ }
622
+
623
+ authData.user = channel.options.user;
624
+ }
625
+
626
+ return authData;
627
+ }
628
+
629
+ /**
630
+ * Определение типа канала
631
+ * Формат канала: {appKey}:{type-}channelName
632
+ */
633
+ getChannelType(channelName) {
634
+ // Извлекаем имя канала без appKey
635
+ const baseName = this.extractChannelName(channelName);
636
+
637
+ if (baseName.startsWith('private-')) {
638
+ return ChannelTypes.PRIVATE;
639
+ }
640
+ if (baseName.startsWith('presence-')) {
641
+ return ChannelTypes.PRESENCE;
642
+ }
643
+ return ChannelTypes.PUBLIC;
644
+ }
645
+
646
+ /**
647
+ * Отправка сообщения через WebSocket
648
+ */
649
+ sendMessage(message) {
650
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
651
+ this.socket.send(JSON.stringify(message));
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Генерация уникального ID сокета
657
+ */
658
+ generateSocketId() {
659
+ return Math.random().toString(36).substr(2, 9) + '.' + Math.floor(Math.random() * 1000000);
660
+ }
661
+
662
+ /**
663
+ * Планирование переподключения
664
+ */
665
+ scheduleReconnect() {
666
+ if (this.reconnectTimer) {
667
+ clearTimeout(this.reconnectTimer);
668
+ }
669
+
670
+ this.reconnectAttempts++;
671
+ this.connectionState = ConnectionStates.RECONNECTING;
672
+ this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
673
+
674
+ this.reconnectTimer = setTimeout(() => {
675
+ this.connect();
676
+ }, this.reconnectDelay * this.reconnectAttempts);
677
+ }
678
+
679
+ /**
680
+ * Обработка ошибки подключения
681
+ */
682
+ handleConnectionError(error) {
683
+ this.connectionState = ConnectionStates.FAILED;
684
+ this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
685
+
686
+ // Формируем более информативное сообщение об ошибке
687
+ let errorMessage = 'Connection failed';
688
+ if (error && error.message) {
689
+ errorMessage = error.message;
690
+ } else if (error && typeof error === 'object') {
691
+ errorMessage = JSON.stringify(error);
692
+ } else if (typeof error === 'string') {
693
+ errorMessage = error;
694
+ }
695
+
696
+ // Добавляем информацию о URL для отладки
697
+ const fullErrorMessage = `WebSocket connection to '${this.wsUrl}' failed: ${errorMessage}`;
698
+
699
+ console.error('PushlerClient connection error:', fullErrorMessage);
700
+
701
+ this.emit(Events.ERROR, {
702
+ code: ErrorCodes.CONNECTION_FAILED,
703
+ message: fullErrorMessage,
704
+ url: this.wsUrl,
705
+ originalError: error
706
+ });
707
+ }
708
+
709
+ /**
710
+ * Отключение от WebSocket сервера
711
+ */
712
+ disconnect() {
713
+ if (this.reconnectTimer) {
714
+ clearTimeout(this.reconnectTimer);
715
+ this.reconnectTimer = null;
716
+ }
717
+
718
+ if (this.authTimeout) {
719
+ clearTimeout(this.authTimeout);
720
+ this.authTimeout = null;
721
+ }
722
+
723
+ if (this.socket) {
724
+ this.socket.close();
725
+ this.socket = null;
726
+ }
727
+
728
+ // Очищаем каналы и алиасы
729
+ this.channels.clear();
730
+ this.channelAliases.clear();
731
+
732
+ this.connectionState = ConnectionStates.DISCONNECTED;
733
+ this.emit(Events.CONNECTION_STATE_CHANGED, this.connectionState);
734
+ }
735
+
736
+ /**
737
+ * Подписка на событие
738
+ */
739
+ on(event, callback) {
740
+ if (!this.eventListeners.has(event)) {
741
+ this.eventListeners.set(event, []);
742
+ }
743
+ this.eventListeners.get(event).push(callback);
744
+ }
745
+
746
+ /**
747
+ * Отписка от события
748
+ */
749
+ off(event, callback) {
750
+ if (this.eventListeners.has(event)) {
751
+ const listeners = this.eventListeners.get(event);
752
+ const index = listeners.indexOf(callback);
753
+ if (index > -1) {
754
+ listeners.splice(index, 1);
755
+ }
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Вызов события
761
+ */
762
+ emit(event, data) {
763
+ if (this.eventListeners.has(event)) {
764
+ this.eventListeners.get(event).forEach(callback => {
765
+ try {
766
+ callback(data);
767
+ } catch (error) {
768
+ console.error('Error in event listener:', error);
769
+ }
770
+ });
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Получение состояния подключения
776
+ */
777
+ getConnectionState() {
778
+ return this.connectionState;
779
+ }
780
+
781
+ /**
782
+ * Получение всех каналов (возвращает оригинальные имена без appKey)
783
+ */
784
+ getChannels() {
785
+ return Array.from(this.channels.values()).map(channel =>
786
+ channel.originalName || this.extractChannelName(channel.name)
787
+ );
788
+ }
789
+
790
+ /**
791
+ * Получение канала по имени
792
+ * @param {string} channelName - Имя канала (можно использовать оригинальное или полное)
793
+ * @returns {Channel|undefined} Объект канала
794
+ */
795
+ channel(channelName) {
796
+ const fullChannelName = this.channelAliases.get(channelName) || this.formatChannelName(channelName);
797
+ return this.channels.get(fullChannelName);
798
+ }
799
+ }
800
+
801
+ var PushlerClient$1 = PushlerClient;
802
+
803
+ /**
804
+ * Серверный SDK для отправки сообщений через Pushler.ru API
805
+ * Используется на сервере Node.js для отправки событий в каналы
806
+ */
807
+ class PushlerServer {
808
+ /**
809
+ * @param {Object} options - Параметры клиента
810
+ * @param {string} options.appKey - Ключ приложения (key_xxx)
811
+ * @param {string} options.appSecret - Секрет приложения (secret_xxx)
812
+ * @param {string} [options.apiUrl='http://localhost:8000/api'] - URL API сервера
813
+ * @param {number} [options.timeout=30000] - Таймаут запроса в мс
814
+ */
815
+ constructor(options = {}) {
816
+ if (!options.appKey) {
817
+ throw new Error('appKey is required');
818
+ }
819
+ if (!options.appSecret) {
820
+ throw new Error('appSecret is required');
821
+ }
822
+
823
+ this.appKey = options.appKey;
824
+ this.appSecret = options.appSecret;
825
+ this.apiUrl = (options.apiUrl || 'http://localhost:8000/api').replace(/\/$/, '');
826
+ this.timeout = options.timeout || 30000;
827
+ }
828
+
829
+ /**
830
+ * Формирование полного имени канала с префиксом appKey
831
+ * Формат: {appKey}:{channelName}
832
+ * @param {string} channelName - Оригинальное имя канала
833
+ * @returns {string} Полное имя канала
834
+ */
835
+ formatChannelName(channelName) {
836
+ // Если канал уже содержит appKey, возвращаем как есть
837
+ if (channelName.startsWith(this.appKey + ':')) {
838
+ return channelName;
839
+ }
840
+ return `${this.appKey}:${channelName}`;
841
+ }
842
+
843
+ /**
844
+ * Извлечение оригинального имени канала (без appKey)
845
+ * @param {string} fullChannelName - Полное имя канала
846
+ * @returns {string} Оригинальное имя канала
847
+ */
848
+ extractChannelName(fullChannelName) {
849
+ const prefix = this.appKey + ':';
850
+ if (fullChannelName.startsWith(prefix)) {
851
+ return fullChannelName.substring(prefix.length);
852
+ }
853
+ return fullChannelName;
854
+ }
855
+
856
+ /**
857
+ * Генерация подписи для API запроса
858
+ *
859
+ * @param {string} body - JSON тело запроса
860
+ * @param {number} timestamp - Unix timestamp
861
+ * @returns {string} HMAC-SHA256 подпись
862
+ */
863
+ generateApiSignature(body, timestamp) {
864
+ const signatureString = body + timestamp;
865
+ return crypto.createHmac('sha256', this.appSecret)
866
+ .update(signatureString)
867
+ .digest('hex');
868
+ }
869
+
870
+ /**
871
+ * Генерация подписи для авторизации канала
872
+ *
873
+ * @param {string} channelName - Название канала (полное с appKey)
874
+ * @param {string} socketId - ID сокета
875
+ * @returns {string} HMAC-SHA256 подпись
876
+ */
877
+ generateChannelSignature(channelName, socketId) {
878
+ // Подпись генерируется для полного имени канала
879
+ const fullChannelName = this.formatChannelName(channelName);
880
+ const signatureString = socketId + ':' + fullChannelName;
881
+ return crypto.createHmac('sha256', this.appSecret)
882
+ .update(signatureString)
883
+ .digest('hex');
884
+ }
885
+
886
+ /**
887
+ * Авторизация приватного канала
888
+ *
889
+ * @param {string} channelName - Название канала (appKey будет добавлен автоматически)
890
+ * @param {string} socketId - ID сокета
891
+ * @returns {Object} Результат авторизации с полным именем канала
892
+ */
893
+ authorizeChannel(channelName, socketId) {
894
+ const fullChannelName = this.formatChannelName(channelName);
895
+ const signature = this.generateChannelSignature(fullChannelName, socketId);
896
+ return {
897
+ auth: this.appKey + ':' + signature,
898
+ channel: fullChannelName
899
+ };
900
+ }
901
+
902
+ /**
903
+ * Авторизация presence канала
904
+ *
905
+ * @param {string} channelName - Название канала (appKey будет добавлен автоматически)
906
+ * @param {string} socketId - ID сокета
907
+ * @param {Object} userData - Данные пользователя
908
+ * @param {string|number} userData.user_id - ID пользователя (обязательно)
909
+ * @param {Object} [userData.user_info] - Дополнительная информация о пользователе
910
+ * @returns {Object} Результат авторизации с полным именем канала
911
+ */
912
+ authorizePresenceChannel(channelName, socketId, userData) {
913
+ if (!userData || !userData.user_id) {
914
+ throw new Error('user_id is required for presence channels');
915
+ }
916
+
917
+ const fullChannelName = this.formatChannelName(channelName);
918
+ const signature = this.generateChannelSignature(fullChannelName, socketId);
919
+
920
+ return {
921
+ auth: this.appKey + ':' + signature,
922
+ channel: fullChannelName,
923
+ channel_data: JSON.stringify({
924
+ user_id: userData.user_id,
925
+ user_info: userData.user_info || {}
926
+ })
927
+ };
928
+ }
929
+
930
+ /**
931
+ * Аутентификация пользователя для WebSocket соединения
932
+ *
933
+ * @param {string} socketId - ID сокета
934
+ * @param {Object} userData - Данные пользователя
935
+ * @returns {Object} Результат аутентификации
936
+ */
937
+ authenticateUser(socketId, userData) {
938
+ if (!userData || !userData.user_id) {
939
+ throw new Error('user_id is required for user authentication');
940
+ }
941
+
942
+ const signature = this.generateChannelSignature('user-' + userData.user_id, socketId);
943
+
944
+ return {
945
+ auth: this.appKey + ':' + signature,
946
+ user_data: JSON.stringify(userData)
947
+ };
948
+ }
949
+
950
+ /**
951
+ * Отправка события в канал
952
+ *
953
+ * @param {string|string[]} channels - Канал или массив каналов (appKey будет добавлен автоматически)
954
+ * @param {string} event - Название события
955
+ * @param {Object} data - Данные события
956
+ * @param {string} [socketId] - ID сокета (для исключения отправителя)
957
+ * @returns {Promise<Object>} Результат отправки
958
+ */
959
+ async trigger(channels, event, data, socketId = null) {
960
+ const channelList = Array.isArray(channels) ? channels : [channels];
961
+
962
+ // Добавляем appKey ко всем каналам
963
+ const formattedChannels = channelList.map(ch => this.formatChannelName(ch));
964
+
965
+ // Для одного канала используем /messages/send
966
+ if (formattedChannels.length === 1) {
967
+ const payload = {
968
+ channel: formattedChannels[0],
969
+ event,
970
+ data
971
+ };
972
+
973
+ if (socketId) {
974
+ payload.socket_id = socketId;
975
+ }
976
+
977
+ return this.makeSignedRequest('POST', '/messages/send', payload);
978
+ }
979
+
980
+ // Для нескольких каналов используем /messages/broadcast
981
+ const payload = {
982
+ channels: formattedChannels,
983
+ event,
984
+ data
985
+ };
986
+
987
+ if (socketId) {
988
+ payload.socket_id = socketId;
989
+ }
990
+
991
+ return this.makeSignedRequest('POST', '/messages/broadcast', payload);
992
+ }
993
+
994
+ /**
995
+ * Отправка события в несколько каналов одновременно
996
+ *
997
+ * @param {string[]} channels - Массив каналов
998
+ * @param {string} event - Название события
999
+ * @param {Object} data - Данные события
1000
+ * @param {string} [socketId] - ID сокета (для исключения отправителя)
1001
+ * @returns {Promise<Object>} Результат отправки
1002
+ */
1003
+ async triggerBatch(channels, event, data, socketId = null) {
1004
+ return this.trigger(channels, event, data, socketId);
1005
+ }
1006
+
1007
+ /**
1008
+ * Получение статуса сообщения
1009
+ *
1010
+ * @param {string|number} messageId - ID сообщения
1011
+ * @returns {Promise<Object>} Статус сообщения
1012
+ */
1013
+ async getMessageStatus(messageId) {
1014
+ return this.makeSignedRequest('GET', `/messages/${messageId}/status`);
1015
+ }
1016
+
1017
+ /**
1018
+ * Получение списка сообщений
1019
+ *
1020
+ * @param {Object} [params] - Параметры запроса
1021
+ * @param {string} [params.status] - Фильтр по статусу
1022
+ * @param {number} [params.per_page] - Количество на страницу
1023
+ * @returns {Promise<Object>} Список сообщений
1024
+ */
1025
+ async getMessages(params = {}) {
1026
+ const query = new URLSearchParams(params).toString();
1027
+ const endpoint = '/messages' + (query ? '?' + query : '');
1028
+ return this.makeSignedRequest('GET', endpoint);
1029
+ }
1030
+
1031
+ /**
1032
+ * Проверка валидности приложения
1033
+ *
1034
+ * @returns {Promise<Object>} Информация о приложении
1035
+ */
1036
+ async validateApplication() {
1037
+ const response = await this.makeHttpRequest('GET', `/applications/validate/${this.appKey}`);
1038
+
1039
+ if (!response.id) {
1040
+ throw new Error(response.error || 'Application validation failed');
1041
+ }
1042
+
1043
+ return response;
1044
+ }
1045
+
1046
+ /**
1047
+ * Получение информации о приложении
1048
+ *
1049
+ * @returns {Promise<Object>} Информация о приложении
1050
+ */
1051
+ async getApplicationInfo() {
1052
+ return this.validateApplication();
1053
+ }
1054
+
1055
+ /**
1056
+ * Валидация вебхука
1057
+ *
1058
+ * @param {Object} headers - Заголовки запроса
1059
+ * @param {string} body - Тело запроса
1060
+ * @returns {Object} Данные вебхука
1061
+ * @throws {Error} При невалидной подписи
1062
+ */
1063
+ verifyWebhook(headers, body) {
1064
+ const signature = headers['X-Pushler-Signature'] || headers['x-pushler-signature'] || '';
1065
+
1066
+ if (!signature) {
1067
+ throw new Error('Missing X-Pushler-Signature header');
1068
+ }
1069
+
1070
+ const expectedSignature = 'sha256=' + crypto.createHmac('sha256', this.appSecret)
1071
+ .update(body)
1072
+ .digest('hex');
1073
+
1074
+ // Timing-safe сравнение
1075
+ if (!this.timingSafeEqual(expectedSignature, signature)) {
1076
+ throw new Error('Invalid webhook signature');
1077
+ }
1078
+
1079
+ const data = JSON.parse(body);
1080
+ return data;
1081
+ }
1082
+
1083
+ /**
1084
+ * Timing-safe сравнение строк
1085
+ * @private
1086
+ */
1087
+ timingSafeEqual(a, b) {
1088
+ if (a.length !== b.length) {
1089
+ return false;
1090
+ }
1091
+
1092
+ let result = 0;
1093
+ for (let i = 0; i < a.length; i++) {
1094
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
1095
+ }
1096
+ return result === 0;
1097
+ }
1098
+
1099
+ /**
1100
+ * Выполнение подписанного HTTP запроса
1101
+ *
1102
+ * @private
1103
+ * @param {string} method - HTTP метод
1104
+ * @param {string} endpoint - API endpoint
1105
+ * @param {Object} [data] - Данные запроса
1106
+ * @returns {Promise<Object>} Результат запроса
1107
+ */
1108
+ async makeSignedRequest(method, endpoint, data = null) {
1109
+ const url = this.apiUrl + endpoint;
1110
+ const timestamp = Math.floor(Date.now() / 1000);
1111
+ const body = data ? JSON.stringify(data) : '';
1112
+ const signature = this.generateApiSignature(body, timestamp);
1113
+
1114
+ const headers = {
1115
+ 'Content-Type': 'application/json',
1116
+ 'Accept': 'application/json',
1117
+ 'User-Agent': 'PushlerRu-JS-SDK/1.0.0',
1118
+ 'X-Pushler-Key': this.appKey,
1119
+ 'X-Pushler-Signature': signature,
1120
+ 'X-Pushler-Timestamp': String(timestamp)
1121
+ };
1122
+
1123
+ const options = {
1124
+ method,
1125
+ headers,
1126
+ signal: AbortSignal.timeout(this.timeout)
1127
+ };
1128
+
1129
+ if (method === 'POST' && body) {
1130
+ options.body = body;
1131
+ }
1132
+
1133
+ const response = await fetch(url, options);
1134
+ const result = await response.json();
1135
+
1136
+ if (!response.ok) {
1137
+ const message = result.message || `HTTP request failed with status: ${response.status}`;
1138
+ const error = new Error(message);
1139
+ error.statusCode = response.status;
1140
+ error.response = result;
1141
+ throw error;
1142
+ }
1143
+
1144
+ return result;
1145
+ }
1146
+
1147
+ /**
1148
+ * Выполнение обычного HTTP запроса (без подписи)
1149
+ *
1150
+ * @private
1151
+ * @param {string} method - HTTP метод
1152
+ * @param {string} endpoint - API endpoint
1153
+ * @param {Object} [data] - Данные запроса
1154
+ * @returns {Promise<Object>} Результат запроса
1155
+ */
1156
+ async makeHttpRequest(method, endpoint, data = null) {
1157
+ const url = this.apiUrl + endpoint;
1158
+
1159
+ const headers = {
1160
+ 'Content-Type': 'application/json',
1161
+ 'Accept': 'application/json',
1162
+ 'User-Agent': 'PushlerRu-JS-SDK/1.0.0'
1163
+ };
1164
+
1165
+ const options = {
1166
+ method,
1167
+ headers,
1168
+ signal: AbortSignal.timeout(this.timeout)
1169
+ };
1170
+
1171
+ if (method === 'POST' && data) {
1172
+ options.body = JSON.stringify(data);
1173
+ }
1174
+
1175
+ const response = await fetch(url, options);
1176
+ const result = await response.json();
1177
+
1178
+ if (!response.ok) {
1179
+ throw new Error(`HTTP request failed with status: ${response.status}`);
1180
+ }
1181
+
1182
+ return result;
1183
+ }
1184
+
1185
+ /**
1186
+ * Получение App Key
1187
+ * @returns {string}
1188
+ */
1189
+ getAppKey() {
1190
+ return this.appKey;
1191
+ }
1192
+
1193
+ /**
1194
+ * Получение API URL
1195
+ * @returns {string}
1196
+ */
1197
+ getApiUrl() {
1198
+ return this.apiUrl;
1199
+ }
1200
+ }
1201
+
1202
+ var PushlerServer$1 = PushlerServer;
1203
+
1204
+ /**
1205
+ * Валидация данных канала
1206
+ * @param {string} channelName - Название канала
1207
+ * @param {string} channelType - Тип канала
1208
+ * @param {Object} options - Опции канала
1209
+ * @returns {Object} - Результат валидации
1210
+ */
1211
+
1212
+ /**
1213
+ * Генерация уникального ID сокета
1214
+ * @returns {string} - Уникальный ID сокета
1215
+ */
1216
+ function generateSocketId() {
1217
+ return Math.random().toString(36).substr(2, 9) + '.' + Math.floor(Math.random() * 1000000);
1218
+ }
1219
+
1220
+ // Создаем объект с методами для удобного API
1221
+ const Pushler = {
1222
+ // Клиентский SDK для браузера (WebSocket)
1223
+ CreateClient: (options) => new PushlerClient$1(options),
1224
+ Create: (options) => new PushlerClient$1(options), // Алиас для краткости
1225
+
1226
+ // Серверный SDK для Node.js (отправка сообщений через API)
1227
+ CreateServer: (options) => new PushlerServer$1(options),
1228
+ Server: (options) => new PushlerServer$1(options), // Алиас для краткости
1229
+
1230
+ Channel: Channel$1,
1231
+ ChannelTypes,
1232
+ ConnectionStates,
1233
+ generateSocketId
1234
+ };
1235
+
1236
+ exports.Channel = Channel$1;
1237
+ exports.ChannelTypes = ChannelTypes;
1238
+ exports.ConnectionStates = ConnectionStates;
1239
+ exports.Pushler = Pushler;
1240
+ exports.PushlerClient = PushlerClient$1;
1241
+ exports.PushlerServer = PushlerServer$1;
1242
+ exports.default = Pushler;
1243
+ exports.generateSocketId = generateSocketId;
1244
+
1245
+ Object.defineProperty(exports, '__esModule', { value: true });
1246
+
1247
+ }));
1248
+
1249
+ // Устанавливаем Pushler как сам объект для удобного API
1250
+ if (typeof window !== "undefined" && window.Pushler && window.Pushler.Pushler) {
1251
+ window.Pushler = window.Pushler.Pushler;
1252
+ }
1253
+ //# sourceMappingURL=pushler-ru.js.map