@yaebal/again

awaited auto-retry for 429 retry_after and transient 5xx errors.

install

terminal
pnpm add @yaebal/again

usage

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.

bot.ts
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:

api.ts
// 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

  • 429 with error.parameters.retry_after waits exactly that value, plus optional padding.
  • 429 without structured retry_after falls back to exponential backoff.
  • 5xx uses exponential backoff when retryOnInternal is true.
  • 4xx client errors are not retried.
  • onRetry observes every scheduled retry for logs and metrics.
policy.ts
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

exportsignaturedescription
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 | undefinedpure retry-policy function for unit tests and custom policy checks
againTestPack(options?) => TestPackwires auto-retry into @yaebal/test's mock API

AutoRetryOptions

fieldtypedefaultdescription
maxRetriesnumber3max retries after the first attempt
maxDelayMsnumber30000cap on one wait in milliseconds
baseDelayMsnumber1000base for exponential backoff
retryAfterPaddingMsnumber0extra safety delay added to Telegram-provided waits
jitternumber | function0randomize delays or provide a custom delay transform
retryOnInternalbooleantruealso 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.

again.test.ts
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.