PhiaUi.ClassMerger.Cache (phia_ui v0.1.17)

Copy Markdown View Source

ETS-backed cache for memoising resolved Tailwind class strings.

Stores {input_key, resolved_string} pairs so that identical lists of class tokens are merged only once per application lifetime. This is the performance backbone of PhiaUi.ClassMerger.cn/1: the first call for a given input list runs the full resolution pipeline; every subsequent call with the same input is a direct ETS read.

Why GenServer as Table Owner?

ETS tables are owned by the Erlang process that created them. If the owning process exits, the table is garbage-collected and all cached data is lost. Without a long-lived owner, every application restart (or crash) would destroy the cache.

PhiaUi.ClassMerger.Cache is a GenServer whose sole initialisation side-effect is creating the ETS table. It holds no other meaningful state (the GenServer state is an empty list []). The GenServer is supervised by PhiaUi.Supervisor, so it is restarted automatically on failure, and the ETS table is recreated on restart.

Why :public + Direct ETS Calls for Reads and Writes?

Routing all reads and writes through the GenServer's handle_call/3 would create a sequential bottleneck: every LiveView process would have to queue behind one another waiting for the GenServer to process their request.

Instead the table is created as :public, which allows any process to call :ets.lookup/2 and :ets.insert/2 directly — bypassing the GenServer message queue entirely. The GenServer only needs to be involved at initialisation time to create the table. After that it exists purely to keep the table alive.

Why read_concurrency: true?

cn/1 is called far more often than it writes (the cache is warm after the first few renders). The :read_concurrency option switches ETS to an internal implementation that uses a lock-free read path optimised for this workload. Multiple LiveView processes can read the same key simultaneously without any serialisation.

Table Configuration

OptionValueReason
:named_table(flag)Allows lookup by module name without a table
reference — any process can call the module
name as if it were an ETS table identifier.
:public(flag)Any process may read and write without going
through the owning GenServer.
read_concurrencytrueLock-free read path for high read / low write
workloads.

The default ETS table type (:set) is used, giving O(1) key lookup via hashing. Each {key, value} pair is unique by key.

Examples

iex> PhiaUi.ClassMerger.Cache.get(["px-4", "px-2"])
nil

iex> PhiaUi.ClassMerger.Cache.put(["px-4", "px-2"], "px-2")
"px-2"

iex> PhiaUi.ClassMerger.Cache.get(["px-4", "px-2"])
"px-2"

Summary

Functions

Returns a specification to start this module under a supervisor.

Returns the cached merged class string for key, or nil if absent.

Stores value under key and returns value.

Starts the cache GenServer and registers it under its module name.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

get(key)

@spec get(term()) :: String.t() | nil

Returns the cached merged class string for key, or nil if absent.

Reads directly from ETS — no GenServer message is sent. Safe to call from any process concurrently.

Examples

iex> PhiaUi.ClassMerger.Cache.get(["bg-red-500", "bg-blue-500"])
nil  # cache miss on first call

put(key, value)

@spec put(term(), String.t()) :: String.t()

Stores value under key and returns value.

Writes directly to ETS via :ets.insert/2 — no GenServer message is sent. Because the table is :public, any process can write without serialising through the GenServer. In the rare case of two processes computing the same key concurrently, the last writer wins — both will produce the same string, so this is safe.

Examples

iex> PhiaUi.ClassMerger.Cache.put(["text-sm", "text-lg"], "text-lg")
"text-lg"

start_link(opts)

Starts the cache GenServer and registers it under its module name.

The GenServer's init/1 callback creates the ETS table immediately, so the table is available as soon as start_link/1 returns. Called by PhiaUi.Supervisor during application startup.