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 по-прежнему есть — они просто делегируют в композер. Зато теперь плагины и отдельные композеры тоже могут использовать эти хелперы напрямую:
// Теперь работает внутри плагина, не только на 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 и переиспользует ту же ссылку. Никаких вызовов функций в рантайме:
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 (опциональные):
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 и их происхождения:
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 показывает источник:
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 или любую трассировку одним коллбеком. Когда трассировка не используется — нулевые накладные расходы:
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() и прочие:
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():
const msg = await user.sendMessage("Хороший бот!");
// Один эмодзи
await user.react("👍", msg);
// Несколько
await user.react(["👍", "❤"], msg);
// Смена реакции
await user.react("❤", msg, { oldReactions: ["👍"] });Используйте ReactObject для более тонкого управления:
await user.react(
new ReactObject()
.on(msg) // привязка к сообщению (чат выводится автоматически)
.add("👍", "🔥") // new_reaction
.remove("😢") // old_reaction
);Автоматическое отслеживание реакций в MessageObject
MessageObject теперь ведёт Map реакций по пользователям. При вызове user.react() поле old_reaction вычисляется автоматически из памяти — не нужно объявлять его вручную:
await user.react("👍", msg); // добавляет 👍, old_reaction = []
await user.react("❤", msg); // добавляет ❤, old_reaction = ["👍"] — вычислено автоматически!user.sendInlineQuery() — тестируйте inline-режим
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
await user.chooseInlineResult("result-1", "поиск кошек");
await user.chooseInlineResult("result-1", "поиск кошек", { inline_message_id: "abc" });Fluent-скоупы — user.in(chat) и user.on(msg)
Два новых класса делают тесты читаемыми как пользовательские сценарии:
// 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 — кастомный обработчик, который срабатывает при ошибке валидации вместо автоматической отправки сообщения об ошибке:
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/storage→ 2.0.1@gramio/storage-redis→ 1.0.5@gramio/storage-cloudflare→ 0.0.2@gramio/storage-sqlite→ 0.0.2
Поддержка двух рантаймов в @gramio/storage-sqlite
Адаптер SQLite теперь полноценно работает на Bun и Node.js. Пайплайн prepublishOnly прогоняет проверку типов, сборку и тесты на обоих рантаймах перед публикацией. README обновлён с примерами установки и использования для каждой среды.
@gramio/session v0.2.0 — Ленивые сессии и модульная архитектура
Ленивая загрузка сессий
Сессии теперь загружаются только при первом обращении, а не при каждом входящем апдейте. Это может сократить количество обращений к базе данных на 50–90% для ботов, в которых большинство обработчиков не работают с сессией.
bot.use(session({
storage: redisStorage,
lazy: true,
}));При lazy: true вызов get к хранилищу откладывается до первого чтения ctx.session. Запись обратно в конце middleware-цепочки остаётся без изменений.
Рефакторинг на модульную архитектуру
Реализация из одного файла разбита на модули в src/lib/. Публичный API не изменился — это внутренняя реорганизация для улучшения поддерживаемости и изоляции тестов.
Полноценное покрытие тестами
Добавлен полный набор тестов, покрывающий чтение/запись сессии, очистку через ctx.session = null, пользовательские ключи сессий и поведение ленивой загрузки.