# `PhoenixKit.Cache`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.55.1/lib/phoenix_kit/cache/cache.ex#L1)

Generic caching system for PhoenixKit with ETS-backed storage.

This module provides a flexible caching foundation that can be used for:
- Settings caching
- User roles and permissions
- Module configurations
- Any frequently accessed data

## Features

- ETS-backed storage for high-performance lookups
- Automatic cache warming
- TTL support for expiring data
- Statistics tracking
- Robust fallback mechanisms
- Multiple cache instances via registry

## Usage

    # Start a cache instance
    {:ok, pid} = PhoenixKit.Cache.start_link(name: :my_cache, warmer: &MyApp.load_data/0)

    # Basic operations
    PhoenixKit.Cache.put(:my_cache, "key", "value")
    PhoenixKit.Cache.get(:my_cache, "key", "default")
    PhoenixKit.Cache.invalidate(:my_cache, "key")

    # Batch operations
    PhoenixKit.Cache.get_multiple(:my_cache, ["key1", "key2"], %{"key1" => "default1"})
    PhoenixKit.Cache.invalidate_multiple(:my_cache, ["key1", "key2"])

## Configuration

Cache instances can be configured with:

- `:name` - Unique name for the cache instance
- `:warmer` - Function to warm the cache on startup
- `:ttl` - Time-to-live for cache entries (optional)
- `:max_size` - Maximum number of entries (optional)

# `cache_key`

```elixir
@type cache_key() :: any()
```

# `cache_name`

```elixir
@type cache_name() :: atom()
```

# `cache_value`

```elixir
@type cache_value() :: any()
```

# `default_value`

```elixir
@type default_value() :: any()
```

# `options`

```elixir
@type options() :: [
  name: cache_name(),
  warmer: warmer_fun(),
  ttl: pos_integer() | nil,
  max_size: pos_integer() | nil
]
```

# `warmer_fun`

```elixir
@type warmer_fun() :: (-&gt; map() | nil)
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear`

```elixir
@spec clear(cache_name()) :: :ok
```

Clears all entries from the cache.

## Examples

    PhoenixKit.Cache.clear(:settings)

# `clear_by_prefix`

```elixir
@spec clear_by_prefix(cache_name(), String.t()) ::
  {:ok, non_neg_integer()} | {:error, any()}
```

Clears all cache entries whose keys start with the given prefix.

Returns the count of entries cleared.

## Examples

    PhoenixKit.Cache.clear_by_prefix(:publishing_posts, "v1:publishing_post:my-blog:")
    # => {:ok, 15}

# `get`

```elixir
@spec get(cache_name(), cache_key(), default_value()) :: cache_value()
```

Gets a value from the cache.

Returns the default value if the key is not found or the cache is unavailable.

## Examples

    PhoenixKit.Cache.get(:settings, "date_format", "Y-m-d")
    PhoenixKit.Cache.get(:user_roles, user_uuid, [])

# `get_multiple`

```elixir
@spec get_multiple(cache_name(), [cache_key()], map()) :: map()
```

Gets multiple values from the cache.

Returns a map with the requested keys and their values, using defaults for missing keys.

## Examples

    defaults = %{"date_format" => "Y-m-d", "time_format" => "H:i"}
    PhoenixKit.Cache.get_multiple(:settings, ["date_format", "time_format"], defaults)

# `get_multiple_with_generation`

```elixir
@spec get_multiple_with_generation(cache_name(), [cache_key()], map()) ::
  {map(), non_neg_integer() | nil}
```

`get_multiple/3`, plus the generation — see `get_with_generation/3`.

# `get_with_generation`

```elixir
@spec get_with_generation(cache_name(), cache_key(), default_value()) ::
  {cache_value(), non_neg_integer() | nil}
```

`get/3`, plus the cache's generation at the moment of the read — for a
caller that fills a miss from the database. Pass it to `put/4` as
`if_generation:`.

A miss-fill races every write: the reader misses, reads the old row, and
the writer commits and invalidates before the reader's `put` arrives. That
`put` would bring the old value back after the invalidation, and it would
stay until its TTL. Carrying the generation drops it instead.

The generation is `nil` when the cache could not answer; a `put/4` with
`if_generation: nil` writes nothing.

# `invalidate`

```elixir
@spec invalidate(cache_name(), cache_key()) :: :ok
```

Invalidates a key in the cache.

## Examples

    PhoenixKit.Cache.invalidate(:settings, "date_format")

# `invalidate_multiple`

```elixir
@spec invalidate_multiple(cache_name(), [cache_key()]) :: :ok
```

Invalidates multiple keys in the cache.

## Examples

    PhoenixKit.Cache.invalidate_multiple(:settings, ["date_format", "time_format"])

# `invalidate_now`

```elixir
@spec invalidate_now(cache_name(), [cache_key()]) :: :ok
```

Invalidates `keys` and returns only once they are gone.

`invalidate/2` and `invalidate_multiple/2` are casts: the caller moves on
while the cache process still holds the old values. That is fine for a
write nobody is watching, and wrong for one that is about to be announced —
a subscriber that reacts to "this setting changed" by reading it again
would get the value from before the change. Use this when a notification
follows.

Never turns a successful write into a failure: a slow, dead or missing
cache process is logged and reported as `:ok`, and the entry expires on its
TTL like any other.

# `put`

```elixir
@spec put(cache_name(), cache_key(), cache_value(), keyword()) :: :ok
```

Puts a value in the cache.

With `if_generation: generation` (from `get_with_generation/3`), the value
is written only if nothing has been invalidated since that read — the form
a miss-fill must use. Without it the write is unconditional.

With `expires_in: ms` the entry expires exactly that many milliseconds
from now, instead of after the cache's own TTL (and without its jitter —
a calendar boundary must not be overshot).

## Examples

    PhoenixKit.Cache.put(:settings, "date_format", "m/d/Y")
    PhoenixKit.Cache.put(:user_roles, user_uuid, ["admin", "user"])

# `put_multiple`

```elixir
@spec put_multiple(cache_name(), map(), keyword()) :: :ok
```

Puts multiple values in the cache. Takes `if_generation:` like `put/4`.

## Examples

    PhoenixKit.Cache.put_multiple(:settings, %{"date_format" => "m/d/Y", "time_format" => "h:i A"})

# `remember`

```elixir
@spec remember(cache_name(), cache_key(), (-&gt; cache_value()), keyword()) ::
  cache_value()
```

Returns the cached value for `key`, or runs `fun`, caches its result and
returns it — get-or-compute.

## Options

  * `:until` — when the computed value expires:
    * an integer: that many milliseconds from now;
    * `{:end_of_minute | :end_of_hour | :end_of_day, time_zone}`: the next
      such boundary on the wall clock of `time_zone` (an IANA name, e.g.
      `"Europe/Tallinn"`) — "valid for this clock minute / hour / day, then
      dead", daylight-saving changes included;
    * omitted: the cache's own TTL.

The fill is generation-checked: if the key (or the cache) is invalidated
while `fun` runs, its result is returned to this caller but not stored.

There is no single-flight lock: when a key expires, every process that
misses before the first fill lands runs `fun`. Keep `fun` free of side
effects, and put anything expensive behind its own guard if that matters.
A cached `nil` counts as a hit. An error — `:error` or `{:error, _}` — is
returned but never stored, so one failed load (a timeout) is not served
until the TTL runs out.

## Example

    Cache.remember(:prices, {:day, zone}, fn -> load_prices(zone) end,
      until: {:end_of_day, "Europe/Tallinn"}
    )

# `start_link`

```elixir
@spec start_link(options()) :: GenServer.on_start()
```

Starts a new cache instance.

## Options

- `:name` - Required. Unique name for the cache instance
- `:warmer` - Optional. Function to warm the cache on startup
- `:ttl` - Optional. Time-to-live for cache entries in milliseconds
- `:max_size` - Optional. Maximum number of entries before eviction

## Examples

    {:ok, pid} = PhoenixKit.Cache.start_link(name: :settings)
    {:ok, pid} = PhoenixKit.Cache.start_link(name: :user_roles, warmer: &MyApp.load_user_roles/0)

# `stats`

```elixir
@spec stats(cache_name()) :: map()
```

Gets cache statistics.

## Examples

    PhoenixKit.Cache.stats(:settings)
    # => %{hits: 150, misses: 5, puts: 20, invalidations: 3, hit_rate: 0.97}

# `warm`

```elixir
@spec warm(cache_name()) :: :ok
```

Warms the cache using the configured warmer function.

## Examples

    PhoenixKit.Cache.warm(:settings)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
