@yaebal/again
awaited auto-retry for 429 retry_after and transient 5xx errors.
install
pnpm add @yaebal/againusage
install autoRetry() on the bot. it attaches an api.onError hook; core
keeps the original promise alive, waits, and re-runs the same API call.
import { Bot } from "@yaebal/core";
import { autoRetry } from "@yaebal/again";
const bot = new Bot(process.env.BOT_TOKEN!)
.install(autoRetry({
maxRetries: 5,
retryAfterPaddingMs: 250,
onRetry: (event) => {
console.log(event.method, event.reason, event.delayMs);
},
}))
.on("message:text", (ctx) => ctx.reply("hello!"));
await bot.start();the direct API-hook form remains available when you are wiring a standalone Api instance instead of a bot:
// lower-level form, useful when you only have an Api instance
autoRetry(bot.api, { maxRetries: 5 });why this is cleaner
Telegram sends flood-wait data as response_parameters.retry_after. @yaebal/core now copies that object into TelegramError.parameters, so again no longer parses "retry after N" from the human-readable error text.
retry logic
429witherror.parameters.retry_afterwaits exactly that value, plus optional padding.429without structuredretry_afterfalls back to exponential backoff.5xxuses exponential backoff whenretryOnInternalis true.4xxclient errors are not retried.onRetryobserves every scheduled retry for logs and metrics.
import { decideRetry, type AutoRetryOptions } from "@yaebal/again";
import { TelegramError } from "@yaebal/core";
const opts: AutoRetryOptions = { maxRetries: 3, maxDelayMs: 30_000 };
const error = new TelegramError("sendMessage", 429, "Too Many Requests", { retry_after: 7 });
const action = decideRetry(error, 1, opts);
// => { retry: true, delayMs: 7000, reason: "retry_after", retryAfterMs: 7000 }api
| export | signature | description |
|---|---|---|
autoRetry | (options?: AutoRetryOptions) => BotPlugin(api: Api, options?: AutoRetryOptions) => void | creates an installable bot plugin or installs the retry hook on an Api directly |
decideRetry | (error, attempt, options?) => RetryDecision | undefined | pure retry-policy function for unit tests and custom policy checks |
againTestPack | (options?) => TestPack | wires auto-retry into @yaebal/test's mock API |
AutoRetryOptions
| field | type | default | description |
|---|---|---|---|
maxRetries | number | 3 | max retries after the first attempt |
maxDelayMs | number | 30000 | cap on one wait in milliseconds |
baseDelayMs | number | 1000 | base for exponential backoff |
retryAfterPaddingMs | number | 0 | extra safety delay added to Telegram-provided waits |
jitter | number | function | 0 | randomize delays or provide a custom delay transform |
retryOnInternal | boolean | true | also retry transient 5xx server errors |
onRetry | (event) => unknown | - | observe scheduled retries with method, params, reason and delay |
testing
@yaebal/again/test-pack installs the plugin on a TestEnv mock API, so tests
can simulate real Telegram errors with structured retry_after data.
import { againTestPack } from "@yaebal/again/test-pack";
import { apiError, createTestEnv } from "@yaebal/test";
const env = createTestEnv(bot, { packs: [againTestPack({ maxRetries: 2 })] });
env.onApi("sendMessage", apiError(429, "Too Many Requests", { retry_after: 0 }), { times: 1 });pairs with throttle. core runs every error hook before it decides whether to
retry, so
@yaebal/throttle can learn retry_after freezes even when again is the hook that requests the retry.