The O(1) dispatch reload contract — the ONE canonical policy for what a
bridge reloads after a dispatch. Every transport bridge calls
after_dispatch/4; none owns a private copy of the policy, so a new
target cannot drift from it.
The contract:
- Data reloads are exactly (a) the notification ECHO — the action's own
synchronous write notifications, scoped to the view dependency they
touched — and (b) the view roots whose declared read depends on a root
attribute the action changed (
dependent_roots_for_attributes/2). - Compiler event paths (a binding's each source and render deps) are RENDER scope only — they address the differential re-render and never justify a data read: entry rows did not change unless a notification says so.
- A
:root-scoped echo is the page row's own write notification. The dispatch result is already the authoritative fresh root row, so it adds no data reload (the historical drift: one bridge dropped it, the other answered with a full view reload). - Unaffected dependencies keep their already-loaded values
(
AshUi.preserve_loaded_values/2).
Dependency narrowing derives from declarations, never from code:
- A declarative relationship depends on its
source_attributeand any root attribute referenced throughparent(...)in its filter. - A
manualrelationship's read is opaque; it conservatively depends on every attribute unless the declaration names its dependency set:manual {MyRel, depends_on: [:sort_by, :q]}(an empty list is a valid declaration: the read depends on no root attribute). - An expression calculation depends on the root attributes its expression references (through other calculations transitively) and is invalidated by data changes on any relationship path it references.
- A module calculation declares its dependencies through
load/3; when that cannot be read it conservatively depends on everything. - An aggregate depends on its relationship path's first edge exactly like a declarative relationship, and is invalidated by data changes on that path.
Summary
Functions
The canonical post-dispatch refresh. current is the rendered page record,
updated the same-row action result, echo the dispatch's notification
echo (:root or a list of :root/relationship-path entries). Returns the
record to render from, with unaffected loaded values preserved and only the
affected dependencies re-read.
Root attributes whose value differs between the rendered record and the action result — the attribute-diff half of the reload contract.
The top-level view dependencies whose value can change when the given root attributes change, per the declaration-derived dependency index.
The top-level view roots invalidated by a DATA change on the given
relationship paths: calculations and aggregates whose declared dependencies
traverse a changed path. This is what keeps a derived root (a flattened
rows calculation over :messages, an aggregate over :albums) fresh when
the change arrives as a notification rather than a root-attribute write.
What a derived view field READS, from the same declaration-derived
dependency derivation the reload index uses: every relationship path a
calculation or aggregate traverses (through load/3, DSL load:,
expression references, aggregate paths and nested calculations or
aggregates), and {:resource, module} for an aggregate over an unrelated
resource. This is the subscription side of "whatever a page reads, it
subscribes to": Ash loads these dependencies as hidden internal loads, so
they never appear on the rendered record for a relationship walk to see.
True when the dispatch result is the same session row the page renders — the case where the action result IS the fresh page state and the narrowed refresh applies. Any other row needs the page re-read.
Types
Functions
@spec after_dispatch(struct(), struct(), :root | [term()], keyword()) :: {:ok, struct()} | {:error, term()}
The canonical post-dispatch refresh. current is the rendered page record,
updated the same-row action result, echo the dispatch's notification
echo (:root or a list of :root/relationship-path entries). Returns the
record to render from, with unaffected loaded values preserved and only the
affected dependencies re-read.
Root attributes whose value differs between the rendered record and the action result — the attribute-diff half of the reload contract.
The top-level view dependencies whose value can change when the given root attributes change, per the declaration-derived dependency index.
The top-level view roots invalidated by a DATA change on the given
relationship paths: calculations and aggregates whose declared dependencies
traverse a changed path. This is what keeps a derived root (a flattened
rows calculation over :messages, an aggregate over :albums) fresh when
the change arrives as a notification rather than a root-attribute write.
What a derived view field READS, from the same declaration-derived
dependency derivation the reload index uses: every relationship path a
calculation or aggregate traverses (through load/3, DSL load:,
expression references, aggregate paths and nested calculations or
aggregates), and {:resource, module} for an aggregate over an unrelated
resource. This is the subscription side of "whatever a page reads, it
subscribes to": Ash loads these dependencies as hidden internal loads, so
they never appear on the rendered record for a relationship walk to see.
An undeclarable dependency (a raising load/3, an expression Ash cannot
walk) contributes nothing beyond what was already derived.
True when the dispatch result is the same session row the page renders — the case where the action result IS the fresh page state and the narrowed refresh applies. Any other row needs the page re-read.