defmodule Orcha do @type message() :: term() @type timestamp() :: pos_integer() @type request_info() :: {timestamp(), realm()} @type callback() :: {module(), func :: atom(), args :: list()} @opaque realm() :: %__MODULE__{ registry: %{atom() => service_state :: term()} } @callback handle_request(message(), request_info(), 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()}, timestamp(), realm()) :: {result :: term(), realm()} def request({mod, msg}, timestamp, realm) do with {:ok, state} <- Map.fetch(realm.registry, mod) do case apply(mod, :handle_request, [msg, {timestamp, realm}, state]) do {result, new_state} -> new_realm = put_state(mod, new_state, realm) {result, new_realm} {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