Стратегии миграции

Миграция между версиями STOMP.js обычно связана с несколькими факторами:

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

Наиболее заметные изменения произошли при переходе:

  • со старых версий stompjs на @stomp/stompjs;
  • с callback-архитектуры на конфигурационный подход;
  • с ручного управления сокетами на внутренний lifecycle-контроль клиента.

Переход со старого пакета stompjs

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

npm install stompjs

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

import Stomp from 'stompjs';

Современная версия использует пакет:

npm install @stomp/stompjs

Новый импорт:

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

Основные отличия

Старый API Новый API
Stomp.client() new Client()
Callback connect Конфигурация клиента
reconnect_delay reconnectDelay
Частичная типизация Полная поддержка TypeScript
Глобальный объект ES-модули

Стратегия постепенной миграции

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

Этап 1. Изоляция STOMP-логики

Перед обновлением рекомендуется вынести работу с STOMP в отдельный сервис.

Плохой вариант:

const socket = new WebSocket(url);
const client = Stomp.over(socket);

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

STOMP-логика распределена по приложению.

Хороший вариант:

export class MessagingService {
    connect() {}
    disconnect() {}
    subscribe() {}
    publish() {}
}

После изоляции миграция становится локальной задачей.


Создание адаптера совместимости

При большом количестве legacy-кода полезно создавать промежуточный слой совместимости.

Старый API

client.send('/topic/test', {}, JSON.stringify(data));

Новый API

client.publish({
    destination: '/topic/test',
    body: JSON.stringify(data)
});

Адаптер

class LegacyAdapter {
    constructor(client) {
        this.client = client;
    }

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

Такой подход позволяет обновлять систему постепенно.


Миграция подключения

Старый способ

const socket = new WebSocket(url);

const client = Stomp.over(socket);

client.connect(
    login,
    password,
    onConnect,
    onError
);

Новый способ

const client = new Client({
    brokerURL: url,

    connectHeaders: {
        login,
        passcode: password
    },

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

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

Ключевые изменения

1. Конфигурационный объект

Современная архитектура строится вокруг объекта конфигурации.

2. Lifecycle-события

Вместо большого числа callback-функций используются обработчики событий:

onConnect
onDisconnect
onWebSocketClose
onWebSocketError
onUnhandledMessage
onUnhandledReceipt

3. Активация клиента

Раньше соединение создавалось через connect().

Теперь:

client.activate();

Отключение:

client.deactivate();

Миграция подписок

Старый вариант

client.subscribe('/queue/test', callback);

Новый вариант

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

На первый взгляд код почти идентичен, однако поведение acknowledgement и lifecycle отличается.


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

Старые проекты часто используют implicit ack.

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

client.subscribe('/queue/tasks', message => {
    processTask(message);
});

Современный explicit ack

client.subscribe(
    '/queue/tasks',
    message => {
        processTask(message);
        message.ack();
    },
    {
        ack: 'client'
    }
);

Причины перехода

Implicit ack опасен:

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

Миграция reconnect-механизма

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

client.reconnect_delay = 5000;

Новые версии

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

Дополнительные возможности

Современные версии поддерживают:

  • backoff-стратегии;
  • heartbeat-контроль;
  • reconnect lifecycle hooks;
  • отслеживание состояния подключения.

Стратегия безопасного reconnect

Автоматический reconnect может привести к лавинообразным подключениям.

Плохой вариант:

reconnectDelay: 100

Это создаёт:

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

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

reconnectDelay: 5000

Или динамический backoff:

let reconnectDelay = 1000;

client.onWebSocketCl ose = () => {
    reconnectDelay = Math.min(reconnectDelay * 2, 30000);

    client.configure({
        reconnectDelay
    });
};

Миграция heartbeat-настроек

Старый формат

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

Новый формат

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

Миграция publish API

Старый send

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

Новый publish

client.publish({
    destination: '/topic/chat',

    headers: {
        priority: 9
    },

    body: JSON.stringify(data)
});

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

Старые версии STOMP.js были ориентированы преимущественно на текстовые payload.

Современные версии поддерживают бинарные данные.

Отправка Uint8Array

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

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

  • передача protobuf;
  • работа с MessagePack;
  • передача файлов;
  • оптимизация трафика;
  • снижение нагрузки на JSON parser.

Стратегия миграции больших приложений

Горизонтальная миграция

Обновление выполняется по модулям:

  • чат;
  • уведомления;
  • аналитика;
  • очереди задач;
  • realtime dashboard.

Вертикальная миграция

Полная замена STOMP-слоя сразу во всём проекте.

Для production-систем обычно безопаснее горизонтальный подход.


Миграция legacy callback-архитектуры

Старый стиль

client.connect({}, function() {

    client.subscribe('/topic/a', function(msg) {
        console.log(msg.body);
    });

});

Современный стиль

const client = new Client({
    brokerURL: url,

    onConnect: () => {
        client.subscribe('/topic/a', msg => {
            console.log(msg.body);
        });
    }
});

Переход на TypeScript

Одним из главных преимуществ современных версий является встроенная типизация.

JavaScript

client.publish({
    destination: '/topic/test',
    body: data
});

TypeScript

client.publish({
    destination: '/topic/test',
    body: JSON.stringify(data)
});

Типизация сообщений

interface ChatMessage {
    id: number;
    text: string;
    author: string;
}

Использование DTO снижает вероятность ошибок сериализации.


Миграция обработчиков ошибок

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

client.connect({}, onConnect, onError);

Новые версии

const client = new Client({

    onStompError: frame => {
        console.log(frame.headers['message']);
    },

    onWebSocketError: event => {
        console.log(event);
    }
});

Разделение ошибок

Современная архитектура различает:

Тип ошибки Назначение
onStompError Ошибки STOMP-протокола
onWebSocketError Ошибки транспорта
onWebSocketClose Закрытие соединения
onDisconnect Корректное отключение

Стратегия миграции брокеров

Иногда обновление STOMP.js сопровождается переходом между брокерами:

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

Возможные проблемы

Различия в ACK

Некоторые брокеры:

  • по-разному интерпретируют client-individual;
  • используют собственные заголовки;
  • имеют ограничения по heartbeat.

Различия в destination

Примеры:

/topic/chat
/queue/tasks
/exchange/logs
/amq/queue/test

Во время миграции рекомендуется создавать abstraction layer.


Стратегия двойного клиента

В критически важных системах иногда временно используются два клиента.

Пример

const legacyClient = createLegacyClient();
const modernClient = createModernClient();

Назначение

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

Canary migration

Часть пользователей переводится на новую версию постепенно.

Пример

if (featureFlags.newStompClient) {
    useModernClient();
} else {
    useLegacyClient();
}

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

  • контролируемая нагрузка;
  • быстрое выявление ошибок;
  • безопасный rollback.

Blue-Green миграция

Используются две независимые среды.

Blue

Старая STOMP-инфраструктура.

Green

Новая версия STOMP.js и новый messaging layer.

После проверки трафик переключается полностью.


Стратегия rollback

Любая миграция должна предусматривать откат.

Критически важные элементы rollback

1. Совместимость протоколов

Нельзя удалять поддержку старого формата сообщений слишком рано.

2. Версионирование payload

Пример:

{
    "version": 2,
    "payload": {}
}

3. Изоляция transport layer

Бизнес-логика не должна зависеть от реализации STOMP-клиента.


Миграция RxJS-интеграции

Современные приложения часто интегрируют STOMP.js с RxJS.

Пример Observable-обёртки

import { Observable } from 'rxjs';

function observeTopic(client, topic) {
    return new Observable(subscriber => {

        const subscription = client.subscribe(
            topic,
            message => subscriber.next(message)
        );

        return () => subscription.unsubscribe();
    });
}

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

  • реактивная архитектура;
  • управление потоками;
  • debounce/throttle;
  • объединение realtime streams.

Миграция SPA-приложений

Особое внимание требуется для:

  • React;
  • Vue;
  • Angular.

Типичная ошибка

Создание нового STOMP-клиента при каждом рендере компонента.

Плохой вариант:

function Chat() {
    const client = new Client();
}

Правильная стратегия

Singleton или service container.

export const stompClient = new Client();

Миграция authentication-механизма

Старые системы часто используют login/passcode.

Современные приложения переходят на:

  • JWT;
  • OAuth2;
  • Bearer Token;
  • session gateway.

Пример JWT

const client = new Client({

    connectHeaders: {
        Authorization: `Bearer ${token}`
    }
});

Миграция Spring-инфраструктуры

При использовании Spring Framework часто встречается переход:

/sockjs

к полноценному WebSocket:

/ws

Причины

  • отказ от legacy browser support;
  • уменьшение overhead;
  • снижение latency;
  • отказ от fallback transport.

Стратегия тестирования миграции

Обязательные проверки

Подключение

Проверяется:

  • reconnect;
  • disconnect;
  • heartbeat;
  • SSL;
  • авторизация.

Подписки

Проверяется:

  • повторная подписка;
  • duplicate delivery;
  • unsubscribe;
  • ack.

Нагрузочное тестирование

Проверяются:

  • memory leaks;
  • reconnect storm;
  • broker saturation;
  • backlog accumulation.

Стратегия миграции в микросервисной архитектуре

При наличии множества frontend и backend сервисов миграция усложняется.

Типичная схема

Frontend → Gateway → Broker → Services

Рекомендуемый подход

1. Версионирование каналов

/topic/v1/chat
/topic/v2/chat

2. Поддержка двух форматов

{
    "version": 1
}
{
    "version": 2
}

3. Постепенное отключение legacy consumers


Миграция SockJS-интеграции

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

import SockJS from 'sockjs-client';

const socket = new SockJS(url);

const client = Stomp.over(socket);

Новый подход

const client = new Client({

    webSocketFactory: () => {
        return new SockJS(url);
    }
});

Миграция конфигурации

Ранние версии часто использовали mutation-style конфигурирование.

Старый вариант

client.debug = null;
client.reconnect_delay = 5000;

Новый вариант

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

Стратегия устранения breaking changes

Проблема camelCase

Старые свойства:

reconnect_delay

Новые свойства:

reconnectDelay

Возможная стратегия

Создание compatibility normalizer:

function normalize(config) {
    return {
        reconnectDelay: config.reconnect_delay
    };
}

Миграция монолитов

В монолитных frontend-приложениях STOMP-код часто смешан с UI.

Типичная проблема

button.oncl ick = () => {
    client.send(...);
};

Рекомендуемая стратегия

Разделение:

  • transport layer;
  • messaging layer;
  • application layer;
  • UI layer.

Миграция realtime state management

Современные приложения часто интегрируют STOMP.js с:

  • Redux;
  • Zustand;
  • MobX;
  • Pinia;
  • Vuex.

Пример Redux-dispatch

client.subscribe('/topic/chat', message => {

    store.dispatch({
        type: 'MESSAGE_RECEIVED',
        payload: JSON.parse(message.body)
    });

});

Проблемы совместимости браузеров

После миграции могут появиться ошибки:

  • отсутствие polyfill;
  • проблемы ESM;
  • несовместимость bundler;
  • ошибки tree shaking.

Частая проблема

global is not defined

Решение

Настройка bundler environment.


Стратегия финального отключения legacy-кода

После завершения миграции удаляются:

  • старые адаптеры;
  • compatibility wrappers;
  • fallback reconnect;
  • старые форматы сообщений;
  • deprecated endpoints;
  • временные feature flags.

Слишком раннее удаление legacy-слоя часто приводит к production-регрессиям, поэтому очистка инфраструктуры выполняется только после длительного периода стабильной эксплуатации новой версии.