Current section

Files

Jump to
ash_dispatch lib transport.ex
Raw

lib/transport.ex

defmodule AshDispatch.Transport do
  @moduledoc """
  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 `use`s 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)}`.
  """

  @doc """
  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.
  """
  @callback deliver(
              receipt :: map(),
              context :: map(),
              channel :: map(),
              event_config :: keyword()
            ) :: {:ok, map()} | {:error, term()}

  @doc "The transport atom this module handles (e.g. `:oban`, `:broadcast`)."
  @callback transport_atom() :: atom()

  @doc """
  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`).
  """
  @callback skip_receipt?() :: boolean()

  @doc """
  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 `[]`.
  """
  @callback required_event_metadata_keys() :: [atom()]

  @optional_callbacks required_event_metadata_keys: 0

  defmacro __using__(opts) do
    atom = Keyword.fetch!(opts, :atom)
    skip_receipt? = Keyword.get(opts, :skip_receipt?, false)
    required_keys = Keyword.get(opts, :required_event_metadata_keys, [])

    quote do
      @behaviour AshDispatch.Transport

      @impl AshDispatch.Transport
      def transport_atom, do: unquote(atom)

      @impl AshDispatch.Transport
      def skip_receipt?, do: unquote(skip_receipt?)

      @impl AshDispatch.Transport
      def required_event_metadata_keys, do: unquote(required_keys)

      defoverridable transport_atom: 0, skip_receipt?: 0, required_event_metadata_keys: 0
    end
  end
end