# cached 1.1 → 2.0 Migration (Agent Instructions)

Versions: `cached 1.1` → `cached 2.0` / `cached_proc_macro 1.1` → `cached_proc_macro 2.0`

## Breaking changes

---

### 1. `result = true` removed from `#[cached]` and `#[once]`

**Old behavior:** `result = true` was required to skip caching `Err`.
**New behavior:** Any `Result<T, E>` return type automatically skips caching `Err`.

**Detection:** search for `result = true` on `#[cached]` and `#[once]` annotations.

**Action (most users):** remove `result = true` entirely. The behavior is now the default.

**Possible regression:** if a `#[cached]` or `#[once]` function returns `Result<T, E>` and previously relied on caching `Err` values (no `result = true`), add `cache_err = true` to preserve that behavior.

---

### 2. `option = true` removed from `#[cached]` and `#[once]`

**Old behavior:** `option = true` was required to skip caching `None`.
**New behavior:** Any `Option<T>` return type automatically skips caching `None`.

**Detection:** search for `option = true` on `#[cached]` and `#[once]` annotations.

**Action (most users):** remove `option = true` entirely.

**Possible regression:** if a `#[cached]` or `#[once]` function returns `Option<T>` and previously relied on caching `None` values (no `option = true`), add `cache_none = true` to preserve that behavior.

---

### 3. `result_fallback = true` no longer requires `result = true`

**Detection:** search for `result = true` on annotations that also have `result_fallback = true`.

**Action:** remove `result = true`; keep `result_fallback = true`.

---

### 4. Custom-`ty` users storing `Option<T>` or `Result<T,E>` directly

**Detection:** search for `ty = "..."` annotations where the store value type is `Option<…>` or `Result<…>`.

**Action:** add `cache_none = true` (for `Option<T>` stores) or `cache_err = true` (for `Result<T,E>` stores) to prevent the macro from extracting the inner type and mismatching the declared store type.

---

### 5. `cache_remove` on expiring stores now returns `None` for expired-but-present entries

**Affected stores:** `ExpiringCache`, `ExpiringLruCache`, `ShardedExpiringCache`, `ShardedExpiringLruCache`.

**Old behavior:** `cache_remove(&k)` on an entry that was present but already expired returned `Some(value)`.
**New behavior:** returns `None`. The entry is still removed and `on_evict` still fires.

**Detection:** search for `.cache_remove(` / `.remove(` / `.cache_delete(` / `.delete(` calls on an expiring store.

**Action:** if the code relied on `Some(value)` for expired-but-present entries, add an explicit `is_expired()` check before removal, or ignore the return value. `cache_delete` / `delete` now return `true` whenever an entry is physically removed, including expired entries. If you need to distinguish a live removal from an expired one, use `cache_remove`; if you need to distinguish an expired-but-present entry from an absent key, use `cache_remove_entry`.

---

### 6. `LruCache::retain` now fires `on_evict` and increments `cache_evictions()`

**Old behavior:** `retain` was side-effect-free.
**New behavior:** each entry removed by `retain` fires `on_evict` (if set) and increments `cache_evictions()`.

**Detection:** search for `.retain(` on an `LruCache` instance that has an `on_evict` callback.

**Action:** if `retain` was used because it was side-effect-free, collect keys first and use a manual removal loop instead.

---

### 7. All non-unbound stores now fire `on_evict` on explicit `cache_remove`

**Old behavior:** only `UnboundCache` fired `on_evict` on explicit removes.
**New behavior:** every in-memory store fires `on_evict` on `cache_remove`.

**Detection:** search for `.cache_remove(` / `.remove(` / `.delete(` on stores with an `on_evict` callback (excluding `UnboundCache`).

**Action:** verify the `on_evict` callback is safe to call for explicitly removed entries.

---

### 8. `BuildError::InvalidTtl { ttl }` — new variant

All TTL-capable builders now return this variant for a zero TTL.

**Detection:** exhaustive `match` on `BuildError`.

**Action:** add an arm for `InvalidTtl { .. }`.

---

### 9. Builder-only construction: store constructors removed, `build()` returns `Result`

All direct, store-returning constructors are **removed**. Every store is now built through its builder: `X::builder().…setters….build()?`. `build()` now returns `Result<Store, BuildError>` for every in-memory and sharded store (it previously returned the store and panicked on invalid config), and the redundant `try_build()` alias is removed.

**Removed constructor → builder replacement:**

| Removed | Replacement |
| --- | --- |
| `UnboundCache::new()` | `UnboundCache::builder().build()?` |
| `UnboundCache::with_capacity(n)` | `UnboundCache::builder().capacity(n).build()?` |
| `LruCache::with_max_size(n)` / `try_with_max_size(n)` | `LruCache::builder().max_size(n).build()?` |
| `TtlCache::with_ttl(t)` | `TtlCache::builder().ttl(t).build()?` |
| `TtlCache::with_ttl_and_capacity(t, n)` | `TtlCache::builder().ttl(t).capacity(n).build()?` |
| `TtlCache::with_ttl_and_refresh(t, r)` | `TtlCache::builder().ttl(t).refresh_on_hit(r).build()?` |
| `LruTtlCache::with_max_size_and_ttl(n, t)` / `try_…` | `LruTtlCache::builder().max_size(n).ttl(t).build()?` |
| `LruTtlCache::with_max_size_and_ttl_and_refresh(n, t, r)` | `LruTtlCache::builder().max_size(n).ttl(t).refresh_on_hit(r).build()?` |
| `TtlSortedCache::new(t)` | `TtlSortedCache::builder().ttl(t).build()?` |
| `TtlSortedCache::with_ttl_and_capacity(t, n)` | `TtlSortedCache::builder().ttl(t).capacity(n).build()?` |
| `ExpiringCache::new()` | `ExpiringCache::builder().build()?` |
| `ExpiringCache::with_capacity(n)` | `ExpiringCache::builder().capacity(n).build()?` |
| `ExpiringLruCache::with_max_size(n)` / `try_with_max_size(n)` | `ExpiringLruCache::builder().max_size(n).build()?` |
| `ShardedCache::new()` / `with_shards(s)` | `ShardedCache::builder()[.shards(s)].build()?` |
| `ShardedLruCache::with_max_size(n)` / `with_max_size_and_shards(n, s)` | `ShardedLruCache::builder().max_size(n)[.shards(s)].build()?` |
| `ShardedTtlCache::with_ttl(t)` / `with_ttl_and_shards(t, s)` | `ShardedTtlCache::builder().ttl(t)[.shards(s)].build()?` |
| `ShardedLruTtlCache::with_max_size_and_ttl(n, t)` / `…_and_shards(n, t, s)` | `ShardedLruTtlCache::builder().max_size(n).ttl(t)[.shards(s)].build()?` |
| `ShardedExpiringCache::new()` / `with_shards(s)` | `ShardedExpiringCache::builder()[.shards(s)].build()?` |
| `ShardedExpiringLruCache::with_max_size(n)` / `…_and_shards(n, s)` | `ShardedExpiringLruCache::builder().max_size(n)[.shards(s)].build()?` |

> **Note on the `Sharded*` rows:** the sharded stores are **new in 2.0** — they had no 1.1 equivalent, so a 1.1 codebase will not contain any of their `new()` / `with_shards()` calls to migrate. They are listed here only to document that, like every other store, they are constructed exclusively through the builder. The non-builder constructors shown in the left column never shipped in a stable release.

`DiskCache` / `RedisCache` / `AsyncRedisCache` are **unaffected** — `X::new(...)` / `X::builder(...)` already return a builder and their `build()` already returned `Result`. (Only their redundant `try_build()` alias is removed.)

**Also:**
- `.try_build()` → `.build()` everywhere — semantics are identical now; the alias is gone.
- Any existing `Foo::builder()….build()` on an in-memory/sharded store now returns `Result` — add `?` or `.unwrap()`.
- **Zero TTL is now always rejected**: `…builder().ttl(Duration::ZERO).build()` returns `Err(BuildError::InvalidTtl { ttl })`. The old permissive `with_ttl(Duration::ZERO)` path (everything-expires-immediately) no longer exists. If you previously relied on a zero TTL to make every entry expire immediately, there is no equivalent construction in 2.0 — either skip caching that code path entirely, or use a very small non-zero TTL (e.g. `Duration::from_nanos(1)`) if entries must be treated as expired on the very next access.
- `TtlSortedCache`'s preallocation hint moves from the `with_ttl_and_capacity` `size` arg to the builder's new `.capacity(n)` setter — distinct from `.max_size(n)`, which is the eviction bound.

**Detection:** search for `::new(`, `::with_capacity(`, `with_max_size`, `with_ttl`, `with_shards`, `try_with_`, `.try_build(` on cache types/builders. Compiler errors: `E0599: no function or associated item named ...` (removed constructor) and `unused ... Result` / `E0308 mismatched types` (the new `build()` return needs `?`/`.unwrap()`).

**Action:** rewrite per the table; add `?`/`.unwrap()` to every `build()`; replace `try_build` with `build`.

### 9a. `size` → `max_size` naming (setter, macro attribute, runtime setters)

The size **vocabulary** is unified to `max_size`:

| Old | New | Notes |
| --- | --- | --- |
| `.size(n)` (builder) | `.max_size(n)` | LRU-family builders and `TtlSortedCache`. Sharded per-shard cap setter is `per_shard_max_size`. |
| `#[cached(size = N)]` / `#[concurrent_cached(size = N)]` | `max_size = N` | `size = N` still compiles as a **deprecated alias** (warns at the `size` token); must change under `-D warnings` / `#![deny(deprecated)]`. |
| `cache.size_limit(n)` (`TtlSortedCache`) | `cache.set_max_size(n)` | Runtime max-size setter; returns previous value. |
| `cache.try_size_limit(n)` (`TtlSortedCache`) | `cache.try_set_max_size(n)` | Fallible runtime variant. |

**Detection:** search for `.size(`, `size = ` (macro attrs), `.size_limit(`, `.try_size_limit(`.

**Action:** mechanical rename per the table.

---

### 10. `ConcurrentCached` / `ConcurrentCachedAsync` mutators now take `&self`

`set_refresh_on_hit`, `set_ttl`, and `unset_ttl` on the `ConcurrentCached` and `ConcurrentCachedAsync` traits changed receiver from `&mut self` to `&self`, matching the internally-synchronized `&self` contract of the rest of these traits. Affects **custom implementors** of these traits; callers are unaffected (a `&self` call compiles everywhere a `&mut self` call did, and you can now call these on a shared/`Arc`/`static` store).

**Detection:** `impl ConcurrentCached` / `impl ConcurrentCachedAsync` blocks defining `fn set_refresh_on_hit(&mut self, …)`, `fn set_ttl(&mut self, …)`, or `fn unset_ttl(&mut self, …)`. Compiler error `method ... has an incompatible type for trait` / `expected &self`.

**Action:** change the receiver `&mut self` → `&self` on those three methods and move any mutated state behind interior mutability (e.g. `parking_lot::Mutex` / `AtomicBool`), as the bundled `DiskCache` / `RedisCache` / `AsyncRedisCache` impls now do. The single-owner `Cached` and `CacheTtl` traits are unchanged — their mutators remain `&mut self`.

---

### 11. `cache_size` added to `ConcurrentCached` / `ConcurrentCachedAsync`

New method `fn cache_size(&self) -> Result<Option<usize>, Self::Error>` with a default of `Ok(None)`. Additive for existing implementors (the default applies), so **no action is required** unless you want to report a count.

**Detection:** none needed — existing `impl` blocks keep compiling via the default.

**Action (optional):** override `cache_size` to return `Ok(Some(n))` if your store can produce an entry count cheaply. The six sharded stores already do; the external-backend stores (`DiskCache`, `RedisCache`, `AsyncRedisCache`) keep the `Ok(None)` default because their backends expose no O(1) size.

---

## New APIs (additive — no action required unless adopting)

### Macro attribute changes

- `max_size = N` — the preferred LRU-bound attribute on `#[cached]` / `#[concurrent_cached]`, mirroring the renamed `max_size` builder methods. The old `size = N` spelling is now a **deprecated alias** (emits a deprecation warning at the `size` token); setting both on one annotation is a compile error.
- `cache_err = true` — opt-in to cache `Err` values (requires `Result<T,E>`; mutually exclusive with `result_fallback`).
- `cache_none = true` — opt-in to cache `None` values (requires `Option<T>`).
- `result_fallback = true` on `#[concurrent_cached]` — fallback to the last cached `Ok` on `Err`. Restricted to the default in-memory sharded path; **requires `ttl`** (compile error otherwise). The stale value is held in the primary cache slot and re-cached with a fresh TTL window on `Err`; no separate `_FALLBACK` store is created.

### `#[concurrent_cached]` now defaults to an in-memory sharded store

Before 2.0, `#[concurrent_cached]` required `ty`, `create`, and `map_error` for any in-memory use.
Starting in 2.0, omitting those selects a sharded store automatically:

```rust
#[concurrent_cached]                           // → ShardedCache
#[concurrent_cached(max_size = 1000)]              // → ShardedLruCache
#[concurrent_cached(ttl = 60)]                     // → ShardedTtlCache (time_stores)
#[concurrent_cached(max_size = 500, ttl = 30)]     // → ShardedLruTtlCache (time_stores)
#[concurrent_cached(expires = true)]               // → ShardedExpiringCache
#[concurrent_cached(expires = true, max_size=500)] // → ShardedExpiringLruCache
```

### Six new sharded concurrent in-memory stores

```
ShardedCache<K, V>            — unbounded, no TTL
ShardedLruCache<K, V>         — LRU-bounded
ShardedTtlCache<K, V>         — unbounded, global TTL     (time_stores)
ShardedLruTtlCache<K, V>      — LRU + global TTL          (time_stores)
ShardedExpiringCache<K, V>    — unbounded, per-value expiry
ShardedExpiringLruCache<K, V> — LRU, per-value expiry
```

Cache operations (`cache_get` / `cache_set` / `cache_remove` / `cache_remove_entry` / `cache_delete`) are provided through the `ConcurrentCached` trait (and `ConcurrentCachedAsync` for `.await` use) — bring it into scope: `use cached::ConcurrentCached;`. All operations return `Result<_, std::convert::Infallible>`, so call sites use `.expect("infallible")` or `let _ =`. Construction, `metrics()`, `len()`, `clear()`, `cache_clear_with_on_evict()`, and `copy_from()` are inherent (no trait import needed).

### `ConcurrentCached::get` / `set` / `remove` / `delete` short aliases

Delegate to `cache_*` methods. Existing call sites need no change.

### `capacity()` getter on the LRU-family stores

Returns the max-entry bound, distinct from `cache_size()` (current live entry count). Available on `LruCache`, `LruTtlCache`, and `ExpiringLruCache`, as well as their sharded counterparts `ShardedLruCache`, `ShardedLruTtlCache`, and `ShardedExpiringLruCache`.

For the **non-sharded** stores (`LruCache`, `LruTtlCache`, `ExpiringLruCache`), `capacity()` returns exactly the value passed to `.max_size(n)`.

For the **sharded** stores, `capacity()` returns the effective total across all shards, which may be **larger** than the requested `max_size`. Per-shard capacity is computed via ceiling division (`max_size.div_ceil(shards)`), so the actual total is `per_shard_cap × shards`. For example, `ShardedLruCache::builder().max_size(1000).build()?` may yield `capacity() == 1008` (with 16 shards, `ceil(1000/16) == 63`, and `63 × 16 == 1008`).

---

## Required Cargo.toml change

```toml
# Before
cached = "1.1"

# After
cached = "2.0"
```

If pinning the proc-macro crate directly:
```toml
cached_proc_macro = "2.0"
```

`cached_proc_macro_types` did not change; leave it at `"1.0"` if pinned.

## VERIFY

```
cargo build
cargo test
```

Expected new compile errors and their fixes:
- `Unknown field: result` / `Unknown field: option` on `#[cached]` / `#[once]` — remove those attributes.
- `E0004` on exhaustive `match BuildError` — add `InvalidTtl { .. }` arm.
- `E0004` on exhaustive `match DiskCacheBuildError` — add `InvalidTtl(..)` arm (a new `InvalidTtl(BuildError)` variant was added to `DiskCacheBuildError`).
- `E0004` on exhaustive `match RedisCacheBuildError` — add `InvalidTtl(..)` arm (a new `InvalidTtl(BuildError)` variant was added to `RedisCacheBuildError`).
- Type mismatch on a custom `ty` holding `Option<T>` or `Result<T,E>` — add `cache_none = true` / `cache_err = true`.
- `cache_none = true` alongside `with_cached_flag = true` (on `#[cached]`, `#[once]`, or `#[concurrent_cached]`) — now a compile error. If you mechanically replaced `option = true` with `cache_none = true` on an annotation that also had `with_cached_flag = true`, remove one of the two: keep `with_cached_flag = true` alone (`None` is not cached by default) or `cache_none = true` alone.
- `the trait bound K: Clone is not satisfied` on `DiskCache` / `RedisCache` / `AsyncRedisCache` — the `ConcurrentCached` / `ConcurrentCachedAsync` impls for these stores now require `K: Clone` (needed by `cache_remove_entry` to return the stored key). Add `#[derive(Clone)]` or implement `Clone` for the key type.
- `no function or associated item named with_size` / `with_max_size` / `with_ttl` / `new` / `try_with_*` / `try_build` on a cache type or builder — the constructor was removed; switch to `X::builder().…build()?` per the table in breaking change #9. `no method named size` on a builder — rename the setter to `.max_size(n)` per #9a.
- `use of deprecated constant cached::__DEPRECATED_SIZE_ATTR` (only under `-D warnings` / `#![deny(deprecated)]`) — a `#[cached(size = N)]` / `#[concurrent_cached(size = N)]` macro attribute. Rename the attribute `size = N` → `max_size = N`.

Behavioral regressions to watch in tests:
- Tests asserting `cache_remove` returns `Some(v)` on an expired expiring-store entry — update to expect `None`.
- Tests asserting `on_evict` is NOT called after `retain` on `LruCache` — update to expect the callback to fire.
- Tests asserting `on_evict` is NOT called after explicit `cache_remove` on a non-`UnboundCache` — update to expect the callback.
- Tests asserting that `None` / `Err` returns from `#[cached]` functions are cached — add `cache_none = true` / `cache_err = true`.
