Skip to content
teleproto

Updates & Events

teleproto opens one MTProto socket and pushes every update over it. You subscribe with addEventHandler(callback, builder) and Telegram does the rest. This page is the mental model and the lifecycle — for end-to-end recipes see Events.

Pipeline#

One socket, one path. An incoming TL update is reconciled against the session's diff state, then matched against every registered EventBuilder filter, and only the ones whose filter accepts the update fire the callback. The callback receives a typed event — already parsed, already routed.

hello-handler.ts
import { NewMessage } from "teleproto/events";

client.addEventHandler((event) => {
  console.log(event.message.message);
}, new NewMessage({}));

That's the whole subscription model. No registration object, no decorator metadata, no router.

EventBuilder#

Every builder extends a common DefaultEventInterface chats, blacklistChats, and a free-form func(event) predicate — plus its own specifics. NewMessage adds incoming, outgoing, fromUsers, forwards, and a pattern regex.

filter.ts
import { NewMessage } from "teleproto/events";

// Long messages from a specific set of users, ignoring forwards.
client.addEventHandler(async (event) => {
  await event.message.reply({ message: "tl;dr please" });
}, new NewMessage({
  fromUsers: ["alice", "bob"],
  forwards: false,
  func: (e) => e.message.text.length > 100,
}));

The func callback runs after the structured filters, so it only sees events that passed everything else. Keep it cheap — it runs on every matching update.

sequentialUpdates#

A flag on TelegramClientParams that changes how the client handles concurrency. By default, handlers fire concurrently — update N+1 starts processing as soon as it arrives, regardless of whether update N has finished. Set sequentialUpdates: true and the client awaits each handler before reading the next update off the socket.

client.ts
const client = new TelegramClient(session, apiId, apiHash, {
  connectionRetries: 5,
  sequentialUpdates: true, // await each handler before the next update
});

Tradeoff: throughput versus ordering. Pick true if your handlers write to shared state and you don't want to think about races. Leave it default if you're building something high-volume where one slow handler shouldn't stall everything else.

catchUp#

A persistent session remembers pts and seq numbers — Telegram's cursor for "what you've already seen". After client.connect(), call client.catchUp() to ask Telegram to replay everything missed since the cursor.

catchup.ts
import { StoreSession } from "teleproto/sessions";

const client = new TelegramClient(
  new StoreSession("worker"),
  apiId,
  apiHash,
  { connectionRetries: 5 },
);

await client.connect();
await client.catchUp(); // replay missed updates since last persisted state

Especially useful when you're running with StoreSession and restarting on deploys — your handlers will fire for the messages that arrived while you were down. With MemorySession the cursor is gone on restart and catchUp has nothing to replay.

Removing handlers#

The inverse of addEventHandler. You need the same callback reference and the same builder you registered with. listEventHandlers() dumps everything currently wired up if you're debugging a leak.

lifecycle.ts
import { NewMessage } from "teleproto/events";

const event = new NewMessage({});
const handler = (e) => console.log(e.message.message);

client.addEventHandler(handler, event);

console.log(client.listEventHandlers()); // inspect what's wired up

client.removeEventHandler(handler, event);

No dispatcher, no middleware#

teleproto has flat addEventHandler and nothing else. There is no .use(middleware), no next(), no Dispatcher, no Conversation, no scenes/FSM. If your prior library shipped these, expect to write the equivalent yourself — usually in fewer lines than you'd think.

The classic case is "send a question, wait for the user's reply". Register a one-shot handler scoped to that chat, resolve a promise, tear it down:

wait-for-reply.ts
import { NewMessage } from "teleproto/events";

function waitFor(client, peerId, { timeout = 30_000 } = {}) {
  return new Promise((resolve, reject) => {
    const builder = new NewMessage({ chats: [peerId], incoming: true });
    const handler = (event) => {
      client.removeEventHandler(handler, builder);
      clearTimeout(timer);
      resolve(event.message);
    };
    const timer = setTimeout(() => {
      client.removeEventHandler(handler, builder);
      reject(new Error("timeout"));
    }, timeout);
    client.addEventHandler(handler, builder);
  });
}

await client.sendMessage(peerId, { message: "what's your name?" });
const reply = await waitFor(client, peerId);
console.log("got:", reply.message);