AshDispatch.Transport behaviour (AshDispatch v0.5.0)

View Source

Behaviour for AshDispatch transports.

Before this behaviour, adding a new transport (e.g. :oban) required edits in 3-4 places in AshDispatch.Dispatcher:

  1. The dispatch_to_transport/4 case statement (routing)
  2. The skip_receipt_for_transport?/1 truth table
  3. The moduledoc transport list
  4. (Optionally) tests against any of the above

With this behaviour, each transport uses the macro with its atom + receipt-skip declaration, and AshDispatch.Transport.Registry aggregates them at compile time. The dispatcher's case statement becomes a single registry lookup; adding a transport is one new file + one entry in the registry's module list.

Implementing

defmodule MyApp.Transports.Foo do
  use AshDispatch.Transport, atom: :foo, skip_receipt?: false

  def deliver(receipt, context, channel, event_config) do
    # ... transport-specific work
    {:ok, updated_receipt}
  end
end

Both transport_atom/0 and skip_receipt?/0 are generated by the macro and defoverridable, so subclasses can override either if a transport's receipt-skip behaviour is conditional (rare).

Receipt-skip semantics

skip_receipt?: true means the transport is "lightweight" — no DeliveryReceipt row is created when this channel fires. Used by :broadcast (the audit trail is the PubSub log) and :oban (the audit trail is the enqueued oban_jobs row).

Lightweight transports receive a pseudo-receipt in deliver/4: %{id: nil, ...}. They should NOT call Ash.update on the pseudo-receipt; instead return {:ok, Map.put(receipt, :status, :sent | :skipped)}.

Summary

Callbacks

Deliver an event payload to a recipient/channel.

Event metadata keys this transport REQUIRES at the event-registration layer (F4). ValidateChannels enforces presence at compile-time — an event registering a [transport: :oban] channel must declare metadata: [oban_worker: ...] or the DSL fails with a precise Spark.Error.DslError pointing at the registration site.

Whether to skip DeliveryReceipt creation for this transport. Defaults to false (Receipt + status-tracking is the default semantics; opt out only for transports where another artifact IS the audit trail — see :broadcast / :oban).

The transport atom this module handles (e.g. :oban, :broadcast).

Callbacks

deliver(receipt, context, channel, event_config)

@callback deliver(
  receipt :: map(),
  context :: map(),
  channel :: map(),
  event_config :: keyword()
) :: {:ok, map()} | {:error, term()}

Deliver an event payload to a recipient/channel.

Returns {:ok, receipt} on success (the receipt may be updated to reflect terminal status) or {:error, reason}. Errors propagate to Dispatcher.dispatch_to_transport/4 which logs + folds them into the per-recipient verdict; the dispatcher itself absorbs errors so one transport's failure doesn't block the others on the same event.

required_event_metadata_keys()

(optional)
@callback required_event_metadata_keys() :: [atom()]

Event metadata keys this transport REQUIRES at the event-registration layer (F4). ValidateChannels enforces presence at compile-time — an event registering a [transport: :oban] channel must declare metadata: [oban_worker: ...] or the DSL fails with a precise Spark.Error.DslError pointing at the registration site.

Defaults to [] (no required keys). :oban overrides to [:oban_worker].

Optional — transports that don't need event metadata return [].

skip_receipt?()

@callback skip_receipt?() :: boolean()

Whether to skip DeliveryReceipt creation for this transport. Defaults to false (Receipt + status-tracking is the default semantics; opt out only for transports where another artifact IS the audit trail — see :broadcast / :oban).

transport_atom()

@callback transport_atom() :: atom()

The transport atom this module handles (e.g. :oban, :broadcast).