Sassi is a typed cache substrate for Rust applications with composable predicate algebra and cross-runtime trait queries. It is built for when cached Rust data stops being just a key-value lookup and starts becoming typed local application state.
It provides an in-memory pool (Punnu<T>) that lets you look up domain objects by identity, refresh them, and query a local view using composable predicates (BasicPredicate<T>, MemQ<T>)—without tying that view to an ORM, a web framework, or a database client. Whether you are building a native service, a desktop worker, or a wasm32-unknown-unknown application, Sassi gives your intermediate cache layer a shared, typed shape.
Named after Sassi and Punnu, the central figures of a classic Punjabi folk tale.
The name is meant in that literary context. If it is unfamiliar, the source material is worth exploring on its own terms; this project borrows from that tradition, not from an acronym or an invented technical metaphor. Any similarity to unrelated software, tools, or projects is coincidental. Sassi is independent and unaffiliated.
Rust has excellent key-value caches (moka, cached) and flexible ways to query collections, but many applications need typed cached state that can be queried without becoming a full database.
As applications grow, they often hand-roll a layer between the source of truth and local readers: identity maps, predicate helpers, refresh tasks, and eviction policies. Sassi gives that intermediate layer a shared, typed shape. It is a cache substrate designed for the space between simple memoization and durable storage: storing values by typed identity, reading through explicit predicates, and keeping cache policy visible in Rust types.
First add the dependencies, then cache typed users by id and query active
adults. This example is also available as a CI-verified file at
sassi/examples/quick_tour.rs;
the three surfaces (example file, crate-level rustdoc, this README block) mirror
the same body.
# Cargo.toml
[dependencies]
sassi = "0.1.0-beta.4"
tokio = { version = "1.52", features = ["macros", "rt"] }use sassi::{Cacheable, MemQ, Punnu};
#[derive(sassi::Cacheable)]
struct User {
id: i64,
age: u32,
is_active: bool,
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// 1. Build the in-memory pool and populate it.
let users = Punnu::<User>::builder().build();
users
.insert(User { id: 1, age: 32, is_active: true })
.await
.unwrap();
// 2. Query local state with composable predicates.
let adults = users
.scope(vec![MemQ::filter_basic(
User::fields().age.gte(18) & User::fields().is_active.eq(true),
)])
.take(10)
.collect();
assert_eq!(adults.len(), 1);
}The #[derive(Cacheable)] macro requires a field literally named id. Types
whose identifier uses a different name (e.g. user_id) need a hand-written
Cacheable impl until v0.2 adds #[cacheable(id)].
Cacheable: Tells Sassi how to identify a value, exposing canonical identities (Id) and searchable fields.Punnu<T>: The in-process typed object pool. It stores values by identity and publishes new immutable snapshots after writes.BasicPredicate<T>: A shared predicate algebra (using&,|,^,!) that is walkable. This allows both Sassi to replay it in memory and your fetchers to inspect it.MemQ<T>: For local work after data has reached the pool: filtering, sorting, taking, mapping, unique-ing, grouping, and folding.
- Fetch-on-miss and coalescing:
get_or_fetchcollapses concurrent fetches for the same id into a single execution.get_or_fetch_manydeduplicates ids within a single batch call; concurrent batch calls for the same ids are not cross-coalesced in v0.1.0-beta.4. - Bounded Eviction: Sampled-LRU eviction and optional TTL keep your memory footprint bounded. TTL cleanup is lazy by default — expired entries are observed as misses by
getand reclaimed under capacity pressure; configurettl_sweep_intervalto opt into a background sweep task. - Refresh: Built-in mechanisms for periodic polling and watermark-based delta sync drive background updates. Eviction recovery (re-fetching entries that LRU pressure dropped) is opt-in via
DeltaRefreshHandle::with_eviction_recovery(true). - Events: Subscribe to streams of inserts, updates, and deletes to trigger application UI renders or downstream side effects. Events are best-effort observability with a lossy contract — slow subscribers and missing subscribers drop events — not a durable log; build durable side-effect pipelines on the source of truth instead.
When using an L2 backend (Redis, FileBackend), set
#[cacheable(type_name = "stable.name")] on cached types — the default
Cacheable::cache_type_name() is tied to the Rust type path, so renaming a
module invalidates the L2 keyspace. See
Backends And Runtimes
for full L2 keyspace mechanics. Configuration details for TTL durations,
eviction thresholds, and refresh intervals also live in that document.
Use Sassi when:
- Your cached data is becoming typed local application state.
- You need to query your cached data by attributes (using composable predicates) alongside identity lookups.
- You need structured cache lifecycles: lazy fetch-on-miss coalescing, TTL/LRU eviction, and event streams.
- You need your cache layer to expose the same API surface across native targets and
wasm32-unknown-unknown(runtime-specific code paths remain internal).
Do not use Sassi when:
- You only need simple exact-key lookup (use
mokaorcached). - Your data is small, transient, and local to a single function (use
HashMaporVec::iter().filter()). - You need durable transactions, secondary indexes, query planning, or relational joins (use a real database).
- You are looking for a full LINQ clone or a database ORM.
HashMap/Vec::iter().filter(): Great for small, transient data. However, they do not provide cache eviction policies (LRU/TTL), background refresh, or cross-request single-flight fetch coalescing.moka: An excellent, high-performance concurrent cache for exact-key lookups. It is optimized for hit-rate and throughput for key-value data. Usemokawhen you only needget(key). Sassi focuses on typed object pools where data is queried by predicates alongside key lookups.cached: A fantastic tool for simple memoization and function-level caching. Sassi operates at the domain-model level rather than the function-return level, maintaining a queryable local view of the data.- SQL/LINQ-style collection query builders: Great for general-purpose querying over Rust collections. Sassi is not a general-purpose query builder; it is a cache pool that happens to support basic predicate filtering, explicitly avoiding joins and complex relational algebra.
- Databases (SQLite, PostgreSQL, etc.): Provide durable storage, ACID transactions, secondary indexes, and query planning. Sassi provides none of these. Sassi is an in-memory cache substrate for fast, local reads; predicate queries in Sassi are currently evaluated as in-memory scans.
- Not a database: Sassi does not provide ACID transactions, query planning, or secondary indexes. The optional
FileBackendwrites cache values to disk for development and simple local persistence, but L2 backends in Sassi are cache substrate, not a system of record. - Not a full ORM: Sassi maps cache identities to objects, but it does not map database schemas or track foreign key relations.
- Not a full LINQ clone: Predicates are limited to basic logical composition (
&,|,^,!) and do not support arbitrary joins or complex relational projection. - Not a replacement for simple caches: If you only need
get(key), Sassi introduces unnecessary overhead compared to specialized K/V caches. - Not a magic index/query planner: Predicate queries in Sassi are currently evaluated as in-memory scans. There is no query planner or secondary indexing.
Sassi's current public beta is 0.1.0-beta.4. The core API, Redis companion, Bardownski TUI example, benchmark harness, and adopter docs are in place, with the caution that beta APIs can still move when integration work finds correctness or ergonomics gaps.
sassi/ # library crate
sassi-codegen/ # support crate for macro/codegen integrations
sassi-macros/ # support proc-macro crate re-exported by sassi
sassi-cache-redis/ # Redis CacheBackend companion crate
examples/bardownski/ # dependency-light TUI showcase
xtask/ # internal build tooling (not published)
Most adopters add only sassi to Cargo.toml, plus sassi-cache-redis when
Redis L2 support is needed. sassi-macros and sassi-codegen are published
support crates for Sassi's derive macros and downstream macro integrations; they
are not part of the ordinary application dependency story.
Authors writing their own proc-macro crates against sassi-codegen must depend
on proc-macro-crate at a version compatible with the one sassi-codegen pins
(see sassi-codegen/Cargo.toml). sassi_codegen::resolve_sassi_path accepts a
proc_macro_crate::FoundCrate parameter in its public signature, so the two
crates must agree on the FoundCrate type identity.
- Getting Started
- Concepts
- Query And Refresh Boundaries
- Backends And Runtimes
- Dependency Footprint — transitive dep graph per feature combination, for adopters auditing binary size or supply-chain surface.
- Advanced Guide
— predicate walk surface, scope chaining,
MemQterminals,#[trait_impl]registry, delta refresh handle operations, snapshot/restore modes, and custom-backend implementer notes. - Release Readiness
- Bardownski TUI Showcase
- Benchmarks
- Contributing (pre-commit guidance, the sensitive-info guard, and the public-text placeholder conventions)
- Changelog
Dual-licensed under MIT or Apache-2.0.