В STOMP.js каждая подписка представляет собой активный канал
получения сообщений от брокера по конкретному destination (топику или
очереди). При вызове client.subscribe() создаётся объект
подписки, который удерживается клиентом и связан с внутренним
идентификатором на стороне брокера.
Каждая подписка потребляет ресурсы сразу на двух уровнях:
При увеличении количества подписок линейно растёт нагрузка на обе стороны, поэтому управление их количеством становится критическим аспектом архитектуры real-time приложения.
Типичный цикл работы подписки в STOMP.js выглядит следующим образом:
const subscription = client.subscribe('/topic/updates', (message) => {
const body = JSON.parse(message.body);
console.log(body);
});
Отписка выполняется явно:
subscription.unsubscribe();
Каждый вызов subscribe() создаёт отдельный канал. Даже
если destination одинаковый, повторный вызов формирует новую подписку,
если не реализована дополнительная логика дедупликации.
Ключевой момент: STOMP.js не ограничивает количество подписок на уровне API, поэтому контроль полностью ложится на разработчика.
В реальных приложениях часто возникает ситуация, когда подписки накапливаются:
Последствия:
Практический подход — введение слоя управления подписками поверх STOMP.js.
Структура обычно строится как реестр:
class SubscriptionManager {
constructor(client) {
this.client = client;
this.subscriptions = new Map();
}
add(id, destination, callback) {
if (this.subscriptions.has(id)) {
return this.subscriptions.get(id);
}
const subscription = this.client.subscribe(destination, callback);
this.subscriptions.set(id, subscription);
return subscription;
}
remove(id) {
const sub = this.subscriptions.get(id);
if (sub) {
sub.unsubscribe();
this.subscriptions.delete(id);
}
}
clear() {
for (const sub of this.subscriptions.values()) {
sub.unsubscribe();
}
this.subscriptions.clear();
}
}
Такой слой решает сразу несколько задач:
STOMP.js сам по себе не накладывает лимиты, однако ограничения возникают на уровне:
Практическая стратегия — введение лимита подписок на клиенте:
class LimitedSubscriptionManager {
constructor(client, limit = 50) {
this.client = client;
this.limit = limit;
this.subscriptions = new Map();
}
add(id, destination, callback) {
if (this.subscriptions.size >= this.limit) {
const firstKey = this.subscriptions.keys().next().value;
this.remove(firstKey);
}
const subscription = this.client.subscribe(destination, callback);
this.subscriptions.set(id, subscription);
return subscription;
}
remove(id) {
const sub = this.subscriptions.get(id);
if (sub) {
sub.unsubscribe();
this.subscriptions.delete(id);
}
}
}
Такой подход превращает подписки в управляемый пул с вытеснением.
При росте сложности системы подписки целесообразно группировать по логическим доменам:
Пример структуры:
{
user: {
notifications: Subscription,
presence: Subscription
},
chat: {
room_1: Subscription,
room_2: Subscription
}
}
Это позволяет:
Частая ошибка — многократная подписка на один и тот же destination.
Решение — использование ключа дедупликации:
const key = `${destination}:${handlerId}`;
if (manager.has(key)) {
return manager.get(key);
}
Важно учитывать, что одинаковый destination может использоваться разными обработчиками. Поэтому ключ должен учитывать не только маршрут, но и контекст обработки.
В одностраничных приложениях управление подписками тесно связано с жизненным циклом компонентов.
Проблемная модель:
Корректная модель:
В React это обычно выражается через cleanup:
useEffect(() => {
const sub = client.subscribe('/topic/data', handler);
return () => sub.unsubscribe();
}, []);
При разрыве WebSocket соединения все подписки на стороне клиента становятся недействительными и требуют восстановления после reconnect.
Типичная архитектура включает:
Пример:
class ResilientManager {
constructor(client) {
this.client = client;
this.registry = [];
}
register(destination, callback) {
this.registry.push({ destination, callback });
}
restore() {
this.registry.forEach(({ destination, callback }) => {
this.client.subscribe(destination, callback);
});
}
}
При большом количестве подписок деградация проявляется в нескольких формах:
Особенно критично это при использовании wildcard-подписок:
/topic/*
/queue/user.*
Одна такая подписка может эквивалентно нагрузить систему как десятки или сотни точечных подписок.
Вместо множества подписок на мелкие события используется агрегация:
Пример:
client.subscribe('/topic/events', (message) => {
const event = JSON.parse(message.body);
switch (event.type) {
case 'chat':
handleChat(event);
break;
case 'notification':
handleNotification(event);
break;
}
});
Это снижает количество подписок, но увеличивает нагрузку на клиентскую обработку, что требует балансировки.
При смене пользователя критически важно полностью очищать подписки, связанные с предыдущей сессией:
Игнорирование этого приводит к утечкам данных между пользователями и некорректной маршрутизации сообщений.
В архитектурах с несколькими WebSocket соединениями (например, multi-tab или multi-tenant интерфейсы) каждая сессия должна иметь собственный набор подписок.
Смешивание подписок между соединениями приводит к:
Практическая модель — привязка SubscriptionManager к конкретному STOMP client instance.
В зрелых приложениях подписки перестают быть локальной логикой и переходят в слой состояния приложения:
Это позволяет:
Наиболее распространённые проблемы:
Эти ошибки накапливаются и проявляются только под нагрузкой, что усложняет диагностику.
Оптимизация всегда сводится к выбору стратегии:
STOMP.js не диктует архитектуру, но делает управление подписками центральным элементом производительности всей системы.