Ленивая подписка

Ленивая подписка в контексте STOMP.js представляет собой стратегию управления подписками на каналы сообщений, при которой подписка создаётся не в момент установления соединения, а только тогда, когда появляется фактическая потребность в получении данных. Такой подход переносит акцент с «предварительного подключения ко всему» на динамическое подключение к конкретным топикам по мере их использования.

В рамках STOMP-протокола каждая подписка является отдельной операцией, привязанной к destination (например, /topic/chat, /queue/notifications). Ленивая модель предполагает, что эти операции инициируются по требованию, а не заранее.

Зачем нужна ленивая подписка

Основная проблема жадной модели подписок — избыточное потребление ресурсов. При подключении к большому количеству топиков одновременно возникают следующие эффекты:

  • увеличивается нагрузка на брокер сообщений (RabbitMQ, ActiveMQ, Apollo и др.)
  • растёт количество открытых подписок на клиенте и сервере
  • ухудшается масштабируемость при большом числе пользователей
  • увеличивается сетевой шум при доставке сообщений в неиспользуемые каналы

Ленивая подписка решает эти проблемы за счёт сокращения числа активных подписок до минимально необходимого набора. Это особенно критично в SPA, где пользователь может никогда не открыть часть функциональных модулей, но соединение при этом уже установлено.

Базовая модель STOMP подписок

STOMP.js реализует подписки через метод клиента subscribe, возвращающий объект подписки с методом unsubscribe.

Типовой сценарий:

const subscription = client.subscribe('/topic/messages', (message) => {
  const body = JSON.parse(message.body);
  console.log(body);
});

Отписка:

subscription.unsubscribe();

Эта модель напрямую отражает команду STOMP SUBSCRIBE и UNSUBSCRIBE, отправляемые брокеру.

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

Реализация ленивой подписки в STOMP.js

Ленивая подписка строится вокруг идеи отложенного вызова subscribe до момента первого реального потребления данных.

Базовый шаблон управления:

class LazyStompSubscription {
  constructor(client, destination, handler) {
    this.client = client;
    this.destination = destination;
    this.handler = handler;
    this.subscription = null;
  }

  ensureSubscribed() {
    if (!this.subscription) {
      this.subscription = this.client.subscribe(
        this.destination,
        this.handler
      );
    }
  }

  unsubscribe() {
    if (this.subscription) {
      this.subscription.unsubscribe();
      this.subscription = null;
    }
  }
}

Такой подход позволяет контролировать момент создания подписки и предотвращать преждевременное подключение к каналам.

Динамическая активация подписки

Часто ленивая подписка связывается с событиями пользовательского интерфейса или жизненного цикла приложения:

const chatSubscription = new LazyStompSubscription(
  client,
  '/topic/chat',
  (msg) => renderMessage(JSON.parse(msg.body))
);

// подписка активируется только при открытии чата
function onChatOpen() {
  chatSubscription.ensureSubscribed();
}

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

Управление жизненным циклом подписки

Ленивая подписка требует чёткого контроля жизненного цикла:

  • создание при первом обращении
  • сохранение состояния активности
  • корректная отписка при уничтожении компонента
  • восстановление после реконнекта

При использовании STOMP.js важно учитывать, что реконнект приводит к потере всех подписок, поэтому требуется механизм реинициализации:

function recreateSubscriptions() {
  if (chatSubscription) {
    chatSubscription.subscription = null;
  }
}

В более сложных системах вводится registry подписок, который хранит метаданные и пересоздаёт активные подписки автоматически.

Интеграция с SPA архитектурой

В SPA (React, Vue, Angular) ленивая подписка обычно привязывается к монтированию компонентов.

Пример логики:

  • компонент смонтирован → подписка активируется
  • компонент размонтирован → подписка уничтожается
useEffect(() => {
  const sub = client.subscribe('/topic/notifications', handleMessage);

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

Ленивая модель в этом случае проявляется не только как отложенное создание, но и как отсутствие глобальных подписок «на всякий случай».

Паттерн Subscription Registry

Для масштабных приложений применяется реестр подписок:

class SubscriptionRegistry {
  constructor(client) {
    this.client = client;
    this.map = new Map();
  }

  lazySubscribe(key, destination, handler) {
    if (!this.map.has(key)) {
      this.map.set(key, {
        destination,
        handler,
        subscription: null
      });
    }

    const entry = this.map.get(key);

    if (!entry.subscription) {
      entry.subscription = this.client.subscribe(
        entry.destination,
        entry.handler
      );
    }

    return entry.subscription;
  }

  unsubscribe(key) {
    const entry = this.map.get(key);
    if (entry?.subscription) {
      entry.subscription.unsubscribe();
      entry.subscription = null;
    }
  }
}

Такой реестр позволяет централизованно управлять ленивыми подписками и избегать дублирования логики.

Обработка гонок состояний

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

  • установлением соединения
  • вызовом subscribe
  • восстановлением после reconnect

Типичная проблема — попытка подписаться до client.connected.

Решение заключается в проверке состояния и очереди операций:

function safeSubscribe(client, destination, handler) {
  if (!client.connected) {
    client.onConn ect = () => {
      client.subscribe(destination, handler);
    };
    return;
  }

  client.subscribe(destination, handler);
}

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

Оптимизация количества подписок

Ленивая модель часто комбинируется с агрегацией:

  • объединение нескольких логических каналов в один топик
  • фильтрация сообщений на клиенте
  • использование wildcard подписок (/topic/chat.*)

Это снижает число подписок даже при ленивом создании.

Дополнительный уровень оптимизации — временная деактивация подписок, а не их полное удаление:

if (inactive) {
  subscription.unsubscribe();
  cache.set(key, subscription);
}

Сценарии условной подписки

Ленивая подписка особенно эффективна при наличии условий:

  • подписка только при открытии вкладки
  • подписка при прокрутке до блока UI
  • подписка при включении feature flag
  • подписка при наличии прав доступа

Пример с feature flag:

if (features.realtimeEnabled) {
  chat.ensureSubscribed();
}

Связь с архитектурой брокера

Поведение ленивых подписок напрямую влияет на нагрузку брокера STOMP:

  • меньше SUBSCRIBE команд
  • меньше маршрутизируемых сообщений
  • ниже потребление памяти на очереди

Однако при слишком агрессивной ленивости возможно увеличение задержек первого сообщения из-за времени подписки, что требует балансировки между «горячими» и «холодными» каналами.

Кэширование активных подписок

Часто используется кэш активных подписок для предотвращения повторного создания:

const active = new Set();

function lazySubscribe(client, topic, handler) {
  if (active.has(topic)) return;

  client.subscribe(topic, handler);
  active.add(topic);
}

Такой подход предотвращает дублирование подписок при повторных входах в один и тот же раздел приложения.