lib/nostrum/cache/presence_cache/mnesia.ex

if Code.ensure_loaded?(:mnesia) do
  defmodule Nostrum.Cache.PresenceCache.Mnesia do
    @moduledoc """
    An Mnesia-based cache for presences.

    #{Nostrum.Cache.Base.mnesia_note()}
    """
    @moduledoc since: "0.8.0"

    @table_name :nostrum_presences
    @record_name @table_name

    @behaviour Nostrum.Cache.PresenceCache

    alias Nostrum.Cache.PresenceCache
    alias Nostrum.Struct.Guild
    use Supervisor

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

    @doc "Clear any objects in the cache."
    @spec clear() :: {:atomic, :ok} | {:aborted, term()}
    def clear, do: :mnesia.clear_table(@table_name)

    @doc "Drop the table used for caching."
    @spec teardown() :: {:atomic, :ok} | {:aborted, term()}
    def teardown, do: :mnesia.delete_table(@table_name)

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

    @impl Supervisor
    @doc "Set up the cache's Mnesia table."
    def init(_init_arg) do
      options = [attributes: [:key, :data], record_name: @record_name]

      case :mnesia.create_table(@table_name, options) do
        {:atomic, :ok} -> :ok
        {:aborted, {:already_exists, _tab}} -> :ok
      end

      Supervisor.init([], strategy: :one_for_one)
    end

    @impl PresenceCache
    @doc "Add the given presence to the cache."
    @spec create(map()) :: :ok
    def create(presence) do
      record = {@record_name, {presence.guild_id, presence.user.id}, presence}
      :ok = :mnesia.activity(:sync_transaction, fn -> :mnesia.write(record) end)
    end

    @impl PresenceCache
    @doc "Update the given presence in the cache."
    @spec update(map()) :: {Guild.id(), presence | nil, presence} | :noop
          when presence: PresenceCache.presence()
    def update(new) do
      key = {new.guild_id, new.user.id}

      :mnesia.activity(:sync_transaction, fn ->
        new_activities = new[:activities]
        new_status = new[:status]

        case :mnesia.read(@table_name, key, :write) do
          [{_tag, _key, %{activities: ^new_activities, status: ^new_status}}] ->
            :noop

          [{_tag, _key, old}] ->
            merged = Map.merge(old, new)
            :mnesia.write(@table_name, {@record_name, key, merged}, :write)
            {new.guild_id, old, merged}

          [] ->
            {new.guild_id, nil, new}
        end
      end)
    end

    @impl PresenceCache
    @doc "Bulk create a chunk of presences for the given guild in the cache."
    def bulk_create(guild_id, presences) do
      :mnesia.activity(:sync_transaction, fn ->
        # https://erlang.org/pipermail/erlang-questions/2005-August/016382.html
        # Substantially reduces locking overhead for large amount of records.
        :mnesia.write_lock_table(@table_name)

        Enum.each(
          presences,
          &:mnesia.write({@record_name, {guild_id, &1.user.id}, &1})
        )
      end)
    end

    @impl PresenceCache
    @doc "Retrieve a query handle for the table."
    @doc since: "0.8.0"
    @spec query_handle :: :qlc.query_handle()
    def query_handle do
      # Note: :"$1" holds a pair here (`{guild_id, user_id}`).
      ms = [{{:_, :"$1", :"$2"}, [], [{{:"$1", :"$2"}}]}]
      :mnesia.table(@table_name, {:traverse, {:select, ms}})
    end

    @impl PresenceCache
    @doc "Wrap QLC operations in a transaction."
    @doc since: "0.8.0"
    def wrap_qlc(fun) do
      :mnesia.activity(:sync_transaction, fun)
    end
  end
end