AshUi.Reload (ash_ui v0.1.0)

Copy Markdown View Source

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_attribute and any root attribute referenced through parent(...) in its filter.
  • A manual relationship'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

dep()

@type dep() :: :all | %{attributes: MapSet.t(atom()), relationships: MapSet.t(atom())}

Functions

after_dispatch(current, updated, echo, opts \\ [])

@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.

changed_attributes(current, updated)

@spec changed_attributes(struct(), struct()) :: [atom()]

Root attributes whose value differs between the rendered record and the action result — the attribute-diff half of the reload contract.

dependent_roots_for_attributes(resource, changed_attributes)

@spec dependent_roots_for_attributes(module(), [atom()]) :: [atom()]

The top-level view dependencies whose value can change when the given root attributes change, per the declaration-derived dependency index.

dependent_roots_for_paths(resource, paths)

@spec dependent_roots_for_paths(module(), [[atom()] | atom()]) :: [atom()]

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.

derived_reads(resource, field)

@spec derived_reads(module(), atom()) :: [[atom()] | {:resource, module()}]

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.

same_row?(current, updated)

@spec same_row?(struct(), struct()) :: boolean()

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.