Отмена подписки

Подписка в STOMP.js создаётся методом subscribe(). После регистрации подписчик начинает получать сообщения из брокера, пока соединение активно или пока подписка не будет отменена. Управление жизненным циклом подписок играет критически важную роль в приложениях реального времени:

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

Каждая подписка в STOMP.js представляет отдельный объект, содержащий метод unsubscribe().


Возвращаемый объект подписки

Метод subscribe() возвращает экземпляр подписки:

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

Этот объект используется для отмены подписки:

subscription.unsubscribe();

После вызова:

  • брокер прекращает отправку сообщений;
  • callback больше не вызывается;
  • подписка удаляется из внутреннего реестра клиента.

Принцип работы unsubscribe()

При отмене подписки STOMP.js отправляет брокеру STOMP-фрейм UNSUBSCRIBE.

Пример внутренней структуры:

UNSUBSCRIBE
id:sub-0

\0

Ключевым элементом является id, который соответствует идентификатору подписки.

Брокер:

  1. находит активную подписку;
  2. удаляет её;
  3. прекращает доставку сообщений клиенту.

Базовая отмена подписки

Создание подписки

const subscription = client.subscribe('/queue/tasks', message => {
    console.log('Новое сообщение:', message.body);
});

Отмена

subscription.unsubscribe();

После этого новые сообщения приходить не будут.


Повторная подписка после отмены

После отмены можно создать новую подписку:

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

subscription.unsubscribe();

const newSubscription = client.subscribe('/topic/chat', message => {
    console.log('Повторная подписка:', message.body);
});

Новая подписка получает новый идентификатор.


Что происходит после unsubscribe()

После вызова unsubscribe():

Состояние Результат
Callback Больше не вызывается
Подписка Удаляется
Брокер Прекращает доставку
WebSocket Остаётся активным
Остальные подписки Продолжают работать

Отмена подписки не закрывает соединение STOMP и не влияет на другие каналы.


Отмена одной из нескольких подписок

Каждая подписка независима.

const ordersSub = client.subscribe('/topic/orders', message => {
    console.log('Заказы:', message.body);
});

const usersSub = client.subscribe('/topic/users', message => {
    console.log('Пользователи:', message.body);
});

Можно отключить только одну:

ordersSub.unsubscribe();

Подписка /topic/users продолжит работу.


Отмена подписки по идентификатору

STOMP.js позволяет задавать собственный id.

client.subscribe(
    '/topic/logs',
    message => {
        console.log(message.body);
    },
    {
        id: 'logs-subscription'
    }
);

Позже можно отменить подписку через клиент:

client.unsubscribe('logs-subscription');

Такой подход удобен:

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

unsubscribe() у объекта против client.unsubscribe()

Существуют два варианта отмены.

Через объект подписки

const sub = client.subscribe('/topic/test', callback);

sub.unsubscribe();

Через идентификатор

client.unsubscribe('my-subscription-id');

Оба варианта приводят к отправке UNSUBSCRIBE.


Передача заголовков при отмене

Метод unsubscribe() поддерживает заголовки.

subscription.unsubscribe({
    receipt: 'unsubscribe-1'
});

STOMP-фрейм:

UNSUBSCRIBE
id:sub-0
receipt:unsubscribe-1

\0

Подтверждение отмены через receipt

Брокер может подтвердить выполнение операции.

client.onRece ipt = frame => {
    console.log('Получено подтверждение:', frame.headers['receipt-id']);
};

subscription.unsubscribe({
    receipt: 'remove-sub'
});

После успешной обработки брокер отправит RECEIPT.

Это важно:

  • при критичных операциях;
  • при строгом контроле состояния;
  • в финансовых системах;
  • в системах очередей задач.

Автоматическая отмена при отключении

При закрытии STOMP-соединения все подписки удаляются автоматически.

client.deactivate();

После деактивации:

  • соединение WebSocket закрывается;
  • подписки уничтожаются;
  • брокер очищает связанные ресурсы.

Утечки подписок

Одна из самых распространённых проблем — создание новых подписок без отмены старых.

Пример ошибки:

function initChat() {
    client.subscribe('/topic/chat', message => {
        console.log(message.body);
    });
}

Если initChat() вызывается многократно:

  • число подписчиков растёт;
  • сообщения начинают дублироваться;
  • увеличивается нагрузка;
  • расходуется память.

Правильное управление жизненным циклом

Сохранение подписки

let chatSubscription = null;

function connectChat() {
    chatSubscription = client.subscribe('/topic/chat', message => {
        console.log(message.body);
    });
}

Очистка

function disconnectChat() {
    if (chatSubscription) {
        chatSubscription.unsubscribe();
        chatSubscription = null;
    }
}

Отмена подписок в SPA-приложениях

В React, Vue и Angular отмена подписок обязательна при уничтожении компонентов.


React

Подписка

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

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

Очистка в return предотвращает:

  • дублирование обработчиков;
  • накопление подписок;
  • повторную обработку сообщений.

Vue

export default {
    mounted() {
        this.subscription = client.subscribe('/topic/data', message => {
            console.log(message.body);
        });
    },

    beforeUnmount() {
        if (this.subscription) {
            this.subscription.unsubscribe();
        }
    }
};

Angular

private stompSubscription: StompSubscription;

ngOnInit(): void {
    this.stompSubscription = this.client.subscribe(
        '/topic/orders',
        message => {
            console.log(message.body);
        }
    );
}

ngOnDestroy(): void {
    if (this.stompSubscription) {
        this.stompSubscription.unsubscribe();
    }
}

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

При реконнекте STOMP.js может автоматически восстанавливать подписки, если логика построена неправильно.

Проблемный вариант:

client.onConn ect = () => {
    client.subscribe('/topic/chat', callback);
};

Если соединение потеряется и восстановится:

  • onConnect вызовется снова;
  • создастся новая подписка;
  • старые обработчики могут сохраниться.

Безопасная схема реконнекта

let subscription = null;

client.onConn ect = () => {

    if (subscription) {
        subscription.unsubscribe();
    }

    subscription = client.subscribe('/topic/chat', callback);
};

Массовая отмена подписок

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

Хранение массива подписок

const subscriptions = [];

Добавление

subscriptions.push(
    client.subscribe('/topic/a', callbackA)
);

subscriptions.push(
    client.subscribe('/topic/b', callbackB)
);

Массовая очистка

subscriptions.forEach(sub => {
    sub.unsubscribe();
});

subscriptions.length = 0;

Менеджер подписок

class SubscriptionManager {

    constructor() {
        this.subscriptions = [];
    }

    add(subscription) {
        this.subscriptions.push(subscription);
    }

    clear() {

        this.subscriptions.forEach(sub => {
            sub.unsubscribe();
        });

        this.subscriptions = [];
    }
}

Использование:

const manager = new SubscriptionManager();

manager.add(
    client.subscribe('/topic/chat', callback)
);

manager.add(
    client.subscribe('/topic/news', callback)
);

manager.clear();

Поведение сообщений после отмены

Сообщения, отправленные после unsubscribe(), доставляться не будут.

Однако сообщения, уже находящиеся:

  • в сетевом буфере;
  • в очереди обработки браузера;
  • в event loop,

могут быть обработаны.

Поэтому иногда callback может выполниться ещё один раз сразу после отмены.


Защита от обработки после unsubscribe()

Используется флаг активности.

let active = true;

const subscription = client.subscribe('/topic/test', message => {

    if (!active) {
        return;
    }

    console.log(message.body);
});

Отмена:

active = false;
subscription.unsubscribe();

Ошибки при отмене подписки

Повторный unsubscribe()

subscription.unsubscribe();
subscription.unsubscribe();

Поведение зависит от версии STOMP.js и реализации брокера.

Возможны:

  • игнорирование;
  • предупреждение;
  • ошибка;
  • отсутствие эффекта.

Отмена несуществующей подписки

client.unsubscribe('unknown-id');

Возможные последствия:

  • брокер проигнорирует запрос;
  • будет возвращена ошибка;
  • клиент выведет предупреждение.

Проверка существования подписки

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

Связь ack и unsubscribe()

При ack: 'client' возможна ситуация:

  1. сообщение получено;
  2. подписка отменяется;
  3. подтверждение не отправлено.

Пример:

const sub = client.subscribe(
    '/queue/tasks',
    message => {

        processTask(message);

        message.ack();
    },
    {
        ack: 'client'
    }
);

Если выполнить unsubscribe() раньше ack(), брокер может:

  • переотправить сообщение;
  • считать обработку незавершённой;
  • вернуть сообщение в очередь.

Безопасный порядок действий

Правильная последовательность:

  1. обработка сообщения;
  2. отправка ACK;
  3. отмена подписки.
message.ack();
subscription.unsubscribe();

Временные подписки

Иногда подписка нужна только на короткий период.

Одноразовое получение сообщения

const subscription = client.subscribe('/topic/once', message => {

    console.log(message.body);

    subscription.unsubscribe();
});

После первого сообщения подписка удаляется.


Таймер автоматической отмены

const subscription = client.subscribe('/topic/live', callback);

setTimeout(() => {
    subscription.unsubscribe();
}, 5000);

Подписка будет активна 5 секунд.


Отмена при смене страницы

window.addEventListener('beforeunload', () => {

    if (subscription) {
        subscription.unsubscribe();
    }

    client.deactivate();
});

Это уменьшает вероятность зависших подписок на брокере.


Диагностика проблем с unsubscribe()

Типичные признаки неправильной очистки:

Симптом Возможная причина
Дублирование сообщений Несколько активных подписок
Рост памяти Подписки не удаляются
Повторные callback Ошибки реконнекта
Повышенная нагрузка Избыточные подписчики
Сообщения после ухода со страницы Нет unsubscribe()

Логирование отмены подписок

const subscription = client.subscribe('/topic/logs', callback);

console.log('Подписка создана');

subscription.unsubscribe();

console.log('Подписка удалена');

Отладка через debug

client.debug = str => {
    console.log(str);
};

При отмене можно увидеть:

>>> UNSUBSCRIBE
id:sub-0

Это помогает:

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

Рекомендации по управлению подписками

Централизованное хранение

Подписки не должны существовать в случайных местах кода.

Лучше:

  • использовать менеджеры;
  • хранить ссылки;
  • контролировать жизненный цикл.

Обязательная очистка

Каждая подписка должна иметь:

  • точку создания;
  • точку удаления.

Защита от дублирования

Перед новой подпиской полезно удалять старую:

if (subscription) {
    subscription.unsubscribe();
}

Очистка при реконнекте

После переподключения необходимо:

  • проверять старые подписки;
  • не создавать дубликаты;
  • восстанавливать только актуальные каналы.

Контроль временных подписок

Временные каналы особенно опасны с точки зрения утечек.

Необходимо:

  • удалять их после завершения операции;
  • использовать таймеры;
  • очищать callback-обработчики.