defmodule Kameleoon.Native.Events do
@moduledoc false
use GenServer
require Logger
alias Kameleoon.Client
alias Kameleoon.Error
@type client_key :: {String.t(), String.t() | nil}
@type state :: %{
handlers: %{client_key() => (() -> any())},
logger: Kameleoon.Logger.logger() | nil
}
@spec start(keyword()) :: GenServer.on_start()
def start(opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
opts = Keyword.put(opts, :name, name)
GenServer.start(__MODULE__, opts, name: name)
end
@spec ensure_started(GenServer.server()) :: {:ok, pid()} | {:error, Error.t()}
def ensure_started(server \\ __MODULE__)
def ensure_started(server) when is_pid(server), do: {:ok, server}
def ensure_started(server) when is_atom(server) do
case Process.whereis(server) do
nil ->
case start(name: server) do
{:ok, pid} ->
{:ok, pid}
{:error, {:already_started, pid}} ->
{:ok, pid}
{:error, reason} ->
{:error, %Error{message: "failed to start native events: #{inspect(reason)}"}}
end
pid ->
{:ok, pid}
end
end
def ensure_started(server) do
case GenServer.whereis(server) do
nil -> {:error, %Error{message: "native events process is not started: #{inspect(server)}"}}
pid -> {:ok, pid}
end
end
@spec set_logger(Kameleoon.Logger.logger() | nil, GenServer.server()) ::
{:ok, pid()} | {:error, Error.t()}
def set_logger(logger, server \\ __MODULE__) do
with {:ok, pid} <- ensure_started(server),
:ok <- GenServer.call(server, {:set_logger, logger}) do
{:ok, pid}
end
end
@spec forget(String.t(), String.t() | nil, GenServer.server()) :: :ok | {:error, Error.t()}
def forget(site_code, environment, server \\ __MODULE__) do
with {:ok, _pid} <- ensure_started(server) do
GenServer.call(server, {:forget, site_code, environment})
end
end
@spec set_handler(Client.t(), (() -> any()) | nil, GenServer.server()) ::
:ok | {:error, Error.t()}
def set_handler(client, fun, server \\ __MODULE__) when is_function(fun, 0) or is_nil(fun) do
with {:ok, _pid} <- ensure_started(server) do
GenServer.call(server, {:set_handler, client_key(client), fun})
end
end
@impl true
def init(_opts) do
{:ok, %{handlers: %{}, logger: nil}}
end
@impl true
def handle_call({:set_logger, logger}, _from, state) do
{:reply, :ok, %{state | logger: logger}}
end
def handle_call({:forget, site_code, nil}, _from, state) do
keys =
state.handlers
|> Map.keys()
|> Enum.filter(fn {tracked_site, _env} -> tracked_site == site_code end)
{:reply, :ok, forget_keys(state, keys)}
end
def handle_call({:forget, site_code, environment}, _from, state) do
{:reply, :ok, forget_keys(state, [{site_code, environment}])}
end
def handle_call({:set_handler, key, nil}, _from, state) do
handlers = Map.delete(state.handlers, key)
{:reply, :ok, %{state | handlers: handlers}}
end
def handle_call({:set_handler, key, fun}, _from, state) do
{:reply, :ok, %{state | handlers: Map.put(state.handlers, key, fun)}}
end
@impl true
def handle_info({:datafile_updated, site_code, environment}, state) do
{:noreply, dispatch_datafile_updated(state, {site_code, environment})}
end
def handle_info({:kameleoon_log, level, message}, state) do
dispatch_log(state.logger, level, message)
{:noreply, state}
end
def handle_info(message, state) do
Logger.warning("ignoring unknown native event message: #{inspect(message)}")
{:noreply, state}
end
defp dispatch_datafile_updated(state, key) do
if handler = Map.get(state.handlers, key) do
Task.start(fn ->
try do
handler.()
rescue
error ->
Logger.warning("configuration update handler crashed: #{Exception.message(error)}")
end
end)
end
state
end
defp dispatch_log(nil, _level, _message), do: :ok
defp dispatch_log(module, level, message), do: safe_log(fn -> module.log(level, message) end)
defp safe_log(fun) do
fun.()
rescue
error ->
Logger.warning("kameleoon logger callback crashed: #{Exception.message(error)}")
catch
kind, reason ->
Logger.warning("kameleoon logger callback exited: #{inspect({kind, reason})}")
end
defp forget_keys(state, keys) do
Enum.reduce(keys, state, fn key, state ->
%{
state
| handlers: Map.delete(state.handlers, key)
}
end)
end
defp client_key(%Client{site_code: site_code, environment: environment}),
do: {site_code, environment}
end