# `Latch.Store`

Storage backend for in-flight OAuth requests and long-lived sessions.

Implement this behavior in your app. The library is storage-agnostic.
It hands you plains tructs and expects you to persist them however you
like (Ecto, Redis, ETS, ...). The library never sees encryption or the
database, that's your concerns.

Requests are keyed by the OAuth `state`.
Sessions are keyed by the user's DID.

# `did`

```elixir
@type did() :: binary()
```

# `reason`

```elixir
@type reason() :: :not_found | :backend_error
```

# `state`

```elixir
@type state() :: binary()
```

# `store_error`

```elixir
@type store_error() :: {:error, reason()}
```

# `delete_session`

```elixir
@callback delete_session(did()) :: :ok | store_error()
```

Delete the session for `did`. Return `:ok` even if missing.

# `fetch_session`

```elixir
@callback fetch_session(did()) :: {:ok, Latch.Session.t()} | store_error()
```

Fetch the session for `did`.

# `put_request`

```elixir
@callback put_request(state(), Latch.Request.t(), ttl_seconds :: pos_integer()) ::
  :ok | store_error()
```

Store an inflight request under `state` for at least `ttl_seconds`.

# `put_session`

```elixir
@callback put_session(did(), Latch.Session.t()) :: :ok | store_error()
```

Insert or replace session for `did`.

# `take_request`

```elixir
@callback take_request(state()) :: {:ok, Latch.Request.t()} | store_error()
```

Atomically fetch and remove the request for `state`.

Single use. Must be consumable exactly once, even under concurrency.
Return `{:error, :not_found}` if used or non-existant.

# `update_session`

```elixir
@callback update_session(did(), (Latch.Session.t() -&gt;
                         {:ok, Latch.Session.t()} | {:error, term()})) ::
  {:ok, Latch.Session.t()} | store_error()
```

Run `fun` against the session for `did` under an exclusive lock, within
an atomic transaction. Rotating refresh tokens are single use, so two
concurrent refreshes must run in serial.

Use `FOR UPDATE` or CAS to make atomic.

- `{:ok, new_session}` persists `new_session` inside the same transaction and returns it.
- `{:error, reason}` rolled back, reason propagated.

`{:error, :not_found}` if no session exists for `did`.

---

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