Current section
Files
Jump to
Current section
Files
src/glimit@bucket.erl
-module(glimit@bucket).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glimit/bucket.gleam").
-export([new/2, refill/2, hit/2, to_pairs/1, from_pairs/1, compute_ttl/1, is_full/2]).
-export_type([store/0, bucket_state/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(
" This module contains pure token-bucket functions used by the rate limiter.\n"
"\n"
).
-type store() :: {store,
fun((binary()) -> {ok, gleam@option:option(bucket_state())} |
{error, nil}),
fun((binary(), bucket_state(), integer()) -> {ok, nil} | {error, nil}),
fun((binary()) -> {ok, nil} | {error, nil})}.
-type bucket_state() :: {bucket_state,
integer(),
integer(),
float(),
gleam@option:option(integer())}.
-file("src/glimit/bucket.gleam", 66).
?DOC(
" Create a new bucket state.\n"
"\n"
" Returns Error(Nil) if max_token_count or token_rate are not positive.\n"
).
-spec new(integer(), integer()) -> {ok, bucket_state()} | {error, nil}.
new(Max_token_count, Token_rate) ->
case (Max_token_count > 0) andalso (Token_rate > 0) of
false ->
{error, nil};
true ->
{ok,
{bucket_state,
Max_token_count,
Token_rate,
erlang:float(Max_token_count),
none}}
end.
-file("src/glimit/bucket.gleam", 81).
?DOC(" Refill the bucket based on elapsed time.\n").
-spec refill(bucket_state(), integer()) -> bucket_state().
refill(State, Now) ->
Time_diff = case erlang:element(5, State) of
none ->
0;
{some, Last_update} ->
gleam@int:max(0, Now - Last_update)
end,
Tokens_to_add = erlang:float(erlang:element(3, State) * Time_diff) / 1000.0,
Token_count = begin
_pipe = (erlang:element(4, State) + Tokens_to_add),
_pipe@1 = gleam@float:min(_pipe, erlang:float(erlang:element(2, State))),
gleam@float:max(_pipe@1, +0.0)
end,
Last_update@1 = case Time_diff > 0 of
true ->
{some, Now};
false ->
case erlang:element(5, State) of
none ->
{some, Now};
{some, _} ->
erlang:element(5, State)
end
end,
{bucket_state,
erlang:element(2, State),
erlang:element(3, State),
Token_count,
Last_update@1}.
-file("src/glimit/bucket.gleam", 108).
?DOC(
" Hit the bucket: refill, then try to consume one token.\n"
"\n"
" Returns `#(Ok(Nil), new_state)` on success,\n"
" `#(Error(Nil), new_state)` when rate-limited.\n"
).
-spec hit(bucket_state(), integer()) -> {{ok, nil} | {error, nil},
bucket_state()}.
hit(State, Now) ->
State@1 = refill(State, Now),
case erlang:element(4, State@1) >= 1.0 of
true ->
{{ok, nil},
{bucket_state,
erlang:element(2, State@1),
erlang:element(3, State@1),
erlang:element(4, State@1) - 1.0,
erlang:element(5, State@1)}};
false ->
{{error, nil}, State@1}
end.
-file("src/glimit/bucket.gleam", 124).
?DOC(
" Convert a BucketState to a list of string key-value pairs.\n"
"\n"
" Useful for serializing bucket state into external stores (e.g. Redis HSET).\n"
" Keys: `\"tc\"` (token count), `\"lu\"` (last update), `\"mt\"` (max tokens), `\"tr\"` (token rate).\n"
).
-spec to_pairs(bucket_state()) -> list({binary(), binary()}).
to_pairs(State) ->
Lu = case erlang:element(5, State) of
{some, V} ->
erlang:integer_to_binary(V);
none ->
<<""/utf8>>
end,
[{<<"tc"/utf8>>, gleam_stdlib:float_to_string(erlang:element(4, State))},
{<<"lu"/utf8>>, Lu},
{<<"mt"/utf8>>, erlang:integer_to_binary(erlang:element(2, State))},
{<<"tr"/utf8>>, erlang:integer_to_binary(erlang:element(3, State))}].
-file("src/glimit/bucket.gleam", 142).
?DOC(
" Parse a BucketState from a list of string key-value pairs.\n"
"\n"
" This is the inverse of `to_pairs`. Returns `Error(Nil)` if any required\n"
" field is missing or cannot be parsed.\n"
).
-spec from_pairs(list({binary(), binary()})) -> {ok, bucket_state()} |
{error, nil}.
from_pairs(Pairs) ->
gleam@result:'try'(
gleam@list:key_find(Pairs, <<"tc"/utf8>>),
fun(Tc_str) ->
gleam@result:'try'(
gleam@list:key_find(Pairs, <<"lu"/utf8>>),
fun(Lu_str) ->
gleam@result:'try'(
gleam@list:key_find(Pairs, <<"mt"/utf8>>),
fun(Mt_str) ->
gleam@result:'try'(
gleam@list:key_find(Pairs, <<"tr"/utf8>>),
fun(Tr_str) ->
gleam@result:'try'(
gleam_stdlib:parse_float(Tc_str),
fun(Tc) ->
gleam@result:'try'(
gleam_stdlib:parse_int(Mt_str),
fun(Mt) ->
gleam@result:'try'(
gleam_stdlib:parse_int(
Tr_str
),
fun(Tr) ->
Lu = case Lu_str of
<<""/utf8>> ->
none;
S ->
case gleam_stdlib:parse_int(
S
) of
{ok, V} ->
{some,
V};
{error,
_} ->
none
end
end,
{ok,
{bucket_state,
Mt,
Tr,
Tc,
Lu}}
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/glimit/bucket.gleam", 171).
?DOC(
" Compute a TTL in seconds for the bucket state.\n"
"\n"
" The TTL is based on the time it takes to fully refill from empty, with a\n"
" minimum of 60 seconds.\n"
).
-spec compute_ttl(bucket_state()) -> integer().
compute_ttl(B) ->
case erlang:element(3, B) > 0 of
true ->
Refill_seconds = case erlang:element(3, B) of
0 -> 0;
Gleam@denominator -> ((erlang:element(2, B) + erlang:element(
3,
B
))
- 1)
div Gleam@denominator
end,
gleam@int:max(Refill_seconds * 2, 60);
false ->
60
end.
-file("src/glimit/bucket.gleam", 184).
?DOC(" Returns True if the bucket is full after refilling.\n").
-spec is_full(bucket_state(), integer()) -> boolean().
is_full(State, Now) ->
State@1 = refill(State, Now),
erlang:element(4, State@1) >= erlang:float(erlang:element(2, State@1)).