# 🔌 Координация WebSocket между вкладками браузера

## 📋 Описание проблемы

При открытии нескольких вкладок сайта каждая вкладка создает свое WebSocket соединение. Это приводит к:
- Дублированию сообщений
- Конфликтам при обработке событий (например, подпись пакетов)
- Лишней нагрузке на сервер
- Неожиданному поведению в UI

## ✅ Решение

Реализована система координации между вкладками, которая гарантирует, что **только одна вкладка** слушает WebSocket соединение для каждой группы пользователей.

---

## 🏗️ Архитектура

### Компоненты системы:

1. **localStorage** - хранит информацию об активной вкладке
2. **BroadcastChannel API** - мгновенное уведомление между вкладками
3. **Heartbeat механизм** - подтверждение активности
4. **Автоматическая смена** - переподключение при освобождении слота
5. **Приоритет страниц мониторинга** - страницы `/cab/monitor/` и `/cab/monitor/pl/` имеют приоритет

---

## 🔑 Ключевые переменные

### Глобальные переменные:

```javascript
var tabId = 'ws_tab_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Уникальный ID каждой вкладки

var isActiveTab = false;
// Флаг активности текущей вкладки

var wsLockKey = 'ws_active_tab_' + settings.group;
// Ключ localStorage для хранения ID активной вкладки

var wsLockTimestampKey = 'ws_active_tab_timestamp_' + settings.group;
// Ключ localStorage для хранения времени последнего heartbeat

var wsLockIsMonitorKey = 'ws_active_tab_is_monitor_' + settings.group;
// Ключ localStorage для хранения флага, является ли активная вкладка страницей мониторинга

var is_monitor_page = page_url.includes("/cab/monitor/") || page_url.includes("/cab/monitor/pl/");
// Флаг, является ли текущая вкладка страницей мониторинга

var broadcastChannel = null;
// BroadcastChannel для общения между вкладками

var heartbeatInterval = null;
// Интервал heartbeat (обновление активности)

var lockCheckInterval = null;
// Интервал проверки освобождения слота
```

---

## 🔄 Основные функции

### 1. `tryBecomeActive()`

**Назначение:** Попытка стать активной вкладкой для WebSocket

**Логика:**
```
1. Проверяет localStorage на наличие активной вкладки
2. Если слот свободен (нет записи или timestamp старше 5 сек):
   - Захватывает слот (сохраняет tabId)
   - Обновляет timestamp
   - Сохраняет флаг мониторинга (is_monitor_page)
   - Уведомляет через BroadcastChannel
   - Возвращает true
3. Если мы уже активны:
   - Обновляет timestamp
   - Обновляет флаг мониторинга
   - Возвращает true
4. Если другая вкладка активна:
   - Проверяет приоритет страницы мониторинга:
     * Если текущая - мониторинг, активная - нет → вытесняем (приоритет!)
     * Если текущая - не мониторинг, активная - мониторинг → возвращает false
     * Если обе одинаковые → возвращает false (обычная логика)
```

**Ключевые проверки:**
- Нет активной вкладки → слот свободен
- Timestamp старше 5 секунд → вкладка "умерла", слот свободен
- Timestamp свежий → вкладка активна, слот занят
- **Приоритет мониторинга:** страницы `/cab/monitor/` и `/cab/monitor/pl/` всегда вытесняют не-мониторинг страницы

---

### 2. `releaseConnection()`

**Назначение:** Освобождение WebSocket соединения и слота

**Действия:**
1. Закрывает WebSocket соединение (если открыто)
2. Останавливает heartbeat
3. Очищает localStorage (только если мы владельцы слота)
4. Сбрасывает флаг `isActiveTab`
5. **Запускает проверку слота** (`startLockCheck`) для попытки снова стать активной

**Вызывается при:**
- Закрытии вкладки (`beforeunload`)
- Получении уведомления о смене активной вкладки
- Вручную при необходимости

**Важно:** После освобождения соединения вкладка автоматически начинает проверять возможность снова стать активной

---

### 3. `startHeartbeat()`

**Назначение:** Подтверждение активности активной вкладки

**Механизм:**
- Запускается каждые **2 секунды**
- Обновляет `wsLockTimestampKey` в localStorage
- Позволяет другим вкладкам определить, жива ли активная вкладка

**Важно:** Heartbeat работает только у активной вкладки

---

### 4. `startLockCheck()`

**Назначение:** Периодическая проверка освобождения слота неактивными вкладками

**Механизм:**
- Запускается каждые **3 секунды** у неактивных вкладок
- Проверяет, не освободился ли слот
- Если слот свободен → пытается стать активной

**Защита от гонок:**
- `tryBecomeActive()` атомарно захватывает слот
- Только одна вкладка успеет захватить слот

---

### 5. `connect_ws()`

**Назначение:** Установка WebSocket соединения

**Изменения:**
- Добавлена проверка `tryBecomeActive()` перед подключением
- Подключается только если вкладка активна
- Запускает heartbeat после успешного подключения

**Проверка активности:**
```javascript
if (!isActiveTab) {
    console.log('Невозможно подключить WebSocket: другая вкладка активна');
    return;
}
```

---

### 6. `notifyUser(obj)`

**Назначение:** Обработка WebSocket сообщений

**Изменения:**
- Добавлена проверка активности перед обработкой
- Неактивные вкладки игнорируют все сообщения

```javascript
if (!isActiveTab) {
    console.log('notifyUser: вкладка не активна, игнорируем сообщение');
    return;
}
```

---

## 📡 BroadcastChannel API

### Инициализация:

```javascript
broadcastChannel = new BroadcastChannel('ws_tab_coordination_' + settings.group);
```

**Канал уникален для каждой группы**, что позволяет:
- Разным группам иметь разные активные вкладки
- Не мешать друг другу при координации

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

**При активации вкладки:**
```javascript
broadcastChannel.postMessage({
    type: 'tab_activated',
    tabId: tabId,
    isMonitor: is_monitor_page  // Флаг мониторинга
});
```

**При получении уведомления:**
```javascript
broadcastChannel.onmessage = function(event) {
    if (event.data.type === 'tab_activated' && event.data.tabId !== tabId) {
        var otherIsMonitor = event.data.isMonitor === true;
        
        // Если другая вкладка - мониторинг, а мы нет, освобождаем
        if (otherIsMonitor && !is_monitor_page) {
            releaseConnection();
        }
        // Если мы мониторинг, а другая нет - пытаемся захватить (приоритет)
        else if (!otherIsMonitor && is_monitor_page) {
            tryBecomeActive(); // Попытка вытеснить не-мониторинг
        }
        // Обе одинаковые - обычная логика
        else {
            releaseConnection();
        }
    }
};
```

---

## ⏱️ Таймауты и интервалы

| Механизм | Интервал | Назначение |
|----------|----------|------------|
| **Heartbeat** | 2 секунды | Обновление timestamp активности |
| **Lock Check** | 3 секунды | Проверка освобождения слота |
| **Timeout** | 5 секунд | Максимальное время без heartbeat |

**Логика:**
- Если heartbeat не обновлялся > 5 секунд → вкладка считается "мертвой"
- Слот автоматически освобождается для других вкладок

---

## 🎬 Сценарии работы

### Сценарий 1: Открытие первой вкладки

```
1. Вкладка 1 загружается
2. Вызывается tryBecomeActive()
3. localStorage пуст → слот свободен ✅
4. Вкладка 1 захватывает слот
5. connect_ws() → WebSocket подключается
6. startHeartbeat() → начинается подтверждение активности
7. WebSocket активен ✅
```

---

### Сценарий 2: Открытие второй вкладки

```
1. Вкладка 2 загружается
2. Вызывается tryBecomeActive()
3. localStorage содержит tabId вкладки 1
4. Timestamp свежий (< 5 сек) → слот занят ❌
5. Вкладка 2 не подключается к WebSocket
6. startLockCheck() → начинает проверку освобождения
7. WebSocket не активен, ждет освобождения ⏳
```

---

### Сценарий 3: Закрытие активной вкладки

```
1. Вкладка 1 закрывается
2. beforeunload → releaseConnection()
3. WebSocket закрывается
4. localStorage очищается (wsLockKey, wsLockTimestampKey)
5. BroadcastChannel уведомляет (не срабатывает, вкладка закрыта)

Вкладка 2 (через 3 сек):
6. lockCheckInterval проверяет слот
7. tryBecomeActive() → слот свободен ✅
8. Вкладка 2 захватывает слот
9. connect_ws() → WebSocket подключается
10. WebSocket активен ✅
```

---

### Сценарий 4: Зависание активной вкладки

```
1. Вкладка 1 зависает/крэшится
2. Heartbeat перестает обновляться
3. Timestamp не обновляется > 5 секунд

Вкладка 2 (через 3 сек):
4. lockCheckInterval проверяет слот
5. tryBecomeActive():
   - Timestamp старше 5 сек → вкладка 1 "мертва"
   - Слот считается свободным ✅
6. Вкладка 2 захватывает слот
7. connect_ws() → WebSocket подключается
8. WebSocket активен ✅
```

---

### Сценарий 5: Переключение между вкладками

```
1. Вкладка 1 активна, WebSocket работает
2. Пользователь переключается на вкладку 2
3. Вкладка 2 получает visibilitychange (visible)
4. tryBecomeActive():
   - Вкладка 1 активна (timestamp свежий)
   - Возвращает false ❌
5. Вкладка 2 остается неактивной

Если вкладка 1 закрыта:
6. Вкладка 2 через 3 сек проверяет слот
7. Слот свободен → вкладка 2 становится активной ✅
```

---

### Сценарий 6: Приоритет страницы мониторинга

```
1. Вкладка 1 (обычная страница) активна, WebSocket работает
2. Пользователь открывает вкладку 2 (/cab/monitor/pl/)
3. Вкладка 2 вызывает tryBecomeActive():
   - Вкладка 1 активна (timestamp свежий)
   - Проверяет: вкладка 1 - не мониторинг, вкладка 2 - мониторинг
   - ПРИОРИТЕТ! → Вытесняет вкладку 1 ✅
4. Вкладка 2 захватывает слот
5. BroadcastChannel уведомляет вкладку 1
6. Вкладка 1 получает уведомление:
   - Проверяет: другая вкладка - мониторинг, мы - нет
   - Освобождает слот и закрывает WebSocket
7. Вкладка 2 подключается к WebSocket ✅
```

---

### Сценарий 7: Две вкладки мониторинга

```
1. Вкладка 1 (/cab/monitor/) активна, WebSocket работает
2. Пользователь открывает вкладку 2 (/cab/monitor/pl/)
3. Вкладка 2 вызывает tryBecomeActive():
   - Вкладка 1 активна (timestamp свежий)
   - Обе вкладки - мониторинг
   - Нет приоритета → возвращает false ❌
4. Вкладка 2 остается неактивной
5. Работает обычная логика: первая захваченная вкладка остается активной
```

---

## 🎯 Приоритет страниц мониторинга

### Описание

Страницы мониторинга (`/cab/monitor/` и `/cab/monitor/pl/`) имеют **приоритет** над другими страницами при захвате WebSocket соединения.

### Правила приоритета:

1. **Страница мониторинга вытесняет обычную страницу:**
   - Если активна обычная страница, а открывается мониторинг → мониторинг получает соединение
   - Обычная страница автоматически освобождает слот

2. **Обычная страница НЕ вытесняет мониторинг:**
   - Если активна страница мониторинга, обычная страница ждет освобождения

3. **Между страницами мониторинга:**
   - Работает обычная логика "первая захватила"
   - Нет дополнительного приоритета между `/cab/monitor/` и `/cab/monitor/pl/`

### Реализация:

**Проверка в `tryBecomeActive()`:**
```javascript
if (is_monitor_page && !activeIsMonitor) {
    // Вытесняем не-мониторинг вкладку
    // Захватываем слот
    return true;
}
```

**BroadcastChannel обработка:**
- Мониторинг пытается захватить слот при получении уведомления о не-мониторинг вкладке
- Не-мониторинг освобождает слот при получении уведомления о мониторинг вкладке

### Преимущества:

✅ **Страницы подписи всегда работают** - при открытии страницы подписи пакетов она получает соединение
✅ **Нет конфликтов** - обычные страницы не мешают мониторингу
✅ **Автоматическое переключение** - не нужно закрывать другие вкладки вручную

---

## 🔒 Изоляция по группам

### Ключи localStorage:

```javascript
'ws_active_tab_' + group_id        // ID активной вкладки
'ws_active_tab_timestamp_' + group_id  // Timestamp активности
'ws_active_tab_is_monitor_' + group_id  // Флаг: является ли активная вкладка страницей мониторинга ('true'/'false')
```

### BroadcastChannel:

```javascript
'ws_tab_coordination_' + group_id
```

**Преимущества:**
- Пользователи разных групп не мешают друг другу
- Каждая группа имеет свой независимый слот
- Можно открыть активную вкладку для каждой группы

---

## 📊 Диаграмма состояний

```
┌─────────────────────────────────────────────────────────┐
│                    НОВАЯ ВКЛАДКА                        │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
         ┌───────────────────────┐
         │ tryBecomeActive()     │
         └───────────┬───────────┘
                     │
        ┌────────────┴────────────┐
        │                         │
        ▼                         ▼
   ┌─────────┐             ┌──────────┐
   │  TRUE   │             │  FALSE   │
   └────┬────┘             └────┬─────┘
        │                       │
        │                       ▼
        │              ┌─────────────────┐
        │              │  НЕАКТИВНАЯ     │
        │              │  startLockCheck │
        │              │  (каждые 3 сек) │
        │              └────────┬────────┘
        │                       │
        │                       │ (слот свободен)
        │                       ▼
        │              ┌─────────────────┐
        │              │ tryBecomeActive │
        │              └────────┬────────┘
        │                       │
        └───────────────────────┼───────────┐
                                │           │
                                ▼           ▼
                    ┌───────────────────────────┐
                    │      АКТИВНАЯ             │
                    │  connect_ws()             │
                    │  startHeartbeat()         │
                    │  (каждые 2 сек)           │
                    └───────────┬───────────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
        ┌──────────────────┐   ┌──────────────────┐
        │  beforeunload    │   │  Другая вкладка  │
        │  releaseConn()   │   │  активирована    │
        └──────────────────┘   │  releaseConn()   │
                               └──────────────────┘
```

---

## 🛡️ Защита от гонок (Race Conditions)

### Проблема:
Несколько вкладок могут одновременно попытаться захватить свободный слот.

### Решение:

1. **Атомарная проверка и захват:**
   ```javascript
   var activeTabId = localStorage.getItem(wsLockKey);
   if (!activeTabId || timestamp_expired) {
       localStorage.setItem(wsLockKey, tabId);  // Атомарно
   }
   ```

2. **Повторная проверка после захвата:**
   ```javascript
   var currentActive = localStorage.getItem(wsLockKey);
   if (currentActive === tabId) {
       // Мы успешно захватили слот
   }
   ```

3. **BroadcastChannel уведомление:**
   - Мгновенно информирует другие вкладки
   - Они сразу освобождают свои попытки

---

## 🔍 Логирование и отладка

### Сообщения в консоли:

**При активации:**
```
Эта вкладка стала активной для WebSocket. Tab ID: ws_tab_1234567890_abc123, Мониторинг: true
```

**При блокировке:**
```
Другая вкладка активна для WebSocket. Активная: ws_tab_1234567890_xyz789, Мониторинг: false
WebSocket не будет подключен в этой вкладке. Другая вкладка уже активна.
```

**При приоритете мониторинга:**
```
Вкладка мониторинга получает приоритет. Вытесняем вкладку: ws_tab_1234567890_xyz789
Вкладка мониторинга стала активной. Tab ID: ws_tab_1234567890_abc123
Вкладка мониторинга стала активной: ws_tab_1234567890_abc123. Освобождаем слот.
Вкладка мониторинга получает приоритет. Пытаемся захватить слот.
```

**При освобождении:**
```
Другая вкладка стала активной: ws_tab_1234567890_xyz789
Освобождение WebSocket соединения
```

**При получении сообщения неактивной вкладкой:**
```
Получено сообщение, но вкладка не активна. Игнорируем.
notifyUser: вкладка не активна, игнорируем сообщение
```

---

## ⚙️ Настройка таймаутов

### Текущие значения:

```javascript
HEARTBEAT_INTERVAL = 2000;    // 2 секунды - обновление активности
LOCK_CHECK_INTERVAL = 3000;   // 3 секунды - проверка освобождения
TIMEOUT_THRESHOLD = 5000;     // 5 секунд - таймаут неактивности
RECONNECT_DELAY = 1000;       // 1 секунда - задержка переподключения
```

### Рекомендации:

- **Heartbeat < Timeout** - heartbeat должен быть меньше таймаута
- **Lock Check < Timeout** - проверка должна быть чаще таймаута
- **Reconnect Delay** - достаточно 1 секунды для стабильности

---

## 🌐 Совместимость браузеров

### BroadcastChannel API:
- ✅ Chrome 54+
- ✅ Firefox 38+
- ✅ Safari 15.4+
- ✅ Edge 79+
- ⚠️ IE11 - не поддерживается (fallback через localStorage)

### Fallback механизм:

Если BroadcastChannel недоступен:
- Координация работает только через localStorage
- Смена активной вкладки происходит с задержкой (до 3 секунд)
- Функциональность сохраняется

---

## 🐛 Возможные проблемы и решения

### Проблема 1: Две вкладки одновременно активны

**Причина:** Редкая гонка при одновременной проверке

**Решение:** 
- Повторная проверка в `connect_ws()`
- Игнорирование сообщений неактивными вкладками
- Heartbeat подтверждает реальную активность

---

### Проблема 2: Вкладка не освобождает слот при зависании

**Решение:**
- Timeout 5 секунд определяет "мертвые" вкладки
- Другие вкладки автоматически захватывают слот

---

### Проблема 3: WebSocket не поднимается в других вкладках после закрытия активной

**Причина:** Неактивные вкладки останавливали проверку слота (`lockCheckInterval`) при получении уведомления о смене активной вкладки

**Решение:**
- `releaseConnection()` больше не останавливает `lockCheckInterval`
- После освобождения соединения вкладка автоматически запускает `startLockCheck()`
- Неактивные вкладки всегда имеют запущенную проверку слота
- Это гарантирует, что при закрытии активной вкладки другая вкладка подхватит соединение через 3 секунды

---

### Проблема 4: Частые переключения между вкладками

**Текущее поведение:**
- Активная вкладка остается активной даже при потере фокуса
- Переключение происходит только при закрытии активной вкладки

**Возможное улучшение:**
Можно добавить переключение при потере фокуса, но это может быть избыточно.

---

## 📝 Изменения в коде

### Файл: `script_socket_event.js`

**Добавлено:**
- Координация между вкладками
- Heartbeat механизм
- Проверка активности перед обработкой сообщений
- Автоматическое освобождение и захват слотов
- **Приоритет страниц мониторинга** (`/cab/monitor/`, `/cab/monitor/pl/`)
- Флаг мониторинга в localStorage и BroadcastChannel
- Вытеснение не-мониторинг страниц страницами мониторинга

**Модифицировано:**
- `connect_ws()` - проверка активности
- `ws.onmessage` - игнорирование неактивными вкладками
- `notifyUser()` - проверка активности
- `ws.onclose` - переподключение только активной вкладки
- `tryBecomeActive()` - добавлена логика приоритета мониторинга
- `releaseConnection()` - очистка флага мониторинга, **запуск проверки слота после освобождения**
- `startHeartbeat()` - обновление флага мониторинга
- `broadcastChannel.onmessage` - обработка приоритета мониторинга, **гарантия запуска проверки слота у неактивных вкладок**
- `visibilitychange` - учет приоритета при смене видимости

**Исправлено (Dec 2025):**
- Проблема, когда WebSocket не поднимался в других вкладках после закрытия активной
- Теперь все неактивные вкладки всегда имеют запущенную проверку слота
- После `releaseConnection()` вкладка автоматически запускает `startLockCheck()`

---

## ✅ Преимущества решения

1. ✅ **Одно соединение** - только одна вкладка слушает WebSocket
2. ✅ **Автоматическая смена** - при закрытии активной вкладки
3. ✅ **Защита от зависаний** - timeout определяет неактивные вкладки
4. ✅ **Изоляция по группам** - каждая группа независима
5. ✅ **Минимальная задержка** - BroadcastChannel для мгновенных уведомлений
6. ✅ **Совместимость** - работает даже без BroadcastChannel
7. ✅ **Безопасность** - защита от гонок и конфликтов
8. ✅ **Приоритет мониторинга** - страницы подписи пакетов всегда получают соединение
9. ✅ **Автоматическое вытеснение** - мониторинг вытесняет обычные страницы без задержки

---

## 🔮 Возможные улучшения

1. **Визуальная индикация:**
   - Показывать статус "активная/ожидание" в UI
   - Информация о количестве открытых вкладок

2. **Настройки:**
   - Позволить пользователю выбирать активную вкладку вручную
   - Настройка таймаутов через параметры

3. **Аналитика:**
   - Логирование переключений между вкладками
   - Метрики использования WebSocket

4. **Улучшенное переключение:**
   - Переключение при потере фокуса
   - Приоритет видимой вкладки

---

## 📚 Связанные файлы

- `esmo/src/class_v.esmo/ext/aspmo_monitor/script_socket_event.js` - основная реализация
- `esmo/src/class_v.esmo/ext/aspmo_monitor/script_mon_ws.js` - обработка событий мониторинга
- `esmo/src/class_v.esmo/ext/aspmo_monitor/c_monitor_pl.php` - страница подписи пакетов

---

**Дата создания:** 2024
**Версия:** 1.0
**Автор:** Система координации WebSocket между вкладками

