I've been looking for an #elixir redux-alike (primarly the event-bus stuff), and have found that Phoenix.PubSub paired with a GenServer and a Task supervisor seems to get the job done.

I did chase a few different constructions, like using Task.start with callbacks, but found that GenServer doesn't have a predicable state when callbacks are executed.

The following is scratched out, as there's opportunity to make it a bit more generic, like sending messages back to the original caller, etc.

objective: Schedule async work that can crash.

defmodule MyApp.Dispatcher do
  use GenServer

  @topic "MY_TOPIC"

  def start_link(_) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def init(state \\ %{}) do
    # subscribe to events emitted elsewhere
    Phoenix.PubSub.subscribe(MyApp.PubSub, @topic)

    # Start a Task.Supervisor
    Task.Supervisor.start_link(name: MyApp.Dispatcher.TaskSupervisor)

    {:ok, state}
  end

  # handle messages sent from dispatched tasks
  def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
    next = Map.delete(state, pid)

    # broadcast that a task is complete
    PubSub.broadcast(MyApp.PubSub, @topic, {:tasks?, next})

    {:noreply, next}
  end

  def handle_info({_event, %{id: _id}} = msg, state) do
    task =
      Task.Supervisor.async_nolink(MyApp.Dispatcher.TaskSupervisor, fn ->
        # doing something async. It may or may not crash
        # For instance, maybe this does a database write
      end)

    {:noreply, Map.put(state, task.pid, task)}
  end

  def handle_info(_, state) do
    {:noreply, state}
  end
end