defmodule IO.ANSI.Table.SpecServer do
@moduledoc """
A server process that holds a table `spec` struct as its state.
"""
use GenServer, restart: :transient
use PersistConfig
alias __MODULE__
alias IO.ANSI.Table.{Log, Spec}
@ets get_env(:ets_name)
@reg get_env(:registry)
@wait 50
@doc """
Spawns a new table spec server process to be registered via a `spec name`.
"""
@spec start_link(Spec.t()) :: GenServer.on_start()
def start_link(spec) do
GenServer.start_link(SpecServer, spec, name: via(spec.spec_name))
end
@doc """
Allows to register or look up a table spec server process via a `spec name`.
"""
@spec via(String.t()) :: {:via, Registry, tuple}
def via(spec_name), do: {:via, Registry, {@reg, key(spec_name)}}
## Private functions
@spec key(String.t()) :: tuple
defp key(spec_name), do: {SpecServer, spec_name}
@spec extend(Spec.t()) :: Spec.t()
defp extend(spec) do
case :ets.lookup(@ets, key(spec.spec_name)) do
[] ->
:ok = Log.info(:spawned, {spec})
Spec.extend(spec) |> save()
[{_key, spec}] ->
:ok = Log.info(:restarted, {spec})
spec
end
end
@spec save(Spec.t()) :: Spec.t()
defp save(spec) do
:ok = Log.info(:save, {spec, __ENV__})
true = :ets.insert(@ets, {key(spec.spec_name), spec})
spec
end
## Callbacks
@spec init(Spec.t()) :: {:ok, Spec.t()}
def init(spec), do: {:ok, extend(spec)}
@spec handle_cast(term, Spec.t()) :: {:noreply, Spec.t()}
def handle_cast({:write, maps, options} = request, spec) do
:ok = Log.info(:handle_cast, {spec, request, __ENV__})
:ok = Spec.write_table(maps, spec, options)
{:noreply, spec}
end
@spec handle_call(term, GenServer.from(), Spec.t()) ::
{:reply, :ok | Spec.t(), Spec.t()}
def handle_call({:format, maps, options} = request, {from_pid, _tag}, spec) do
group_leader = Process.info(from_pid)[:group_leader]
if group_leader, do: self() |> Process.group_leader(group_leader)
:ok = Log.info(:handle_call, {spec, request, __ENV__})
:ok = Spec.write_table(maps, spec, options)
{:reply, :ok, spec}
end
def handle_call(:get = request, _from, spec) do
:ok = Log.info(:handle_call, {spec, request, __ENV__})
{:reply, spec, spec}
end
@spec terminate(term, Spec.t()) :: :ok
def terminate(:shutdown = reason, spec) do
:ok = Log.info(:terminate, {reason, spec, __ENV__})
true = :ets.delete(@ets, key(spec.spec_name))
# Ensure message logged before exiting...
Process.sleep(@wait)
end
def terminate(reason, spec) do
:ok = Log.error(:terminate, {reason, spec, __ENV__})
true = :ets.delete(@ets, key(spec.spec_name))
# Ensure message logged before exiting...
Process.sleep(@wait)
end
end