# `Bourse.RateLimiter`
[🔗](https://github.com/ZenHive/bourse/blob/main/lib/bourse/rate_limiter.ex#L1)

Per-credential weighted rate limiter for exchange API requests.

Tracks request costs per `{exchange_id, credential_key, bucket_axis}` using a
sliding window. Costs are summed (not counted) to handle weighted endpoints
correctly.

## Usage

    # Authenticated request (per-API-key tracking)
    key = {"binance", api_key, "ip"}
    case Bourse.RateLimiter.check_rate(key, %{requests: 1200, period: 60_000}, 4) do
      :ok -> make_request()
      {:delay, ms} -> Process.sleep(ms); make_request()
    end

    # Or use wait_for_capacity which blocks until ready:
    :ok = Bourse.RateLimiter.wait_for_capacity(key, rate_limit, cost)
    make_request()

## Credential Keys

The key is a tuple `{exchange_id, credential_key, bucket_axis}` where:
- `exchange_id` is the exchange string ID (`"binance"`, `"bybit"`, etc.)
- `credential_key` is either:
  - The API key string (for authenticated requests) -- isolates per-user limits
  - `:public` atom (for public requests) -- shared pool for unauthenticated requests
- `bucket_axis` is the spec/header bucket axis (`"request"`, `"ip"`,
  `"uid"`, `"order_weight"`, etc.)

# `bucket_check`

```elixir
@type bucket_check() :: {key(), rate_limit() | nil, number()}
```

A single bucket capacity check: `{key, rate_limit, cost}`.

# `key`

```elixir
@type key() ::
  {String.t(), String.t() | :public}
  | {String.t(), String.t() | :public, String.t()}
```

Rate limiter key: `{exchange_id, api_key | :public, bucket_axis}`.

# `rate_limit`

```elixir
@type rate_limit() :: %{requests: pos_integer(), period: pos_integer()}
```

Rate limit configuration with max weight and period in milliseconds

# `check_rate`

```elixir
@spec check_rate(key(), rate_limit() | nil, number(), GenServer.name()) ::
  :ok | {:delay, pos_integer()}
```

Checks if a request can be made within rate limits.

Returns `:ok` if within limits (and records the request), or `{:delay, milliseconds}`
if the caller should wait before making the request.

## Parameters

- `key` -- `{exchange_id, api_key}` or `{exchange_id, :public}` tuple
- `rate_limit` -- `%{requests: max_weight, period: period_ms}` or nil (no limiting)
- `cost` -- Request weight/cost (default: 1)
- `name` -- GenServer name (default: `Bourse.RateLimiter`)

# `check_rates`

```elixir
@spec check_rates([bucket_check()], GenServer.name()) :: :ok | {:delay, pos_integer()}
```

Checks multiple bucket capacities atomically.

Returns `:ok` only when every bucket has capacity, recording every cost in the
same GenServer transition. Returns `{:delay, milliseconds}` without recording
any bucket when at least one bucket is over limit.

# `child_spec`

```elixir
@spec child_spec(keyword()) :: Supervisor.child_spec()
```

Returns a child specification for starting the rate limiter under a supervisor.

# `get_cost`

```elixir
@spec get_cost(key(), pos_integer(), GenServer.name()) :: number()
```

Gets current total cost for a key within a time window.

Useful for debugging and monitoring.

# `record_request`

```elixir
@spec record_request(key(), number(), GenServer.name()) :: :ok
```

Records a request for a key with specified cost.

Called automatically by `check_rate/4` when it returns `:ok`.
Exposed for manual tracking if needed.

# `reset`

```elixir
@spec reset(key(), GenServer.name()) :: :ok
```

Resets rate limit tracking for a key.

# `reset_all`

```elixir
@spec reset_all(GenServer.name()) :: :ok
```

Clears all rate-limit tracking state.

Used by offline fixture gates so co-running live probes cannot poison
capacity for Req.Test stub traffic (Task 179).

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Starts the rate limiter.

# `wait_for_capacity`

```elixir
@spec wait_for_capacity(key(), rate_limit() | nil, number(), GenServer.name()) :: :ok
```

Blocks until rate limit capacity is available, then records the request.

## Parameters

- `key` -- `{exchange_id, api_key}` or `{exchange_id, :public}` tuple
- `rate_limit` -- `%{requests: max_weight, period: period_ms}` or nil
- `cost` -- Request weight/cost (default: 1)
- `name` -- GenServer name (default: `Bourse.RateLimiter`)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
