Hummbitv1.x

Middleware: подробности

Middleware в hummbit — это композиционный слой перехвата вокруг переходов состояния и событий DevTools. Он нужен для сквозной (cross-cutting) логики без засорения доменных action:

  • логирование и трассировка;
  • аналитика и телеметрия;
  • замер производительности;
  • инварианты и runtime-проверки;
  • нормализация событий перед отправкой в DevTools.

1) Ментальная модель

Каждый middleware получает контекст (ctx) и возвращает любой набор hook-ов. Можно реализовать один hook или несколько hook-ов в одном middleware.

Поддерживаемые hooks:

  • wrapAction(name, next)
  • wrapSetState(next)
  • wrapMergeState(next)
  • afterUpdate(info)
  • devtoolsFilter(event)
  • devtoolsTransform(event)

wrap*-hooks работают как декораторы функций:

  • next — это "текущая реализация";
  • ваш hook может выполнить код до и/или после next;
  • можно пробрасывать аргументы как есть, модифицировать их или делать short-circuit.

2) Пайплайн выполнения и порядок

Middleware применяются по порядку в массиве. Для wrapper-хуков это означает: первый middleware становится самым внешним слоем:

const m1: Middleware<S> = () => ({
  wrapAction:
    (name, next) =>
    async (...args) => {
      console.log("m1 before", name);
      const result = await next(...args);
      console.log("m1 after", name);
      return result;
    },
});

const m2: Middleware<S> = () => ({
  wrapAction:
    (name, next) =>
    async (...args) => {
      console.log("m2 before", name);
      const result = await next(...args);
      console.log("m2 after", name);
      return result;
    },
});

// configureGlobalStore({ middleware: [m1, m2] })
// порядок вызовов:
// m1 before -> m2 before -> action body -> m2 after -> m1 after

Для не-wrapper hook-ов:

  • afterUpdate(info) вызывается после успешного обновления состояния;
  • devtoolsFilter и devtoolsTransform применяются к каждому исходящему событию DevTools в порядке middleware.

3) Подробно по каждому hook

wrapAction(name, next)

Перехватывает action, созданные через actionCreator(...). Типичные сценарии:

  • замер latency action;
  • добавление trace id;
  • глобальный try/catch с отправкой ошибок;
  • сбор аргументов/результата для отладки.

Нюансы:

  • action могут быть sync и async, поэтому wrapper обычно делают async и await next(...);
  • ошибки лучше пробрасывать дальше (throw), если вы не делаете осознанное преобразование.

wrapSetState(next)

Перехватывает вызовы setState. Полезно для:

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

Этот hook видит все прямые setState, включая вызовы внутри action.

wrapMergeState(next)

Перехватывает вызовы mergeState (API shallow merge по корню). Применяется для:

  • контроля разрешённых ключей в partial update;
  • нормализации частичных payload;
  • отдельного аудита merge-обновлений.

afterUpdate(info)

Hook пост-обновления. Подходит для:

  • неблокирующих side effects, которые не должны мутировать store-логику;
  • отправки телеметрии после фиксации состояния;
  • синхронизации с внешними наблюдателями.

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

devtoolsFilter(event)

Возвращает, нужно ли отправлять событие в DevTools. Используется для снижения шума (например, скрытия повторяющихся низкоуровневых событий).

devtoolsTransform(event)

Преобразует payload события перед отправкой в DevTools. Полезно для:

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

Рекомендуемый паттерн: сначала фильтрация, затем трансформация (в одном middleware или разнесённо по нескольким в нужном порядке).

4) Рекомендации по ctx

Контекст middleware даёт доступ к runtime-возможностям рядом со store (например, чтение текущего состояния через предоставленные API и конфигурационные ссылки, доступные middleware). Практические правила:

  • используйте ctx как преимущественно read-only инфраструктурный контекст;
  • не храните изменяемые shared-объекты в module scope;
  • стремитесь к детерминированному поведению (один и тот же вход -> один и тот же выход), кроме явно side-effect hook-ов.

5) Практические паттерны

Логирующий middleware

const logger: Middleware<RootState> = () => ({
  wrapAction:
    (name, next) =>
    async (...args) => {
      const t0 = performance.now();
      try {
        return await next(...args);
      } finally {
        console.log(`[action] ${name} took ${performance.now() - t0}ms`);
      }
    },
  afterUpdate: ({ type }) => {
    console.log("[update]", type);
  },
});

Middleware с runtime-инвариантами

const invariantMw: Middleware<RootState> = () => ({
  wrapSetState: (next) => async (updater) => {
    await next(updater);
    // Пример: проверить, что counter не уходит ниже нуля
    // (замените на ваши доменные проверки)
  },
});

Middleware для редактирования чувствительных данных в DevTools

const redactSecrets: Middleware<RootState> = () => ({
  devtoolsTransform: (event) => ({
    ...event,
    state: {
      ...event.state,
      auth: event.state?.auth
        ? { ...event.state.auth, token: "***redacted***" }
        : event.state?.auth,
    },
  }),
});

6) Частые ошибки

  • Забывают return await next(...) в wrapper — ломается цепочка возврата результата.
  • Случайно "глотают" ошибки в глобальном try/catch.
  • Делают тяжёлую синхронную работу в afterUpdate, что может приводить к фризам UI.
  • Мутируют входные объекты в transform вместо возврата новых значений.
  • Ожидают deep merge от mergeState, хотя это shallow merge по корню.

7) Рекомендации по производительности

  • Делайте wrappers тонкими; тяжёлую диагностику выносите в conditional/sampled логику.
  • Не сериализуйте большие snapshot state на каждый hook без необходимости.
  • Для дорогих диагностических middleware используйте feature flags, особенно в dev-only.
  • Перед глобальным включением тяжёлого middleware проверяйте нагрузку на production-подобном профиле.

8) Настройка для local store и global singleton

Middleware можно конфигурировать:

  • для конкретного store instance через initStore(..., { ... }) (где применимо в вашем setup);
  • глобально через configureGlobalStore({ middleware: [...] }) для singleton mode (hummbit/react).

В singleton mode вызывайте configureGlobalStore(...) на bootstrap до использования store/action.

Автор: Alexey TolkachevLinkedIn