-module(speedbump). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -define(FILEPATH, "src/speedbump.gleam"). -export([start/1, supervised/1, lazy_guard/4, ask/3, hits_per_second/1, hits_per_seconds/2, hits_per_minute/1, hits_per_minutes/2, hits_per_hour/1, hits_per_hours/2]). -export_type([limit/0, msg/0, 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. -type limit() :: {limit, integer(), integer(), integer(), binary()}. -opaque msg() :: {hit, gleam@erlang@process:subject({ok, nil} | {error, binary()})} | {ask, gleam@erlang@process:subject(integer()), integer()}. -opaque state() :: {state, list(limit()), integer()}. -file("src/speedbump.gleam", 32). ?DOC(" Check whether a limit has been properly configured.\n"). -spec is_valid(limit()) -> boolean(). is_valid(Limit) -> (((erlang:element(3, Limit) > 0) andalso (erlang:element(4, Limit) >= erlang:element( 3, Limit ))) andalso (erlang:element(2, Limit) > 0)) andalso (erlang:element(5, Limit) /= <<""/utf8>>). -file("src/speedbump.gleam", 40). -spec replenish_tokens(limit(), integer(), integer()) -> limit(). replenish_tokens(Limit, Last_hit_ns, Curr_time_ns) -> Tokens = begin _pipe = Curr_time_ns, _pipe@1 = gleam@int:subtract(_pipe, Last_hit_ns), _pipe@2 = gleam@int:divide(_pipe@1, erlang:element(2, Limit)), _pipe@3 = gleam@result:unwrap(_pipe@2, 0), _pipe@4 = gleam@int:add(_pipe@3, erlang:element(3, Limit)), gleam@int:min(_pipe@4, erlang:element(4, Limit)) end, _record = Limit, {limit, erlang:element(2, _record), Tokens, erlang:element(4, _record), erlang:element(5, _record)}. -file("src/speedbump.gleam", 65). -spec hits_per_ns(integer(), integer(), binary()) -> limit(). hits_per_ns(Hits, Ns, Description) -> Ns_per_token = case case Hits of 0 -> 0; Gleam@denominator -> Ns rem Gleam@denominator end of 0 -> case Hits of 0 -> 0; Gleam@denominator@1 -> Ns div Gleam@denominator@1 end; _ -> (case Hits of 0 -> 0; Gleam@denominator@2 -> Ns div Gleam@denominator@2 end) + 1 end, {limit, Ns_per_token, Hits, Hits, Description}. -file("src/speedbump.gleam", 177). -spec handle_msg(state(), msg()) -> gleam@otp@actor:next(state(), msg()). handle_msg(State, Msg) -> Curr_time_ns = ffi:nanosecond(), Updated_limits@1 = gleam@list:fold( erlang:element(2, State), [], fun(Updated_limits, Limit) -> _pipe = replenish_tokens( Limit, erlang:element(3, State), Curr_time_ns ), gleam@list:prepend(Updated_limits, _pipe) end ), State@1 = begin _record = State, {state, Updated_limits@1, erlang:element(3, _record)} end, case Msg of {hit, Reply_with} -> Res = gleam@list:try_fold( erlang:element(2, State@1), [], fun(Updated_limits@2, Limit@1) -> case erlang:element(3, Limit@1) of 0 -> {error, Limit@1}; X -> {ok, [begin _record@1 = Limit@1, {limit, erlang:element(2, _record@1), X - 1, erlang:element(4, _record@1), erlang:element(5, _record@1)} end | Updated_limits@2]} end end ), case Res of {error, Limit@2} -> gleam@erlang@process:send( Reply_with, {error, erlang:element(5, Limit@2)} ), gleam@otp@actor:continue(State@1); {ok, Updated_limits@3} -> gleam@erlang@process:send(Reply_with, {ok, nil}), gleam@otp@actor:continue( begin _record@2 = State@1, {state, Updated_limits@3, erlang:element(3, _record@2)} end ) end; {ask, Reply_with@1, N_requests} -> Wait = gleam@list:fold( erlang:element(2, State@1), 0, fun(Wait_remaining, Limit@3) -> Cost_of_limit = begin Not_free_requests = N_requests - erlang:element( 3, Limit@3 ), Partial_waiting_period_ns = ffi:nanosecond() - erlang:element( 3, State@1 ), case Not_free_requests of 0 -> 0; 1 -> Partial_waiting_period_ns; _ -> Partial_waiting_period_ns + (erlang:element( 2, Limit@3 ) * (Not_free_requests - 1)) end end, gleam@int:max(Wait_remaining, Cost_of_limit) end ), gleam@erlang@process:send(Reply_with@1, Wait), gleam@otp@actor:continue(State@1) end. -file("src/speedbump.gleam", 257). ?DOC(" Start a new rate limiter actor.\n"). -spec start(list(limit())) -> {ok, gleam@otp@actor:started(gleam@erlang@process:subject(msg()))} | {error, gleam@otp@actor:start_error()}. start(Limits) -> _pipe@5 = gleam@otp@actor:new_with_initialiser( 1000, fun(Subj) -> gleam@bool:guard( gleam@list:any(Limits, fun(Limit) -> not is_valid(Limit) end), {error, <<"invalid limit"/utf8>>}, fun() -> _pipe = {state, Limits, ffi:nanosecond()}, _pipe@1 = gleam@otp@actor:initialised(_pipe), _pipe@3 = gleam@otp@actor:selecting( _pipe@1, begin _pipe@2 = gleam_erlang_ffi:new_selector(), gleam@erlang@process:select(_pipe@2, Subj) end ), _pipe@4 = gleam@otp@actor:returning(_pipe@3, Subj), {ok, _pipe@4} end ) end ), _pipe@6 = gleam@otp@actor:on_message(_pipe@5, fun handle_msg/2), gleam@otp@actor:start(_pipe@6). -file("src/speedbump.gleam", 278). ?DOC(" Get a constructor for a rate limiter that can be used as part of a larger supervision tree.\n"). -spec supervised(list(limit())) -> gleam@otp@supervision:child_specification(gleam@erlang@process:subject(msg())). supervised(Limits) -> gleam@otp@supervision:worker(fun() -> start(Limits) end). -file("src/speedbump.gleam", 292). ?DOC( " This function allows the rate limiter to be used as a lazy guard.\n" " Example:\n" " ```gleam\n" " use <- speedbump.lazy_guard(limiter, fn(limit_description) {\n" " // ... Construct the appropriate error. `limit_description` is a text description of the limit that was violated.\n" " })\n" " // ... Continue with the function\n" " ```\n" ). -spec lazy_guard( gleam@otp@actor:started(gleam@erlang@process:subject(msg())), integer(), fun((binary()) -> FSE), fun(() -> FSE) ) -> FSE. lazy_guard(Rate_limiter, Timeout_ms, Or_else, Do) -> case gleam@erlang@process:call( erlang:element(3, Rate_limiter), Timeout_ms, fun(Field@0) -> {hit, Field@0} end ) of {error, Desc} -> Or_else(Desc); {ok, nil} -> Do() end. -file("src/speedbump.gleam", 309). ?DOC( " Ask the rate limiter how much time is left before you can make n requests.\n" " The response is in *nanoseconds*.\n" ). -spec ask( gleam@otp@actor:started(gleam@erlang@process:subject(msg())), integer(), integer() ) -> integer(). ask(Rate_limiter, Timeout_ms, N_requests) -> gleam@erlang@process:call( erlang:element(3, Rate_limiter), Timeout_ms, fun(_capture) -> {ask, _capture, N_requests} end ). -file("src/speedbump.gleam", 91). ?DOC(" Creates a limit of `hits` number of requests per second.\n"). -spec hits_per_second(integer()) -> limit(). hits_per_second(Hits) -> hits_per_ns( Hits, 1000000000, <<(erlang:integer_to_binary(Hits))/binary, " requests per second"/utf8>> ). -file("src/speedbump.gleam", 100). ?DOC(" Creates a limit of `hits` number of requests per `seconds` seconds.\n"). -spec hits_per_seconds(integer(), integer()) -> limit(). hits_per_seconds(Hits, Secs) -> hits_per_ns( Hits, 1000000000 * Secs, <<<<<<(erlang:integer_to_binary(Hits))/binary, " requests per "/utf8>>/binary, (erlang:integer_to_binary(Secs))/binary>>/binary, " seconds"/utf8>> ). -file("src/speedbump.gleam", 112). ?DOC(" Creates a limit of `hits` number of requests per minute.\n"). -spec hits_per_minute(integer()) -> limit(). hits_per_minute(Hits) -> hits_per_ns( Hits, 1000000000 * 60, <<(erlang:integer_to_binary(Hits))/binary, " requests per minute"/utf8>> ). -file("src/speedbump.gleam", 121). ?DOC(" Creates a limit of `hits` number of requests per `minutes` minutes.\n"). -spec hits_per_minutes(integer(), integer()) -> limit(). hits_per_minutes(Hits, Mins) -> hits_per_ns( Hits, (1000000000 * 60) * Mins, <<<<<<(erlang:integer_to_binary(Hits))/binary, " requests per "/utf8>>/binary, (erlang:integer_to_binary(Mins))/binary>>/binary, " minutes"/utf8>> ). -file("src/speedbump.gleam", 133). ?DOC(" Creates a limit of `hits` number of requests per hour.\n"). -spec hits_per_hour(integer()) -> limit(). hits_per_hour(Hits) -> hits_per_ns( Hits, (1000000000 * 60) * 60, <<(erlang:integer_to_binary(Hits))/binary, " requests per hour"/utf8>> ). -file("src/speedbump.gleam", 142). ?DOC(" Creates a limit of `hits` number of requests per `hours` hours.\n"). -spec hits_per_hours(integer(), integer()) -> limit(). hits_per_hours(Hits, Hrs) -> hits_per_ns( Hits, ((1000000000 * 60) * 60) * Hrs, <<<<<<(erlang:integer_to_binary(Hits))/binary, " requests per "/utf8>>/binary, (erlang:integer_to_binary(Hrs))/binary>>/binary, " hours"/utf8>> ).