# `Bluez.DeviceCache`
[🔗](https://github.com/bbangert/bluez/blob/v0.1.0/lib/bluez/device_cache.ex#L1)

Pure per-device advertisement cache with emit-gating and a bounded size.
No I/O — `Bluez.Client` owns the side effects (it emits the
adverts this module returns).

Each device path maps to `%{props, last_raw, last_emit, last_seen}`. On
`upsert/4` the new props are merged, the advert is reconstructed
(`Bluez.Advert`), and an advert is returned to emit only when:

  * it's the first sighting, or
  * the reconstructed AD payload changed (sensor data update), or
  * the heartbeat interval has elapsed (keeps RSSI/last-seen fresh in HA
    without forwarding every PDU).

The map is capped at `@max_devices` (LRU by `last_seen`) so a device spraying
randomized MACs — each a new BlueZ object path — can't grow it without bound
(BlueZ's `InterfacesRemoved` is the only other prune and is outside our
control).

# `t`

```elixir
@type t() :: %Bluez.DeviceCache{devices: %{optional(String.t()) =&gt; map()}}
```

# `emit?`

```elixir
@spec emit?(binary(), binary() | nil, integer() | nil, integer(), integer()) ::
  boolean()
```

Emit decision (pure). Emit on first sight (`last_raw == nil`), on a payload
change, or once the heartbeat interval has elapsed.

# `new`

```elixir
@spec new() :: t()
```

An empty cache.

# `remove`

```elixir
@spec remove(t(), String.t()) :: t()
```

Drop `path`'s entry (BlueZ emitted `InterfacesRemoved` for it).

# `seen_within`

```elixir
@spec seen_within(t(), integer(), pos_integer()) :: non_neg_integer()
```

Distinct devices seen within the last `window_ms` of monotonic `now_ms` —
the web tab's "devices (15 min)" stat. A plain scan bounded by the LRU
cap, so it's cheap enough to run per stats tick.

Note the cap also bounds the answer: with > `512` active
devices the count saturates at the cap (eviction forgets the oldest).

# `size`

```elixir
@spec size(t()) :: non_neg_integer()
```

Number of device entries currently cached.

# `upsert`

```elixir
@spec upsert(t(), String.t(), map(), integer()) :: {t(), [Bluez.Advert.advert()]}
```

Merge `new_props` for `path` at monotonic `now_ms`. Returns
`{cache, adverts}` where `adverts` is `[]` or a single reconstructed advert
the caller should emit.

---

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