Mob.Agent.Receipts (mob v0.8.3)

Copy Markdown View Source

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

count()

@spec count() :: non_neg_integer()

How many receipts are currently held.

dropped()

@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(action_id)

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

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

recent(limit \\ 20)

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

The most recent receipts, newest first.

record(receipt)

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

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