defmodule Lexical.ProcessCache do
@moduledoc """
A simple cache with a timeout that lives in the process dictionary
"""
defmodule Entry do
@moduledoc false
defstruct [:value, :expiry]
def new(value, timeout_ms) do
expiry_ts = now_ts() + timeout_ms
%__MODULE__{value: value, expiry: expiry_ts}
end
def valid?(%__MODULE__{} = entry) do
now_ts() < entry.expiry
end
defp now_ts do
System.os_time(:millisecond)
end
end
@type key :: term()
@type fetch_result :: {:ok, term()} | :error
@doc """
Retrieves a value from the cache
If the value is not found, the default is returned
"""
@spec get(key()) :: term() | nil
@spec get(key(), term()) :: term() | nil
def get(key, default \\ nil) do
case fetch(key) do
{:ok, val} -> val
:error -> default
end
end
@doc """
Retrieves a value from the cache
If the value is not found, the default is returned
"""
@spec fetch(key()) :: fetch_result()
def fetch(key) do
case Process.get(key, :unset) do
%Entry{} = entry ->
if Entry.valid?(entry) do
{:ok, entry.value}
else
Process.delete(key)
:error
end
:unset ->
:error
end
end
def clear_keys do
keys()
|> MapSet.put(all_keys_key())
|> Enum.each(&Process.delete/1)
end
@doc """
Retrieves and optionally sets a value in the cache.
Trans looks up a value in the cache under key. If that value isn't
found, the compute_fn is then executed, and its return value is set
in the cache. The cached value will live in the cache for `timeout`
milliseconds
"""
def trans(key, timeout_ms \\ 5000, compute_fn) do
case fetch(key) do
:error ->
set(key, timeout_ms, compute_fn)
{:ok, result} ->
result
end
end
defmacro with_cleanup(do: block) do
quote do
try do
unquote(block)
after
unquote(__MODULE__).clear_keys()
end
end
end
defp set(key, timeout_ms, compute_fn) do
value = compute_fn.()
add_key(key)
Process.put(key, Entry.new(value, timeout_ms))
value
end
defp add_key(key) do
updated_keys = MapSet.put(keys(), key)
Process.put(all_keys_key(), updated_keys)
end
defp all_keys_key do
{__MODULE__, :all_keys}
end
defp keys do
Process.get(all_keys_key(), MapSet.new())
end
end