defmodule Phoenix.ReactServer.Cache do @moduledoc """ High-performance ETS-based caching for React component rendering. This module provides intelligent caching for rendered React components with configurable TTL and automatic cleanup of expired entries. ## Features - **ETS-based Storage**: In-memory caching with O(1) access time - **Configurable TTL**: Time-to-live per cache entry (default: 3600 seconds) - **Automatic Cleanup**: Garbage collection runs every 60 seconds - **Smart Cache Keys**: Based on component name, props, and static flag - **Memory Efficient**: Automatic cleanup prevents memory leaks ## Cache Key Structure Cache keys are tuples: `{component, props, static_flag}` - `component` - The React component name - `props` - Serialized component props - `static_flag` - Whether rendering is static (affects caching strategy) ## Configuration ```elixir config :phoenix_react_ng, Phoenix.ReactServer, cache_ttl: 3600 # Cache TTL in seconds (default: 1 hour) ``` Set `cache_ttl: 0` to disable caching entirely. ## Performance - Cache hit: O(1) lookup time - Cache miss: Triggers component rendering and stores result - Memory usage: Proportional to number of cached components - Cleanup cost: O(n) where n is number of expired entries """ use GenServer alias Telemetry @ets_table_name :react_component_cache @default_ttl Application.compile_env(:phoenix_react_ng, Phoenix.ReactServer, []) |> Keyword.get(:cache_ttl, 3600) def start_link(_) do GenServer.start_link(__MODULE__, nil, name: __MODULE__) end @impl true @spec init(term()) :: {:ok, map()} def init(_) do state = %{} ensure_started() schedule_work() {:ok, state} end @impl true def handle_info(:gc, state) do ts = :os.system_time(:seconds) match_spec = [ {{:"$1", :_, :"$2"}, [{:<, :"$2", ts}], [:"$1"]} ] :ets.select(@ets_table_name, match_spec) |> Enum.each(fn key -> :ets.delete(@ets_table_name, key) end) schedule_work() {:noreply, state} end defp schedule_work do # Every 60 seconds by default gc_time = Application.get_env(:phoenix_react_ng, Phoenix.ReactServer) |> Keyword.get(:gc_time, 60_000) Process.send_after(self(), :gc, gc_time) end @typedoc "Cache key type for component rendering" @type cache_key :: {String.t(), map(), atom()} @typedoc "Cache record stored in ETS" @type cache_record :: {cache_key(), String.t(), integer()} @typedoc "Cache method type" @type cache_method :: :render_to_static_markup | :render_to_string | :render_to_readable_stream @typedoc "TTL in seconds" @type ttl :: non_neg_integer() @doc """ Retrieves a cached rendering result. ## Parameters - `component` - Component name - `props` - Component props - `method` - Rendering method ## Returns - `binary()` - Cached HTML if found and not expired - `nil` - No cached result found or expired ## Example iex> Phoenix.ReactServer.Cache.get("chart", %{"data" => [1,2,3]}, :render_to_string) "