AshDispatch.Event.CustomTopic (AshDispatch v0.5.1)

View Source

Lightweight :custom_topic transport for AshDispatch.Event.

Some events are fire-and-forget per-record PubSub broadcasts to a fixed (or per-record) topic — no recipients, no content rendering, no DeliveryReceipt rows. The full AshDispatch.Event Spark DSL + Dispatcher pipeline is the wrong shape for that workload: it exists to fan a single event out across N channels with per- audience templates and receipts.

use AshDispatch.Event, transports: [custom_topic: [...]] injects a thin helper layer that bypasses the dispatcher and calls Phoenix.PubSub.broadcast/3 directly, with the safe-broadcast + telemetry wrapping that hand-rolled broadcasters were otherwise re-implementing.

Usage

defmodule MyApp.Events.SomethingHappened do
  use AshDispatch.Event,
    transports: [
      custom_topic: [
        pubsub: MyApp.PubSub,
        topic: "things:all",
        event_name: :something_happened
      ]
    ]

  def broadcast(record) do
    payload = %{id: record.id, foo: record.foo}
    safe_broadcast({event_name(), payload})
  end
end

Options

  • :pubsub (required) — the Phoenix.PubSub module to broadcast through.
  • :topic (required) — either a string topic OR an {Module, :function} tuple resolved at broadcast time with the record as the single arg. Per-record routing is the foundation for per-wave / per-crucible topic variants without changing the call sites.
  • :event_name (optional) — atom subscribers receive in the wire-shape {event_name, payload}. Defaults to the underscored last segment of the module name (e.g. EngagementClaimed:engagement_claimed).

Generated helpers

Injected into the implementor module:

  • topic/0 — static topic string (raises if :topic is an MFA tuple).
  • topic/1 — resolves the topic for a given record (calls the MFA tuple or returns the static string).
  • event_name/0 — the wire-event atom.
  • safe_broadcast/1 — wraps Phoenix.PubSub.broadcast/3 with rescue + [:ash_dispatch, :custom_topic, :broadcast_failure] telemetry + log. Always returns :ok. Takes the {event_name, payload} tuple.
  • safe_broadcast/2 — same as /1 but takes an explicit record for per-record topic resolution: safe_broadcast(record, {evt, payload}).

All generated functions are defoverridable — implementors can replace any of them without losing the others' compile-time wiring.

Why this lives next to AshDispatch.Event

The use call is the same module name; the heavyweight DSL path (channels / recipients / content) remains the default when no :transports option is passed. The split happens at macro-expansion time: when transports: [custom_topic: ...] is present, this module's inject/1 returns extra quoted AST appended to the existing DSL setup; when absent, behaviour is identical to pre-extension.