defmodule Cachex.Actions.Take do
  @moduledoc false
  # Command module to allow taking of cache entries.
  #
  # The notion of taking a key is the act of retrieving a key and deleting it
  # in a single atomic action. It's useful when used to guarantee that a given
  # process retrieves the final value of an entry.
  #
  # Taking a key is clearly destructive, so it operates in a lock context.
  alias Cachex.Services.Informant
  alias Cachex.Services.Janitor
  alias Cachex.Services.Locksmith

  # we need our imports
  import Cachex.Spec

  ##############
  # Public API #
  ##############

  @doc """
  Takes an entry from a cache.

  This will always remove the entry from the cache, ensuring that the entry no
  longer exists immediately after this call finishes.

  Expirations are lazily checked here, ensuring that even if a value is taken
  from the cache it's only returned if it has not yet expired.

  Taking a value happens in a lock aware context to ensure that the key isn't
  being currently locked by another write sequence.
  """
  def execute(cache(name: name) = cache, key, _options) do
    Locksmith.write(cache, [key], fn ->
      name
      |> :ets.take(key)
      |> handle_take(cache)
    end)
  end

  ###############
  # Private API #
  ###############

  # Handles the result of taking a key from the backing table.
  #
  # If an entry comes back from the call, we check for expiration before returning
  # back to the caller. If the entry has expired, we broadcast the expiry (as the
  # entry was already removed when we took if from the cache).
  defp handle_take([entry(value: value) = entry], cache) do
    case Janitor.expired?(cache, entry) do
      false ->
        {:ok, value}

      true ->
        Informant.broadcast(
          cache,
          const(:purge_override_call),
          const(:purge_override_result)
        )

        {:ok, nil}
    end
  end

  defp handle_take([], _cache),
    do: {:ok, nil}
end
