lib/nostrum/cache/user_cache/ets.ex

defmodule Nostrum.Cache.UserCache.ETS do
  @table_name :nostrum_users
  @moduledoc """
  An ETS-based cache for users.

  The default table name under which users are cached is `#{@table_name}`.
  In addition to the cache behaviour implementations provided by this module,
  you can also call regular ETS table methods on it, such as `:ets.info`.
  """
  @moduledoc since: "0.5.0"

  @behaviour Nostrum.Cache.UserCache

  alias Nostrum.Struct.User
  use Supervisor

  @doc "Start the supervisor."
  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  @doc "Set up the ETS table."
  @impl Supervisor
  def init(_init_arg) do
    :ets.new(@table_name, [:set, :public, :named_table])
    Supervisor.init([], strategy: :one_for_one)
  end

  @doc "Retrieve the ETS table name used for the cache."
  @spec tabname :: atom()
  def tabname, do: @table_name

  @doc "Bulk create a list of users from upstream data."
  @impl Nostrum.Cache.UserCache
  @spec bulk_create(Enum.t()) :: :ok
  def bulk_create(users) do
    Enum.each(users, &:ets.insert(@table_name, {&1.id, &1}))
  end

  @doc "Create a user from upstream data."
  @impl Nostrum.Cache.UserCache
  @spec create(map()) :: User.t()
  def create(user) do
    :ets.insert(@table_name, {user.id, user})
    User.to_struct(user)
  end

  @impl Nostrum.Cache.UserCache
  @spec delete(User.id()) :: User.t() | :noop
  def delete(id) do
    case lookup(id) do
      {:ok, user} ->
        :ets.delete(@table_name, id)
        User.to_struct(user)

      _ ->
        :noop
    end
  end

  @doc "Get a user by ID."
  @impl Nostrum.Cache.UserCache
  @spec get(User.id()) :: {:ok, User.t()} | {:error, atom}
  def get(id) do
    case lookup(id) do
      {:ok, user} -> {:ok, User.to_struct(user)}
      error -> error
    end
  end

  @doc "Update a user from upstream data."
  @impl Nostrum.Cache.UserCache
  @spec update(map()) :: {User.t(), User.t()} | :noop
  def update(info) do
    with {:ok, u} <- lookup(info.id),
         new_user = Map.merge(u, info),
         false <- u == new_user do
      :ets.insert(@table_name, {new_user.id, new_user})
      {User.to_struct(u), User.to_struct(new_user)}
    else
      {:error, _} ->
        # User just came online, make sure to cache if possible
        if Enum.all?([:username, :discriminator], &Map.has_key?(info, &1)),
          do: :ets.insert(@table_name, {info.id, info})

        :noop

      true ->
        :noop
    end
  end

  @doc false
  @spec lookup(User.id()) :: {:error, :user_not_found} | {:ok, map}
  def lookup(id) do
    case :ets.lookup(@table_name, id) do
      [] ->
        {:error, :user_not_found}

      [{^id, user}] ->
        {:ok, user}
    end
  end
end