# `Ankusa.Routes.Store`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/routes/store.ex#L1)

Where route definitions live. This module is both the **behaviour** every
store implements and the instance-scoped **facade** the context calls, the
same split as `Ankusa.SourceStore`: the facade resolves
`config.routes.store`'s `{module, opts}` and delegates.

## Two stores

  * `Ankusa.Routes.Store.ETS` — the default. Definitions are node-local and
    vanish on restart; `config.routes.seed` is how a standalone deployment
    gets a durable starting point, and it is applied on every boot.
  * `Ankusa.Routes.Store.Redis` (in `ankusa_redis`) — definitions live in
    Redis and a version counter plus pub/sub invalidates every node. The
    guard still reads the in-memory snapshot, never Redis.

## Definitions, never evictions

A store holds definitions, so `insert/3` is capped by
`config.routes.max_routes` and returns `{:error, :too_many_routes}` past it —
nothing is silently evicted. An evicted definition would turn into a rejected
webhook, and a rejected webhook is an operator's outage, not a cache miss.

## Writes are version-checked

`Ankusa.Routes` validates a write — a free id, no colliding enabled route —
against the snapshot it just read, so a store may only apply it if that
snapshot is still current. `insert/3` and `replace/3` therefore take the
`version` of the snapshot the caller validated against, and answer
`{:error, :stale}`, having changed nothing, when the table has moved on. Before
answering `:stale` a store MUST have published the newer snapshot, so the
caller's retry validates against what is really there.

A store shared by several nodes checks that version in the shared store,
atomically with the write, not against its own copy: a node whose copy lags
would otherwise happily validate against a table another node already changed,
and two nodes could both create the same id.

`delete/3` and `put_ip_rules/2` carry no cross-route validation, so they take
no version: a delete answers `{:error, :not_found}` from the store's own
authoritative state, and replacing the global rules is a whole-value write.
A delete made on behalf of one tenant passes it as `tenant`, and the store
answers `{:error, :not_found}` for a route another tenant owns, checked in
the same step as the delete, so a route that was re-created under another
tenant between the caller's read and the delete is never removed; `nil`
deletes whoever owns it.

`{:error, :store_unavailable}` is the transient failure — Redis is down, or the
store process did not answer, which the facade reports the same way. The
in-memory store never returns it itself.

# `error`

```elixir
@type error() :: :too_many_routes | :not_found | :stale | :store_unavailable
```

# `delete`

```elixir
@callback delete(atom(), String.t(), String.t() | nil) :: :ok | {:error, error()}
```

# `insert`

```elixir
@callback insert(atom(), Ankusa.Routes.Route.t(), pos_integer()) ::
  :ok | {:error, error()}
```

# `put_ip_rules`

```elixir
@callback put_ip_rules(atom(), map()) :: :ok | {:error, error()}
```

# `replace`

```elixir
@callback replace(atom(), Ankusa.Routes.Route.t(), pos_integer()) ::
  :ok | {:error, error()}
```

# `start_link`

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

Called by `Ankusa.Instance` with `[instance: instance]`; read the store's own
options from `Ankusa.config(instance).routes.store`.

# `delete`

```elixir
@spec delete(atom(), String.t(), String.t() | nil) :: :ok | {:error, error()}
```

# `insert`

```elixir
@spec insert(atom(), Ankusa.Routes.Route.t(), pos_integer()) ::
  :ok | {:error, error()}
```

# `put_ip_rules`

```elixir
@spec put_ip_rules(atom(), map()) :: :ok | {:error, error()}
```

# `replace`

```elixir
@spec replace(atom(), Ankusa.Routes.Route.t(), pos_integer()) ::
  :ok | {:error, error()}
```

---

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