Эмуляция брокера

Эмуляция брокера в контексте работы с STOMP.js представляет собой создание контролируемой среды, имитирующей поведение настоящего STOMP-брокера сообщений. Такая техника используется для тестирования, разработки без инфраструктурных зависимостей и воспроизведения сложных сценариев доставки сообщений без подключения к реальному серверу WebSocket/STOMP.


STOMP-протокол опирается на концепцию брокера сообщений, который принимает команды клиента и маршрутизирует сообщения между подписчиками по адресам (destinations). В типичной архитектуре брокер отвечает за:

  • управление подписками (SUBSCRIBE)
  • маршрутизацию сообщений по destination
  • обработку отправки сообщений (SEND)
  • подтверждение доставки (ACK, NACK)
  • жизненный цикл соединения

Эмуляция может затрагивать любой из этих уровней, но на практике выделяются три основных подхода:

  • эмуляция на уровне клиента (mock STOMP-клиента)
  • эмуляция транспортного уровня (WebSocket stub)
  • эмуляция брокера как отдельного in-memory сервиса

Клиентская эмуляция брокера

Наиболее простой вариант — подмена реального STOMP-клиента логикой, которая имитирует поведение брокера внутри процесса.

Основная идея: все подписки и сообщения хранятся в памяти.

Базовая структура

class MockBroker {
  constructor() {
    this.subscriptions = new Map();
  }

  subscribe(destination, callback) {
    if (!this.subscriptions.has(destination)) {
      this.subscriptions.set(destination, new Set());
    }

    this.subscriptions.get(destination).add(callback);

    return {
      unsubscribe: () => {
        this.subscriptions.get(destination)?.delete(callback);
      }
    };
  }

  send(destination, body) {
    const subs = this.subscriptions.get(destination);
    if (!subs) return;

    const frame = {
      destination,
      body: typeof body === 'string' ? body : JSON.stringify(body)
    };

    subs.forEach(cb => cb(frame));
  }
}

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


Интеграция MockBroker с STOMP-клиентом

При использовании STOMP.js можно подменить транспортный слой, внедрив mock через адаптер.

Идея адаптера

STOMP-клиент ожидает WebSocket-подобный интерфейс:

  • send
  • close
  • onmessage

Эмуляция может реализовать этот контракт:

class MockWebSocket {
  constructor(broker) {
    this.broker = broker;
    this.onmess age = null;
  }

  send(frame) {
    const parsed = JSON.parse(frame);

    if (parsed.command === 'SUBSCRIBE') {
      this.broker.subscribe(parsed.destination, (msg) => {
        this.onmessage?.({
          data: JSON.stringify(msg)
        });
      });
    }

    if (parsed.command === 'SEND') {
      this.broker.send(parsed.destination, parsed.body);
    }
  }

  close() {}
}

Эмуляция маршрутизации сообщений

Реальный брокер поддерживает сложные правила маршрутизации:

  • очереди (/queue/*)
  • топики (/topic/*)
  • wildcard-подписки
  • fan-out доставка

В эмуляции важно воспроизвести хотя бы базовую семантику.

Простая маршрутизация

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

  subscribe(pattern, callback) {
    this.subscriptions.push({ pattern, callback });
  }

  send(destination, message) {
    this.subscriptions.forEach(sub => {
      if (this.match(sub.pattern, destination)) {
        sub.callback({ destination, body: message });
      }
    });
  }

  match(pattern, destination) {
    if (pattern === destination) return true;

    if (pattern.endsWith('*')) {
      const base = pattern.slice(0, -1);
      return destination.startsWith(base);
    }

    return false;
  }
}

Эмуляция очередей и топиков

Для более точной симуляции поведения брокера необходимо разделять semantics:

  • queue: сообщение доставляется одному подписчику
  • topic: сообщение доставляется всем подписчикам

Реализация

class AdvancedMockBroker {
  constructor() {
    this.queues = new Map();
    this.topics = new Map();
  }

  subscribe(destination, callback) {
    const isQueue = destination.startsWith('/queue/');
    const store = isQueue ? this.queues : this.topics;

    if (!store.has(destination)) {
      store.set(destination, new Set());
    }

    const set = store.get(destination);
    set.add(callback);

    return {
      unsubscribe: () => set.delete(callback)
    };
  }

  send(destination, message) {
    const isQueue = destination.startsWith('/queue/');
    const store = isQueue ? this.queues : this.topics;

    const subscribers = store.get(destination);
    if (!subscribers || subscribers.size === 0) return;

    if (isQueue) {
      const [first] = subscribers;
      first({ destination, body: message });
    } else {
      subscribers.forEach(cb => cb({ destination, body: message }));
    }
  }
}

Эмуляция ACK/NACK механики

В реальном STOMP-брокере подтверждение доставки играет ключевую роль. Эмуляция может включать ручное управление состоянием сообщений.

Модель с подтверждением

class AckMockBroker {
  constructor() {
    this.subscriptions = new Map();
    this.unacked = new Map();
  }

  subscribe(destination, callback) {
    this.subscriptions.set(destination, callback);
  }

  send(destination, message) {
    const cb = this.subscriptions.get(destination);
    if (!cb) return;

    const messageId = Date.now() + Math.random();

    const frame = {
      id: messageId,
      destination,
      body: message,
      ack: (status) => this.ack(messageId, status)
    };

    this.unacked.set(messageId, frame);
    cb(frame);
  }

  ack(id, status) {
    const msg = this.unacked.get(id);
    if (!msg) return;

    if (status === 'ACK') {
      this.unacked.delete(id);
    }

    if (status === 'NACK') {
      this.resend(msg);
    }
  }

  resend(msg) {
    const cb = this.subscriptions.get(msg.destination);
    cb?.(msg);
  }
}

Использование эмуляции в модульном тестировании

Эмуляция брокера применяется для проверки:

  • корректности подписок
  • логики обработки сообщений
  • повторной доставки
  • отказоустойчивости

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

test('message delivery', () => {
  const broker = new AdvancedMockBroker();

  let received = null;

  broker.subscribe('/topic/news', (msg) => {
    received = msg.body;
  });

  broker.send('/topic/news', 'hello');

  expect(received).toBe('hello');
});

Эмуляция задержек и асинхронности

Реальные брокеры работают асинхронно, поэтому mock должен учитывать тайминги.

send(destination, message, delay = 0) {
  setTimeout(() => {
    const subs = this.subscriptions.get(destination);
    subs?.forEach(cb => cb({ destination, body: message }));
  }, delay);
}

Такой подход позволяет тестировать:

  • race conditions
  • порядок доставки
  • debounce/throttle-логику

Симуляция отказов и сетевых ошибок

Эмуляция брокера часто включает сценарии нестабильной сети:

  • разрыв соединения
  • потеря сообщений
  • дублирование доставки

Пример отказа доставки

send(destination, message) {
  if (Math.random() < 0.2) {
    throw new Error('Simulated broker failure');
  }

  const subs = this.subscriptions.get(destination);
  subs?.forEach(cb => cb({ destination, body: message }));
}

Комбинированная архитектура эмулятора

В сложных тестовых средах эмулятор брокера включает несколько слоёв:

  • transport layer (WebSocket mock)
  • protocol layer (STOMP frame parsing)
  • broker core (routing, queues, topics)
  • delivery layer (ack/retry/delay)

Такая структура позволяет максимально приблизиться к поведению реального сервера без внешних зависимостей и сохраняет предсказуемость тестов при работе с STOMP.js.