Packages
Event-driven notification system for Ash Framework with multiple transport types
Current section
Files
Jump to
Current section
Files
lib/transport/registry.ex
defmodule AshDispatch.Transport.Registry do
@moduledoc """
Compile-time aggregator over `AshDispatch.Transport` implementations.
The dispatcher routes by `channel.transport` atom (`:oban`, `:email`,
…); this module owns the `atom => module` mapping and the
`skip_receipt?` lookup. Each known transport is listed below in
`@transports`; adding one is a single-line addition + a new transport
file that `use`s `AshDispatch.Transport`.
## Why compile-time
At runtime, `Dispatcher.dispatch_to_transport/4` is hot-path — every
event fire passes through it. A compile-time `Map.new` from the
module list avoids any runtime introspection cost. The Map is
module-attribute-frozen; lookups are constant time.
## Why hardcoded module list (not auto-discovery)
We could walk `:application.get_key(:ash_dispatch, :modules)` and
filter implementers of the behaviour, but:
1. Boot order matters — the dispatcher could be invoked before
transport modules finish loading, producing a partial registry.
2. AshDispatch is a library; its consumers can't add their own
transports today (only the 8 listed here). If that changes,
this module evolves to a Spark DSL-driven registry.
Today's static list is the right shape for the actual consumer set.
"""
@transports [
AshDispatch.Transports.InApp,
AshDispatch.Transports.Email,
AshDispatch.Transports.Discord,
AshDispatch.Transports.Slack,
AshDispatch.Transports.SMS,
AshDispatch.Transports.Webhook,
AshDispatch.Transports.Broadcast,
AshDispatch.Transports.Oban
]
# Compile-time map. Each transport's `transport_atom/0` must be a
# compile-time constant for this to work, which the `use` macro
# guarantees (the atom is interpolated into the function body).
@atom_to_module Map.new(@transports, fn module -> {module.transport_atom(), module} end)
@doc "All registered transport modules."
@spec all() :: [module()]
def all, do: @transports
@doc "All registered transport atoms."
@spec atoms() :: [atom()]
def atoms, do: Map.keys(@atom_to_module)
@doc """
Find the module that handles a transport atom.
Returns `{:ok, module}` or `:error` for unknown atoms. Callers
should `:error`-handle by logging + skipping the receipt (matches
the pre-F1 case-fallthrough behaviour).
"""
@spec module_for(atom()) :: {:ok, module()} | :error
def module_for(atom) when is_atom(atom) do
case Map.fetch(@atom_to_module, atom) do
{:ok, _} = ok -> ok
:error -> :error
end
end
@doc """
Whether the transport skips `DeliveryReceipt` creation. Returns
`false` for unknown atoms (the safe default — produce a receipt
so the audit trail isn't silent).
"""
@spec skip_receipt?(atom()) :: boolean()
def skip_receipt?(atom) when is_atom(atom) do
case module_for(atom) do
{:ok, module} -> module.skip_receipt?()
:error -> false
end
end
@doc """
Required event-metadata keys for the transport (F4). Returns `[]`
for unknown atoms — `ValidateChannels` will already have raised
on the unknown transport itself.
"""
@spec required_event_metadata_keys(atom()) :: [atom()]
def required_event_metadata_keys(atom) when is_atom(atom) do
case module_for(atom) do
{:ok, module} ->
if function_exported?(module, :required_event_metadata_keys, 0) do
module.required_event_metadata_keys()
else
[]
end
:error ->
[]
end
end
end