Цепочка обработчиков

Модель обработки сообщений в STOMP.js естественным образом опирается на последовательную трансформацию входящих и исходящих данных через набор независимых обработчиков. Такой подход позволяет выстраивать расширяемую систему промежуточных слоёв, где каждый компонент отвечает за строго ограниченную задачу: модификацию заголовков, фильтрацию сообщений, логирование, повторные попытки, нормализацию payload или интеграцию с внешними состояниями приложения.

Цепочка обработчиков формирует линейный или частично разветвлённый pipeline, через который проходят все операции взаимодействия с брокером сообщений. Внутри STOMP.js это особенно важно из-за асинхронной природы доставки сообщений и необходимости гибко управлять жизненным циклом подписок.


Базовая модель цепочки

Цепочка обработчиков представляет собой последовательность функций, каждая из которых получает результат предыдущей и передаёт управление дальше.

Формально:

input → handler1 → handler2 → handler3 → output

Каждый обработчик имеет единый контракт:

  • принимает контекст сообщения
  • может модифицировать его
  • может прервать дальнейшую обработку
  • возвращает результат или передаёт управление дальше

Контекст обычно включает:

  • payload сообщения
  • headers STOMP-пакета
  • метаданные подписки
  • состояние соединения
  • служебные флаги обработки

Роль цепочки в STOMP.js

STOMP.js работает поверх WebSocket и реализует протокол STOMP, где каждое сообщение может быть отправлено или получено через брокер (RabbitMQ, ActiveMQ, Apollo и др.).

Цепочка обработчиков используется в нескольких ключевых местах:

  • обработка входящих сообщений (frame dispatch)
  • обработка подписок (subscription pipeline)
  • обработка отправки сообщений (publish pipeline)
  • управление reconnect логикой
  • трансформация payload (JSON, binary, custom codecs)

Такой дизайн позволяет отделить транспортный слой от бизнес-логики.


Контракт обработчика

Типичный обработчик в цепочке имеет следующую форму:

function handler(context, next) {
  // обработка контекста
  return next(context);
}

Где:

  • context — объект сообщения или операции
  • next — функция передачи управления дальше по цепочке

Ключевой момент заключается в том, что обработчик может:

  1. модифицировать контекст перед передачей
  2. заменить результат полностью
  3. не вызывать next, тем самым прерывая цепочку
  4. выполнять асинхронные операции

Синхронные и асинхронные цепочки

STOMP.js должен учитывать, что обработка сообщений часто включает асинхронные операции: декодирование, запросы к состоянию, запись в store, валидацию токенов.

Синхронная модель

handler1 → handler2 → handler3

Используется при простых трансформациях:

  • преобразование заголовков
  • нормализация payload
  • логирование

Асинхронная модель

await handler1 → await handler2 → await handler3

Используется при:

  • обращении к внешним API
  • работе с хранилищем состояния
  • восстановлении сессии
  • обработке retry/backoff логики

Реализация композиции цепочки

Основной механизм построения цепочки — композиция функций.

function compose(handlers) {
  return function(context) {
    let index = -1;

    function dispatch(i, ctx) {
      if (i <= index) {
        throw new Error("next() called multiple times");
      }

      index = i;

      const handler = handlers[i];
      if (!handler) return ctx;

      return handler(ctx, (nextCtx) => dispatch(i + 1, nextCtx));
    }

    return dispatch(0, context);
  };
}

Такая реализация обеспечивает:

  • строгий порядок выполнения
  • защиту от повторного вызова next
  • возможность трансформации контекста на каждом этапе
  • единый поток управления

Контекст как носитель состояния

Важным элементом цепочки является объект контекста. В STOMP.js он играет роль переносчика состояния между обработчиками.

Типичный контекст:

{
  frame: {
    command: "MESSAGE",
    headers: {},
    body: ""
  },
  subscription: {},
  connection: {},
  meta: {
    timestamp: Date.now(),
    retries: 0
  }
}

Особенность заключается в том, что контекст не копируется, а передаётся по ссылке. Это позволяет:

  • избегать лишних затрат памяти
  • сохранять состояние между этапами
  • реализовывать middleware с накоплением данных

Разрыв цепочки

Одной из ключевых возможностей является возможность прервать выполнение цепочки.

function authHandler(ctx, next) {
  if (!ctx.frame.headers.authorization) {
    ctx.error = "Unauthorized";
    return ctx;
  }

  return next(ctx);
}

Разрыв используется в сценариях:

  • аутентификация и авторизация
  • фильтрация сообщений
  • дедупликация
  • rate limiting
  • контроль целостности данных

Порядок обработчиков и приоритеты

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

Общая модель приоритетов:

  1. системные обработчики (низкоуровневые)
  2. транспортные трансформеры
  3. middleware приложения
  4. бизнес-логика

Нарушение порядка приводит к:

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

Обработка входящих сообщений

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

  1. декодирование WebSocket payload
  2. парсинг STOMP frame
  3. нормализация заголовков
  4. вызов цепочки обработчиков
  5. маршрутизация в подписки

Пример логической цепочки:

decode → parseFrame → normalize → middlewareChain → dispatchToSubscriber

Каждый этап может быть расширен через собственные обработчики.


Обработка исходящих сообщений

При отправке сообщений цепочка используется аналогично:

validate → enrichHeaders → serialize → sendFrame

Обработчики могут:

  • добавлять correlation-id
  • внедрять trace-id
  • сериализовать JSON или binary payload
  • применять compression

Асинхронное прерывание и отложенная передача

Некоторые обработчики требуют задержки выполнения:

  • ожидание токена авторизации
  • обновление состояния соединения
  • синхронизация с store

В таких случаях цепочка превращается в поток с отложенным разрешением:

async function tokenHandler(ctx, next) {
  ctx.token = await refreshTokenIfNeeded();
  return next(ctx);
}

Вложенные цепочки

STOMP.js позволяет строить вложенные pipeline, где один обработчик содержит собственную цепочку.

mainChain
   ↓
subChain (subscription-specific)
   ↓
handler

Это используется для:

  • изоляции подписок
  • отдельных политик обработки
  • динамического подключения middleware

Ошибки в цепочке

Ошибки могут возникать на любом этапе. Их обработка требует отдельного слоя:

function errorHandler(ctx, next) {
  try {
    return next(ctx);
  } catch (err) {
    ctx.error = err;
    return ctx;
  }
}

Типичные стратегии:

  • проброс ошибки вверх
  • преобразование в frame error
  • повторная отправка
  • переключение fallback-обработчика

Декорирование обработчиков

Обработчики часто оборачиваются декораторами:

  • логирование времени выполнения
  • трассировка вызовов
  • кеширование результатов
  • контроль повторов

Пример:

function withLogging(handler) {
  return async (ctx, next) => {
    const start = Date.now();
    const result = await handler(ctx, next);
    const end = Date.now();

    ctx.meta.duration = end - start;
    return result;
  };
}

Совместимость с реактивными системами

Цепочка обработчиков часто интегрируется с реактивными хранилищами состояния:

  • Redux
  • Zustand
  • MobX
  • custom reactive stores

В этом случае обработчики не только трансформируют сообщения, но и инициируют изменения состояния приложения.


Масштабирование цепочки

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

  • избыточные вызовы next
  • глубокая вложенность
  • потеря читаемости

Оптимизации включают:

  • кэширование результатов обработчиков
  • объединение статических middleware
  • разделение цепочек по типам сообщений
  • ленивую инициализацию pipeline

Итоговая модель поведения

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