Ash.Signals (ash v3.36.1)

Copy Markdown View Source

Typed signals that resources emit, and other resources listen to with their own actions.

A signal module declares signals, and is the contract between the resources that emit them and the resources that listen to them:

defmodule MyApp.Shop.Signals do
  use Ash.Signals

  signals do
    signal :order_placed do
      argument :order_id, :uuid, allow_nil?: false
    end
  end
end

See the signals guide for how to emit and listen to signals, and the Signals DSL documentation for every option.

Options

  • :extensions (list of module that adopts Spark.Dsl.Extension) - A list of DSL extensions to add to the Spark.Dsl

  • :otp_app (atom/0) - The otp_app to use for any application configurable options

  • :fragments (list of module/0) - Fragments to include in the Spark.Dsl. See the fragments guide for more.

Summary

Types

The signals that led to a listener running, starting with the one it received, followed by the signals that led to it. Listeners have it in the signal_path key of their context.

t()

Functions

Dumps signal structs to maps that can be stored, for example in a job's arguments.

Dumps a signal_path/0 to maps of strings that can be stored, for example in a job's arguments.

Emits a signal, delivering it to its listeners.

Emits a signal once for each payload, delivering them to its listeners together.

Returns the queries that select the records an update or destroy listener runs on.

Loads signal structs from maps produced by dump/3.

Runs a listener for the given signals, which is what the default delivery does for each listener.

Types

signal_path()

@type signal_path() :: [
  %{
    signal_module: t(),
    signal: atom(),
    resource: Ash.Resource.t() | nil,
    action: atom() | nil
  }
]

The signals that led to a listener running, starting with the one it received, followed by the signals that led to it. Listeners have it in the signal_path key of their context.

Each entry has the signal's signal_module and signal name, and the resource and action that emitted it, which are nil for signals delivered by Ash.Signals.Test.react_to_signal/4. It doesn't contain the signals themselves, because a listener can receive many signals at once.

t()

@type t() :: module()

Functions

dump(signal_module, name, signal_structs)

@spec dump(t(), atom(), [struct()]) :: {:ok, [map()]} | {:error, term()}

Dumps signal structs to maps that can be stored, for example in a job's arguments.

Load them again with load/3.

dump_signal_path(signal_path)

@spec dump_signal_path(signal_path()) :: [map()]

Dumps a signal_path/0 to maps of strings that can be stored, for example in a job's arguments.

Load it again with load_signal_path/1.

emit(subject, signal_module, name, payload, opts \\ [])

@spec emit(
  Ash.Changeset.t() | Ash.ActionInput.t(),
  t(),
  atom(),
  map() | Keyword.t(),
  Keyword.t()
) ::
  :ok | {:error, term()}

Emits a signal, delivering it to its listeners.

Takes the changeset of a create, update or destroy action, or the input of a generic action. Must be called during the phase declared by the signal, by an action that declares that it emits the signal dynamically:

create :place do
  emit MyApp.Shop.Signals, :order_placed, dynamic?: true
  change MyApp.Shop.Changes.EmitOrderPlaced
end

How each listener receives the signal depends on its delivery. By default, listeners run immediately. See Ash.Signals.Delivery.

Every signal has a signal_id, a UUIDv7 generated when it is emitted. Include signal_id in the payload to set it yourself, for example to emit the same signal again.

Options

  • :actor - The actor to run the listeners as. Defaults to the emitting action's actor.
  • :tenant - Defaults to the emitting action's tenant.
  • :authorize? - Defaults to the emitting action's authorize?.
  • :tracer - Defaults to the emitting action's tracer.
  • :context - Context to pass to the listeners, merged into the emitting action's shared context.
  • :scope - A value implementing Ash.Scope.ToOpts. Any of the above that are also given explicitly take precedence over the scope.

emit_many(subject, signal_module, name, payloads, opts \\ [])

@spec emit_many(
  Ash.Changeset.t() | Ash.ActionInput.t(),
  t(),
  atom(),
  [map() | Keyword.t()],
  Keyword.t()
) :: :ok | {:error, term()}

Emits a signal once for each payload, delivering them to its listeners together.

Use this from the batch callbacks of a change, so that a bulk action emits its signals together. Pass any one of the batch's changesets as the subject, for example in Ash.Resource.Change.after_batch/3:

def after_batch([{changeset, _} | _] = changesets_and_posts, _opts, _context) do
  payloads = Enum.map(changesets_and_posts, fn {_, post} -> %{post_id: post.id} end)

  case Ash.Signals.emit_many(changeset, MyApp.Blog.Signals, :post_created, payloads) do
    :ok -> Enum.map(changesets_and_posts, fn {_, post} -> {:ok, post} end)
    {:error, error} -> [{:error, error}]
  end
end

Every payload is cast before the signals are delivered, so an invalid payload emits nothing. With the default delivery, each listener then handles every signal before the next listener runs.

Takes the same options as emit/5.

listener_queries(listener, signal_structs, opts)

@spec listener_queries(Ash.Resource.SignalListener.t(), [struct()], Keyword.t()) :: [
  {Ash.Query.t(), map(), [struct()]}
]

Returns the queries that select the records an update or destroy listener runs on.

Signals are grouped by the arguments they give the listener's read action and the inputs they give its action. Returns a {query, inputs, signals} tuple for each group.

load(signal_module, name, dumped)

@spec load(t(), atom(), [map()]) :: {:ok, [struct()]} | {:error, term()}

Loads signal structs from maps produced by dump/3.

Fields the signal no longer has are ignored, and a required field that is missing is an error, so maps dumped by an older version of the signal load if they are still valid.

load_signal_path(dumped)

@spec load_signal_path([map()]) :: signal_path()

Loads a signal_path/0 from maps produced by dump_signal_path/1.

This creates the atoms the path refers to, so only load what dump_signal_path/1 produced, from storage you trust.

run_listener(listener, signal_structs, opts)

@spec run_listener(Ash.Resource.SignalListener.t(), [struct()], Keyword.t()) ::
  :ok | {:error, term()}

Runs a listener for the given signals, which is what the default delivery does for each listener.

Deliveries that run listeners later, for example in a job, call this to run them the same way. Takes the options of the action the listener runs: :actor, :tenant, :authorize?, :tracer and :context, which should contain the signal_path of the delivery context.