Переход с старых версий

Миграция между версиями STOMP.js чаще всего связана со следующими изменениями:

  • отказ от устаревших API;
  • переход на TypeScript-ориентированную архитектуру;
  • изменение механизма подключения;
  • переработка обработки ошибок и reconnect-логики;
  • отказ от глобального объекта Stomp;
  • изменение схемы импорта;
  • улучшение совместимости с современными сборщиками;
  • разделение SockJS и STOMP-клиента;
  • поддержка ESM и tree-shaking.

Особенно заметны изменения при переходе:

  • с stompjs@stomp/stompjs;
  • с версий 2.x → 5.x;
  • с версий 5.x → 6.x/7.x.

Старый пакет stompjs

Ранние проекты часто использовали библиотеку:

npm install stompjs

Импорт обычно выглядел следующим образом:

import Stomp from 'stompjs';

Или:

<script src="stomp.min.js"></script>

Создание клиента:

const socket = new WebSocket('ws://localhost:15674/ws');

const client = Stomp.over(socket);

client.connect({}, () => {
    console.log('Connected');
});

Главные особенности старой архитектуры:

  • глобальный namespace;
  • callback-based API;
  • отсутствие полноценной типизации;
  • слабая интеграция с современными bundler;
  • ограниченные возможности reconnect;
  • высокая зависимость от SockJS;
  • большое количество неявного поведения.

Современный пакет @stomp/stompjs

Новая библиотека устанавливается так:

npm install @stomp/stompjs

Импорт:

import { Client } from '@stomp/stompjs';

Создание клиента:

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws'
});

Подключение:

client.activate();

Главное отличие — объект Client теперь управляет всем жизненным циклом соединения.


Переход с Stomp.over() на Client

Старый подход

const socket = new WebSocket(url);

const client = Stomp.over(socket);

client.connect(headers, onConnect);

Новый подход

const client = new Client({
    brokerURL: url,
    connectHeaders: headers,
    onConnect: () => {
        console.log('Connected');
    }
});

client.activate();

Изменения:

Старый API Новый API
Stomp.over() new Client()
connect() activate()
disconnect() deactivate()
debug = fn debug: fn
reconnect_delay reconnectDelay

Изменение механизма подключения

Старый стиль

client.connect(
    {
        login: 'admin',
        passcode: 'admin'
    },
    () => {
        console.log('Connected');
    },
    (error) => {
        console.error(error);
    }
);

Новый стиль

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',

    connectHeaders: {
        login: 'admin',
        passcode: 'admin'
    },

    onConnect: () => {
        console.log('Connected');
    },

    onStompError: (frame) => {
        console.error(frame);
    }
});

Новая модель убирает длинные callback-цепочки и делает конфигурацию декларативной.


Изменение reconnect-логики

Старые версии

Ранее reconnect выглядел так:

client.reconnect_delay = 5000;

Это поле изменялось напрямую после создания клиента.

Новые версии

Теперь reconnect задаётся через конфигурацию:

const client = new Client({
    brokerURL: url,
    reconnectDelay: 5000
});

Либо:

client.reconnectDelay = 5000;

Современный reconnect работает стабильнее:

  • поддерживает автоматический restart heartbeat;
  • корректно восстанавливает subscriptions;
  • предотвращает гонки reconnect;
  • уменьшает количество дублирующих соединений.

Изменение heartbeat API

Старые версии

client.heartbeat.outgoing = 20000;
client.heartbeat.incoming = 0;

Новые версии

API сохранилось, но применяется через Client:

const client = new Client({
    heartbeatIncoming: 0,
    heartbeatOutgoing: 20000
});

Допускается и динамическое изменение:

client.heartbeatIncoming = 0;
client.heartbeatOutgoing = 20000;

Переход от callback API к lifecycle callbacks

Старые версии использовали callbacks прямо внутри connect():

client.connect(headers, onConnect, onError);

Современные версии используют lifecycle handlers:

const client = new Client({
    onConnect: () => {},
    onDisconnect: () => {},
    onWebSocketClose: () => {},
    onWebSocketError: () => {},
    onStompError: () => {}
});

Преимущества:

  • события разделены логически;
  • легче поддерживать код;
  • упрощается тестирование;
  • проще интегрировать Redux/Vuex/Pinia;
  • улучшается совместимость с React hooks.

Изменение подписок

Старый стиль

client.subscribe('/queue/test', (message) => {
    console.log(message.body);
});

Новый стиль

Синтаксис почти не изменился:

client.subscribe('/queue/test', (message) => {
    console.log(message.body);
});

Но изменилось поведение reconnect.

В старых версиях после reconnect подписки могли теряться.

В новых версиях:

  • subscriptions восстанавливаются автоматически;
  • клиент отслеживает активные подписки;
  • уменьшается вероятность race condition.

Изменение публикации сообщений

Старые версии

client.send(
    '/queue/test',
    {
        priority: 9
    },
    JSON.stringify(data)
);

Новые версии

client.publish({
    destination: '/queue/test',
    headers: {
        priority: 9
    },
    body: JSON.stringify(data)
});

Причины изменения:

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

Переход с send() на publish()

Старый API:

client.send(destination, headers, body);

Проблемы:

  • сложно помнить порядок аргументов;
  • большое количество optional параметров;
  • ошибки при передаче null;
  • неочевидность назначения аргументов.

Новый API:

client.publish({
    destination,
    headers,
    body
});

Преимущества:

  • самодокументируемый код;
  • проще рефакторинг;
  • удобнее типизация;
  • легче расширять API.

Изменение отключения

Старые версии

client.disconnect(() => {
    console.log('Disconnected');
});

Новые версии

await client.deactivate();

Либо:

client.deactivate().then(() => {
    console.log('Disconnected');
});

Теперь отключение основано на Promise API.


Переход на Promise-ориентированную архитектуру

Старые версии активно использовали callbacks:

client.disconnect(callback);

Новые версии используют Promise:

await client.deactivate();

Это упрощает:

  • async/await;
  • error handling;
  • последовательность shutdown;
  • интеграцию с framework lifecycle.

Изменение debug API

Старые версии

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

Новые версии

const client = new Client({
    debug: (message) => {
        console.log(message);
    }
});

Либо:

client.debug = console.log;

Отключение debug в production

Современные версии позволяют централизованно отключать debug:

const client = new Client({
    debug: () => {}
});

Либо:

const client = new Client({
    debug: process.env.NODE_ENV === 'development'
        ? console.log
        : () => {}
});

Изменения в SockJS-интеграции

Ранее SockJS часто использовался автоматически:

const socket = new SockJS('/ws');

const client = Stomp.over(socket);

В новых версиях SockJS интегрируется через webSocketFactory.


Новый способ подключения SockJS

import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';

const client = new Client({
    webSocketFactory: () => {
        return new SockJS('/ws');
    }
});

Причины изменений:

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

Изменение auto-connect поведения

Старые версии:

client.connect();

Соединение создавалось сразу.

Новые версии:

client.activate();

Клиент переходит в активное состояние и сам управляет подключением.

Это важно для:

  • reconnect;
  • background retry;
  • heartbeat lifecycle;
  • автоматического восстановления.

Изменение структуры ошибок

Старый подход

client.connect({}, onConnect, (error) => {
    console.error(error);
});

Новый подход

const client = new Client({
    onStompError: (frame) => {
        console.error(frame.headers['message']);
        console.error(frame.body);
    }
});

Дополнительно появились:

onWebSocketError
onWebSocketClose
onDisconnect

Теперь можно отдельно обрабатывать:

  • STOMP protocol errors;
  • TCP disconnect;
  • WebSocket close;
  • transport errors;
  • broker exceptions.

Изменения в ACK API

Старые версии

message.ack();

И:

message.nack();

Новые версии

API осталось похожим:

message.ack();
message.nack();

Но изменилась внутренняя реализация:

  • улучшена поддержка transactions;
  • исправлены проблемы с reconnect;
  • улучшена совместимость с RabbitMQ;
  • исправлены race conditions.

Изменения в transactions

Старый стиль

const tx = client.begin();

client.send('/queue/test', {}, 'message');

tx.commit();

Новый стиль

const tx = client.begin();

client.publish({
    destination: '/queue/test',
    body: 'message',
    transaction: tx.id
});

tx.commit();

Изменения в типизации

Старые версии практически не имели нормальной TypeScript-поддержки.

Новые версии:

import { Client, IMessage } from '@stomp/stompjs';

const client: Client = new Client();

client.onConn ect = () => {};

client.subscribe('/queue/test', (message: IMessage) => {
    console.log(message.body);
});

Преимущества:

  • автодополнение;
  • безопасный refactoring;
  • строгая проверка типов;
  • лучшая IDE-интеграция.

Изменение импорта

CommonJS

Старые проекты:

const Stomp = require('stompjs');

ESM

Новые проекты:

import { Client } from '@stomp/stompjs';

Проблемы при миграции CommonJS

Некоторые старые webpack-конфигурации ожидают:

module.exports

Но современный пакет ориентирован на:

export

Из-за этого возникают ошибки:

Client is not a constructor

или:

Cannot read property 'over' of undefined

Миграция legacy-кода

Старый код

const socket = new SockJS('/ws');

const client = Stomp.over(socket);

client.connect({}, () => {
    client.send('/topic/chat', {}, 'Hello');
});

Современный код

import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';

const client = new Client({
    webSocketFactory: () => new SockJS('/ws'),

    onConnect: () => {
        client.publish({
            destination: '/topic/chat',
            body: 'Hello'
        });
    }
});

client.activate();

Типичные ошибки при переходе

Использование connect()

После миграции многие продолжают вызывать:

client.connect();

В новых версиях правильный вариант:

client.activate();

Использование send()

Старый API:

client.send(...);

Новый API:

client.publish(...);

Использование disconnect()

Старый API:

client.disconnect();

Новый API:

await client.deactivate();

Попытка использовать Stomp.over()

В новых версиях:

Stomp.over()

чаще всего отсутствует.

Необходимо использовать:

new Client()

Изменение внутренней архитектуры

Современный STOMP.js:

  • использует state machine;
  • лучше управляет reconnect;
  • имеет предсказуемый lifecycle;
  • уменьшает memory leaks;
  • корректнее обрабатывает browser sleep;
  • поддерживает async shutdown;
  • устойчивее к нестабильной сети.

Миграция больших проектов

В крупных приложениях переход обычно выполняется поэтапно.

Часто используется адаптер:

class LegacyStompAdapter {
    constructor(url) {
        this.client = new Client({
            brokerURL: url
        });
    }

    connect(headers, callback) {
        this.client.onConn ect = callback;
        this.client.activate();
    }

    send(destination, headers, body) {
        this.client.publish({
            destination,
            headers,
            body
        });
    }
}

Такой подход позволяет:

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

Проверка совместимости брокера

При переходе необходимо проверять:

  • поддержку heartbeat;
  • поддержку STOMP 1.2;
  • корректность ACK;
  • reconnect behavior;
  • обработку transactions;
  • совместимость SockJS;
  • поддержку бинарных payload.

Особенно важно тестировать:

  • RabbitMQ;
  • ActiveMQ;
  • Artemis;
  • Spring WebSocket Broker.

Изменения в STOMP-протоколе

Современный STOMP.js лучше поддерживает:

  • STOMP 1.1;
  • STOMP 1.2;
  • heart-beating;
  • receipt handling;
  • nack frames;
  • binary frames.

Переход на бинарные сообщения

Старые версии плохо работали с binary payload.

Новые версии поддерживают:

client.publish({
    destination: '/queue/file',
    binaryBody: uint8Array
});

Получение:

client.subscribe('/queue/file', (message) => {
    const data = message.binaryBody;
});

Изменение receipt handling

Старые версии

client.send('/queue/test', {
    receipt: '123'
}, 'hello');

Новые версии

client.watchForReceipt('123', () => {
    console.log('Received');
});

client.publish({
    destination: '/queue/test',
    headers: {
        receipt: '123'
    },
    body: 'hello'
});

Рекомендации при миграции

Наиболее безопасная стратегия:

  1. обновить зависимости;
  2. заменить импорт;
  3. заменить Stomp.over;
  4. заменить connect;
  5. заменить send;
  6. заменить disconnect;
  7. проверить reconnect;
  8. проверить subscriptions;
  9. протестировать heartbeat;
  10. протестировать broker compatibility.

Полный пример современного клиента

import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';

const client = new Client({
    webSocketFactory: () => {
        return new SockJS('/ws');
    },

    reconnectDelay: 5000,

    heartbeatIncoming: 10000,
    heartbeatOutgoing: 10000,

    connectHeaders: {
        login: 'admin',
        passcode: 'admin'
    },

    debug: console.log,

    onConnect: () => {
        console.log('Connected');

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

        client.publish({
            destination: '/topic/chat',
            body: 'Hello'
        });
    },

    onStompError: (frame) => {
        console.error(frame);
    },

    onWebSocketClose: () => {
        console.log('Socket closed');
    }
});

client.activate();