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.
Summary
Functions
How many receipts are currently held.
How many receipts have been evicted since the table was created.
The receipt for action_id, or :error if it is not held.
The most recent receipts, newest first.
Record receipt, evicting the oldest when the table is full.
Functions
@spec count() :: non_neg_integer()
How many receipts are currently held.
@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.
@spec fetch(String.t()) :: {:ok, Mob.Agent.Receipt.t()} | :error
The receipt for action_id, or :error if it is not held.
@spec recent(pos_integer()) :: [Mob.Agent.Receipt.t()]
The most recent receipts, newest first.
@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.