defmodule Receiver do @moduledoc """ Conveniences for creating processes that hold state. A simple wrapper around an `Agent` that reduces boilerplate code and makes it easy to store state in a separate supervised process. # Use cases * Storing persistent process state outside of the worker process, or as a shared repository for multiple processes. * Creating a "stash" to persist process state across restarts. See example below. * Testing higher order functions. By passing a function call to a `Receiver` process into a higher order function you can test if the function is executed as intended by checking the change in state. ## Example defmodule Counter do @moduledoc false use GenServer use Receiver, as: :stash def start_link(arg) do GenServer.start_link(__MODULE__, arg, name: __MODULE__) end def increment(num) do GenServer.cast(__MODULE__, {:increment, num}) end def get do GenServer.call(__MODULE__, :get) end # The stash is started with the initial state of the counter. If the stash is already # started its state will not change. The state of the stash is returned as the # initial counter state whenever the counter is started. def init(arg) do start_stash(arg) {:ok, get_stash()} end def handle_cast({:increment, num}, state) do {:noreply, state + num} end def handle_call(:get, _from, state) do {:reply, state, state} end # The stash is updated to the current counter state before the counter exits. # This state will be stored for use as the initial state of the counter when # it restarts. def terminate(_reason, state) do update_stash(state) end end The line `use Receiver, as: :stash` creates a module and named `Agent` process with the name `Counter.Stash`. The stash is supervised in the `Receiver` application supervision tree, not in your own application's. It also defines the following client functions in the `Counter` module: * `start_stash/0` - Defaults the inital state to an empty list. * `start_stash/1` - Expects a value or anonymous function that will return the initial state. * `start_stash/3` - Expects a module, function name, and list of args that will return the initial state when called. * `stop_stash/2` - Optional `reason` and `timeout` args. See `Agent.stop/3` for more information. * `get_stash/0` - Returns the current state of the stash. * `update_stash/1` - Updates the state of the stash. expects a value or an anonymous function that receives the current state as an argument and returns the updated state. If no `:as` option were given in this example then the process would take the name `Counter.Receiver`, and the functions would be named like `start_receiver/0`. The `Counter` can now be supervised and its state will be isolated from failure and persisted across restarts. # Start the counter under a supervisor {:ok, _pid} = Supervisor.start_link([{Counter, 0}], strategy: :one_for_one) # Get the state of the counter Counter.get() #=> 0 # Increment the counter Counter.increment(2) #=> :ok # Get the updated state of the counter Counter.get() #=> 2 # The stash is still set to the initial value Counter.get_stash() #=> 0 # Stop the counter, initiating a restart GenServer.stop(Counter) #=> :ok # Get the counter state, which was persisted across restarts Counter.get() #=> 2 # Get the state of the stash, which was updated when the counter exited Counter.get_stash() #=> 2 """ defmacro __using__(opts) do {name, opts} = Keyword.pop(opts, :as, :receiver) {test, _} = Keyword.pop(opts, :test, false) module_name = __CALLER__.module |> Module.split() |> Enum.concat([Atom.to_string(name) |> Macro.camelize()]) |> Module.concat() quote bind_quoted: [name: name, module_name: module_name, test: test] do import Receiver @receiver_module_name module_name @behaviour module_name defmodule module_name do @moduledoc false use Agent, restart: :transient @callback unquote(:"start_#{name}")() :: {:ok, pid} @callback unquote(:"start_#{name}")(arg :: term) :: {:ok, pid} @callback unquote(:"start_#{name}")(module, fun, args :: [term]) :: {:ok, pid} @callback unquote(:"stop_#{name}")(reason :: term, timeout) :: :ok @callback unquote(:"get_#{name}")() :: term @callback unquote(:"update_#{name}")(fun) :: :ok @callback unquote(:"update_#{name}")(term) :: :ok def start_link(arg) def start_link([module, fun, args]) do Agent.start_link(module, fun, args, name: __MODULE__) end def start_link(fun) when is_function(fun) do Agent.start_link(fun, name: __MODULE__) end def start_link(arg) do Agent.start_link(fn -> arg end, name: __MODULE__) end end if test do def unquote(:"start_#{name}")(args \\ []) do start_supervised({@receiver_module_name, args}) end def unquote(:"start_#{name}")(module, fun, args) do start_supervised({@receiver_module_name, [module, fun, args]}) end else def unquote(:"start_#{name}")(args \\ []) do DynamicSupervisor.start_child(Receiver.Supervisor, {@receiver_module_name, args}) end def unquote(:"start_#{name}")(module, fun, args) do DynamicSupervisor.start_child(Receiver.Supervisor, {@receiver_module_name, [module, fun, args]}) end end def unquote(:"stop_#{name}")(reason \\ :normal, timeout \\ :infinity) do Agent.stop(@receiver_module_name, reason, timeout) end def unquote(:"get_#{name}")() do Agent.get(@receiver_module_name, & &1) end def unquote(:"update_#{name}")(fun) when is_function(fun) do Agent.update(@receiver_module_name, fun) end def unquote(:"update_#{name}")(value) do Agent.update(@receiver_module_name, fn _ -> value end) end end end end