-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, max_idle/2, store/2, on_limit_exceeded/2, identifier/2, build/1, apply_built/2, apply/2, get_count/1, remove/2, sweep/1, apply2/2, apply3/2, apply4/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 rate limiter that can be used to limit the number of\n" " requests or function calls per second for a given identifier.\n" "\n" " A single rate limiter actor stores all token bucket state. Each hit is a single\n" " message to the rate limiter, which performs the Token Bucket calculation inline.\n" " A periodic sweep removes full or idle buckets to reduce memory usage. The\n" " idle threshold defaults to 60 seconds and can be configured via `max_idle`.\n" " The rate limiter fails open — if the rate limiter actor is unavailable,\n" " requests are allowed through.\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" " # Multi-argument functions\n" "\n" " `apply` wraps a single-argument function `fn(a) -> b`. To rate-limit a\n" " function with multiple arguments, use `apply2`, `apply3`, or `apply4`:\n" "\n" " ```gleam\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.identifier(fn(args: #(String, String)) { args.0 })\n" " |> glimit.on_limit_exceeded(fn(_args) { too_many_requests() })\n" "\n" " let limited_handle =\n" " handle\n" " |> glimit.apply2(limiter)\n" "\n" " limited_handle(\"user_123\", \"upload\")\n" " ```\n" "\n" " # Pluggable store backend\n" "\n" " By default, rate limit state is stored in-memory using an OTP actor. For\n" " distributed rate limiting (e.g. across multiple nodes), you can provide a\n" " custom `Store` that persists bucket state externally (Redis, Postgres, etc.).\n" "\n" " All token bucket logic stays in glimit — adapters only implement simple\n" " get/set/lock/unlock operations. The `glimit/bucket` module is public and\n" " provides `to_pairs`/`from_pairs` helpers for serialization.\n" "\n" " ```gleam\n" " import glimit\n" " import glimit/bucket\n" "\n" " // Redis adapter example (using radish):\n" " let store = glimit.Store(\n" " get: fn(key) {\n" " case radish.execute(client, [\"HGETALL\", key], 1000) {\n" " Ok(fields) -> Ok(bucket.from_pairs(parse_hgetall_response(fields)))\n" " Error(_) -> Error(Nil)\n" " }\n" " },\n" " set: fn(key, state, ttl) {\n" " let pairs = bucket.to_pairs(state) |> list.flat_map(fn(p) { [p.0, p.1] })\n" " let _ = radish.execute(client, [\"HSET\", key, ..pairs], 1000)\n" " let _ = radish.execute(client, [\"EXPIRE\", key, int.to_string(ttl)], 1000)\n" " Ok(Nil)\n" " },\n" " lock: fn(key) {\n" " case radish.execute(client, [\"SET\", key <> \":lock\", \"1\", \"NX\", \"EX\", \"5\"], 1000) {\n" " Ok(_) -> Ok(Nil)\n" " Error(_) -> Error(Nil)\n" " }\n" " },\n" " unlock: fn(key) {\n" " let _ = radish.execute(client, [\"DEL\", key <> \":lock\"], 1000)\n" " Ok(Nil)\n" " },\n" " )\n" "\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.store(store)\n" " |> glimit.identifier(fn(req) { req.ip })\n" " |> glimit.on_limit_exceeded(fn(_) { \"Rate limited\" })\n" " |> glimit.apply(handler)\n" " ```\n" "\n" ). -type rate_limiter(GDJ, GDK, GDL) :: {rate_limiter, gleam@erlang@process:subject(glimit@rate_limiter:message(GDL)), fun((GDJ) -> GDK), fun((GDJ) -> GDL), gleam@option:option(glimit@memory_store:memory_store())}. -type rate_limiter_builder(GDM, GDN, GDO) :: {rate_limiter_builder, gleam@option:option(fun((GDO) -> integer())), gleam@option:option(fun((GDO) -> integer())), gleam@option:option(fun((GDM) -> GDO)), gleam@option:option(fun((GDM) -> GDN)), gleam@option:option(integer()), gleam@option:option(glimit@bucket:store())}. -file("src/glimit.gleam", 151). ?DOC(" Create a new rate limiter builder.\n"). -spec new() -> rate_limiter_builder(any(), any(), any()). new() -> {rate_limiter_builder, none, none, none, none, {some, 60000}, none}. -file("src/glimit.gleam", 181). ?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(GDV, GDW, GDX), integer()) -> rate_limiter_builder(GDV, GDW, GDX). per_second(Limiter, Limit) -> {rate_limiter_builder, {some, fun(_) -> Limit end}, erlang:element(3, Limiter), erlang:element(4, Limiter), erlang:element(5, Limiter), erlang:element(6, Limiter), erlang:element(7, Limiter)}. -file("src/glimit.gleam", 208). ?DOC( " Set the rate limit per second, based on the identifier.\n" "\n" " Note: this function is evaluated once when a bucket is first created for an\n" " identifier. If the function returns a different value later, existing buckets\n" " are not affected until they are swept (due to idleness or being full) and\n" " re-created on the next hit.\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(GEE, GEF, GEG), fun((GEG) -> integer()) ) -> rate_limiter_builder(GEE, GEF, GEG). per_second_fn(Limiter, Limit_fn) -> {rate_limiter_builder, {some, Limit_fn}, erlang:element(3, Limiter), erlang:element(4, Limiter), erlang:element(5, Limiter), erlang:element(6, Limiter), erlang:element(7, Limiter)}. -file("src/glimit.gleam", 232). ?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 burst when the bucket is full. The default value is the same as the\n" " 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(GEN, GEO, GEP), integer()) -> rate_limiter_builder(GEN, GEO, GEP). 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), erlang:element(6, Limiter), erlang:element(7, Limiter)}. -file("src/glimit.gleam", 260). ?DOC( " Set the maximum number of available tokens, based on the identifier.\n" "\n" " Note: this function is evaluated once when a bucket is first created for an\n" " identifier. If the function returns a different value later, existing buckets\n" " are not affected until they are swept (due to idleness or being full) and\n" " re-created on the next hit.\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(GEW, GEX, GEY), fun((GEY) -> integer()) ) -> rate_limiter_builder(GEW, GEX, GEY). 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), erlang:element(6, Limiter), erlang:element(7, Limiter)}. -file("src/glimit.gleam", 290). ?DOC( " Set the idle eviction threshold in seconds.\n" "\n" " Buckets that have not been hit for longer than this duration are removed\n" " during periodic sweeps. The default is 60 seconds. Set to `0` to disable\n" " idle eviction entirely.\n" "\n" " For rate limiters with a high `burst_limit` relative to `per_second`, you\n" " may want to increase this value so that partially-refilled buckets are not\n" " evicted prematurely. A good rule of thumb is\n" " `burst_limit / per_second` seconds.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(1)\n" " |> glimit.burst_limit(1000)\n" " |> glimit.max_idle(1000)\n" " ```\n" ). -spec max_idle(rate_limiter_builder(GFF, GFG, GFH), integer()) -> rate_limiter_builder(GFF, GFG, GFH). max_idle(Limiter, Seconds) -> case Seconds of S when S =< 0 -> {rate_limiter_builder, erlang:element(2, Limiter), erlang:element(3, Limiter), erlang:element(4, Limiter), erlang:element(5, Limiter), none, erlang:element(7, Limiter)}; S@1 -> {rate_limiter_builder, erlang:element(2, Limiter), erlang:element(3, Limiter), erlang:element(4, Limiter), erlang:element(5, Limiter), {some, S@1 * 1000}, erlang:element(7, Limiter)} end. -file("src/glimit.gleam", 319). ?DOC( " Set a pluggable store backend for distributed rate limiting.\n" "\n" " When a store is configured, bucket state is read from and written to the\n" " store on each hit instead of being kept in the actor's in-memory dictionary.\n" " The periodic sweep becomes a no-op since external stores handle expiry via TTL.\n" "\n" " # Example\n" "\n" " ```gleam\n" " import glimit\n" "\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.store(my_redis_store)\n" " |> glimit.identifier(fn(request) { request.ip })\n" " |> glimit.on_limit_exceeded(fn(_request) { \"Rate limit reached\" })\n" " ```\n" ). -spec store(rate_limiter_builder(GFO, GFP, GFQ), glimit@bucket:store()) -> rate_limiter_builder(GFO, GFP, GFQ). store(Limiter, Store) -> {rate_limiter_builder, erlang:element(2, Limiter), erlang:element(3, Limiter), erlang:element(4, Limiter), erlang:element(5, Limiter), erlang:element(6, Limiter), {some, Store}}. -file("src/glimit.gleam", 339). ?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(GFX, GFY, GFZ), fun((GFX) -> GFY)) -> rate_limiter_builder(GFX, GFY, GFZ). 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}, erlang:element(6, Limiter), erlang:element(7, Limiter)}. -file("src/glimit.gleam", 358). ?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(GGG, GGH, GGI), fun((GGG) -> GGI)) -> rate_limiter_builder(GGG, GGH, GGI). identifier(Limiter, Identifier) -> {rate_limiter_builder, erlang:element(2, Limiter), erlang:element(3, Limiter), {some, Identifier}, erlang:element(5, Limiter), erlang:element(6, Limiter), erlang:element(7, Limiter)}. -file("src/glimit.gleam", 374). ?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(GGP, GGQ, GGR)) -> {ok, rate_limiter(GGP, GGQ, GGR)} | {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'(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) -> gleam@result:'try'(case erlang:element(7, Config) of {some, S} -> {ok, {S, none}}; none -> case glimit@memory_store:new( erlang:element(6, Config), 10000 ) of {ok, {S@1, Handle}} -> {ok, {S@1, {some, Handle}}}; {error, _} -> {error, <<"Failed to start memory store"/utf8>>} end end, fun(_use0) -> {Resolved_store, Mem_store} = _use0, gleam@result:'try'( begin _pipe = glimit@rate_limiter:new( Per_second@1, Burst_limit@1, Resolved_store ), gleam@result:map_error( _pipe, fun(_) -> <<"Failed to start rate limiter"/utf8>> end ) end, fun(Rate_limiter_actor) -> {ok, {rate_limiter, Rate_limiter_actor, On_limit_exceeded@1, Identifier@1, Mem_store}} end ) end) end) end) end). -file("src/glimit.gleam", 439). ?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((GHG) -> GHH), rate_limiter(GHG, GHH, any())) -> fun((GHG) -> GHH). apply_built(Func, Limiter) -> fun(Input) -> Identifier = (erlang:element(4, Limiter))(Input), case glimit@rate_limiter:hit(erlang:element(2, Limiter), Identifier) of {ok, nil} -> Func(Input); {error, rate_limited} -> (erlang:element(3, Limiter))(Input); {error, unavailable} -> Func(Input); {error, store_lock_failed} -> Func(Input) end end. -file("src/glimit.gleam", 423). ?DOC( " Apply the rate limiter to a request handler or function.\n" "\n" " Panics if the rate limiter cannot be started or if the `identifier`\n" " function or `on_limit_exceeded` function is missing.\n" ). -spec apply(fun((GHA) -> GHB), rate_limiter_builder(GHA, GHB, any())) -> fun((GHA) -> GHB). 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 => 429}) end, apply_built(Func, Limiter@1). -file("src/glimit.gleam", 458). ?DOC( " Return the number of tracked identifiers in the in-memory store.\n" "\n" " Returns 0 if the rate limiter uses an external store.\n" ). -spec get_count(rate_limiter(any(), any(), any())) -> integer(). get_count(Limiter) -> case erlang:element(5, Limiter) of {some, Ms} -> glimit@memory_store:get_count(Ms); none -> 0 end. -file("src/glimit.gleam", 469). ?DOC( " Remove an identifier from the in-memory store.\n" "\n" " No-op if the rate limiter uses an external store.\n" ). -spec remove(rate_limiter(any(), any(), GHU), GHU) -> nil. remove(Limiter, Identifier) -> case erlang:element(5, Limiter) of {some, Ms} -> Key = <<"glimit:"/utf8, (gleam@string:inspect(Identifier))/binary>>, _ = glimit@memory_store:remove(Ms, Key), nil; none -> nil end. -file("src/glimit.gleam", 484). ?DOC( " Remove full or idle buckets from the in-memory store synchronously.\n" "\n" " No-op if the rate limiter uses an external store.\n" ). -spec sweep(rate_limiter(any(), any(), any())) -> nil. sweep(Limiter) -> case erlang:element(5, Limiter) of {some, _} -> nil; none -> nil end. -file("src/glimit.gleam", 511). ?DOC( " Apply the rate limiter to a 2-argument function.\n" "\n" " The config's `identifier` and `on_limit_exceeded` receive a `#(a, b)` tuple.\n" "\n" " # Example\n" "\n" " ```gleam\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.identifier(fn(args: #(String, String)) { args.0 })\n" " |> glimit.on_limit_exceeded(fn(_) { \"Rate limited\" })\n" "\n" " let limited =\n" " handle\n" " |> glimit.apply2(limiter)\n" "\n" " limited(\"user_123\", \"upload\")\n" " ```\n" ). -spec apply2( fun((GIE, GIF) -> GIG), rate_limiter_builder({GIE, GIF}, GIG, any()) ) -> fun((GIE, GIF) -> GIG). apply2(Func, Config) -> Wrapped = begin _pipe = fun(Args) -> Func(erlang:element(1, Args), erlang:element(2, Args)) end, apply(_pipe, Config) end, fun(A, B) -> Wrapped({A, B}) end. -file("src/glimit.gleam", 541). ?DOC( " Apply the rate limiter to a 3-argument function.\n" "\n" " The config's `identifier` and `on_limit_exceeded` receive a `#(a, b, c)` tuple.\n" "\n" " # Example\n" "\n" " ```gleam\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.identifier(fn(args: #(String, String, Int)) { args.0 })\n" " |> glimit.on_limit_exceeded(fn(_) { \"Rate limited\" })\n" "\n" " let limited =\n" " handle\n" " |> glimit.apply3(limiter)\n" "\n" " limited(\"user_123\", \"upload\", 42)\n" " ```\n" ). -spec apply3( fun((GIL, GIM, GIN) -> GIO), rate_limiter_builder({GIL, GIM, GIN}, GIO, any()) ) -> fun((GIL, GIM, GIN) -> GIO). apply3(Func, Config) -> Wrapped = begin _pipe = fun(Args) -> Func( erlang:element(1, Args), erlang:element(2, Args), erlang:element(3, Args) ) end, apply(_pipe, Config) end, fun(A, B, C) -> Wrapped({A, B, C}) end. -file("src/glimit.gleam", 571). ?DOC( " Apply the rate limiter to a 4-argument function.\n" "\n" " The config's `identifier` and `on_limit_exceeded` receive a `#(a, b, c, d)` tuple.\n" "\n" " # Example\n" "\n" " ```gleam\n" " let limiter =\n" " glimit.new()\n" " |> glimit.per_second(10)\n" " |> glimit.identifier(fn(args: #(String, String, Int, Bool)) { args.0 })\n" " |> glimit.on_limit_exceeded(fn(_) { \"Rate limited\" })\n" "\n" " let limited =\n" " handle\n" " |> glimit.apply4(limiter)\n" "\n" " limited(\"user_123\", \"upload\", 42, True)\n" " ```\n" ). -spec apply4( fun((GIT, GIU, GIV, GIW) -> GIX), rate_limiter_builder({GIT, GIU, GIV, GIW}, GIX, any()) ) -> fun((GIT, GIU, GIV, GIW) -> GIX). apply4(Func, Config) -> Wrapped = begin _pipe = fun(Args) -> Func( erlang:element(1, Args), erlang:element(2, Args), erlang:element(3, Args), erlang:element(4, Args) ) end, apply(_pipe, Config) end, fun(A, B, C, D) -> Wrapped({A, B, C, D}) end.