@pushler/vue 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.
- package/README.md +499 -0
- package/dist/index.d.ts +176 -0
- package/dist/pushler-vue.cjs.js +872 -0
- package/dist/pushler-vue.cjs.js.map +1 -0
- package/dist/pushler-vue.esm.js +861 -0
- package/dist/pushler-vue.esm.js.map +1 -0
- package/dist/pushler-vue.umd.js +876 -0
- package/dist/pushler-vue.umd.js.map +1 -0
- package/dist/pushler-vue.umd.min.js +7 -0
- package/dist/pushler-vue.umd.min.js.map +1 -0
- package/package.json +58 -0
- package/src/index.d.ts +176 -0
- package/src/index.js +58 -0
- package/src/plugin.js +65 -0
- package/src/usePushler.js +611 -0
- package/src/usePushlerChannel.js +137 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { ref, reactive, readonly, onUnmounted, watch, computed } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Состояния подключения
|
|
5
|
+
*/
|
|
6
|
+
export const ConnectionStates = {
|
|
7
|
+
CONNECTING: 'connecting',
|
|
8
|
+
CONNECTED: 'connected',
|
|
9
|
+
DISCONNECTED: 'disconnected',
|
|
10
|
+
RECONNECTING: 'reconnecting',
|
|
11
|
+
FAILED: 'failed'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Типы каналов
|
|
16
|
+
*/
|
|
17
|
+
export const ChannelTypes = {
|
|
18
|
+
PUBLIC: 'public',
|
|
19
|
+
PRIVATE: 'private',
|
|
20
|
+
PRESENCE: 'presence'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Vue composable для работы с Pushler WebSocket
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} options - Опции подключения
|
|
27
|
+
* @param {string} options.appKey - Ключ приложения
|
|
28
|
+
* @param {string} options.wsUrl - URL WebSocket сервера
|
|
29
|
+
* @param {string} options.authEndpoint - Эндпоинт авторизации для приватных каналов
|
|
30
|
+
* @param {boolean} options.autoConnect - Автоматическое подключение (по умолчанию false)
|
|
31
|
+
* @param {number} options.reconnectDelay - Задержка переподключения в мс (по умолчанию 1000)
|
|
32
|
+
* @param {number} options.maxReconnectAttempts - Максимум попыток переподключения (по умолчанию 5)
|
|
33
|
+
* @returns {Object} Реактивный объект с методами и состоянием
|
|
34
|
+
*/
|
|
35
|
+
export function usePushler(options = {}) {
|
|
36
|
+
// Реактивное состояние
|
|
37
|
+
const connectionState = ref(ConnectionStates.DISCONNECTED);
|
|
38
|
+
const socketId = ref(null);
|
|
39
|
+
const error = ref(null);
|
|
40
|
+
const reconnectAttempts = ref(0);
|
|
41
|
+
|
|
42
|
+
// Каналы
|
|
43
|
+
const channels = reactive(new Map());
|
|
44
|
+
const channelAliases = reactive(new Map());
|
|
45
|
+
|
|
46
|
+
// Внутренние переменные
|
|
47
|
+
let socket = null;
|
|
48
|
+
let reconnectTimer = null;
|
|
49
|
+
const eventListeners = new Map();
|
|
50
|
+
|
|
51
|
+
// Конфигурация
|
|
52
|
+
const config = {
|
|
53
|
+
appKey: options.appKey || '',
|
|
54
|
+
wsUrl: options.wsUrl || 'ws://localhost:8081/app',
|
|
55
|
+
authEndpoint: options.authEndpoint || '/pushler/auth',
|
|
56
|
+
autoConnect: options.autoConnect || false,
|
|
57
|
+
reconnectDelay: options.reconnectDelay || 1000,
|
|
58
|
+
maxReconnectAttempts: options.maxReconnectAttempts || 5,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Вычисляемые свойства
|
|
62
|
+
const isConnected = computed(() => connectionState.value === ConnectionStates.CONNECTED);
|
|
63
|
+
const isConnecting = computed(() => connectionState.value === ConnectionStates.CONNECTING);
|
|
64
|
+
const isReconnecting = computed(() => connectionState.value === ConnectionStates.RECONNECTING);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Форматирование имени канала с префиксом appKey
|
|
68
|
+
*/
|
|
69
|
+
function formatChannelName(channelName) {
|
|
70
|
+
if (channelName.startsWith(config.appKey + ':')) {
|
|
71
|
+
return channelName;
|
|
72
|
+
}
|
|
73
|
+
return `${config.appKey}:${channelName}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Извлечение оригинального имени канала
|
|
78
|
+
*/
|
|
79
|
+
function extractChannelName(fullChannelName) {
|
|
80
|
+
const prefix = config.appKey + ':';
|
|
81
|
+
if (fullChannelName.startsWith(prefix)) {
|
|
82
|
+
return fullChannelName.substring(prefix.length);
|
|
83
|
+
}
|
|
84
|
+
return fullChannelName;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Определение типа канала
|
|
89
|
+
*/
|
|
90
|
+
function getChannelType(channelName) {
|
|
91
|
+
const baseName = extractChannelName(channelName);
|
|
92
|
+
if (baseName.startsWith('private-')) return ChannelTypes.PRIVATE;
|
|
93
|
+
if (baseName.startsWith('presence-')) return ChannelTypes.PRESENCE;
|
|
94
|
+
return ChannelTypes.PUBLIC;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Подключение к WebSocket серверу
|
|
99
|
+
*/
|
|
100
|
+
function connect(connectOptions = {}) {
|
|
101
|
+
// Обновляем конфигурацию если переданы новые параметры
|
|
102
|
+
if (connectOptions.appKey) config.appKey = connectOptions.appKey;
|
|
103
|
+
if (connectOptions.wsUrl) config.wsUrl = connectOptions.wsUrl;
|
|
104
|
+
if (connectOptions.authEndpoint) config.authEndpoint = connectOptions.authEndpoint;
|
|
105
|
+
|
|
106
|
+
if (connectionState.value === ConnectionStates.CONNECTED ||
|
|
107
|
+
connectionState.value === ConnectionStates.CONNECTING) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
connectionState.value = ConnectionStates.CONNECTING;
|
|
112
|
+
error.value = null;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Формируем URL с ключом приложения
|
|
116
|
+
const wsUrl = config.wsUrl.endsWith('/')
|
|
117
|
+
? `${config.wsUrl}${config.appKey}`
|
|
118
|
+
: `${config.wsUrl}/${config.appKey}`;
|
|
119
|
+
|
|
120
|
+
console.log('[Pushler Vue] Connecting to:', wsUrl);
|
|
121
|
+
socket = new WebSocket(wsUrl);
|
|
122
|
+
setupSocketHandlers();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error('[Pushler Vue] Connection error:', err);
|
|
125
|
+
connectionState.value = ConnectionStates.FAILED;
|
|
126
|
+
error.value = err.message;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Настройка обработчиков WebSocket
|
|
132
|
+
*/
|
|
133
|
+
function setupSocketHandlers() {
|
|
134
|
+
socket.onopen = () => {
|
|
135
|
+
console.log('[Pushler Vue] WebSocket opened, waiting for connection_established...');
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
socket.onmessage = (event) => {
|
|
139
|
+
handleMessage(event);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
socket.onclose = (event) => {
|
|
143
|
+
console.log('[Pushler Vue] WebSocket closed:', event.code, event.reason);
|
|
144
|
+
connectionState.value = ConnectionStates.DISCONNECTED;
|
|
145
|
+
socketId.value = null;
|
|
146
|
+
|
|
147
|
+
// Автопереподключение
|
|
148
|
+
if (event.code !== 1000 && reconnectAttempts.value < config.maxReconnectAttempts) {
|
|
149
|
+
scheduleReconnect();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
socket.onerror = (err) => {
|
|
154
|
+
console.error('[Pushler Vue] WebSocket error:', err);
|
|
155
|
+
connectionState.value = ConnectionStates.FAILED;
|
|
156
|
+
error.value = 'Connection failed';
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Обработка входящих сообщений
|
|
162
|
+
*/
|
|
163
|
+
function handleMessage(event) {
|
|
164
|
+
try {
|
|
165
|
+
// Поддержка нескольких сообщений в одном event
|
|
166
|
+
const messages = event.data.trim().split('\n').filter(line => line.trim());
|
|
167
|
+
|
|
168
|
+
for (const msgData of messages) {
|
|
169
|
+
processMessage(JSON.parse(msgData));
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('[Pushler Vue] Error parsing message:', err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Обработка отдельного сообщения
|
|
178
|
+
*/
|
|
179
|
+
function processMessage(message) {
|
|
180
|
+
switch (message.event) {
|
|
181
|
+
case 'pushler:connection_established':
|
|
182
|
+
handleConnectionEstablished(message.data);
|
|
183
|
+
break;
|
|
184
|
+
case 'pushler:subscription_succeeded':
|
|
185
|
+
handleSubscriptionSucceeded(message);
|
|
186
|
+
break;
|
|
187
|
+
case 'pushler:auth_success':
|
|
188
|
+
handleAuthSuccess(message);
|
|
189
|
+
break;
|
|
190
|
+
case 'pushler:auth_error':
|
|
191
|
+
handleAuthError(message);
|
|
192
|
+
break;
|
|
193
|
+
default:
|
|
194
|
+
handleChannelMessage(message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Обработка установления соединения
|
|
200
|
+
*/
|
|
201
|
+
function handleConnectionEstablished(data) {
|
|
202
|
+
socketId.value = data.socket_id;
|
|
203
|
+
connectionState.value = ConnectionStates.CONNECTED;
|
|
204
|
+
reconnectAttempts.value = 0;
|
|
205
|
+
|
|
206
|
+
console.log('[Pushler Vue] Connected with socket ID:', socketId.value);
|
|
207
|
+
|
|
208
|
+
// Переподписка на все каналы
|
|
209
|
+
resubscribeAllChannels();
|
|
210
|
+
|
|
211
|
+
emit('connected', { socketId: socketId.value });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Переподписка на все каналы
|
|
216
|
+
*/
|
|
217
|
+
function resubscribeAllChannels() {
|
|
218
|
+
for (const [name, channel] of channels.entries()) {
|
|
219
|
+
channel.subscribed = false;
|
|
220
|
+
performChannelSubscription(channel);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Обработка успешной подписки
|
|
226
|
+
*/
|
|
227
|
+
function handleSubscriptionSucceeded(message) {
|
|
228
|
+
const channel = channels.get(message.channel);
|
|
229
|
+
if (channel) {
|
|
230
|
+
channel.subscribed = true;
|
|
231
|
+
channel.emit('subscribed', message.data || {});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Обработка успешной авторизации
|
|
237
|
+
*/
|
|
238
|
+
function handleAuthSuccess(message) {
|
|
239
|
+
const channel = channels.get(message.data.channel);
|
|
240
|
+
if (channel) {
|
|
241
|
+
channel.subscribed = true;
|
|
242
|
+
channel.emit('subscribed', message.data);
|
|
243
|
+
}
|
|
244
|
+
emit('auth_success', message.data);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Обработка ошибки авторизации
|
|
249
|
+
*/
|
|
250
|
+
function handleAuthError(message) {
|
|
251
|
+
error.value = message.data.error;
|
|
252
|
+
emit('auth_error', message.data);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Обработка сообщения канала
|
|
257
|
+
*/
|
|
258
|
+
function handleChannelMessage(message) {
|
|
259
|
+
const { channel: channelName, event, data } = message;
|
|
260
|
+
const channel = channels.get(channelName);
|
|
261
|
+
|
|
262
|
+
if (channel) {
|
|
263
|
+
channel.emit(event, data);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
emit('message', {
|
|
267
|
+
channel: extractChannelName(channelName),
|
|
268
|
+
event,
|
|
269
|
+
data
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Планирование переподключения
|
|
275
|
+
*/
|
|
276
|
+
function scheduleReconnect() {
|
|
277
|
+
if (reconnectTimer) {
|
|
278
|
+
clearTimeout(reconnectTimer);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
reconnectAttempts.value++;
|
|
282
|
+
connectionState.value = ConnectionStates.RECONNECTING;
|
|
283
|
+
|
|
284
|
+
const delay = config.reconnectDelay * reconnectAttempts.value;
|
|
285
|
+
console.log(`[Pushler Vue] Reconnecting in ${delay}ms (attempt ${reconnectAttempts.value}/${config.maxReconnectAttempts})`);
|
|
286
|
+
|
|
287
|
+
reconnectTimer = setTimeout(() => {
|
|
288
|
+
connect();
|
|
289
|
+
}, delay);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Отключение от сервера
|
|
294
|
+
*/
|
|
295
|
+
function disconnect() {
|
|
296
|
+
if (reconnectTimer) {
|
|
297
|
+
clearTimeout(reconnectTimer);
|
|
298
|
+
reconnectTimer = null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (socket) {
|
|
302
|
+
socket.close(1000, 'User disconnect');
|
|
303
|
+
socket = null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
connectionState.value = ConnectionStates.DISCONNECTED;
|
|
307
|
+
socketId.value = null;
|
|
308
|
+
channels.clear();
|
|
309
|
+
channelAliases.clear();
|
|
310
|
+
reconnectAttempts.value = 0;
|
|
311
|
+
|
|
312
|
+
emit('disconnected');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Отправка сообщения через WebSocket
|
|
317
|
+
*/
|
|
318
|
+
function sendMessage(message) {
|
|
319
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
320
|
+
socket.send(JSON.stringify(message));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Подписка на канал
|
|
326
|
+
* @param {string} channelName - Имя канала
|
|
327
|
+
* @param {Object} options - Опции (signature для приватных, user для presence)
|
|
328
|
+
* @returns {Object} Реактивный объект канала
|
|
329
|
+
*/
|
|
330
|
+
function subscribe(channelName, subscribeOptions = {}) {
|
|
331
|
+
const fullChannelName = formatChannelName(channelName);
|
|
332
|
+
|
|
333
|
+
if (channels.has(fullChannelName)) {
|
|
334
|
+
return channels.get(fullChannelName);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Создаем реактивный канал
|
|
338
|
+
const channel = reactive({
|
|
339
|
+
name: fullChannelName,
|
|
340
|
+
originalName: channelName,
|
|
341
|
+
type: getChannelType(channelName),
|
|
342
|
+
subscribed: false,
|
|
343
|
+
options: subscribeOptions,
|
|
344
|
+
listeners: new Map(),
|
|
345
|
+
|
|
346
|
+
// Методы канала
|
|
347
|
+
on(event, callback) {
|
|
348
|
+
if (!this.listeners.has(event)) {
|
|
349
|
+
this.listeners.set(event, []);
|
|
350
|
+
}
|
|
351
|
+
this.listeners.get(event).push(callback);
|
|
352
|
+
return this;
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
off(event, callback) {
|
|
356
|
+
if (this.listeners.has(event)) {
|
|
357
|
+
const list = this.listeners.get(event);
|
|
358
|
+
const idx = list.indexOf(callback);
|
|
359
|
+
if (idx > -1) list.splice(idx, 1);
|
|
360
|
+
}
|
|
361
|
+
return this;
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
emit(event, data) {
|
|
365
|
+
if (this.listeners.has(event)) {
|
|
366
|
+
this.listeners.get(event).forEach(cb => {
|
|
367
|
+
try {
|
|
368
|
+
cb(data);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.error('[Pushler Vue] Channel event error:', err);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
unsubscribe() {
|
|
377
|
+
unsubscribe(channelName);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
channels.set(fullChannelName, channel);
|
|
382
|
+
channelAliases.set(channelName, fullChannelName);
|
|
383
|
+
|
|
384
|
+
// Подписываемся если уже подключены
|
|
385
|
+
if (connectionState.value === ConnectionStates.CONNECTED) {
|
|
386
|
+
performChannelSubscription(channel);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return channel;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Выполнение подписки на канал
|
|
394
|
+
*/
|
|
395
|
+
async function performChannelSubscription(channel) {
|
|
396
|
+
if (connectionState.value !== ConnectionStates.CONNECTED) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const channelType = channel.type;
|
|
401
|
+
|
|
402
|
+
// Публичные каналы
|
|
403
|
+
if (channelType === ChannelTypes.PUBLIC) {
|
|
404
|
+
sendMessage({
|
|
405
|
+
event: 'pushler:subscribe',
|
|
406
|
+
data: {
|
|
407
|
+
channel: channel.name,
|
|
408
|
+
app_key: config.appKey
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Приватные и presence каналы требуют авторизации
|
|
415
|
+
try {
|
|
416
|
+
const authData = await getAuthData(channel);
|
|
417
|
+
sendMessage({
|
|
418
|
+
event: 'pushler:auth',
|
|
419
|
+
data: authData
|
|
420
|
+
});
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error('[Pushler Vue] Auth error:', err);
|
|
423
|
+
error.value = err.message;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Получение данных авторизации для приватных/presence каналов
|
|
429
|
+
*
|
|
430
|
+
* Для приватных каналов подпись ДОЛЖНА быть получена с вашего бэкенда,
|
|
431
|
+
* либо передана в options.signature при подписке.
|
|
432
|
+
*
|
|
433
|
+
* НИКОГДА не храните секретный ключ в клиентском коде!
|
|
434
|
+
*/
|
|
435
|
+
async function getAuthData(channel) {
|
|
436
|
+
if (!socketId.value) {
|
|
437
|
+
throw new Error('Socket ID not available');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const authData = {
|
|
441
|
+
app_key: config.appKey,
|
|
442
|
+
channel: channel.name,
|
|
443
|
+
socket_id: socketId.value
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Если подпись уже предоставлена в options
|
|
447
|
+
if (channel.options.signature) {
|
|
448
|
+
authData.signature = channel.options.signature;
|
|
449
|
+
} else {
|
|
450
|
+
// Запрашиваем подпись с бэкенда
|
|
451
|
+
authData.signature = await fetchSignature(channel);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Данные пользователя для presence каналов
|
|
455
|
+
if (channel.type === ChannelTypes.PRESENCE && channel.options.user) {
|
|
456
|
+
authData.user = channel.options.user;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return authData;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Запрос подписи с бэкенда
|
|
464
|
+
*
|
|
465
|
+
* Ваш бэкенд должен:
|
|
466
|
+
* 1. Проверить авторизацию пользователя
|
|
467
|
+
* 2. Сгенерировать HMAC-SHA256 подпись с секретным ключом
|
|
468
|
+
* 3. Вернуть подпись клиенту
|
|
469
|
+
*/
|
|
470
|
+
async function fetchSignature(channel) {
|
|
471
|
+
const response = await fetch(config.authEndpoint, {
|
|
472
|
+
method: 'POST',
|
|
473
|
+
headers: { 'Content-Type': 'application/json' },
|
|
474
|
+
credentials: 'include', // Включаем куки для авторизации
|
|
475
|
+
body: JSON.stringify({
|
|
476
|
+
channel_name: channel.name,
|
|
477
|
+
socket_id: socketId.value,
|
|
478
|
+
app_key: config.appKey,
|
|
479
|
+
user_data: channel.options.user
|
|
480
|
+
})
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (!response.ok) {
|
|
484
|
+
const err = await response.json().catch(() => ({ error: 'Auth request failed' }));
|
|
485
|
+
throw new Error(err.error || 'Failed to get signature');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const data = await response.json();
|
|
489
|
+
return data.signature;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Отписка от канала
|
|
494
|
+
*/
|
|
495
|
+
function unsubscribe(channelName) {
|
|
496
|
+
const fullChannelName = channelAliases.get(channelName) || formatChannelName(channelName);
|
|
497
|
+
const channel = channels.get(fullChannelName);
|
|
498
|
+
|
|
499
|
+
if (channel) {
|
|
500
|
+
if (channel.subscribed && connectionState.value === ConnectionStates.CONNECTED) {
|
|
501
|
+
sendMessage({
|
|
502
|
+
event: 'pushler:unsubscribe',
|
|
503
|
+
data: { channel: fullChannelName }
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
channels.delete(fullChannelName);
|
|
508
|
+
channelAliases.delete(channelName);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Получение канала по имени
|
|
514
|
+
*/
|
|
515
|
+
function channel(channelName) {
|
|
516
|
+
const fullChannelName = channelAliases.get(channelName) || formatChannelName(channelName);
|
|
517
|
+
return channels.get(fullChannelName);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Получение списка каналов
|
|
522
|
+
*/
|
|
523
|
+
function getChannels() {
|
|
524
|
+
return Array.from(channels.values()).map(ch => ({
|
|
525
|
+
name: ch.originalName,
|
|
526
|
+
fullName: ch.name,
|
|
527
|
+
type: ch.type,
|
|
528
|
+
subscribed: ch.subscribed
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Подписка на глобальное событие
|
|
534
|
+
*/
|
|
535
|
+
function on(event, callback) {
|
|
536
|
+
if (!eventListeners.has(event)) {
|
|
537
|
+
eventListeners.set(event, []);
|
|
538
|
+
}
|
|
539
|
+
eventListeners.get(event).push(callback);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Отписка от глобального события
|
|
544
|
+
*/
|
|
545
|
+
function off(event, callback) {
|
|
546
|
+
if (eventListeners.has(event)) {
|
|
547
|
+
const list = eventListeners.get(event);
|
|
548
|
+
const idx = list.indexOf(callback);
|
|
549
|
+
if (idx > -1) list.splice(idx, 1);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Вызов глобального события
|
|
555
|
+
*/
|
|
556
|
+
function emit(event, data) {
|
|
557
|
+
if (eventListeners.has(event)) {
|
|
558
|
+
eventListeners.get(event).forEach(cb => {
|
|
559
|
+
try {
|
|
560
|
+
cb(data);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
console.error('[Pushler Vue] Event error:', err);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Автоматическое отключение при размонтировании компонента
|
|
569
|
+
onUnmounted(() => {
|
|
570
|
+
disconnect();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Автоподключение если указано
|
|
574
|
+
if (config.autoConnect && config.appKey) {
|
|
575
|
+
connect();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
// Состояние (readonly для защиты от внешних изменений)
|
|
580
|
+
connectionState: readonly(connectionState),
|
|
581
|
+
socketId: readonly(socketId),
|
|
582
|
+
error: readonly(error),
|
|
583
|
+
reconnectAttempts: readonly(reconnectAttempts),
|
|
584
|
+
|
|
585
|
+
// Вычисляемые свойства
|
|
586
|
+
isConnected,
|
|
587
|
+
isConnecting,
|
|
588
|
+
isReconnecting,
|
|
589
|
+
|
|
590
|
+
// Методы
|
|
591
|
+
connect,
|
|
592
|
+
disconnect,
|
|
593
|
+
subscribe,
|
|
594
|
+
unsubscribe,
|
|
595
|
+
channel,
|
|
596
|
+
getChannels,
|
|
597
|
+
on,
|
|
598
|
+
off,
|
|
599
|
+
|
|
600
|
+
// Утилиты
|
|
601
|
+
formatChannelName,
|
|
602
|
+
extractChannelName,
|
|
603
|
+
getChannelType,
|
|
604
|
+
|
|
605
|
+
// Константы
|
|
606
|
+
ConnectionStates,
|
|
607
|
+
ChannelTypes,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export default usePushler;
|