# `KeenPhoenixSvelte.Apps.ProxyCache`
[🔗](https://github.com/KeenMate/keen-phoenix-svelte/blob/v1.0.0-rc.9/lib/keen_phoenix_svelte/apps/proxy_cache.ex#L1)

Server-side cache and single-flight refresher for `:proxy`-mode app bundles.

Each fetched bundle is held in a `:public` ETS table keyed by its upstream URL,
together with its validators (`ETag` / `Last-Modified`), the upstream
`Cache-Control`, and a computed freshness window. A **fresh** read never touches
this process — it's a direct, concurrent ETS read straight from the plug (bundle
bodies are large refc binaries, so ETS shares them by reference rather than
copying), so the hot path stays as fast as a static file. Only a **stale** (or
missing) entry routes through the GenServer, where concurrent refreshes for the
same URL are collapsed into a single upstream request (no cache stampede).

## Freshness

When an entry goes stale it is **revalidated**, not blindly re-downloaded: the
refresh sends a conditional `GET` (`If-None-Match` / `If-Modified-Since`). A
`304 Not Modified` just bumps the timestamp and keeps the cached bytes; a `200`
replaces them. How long an entry stays fresh (`Apps.proxy_opts/1`):

  * `respect_upstream: true` (default) — the upstream `Cache-Control: max-age`
    (or `s-maxage`) sets the window; `no-cache`/`no-store` forces revalidation
    on every request. When the origin says nothing, the `:ttl` fallback applies.
  * a per-app `immutable: true` pins the entry (never revalidated server-side).
  * a `freshness: fn ctx -> seconds end` config hook overrides everything.

This is what makes the proxy correct for **unversioned** upstreams
(`cdn/app.js` with no version in the path): they can't be cache-busted by URL,
so the proxy polls/revalidates them on the `:ttl` cadence instead.

## Fetching & TLS

The built-in fetcher is Erlang's `:httpc`. Because its bytes end up running
same-origin in users' browsers, the fetch leg is locked down: TLS certificates
are **verified** against the system trust store (`verify: :verify_peer` with
hostname checking) and upstream **redirects are not followed**
(`autoredirect: false`, closing an SSRF path where a 30x could point the fetch
at an internal address). Override the TLS options with `:proxy_cache`
`:ssl_options` (e.g. a custom CA bundle), or replace the client entirely — and
its policy — via the `:app_provider` hook (see `KeenPhoenixSvelte.Apps.Proxy`).

## Memory

Entries are keyed by upstream URL, so a refresh **upserts** in place — the table
never grows from re-fetching. The only growth is *orphaned* URLs: ones cached
before the registry pointed a name at a new URL (or dropped the app). A periodic
sweep (`:proxy_cache` `:sweep_interval`, default 1h; set `false` to disable)
evicts any cached URL no longer in `KeenPhoenixSvelte.Apps.registered/0`, so a
rotating DB-driven registry stays bounded to its current working set.

# `entry`

```elixir
@type entry() :: %{
  body: binary() | nil,
  etag: String.t() | nil,
  last_modified: String.t() | nil,
  cache_control: String.t() | nil,
  fetched_at: integer(),
  fresh_for: non_neg_integer(),
  negative: term()
}
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `get`

```elixir
@spec get(String.t(), map()) :: {:ok, entry()} | {:error, term()}
```

Return a cache entry for `url`, fetching or revalidating upstream if the cached
copy is missing or stale. `opts` is a resolved `Apps.proxy_opts/1` map. On an
upstream error a still-cached (stale) copy is served fail-open.

# `manifest_set`

```elixir
@spec manifest_set(String.t(), map()) ::
  {:ok, MapSet.t(String.t())} | {:error, term()}
```

The set of file paths an app's `manifest` lists (its servable allowlist), fetched
and cached like any bundle and parsed once per content change. `url` is the
resolved manifest source, `opts` the app's `Apps.proxy_opts/1`.

# `max_concurrent_fetches`

```elixir
@spec max_concurrent_fetches() :: pos_integer()
```

Ceiling on concurrent in-flight upstream fetches (`:proxy_cache`
`:max_concurrent_fetches`, default 32). Read at boot by the
`Task.Supervisor` and per-refresh by the GenServer.

# `start_link`

---

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