defmodule Microprints.MicroprintCache do @moduledoc """ GenServer that caches microprint visualizations in ETS. Subscribes to a PubSub topic and invalidates cache entries when files change via LiveReload (or any other file watcher). ## Configuration The PubSub module and topic can be configured via `Application` environment: config :microprints, pubsub: MyApp.PubSub, pubsub_topic: "dev_tools_files" Defaults to `Phoenix.PubSub` and `"dev_tools_files"`. """ use GenServer alias Microprints.Microprint @table_name :microprint_cache @live_reload_topic "dev_tools_files" # Client API @doc """ Starts the MicroprintCache GenServer. ## Options * `:pubsub` - The PubSub module to use (defaults to `Phoenix.PubSub`) * `:pubsub_topic` - The PubSub topic to subscribe to (defaults to `"dev_tools_files"`) """ def start_link(opts \\ []) do pubsub = opts[:pubsub] || Application.get_env(:microprints, :pubsub, Phoenix.PubSub) pubsub_topic = opts[:pubsub_topic] || Application.get_env(:microprints, :pubsub_topic, @live_reload_topic) GenServer.start_link(__MODULE__, %{pubsub: pubsub, pubsub_topic: pubsub_topic}, name: __MODULE__) end @doc """ Returns the microprint for the given file path. Returns cached version if available, otherwise generates and caches it. """ @spec get_microprint(String.t()) :: {:ok, Microprint.microprint()} | {:error, term()} def get_microprint(path) do absolute_path = Path.expand(path) case :ets.lookup(@table_name, absolute_path) do [{^absolute_path, microprint}] -> {:ok, microprint} [] -> case Microprint.generate(absolute_path) do {:ok, microprint} -> :ets.insert(@table_name, {absolute_path, microprint}) {:ok, microprint} error -> error end end end @doc """ Invalidates the cache entry for the given path. """ @spec invalidate(String.t()) :: true def invalidate(path) do absolute_path = Path.expand(path) :ets.delete(@table_name, absolute_path) end @doc """ Clears all cached microprints. """ @spec clear_all() :: true def clear_all do :ets.delete_all_objects(@table_name) end # Server callbacks @impl true def init(%{pubsub: pubsub, pubsub_topic: pubsub_topic}) do table = :ets.new(@table_name, [:named_table, :public, :set, read_concurrency: true]) Phoenix.PubSub.subscribe(pubsub, pubsub_topic) {:ok, %{table: table, pubsub_topic: pubsub_topic}} end @impl true def handle_info({:phoenix_live_reload, _topic, path}, state) do invalidate(path) {:noreply, state} end def handle_info(_msg, state) do {:noreply, state} end end