lib/trigger.ex

defmodule Trigger do
  @moduledoc """
  A simple way to sync between processes.
  """

  @enforce_keys [:ref, :receiver]
  defstruct [:ref, :receiver]

  @opaque t() :: %Trigger{
    ref: reference(),
    receiver: pid(),
  }

  @spec new(options) :: t
        when options: [
          receiver: pid(),
        ]
  def new(opts \\ []) do
    receiver = Keyword.get(opts, :receiver, self())
    ref = make_ref()
    %Trigger{ref: ref, receiver: receiver}
  end

  @spec fire(t, term) :: :ok
  def fire(%Trigger{} = trigger, data \\ nil) do
    send(trigger.receiver, {trigger.ref, self(), data})
    :ok
  end

  @spec fire_wait(t, term, timeout) :: term | no_return
  def fire_wait(%Trigger{ref: ref} = trigger, data \\ nil, timeout \\ :infinity) do
    fire(trigger, data)
    receive do
      {^ref, data} -> data
    after
      timeout -> exit({:timeout, {__MODULE__, :fire_wait, [trigger, data, timeout]}})
    end
  end

  @spec wait(t, timeout) :: {pid, term} | no_return
  def wait(%Trigger{ref: ref} = trigger, timeout \\ :infinity) do
    receive do
      {^ref, from, data} -> {from, data}
    after
      timeout -> exit({:timeout, {__MODULE__, :wait, [trigger, timeout]}})
    end
  end

  @spec reply(t, pid, term) :: :ok
  def reply(%Trigger{ref: ref}, to, reply \\ nil) do
    send(to, {ref, reply})
    :ok
  end

  @spec wait_reply(t, term, timeout) :: term | no_return
  def wait_reply(%Trigger{} = trigger, data \\ nil, timeout \\ :infinity) do
    {from, data_in} = wait(trigger, timeout)
    reply(trigger, from, data)
    data_in
  end

end