Управление количеством подписок

В STOMP.js каждая подписка представляет собой активный канал получения сообщений от брокера по конкретному destination (топику или очереди). При вызове client.subscribe() создаётся объект подписки, который удерживается клиентом и связан с внутренним идентификатором на стороне брокера.

Каждая подписка потребляет ресурсы сразу на двух уровнях:

  • клиентском (обработчики, замыкания, память под очередь сообщений),
  • серверном (регистрация consumer’а в брокере, маршрутизация сообщений).

При увеличении количества подписок линейно растёт нагрузка на обе стороны, поэтому управление их количеством становится критическим аспектом архитектуры 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, поэтому контроль полностью ложится на разработчика.


Проблема неконтролируемого роста подписок

В реальных приложениях часто возникает ситуация, когда подписки накапливаются:

  • переход между страницами SPA без очистки старых подписок,
  • повторное выполнение эффектов в React без корректного cleanup,
  • динамическое создание подписок при каждом рендере компонента,
  • отсутствие централизованного реестра подписок.

Последствия:

  • дублирование сообщений,
  • рост задержек обработки,
  • утечки памяти в браузере,
  • перегрузка брокера (особенно при ActiveMQ / RabbitMQ / Artemis),
  • нестабильность соединения при высоком количестве consumers.

Централизованное управление подписками

Практический подход — введение слоя управления подписками поверх 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 сам по себе не накладывает лимиты, однако ограничения возникают на уровне:

  • брокера сообщений,
  • сетевого соединения WebSocket,
  • браузерных ресурсов.

Практическая стратегия — введение лимита подписок на клиенте:

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
  }
}

Это позволяет:

  • быстро отключать целые категории,
  • управлять контекстами (например, при logout),
  • избегать “осиротевших” подписок.

Дедупликация подписок

Частая ошибка — многократная подписка на один и тот же destination.

Решение — использование ключа дедупликации:

const key = `${destination}:${handlerId}`;

if (manager.has(key)) {
  return manager.get(key);
}

Важно учитывать, что одинаковый destination может использоваться разными обработчиками. Поэтому ключ должен учитывать не только маршрут, но и контекст обработки.


Жизненный цикл подписки в SPA

В одностраничных приложениях управление подписками тесно связано с жизненным циклом компонентов.

Проблемная модель:

  • компонент смонтирован → подписка создана,
  • компонент обновился → подписка создана снова,
  • компонент размонтирован → подписка не удалена.

Корректная модель:

  • создание подписки только при инициализации,
  • обязательная очистка при уничтожении контекста.

В React это обычно выражается через cleanup:

useEffect(() => {
  const sub = client.subscribe('/topic/data', handler);

  return () => sub.unsubscribe();
}, []);

Переподключение и восстановление подписок

При разрыве WebSocket соединения все подписки на стороне клиента становятся недействительными и требуют восстановления после reconnect.

Типичная архитектура включает:

  1. хранение декларативного списка подписок,
  2. автоматическое восстановление после reconnect,
  3. повторную регистрацию через client.subscribe.

Пример:

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);
    });
  }
}

Переизбыток подписок и деградация производительности

При большом количестве подписок деградация проявляется в нескольких формах:

  • рост latency доставки сообщений,
  • увеличение CPU нагрузки на брокере,
  • рост числа открытых consumer’ов,
  • увеличение времени обработки маршрутизации.

Особенно критично это при использовании 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.


Управление подписками как часть общей системы состояния

В зрелых приложениях подписки перестают быть локальной логикой и переходят в слой состояния приложения:

  • Redux / Zustand / MobX store,
  • отдельный WebSocket service,
  • event-driven middleware.

Это позволяет:

  • централизованно отслеживать активные каналы,
  • логировать жизненный цикл подписок,
  • проводить диагностику в runtime.

Частые архитектурные ошибки

Наиболее распространённые проблемы:

  • создание подписок внутри render-цикла,
  • отсутствие unsubscribe при смене маршрута,
  • хранение подписок без идентификаторов,
  • дублирование одного destination без контроля,
  • отсутствие восстановления после reconnect.

Эти ошибки накапливаются и проявляются только под нагрузкой, что усложняет диагностику.


Баланс между количеством подписок и нагрузкой

Оптимизация всегда сводится к выбору стратегии:

  • больше подписок → меньше логики на клиенте, больше нагрузка на брокер,
  • меньше подписок → больше агрегации и обработки на клиенте.

STOMP.js не диктует архитектуру, но делает управление подписками центральным элементом производительности всей системы.