# cache-mod v0.4.0 — TTL eviction policy

**The third policy lands.** 0.2.0 set the API surface; 0.3.0 added LFU; this release adds `TtlCache<K, V>` — bounded capacity with per-entry time-to-live and lazy expiry. Purely additive: every 0.3.0 call-site compiles and behaves identically against 0.4.0.

## What is cache-mod?

High-performance in-process caching for Rust with multiple eviction policies (LRU, LFU, TTL now; TinyLFU and size-bounded next). Async-safe (`&self` everywhere, `Send + Sync` cache instances), lock-minimized internals. Typed key-value API. No dependency on any external store.

## What's in 0.4.0

- **`TtlCache<K, V>`.** Bounded, thread-safe cache with per-entry time-to-live. Each entry is stamped with a deadline at insert time. On every access ([`get`], [`contains_key`], [`len`]) any entry whose deadline has passed is removed lazily — there is no background thread. Both [`insert`] and [`insert_with_ttl`] reset the deadline on the affected entry; writes always re-arm the timer. On capacity overflow the entry with the **soonest expiration** is evicted, which naturally prefers already-expired entries over live ones — calls do not waste a live entry while a stale one sits in the map.

  Two constructors mirror the rest of the family: `TtlCache::new(usize, Duration) -> Result<Self, CacheError>` (fallible, rejects zero capacity) and `TtlCache::with_capacity(NonZeroUsize, Duration) -> Self` (infallible). The `Duration` argument is the **default** TTL applied to plain `insert`.

  Same `&self`-everywhere shape and poison-tolerant `Mutex` as `LruCache` / `LfuCache`. `TtlCache<K, V>` is `Send + Sync` when `K, V: Send`. No new external dependencies.

- **`insert_with_ttl(&self, key, value, ttl)`.** Per-call TTL that overrides the cache default for one entry — useful when most entries share a TTL but a few need a different lifetime (short-lived flash data, long-lived background results).

- **Re-inserting an expired entry returns `None`, not the stale value.** Surprised me when I traced it, but it's the right call: from the user's perspective an expired entry is gone, so an `insert` that lands on a stale-but-not-yet-cleaned slot must read as a fresh insert. Covered by `ttl_insert_returns_none_when_overwriting_expired_entry`.

- **Overflow guard.** `compute_deadline` uses `Instant::checked_add` and falls back to a ~100-year deadline if the TTL would push the deadline past `Instant`'s representable range. No panics on absurd `Duration::MAX`-style inputs.

- **13 new integration tests.** Zero-capacity rejection, in-window get, lazy expiry through `get` / `contains_key` / `len`, per-call TTL override, soonest-expiry-first eviction, preference for already-expired entries over live ones, stale-as-absent on overwrite, live-update returning the old value, removal, clear, and `Send + Sync` static-assertion. 4 new doctests on the type + both constructors + `insert_with_ttl`. Tests use `Duration::from_millis(1)` TTLs with 10ms sleeps — 10× margin keeps them reliable on slow CI runners; total added test time is ~70ms.

## What's not in 0.4.0

By design, deferred to subsequent minors:

- **Clock injection.** Tests currently use real-time sleeps. A `Clock` abstraction with a `ManualClock` test impl is a candidate for 0.5.0 if the testability cost becomes painful — it is additive.
- **TinyLFU and `SizedCache`.** Both land in 0.5.0 alongside the lock-free, arena-backed rewrites of `LruCache` / `LfuCache` / `TtlCache`.
- **Shared abstraction between the three cache types.** With three concrete policies in place, the duplication is now visible — extracting a common `Mutex`-recover helper, `find_victim`-style scan, and `NonZeroUsize` capacity wrapper is the right move during the 0.5.0 quality pass.
- **Background expiry sweeper.** The lazy approach is correct and avoids a runtime thread; bulk-expiry under high write churn (where `len` is rarely called) is a 0.5.0+ tuning concern.

## Breaking changes

**None.** `TtlCache` is a new public type alongside `LruCache` and `LfuCache`; the `Cache` trait, `CacheError`, `LruCache`, and `LfuCache` signatures are unchanged. Every 0.3.0 call-site compiles and behaves identically against 0.4.0.

The crate remains pre-1.0; minor versions may break in the future. Pin exact versions.

## Verification

Local run on Windows x86_64, Rust stable; identical commands pass via the configured CI matrix (Linux / macOS / Windows on stable + MSRV 1.75):

```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --all-targets --no-default-features -- -D warnings
cargo test --all-features
cargo doc --no-deps --all-features
```

All green. Test totals:

- Integration tests: **32 passed** (11 LRU + 8 LFU + 13 TTL).
- Doctests: **13 passed** (6 from 0.2.0 + 3 LFU + 4 TTL).

REPS lint surface declared in `src/lib.rs` is honored: every `deny(...)` clippy/rustc lint from 0.2.0 still holds. No `unsafe` is introduced in this release.

## What's next

`0.5.0` is the implementation-quality milestone. The reference `Mutex`-guarded implementations of `LruCache`, `LfuCache`, and `TtlCache` are replaced by lock-free, arena-backed versions while keeping the public surface identical. `TinyLfuCache` lands here — admission filter (Count-Min Sketch) on top of an LFU main cache. `SizedCache` lands here too — byte-bound capacity (composes with primary policies). Property tests on cache invariants land here. Criterion benchmarks land here. Common scaffolding (`find_victim`, `lock_inner`, capacity types) gets extracted out of the three existing files.

`0.9.0` is the hardening + audit pass; `1.0.0` is the API freeze.

## Installation

```toml
[dependencies]
cache-mod = "0.4"
```

MSRV: Rust 1.75. Edition 2021. `default-features = ["std"]`.

```rust
use std::time::Duration;
use cache_mod::{Cache, TtlCache};

let cache: TtlCache<&'static str, u32> =
    TtlCache::new(64, Duration::from_secs(300)).expect("capacity > 0");

cache.insert("session-42", 1);                                  // default TTL = 5 min
cache.insert_with_ttl("flash-token", 7, Duration::from_secs(5)); // short-lived

assert_eq!(cache.get(&"session-42"), Some(1));
```

## Documentation

- [README](https://github.com/jamesgober/cache-mod/blob/main/README.md)
- [CHANGELOG](https://github.com/jamesgober/cache-mod/blob/main/CHANGELOG.md)
- [REPS standards](https://github.com/jamesgober/cache-mod/blob/main/REPS.md)
- [API reference](https://docs.rs/cache-mod/0.4.0)

---

**Full diff:** [`v0.3.0...v0.4.0`](https://github.com/jamesgober/cache-mod/compare/v0.3.0...v0.4.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/cache-mod/blob/main/CHANGELOG.md#040---2026-05-20).

[`get`]: https://docs.rs/cache-mod/0.4.0/cache_mod/trait.Cache.html#tymethod.get
[`insert`]: https://docs.rs/cache-mod/0.4.0/cache_mod/trait.Cache.html#tymethod.insert
[`contains_key`]: https://docs.rs/cache-mod/0.4.0/cache_mod/trait.Cache.html#tymethod.contains_key
[`len`]: https://docs.rs/cache-mod/0.4.0/cache_mod/trait.Cache.html#tymethod.len
[`insert_with_ttl`]: https://docs.rs/cache-mod/0.4.0/cache_mod/struct.TtlCache.html#method.insert_with_ttl
