lib/cache.ex

defmodule AwsSigner.Cache do
  require Logger
  use Agent

  def start_link(opts \\ []),
    do: Agent.start_link(fn -> {opts[:log], %{}} end, name: __MODULE__)

  def fetch(key, fallback \\ nil) do
    case Agent.get(__MODULE__, & &1) do
      {log, %{^key => {value, nil}}} ->
        hit(key, value, nil, log)

      {log, %{^key => {value, t}}} ->
        now = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
        if now > t,
          do: miss(key, fallback, log),
        else: hit(key, value, t, log)

      {log, %{}} ->
        miss(key, fallback, log)
    end
  end

  def put(key, value, expire_at \\ nil) do
    Agent.update(__MODULE__, fn {log, store} ->
      {log, Map.put(store, key, {value, expire_at})}
    end)

    {value, expire_at}
  end

  def delete(key) do
    Agent.update(__MODULE__, fn {log, store} ->
      {log, Map.delete(store, key)}
    end)
  end

  #
  # private
  #

  defp hit(key, value, expire_at, log) do
    if log,
      do: Logger.info("Cache hit: #{inspect(key)} (#{expire_at})")

    {:hit, {value, expire_at}}
  end

  defp miss(key, fallback, log) do
    if log,
      do: Logger.info("Cache miss: #{inspect(key)}")

    if is_function(fallback) do
      case fallback.() do
        {:ok, value, expire_at} ->
          {:miss, put(key, value, expire_at)}

        {:ok, value} ->
          {:miss, put(key, value)}

        # Fallback failed
        other ->
          {:miss, :error, other}
      end
    else
      :miss
    end
  end
end