# `Mob.Agent.Receipts`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/agent/receipts.ex#L1)

A bounded record of recent action receipts, and the telemetry bridge.

Receipts are written on the path of every action — each dispatched event and
each discrete native input — so this is deliberately cheap: one ETS insert
into a `:set`, one `:atomics.add_get/3`, and an eviction check. **No process
is involved on the write path** — a GenServer in front of the table would
serialise every event in the app through one mailbox, which is the opposite
of what a diagnostic should cost.
The table is owned by a `Mob.Diag.Store` owner so it outlives the screens that
write to it; nothing routes through it.

## Bounded, and honest about it

The table keeps the most recent 256 receipts. An agent
that drives an action and reads its receipt immediately will always find it;
one that drives ten thousand actions and then goes looking for the first will
not. `count/0` reports how many are held and `dropped/0` how many were evicted,
so "no receipt for that id" can be distinguished from "that id never existed" —
a diagnostic that silently forgets is a diagnostic that lies. A receipt that
could not be recorded at all is counted as `lost` in `Mob.Diag.health/0`.

## Telemetry without a dependency

`mob` has exactly one runtime dependency. Adding `:telemetry` for this would
double that, on a framework whose whole premise is running on a phone, so
events are emitted only when the host application already has it loaded.
Apps with telemetry (most Phoenix-adjacent ones) get:

    [:mob, :action, :stop]

with measurements `%{duration_us: ..., }` and metadata carrying the receipt.
The check runs once per store setup and the result is read from the store's
state thereafter (`Mob.Diag.Store.reload/1` re-runs it). Doing it per action
would be far worse than it looks: a *negative* `Code.ensure_loaded?/1` is not
cached, so every event in every app would make a `gen_server` call into
`:code_server` and scan the code path.

# `count`

```elixir
@spec count() :: non_neg_integer()
```

How many receipts are currently held.

# `dropped`

```elixir
@spec dropped() :: non_neg_integer()
```

How many receipts have been evicted since the table was created.

Non-zero means `fetch/1` returning `:error` is ambiguous for old ids.

# `fetch`

```elixir
@spec fetch(String.t()) :: {:ok, Mob.Agent.Receipt.t()} | :error
```

The receipt for `action_id`, or `:error` if it is not held.

# `recent`

```elixir
@spec recent(pos_integer()) :: [Mob.Agent.Receipt.t()]
```

The most recent receipts, newest first.

# `record`

```elixir
@spec record(Mob.Agent.Receipt.t()) :: Mob.Agent.Receipt.t()
```

Record `receipt`, evicting the oldest when the table is full.

Returns the receipt, so this can sit at the end of a pipeline.

---

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