A bounded record of recent action receipts, and the telemetry bridge.
Receipts are written on the path of every dispatched event, 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.
Mob.Agent.Receipts.Owner exists only to own the table 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.
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, in Mob.Agent.Receipts.Owner, and the result is read
from :persistent_term thereafter. 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.