Механизм подписки

Подписка в STOMP.js строится вокруг модели обмена сообщениями через брокер, где клиент не опрашивает сервер, а получает данные по заранее объявленным каналам доставки (destination). После установления соединения клиент регистрирует интерес к определённым маршрутам сообщений и получает поток сообщений асинхронно, в реальном времени.

STOMP использует концепцию destination — логического адреса сообщения. Это может быть очередь или топик, в зависимости от брокера (ActiveMQ, RabbitMQ, Artemis и другие). Клиент подписывается на destination, после чего брокер начинает доставлять сообщения, опубликованные в этот канал.

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

Ключевой принцип заключается в том, что подписка не блокирует поток выполнения. Она регистрирует callback-функцию, которая будет вызвана при поступлении каждого сообщения.

Регистрация подписки

Базовая форма подписки включает указание destination и обработчика сообщений:

  • destination — строка, определяющая канал
  • callback — функция, принимающая сообщение
  • headers — дополнительные параметры управления подпиской

Подписка возвращает объект Subscription, который используется для управления жизненным циклом соединения с каналом.

Структура входящего сообщения

Каждое сообщение, поступающее в callback, имеет строго определённую структуру:

  • body — полезная нагрузка сообщения (обычно строка JSON)
  • headers — метаданные (id сообщения, destination, timestamp, брокерские атрибуты)
  • ack / nack — методы управления подтверждением доставки (если включён режим ручного подтверждения)

STOMP.js не навязывает формат payload, поэтому ответственность за сериализацию и десериализацию лежит на приложении.

Простейшая подписка

Подписка создаётся после успешного подключения клиента:

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

В этом примере /topic/orders — канал публикации событий заказов. Каждый раз при поступлении нового сообщения callback будет вызван с объектом message.

Управление подпиской через Subscription

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

  • unsubscribe() — прекращает получение сообщений
  • id — уникальный идентификатор подписки

Отписка выполняется явно:

subscription.unsubscribe();

После вызова брокер перестаёт доставлять сообщения в данный канал для этого клиента.

Идентификатор подписки и множественные подписки

Каждая подписка может иметь уникальный идентификатор. Это важно в сценариях, где клиент подписывается на один и тот же destination несколько раз с разными обработчиками.

const sub1 = client.subscribe('/topic/chat', handlerA, { id: 'sub-1' });
const sub2 = client.subscribe('/topic/chat', handlerB, { id: 'sub-2' });

В этом случае оба обработчика получают одинаковый поток сообщений независимо друг от друга.

Заголовки подписки

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

  • id — идентификатор подписки
  • ack — режим подтверждения доставки (auto, client, client-individual)
  • durable — устойчивые подписки (в некоторых брокерах)
  • selector — фильтрация сообщений по JMS-подобным выражениям

Пример с ручным подтверждением:

client.subscribe('/queue/tasks', (message) => {
  try {
    const data = JSON.parse(message.body);
    processTask(data);
    message.ack();
  } catch (e) {
    message.nack();
  }
}, { ack: 'client' });

Режимы подтверждения доставки

STOMP поддерживает три основных режима ack:

auto

Сообщение считается подтверждённым сразу после доставки клиенту. Это упрощённый режим, но не гарантирует обработку.

client

Клиент обязан явно вызвать ack() после успешной обработки сообщения. Это позволяет гарантировать доставку.

client-individual

Каждое сообщение подтверждается отдельно, что исключает пакетное подтверждение и повышает точность контроля.

Обработка ошибок и повторная доставка

Если используется режим client или client-individual и сообщение не подтверждается, брокер может повторно доставить его другому подписчику или тому же клиенту.

Это создаёт модель at-least-once delivery, при которой приложение должно быть идемпотентным.

Отписка и утечки подписок

Неправильное управление подписками приводит к накоплению неиспользуемых каналов и утечкам памяти в клиентском приложении. Особенно это критично в SPA-приложениях, где компоненты часто создаются и уничтожаются.

Корректный жизненный цикл выглядит следующим образом:

  • подписка создаётся при инициализации компонента
  • сохраняется ссылка на Subscription
  • при уничтожении компонента вызывается unsubscribe

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

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

  • выбор пользователя
  • текущий чат
  • активный документ
  • фильтры UI

Пример динамической смены канала:

let subscription;

function subscribeToRoom(roomId) {
  if (subscription) {
    subscription.unsubscribe();
  }

  subscription = client.subscribe(`/topic/room/${roomId}`, (message) => {
    renderMessage(JSON.parse(message.body));
  });
}

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

STOMP.js часто используется поверх WebSocket, который может разрываться. При реконнекте подписки не восстанавливаются автоматически.

Поэтому применяется стратегия:

  • хранение списка активных подписок
  • повторная регистрация после reconnect
  • централизованный менеджер подписок
const subscriptions = new Map();

function resubscribe(client) {
  subscriptions.forEach((handler, destination) => {
    const sub = client.subscribe(destination, handler);
    subscriptions.set(destination, sub);
  });
}

Поток сообщений и конкурентность

Подписка не гарантирует последовательность доставки в сложных распределённых системах. Сообщения могут приходить:

  • с задержкой
  • в изменённом порядке
  • дублироваться

Поэтому обработчик должен учитывать:

  • дедупликацию по message-id
  • сортировку по timestamp
  • защиту от повторной обработки

Многоуровневая маршрутизация

Один клиент может подписываться на несколько уровней событий:

  • глобальные события системы (/topic/system)
  • пользовательские события (/user/queue/notifications)
  • контекстные каналы (/topic/document/{id})

Такой подход позволяет строить иерархию событий без изменения клиентской логики, только за счёт структуры destination.

Подписка на user destination

Во многих брокерах поддерживается концепция user-specific очередей:

client.subscribe('/user/queue/messages', (message) => {
  showPrivateMessage(JSON.parse(message.body));
});

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

Фильтрация сообщений на уровне подписки

Некоторые брокеры поддерживают selector — фильтрацию сообщений до их доставки клиенту. Это снижает нагрузку на клиентскую сторону.

client.subscribe('/topic/orders', handler, {
  selector: "status = 'NEW'"
});

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

Жизненный цикл подписки внутри STOMP.js

Процесс можно разбить на этапы:

  • регистрация через SUBSCRIBE frame
  • подтверждение брокером
  • начало доставки сообщений
  • обработка callback на клиенте
  • управление ack/nack
  • завершение через UNSUBSCRIBE

Каждый этап соответствует обмену STOMP-фреймами между клиентом и сервером, где STOMP.js выступает в роли абстракции над протоколом.

Внутреннее представление подписки

Внутри STOMP.js подписка представляет собой объект, связывающий:

  • connection (WebSocket)
  • destination
  • callback handler
  • headers
  • id

При вызове unsubscribe отправляется STOMP frame:

UNSUBSCRIBE
id:sub-1

и брокер удаляет регистрацию доставки сообщений.

Масштабирование подписок

При большом количестве подписок важно учитывать:

  • ограничение брокера на число активных consumers
  • нагрузку на клиент при множественных callback
  • группировку подписок по destination
  • минимизацию дублирующихся каналов

Оптимальная стратегия часто предполагает агрегацию событий и использование более широких каналов с последующей фильтрацией на клиенте.

Синхронизация состояния через подписки

Подписки часто используются не только для событий, но и для синхронизации состояния:

  • live dashboard
  • чаты
  • игровые события
  • мониторинг систем

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