telega/storage
Unified key-value storage contract shared by sessions and flows.
KeyValueStorage is the single low-level contract that every backend
implements (ETS in core; Postgres/SQLite/Redis as separate packages).
Values are opaque Strings — callers serialize to/from JSON themselves.
The two bridges below derive the higher-level SessionSettings and
FlowStorage contracts from a single KeyValueStorage, so a bot only
needs to wire up one backend for both sessions and flows.
Types
Backend-agnostic key-value store.
getreturnsNonefor a missing key.setstores a value with no expiration.set_with_ttlstores a value that expires afterttl_msmilliseconds. Backends without native TTL emulate it with lazy expiration on access.scanreturns every key beginning with the given prefix (live keys only).
pub type KeyValueStorage(error) {
KeyValueStorage(
get: fn(String) -> Result(option.Option(String), error),
set: fn(String, String) -> Result(Nil, error),
set_with_ttl: fn(String, String, Int) -> Result(Nil, error),
delete: fn(String) -> Result(Nil, error),
scan: fn(String) -> Result(List(String), error),
)
}
Constructors
-
KeyValueStorage( get: fn(String) -> Result(option.Option(String), error), set: fn(String, String) -> Result(Nil, error), set_with_ttl: fn(String, String, Int) -> Result(Nil, error), delete: fn(String) -> Result(Nil, error), scan: fn(String) -> Result(List(String), error), )
Values
pub fn dead_letters_from_storage(
storage storage: KeyValueStorage(error),
retention_ms retention_ms: option.Option(Int),
) -> dead_letter.DeadLetters
Derive a dead-letter queue from a KeyValueStorage.
Letters live under the dlq: prefix, alongside sessions (session:) and
flows (flow:), so one backend covers all three. The backend’s error type
is flattened to a String — a dead letter is written from the bot actor’s
crash path, where nothing matches on it.
retention_ms bounds how long a letter is kept. None keeps letters until
they are replayed or dropped, which is what you want while debugging and
not what you want unattended: a bot crashing in a loop writes one entry per
distinct update_id.
telega.with_dead_letters(
builder,
storage.dead_letters_from_storage(storage, retention_ms: Some(604_800_000)),
)
pub fn flow_storage_from_storage(
storage storage: KeyValueStorage(error),
) -> types.FlowStorage(error)
Derive FlowStorage from a KeyValueStorage.
Flow instances are stored under the flow: key namespace as complete JSON
(see instance.to_json), so subflows and parallel state survive restarts.
list_by_user is served by scan over the namespace, replacing the
secondary index used by the legacy ETS-only implementation.
pub fn flow_storage_from_storage_with_retention(
storage storage: KeyValueStorage(error),
retention_ms retention_ms: Int,
) -> types.FlowStorage(error)
Derive FlowStorage that lets the backend reclaim abandoned instances.
Every save renews the entry, so retention_ms is “how long an instance may
sit untouched before the backend drops it”. Redis expires it natively; the
ETS and SQL backends expire lazily on access and skip expired keys in
scan. Without this, a flow a user walked away from stays in storage
forever — nothing sweeps it.
Pick a retention comfortably longer than the flow’s builder.with_ttl:
once the entry is gone the instance simply looks absent, so on_timeout
will not fire for it and the user’s next message starts a fresh flow.
pub fn session_settings_from_storage(
storage storage: KeyValueStorage(error),
encode encode: fn(session) -> json.Json,
decode decoder: decode.Decoder(session),
default default: fn() -> session,
) -> bot.SessionSettings(session, error)
Derive SessionSettings from a KeyValueStorage.
Sessions are stored under the session: key namespace as JSON produced by
encode. A decode failure on load is treated as “no session” so the bot
falls back to default instead of crashing on a corrupt or migrated value —
but it is reported first (telega.storage.decode_error telemetry plus an
error log), because the next handler will persist that default over the
value that failed to decode.
pub fn session_settings_from_storage_versioned(
storage storage: KeyValueStorage(error),
encode encode: fn(session) -> json.Json,
decode decoder: decode.Decoder(session),
default default: fn() -> session,
version version: Int,
migrate migrate: fn(Int, dynamic.Dynamic) -> Result(
session,
Nil,
),
) -> bot.SessionSettings(session, error)
Derive SessionSettings that carry a schema version.
The stored value is wrapped in {"v": <version>, "d": <encoded session>},
so a build that changes the shape of its session can still read what an
older build wrote. On load:
- the envelope’s version matches → decode with
decode; - it does not → hand
migratethe stored version and the raw payload; - there is no envelope (the value was written by
session_settings_from_storage) → the version is0andmigrategets the whole value.
A migrate returning Error(Nil) is treated exactly like a value that
will not decode: reported, then read as “no session”, so the caller falls
back to default.
storage.session_settings_from_storage_versioned(
storage:,
encode: encode_session,
decode: session_decoder(),
default: fn() { Session(name: "", locale: "en") },
version: 2,
migrate: fn(from, raw) {
case from {
// v1 had no `locale`.
1 -> decode.run(raw, v1_decoder()) |> result.replace_error(Nil)
_ -> Error(Nil)
}
},
)