Current section

Files

Jump to
frontier lib frontier store.ex
Raw

lib/frontier/store.ex

defmodule Frontier.Store do
@moduledoc "ETS-backed runtime store for Frontier config: root, contexts, ignored modules, reclassifications, and tracer references."
@root_table :frontier_root
@contexts_table :frontier_contexts
@ignored_table :frontier_ignored
@reclassified_table :frontier_reclassified
@references_table :frontier_references
@initialized_table :frontier_initialized
@set_tables [
@root_table,
@contexts_table,
@ignored_table,
@reclassified_table,
@initialized_table
]
@tables [@references_table | @set_tables]
# ── Lifecycle ────────────────────────────────────────────────────────
@doc "Creates ETS tables if they don't exist."
@spec init :: :ok
def init do
for table <- @set_tables do
safe_create_table(table, [
:set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: true
])
end
safe_create_table(@references_table, [
:duplicate_bag,
:public,
:named_table,
write_concurrency: true
])
:ok
end
@doc "Wipes all ETS tables."
@spec reset :: :ok
def reset do
init()
for table <- @tables do
:ets.delete_all_objects(table)
end
:ok
end
@doc "Loads Frontier config from compiled BEAM files if ETS is empty."
@spec ensure_loaded :: :ok
def ensure_loaded do
init()
if contexts() == %{} do
load_from_modules()
end
end
@doc "Scans compiled BEAM files for persisted `frontier_opts` attributes and registers them."
@spec load_from_modules :: :ok
def load_from_modules do
init()
for module <- all_project_modules(),
attrs = module_frontier_opts(module),
attrs != nil do
case Frontier.Definition.parse_opts(module, attrs) do
{:root, config} ->
register_root(module, config)
for ignored_module <- config.ignore do
register_ignored(ignored_module)
end
{:context, config} ->
register_context(module, config)
{:ignored, mod} ->
register_ignored(mod)
{:reclassified, target} ->
register_reclassified(module, target)
end
end
:ok
end
# ── Root ─────────────────────────────────────────────────────────────
@doc "Registers the root module config."
@spec register_root(module(), map()) :: true
def register_root(module, config) do
:ets.insert(@root_table, {:root, module, config})
end
@doc "Returns the root module and its config, or nil."
@spec root :: {module(), map()} | nil
def root do
case :ets.lookup(@root_table, :root) do
[{:root, module, config}] -> {module, config}
[] -> nil
end
end
# ── Contexts ─────────────────────────────────────────────────────────
@doc "Registers a context module and its config."
@spec register_context(module(), map()) :: true
def register_context(module, config) do
:ets.insert(@contexts_table, {module, config})
end
@doc "Returns the config for a context module, or nil."
@spec context(module()) :: map() | nil
def context(module) do
case :ets.lookup(@contexts_table, module) do
[{^module, config}] -> config
[] -> nil
end
end
@doc "Returns all registered contexts as a map of module => config."
@spec contexts :: %{module() => map()}
def contexts do
@contexts_table
|> :ets.tab2list()
|> Map.new()
end
# ── Ignored ──────────────────────────────────────────────────────────
@doc "Registers a module as ignored."
@spec register_ignored(module()) :: true
def register_ignored(module) do
:ets.insert(@ignored_table, {module})
end
@doc "Returns true if the module is ignored."
@spec ignored?(module()) :: boolean()
def ignored?(module) do
case :ets.lookup(@ignored_table, module) do
[{^module}] -> true
[] -> false
end
end
@doc "Returns all ignored modules as a MapSet."
@spec all_ignored :: MapSet.t(module())
def all_ignored do
@ignored_table
|> :ets.tab2list()
|> Enum.map(fn {mod} -> mod end)
|> MapSet.new()
end
# ── Reclassified ────────────────────────────────────────────────────
@doc "Registers a module as reclassified to a different context."
@spec register_reclassified(module(), module()) :: true
def register_reclassified(module, target_context) do
:ets.insert(@reclassified_table, {module, target_context})
end
@doc "Returns the target context for a reclassified module, or nil."
@spec reclassified_to(module()) :: module() | nil
def reclassified_to(module) do
case :ets.lookup(@reclassified_table, module) do
[{^module, target}] -> target
[] -> nil
end
end
@doc "Returns all reclassified modules as a map of module => target context."
@spec all_reclassified :: %{module() => module()}
def all_reclassified do
@reclassified_table
|> :ets.tab2list()
|> Map.new()
end
# ── References ───────────────────────────────────────────────────────
@doc "Records a cross-module reference from the tracer."
@spec record_reference(module(), map()) :: true
def record_reference(from, reference) do
:ets.insert(@references_table, {from, reference})
end
@doc "Returns all recorded references with `:from` key injected."
@spec references :: [map()]
def references do
@references_table
|> :ets.tab2list()
|> Enum.map(fn {from, ref} -> Map.put(ref, :from, from) end)
end
# ── Module initialization ───────────────────────────────────────────
@doc "Marks a module as initialized, clearing its previous references. Idempotent per compilation."
@spec initialize_module(module()) :: :ok
def initialize_module(module) do
case :ets.lookup(@initialized_table, module) do
[{^module}] ->
:ok
[] ->
:ets.insert(@initialized_table, {module})
:ets.delete(@references_table, module)
end
end
@doc "Resets the initialized tracking table."
@spec reset_initialized :: :ok | true
def reset_initialized do
if :ets.info(@initialized_table) != :undefined do
:ets.delete_all_objects(@initialized_table)
end
end
# ── Private ──────────────────────────────────────────────────────────
defp safe_create_table(name, opts) do
if :ets.info(name) == :undefined do
:ets.new(name, opts)
end
rescue
ArgumentError -> :ok
end
defp all_project_modules do
Mix.Project.compile_path()
|> Path.join("*.beam")
|> Path.wildcard()
|> Enum.map(fn beam ->
beam
|> Path.basename(".beam")
|> String.to_atom()
end)
end
defp module_frontier_opts(module) do
if Code.ensure_loaded?(module) and function_exported?(module, :__info__, 1) do
attrs = module.__info__(:attributes)
if Keyword.has_key?(attrs, :frontier_opts) do
Keyword.get(attrs, :frontier_opts)
end
end
rescue
_ -> nil
end
end