npm package: @pushler/vue
Description: Vue.js SDK for Pushler.ru real-time messaging
Author: Pushler.ru
Latest version: 1.1.0

# Pushler Vue SDK

Реактивный Vue.js SDK для работы с Pushler.ru WebSocket сервером.

[![npm version](https://img.shields.io/npm/v/@pushler/vue.svg)](https://www.npmjs.com/package/@pushler/vue)
[![npm downloads](https://img.shields.io/npm/dm/@pushler/vue.svg)](https://www.npmjs.com/package/@pushler/vue)
[![license](https://img.shields.io/npm/l/@pushler/vue.svg)](https://github.com/pushler/vue-sdk/blob/main/LICENSE)

## Установка

### npm / yarn / pnpm

```bash
npm install @pushler/vue

# или
yarn add @pushler/vue

# или
pnpm add @pushler/vue
```

### CDN

```html
<!-- Последняя версия -->
<script src="https://unpkg.com/@pushler/vue"></script>

<!-- Конкретная версия -->
<script src="https://unpkg.com/@pushler/vue@1.0.0/dist/pushler-vue.umd.min.js"></script>

<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@pushler/vue"></script>
```

## Быстрый старт

### Использование composable

```vue
<script setup>
import { usePushler } from '@pushler/vue';

// Создаём экземпляр Pushler
const pushler = usePushler({
  appKey: 'your-app-key',
  autoConnect: true, // Автоматическое подключение
});

// Подписка на канал
const channel = pushler.subscribe('my-channel');

// Слушаем события
channel.on('message', (data) => {
  console.log('Получено сообщение:', data);
});

channel.on('notification', (data) => {
  console.log('Уведомление:', data);
});
</script>

<template>
  <div>
    <p>Статус: {{ pushler.connectionState }}</p>
    <p v-if="pushler.socketId">Socket ID: {{ pushler.socketId }}</p>
    <button @click="pushler.connect()" :disabled="pushler.isConnected.value">
      Подключиться
    </button>
    <button @click="pushler.disconnect()" :disabled="!pushler.isConnected.value">
      Отключиться
    </button>
  </div>
</template>
```

### Использование Vue Plugin

```js
// main.js
import { createApp } from 'vue';
import { PushlerPlugin } from '@pushler/vue';
import App from './App.vue';

const app = createApp(App);

app.use(PushlerPlugin, {
  appKey: 'your-app-key',
  autoConnect: true,
});

app.mount('#app');
```

Затем в любом компоненте:

```vue
<script setup>
import { inject } from 'vue';
import { PushlerKey } from '@pushler/vue';

const pushler = inject(PushlerKey);

const channel = pushler.subscribe('notifications');
channel.on('new', (data) => {
  console.log('Новое уведомление:', data);
});
</script>
```

## API Reference

### `usePushler(options)`

Основной composable для работы с WebSocket подключением.

#### Опции

| Параметр | Тип | По умолчанию | Описание |
|----------|-----|--------------|----------|
| `appKey` | `string` | — | Ключ приложения Pushler (обязательный) |
| `authEndpoint` | `string` | `'/pushler/auth'` | Эндпоинт для авторизации приватных каналов |
| `autoConnect` | `boolean` | `false` | Автоматическое подключение при инициализации |
| `reconnectDelay` | `number` | `1000` | Задержка между попытками переподключения (мс) |
| `maxReconnectAttempts` | `number` | `5` | Максимальное количество попыток переподключения |

#### Возвращаемые значения

##### Реактивное состояние

```js
const pushler = usePushler(options);

// Состояние подключения: 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'failed'
pushler.connectionState // Ref<string>

// ID сокета (после подключения)
pushler.socketId // Ref<string | null>

// Ошибка (если есть)
pushler.error // Ref<string | null>

// Количество попыток переподключения
pushler.reconnectAttempts // Ref<number>

// Вычисляемые свойства
pushler.isConnected // ComputedRef<boolean>
pushler.isConnecting // ComputedRef<boolean>
pushler.isReconnecting // ComputedRef<boolean>
```

##### Методы

```js
// Подключение к серверу
pushler.connect(options?: { appKey? })

// Отключение
pushler.disconnect()

// Подписка на канал
const channel = pushler.subscribe('channel-name', { 
  signature?: 'pre-signed-signature',
  user?: { id: 1, name: 'User' } // для presence-каналов
})

// Отписка от канала
pushler.unsubscribe('channel-name')

// Получение канала
const channel = pushler.channel('channel-name')

// Получение списка каналов
const channels = pushler.getChannels()
// [{ name, fullName, type, subscribed }]

// Подписка на глобальные события
pushler.on('connected', (data) => console.log('Connected:', data.socketId))
pushler.on('message', (msg) => console.log('Message:', msg))
pushler.on('auth_error', (err) => console.error('Auth error:', err))

// Отписка от событий
pushler.off('connected', handler)
```

### Канал

```js
const channel = pushler.subscribe('my-channel');

// Свойства канала
channel.name        // Полное имя с appKey
channel.originalName // Оригинальное имя
channel.type        // 'public' | 'private' | 'presence'
channel.subscribed  // boolean

// Методы
channel.on('event-name', callback)
channel.off('event-name', callback)
channel.unsubscribe()
```

### Типы каналов

```js
// Публичные каналы (без авторизации)
pushler.subscribe('public-channel')

// Приватные каналы (требуют авторизации через бэкенд)
pushler.subscribe('private-channel')
// SDK автоматически запросит подпись с authEndpoint

// Или можно передать предварительно полученную подпись
pushler.subscribe('private-channel', {
  signature: 'hmac-signature-from-backend'
})

// Presence-каналы (информация о присутствии)
pushler.subscribe('presence-chat', {
  user: {
    id: 123,
    name: 'Иван Петров',
    avatar: 'https://...'
  }
})
```

## Безопасность

### ⚠️ Важно: Секретный ключ

**НИКОГДА не храните секретный ключ в клиентском коде!**

Секретный ключ (`secret`) должен храниться только на вашем бэкенде. Для приватных каналов SDK автоматически запрашивает подпись с вашего бэкенда через `authEndpoint`.

### Схема авторизации приватных каналов

```
┌─────────────┐    1. subscribe('private-chat')    ┌─────────────────┐
│   Browser   │ ──────────────────────────────────►│  Pushler WS     │
│   (SDK)     │                                    │  Server         │
└──────┬──────┘                                    └────────┬────────┘
       │                                                    │
       │ 2. POST /pushler/auth                              │
       │    { channel, socket_id }                          │
       ▼                                                    │
┌─────────────┐    3. Проверка авторизации                  │
│  Ваш        │    + генерация подписи (с secret)           │
│  Backend    │ ───────────────────────────────────────────►│
└─────────────┘    4. signature                             │
```

### Пример бэкенда для авторизации

```php
// Laravel пример
Route::post('/pushler/auth', function (Request $request) {
    $user = auth()->user();
    
    // Проверяем право доступа к каналу
    $channelName = $request->channel_name;
    if (!$user->canAccessChannel($channelName)) {
        return response()->json(['error' => 'Forbidden'], 403);
    }
    
    // Генерируем подпись
    $socketId = $request->socket_id;
    $signature = hash_hmac('sha256', "{$socketId}:{$channelName}", env('PUSHLER_SECRET'));
    
    return response()->json(['signature' => $signature]);
});
```

### Кастомные заголовки авторизации

Вы можете передать дополнительные заголовки для запросов авторизации:

```js
// Статические заголовки
const pushler = usePushler({
  appKey: 'your-app-key',
  authEndpoint: '/api/pushler/auth',
  authHeaders: {
    'Authorization': 'Bearer your-token',
    'X-Custom-Header': 'value'
  }
});

// Динамические заголовки (рекомендуется)
const pushler = usePushler({
  appKey: 'your-app-key',
  authEndpoint: '/api/pushler/auth',
  getAuthHeaders: () => ({
    'Authorization': `Bearer ${authStore.token}`
  })
});
```

При использовании `authHeaders` или `getAuthHeaders` SDK не отправляет cookies (`credentials: 'include'`).

## Promise-based API

### Ожидание подключения

`connect()` возвращает Promise, который резолвится при успешном подключении:

```js
const pushler = usePushler({ appKey: 'your-app-key' });

// Ожидаем подключения
const { socketId } = await pushler.connect();
console.log('Подключены с socketId:', socketId);

// Теперь можно подписываться на приватные каналы
pushler.subscribe('private-user-123');
```

### Метод `waitForConnection()`

Для случаев, когда нужно дождаться соединения с таймаутом:

```js
try {
  await pushler.waitForConnection(10000); // 10 секунд
  console.log('Подключены!');
} catch (error) {
  console.error('Не удалось подключиться:', error.message);
}
```

### `usePushlerChannel(pushler, channelName, options)`

Composable для работы с отдельным каналом.

```vue
<script setup>
import { usePushler, usePushlerChannel } from '@pushler/vue';

const pushler = usePushler({ ... });
const { channel, isSubscribed, events, lastEvent, on, clearEvents } = usePushlerChannel(
  pushler,
  'notifications',
  { 
    autoSubscribe: true,
    maxEvents: 50 
  }
);

on('new', (data) => {
  console.log('Новое уведомление:', data);
});
</script>

<template>
  <div>
    <p>Подписан: {{ isSubscribed }}</p>
    <p>Последнее событие: {{ lastEvent }}</p>
    <ul>
      <li v-for="event in events" :key="event.timestamp">
        {{ event.event }}: {{ event.data }}
      </li>
    </ul>
    <button @click="clearEvents">Очистить</button>
  </div>
</template>
```

## Константы

```js
import { ConnectionStates, ChannelTypes } from '@pushler/vue';

// Состояния подключения
ConnectionStates.CONNECTING   // 'connecting'
ConnectionStates.CONNECTED    // 'connected'
ConnectionStates.DISCONNECTED // 'disconnected'
ConnectionStates.RECONNECTING // 'reconnecting'
ConnectionStates.FAILED       // 'failed'

// Типы каналов
ChannelTypes.PUBLIC   // 'public'
ChannelTypes.PRIVATE  // 'private'
ChannelTypes.PRESENCE // 'presence'
```

## Примеры

### Чат в реальном времени

```vue
<script setup>
import { ref } from 'vue';
import { usePushler } from '@pushler/vue';

const pushler = usePushler({
  appKey: 'chat-app-key',
  autoConnect: true,
});

const messages = ref([]);
const newMessage = ref('');

// Подписка на канал чата
const chatChannel = pushler.subscribe('chat-room-1');

chatChannel.on('message', (data) => {
  messages.value.push(data);
});

chatChannel.on('user_joined', (data) => {
  messages.value.push({
    type: 'system',
    text: `${data.name} присоединился к чату`
  });
});

async function sendMessage() {
  // Отправка через ваш API
  await fetch('/api/chat/send', {
    method: 'POST',
    body: JSON.stringify({
      channel: 'chat-room-1',
      message: newMessage.value
    })
  });
  newMessage.value = '';
}
</script>
```

### Уведомления

```vue
<script setup>
import { usePushler } from '@pushler/vue';
import { useNotifications } from './composables/useNotifications';

const pushler = usePushler({ autoConnect: true, ... });
const { show } = useNotifications();

// Приватный канал для уведомлений пользователя
const notificationChannel = pushler.subscribe('private-user-notifications');

notificationChannel.on('new', (data) => {
  show({
    title: data.title,
    message: data.message,
    type: data.type || 'info'
  });
});

notificationChannel.on('badge_update', (data) => {
  // Обновление счётчика непрочитанных
  document.title = data.count > 0 
    ? `(${data.count}) Мой сайт` 
    : 'Мой сайт';
});
</script>
```

### Индикатор набора текста (Typing indicator)

```vue
<script setup>
import { ref, computed } from 'vue';
import { usePushler } from '@pushler/vue';

const pushler = usePushler({ ... });
const typingUsers = ref(new Set());

const channel = pushler.subscribe('presence-chat', {
  user: { id: currentUserId, name: currentUserName }
});

channel.on('typing_start', (data) => {
  typingUsers.value.add(data.user_name);
});

channel.on('typing_stop', (data) => {
  typingUsers.value.delete(data.user_name);
});

const typingText = computed(() => {
  const users = Array.from(typingUsers.value);
  if (users.length === 0) return '';
  if (users.length === 1) return `${users[0]} печатает...`;
  if (users.length === 2) return `${users.join(' и ')} печатают...`;
  return `${users.length} человек печатают...`;
});
</script>
```

## Использование через CDN

```html
<!DOCTYPE html>
<html>
<head>
  <title>Pushler Vue Example</title>
  <script src="https://unpkg.com/vue@3"></script>
  <script src="https://unpkg.com/@pushler/vue"></script>
</head>
<body>
  <div id="app">
    <p>Status: {{ connectionState }}</p>
    <p v-if="socketId">Socket ID: {{ socketId }}</p>
    <button @click="connect" :disabled="isConnected">Connect</button>
    <button @click="disconnect" :disabled="!isConnected">Disconnect</button>
  </div>

  <script>
    const { createApp } = Vue;
    const { usePushler } = PushlerVue;

    createApp({
      setup() {
        const pushler = usePushler({
          appKey: 'your-app-key'
        });

        return {
          connectionState: pushler.connectionState,
          socketId: pushler.socketId,
          isConnected: pushler.isConnected,
          connect: () => pushler.connect(),
          disconnect: () => pushler.disconnect()
        };
      }
    }).mount('#app');
  </script>
</body>
</html>
```

## TypeScript

SDK включает TypeScript определения. Импортируйте типы:

```ts
import type { 
  UsePushlerReturn, 
  PushlerChannel, 
  ChannelInfo,
  ConnectionState,
  ChannelType 
} from '@pushler/vue';
```

## Лицензия

MIT