lib/cache/ets.ex

defmodule Cache.ETS do
  @opts_definition [
    write_concurrency: [
      type: :boolean,
      doc: "Enable write concurrency"
    ],
    read_concurrency: [
      type: :boolean,
      doc: "Enable read concurrency"
    ],
    decentralized_counters: [
      type: :boolean,
      doc: "Use decentralized counters"
    ],
    type: [
      type: {:in, [:bag, :duplicate_bag, :set]},
      default: :set,
      doc: "Data type of ETS cache"
    ],
    compressed: [
      type: :boolean,
      doc: "Enable ets compression"
    ]
  ]

  @moduledoc """
  ETS (Erlang Term Storage) adapter for high-performance in-memory caching.

  This adapter provides a fast, process-independent cache using Erlang's built-in ETS tables.
  It's ideal for applications requiring low-latency access to cached data within a single node.

  ## Features

  * In-memory storage with configurable concurrency options
  * Direct access to ETS-specific operations
  * Very high performance for read and write operations
  * Support for atomic counter operations

  ## Options
  #{NimbleOptions.docs(@opts_definition)}

  ## Example

  ```elixir
  defmodule MyApp.Cache do
    use Cache,
      adapter: Cache.ETS,
      name: :my_app_cache,
      opts: [
        read_concurrency: true,
        write_concurrency: true
      ]
  end
  ```
  """

  use Task, restart: :permanent

  @behaviour Cache

  defmacro __using__(_opts) do
    quote do
      @doc """
      Match objects in the ETS table that match the given pattern.

      ## Examples

          iex> #{inspect(__MODULE__)}.match_object({:_, :_})
          [...]
      """
      def match_object(pattern) do
        :ets.match_object(@cache_name, pattern)
      end

      @doc """
      Match objects in the ETS table that match the given pattern with limit.

      ## Examples

          iex> #{inspect(__MODULE__)}.match_object({:_, :_}, 10)
          {[...], continuation}
      """
      def match_object(pattern, limit) do
        :ets.match_object(@cache_name, pattern, limit)
      end

      @doc """
      Check if a key is a member of the ETS table.

      ## Examples

          iex> #{inspect(__MODULE__)}.member(:key)
          true
      """
      def member(key) do
        :ets.member(@cache_name, key)
      end

      @doc """
      Select objects from the ETS table using a match specification.

      ## Examples

          iex> #{inspect(__MODULE__)}.select([{{:key, :_}, [], [:'$_']}])
          [...]
      """
      def select(match_spec) do
        :ets.select(@cache_name, match_spec)
      end

      @doc """
      Select objects from the ETS table using a match specification with limit.

      ## Examples

          iex> #{inspect(__MODULE__)}.select([{{:key, :_}, [], [:'$_']}], 10)
          {[...], continuation}
      """
      def select(match_spec, limit) do
        :ets.select(@cache_name, match_spec, limit)
      end

      @doc """
      Get information about the ETS table.

      ## Examples

          iex> #{inspect(__MODULE__)}.info()
          [...]
      """
      def info do
        :ets.info(@cache_name)
      end

      @doc """
      Get specific information about the ETS table.

      ## Examples

          iex> #{inspect(__MODULE__)}.info(:size)
          42
      """
      def info(item) do
        :ets.info(@cache_name, item)
      end

      @doc """
      Delete objects from the ETS table using a match specification.

      ## Examples

          iex> #{inspect(__MODULE__)}.select_delete([{{:key, :_}, [], [true]}])
          42
      """
      def select_delete(match_spec) do
        :ets.select_delete(@cache_name, match_spec)
      end

      @doc """
      Delete objects from the ETS table that match the given pattern.

      ## Examples

          iex> #{inspect(__MODULE__)}.match_delete({:key, :_})
          true
      """
      def match_delete(pattern) do
        :ets.match_delete(@cache_name, pattern)
      end

      @doc """
      Update a counter in the ETS table.

      ## Examples

          iex> #{inspect(__MODULE__)}.update_counter(:counter_key, {2, 1})
          43
      """
      def update_counter(key, update_op) do
        :ets.update_counter(@cache_name, key, update_op)
      end

      @doc """
      Insert raw data into the ETS table using the underlying :ets.insert/2 function.

      ## Examples

          iex> #{inspect(__MODULE__)}.insert_raw({:key, "value"})
          true
          iex> #{inspect(__MODULE__)}.insert_raw([{:key1, "value1"}, {:key2, "value2"}])
          true
      """
      def insert_raw(data) do
        :ets.insert(@cache_name, data)
      end

      if function_exported?(:ets, :to_dets, 1) do
        @doc """
        Convert an ETS table to a DETS table.

        ## Examples

            iex> #{inspect(__MODULE__)}.to_dets(:my_dets_table)
            :ok
        """
        def to_dets(dets_table) do
          :ets.to_dets(@cache_name, dets_table)
        end

        @doc """
        Convert a DETS table to an ETS table.

        ## Examples

            iex> #{inspect(__MODULE__)}.from_dets(:my_dets_table)
            :ok
        """
        def from_dets(dets_table) do
          :ets.from_dets(@cache_name, dets_table)
        end
      end
    end
  end

  @impl Cache
  def opts_definition, do: @opts_definition

  @impl Cache
  def start_link(opts) do
    Task.start_link(fn ->
      table_name = opts[:table_name]

      opts =
        opts
        |> Keyword.drop([:table_name, :type])
        |> Kernel.++([opts[:type], :public, :named_table])

      opts =
        if opts[:compressed] do
          Keyword.delete(opts, :compressed) ++ [:compressed]
        else
          opts
        end

      _ = :ets.new(table_name, opts)

      Process.hibernate(Function, :identity, [nil])
    end)
  end

  @impl Cache
  def child_spec({cache_name, opts}) do
    %{
      id: "#{cache_name}_elixir_cache_ets",
      start: {Cache.ETS, :start_link, [Keyword.put(opts, :table_name, cache_name)]}
    }
  end

  @impl Cache
  def get(cache_name, key, _opts \\ []) do
    case :ets.lookup(cache_name, key) do
      [{^key, value}] -> {:ok, value}
      # This can happen if someone uses insert_raw
      [value] -> {:ok, value}
      [] -> {:ok, nil}
    end
  end

  @impl Cache
  def put(cache_name, key, _ttl \\ nil, value, _opts \\ []) do
    :ets.insert(cache_name, {key, value})

    :ok
  end

  @impl Cache
  def delete(cache_name, key, _opts \\ []) do
    :ets.delete(cache_name, key)

    :ok
  end
end