1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//! Request-owned typed memoization (AP2.1-7).
//!
//! A per-request container that memoizes expensive resolver results for the
//! duration of ONE request. The same key runs the resolver ONCE per request;
//! concurrent same-key callers share the in-flight computation; a resolver
//! failure is cached and affects ONLY requests needing that value; dropping
//! the last waiter cancels in-flight work. Different keys run independently.
//!
//! # Ownership model (load-bearing — AGENTS.md §20)
//!
//! [`RequestCache`] is **request-owned**: it is a value constructed for one
//! request, passed explicitly (by reference) into the resolvers that need
//! it, and dropped when the request ends. It is NOT a thread-local, NOT a
//! task-local, NOT a global, NOT a mutable singleton. Async tasks move
//! between runtime threads; a request-owned value travels with the
//! request, so memoization is correct across `await` points regardless of
//! which worker thread resumes the task.
//!
//! The typical wiring: a middleware or the `state_fn` closure builds one
//! `RequestCache` per request, and resolvers take `&RequestCache` as a
//! parameter. Because it is a plain `Send + Sync` value behind a `Mutex`,
//! it composes with the existing extractors (`Current<User>`, `Inject<T>`,
//! `Db`, …) without a macro-hardcoded type list (ADR-0004): the cache is
//! just another request-owned value the handler threads through.
//!
//! # Isolated heterogeneous erasure (AGENTS.md §19)
//!
//! A per-request memoization cache by definition holds heterogeneous values
//! (`Profile`, `Settings`, `Permissions`, …) under heterogeneous keys.
//! This is the genuine heterogeneous-erasure case §19 admits. The erasure
//! is **isolated** to this module: the call-site API
//! [`RequestCache::get_or_compute`] is fully typed (`K`, `V` are known at
//! the call site); only the internal map type-erases the memoized `Shared`
//! future as `Box<dyn Any + Send + 'static>`. The simple typed path (one
//! resolver, one key, one value) never touches the erasure.
//!
//! # Concurrency / failure / cancellation semantics
//!
//! - **Same-key dedup:** the first caller for a key starts the resolver;
//! subsequent callers receive a clone of the same `Shared` future and
//! resolve together. Proven by an `AtomicUsize` counter: a sleeping
//! expensive resolver runs exactly ONCE for N concurrent same-key
//! callers.
//! - **Different-key independence:** two different keys run two resolvers
//! concurrently; neither waits for the other.
//! - **Failure isolation:** a resolver that returns `Err` caches the
//! error for the key; only callers of THAT key see it. A different key's
//! resolver is unaffected.
//! - **Cancellation:** dropping all `Shared` clones for a key before it
//! resolves drops the in-flight resolver future (cancellation). New
//! callers after cancellation start a fresh computation.
//!
//! # No panic on hostile input (AGENTS.md §17)
//!
//! A key whose serialized form exceeds [`MAX_KEY_BYTES`] is rejected with
//! [`RequestCacheError::OversizedKey`] — no panic, no allocation blow-up.
//! The cache itself is `#![forbid(unsafe_code)]`-clean: a `Mutex`
//! guards the map; no `unsafe`, no `unwrap`/`expect`/panic`.
pub use ;
pub use ;