defmodule CCXT.RateLimiter.State do @moduledoc """ ETS-backed store for rate limit status across exchanges. Stores the latest `CCXT.RateLimiter.Info` from each exchange response, keyed by `{exchange_id, api_key | :public}`. This allows consumers to query current rate limit pressure at any time. ## Architecture A GenServer owns the ETS table (OTP-idiomatic -- handles restarts cleanly). The table is `:public` with `read_concurrency: true` so any process can read without going through the GenServer. ## Usage case CCXT.RateLimiter.State.status("binance") do %Info{remaining: remaining} -> remaining nil -> :unknown end """ use GenServer alias CCXT.RateLimiter.Info @table :ccxt_rate_limit_state @doc "Starts the State GenServer." @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @doc """ Updates the rate limit info for a given key. Key is `{exchange_id, api_key | :public}`. """ @spec update({String.t(), term()}, Info.t()) :: :ok def update(key, %Info{} = info) do :ets.insert(@table, {key, info, System.monotonic_time()}) :ok end @doc """ Returns the latest rate limit info for an exchange's public endpoints. """ @spec status(String.t()) :: Info.t() | nil def status(exchange_id) when is_binary(exchange_id) do status(exchange_id, :public) end @doc """ Returns the latest rate limit info for a specific exchange + credential key. """ @spec status(String.t(), term()) :: Info.t() | nil def status(exchange_id, credential_key) when is_binary(exchange_id) do case :ets.lookup(@table, {exchange_id, credential_key}) do [{_key, info, _timestamp}] -> info [] -> nil end end @doc """ Returns all rate limit entries for an exchange. """ @spec all(String.t()) :: [Info.t()] def all(exchange_id) when is_binary(exchange_id) do # Match all keys starting with {exchange_id, _} match_spec = [{{{exchange_id, :_}, :"$1", :_}, [], [:"$1"]}] :ets.select(@table, match_spec) end # ============================================================================= # GenServer Callbacks # ============================================================================= @impl true def init(_opts) do table = :ets.new(@table, [:named_table, :set, :public, read_concurrency: true]) {:ok, %{table: table}} end end