arcature 2026.2.0

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! 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`.

mod cache;
mod error;

pub use cache::{RequestCache, RequestCacheFactory, from_state};
pub use error::{MAX_KEY_BYTES, RequestCacheError};