В протоколе STOMP каждая подписка клиента на очередь, топик или иной
канал сообщений должна иметь уникальный идентификатор. В библиотеке STOMP.js
этот идентификатор задаётся через заголовок id при вызове
метода subscribe().
Идентификатор подписки выполняет несколько функций:
Без корректной системы идентификаторов управление подписками быстро становится хаотичным, особенно в крупных приложениях с большим количеством динамических каналов.
Простейший пример:
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:15674/ws'
});
client.onConn ect = () => {
client.subscribe(
'/topic/news',
(message) => {
console.log(message.body);
},
{
id: 'news-subscription'
}
);
};
client.activate();
В данном случае подписка получает идентификатор:
news-subscription
Этот идентификатор отправляется брокеру внутри STOMP-фрейма:
SUBSCRIBE
id:news-subscription
destination:/topic/news
Некоторые версии STOMP.js способны автоматически генерировать идентификаторы подписок. Однако поведение зависит от:
Автоматическая генерация удобна только для простых сценариев. В
production-системах рекомендуется всегда задавать id
явно.
Причины:
Протокол STOMP не навязывает строгий формат id. Обычно
используются:
id: 'chat-room-1'
id: '550e8400-e29b-41d4-a716-446655440000'
id: 'user-42-notifications'
id: 'dashboard.orders.active'
Идентификатор должен быть уникальным внутри одного соединения.
Нельзя создавать две подписки с одинаковым id:
client.subscribe('/topic/a', callbackA, {
id: 'same-id'
});
client.subscribe('/topic/b', callbackB, {
id: 'same-id'
});
Возможные последствия:
Поведение зависит от брокера.
let subscriptionCounter = 0;
function createSubscriptionId() {
subscriptionCounter++;
return `sub-${subscriptionCounter}`;
}
Пример:
const id = createSubscriptionId();
client.subscribe('/topic/chat', callback, { id });
const id = `sub-${Date.now()}`;
Современный браузерный API:
const id = crypto.randomUUID();
client.subscribe('/topic/data', callback, { id });
Пример результата:
f3d5e77d-4d0b-4df0-91e7-f19f26c92f61
В крупных приложениях идентификаторы обычно сохраняются.
const subscriptions = new Map();
const subscription = client.subscribe(
'/topic/orders',
callback,
{
id: 'orders-feed'
}
);
subscriptions.set('orders-feed', subscription);
const subscriptions = {};
subscriptions.news = client.subscribe(
'/topic/news',
callback,
{
id: 'news-sub'
}
);
Метод unsubscribe() использует идентификатор
подписки.
const subscription = client.subscribe(
'/topic/news',
callback,
{
id: 'news-sub'
}
);
subscription.unsubscribe();
Внутри STOMP отправляется:
UNSUBSCRIBE
id:news-sub
Иногда unsubscribe выполняется напрямую:
client.unsubscribe('news-sub');
Это возможно только при известном идентификаторе.
При использовании режима подтверждения сообщений (ack)
идентификатор подписки играет важную роль.
Пример:
client.subscribe(
'/queue/tasks',
(message) => {
console.log(message.body);
message.ack();
},
{
id: 'task-worker',
ack: 'client'
}
);
Брокер связывает:
Без корректного id механизм подтверждений может работать
нестабильно.
Проблема часто возникает в multi-tab приложениях.
Например:
id: 'notifications'
Если несколько вкладок используют одинаковые идентификаторы и общий backend-session, возможны конфликты.
Решение:
const tabId = crypto.randomUUID();
const subscriptionId = `notifications-${tabId}`;
При reconnect STOMP.js обычно создаёт новые подписки.
Важно понимать:
Иногда используются постоянные id:
id: 'main-notifications'
Преимущества:
Недостатки:
Другой подход:
id: `main-notifications-${Date.now()}`
Преимущества:
Недостатки:
В React-приложениях подписки часто создаются внутри
useEffect.
Неправильный вариант:
useEffect(() => {
client.subscribe('/topic/data', callback, {
id: 'data-sub'
});
}, []);
При повторном монтировании компонента возможен конфликт.
Более безопасный подход:
useEffect(() => {
const id = crypto.randomUUID();
const subscription = client.subscribe(
'/topic/data',
callback,
{ id }
);
return () => {
subscription.unsubscribe();
};
}, []);
Во Vue подписки часто размещаются внутри lifecycle hooks.
mounted() {
this.subscription = client.subscribe(
'/topic/chat',
this.onMessage,
{
id: `chat-${this._uid}`
}
);
}
Angular-сервисы нередко управляют множеством подписок одновременно.
Пример registry:
class SubscriptionRegistry {
constructor() {
this.items = new Map();
}
add(id, subscription) {
this.items.set(id, subscription);
}
remove(id) {
const sub = this.items.get(id);
if (sub) {
sub.unsubscribe();
this.items.delete(id);
}
}
}
Одна из самых распространённых ошибок:
setInterval(() => {
client.subscribe('/topic/live', callback, {
id: 'live-feed'
});
}, 1000);
Каждую секунду создаётся новая подписка с тем же идентификатором.
Последствия:
Полезно логировать идентификаторы:
const id = crypto.randomUUID();
console.log('Subscribe:', id);
client.subscribe('/topic/orders', callback, {
id
});
В больших приложениях создаётся отдельный слой управления.
Пример:
class StompSubscriptionManager {
constructor(client) {
this.client = client;
this.subscriptions = new Map();
}
subscribe(destination, callback) {
const id = crypto.randomUUID();
const subscription = this.client.subscribe(
destination,
callback,
{ id }
);
this.subscriptions.set(id, subscription);
return id;
}
unsubscribe(id) {
const subscription = this.subscriptions.get(id);
if (!subscription) {
return;
}
subscription.unsubscribe();
this.subscriptions.delete(id);
}
}
Разные брокеры по-разному относятся к subscription id.
RabbitMQ обычно требует строгой уникальности идентификаторов внутри соединения.
Apache ActiveMQ поддерживает более гибкое управление подписками, включая durable subscriptions.
Apache ActiveMQ Artemis может использовать дополнительные внутренние механизмы маршрутизации подписок.
Некоторые брокеры поддерживают постоянные подписки.
Пример:
client.subscribe(
'/topic/events',
callback,
{
id: 'durable-events'
}
);
В таких системах id становится частью постоянного
состояния брокера.
Изменение идентификатора может привести к:
В микрофронтенд-архитектуре особенно важны namespace.
Пример:
id: 'billing.notifications'
id: 'orders.notifications'
id: 'analytics.notifications'
Такой подход уменьшает вероятность конфликтов между независимыми модулями.
Распространённые схемы:
orders-feed
chat-room
notifications
user-42-chat
user-42-alerts
dashboard-orders
sidebar-chat
header-notifications
prod-orders-feed
dev-orders-feed
Типичные симптомы:
Для диагностики полезно:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
debug: (str) => {
console.log(str);
}
});
В логах будут видны:
>>> SUBSCRIBE
id:orders-feed
destination:/topic/orders
Наиболее устойчивой считается комбинация:
Пример итогового подхода:
function createSubscriptionId(module, topic) {
return `${module}.${topic}.${crypto.randomUUID()}`;
}
const id = createSubscriptionId(
'dashboard',
'orders'
);
const subscription = client.subscribe(
'/topic/orders',
callback,
{ id }
);