defmodule RNS.DestinationStore do @moduledoc """ Manages an ets table that holds the paths to destinations(including local ones). """ require Record def child_spec(_args) do %{ id: __MODULE__, start: {__MODULE__, :start_link, []} } end def start_link() do pid = spawn_link(__MODULE__, :init, []) {:ok, pid} end # Client @doc "Find a destination in the store using its hash." @spec fetch(RNS.Destination.hash()) :: {:ok, RNS.Destination.t()} | {:error, :not_found} def fetch(destination_hash) do case :ets.lookup(:destinations, destination_hash) do [result] -> if RNS.Destination.expired?(result) do delete(destination_hash) {:error, :not_found} else {:ok, result} end [] -> {:error, :not_found} end end @doc "Put a destination into the store." @spec put(RNS.Destination.t()) :: true def put(destination) do :ets.insert(:destinations, destination) end @doc "Delete a destination from the store using its hash." @spec delete(RNS.Destination.hash()) :: true def delete(destination_hash) do :ets.delete(:destinations, destination_hash) end @doc "Put a ratchet into the store." @spec put_ratchet(RNS.Identity.ratchet(), RNS.Destination.hash()) :: true def put_ratchet(ratchet, destination_hash) do :ets.insert( :ratchets, {destination_hash, ratchet, NaiveDateTime.add(NaiveDateTime.utc_now(), 30, :day)} ) end @doc "Fetch all ratchets from the store." @spec fetch_ratchets(RNS.Destination.hash()) :: [RNS.Identity.ratchet()] def fetch_ratchets(destination_hash) do ratchets = :ets.lookup(:ratchets, destination_hash) Enum.reduce(ratchets, [], fn ratchet, ratchets -> if RNS.Destination.ratchet_expired?(ratchet) do delete_ratchet(elem(ratchet, 1)) ratchets else [elem(ratchet, 1) | ratchets] end end) end @doc "Fetch the most recent ratchet from the store." @spec fetch_recent_ratchet(RNS.Destination.hash()) :: {:ok, RNS.Identity.ratchet()} | {:error, :not_found} def fetch_recent_ratchet(destination_hash) do ratchets = :ets.lookup(:ratchets, destination_hash) most_recent_ratchet = Enum.reduce(ratchets, nil, fn ratchet, most_recent_ratchet -> if RNS.Destination.ratchet_expired?(ratchet) do delete_ratchet(elem(ratchet, 1)) most_recent_ratchet else if most_recent_ratchet == nil or NaiveDateTime.after?(elem(ratchet, 2), elem(most_recent_ratchet, 2)) do ratchet else most_recent_ratchet end end end) case most_recent_ratchet do nil -> {:error, :not_found} _ -> {:ok, elem(most_recent_ratchet, 1)} end end @doc "Delete a ratchet from the store." @spec delete_ratchet(RNS.Identity.ratchet()) :: true def delete_ratchet(ratchet) do :ets.match_delete(:ratchets, {:_, ratchet, :_}) end # Callbacks @spec init() :: :ok def init() do :ets.new(:destinations, [ {:read_concurrency, true}, {:write_concurrency, true}, {:keypos, RNS.Destination.hash_position() + 1}, :public, :named_table ]) :ets.new(:ratchets, [ {:read_concurrency, true}, {:write_concurrency, true}, :bag, :public, :named_table ]) Process.sleep(:infinity) end end