@yaebal/runner
concurrent update processing — per-chat order preserved, unrelated chats in parallel
install
pnpm add @yaebal/runnerusage
run(bot) replaces bot.start(). it polls getUpdates in batches
and dispatches to a bounded pool, so one slow handler no longer blocks the whole queue. it returns
a RunnerHandle whose stop() halts polling and waits for in-flight updates
to drain.
import { run } from "@yaebal/runner";
// instead of bot.start(): drive the bot concurrently
const handle = run(bot, { concurrency: 50 });
// later, drain in-flight work and stop:
process.once("SIGINT", () => handle.stop());per-chat ordering
updates that share a key run strictly in submit order and never overlap, so per-chat state
(sessions) stays race-free; unrelated chats run in parallel up to concurrency. the
default key comes from chatKey, which resolves the chat id from any update type and
falls back to the actor's user id (callback queries, inline queries, poll answers). pass your own sequentializeBy to change it, or return undefined to disable ordering.
import { run, chatKey } from "@yaebal/runner";
run(bot, {
concurrency: 100,
sequentializeBy: chatKey, // default — chat id, falling back to actor's user id
limit: 100, // getUpdates batch size
timeout: 30, // long-poll seconds
allowedUpdates: ["message"], // telegram allowed_updates
onError: (err, update) => log.error(update?.update_id, err),
});
// undefined disables ordering entirely (everything parallel):
run(bot, { sequentializeBy: () => undefined });the scheduler
the core is a reusable bounded-concurrency scheduler with per-key sequentialization, exported as createScheduler(concurrency) in case you want it directly — for outbound jobs,
migrations, anything that needs ordered-by-key parallelism with backpressure.
import { createScheduler } from "@yaebal/runner";
const s = createScheduler(8); // bound concurrency to 8
s.submit("chat-42", () => doWork()); // same key → strict order; different keys → parallel
s.submit(undefined, () => fireAndForget()); // null/undefined key → unordered
await s.whenBelow(4); // backpressure: wait until < 4 in flight
console.log(s.size()); // queued + running
await s.idle(); // resolves when everything drainsapi
| export | signature | description |
|---|---|---|
run | (bot: RunnerBot, options?: RunnerOptions) => RunnerHandle | drive the bot with concurrent polling |
createScheduler | (concurrency: number) => Scheduler | bounded-concurrency, per-key-ordered queue |
chatKey | (update: Update) => number | undefined | default sequentialization key (chat id → user id) |
RunnerOptions | interface | see below |
RunnerHandle | { stop(): Promise<void> } | stop polling, drain in-flight |
RunnerBot | interface | the bot surface run needs (api.getUpdates, handleUpdate) |
Scheduler | interface | submit / idle / whenBelow / size |
RunnerOptions
| field | type | default | description |
|---|---|---|---|
concurrency | number | 50 | max updates processed at once |
sequentializeBy | (update) => PropertyKey | undefined | chatKey | key whose updates stay ordered; undefined result = no ordering |
limit | number | 100 | getUpdates batch size |
timeout | number | 30 | long-poll timeout (seconds) |
allowedUpdates | string[] | — | restrict update types |
onError | (error, update?) => void | — | handler / polling error callback |
Scheduler
| method | signature | description |
|---|---|---|
submit | (key: PropertyKey | undefined, task: () => Promise<void>) => void | queue a task; tasks sharing a non-null key run in submit order |
idle | () => Promise<void> | resolves once nothing is queued or running |
whenBelow | (n: number) => Promise<void> | resolves once fewer than n tasks are queued/running (backpressure) |
size | () => number | tasks currently queued or running |