В реальных сетевых условиях WebSocket-соединение, поверх которого работает STOMP.js, не является стабильным по умолчанию. Разрыв может произойти по множеству причин: кратковременная потеря интернет-соединения, переход устройства между сетями, перезапуск брокера сообщений, таймауты прокси или балансировщика нагрузки, а также закрытие соединения сервером из-за политики heartbeat.
STOMP.js не гарантирует автоматическое восстановление соединения в базовой конфигурации клиента, поэтому механизм переподключения реализуется на уровне прикладного кода. Отсутствие стратегии восстановления приводит к потере подписок, сообщений и состояния канала, что критично для приложений реального времени: чатов, уведомлений, торговых интерфейсов, мониторинга.
Клиент STOMP.js проходит несколько состояний:
Ключевой момент заключается в том, что после разрыва соединения все подписки теряются, поскольку они привязаны к конкретной сессии брокера. Это означает, что после восстановления соединения необходимо заново подписываться на все топики.
Наиболее распространённая стратегия — цикл с задержкой между
попытками подключения. В STOMP.js клиент создаётся один раз, а попытки
повторного подключения выполняются через повторный вызов
activate().
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'wss://example.com/ws',
reconnectDelay: 0, // отключаем встроенный авто-reconnect
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000,
});
let reconnectAttempts = 0;
function connect() {
client.onConn ect = () => {
reconnectAttempts = 0;
client.subscribe('/topic/updates', (message) => {
console.log('Получено сообщение:', message.body);
});
};
client.onWebSocketCl ose = () => {
scheduleReconnect();
};
client.onStompEr ror = () => {
scheduleReconnect();
};
client.activate();
}
Переподключение без задержки создаёт риск перегрузки сервера. Поэтому используется экспоненциальная задержка (exponential backoff), при которой каждая следующая попытка увеличивает интервал ожидания.
let reconnectAttempts = 0;
let reconnectTimer = null;
function scheduleReconnect() {
if (reconnectTimer) return;
const delay = Math.min(30000, 1000 * Math.pow(2, reconnectAttempts));
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
reconnectAttempts += 1;
client.activate();
}, delay);
}
Такой подход позволяет:
После успешного восстановления соединения необходимо учитывать, что:
Поэтому рекомендуется централизованно описывать все подписки, чтобы
их можно было восстановить после onConnect.
const subscriptions = [];
function addSubscription(destination, callback) {
subscriptions.push({ destination, callback });
if (client.connected) {
client.subscribe(destination, callback);
}
}
function restoreSubscriptions() {
subscriptions.forEach(({ destination, callback }) => {
client.subscribe(destination, callback);
});
}
STOMP.js предоставляет событие onConnect, которое
вызывается при каждом успешном подключении, включая повторные.
client.onConn ect = () => {
reconnectAttempts = 0;
restoreSubscriptions();
};
Этот механизм является центральной точкой восстановления состояния клиента.
Не все сбои приводят к немедленному закрытию WebSocket. Иногда соединение “зависает” и не передаёт данные. Для таких случаев используется heartbeat.
const client = new Client({
brokerURL: 'wss://example.com/ws',
heartbeatIncoming: 5000,
heartbeatOutgoing: 5000,
});
Если heartbeat не подтверждается, STOMP.js инициирует закрытие соединения, что позволяет триггерить механизм переподключения.
При проектировании устойчивого клиента важно разделять:
Это позволяет избежать ситуации, когда логика подписок «размазана» по коду и теряется при реконнекте.
Пример архитектурного разделения:
class StompService {
constructor(client) {
this.client = client;
this.subscriptions = [];
}
connect() {
this.client.onConn ect = () => {
this.restore();
};
this.client.onWebSocketCl ose = () => {
this.reconnect();
};
this.client.activate();
}
subscribe(destination, handler) {
this.subscriptions.push({ destination, handler });
if (this.client.connected) {
this.client.subscribe(destination, handler);
}
}
restore() {
this.subscriptions.forEach(s =>
this.client.subscribe(s.destination, s.handler)
);
}
reconnect() {
setTimeout(() => this.client.activate(), 2000);
}
}
При переподключении существует риск повторной подписки на один и тот же канал, если не контролировать состояние. Это приводит к дублированию сообщений.
Решение заключается в хранении идентификаторов подписок и проверке их наличия перед созданием новой.
const activeSubscriptions = new Map();
function safeSubscribe(destination, callback) {
if (activeSubscriptions.has(destination)) return;
const sub = client.subscribe(destination, callback);
activeSubscriptions.set(destination, sub);
}
При разрыве соединения карта должна очищаться:
client.onDisconn ect = () => {
activeSubscriptions.clear();
};
STOMP.js предоставляет событие onStompError, которое
вызывается при ошибках протокола, например:
client.onStompEr ror = (frame) => {
console.error('STOMP ошибка:', frame.headers['message']);
scheduleReconnect();
};
Важно различать:
Обе категории могут требовать переподключения, но с разной логикой задержек.
В сложных приложениях необходимо вводить ограничения:
if (reconnectAttempts > 10) {
console.error('Превышено число попыток подключения');
return;
}
Это предотвращает бесконечный цикл реконнектов в условиях полной недоступности сервера.
На мобильных и нестабильных соединениях важно учитывать событие
navigator.onLine.
window.addEventListener('offline', () => {
client.deactivate();
});
window.addEventListener('online', () => {
client.activate();
});
Однако navigator.onLine не всегда точно отражает
состояние соединения, поэтому он используется только как дополнительный
сигнал.
После переподключения клиент может получить устаревшее состояние системы. Для синхронизации обычно используется один из подходов:
onConnectclient.onConn ect = () => {
restoreSubscriptions();
fetch('/api/state')
.then(res => res.json())
.then(state => {
console.log('Актуальное состояние:', state);
});
};
В условиях плохой сети возможны каскадные разрывы соединения. В таких сценариях критично:
let isReconnecting = false;
function reconnect() {
if (isReconnecting) return;
isReconnecting = true;
setTimeout(() => {
client.activate();
isReconnecting = false;
}, 2000);
}
Устойчивый STOMP.js клиент при обрыве соединения должен: