lib/docusign/client_registry.ex

defmodule DocuSign.ClientRegistry do
  @moduledoc ~S"""
  GenServer to store API clients and automatically refresh JWT access tokens.

  The registry keeps track of API clients using the user ID (which is the API Username
  in the user's profile on DocuSign).
  """
  use GenServer

  defmodule State do
    defstruct clients: %{}, oauth_impl: nil
  end

  #####
  # External API

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc """
  Get API Client
  """
  @type user_id :: String.t()
  @type oauth_error :: OAuth2.Response.t() | OAuth2.Error.t()
  @spec client(user_id) :: {:ok, OAuth2.Client.t()} | {:error, oauth_error}
  def client(user_id) do
    GenServer.call(__MODULE__, {:get_client, user_id}, 10_000)
  end

  #####
  # GenServer implementation
  def init(opts) do
    oauth_impl = Keyword.get(opts, :oauth_impl, oauth_implementation())
    {:ok, %State{oauth_impl: oauth_impl}}
  end

  def handle_call({:get_client, user_id}, _from, state) do
    try do
      client =
        case Map.get(state.clients, user_id) do
          nil -> create_client(user_id, state.oauth_impl)
          client -> refresh_client(client, state.oauth_impl)
        end

      updated_clients = Map.put(state.clients, user_id, client)

      {:reply, {:ok, client}, %{state | clients: updated_clients}}
    rescue
      error -> {:reply, {:error, error}, state}
    end
  end

  @doc """
  Async refreshes a token.
  """
  def handle_info({:refresh_token, user_id}, state) do
    client = Map.get(state.clients, user_id)
    refreshed_client = state.oauth_impl.refresh_token!(client, true)

    delay = state.oauth_impl.interval_refresh_token(refreshed_client)
    schedule_refresh_token(user_id, delay)

    updated_clients = Map.put(state.clients, user_id, refreshed_client)

    {:noreply, %{state | clients: updated_clients}}
  end

  defp create_client(user_id, oauth_impl) do
    client =
      [user_id: user_id]
      |> oauth_impl.client()
      |> oauth_impl.refresh_token!(true)

    delay = oauth_impl.interval_refresh_token(client)
    schedule_refresh_token(user_id, delay)

    client
  end

  defp refresh_client(client, oauth_impl) do
    oauth_impl.refresh_token!(client)
  end

  defp schedule_refresh_token(user_id, seconds) do
    Process.send_after(self(), {:refresh_token, user_id}, seconds * 1000)
  end

  defp oauth_implementation do
    Application.get_env(:docusign, :oauth_implementation, DocuSign.OAuth.Impl)
  end
end