Подписка в STOMP.js строится вокруг модели обмена сообщениями через брокер, где клиент не опрашивает сервер, а получает данные по заранее объявленным каналам доставки (destination). После установления соединения клиент регистрирует интерес к определённым маршрутам сообщений и получает поток сообщений асинхронно, в реальном времени.
STOMP использует концепцию destination — логического адреса сообщения. Это может быть очередь или топик, в зависимости от брокера (ActiveMQ, RabbitMQ, Artemis и другие). Клиент подписывается на destination, после чего брокер начинает доставлять сообщения, опубликованные в этот канал.
В STOMP.js подписка реализуется через метод subscribe,
вызываемый у объекта соединения.
Ключевой принцип заключается в том, что подписка не блокирует поток выполнения. Она регистрирует callback-функцию, которая будет вызвана при поступлении каждого сообщения.
Базовая форма подписки включает указание destination и обработчика сообщений:
Подписка возвращает объект Subscription, который используется для управления жизненным циклом соединения с каналом.
Каждое сообщение, поступающее в callback, имеет строго определённую структуру:
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();
После вызова брокер перестаёт доставлять сообщения в данный канал для этого клиента.
Каждая подписка может иметь уникальный идентификатор. Это важно в сценариях, где клиент подписывается на один и тот же destination несколько раз с разными обработчиками.
const sub1 = client.subscribe('/topic/chat', handlerA, { id: 'sub-1' });
const sub2 = client.subscribe('/topic/chat', handlerB, { id: 'sub-2' });
В этом случае оба обработчика получают одинаковый поток сообщений независимо друг от друга.
Подписка может содержать дополнительные headers, которые передаются брокеру. Они зависят от реализации сервера, но в STOMP.js стандартно поддерживаются следующие сценарии:
Пример с ручным подтверждением:
client.subscribe('/queue/tasks', (message) => {
try {
const data = JSON.parse(message.body);
processTask(data);
message.ack();
} catch (e) {
message.nack();
}
}, { ack: 'client' });
STOMP поддерживает три основных режима ack:
Сообщение считается подтверждённым сразу после доставки клиенту. Это упрощённый режим, но не гарантирует обработку.
Клиент обязан явно вызвать ack() после успешной
обработки сообщения. Это позволяет гарантировать доставку.
Каждое сообщение подтверждается отдельно, что исключает пакетное подтверждение и повышает точность контроля.
Если используется режим client или
client-individual и сообщение не подтверждается, брокер
может повторно доставить его другому подписчику или тому же клиенту.
Это создаёт модель at-least-once delivery, при которой приложение должно быть идемпотентным.
Неправильное управление подписками приводит к накоплению неиспользуемых каналов и утечкам памяти в клиентском приложении. Особенно это критично в SPA-приложениях, где компоненты часто создаются и уничтожаются.
Корректный жизненный цикл выглядит следующим образом:
В реальных приложениях подписка часто зависит от состояния:
Пример динамической смены канала:
let subscription;
function subscribeToRoom(roomId) {
if (subscription) {
subscription.unsubscribe();
}
subscription = client.subscribe(`/topic/room/${roomId}`, (message) => {
renderMessage(JSON.parse(message.body));
});
}
STOMP.js часто используется поверх WebSocket, который может разрываться. При реконнекте подписки не восстанавливаются автоматически.
Поэтому применяется стратегия:
const subscriptions = new Map();
function resubscribe(client) {
subscriptions.forEach((handler, destination) => {
const sub = client.subscribe(destination, handler);
subscriptions.set(destination, sub);
});
}
Подписка не гарантирует последовательность доставки в сложных распределённых системах. Сообщения могут приходить:
Поэтому обработчик должен учитывать:
Один клиент может подписываться на несколько уровней событий:
/topic/system)/user/queue/notifications)/topic/document/{id})Такой подход позволяет строить иерархию событий без изменения клиентской логики, только за счёт структуры destination.
Во многих брокерах поддерживается концепция user-specific очередей:
client.subscribe('/user/queue/messages', (message) => {
showPrivateMessage(JSON.parse(message.body));
});
Такая подписка гарантирует, что сообщение будет доставлено только конкретному пользователю, даже если сервер отправляет его в общий канал.
Некоторые брокеры поддерживают selector — фильтрацию сообщений до их доставки клиенту. Это снижает нагрузку на клиентскую сторону.
client.subscribe('/topic/orders', handler, {
selector: "status = 'NEW'"
});
В этом случае брокер отправляет только сообщения, удовлетворяющие условию.
Процесс можно разбить на этапы:
SUBSCRIBE frameUNSUBSCRIBEКаждый этап соответствует обмену STOMP-фреймами между клиентом и сервером, где STOMP.js выступает в роли абстракции над протоколом.
Внутри STOMP.js подписка представляет собой объект, связывающий:
При вызове unsubscribe отправляется STOMP frame:
UNSUBSCRIBE
id:sub-1
и брокер удаляет регистрацию доставки сообщений.
При большом количестве подписок важно учитывать:
Оптимальная стратегия часто предполагает агрегацию событий и использование более широких каналов с последующей фильтрацией на клиенте.
Подписки часто используются не только для событий, но и для синхронизации состояния:
В таких случаях сообщение рассматривается как операция изменения состояния, а не просто событие, что требует строгой обработки порядка и консистентности данных