-module(glimit). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/glimit.gleam"). -export([new/0, per_second/2, per_second_fn/2, burst_limit/2, burst_limit_fn/2, on_limit_exceeded/2, identifier/2, build/1, apply_built/2, apply/2]). -export_type([rate_limiter/3, rate_limiter_builder/3]). -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 provides a distributed rate limiter that can be used to limit the\n" " number of requests or function calls per second for a given identifier.\n" "\n" " A single actor is used to assign one rate limiter actor per identifier. The\n" " rate limiter actor then uses a Token Bucket algorithm to determine if a\n" " request or function call should be allowed to proceed. A separate process is\n" " polling the rate limiters to remove full buckets to reduce unnecessary memory\n" " usage.\n" "\n" " The rate limits are configured using the following two options:\n" "\n" " - `per_second`: The rate of new available tokens per second. Think of this\n" " as the steady state rate limit.\n" " - `burst_limit`: The maximum number of available tokens. Think of this as\n" " the burst rate limit. The default value is the `per_second` rate limit.\n" "\n" " The rate limiter can be applied to a function or handler using the `apply`\n" " function, which returns a new function that checks the rate limit before\n" " calling the original function.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.burst_limit(100)\n" " |> glimit.identifier(fn(request) { request.ip })\n" " |> glimit.on_limit_exceeded(fn(_request) { \"Rate limit reached\" })\n" "\n" " let handler =\n" " fn(_request) { \"Hello, world!\" }\n" " |> glimit.apply(limiter)\n" " ```\n" "\n" ). -type rate_limiter(FVC, FVD, FVE) :: {rate_limiter, gleam@erlang@process:subject(glimit@registry:message(FVE)), fun((FVC) -> FVD), fun((FVC) -> FVE)}. -type rate_limiter_builder(FVF, FVG, FVH) :: {rate_limiter_builder, gleam@option:option(fun((FVH) -> integer())), gleam@option:option(fun((FVH) -> integer())), gleam@option:option(fun((FVF) -> FVH)), gleam@option:option(fun((FVF) -> FVG))}. -file("src/glimit.gleam", 67). ?DOC(" Create a new rate limiter builder.\n"). -spec new() -> rate_limiter_builder(any(), any(), any()). new() -> {rate_limiter_builder, none, none, none, none}. -file("src/glimit.gleam", 95). ?DOC( " Set the rate of new available tokens per second.\n" "\n" " Note that this is not the maximum number of requests that can be made in a single\n" " second, but the rate at which tokens are added to the bucket. Think of this as the\n" " steady state rate limit, while the `burst_limit` function sets the maximum number of\n" " available tokens (or the burst rate limit).\n" "\n" " This value is also used as the default value for the `burst_limit` function.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " ```\n" ). -spec per_second(rate_limiter_builder(FVO, FVP, FVQ), integer()) -> rate_limiter_builder(FVO, FVP, FVQ). per_second(Limiter, Limit) -> {rate_limiter_builder, {some, fun(_) -> Limit end}, erlang:element(3, Limiter), erlang:element(4, Limiter), erlang:element(5, Limiter)}. -file("src/glimit.gleam", 117). ?DOC( " Set the rate limit per second, based on the identifier.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.identifier(fn(request) { request.user_id })\n" " |> glimit.per_second_fn(fn(user_id) {\n" " db.get_rate_limit(user_id)\n" " })\n" " ```\n" ). -spec per_second_fn( rate_limiter_builder(FVX, FVY, FVZ), fun((FVZ) -> integer()) ) -> rate_limiter_builder(FVX, FVY, FVZ). per_second_fn(Limiter, Limit_fn) -> {rate_limiter_builder, {some, Limit_fn}, erlang:element(3, Limiter), erlang:element(4, Limiter), erlang:element(5, Limiter)}. -file("src/glimit.gleam", 140). ?DOC( " Set the maximum number of available tokens.\n" "\n" " The maximum number of available tokens is the maximum number of requests that can be\n" " made in a single second. The default value is the same as the rate limit per second.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.burst_limit(100)\n" " ```\n" ). -spec burst_limit(rate_limiter_builder(FWG, FWH, FWI), integer()) -> rate_limiter_builder(FWG, FWH, FWI). burst_limit(Limiter, Burst_limit) -> {rate_limiter_builder, erlang:element(2, Limiter), {some, fun(_) -> Burst_limit end}, erlang:element(4, Limiter), erlang:element(5, Limiter)}. -file("src/glimit.gleam", 163). ?DOC( " Set the maximum number of available tokens, based on the identifier.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.identifier(fn(request) { request.user_id })\n" " |> glimit.per_second(10)\n" " |> glimit.burst_limit_fn(fn(user_id) {\n" " db.get_burst_limit(user_id)\n" " })\n" " ```\n" ). -spec burst_limit_fn( rate_limiter_builder(FWP, FWQ, FWR), fun((FWR) -> integer()) ) -> rate_limiter_builder(FWP, FWQ, FWR). burst_limit_fn(Limiter, Burst_limit_fn) -> {rate_limiter_builder, erlang:element(2, Limiter), {some, Burst_limit_fn}, erlang:element(4, Limiter), erlang:element(5, Limiter)}. -file("src/glimit.gleam", 183). ?DOC( " Set the handler to be called when the rate limit is reached.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.on_limit_exceeded(fn(_request) { \"Rate limit reached\" })\n" " ```\n" ). -spec on_limit_exceeded(rate_limiter_builder(FWY, FWZ, FXA), fun((FWY) -> FWZ)) -> rate_limiter_builder(FWY, FWZ, FXA). on_limit_exceeded(Limiter, On_limit_exceeded) -> {rate_limiter_builder, erlang:element(2, Limiter), erlang:element(3, Limiter), erlang:element(4, Limiter), {some, On_limit_exceeded}}. -file("src/glimit.gleam", 202). ?DOC( " Set the identifier function to be used to identify the rate limit.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.identifier(fn(request) { request.ip })\n" " ```\n" ). -spec identifier(rate_limiter_builder(FXH, FXI, FXJ), fun((FXH) -> FXJ)) -> rate_limiter_builder(FXH, FXI, FXJ). identifier(Limiter, Identifier) -> {rate_limiter_builder, erlang:element(2, Limiter), erlang:element(3, Limiter), {some, Identifier}, erlang:element(5, Limiter)}. -file("src/glimit.gleam", 218). ?DOC( " Build the rate limiter.\n" "\n" " Note that using `apply` will already build the rate limiter, so this function is\n" " only useful if you want to build the rate limiter manually and apply it to multiple\n" " functions.\n" "\n" " To apply the resulting rate limiter to a function or handler, use the `apply_built`\n" " function.\n" ). -spec build(rate_limiter_builder(FXQ, FXR, FXS)) -> {ok, rate_limiter(FXQ, FXR, FXS)} | {error, binary()}. build(Config) -> gleam@result:'try'(case erlang:element(2, Config) of {some, Per_second} -> {ok, Per_second}; none -> {error, <<"`per_second` rate limit is required"/utf8>>} end, fun(Per_second@1) -> Burst_limit@1 = case erlang:element(3, Config) of {some, Burst_limit} -> Burst_limit; none -> Per_second@1 end, gleam@result:'try'( begin _pipe = glimit@registry:new(Per_second@1, Burst_limit@1), gleam@result:map_error( _pipe, fun(_) -> <<"Failed to start rate limiter registry"/utf8>> end ) end, fun(Rate_limiter_registry) -> gleam@result:'try'(case erlang:element(4, Config) of {some, Identifier} -> {ok, Identifier}; none -> {error, <<"`identifier` function is required"/utf8>>} end, fun(Identifier@1) -> gleam@result:'try'(case erlang:element(5, Config) of {some, On_limit_exceeded} -> {ok, On_limit_exceeded}; none -> {error, <<"`on_limit_exceeded` function is required"/utf8>>} end, fun(On_limit_exceeded@1) -> {ok, {rate_limiter, Rate_limiter_registry, On_limit_exceeded@1, Identifier@1}} end) end) end ) end). -file("src/glimit.gleam", 270). ?DOC( " Apply the rate limiter to a request handler or function.\n" "\n" " This function is useful if you want to build the rate limiter manually using the\n" " `build` function.\n" ). -spec apply_built(fun((FYH) -> FYI), rate_limiter(FYH, FYI, any())) -> fun((FYH) -> FYI). apply_built(Func, Limiter) -> fun(Input) -> Identifier = (erlang:element(4, Limiter))(Input), case begin _pipe = erlang:element(2, Limiter), glimit@registry:get_or_create(_pipe, Identifier) end of {ok, Rate_limiter} -> case begin _pipe@1 = Rate_limiter, glimit@rate_limiter:hit(_pipe@1) end of {ok, nil} -> Func(Input); {error, nil} -> (erlang:element(3, Limiter))(Input) end; {error, _} -> erlang:error(#{gleam_error => panic, message => <<"Failed to get rate limiter"/utf8>>, file => <>, module => <<"glimit"/utf8>>, function => <<"apply_built"/utf8>>, line => 283}) end end. -file("src/glimit.gleam", 254). ?DOC( " Apply the rate limiter to a request handler or function.\n" "\n" " Panics if the rate limiter registry cannot be started or if the `identifier`\n" " function or `on_limit_exceeded` function is missing.\n" ). -spec apply(fun((FYB) -> FYC), rate_limiter_builder(FYB, FYC, any())) -> fun((FYB) -> FYC). apply(Func, Config) -> Limiter@1 = case build(Config) of {ok, Limiter} -> Limiter; {error, Message} -> erlang:error(#{gleam_error => panic, message => Message, file => <>, module => <<"glimit"/utf8>>, function => <<"apply"/utf8>>, line => 260}) end, apply_built(Func, Limiter@1).