lib/cache.ex

defmodule Kdb.Cache do
  defstruct [:name, :ttl, :t]

  @type t :: %__MODULE__{
          name: atom(),
          ttl: integer(),
          t: any()
        }

  @unit_time :millisecond
  @key_delete :delete

  def new(opts) do
    name = Keyword.get(opts, :name) || make_ref()

    case Kdb.Registry.get_cache(name) do
      nil ->
        t =
          :ets.new(__MODULE__, [
            :set,
            :public,
            read_concurrency: true,
            write_concurrency: true
          ])

        ttl = Keyword.get(opts, :ttl, :undefined)
        cache = %__MODULE__{name: name, ttl: ttl, t: t}
        public = Keyword.get(opts, :public, true)

        if public do
          Kdb.Registry.register(cache)
        end

        cache

      cache ->
        cache
    end
  end

  @spec put(
          cache :: t(),
          bucket :: atom(),
          key :: binary(),
          value :: term(),
          bucket_ttl :: integer() | :infinity
        ) ::
          boolean()
  def put(%__MODULE__{t: t, ttl: ttl}, bucket, key, value, bucket_ttl) do
    :ets.insert(t, {{bucket, key}, value, calc_ttl(ttl, bucket_ttl)})
  end

  @spec update_counter(
          cache :: t(),
          bucket_name :: atom(),
          key :: binary(),
          amount :: integer(),
          default :: term(),
          bucket_ttl :: integer() | :infinity
        ) ::
          integer()
  def update_counter(%__MODULE__{t: t, ttl: ttl}, bucket_name, key, amount, default, bucket_ttl) do
    id = {bucket_name, key}
    :ets.update_counter(t, id, {2, amount}, {id, default, calc_ttl(ttl, bucket_ttl)})
  end

  @spec get(
          cache :: t(),
          bucket_name :: atom(),
          key :: binary(),
          bucket_ttl :: integer() | :infinity
        ) :: term() | nil
  def get(%__MODULE__{t: t, ttl: ttl}, bucket, key, bucket_ttl) do
    id = {bucket, key}

    case :ets.lookup(t, id) do
      [{_key, @key_delete, _timestamp}] ->
        @key_delete

      [{_key, value, _timestamp}] ->
        update_ttl(t, id, calc_ttl(ttl, bucket_ttl))

        value

      _ ->
        nil
    end
  end

  def has_key?(%__MODULE__{t: t}, bucket, key) do
    case :ets.lookup(t, {bucket, key}) do
      [{_key, @key_delete, _timestamp}] ->
        false

      [{_key, _value, _timestamp}] ->
        true

      _ ->
        nil
    end
  end

  @spec update(
          cache :: t(),
          bucket :: atom(),
          key :: binary(),
          value :: term(),
          default :: term(),
          bucket_ttl :: integer() | :infinity
        ) ::
          boolean()
  def update(%__MODULE__{t: t, ttl: ttl}, bucket, key, value, default, bucket_ttl) do
    :ets.update_element(
      t,
      {bucket, key},
      {2, value},
      {{bucket, key}, default, calc_ttl(ttl, bucket_ttl)}
    )
  end

  @spec delete(cache :: t(), atom(), binary()) :: true
  def delete(%__MODULE__{t: t}, bucket, id) do
    # :ets.delete(t, {bucket, id})
    :ets.insert(t, {{bucket, id}, @key_delete, 0})
  end

  def cleanup(older_than) do
    tid = :ets.whereis(Kdb.Registry)

    :ets.foldl(
      fn
        {{:batch, _batch_id}, %{cache: cache}}, acc ->
          if cache.ttl != :infinity do
            acc + cleanup(cache, older_than)
          else
            acc
          end

        _, acc ->
          acc
      end,
      0,
      tid
    )
  end

  @spec cleanup(batch :: any(), older_than :: integer()) :: integer()
  def cleanup(%Kdb.Cache{t: tid}, older_than) do
    n =
      :ets.foldl(
        fn
          {key, @key_delete, _}, acc ->
            :ets.delete(tid, key)
            acc + 1

          {key, _value, readed_at}, acc when is_integer(readed_at) and readed_at < older_than ->
            :ets.delete(tid, key)
            acc + 1

          _, acc ->
            acc
        end,
        0,
        tid
      )

    n
  end

  defp calc_ttl(:infinity, _b), do: :infinity
  defp calc_ttl(:undefined, :infinity), do: :infinity
  defp calc_ttl(:undefined, ttl), do: now_add(ttl)
  defp calc_ttl(ttl, _), do: now_add(ttl)

  defp now_add(ttl) do
    :os.system_time(@unit_time) + ttl
  end

  defp update_ttl(_t, _id, ttl) when is_atom(ttl), do: :ok

  defp update_ttl(t, id, ttl) do
    :ets.update_element(t, id, {3, now_add(ttl)})
  end
end