defmodule Orcha do @type message() :: term() @type callback() :: {module(), func :: atom(), args :: list()} @opaque realm() :: %__MODULE__{ registry: %{atom() => service_state :: term()} } @callback handle_request(message(), realm(), state :: term()) :: {result :: term(), new_state :: term()} | {result :: term(), new_state :: term(), new_realm :: realm()} defstruct registry: %{} @default_init :initialize def initialize(_args \\ []), do: %__MODULE__{} @spec register(realm(), callback() | module()) :: realm() def register(%__MODULE__{} = realm, {mod, func, args}) do state = apply(mod, func, args) put_state(mod, state, realm) end def register(%__MODULE__{} = realm, mod) when is_atom(mod) do register(realm, {mod, @default_init, []}) end @spec request({module(), message()}, realm()) :: {:ok, result :: term(), realm()} | {:error, reason :: term()} def request({mod, msg}, %__MODULE__{} = realm) do with {:ok, state} <- Map.fetch(realm.registry, mod) do case apply(mod, :handle_request, [msg, realm, state]) do {:result, result} -> result {:result, result, new_state} -> new_realm = put_state(mod, new_state, realm) {result, new_realm} {:result, result, new_state, %__MODULE__{} = new_realm} -> new_realm = put_state(mod, new_state, new_realm) {result, new_realm} end else :error -> raise "Requesting unregistered service: #{inspect(mod)}." end end @spec put_state(module(), state :: term(), realm()) :: realm() defp put_state(mod, state, %__MODULE__{} = realm) do update_in(realm.registry, &Map.put(&1, mod, state)) end end