API Reference reactive_dag v#0.17.0-rc.65

Copy Markdown View Source

Modules

SIGNING OFF ON A VERSION — two columns and one comparison.

A content-addressed digest of a row set — what those rows looked like at a moment, so a later comparison can tell whether they have moved.

A CHANGE, propagated to completion — in one transaction, in memory, stopping only where it must.

The cascade exceeded a budget instead of settling. Three budgets raise it, from bluntest to sharpest

The Oban job that propagates ONE change — the entry point for everything a host writes.

One node in the DAG — the domain-neutral IR both host apps compile down to.

Boot-time validation of config :reactive_dag, ….

Raised by ReactiveDag.Config.validate!/0 when configuration is wrong.

The DSL compile pipeline over the flat Cell IR — the resolve→lower→validate machinery both host DSLs share, parameterized by app hooks so each keeps its own domain vocabulary.

Pure DAG construction: a list of ReactiveDag.Cell → a ReactiveDag.Plan.

The engine, viewed from outside: what the graph LOOKS like, what state each cell is in, and what the last drains actually did.

What every library-provided Oban job needs from its arguments.

The vocabulary of propagation: when changed keys of a child feed a parent, which of the parent's keys become dirty?

A cluster-wide advisory lock, for the one thing that still needs one.

Shared machinery for lowering a NESTED op-expression into a flat cell list — the recursion both host DSLs independently grew (cascade's Lower.resolve_legs, the portal's Graph.build_node). Same algorithm: walk an op's legs, recurse into each, a ref resolves to an existing cell-id (no new cell), a nested op becomes an intermediate cell whose inputs are the recursed leg ids.

The library-owned DDL, callable from a host migration — the suspension table ReactiveDag.Suspension reads and writes

An Ash resource extension that makes a resource a node in a reactive DAG. The resource IS the node and its own payload table: its reactive block defines the computation, its attributes are the rows the node materializes. This is the intended shape — one resource, both roles.

A PURE-ASH-QUERY reduce: the datastore does the grouping via a RELATIONSHIP aggregate. The node's own resource is the group's resource — ONE row per group — and over names its has_many to the rows being aggregated. The library loads the aggregates in ONE Ash query — Postgres computes the GROUP BY — and each parent row's aggregate values are its payload. No rows cross into the BEAM; no into/read/upsert (contrast the in-BEAM reduce, which loads every row).

A grain entry that is a CALCULATION, evaluated on a change rather than read from the datastore.

ORIGINATES A CASCADE from a written record, so what depends on it recomputes. Wired automatically by dirties_on and augmented_by — hosts do not add it.

An anonymous nested op-expression leg: composes inline as an intermediate cell (its as id, or a positional id derived from the parent). Its own legs are ref/compose, so the algebra reads as an expression tree.

The ESCAPE HATCH: declare an arbitrary recompute MODULE (a ReactiveDag.Op) for a node whose computation the reduce/join combinators can't express — an LLM call, a PDF/Tigris fetch, a bespoke multi-input recompute. compute MyApp.EventsExtract sits in the block alongside the combinators, mirroring Ash's calculate :x, :type, MyModule (the arbitrary case is an entity too, not a schema key beside the declarative ones).

A by-name CONTEXT input edge (context :people): the node READS the target as settled context but is NOT recomputed when the target changes. Still a real input (validated, ordered by depth so the target settles first, read at recompute) — it just doesn't propagate. For a node whose recompute is expensive/non-deterministic and consults mutable context it shouldn't be re-triggered by (an LLM step that looks up a human-curated people/positions table). Contrast ref, which dirties this node on change.

Which units a CHANGE affects, from the diff of that change.

A by-name FEEDBACK input edge (feedback :scheduled_meetings): a declared back-edge, closing a loop that is real in the graph but never real in TIME.

The one value that decides whether an observation MOVED.

A declarative JOIN: read ONE input's payload, index it into a LEFT and a RIGHT side (each a %{join_key => item} built from a per-side key fn), then emit one row per left key joined to its right item (right may be absent). The common declared-vs-observed reconcile/variance shape — the author writes the two side keys + the join row, not the read/write/changed plumbing.

THE propagation rule: how a change reaches a parent, decided by what the parent DECLARED. ReactiveDag.Cascade calls this — there is nothing to configure.

What a machine recompute does to a HUMAN's mark: lapse :approved_at, when_changed: :any.

Closes the payload loop for a resource-backed node: writes a combinator's output row into the node's OWN resource (cell.meta.resource), keyed by the cell key.

The PER-ENTRY MAP: for each claimed row of the input, call a generic action with that row and write its structured output into this node's attributes.

This node's rows come from OUTSIDE the graph: poll MuniWatch.Crawler.

THE engine: how a cell recomputes, decided by what its node DECLARED.

Runs a pure-Ash-query aggregate node: the datastore groups + aggregates the node's over relationship in ONE query (a relationship aggregate — Postgres does the GROUP BY), and each parent row's aggregate values become its payload.

The pure builders behind the DECLARATIVE combinator slots. Each turns a declarative spec — attribute atoms, fold keywords, side picks — into the same fn shape the per-slot escape hatches supply, so the recompute pipeline runs one code path however declarative the author went.

Runs a per_key node: for each claimed input row, call a generic action with that row and write its structured output into this node's attributes.

Executes a combinator's READ — always an Ash read of the over node's resource (its primary read action, or the :read action read: names), shaped by the optional query: transformer, and ALWAYS scoped by the library: when the recompute claimed specific dirty keys, the over's payload key is filtered to them (the transformer cannot un-scope; scoping is the substrate's correctness concern, not policy).

Runs a union node: one row per (input cell, key) across several inputs, written into this node's own table.

THE declaration the engine cares about: what unit does a change invalidate? Everything else a combinator declares — group_by, into, key derivation — is mapping data into shape once you already know what to recompute.

A declarative REDUCE (fold): read an input node's payload, group it, and reduce each group to one output row — the common map/fold shape, so the author writes the grouping + reduction, not the read/write/changed plumbing. Anything the combinator can't express (an LLM call, an external fetch, a bespoke join) uses the compute: module escape hatch instead.

A by-name input edge to another named node (ref :id). The general form — nestable inside compose. The flat depends_on: [:a, :b] schema key is sugar that lowers to one %Ref{} per id.

Reads a cell's own rows, keyed the way the DAG keys them.

The ASH-NATIVE escape hatch: run :recompute_keys declares that this node's recompute is a GENERIC action on its own resource — one step less escape than a compute module, because the computation stays a first-class Ash action (arguments, policies, Ash.run_action testability).

A dimension a human may select this node by: slice :fiscal_year.

Wires dirties_on and augmented_by onto ReactiveDag.Node.Changes.MarkDirty, so ordinary Ash writes trigger the cascade with no host boilerplate.

The N-INPUT shape: one row per (input cell, key) across several inputs, materialised into this node's own table.

Compile-time checks for the reactive block — everything verifiable against the node's OWN resource fails at defmodule, not at drain time. (Checks that need the OVER node's resource — named read actions, attribute existence — run at graph assembly instead: that is the earliest point cross-node facts are known.)

The behaviour a node's compute module implements — the recompute for ONE op, the per-cell unit of work.

The compiled DAG plan — pure data the drain executes. Decoupled from any DSL: a host app lowers its declarations into a Cell list and Graph.build/1 produces this. The drain only ever sees the Plan.

What a drain ACTUALLY did — the processing trace, returned by ReactiveDag.Cascade.run/3.

Re-derive a cell's rows without any input having changed — the code moved, not the data.

The Oban job that resumes a suspended cascade — and the one place expensive work runs.

Summing one key across many meta maps — the arithmetic behind every "what did this cost" line, wherever the numbers came from.

What one scan DID — the poll and the drain it triggered, as a single value.

The Oban job that polls one scanner and drains what it changed.

A scanner — how the world gets in.

WHERE A CASCADE STOPPED — the library's only table, and the successor to the dirty queue.

ONE in-memory stand-in for the suspension table, and the transaction primitives around it.

A cell's live verdict: the one-word answer, rolled up from its rows' statuses.