telega/scope
Per-update scratch space carried by Context.
Some things a handler does have to be visible to code that never receives
the handler’s return value: the dialog engine has to know whether the
user’s on_action already answered the callback query, an API transformer
deep inside the client has to know whether this particular call opted out
of the webhook-reply claim, an i18n middleware has to hand the resolved
locale to t(ctx, ...) calls nested anywhere below it.
A Scope is the explicit place for exactly that. It is created once per
update, travels in ctx.scope (so every copy of the context — including
Context(..ctx, session:) — shares it), and is dropped when the update is
handled. Slots live in the process dictionary of the chat instance under a
namespace unique to the scope, which is what makes a leftover flag from an
earlier update unreadable rather than merely unlikely.
A slot is named by a typed Key, so a read and a write agree on
the value’s type by construction:
const locale_key: scope.Key(String) = scope.Key("my_bot/locale")
pub fn set_locale(ctx: Context(s, e, d), locale: String) -> Nil {
scope.put(ctx.scope, locale_key, locale)
}
pub fn locale(ctx: Context(s, e, d)) -> String {
scope.get(ctx.scope, locale_key) |> result.unwrap("en")
}
Name keys after the module that owns them ("my_bot/locale", not
"locale"): a scope is one flat namespace shared by the library, your
bot, and any middleware, and one name used at two different types is the
single way to get a value back that is not the one you stored.
A scope is not a place for application state: the session is per-user
persisted state, telega/store is chat/user/global state, dependencies
are services. It holds only what is true of the update being handled, and
only for as long as that update is.
A scope belongs to the process handling the update. Reading it from a process you spawned yourself finds nothing rather than what the spawning handler put there.
Types
The name of one slot, together with the type of what it holds.
Build it as a constant next to the code that owns the slot, so nothing else can name it at another type:
const answered_key: scope.Key(String) = scope.Key("dialog/answered")
pub type Key(value) {
Key(name: String)
}
Constructors
-
Key(name: String)
Values
pub fn clear(scope scope: Scope) -> Nil
Empty the whole scope. The runtime calls this once the update is handled, which is what keeps a long-lived chat instance’s process dictionary from growing one namespace per update. A cleared scope is still usable.
pub fn get(
scope scope: Scope,
key key: Key(value),
) -> Result(value, Nil)
Read key’s slot back. Error(Nil) when this scope never filled it.
pub fn has(scope scope: Scope, key key: Key(value)) -> Bool
Whether this scope has filled key’s slot.
pub fn new() -> Scope
A fresh scope, sharing slots with no other scope alive or dead.
The runtime creates one per update; call this yourself only when you build
a Context by hand.