The engine, viewed from outside: what the graph LOOKS like, what state each cell is in, and what the last drains actually did.
Everything here is a READ. Nothing new is computed — the plan already carries
the structure and depths, the coordination tuple already carries per-key
status and freshness, and ReactiveDag.Drain.Report is already a complete
causal trace. This module is those three assembled into the shape a human (or
a dashboard, a mix task, an alerting check) actually asks for.
Deliberately UI-free: no Phoenix, no rendering, no assumptions about a web
layer. reactive_dag_dashboard renders this; a host with no web layer at all
can still call summary/1 from a mix task or a health check.
Retaining reports
Drain.run/2 returns a %Report{} and most callers discard it — the drain
deliberately does not persist anything (see ReactiveDag.Drain.Report: the
library reports, the host records). For a rolling window without a host
writing its own storage, record/1 keeps the last N in memory:
{:ok, report} = ReactiveDag.Drain.run(plan, opts)
ReactiveDag.Insights.record(report)recent/1 then reads them back, newest first. This is an opt-in observer, not
a durable log: the buffer is per-node, in-memory, and lost on restart. A host
that needs history stores the report where its runs already live.
Summary
Types
A cell's observable state.
Functions
One cell's observable state: its declaration (depth, inputs, shape) plus its live coordination state (status histogram, key count, freshness, and a small sample of failing keys for the drawer).
The graph's edges as {from, to} pairs — an input edge points from the input
cell TO the cell that reads it, i.e. the direction change flows.
Forget every retained report.
The most recent report, or nil if none has been recorded.
The graph's shape: cells grouped by depth, in execution order.
Cell ids with dirty keys waiting — what the NEXT drain would work on.
The most recent reports, newest first (default all retained).
Keep report in the rolling in-memory window (default 20,
configurable with config :reactive_dag, insights_keep: n).
Every cell's state, in execution order — the dashboard's landing view.
Types
@type cell_status() :: %{ id: String.t(), depth: non_neg_integer(), inputs: [String.t()], leaf?: boolean(), verdict?: boolean(), op: atom() | nil, statuses: %{required(String.t()) => non_neg_integer()}, key_count: non_neg_integer(), last_observed_at: DateTime.t() | nil, failing_sample: [String.t()] }
A cell's observable state.
Functions
@spec cell_status(ReactiveDag.Plan.t(), String.t()) :: cell_status() | nil
One cell's observable state: its declaration (depth, inputs, shape) plus its live coordination state (status histogram, key count, freshness, and a small sample of failing keys for the drawer).
The status read hits the tuple table, so this is a query per cell — call it
for the cells being displayed, not the whole graph, unless the graph is small
(summary/1 does exactly that, with the same caveat).
@spec edges(ReactiveDag.Plan.t()) :: [{String.t(), String.t()}]
The graph's edges as {from, to} pairs — an input edge points from the input
cell TO the cell that reads it, i.e. the direction change flows.
@spec forget_reports() :: :ok
Forget every retained report.
@spec last_report() :: %{report: ReactiveDag.Drain.Report.t(), at: DateTime.t()} | nil
The most recent report, or nil if none has been recorded.
@spec levels(ReactiveDag.Plan.t()) :: [{non_neg_integer(), [ReactiveDag.Cell.t()]}]
The graph's shape: cells grouped by depth, in execution order.
Depth is the longest path from a leaf, so every cell in a level can only depend on shallower ones — which is exactly the order the drain runs them in, and therefore the order worth drawing them in.
@spec pending(ReactiveDag.Plan.t()) :: [String.t()]
Cell ids with dirty keys waiting — what the NEXT drain would work on.
Reads the frontier rather than the tuple table: a cell is pending because something dirtied it, whether or not its tuples have changed yet.
@spec recent(pos_integer() | :all) :: [ %{report: ReactiveDag.Drain.Report.t(), at: DateTime.t()} ]
The most recent reports, newest first (default all retained).
@spec record(ReactiveDag.Drain.Report.t()) :: ReactiveDag.Drain.Report.t()
Keep report in the rolling in-memory window (default 20,
configurable with config :reactive_dag, insights_keep: n).
Opt-in: the drain persists nothing on its own. Returns the report, so it drops into a pipeline:
plan |> ReactiveDag.Drain.run(opts) |> then(fn {:ok, r} -> Insights.record(r) end)
@spec summary(ReactiveDag.Plan.t()) :: [cell_status()]
Every cell's state, in execution order — the dashboard's landing view.
One status query per cell. Fine for the graphs this library is built for
(tens of cells); for a very large graph, page it with cell_status/2 instead.