telega/store
Shared state that is not the session: data keyed by chat, by user, or by nothing at all.
A session belongs to one chat instance — one {chat_id}:{from_id} pair by
default — and is loaded once when that instance starts. That is exactly
wrong for state several instances share: a group counter every member
bumps, a chat-wide language, a global feature flag. Cached per instance it
would go stale the moment another member wrote it.
So a Store is not cached. Every read goes to the backend and every write
goes straight back, which is what makes concurrent readers correct. In
exchange each access costs a storage round-trip, so put per-user state
that only its own handlers touch in the session, and reach for a store for
the things the session cannot express.
// One counter per chat, shared by everyone in it.
let counters =
store.chat_data(
storage: ets_storage,
encode: json.int,
decode: decode.int,
default: fn() { 0 },
)
fn handle_message(ctx, _msg) {
use total <- result.try(
store.update(ctx, counters, fn(n) { n + 1 })
|> result.map_error(StorageError),
)
reply.with_text(ctx, "messages here: " <> int.to_string(total))
}
Stores are plain values: build one at startup, put it in dependencies
(or a module constant) and hand it to whichever handlers need it. Nothing
needs to be registered on the builder.
Read-modify-write is not atomic. update reads, applies your function
and writes; two chat instances doing that at the same time can lose one of
the increments. Where that matters, key the session by chat instead
(telega.with_session_key(bot.chat_session_key)) so every member’s update
is serialized through one process, and keep stores for state that is
written rarely or by one writer.
Types
Values
pub fn chat_data(
storage storage: storage.KeyValueStorage(error),
encode encode: fn(value) -> json.Json,
decode decoder: decode.Decoder(value),
default default: fn() -> value,
) -> Store(value, error)
One value per chat, keyed data:chat:{chat_id} — the group’s own state,
shared by every member.
pub fn custom(
key key_of: fn(update.Update) -> String,
storage storage: storage.KeyValueStorage(error),
encode encode: fn(value) -> json.Json,
decode decoder: decode.Decoder(value),
default default: fn() -> value,
) -> Store(value, error)
A store keyed by anything the update can answer — a forum topic, a business connection, the chat’s owner.
pub fn delete(
ctx: bot.Context(session, error_, dependencies),
store: Store(value, error),
) -> Result(Nil, error)
Forget the value for this update’s key. A later read returns the default.
pub fn delete_at(
store: Store(value, error),
key: String,
) -> Result(Nil, error)
Forget the value at an explicit key. See get_at.
pub fn get(
ctx: bot.Context(session, error_, dependencies),
store: Store(value, error),
) -> Result(value, error)
Read the value for this update’s key, or the store’s default when nothing is stored yet.
A stored value that will not decode is reported (error log plus
telega.storage.decode_error telemetry) and read as absent, the same rule
sessions follow — the next write overwrites it.
pub fn get_at(
store: Store(value, error),
key: String,
) -> Result(value, error)
Read the value stored at an explicit key.
The escape hatch for code with no Context to derive one from — a job, a
migration, an admin command reading another chat’s data. The key is the one
key would have produced, without the data: prefix.
pub fn global_data(
name name: String,
storage storage: storage.KeyValueStorage(error),
encode encode: fn(value) -> json.Json,
decode decoder: decode.Decoder(value),
default default: fn() -> value,
) -> Store(value, error)
One value for the whole bot, keyed data:global:{name} — a feature flag, a
counter of everything, the id of the current broadcast.
pub fn key(
ctx: bot.Context(session, error_, dependencies),
store: Store(value, error),
) -> String
The key this store uses for the update in ctx.
pub fn set(
ctx: bot.Context(session, error_, dependencies),
store: Store(value, error),
value: value,
) -> Result(Nil, error)
Write the value for this update’s key.
pub fn set_at(
store: Store(value, error),
key: String,
value: value,
) -> Result(Nil, error)
Write the value at an explicit key. See get_at.
pub fn update(
ctx: bot.Context(session, error_, dependencies),
store: Store(value, error),
change: fn(value) -> value,
) -> Result(value, error)
Read, apply change, write back, and return the written value.
Not atomic: two instances updating the same key at the same time can lose one of the changes (see the module docs).
pub fn update_at(
store: Store(value, error),
key: String,
change: fn(value) -> value,
) -> Result(value, error)
Read-modify-write at an explicit key. See get_at.
pub fn user_data(
storage storage: storage.KeyValueStorage(error),
encode encode: fn(value) -> json.Json,
decode decoder: decode.Decoder(value),
default default: fn() -> value,
) -> Store(value, error)
One value per user, keyed data:user:{from_id} — state that follows the
user from chat to chat.
Updates that carry no user (a poll update, a channel post) key as
user:-1; see docs/session-serialization.md.