Skip to content

GramIO v0.5.0 — Рефакторинг Composer, наблюдаемость и суперсилы тестирования

16–18 февраля 2026

Большой инженерный цикл. Главное: gramio v0.5.0 окончательно избавляется от middleware-io и строится на @gramio/composer v0.2.0 — типобезопасной библиотеке композиции middleware с наблюдаемостью, условной регистрацией и статическим обогащением контекста. Параллельно @gramio/test доходит до v0.1.0 с реакциями, inline-запросами и fluent-скоупами, которые делают тесты читаемыми как пользовательские сценарии.

gramio v0.5.0 — Прощай, middleware-io

Ядро middleware переведено на @gramio/composer

Зависимость middleware-io удалена. GramIO теперь везде использует @gramio/composer — это означает лучшую дедупликацию, именованные middleware для читаемых стектрейсов и новый API наблюдаемости (inspect(), trace()).

Класс UpdateQueue из src/queue.ts также удалён — заменён на EventQueue из @gramio/composer.

Методы-хелперы переехали в Composer — плагины тоже могут их использовать

reaction(), callbackQuery(), chosenInlineResult(), inlineQuery(), hears(), command() и startParameter() переехали из Bot в Composer. Методы на Bot по-прежнему есть — они просто делегируют в композер. Зато теперь плагины и отдельные композеры тоже могут использовать эти хелперы напрямую:

ts
// Теперь работает внутри плагина, не только на Bot:
const myPlugin = new Plugin("my-plugin")
    .command("help", (ctx) => ctx.send("Помощь!"))
    .hears(/привет/i, (ctx) => ctx.send("Привет!"));

Перегрузки Bot.extend(composer) и Plugin.extend(composer)

Bot и Plugin теперь принимают экземпляры EventComposer в extend() с правильным выводом типов. Плагинные композеры переводятся в режим "scoped" для корректного шаринга контекста без дублирования middleware.

@gramio/types 9.4.1 и @gramio/composer 0.2.0

Обновлены зависимости: @gramio/types → 9.4.1, @gramio/composer → 0.2.0.


@gramio/composer v0.2.0 — Decorate, When, Inspect, Trace

Библиотека получила хороший спринт по фичам.

decorate() — статическое обогащение контекста без накладных расходов

derive() запускает функцию на каждый запрос. decorate() присваивает объект один раз при регистрации middleware и переиспользует ту же ссылку. Никаких вызовов функций в рантайме:

ts
const app = new Composer()
    .decorate({ db: myDbClient, config: appConfig })
    .use((ctx, next) => {
        ctx.db.query(/* ... */); // доступно на каждый запрос
        return next();
    });

Поддерживает ту же систему скоупов, что и derive() — передайте { as: "scoped" } или { as: "global" } для распространения в родительские композеры через extend().

when() — условная регистрация middleware на этапе запуска

Регистрируйте middleware только если условие истинно при старте. В отличие от рантаймового branch(), when() вычисляет условие один раз при загрузке. Свойства, добавленные внутри блока when(), типизируются как Partial (опциональные):

ts
const bot = new Bot(token)
    .when(process.env.NODE_ENV !== "production", (c) =>
        c.use(verboseLogger)
    )
    .when(config.features.auth, (c) =>
        c.derive(() => ({ user: getUser() }))
    )
    .on("message", (ctx) => {
        ctx.user; // тип: User | undefined (может не быть, если фича выключена)
    });

Вложенные when() работают. Ключи дедупликации, обработчики ошибок и определения ошибок из условного блока всё правильно пробрасываются.

inspect() — метаданные зарегистрированных middleware

Получите полную картину зарегистрированных middleware и их происхождения:

ts
const app = new Composer()
    .derive(function getUser() { return { user: "alice" }; })
    .guard(function isAdmin() { return true; })
    .use(async function handleRequest(_, next) { return next(); });

app.inspect();
// [
//   { index: 0, type: "derive", name: "getUser", scope: "local" },
//   { index: 1, type: "guard", name: "isAdmin", scope: "local" },
//   { index: 2, type: "use", name: "handleRequest", scope: "local" },
// ]

При расширении именованного плагина поле plugin показывает источник:

ts
const auth = new Composer({ name: "auth" })
    .derive(function getUser() { return { user: "alice" }; })
    .as("scoped");

new Composer().extend(auth).inspect();
// [{ index: 0, type: "derive", name: "getUser", scope: "local", plugin: "auth" }]

trace() — opt-in инструментация каждого middleware

Подключите OpenTelemetry, Datadog или любую трассировку одним коллбеком. Когда трассировка не используется — нулевые накладные расходы:

ts
const app = new Composer()
    .derive(function getUser() { /* ... */ })
    .use(handler)
    .trace((entry, ctx) => {
        const span = tracer.startSpan(`${entry.type}:${entry.name}`);
        return (error) => {
            if (error) span.recordException(error);
            span.end();
        };
    });

Коллбек очистки получает ошибку, если middleware упало, — после чего ошибка продолжает проброс в onError.

Конфиг methods для createComposer()

Авторы фреймворков могут инжектировать типизированные методы-хелперы прямо в прототип Composer через опцию methods. Именно так в GramIO теперь реализованы hears(), command(), reaction() и прочие:

ts
const { Composer } = createComposer({
    discriminator: (ctx) => ctx.updateType,
    types: eventTypes<{ message: MessageCtx }>(),
    methods: {
        hears(trigger: RegExp | string, handler: (ctx: MessageCtx) => unknown) {
            return this.on("message", (ctx, next) => {
                const text = ctx.text;
                if (typeof trigger === "string" ? text === trigger : trigger.test(text ?? ""))
                    return handler(ctx);
                return next();
            });
        },
    },
});

// Кастомные методы сохраняются через все цепочки:
bot.hears(/hello/, h1).on("message", h2).hears(/bye/, h3); // работает

Рантаймовая проверка конфликтов бросает ошибку, если имя метода совпадает со встроенным (on, use, derive и т.д.).

Именование функций для стектрейсов

Все функции-обёртки, создаваемые методами Composer, теперь получают осмысленные имена через Object.defineProperty. Формат: type:handlerName. Примеры: derive:getUser, guard:isAdmin, on:message. use() не переименовывает пользовательские функции — они сохраняют оригинальные имена в стектрейсах.

cleanErrorStack() чистит стектрейсы

Новая утилита cleanErrorStack() удаляет фреймы самой библиотеки из стектрейсов. Когда ваш обработчик падает — в стектрейсе виден ваш код, а не кишки фреймворка.

MaybeArray теперь принимает readonly-массивы

MaybeArray<T> изменился с T | T[] на T | readonly T[]. as const-массивы теперь работают без явных кастов.


@gramio/test v0.1.0 — Реакции, Inline-запросы и Fluent-скоупы

Библиотека тестирования вырастает из 0.0.x до 0.1.0 с большим расширением API.

user.react() — тестируйте обработчики реакций

Эмитируйте message_reaction апдейты в тестах. Работает с обработчиками bot.reaction():

ts
const msg = await user.sendMessage("Хороший бот!");

// Один эмодзи
await user.react("👍", msg);

// Несколько
await user.react(["👍", "❤"], msg);

// Смена реакции
await user.react("❤", msg, { oldReactions: ["👍"] });

Используйте ReactObject для более тонкого управления:

ts
await user.react(
    new ReactObject()
        .on(msg)          // привязка к сообщению (чат выводится автоматически)
        .add("👍", "🔥") // new_reaction
        .remove("😢")    // old_reaction
);

Автоматическое отслеживание реакций в MessageObject

MessageObject теперь ведёт Map реакций по пользователям. При вызове user.react() поле old_reaction вычисляется автоматически из памяти — не нужно объявлять его вручную:

ts
await user.react("👍", msg);  // добавляет 👍, old_reaction = []
await user.react("❤", msg);  // добавляет ❤, old_reaction = ["👍"] — вычислено автоматически!

user.sendInlineQuery() — тестируйте inline-режим

ts
const q = await user.sendInlineQuery("поиск кошек");
const q2 = await user.sendInlineQuery("поиск кошек", group); // chat_type: "group"
const q3 = await user.sendInlineQuery("поиск собак", { offset: "10" });

user.chooseInlineResult() — тестируйте chosen_inline_result

ts
await user.chooseInlineResult("result-1", "поиск кошек");
await user.chooseInlineResult("result-1", "поиск кошек", { inline_message_id: "abc" });

Fluent-скоупы — user.in(chat) и user.on(msg)

Два новых класса делают тесты читаемыми как пользовательские сценарии:

ts
// user.in(chat) — привязываем действия к чату
await user.in(group).sendMessage("Привет");
await user.in(group).sendInlineQuery("кошки");
await user.in(group).join();
await user.in(group).leave();

// user.on(msg) — привязываем действия к сообщению
await user.on(msg).react("👍");
await user.on(msg).click("action:1");

// Цепочки комбинируются:
await user.in(group).on(msg).react("🔥");
await user.in(group).on(msg).click("выбор:A");

Новые экспорты: ReactObject, InlineQueryObject, ChosenInlineResultObject, UserInChatScope, UserOnMessageScope.


@gramio/scenes — onInvalidInput для ask()

Метод ask() теперь принимает опцию onInvalidInput — кастомный обработчик, который срабатывает при ошибке валидации вместо автоматической отправки сообщения об ошибке:

ts
const scene = new Scene("registration")
    .ask(
        "age",
        z.coerce.number().min(18),
        "Сколько вам лет?",
        {
            onInvalidInput: async (context, error) => {
                await context.send(`❌ ${error.message}\nПопробуйте ещё раз.`);
            }
        }
    );

Также: users_shared добавлен в список событий, peer-зависимость обновлена до gramio >= 0.5.0, @gramio/storage bumped до ^2.0.0.


@gramio/storages — Типизированные ключи (v2.0.1)

Интерфейс Storage теперь использует дженерик Data с типизированными ключами для всех методов (get, set, has, delete). Все адаптеры сериализуют ключи через String() для обратной совместимости.

Версии:

  • @gramio/storage2.0.1
  • @gramio/storage-redis1.0.5
  • @gramio/storage-cloudflare0.0.2
  • @gramio/storage-sqlite0.0.2

Поддержка двух рантаймов в @gramio/storage-sqlite

Адаптер SQLite теперь полноценно работает на Bun и Node.js. Пайплайн prepublishOnly прогоняет проверку типов, сборку и тесты на обоих рантаймах перед публикацией. README обновлён с примерами установки и использования для каждой среды.


@gramio/session v0.2.0 — Ленивые сессии и модульная архитектура

Ленивая загрузка сессий

Сессии теперь загружаются только при первом обращении, а не при каждом входящем апдейте. Это может сократить количество обращений к базе данных на 50–90% для ботов, в которых большинство обработчиков не работают с сессией.

ts
bot.use(session({
    storage: redisStorage,
    lazy: true,
}));

При lazy: true вызов get к хранилищу откладывается до первого чтения ctx.session. Запись обратно в конце middleware-цепочки остаётся без изменений.

Рефакторинг на модульную архитектуру

Реализация из одного файла разбита на модули в src/lib/. Публичный API не изменился — это внутренняя реорганизация для улучшения поддерживаемости и изоляции тестов.

Полноценное покрытие тестами

Добавлен полный набор тестов, покрывающий чтение/запись сессии, очистку через ctx.session = null, пользовательские ключи сессий и поведение ленивой загрузки.