Update recovery
The plain dispatcher delivers updates as they arrive,
but Telegram's update stream is stateful: each update carries a sequence number, and a
client that was briefly offline can miss updates or receive them out of order. Recovering
those gaps means tracking state and calling updates.getDifference /
updates.getChannelDifference at the right moments.
The updates
package — the "gap engine" — does this for you.
Wiring it up
The manager sits between the client and your dispatcher. It needs to both receive
updates (as the UpdateHandler) and observe RPC responses (as a middleware), so it
is installed in two places:
import (
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/updates"
updhook "github.com/gotd/td/telegram/updates/hook"
"github.com/gotd/td/tg"
)
d := tg.NewUpdateDispatcher()
gaps := updates.New(updates.Config{
Handler: d, // your dispatcher receives recovered, ordered updates
Logger: logzap.New(log.Named("gaps")),
})
client, err := telegram.ClientFromEnvironment(telegram.Options{
UpdateHandler: gaps,
Middlewares: []telegram.Middleware{
updhook.UpdateHook(gaps.Handle),
// Keep local pts in sync after self-initiated reads/deletes.
updhook.AffectedHook(gaps),
},
})
Self-initiated reads and deletes
UpdateHook forwards tg.UpdatesClass results to the manager, but some methods
return a different shape. messages.readHistory, messages.deleteMessages,
channels.deleteMessages, messages.readMentions and messages.deleteHistory
return messages.affectedMessages / messages.affectedHistory — results that
carry a pts increment the client must apply, yet are not updates and so never
reach the gap engine.
If you skip them, the server's pts advances while your local pts stays behind, so the next genuine update looks like a gap and is buffered. A common symptom: you read or delete a message in a handler, and a later edit in the same chat is then postponed until some unrelated pts-changing update arrives (gotd/td#1382).
updhook.AffectedHook
closes this gap. It inspects each RPC result, and when it sees an affected-pts
result it calls Manager.HandleAffected to advance the local pts (releasing any
updates postponed behind it), routing channel methods to the right channel
sequence and everything else to the common one. It is opt-in, mirrors TDLib's
behavior, and is a no-op before gaps.Run — so install it alongside UpdateHook
as shown above. See gotd/td#1782.
Running the engine
After authenticating, start the engine with your user ID. It blocks, keeping the client alive and processing updates until the context is cancelled:
return client.Run(ctx, func(ctx context.Context) error {
// ... authenticate ...
self, err := client.Self(ctx)
if err != nil {
return err
}
return gaps.Run(ctx, client.API(), self.ID, updates.AuthOptions{
IsBot: self.Bot,
OnStart: func(ctx context.Context) {
log.Info("recovery started")
},
})
})
Your dispatcher handlers (OnNewMessage, OnNewChannelMessage, …) are registered
exactly as before — the difference is they now receive a gap-free, correctly ordered
stream.
Persisting state
By default the engine keeps state in memory, so a restart re-syncs from scratch. For a
long-running service, give it a Storage (and access-hash storage) so it resumes from
where it left off. The
gotd/contrib module provides bbolt-backed
implementations:
gaps := updates.New(updates.Config{
Handler: d,
Storage: boltstor.NewStateStorage(boltdb),
})
The userbot example uses
persistent state; the updates
example is a minimal in-memory version.
The engine relies on the server for getDifference correctness and cannot recover from
ChannelDifferenceTooLong (it resyncs that channel instead). Stateless updates can't be
ordered. These are inherent to the MTProto update model.