-module(telega@storage@ets). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/telega/storage/ets.gleam"). -export([new/1]). -export_type([ets_table/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " ETS-backed `KeyValueStorage` with lazy TTL expiration.\n" "\n" " Values live in a public, named ETS table for the lifetime of the VM but do\n" " NOT survive a VM restart. TTL is enforced lazily: an expired entry is only\n" " removed when it is next accessed via `get` or `scan`. For persistence\n" " across restarts use a database-backed package (Postgres/SQLite/Redis).\n" ). -type ets_table() :: any(). -file("src/telega/storage/ets.gleam", 59). ?DOC(" An `expires_at` of `0` means \"never expires\".\n"). -spec is_live(integer()) -> boolean(). is_live(Expires_at) -> (Expires_at =:= 0) orelse (telega@internal@utils:current_time_ms() < Expires_at). -file("src/telega/storage/ets.gleam", 77). -spec do_scan(ets_table(), binary()) -> list(binary()). do_scan(Table, Prefix) -> _pipe = ets:tab2list(Table), gleam@list:filter_map( _pipe, fun(Entry) -> {Key, _, Expires_at} = Entry, case gleam_stdlib:string_starts_with(Key, Prefix) andalso is_live( Expires_at ) of true -> {ok, Key}; false -> {error, nil} end end ). -file("src/telega/storage/ets.gleam", 63). -spec do_get(ets_table(), binary()) -> gleam@option:option(binary()). do_get(Table, Key) -> case ets:lookup(Table, Key) of [] -> none; [{_, Value, Expires_at} | _] -> case is_live(Expires_at) of true -> {some, Value}; false -> ets:delete(Table, Key), none end end. -file("src/telega/storage/ets.gleam", 91). -spec is_undefined(gleam@dynamic:dynamic_()) -> boolean(). is_undefined(Value) -> case gleam_erlang_ffi:atom_from_string(<<"undefined"/utf8>>) of {ok, Undefined} -> Value =:= gleam_erlang_ffi:identity(Undefined); {error, _} -> false end. -file("src/telega/storage/ets.gleam", 46). -spec start_table(gleam@erlang@atom:atom_()) -> ets_table(). start_table(Name) -> case is_undefined(ets:whereis(Name)) of true -> ets:new( Name, [erlang:binary_to_atom(<<"set"/utf8>>), erlang:binary_to_atom(<<"public"/utf8>>), erlang:binary_to_atom(<<"named_table"/utf8>>)] ); false -> gleam_stdlib:identity(ets:whereis(Name)) end. -file("src/telega/storage/ets.gleam", 24). ?DOC( " Create an ETS-backed `KeyValueStorage`.\n" "\n" " `name` is the ETS table name; reusing the same name returns a handle to the\n" " existing table. The error type stays generic because none of the operations\n" " can fail, so the result composes with flows/sessions of any error type.\n" ). -spec new(binary()) -> {ok, telega@storage:key_value_storage(any())} | {error, telega@error:telega_error()}. new(Name) -> Table = start_table(erlang:binary_to_atom(Name)), {ok, {key_value_storage, fun(Key) -> {ok, do_get(Table, Key)} end, fun(Key@1, Value) -> ets:insert(Table, {Key@1, Value, 0}), {ok, nil} end, fun(Key@2, Value@1, Ttl_ms) -> ets:insert( Table, {Key@2, Value@1, telega@internal@utils:current_time_ms() + Ttl_ms} ), {ok, nil} end, fun(Key@3) -> ets:delete(Table, Key@3), {ok, nil} end, fun(Prefix) -> {ok, do_scan(Table, Prefix)} end}}.