AshDispatch.Event.CustomTopic (AshDispatch v0.5.0)
View SourceLightweight :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
endOptions
:pubsub(required) — thePhoenix.PubSubmodule 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:topicis 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— wrapsPhoenix.PubSub.broadcast/3with rescue +[:ash_dispatch, :custom_topic, :broadcast_failure]telemetry + log. Always returns:ok. Takes the{event_name, payload}tuple.safe_broadcast/2— same as/1but 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.