В STOMP.js устойчивость соединения строится вокруг повторного установления WebSocket-сессии и последующего восстановления STOMP-уровня поверх транспортного канала. Потеря соединения может происходить по множеству причин: сетевые разрывы, перезапуск брокера, таймауты прокси, переход устройства между сетями. В таких условиях логика переподключения становится частью прикладной архитектуры, а не вспомогательной функцией клиента.
Основной цикл восстановления включает три этапа:
Ключевая сложность заключается в том, что STOMP-сессия не является самовосстанавливающейся. После переподключения требуется заново регистрировать все subscriptions и восстанавливать состояние приложения.
STOMP.js предоставляет события жизненного цикла, которые используются для детекции состояния транспорта. Основной сигнал потери соединения — callback ошибки или закрытия соединения WebSocket.
Типовая точка входа:
onConnect — успешное установление соединенияonStompError — ошибка протокольного уровняonWebSocketClose — разрыв транспортного уровняonWebSocketError — ошибка канала связиРазрыв не всегда означает окончательную потерю связи. Часто соединение может быть восстановлено в течение нескольких секунд, поэтому стратегия переподключения должна учитывать временные сбои.
Самая базовая модель — фиксированная задержка между попытками.
let stompClient;
let retryTimer;
function connect() {
const socket = new WebSocket("wss://example.com/ws");
stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
clearTimeout(retryTimer);
subscribeAll();
}, () => {
scheduleReconnect();
});
socket.oncl ose = scheduleReconnect;
}
function scheduleReconnect() {
retryTimer = setTimeout(connect, 3000);
}
Данная модель проста, но обладает критическим недостатком: при длительных сбоях создаётся высокая нагрузка на сервер из-за одинакового интервала попыток.
Более устойчивая стратегия — exponential backoff. Она увеличивает интервал между попытками с каждой неудачей.
Математически:
[ delay_n = min(delay_{max}, delay_{base} ^n)]
Где:
n — номер попыткиdelay_base — начальная задержкаdelay_max — верхний пределПример реализации:
let retryCount = 0;
const BASE_DELAY = 1000;
const MAX_DELAY = 30000;
function scheduleReconnect() {
const delay = Math.min(MAX_DELAY, BASE_DELAY * Math.pow(2, retryCount));
retryCount++;
setTimeout(connect, delay);
}
Такая стратегия снижает риск перегрузки брокера и сети при массовых сбоях клиентов.
При массовом отключении клиентов экспоненциальная схема может привести к синхронным повторным подключениям. Это создаёт эффект «штормового восстановления».
Для устранения проблемы вводится jitter — случайная вариация задержки:
function getDelayWithJitter(baseDelay) {
const jitter = Math.random() * 0.3 * baseDelay;
return baseDelay + jitter;
}
Расширенная версия:
function scheduleReconnect() {
const base = Math.min(MAX_DELAY, BASE_DELAY * Math.pow(2, retryCount));
const delay = getDelayWithJitter(base);
retryCount++;
setTimeout(connect, delay);
}
Jitter распределяет нагрузку по времени и предотвращает синхронные пики запросов к брокеру.
Для корректной реализации переподключения вводится конечный автомат состояний:
DISCONNECTEDCONNECTINGCONNECTEDRECONNECTINGБез явного управления состояниями легко получить дублирующиеся подключения или гонки таймеров.
Пример структуры:
const state = {
value: "DISCONNECTED"
};
function setState(newState) {
state.value = newState;
}
Использование состояния:
CONNECTING — блокирует повторный вызов connectRECONNECTING — допускает только один активный
таймерCONNECTED — сбрасывает счётчики retryПри нестабильной сети часто возникает ситуация, когда одновременно запускается несколько reconnect-циклов. Это приводит к множественным WebSocket-соединениям.
Решение — использование флага блокировки:
let isConnecting = false;
function connect() {
if (isConnecting) return;
isConnecting = true;
const socket = new WebSocket("wss://example.com/ws");
const client = Stomp.over(socket);
client.connect({}, () => {
isConnecting = false;
retryCount = 0;
}, () => {
isConnecting = false;
scheduleReconnect();
});
}
STOMP не сохраняет подписки после переподключения. Следовательно, требуется внешний реестр подписок.
Подход:
const subscriptions = [];
function subscribeAll(client) {
subscriptions.forEach((sub) => {
client.subscribe(sub.destination, sub.callback);
});
}
function addSubscription(destination, callback) {
subscriptions.push({ destination, callback });
if (stompClient && stompClient.connected) {
stompClient.subscribe(destination, callback);
}
}
После переподключения выполняется полное восстановление:
client.connect({}, () => {
subscribeAll(client);
});
STOMP.js поддерживает heartbeat для проверки живости соединения. Он помогает обнаружить «тихие разрывы», когда TCP-соединение формально открыто, но данные не проходят.
Конфигурация:
const client = Stomp.over(socket);
client.heartbeat.outgoing = 10000;
client.heartbeat.incoming = 10000;
При отсутствии heartbeat-сигналов клиент инициирует переподключение, даже если WebSocket не был явно закрыт.
Более зрелая стратегия учитывает тип сбоя:
Пример:
function getDelay(errorType) {
switch (errorType) {
case "NETWORK":
return 1000;
case "BROKER":
return 10000;
case "RATE_LIMIT":
return 60000;
default:
return 5000;
}
}
После восстановления соединения часто требуется заново передать токены или заголовки авторизации. Некоторые брокеры не сохраняют контекст сессии.
client.connect(
{ Authorization: "Bearer " + token },
onConnect,
onError
);
При истечении токена стратегия переподключения должна учитывать обновление credentials, иначе цикл reconnect будет бесконечным.
При разрыве соединения отправка сообщений невозможна. Для сохранения данных используется буферизация.
const messageQueue = [];
function send(destination, body) {
if (stompClient && stompClient.connected) {
stompClient.send(destination, {}, JSON.stringify(body));
} else {
messageQueue.push({ destination, body });
}
}
function flushQueue() {
while (messageQueue.length && stompClient.connected) {
const msg = messageQueue.shift();
stompClient.send(msg.destination, {}, JSON.stringify(msg.body));
}
}
Reconnect storm возникает при массовом падении соединений и агрессивных retry-стратегиях. Основные меры защиты:
Circuit breaker:
let failures = 0;
let circuitOpen = false;
function scheduleReconnect() {
if (circuitOpen) return;
failures++;
if (failures > 10) {
circuitOpen = true;
setTimeout(() => {
circuitOpen = false;
failures = 0;
connect();
}, 60000);
return;
}
const delay = Math.min(30000, 1000 * Math.pow(2, failures));
setTimeout(connect, delay);
}
В браузере несколько вкладок могут одновременно пытаться восстановить соединение. Это приводит к дублированию подписок и нагрузке на брокер.
Решение — coordination через localStorage:
window.addEventListener("storage", (event) => {
if (event.key === "stomp-master" && event.newValue) {
isMaster = false;
}
});
function becomeMaster() {
localStorage.setItem("stomp-master", Date.now().toString());
isMaster = true;
}
Только master-вкладка выполняет reconnect.
Устойчивое переподключение в STOMP.js строится как комбинация нескольких уровней:
Каждый слой компенсирует слабости другого, формируя устойчивую модель взаимодействия с брокером сообщений в условиях нестабильной сети.