lib/refreshable_agent.ex

defmodule ExMicrosoftBot.RefreshableAgent do
  @callback get_refreshed_state(any, any) :: any
  @callback time_to_refresh_after_in_seconds(any) :: integer

  @doc false
  defmacro __using__(_) do
    quote location: :keep do
      require Logger
      @behaviour ExMicrosoftBot.RefreshableAgent
      use GenServer

      # Init

      def init(args) do
        state =
          args
          |> get_refreshed_state_and_schedule_refresh(nil)
          |> Map.merge(%{original_args: args}, fn _k1, _v1, v2 -> v2 end)

        {:ok, state}
      end

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

      # GenServer API

      def handle_call(:get_state, _from, %{state: mod_state} = state) do
        Logger.debug("refreshable_agent: handle_call/3 -> :get_state -> #{inspect(state)}")
        {:reply, mod_state, state}
      end

      # Cancel the timer, send message to self to refresh and reply with :ok
      def handle_call(:force_refresh, _from, %{timer_ref: timer_ref} = state) do
        Logger.debug("refreshable_agent: handle_call/3 -> :force_refresh")

        Process.cancel_timer(timer_ref)
        send(self(), :refresh)

        {:reply, :ok, state}
      end

      def handle_info(:refresh, %{state: mod_state, original_args: args} = state) do
        new_state =
          args
          |> get_refreshed_state_and_schedule_refresh(mod_state)
          |> Map.merge(state, fn _k, v1, _v2 -> v1 end)

        {:noreply, new_state}
      end

      # Private

      @spec get_state() :: any
      defp get_state() do
        GenServer.call(__MODULE__, :get_state)
      end

      @spec get_refreshed_state_and_schedule_refresh(any, any) :: Map.t()
      defp get_refreshed_state_and_schedule_refresh(args, old_state) do
        Logger.debug(
          "RefreshableAgent.get_refreshed_state_and_schedule_refresh/2 -> #{inspect(old_state)}"
        )

        updated_mod_state = get_refreshed_state(args, old_state)

        Logger.debug(
          "RefreshableAgent.get_refreshed_state_and_schedule_refresh/2 -> #{
            inspect(updated_mod_state)
          }"
        )

        timer_ref =
          updated_mod_state
          |> time_to_refresh_after_in_seconds()
          # Converting to ms
          |> Kernel.*(1000)
          |> schedule_next_refresh

        %{state: updated_mod_state, timer_ref: timer_ref}
      end

      @spec force_refresh_state() :: :ok
      defp force_refresh_state() do
        GenServer.call(__MODULE__, :force_refresh)
      end

      defp schedule_next_refresh(refresh_time_in_seconds) do
        Logger.debug("RefreshableAgent.schedule_next_refresh/1 -> #{refresh_time_in_seconds}")
        Process.send_after(__MODULE__, :refresh, refresh_time_in_seconds)
      end
    end
  end
end