diff --git a/Cargo.lock b/Cargo.lock index 48b324a..976ffcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,13 +106,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -217,7 +217,7 @@ version = "0.7.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -273,7 +273,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn", + "syn 2.0.117", "tempfile", "toml", ] @@ -467,7 +467,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -485,7 +485,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -575,7 +575,7 @@ checksum = "1458c6e22d36d61507034d5afecc64f105c1d39712b7ac6ec3b352c423f715cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -634,7 +634,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1257,7 +1257,7 @@ checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1315,7 +1315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1729,7 +1729,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1789,7 +1789,7 @@ checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1870,6 +1870,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -1887,7 +1898,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1935,7 +1946,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1946,7 +1957,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1997,7 +2008,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2315,7 +2326,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2358,7 +2369,7 @@ checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2650,7 +2661,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2666,7 +2677,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2763,7 +2774,7 @@ dependencies = [ "async-trait", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-macro-support", @@ -2813,7 +2824,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2834,7 +2845,7 @@ checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2854,7 +2865,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2875,7 +2886,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2908,7 +2919,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/README.md b/README.md index a771728..f19d48f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ## Overview -`cachekit-rs` is the Rust SDK for [cachekit.io](https://cachekit.io). Plug in a backend, get dual-layer caching with optional client-side encryption. Bytes never leave your process unencrypted unless you say so. +`cachekit-rs` is the Rust SDK for [cachekit.io](https://cachekit.io). Pick an [intent preset](#intent-presets-recommended) — `minimal`, `production`, `encrypted`, or `io` — and get a pre-configured cache in one call, from bare Redis speed to dual-layer with client-side encryption. Pick `encrypted` and bytes never leave your process in plaintext. | Component | What it does | |:----------|:-------------| @@ -53,15 +53,15 @@ ```toml # Defaults: SaaS + encryption + L1 [dependencies] -cachekit-rs = "0.5" +cachekit-rs = "0.7" # With Redis backend [dependencies] -cachekit-rs = { version = "0.5", features = ["redis"] } +cachekit-rs = { version = "0.7", features = ["redis"] } # For Cloudflare Workers (no L1, no Redis) [dependencies] -cachekit-rs = { version = "0.5", default-features = false, features = ["workers", "encryption"] } +cachekit-rs = { version = "0.7", default-features = false, features = ["workers", "encryption"] } ``` > [!WARNING] @@ -76,6 +76,47 @@ cachekit-rs = { version = "0.5", default-features = false, features = ["workers" ## Quick Start +### Intent Presets (recommended) + +One call that names your use case. Each preset returns a pre-configured builder you can still override before `.build()`: + +| Preset | When to use | Backend | L1 | Encryption | Reliability¹ | Auto-reconnect² | Default TTL | +|:-------|:------------|:--------|:--:|:----------:|:------------:|:---------------:|:-----------:| +| `CacheKit::minimal(url)` | Development, public data, product catalogs — speed first, no extras | Redis³ | ❌ | ❌ | ❌ | ❌ | 300 s | +| `CacheKit::production(url)` | User sessions, API responses, production services | Redis³ | ✅ | ❌ | ✅ | ✅ | 600 s | +| `CacheKit::encrypted(url, key)` | PII, payments, GDPR/HIPAA-sensitive data — zero-knowledge AES-256-GCM | Redis³ | ✅ | ✅ | ✅ | ✅ | 600 s | +| `CacheKit::io(api_key)` | Serverless, edge compute, managed caching without running Redis | cachekit.io | ✅ | ❌ | ✅ | n/a (HTTP) | 3 600 s | + +¹ Retry with backoff + jitter, circuit breaker, backpressure — the [reliability stack](#reliability). Requires the default-on `reliability` feature. +² See the resilience contract below. +³ Requires the `redis` feature flag; `encrypted` also needs the default-on `encryption` feature. + +```rust +use cachekit::prelude::*; + +#[tokio::main] +async fn main() -> Result<(), CachekitError> { + // Needs: cachekit-rs = { version = "0.7", features = ["redis"] } + let cache = CacheKit::production("redis://localhost:6379").await? + .namespace("api") + .build()?; + + cache.set("greeting", &"Hello, world!").await?; + let val: Option = cache.get("greeting").await?; + println!("{val:?}"); + + Ok(()) +} +``` + +**Resilience contract** — connection failures, at construction and mid-run: + +- `production` / `encrypted` **auto-reconnect**: a dropped connection is re-established with exponential backoff (100 ms → 30 s cap), retrying indefinitely. +- `minimal` is **fail-fast**: a dropped connection is not re-established — every subsequent operation errors until you rebuild the client. +- **Initial** connections fail fast for every Redis preset: a bad URL or unreachable Redis errors immediately at construction, never enters a retry loop. `io` opens no connection at construction: an empty API key fails at construction, while an invalid key or unreachable endpoint surfaces at the first request. +- `encrypted` validates the master key **before** any Redis connection is attempted — a bad key is a deterministic local error, never masked by (or paying for) network I/O. +- Auto-reconnect is connection-level repair, distinct from the per-operation [reliability stack](#reliability) (retry, circuit breaker, backpressure) that `production` / `encrypted` / `io` also enable. `minimal` has neither — every failure is yours to handle. + ### From Environment Variables ```rust @@ -228,7 +269,7 @@ let backend = CachekitIO::builder() Native Redis via [fred](https://crates.io/crates/fred) with cluster support, TTL inspection, and distributed locking (`SET NX PX` acquire, atomic Lua compare-and-delete release, `:lock` namespace shared with cachekit-py). Requires the `redis` feature flag. ```toml -cachekit-rs = { version = "0.5", features = ["redis"] } +cachekit-rs = { version = "0.7", features = ["redis"] } ``` ```rust @@ -249,7 +290,7 @@ Memcached via [rust-memcache](https://crates.io/crates/memcache) (single server, TTLs above memcached's 30-day ceiling are clamped (larger values would be misread as absolute timestamps); values above the item-size limit (default 1 MiB) fail loudly client-side, and a server-side "object too large" classifies as permanent (never retried). Requires the `memcached` feature flag. ```toml -cachekit-rs = { version = "0.5", features = ["memcached"] } +cachekit-rs = { version = "0.7", features = ["memcached"] } ``` ```rust @@ -266,7 +307,7 @@ let backend = MemcachedBackend::builder() Local disk cache, **byte-compatible with cachekit-py's File backend** — a py and an rs process pointed at the same directory read each other's entries (Blake2b-128 hashed filenames, shared 14-byte header, atomic write-then-rename, lazy expiry). Implements `TtlInspectable` (TTL read off the on-disk header, in-place refresh). Concurrency matches py: same-process operations serialize on a backend-wide lock (py's `RLock`); on unix, reads and in-place TTL rewrites take advisory `flock` while writes stay lock-free via atomic rename; and expired-entry unlinks are inode-validated so a stale read decision doesn't delete a concurrent writer's fresh entry. On unix the cache directory must be owned by you and not group/other-writable. Not yet ported from py: LRU eviction and size caps — the directory grows until entries expire or you clear it. Requires the `file` feature flag and a tokio runtime (I/O runs via `spawn_blocking`). ```toml -cachekit-rs = { version = "0.5", features = ["file"] } +cachekit-rs = { version = "0.7", features = ["file"] } ``` ```rust @@ -282,7 +323,7 @@ let backend = FileBackend::builder() `wasm32-unknown-unknown` backend using `worker::Fetch`, with distributed locking and TTL inspection against the SaaS lock/TTL endpoints. Requires the `workers` feature with default features disabled. ```toml -cachekit-rs = { version = "0.5", default-features = false, features = ["workers", "encryption"] } +cachekit-rs = { version = "0.7", default-features = false, features = ["workers", "encryption"] } ```
diff --git a/crates/cachekit/Cargo.toml b/crates/cachekit/Cargo.toml index 709574c..8d0d47e 100644 --- a/crates/cachekit/Cargo.toml +++ b/crates/cachekit/Cargo.toml @@ -11,6 +11,14 @@ homepage = "https://cachekit.io" keywords = ["cache", "redis", "encryption", "moka", "cloudflare-workers"] categories = ["caching", "web-programming", "cryptography"] +# docs.rs builds with default features only unless told otherwise; without this +# the Redis intent presets (minimal/production/encrypted) and the optional +# backends never appear in the rendered docs. `workers` stays off — mutually +# exclusive with redis/l1/reliability/memcached/file (the five compile_error +# guards in lib.rs). +[package.metadata.docs.rs] +features = ["cachekitio", "redis", "encryption", "l1", "reliability", "macros", "memcached", "file"] + [lib] name = "cachekit" diff --git a/crates/cachekit/src/intents.rs b/crates/cachekit/src/intents.rs index 95b8524..7ecf363 100644 --- a/crates/cachekit/src/intents.rs +++ b/crates/cachekit/src/intents.rs @@ -5,17 +5,15 @@ //! use case and returns a [`CacheKitBuilder`] so callers can override any //! setting before building. //! -//! | Intent | Backend | L1 | Encryption | Auto-reconnect | Reliability¹ | Default TTL | -//! |------------|-----------|------|------------|----------------|--------------|-------------| -//! | `minimal` | Redis | Off | No | No | Off | 300 s | -//! | `production` | Redis | On | No | Yes | On | 600 s | -//! | `encrypted` | Redis | On | AES-256-GCM | Yes | On | 600 s | -//! | `io` | cachekit.io | On | No | n/a (HTTP) | On | 3 600 s | +//! The preset matrix and per-intent resilience contract live in the +//! crate-level docs (`lib.rs`) — the single rustdoc-rendered copy. This +//! module is private, so docs here reach source readers only; the per-method +//! docs below are what docs.rs renders. //! -//! ¹ Retry with backoff + jitter, a circuit breaker, and backpressure -//! (bounded backend concurrency) around backend ops (requires the -//! `reliability` feature, on by default — see [`crate::reliability`]). -//! Override via [`CacheKitBuilder::reliability`]; +//! Reliability defaults come from +//! [`ReliabilityConfig::default()`](crate::reliability::ReliabilityConfig) +//! (requires the `reliability` feature, on by default). Override via +//! [`CacheKitBuilder::reliability`]; //! [`ReliabilityConfig::disabled()`](crate::reliability::ReliabilityConfig::disabled) //! turns the stack off entirely. @@ -81,8 +79,9 @@ impl CacheKit { /// **Production** — reliability-first Redis cache with L1. /// - /// * Backend: Redis (connects eagerly; **auto-reconnects** with - /// exponential backoff after a dropped connection) + /// * Backend: Redis (connects eagerly, failing fast if unreachable; + /// **auto-reconnects** after a dropped connection with exponential + /// backoff, 100 ms → 30 s, retrying indefinitely) /// * L1 cache: **on** (1 000 entries) /// * Encryption: **no** /// * Reliability: **on** — retry with backoff + jitter, circuit @@ -124,8 +123,9 @@ impl CacheKit { /// **Encrypted** — zero-knowledge encrypted Redis cache. /// - /// * Backend: Redis (connects eagerly; **auto-reconnects** with - /// exponential backoff after a dropped connection) + /// * Backend: Redis (connects eagerly, failing fast if unreachable; + /// **auto-reconnects** after a dropped connection with exponential + /// backoff, 100 ms → 30 s, retrying indefinitely) /// * L1 cache: **on** (1 000 entries, stores ciphertext) /// * Encryption: **AES-256-GCM** with HKDF-SHA256 /// * Reliability: **on** — retry with backoff + jitter, circuit @@ -136,7 +136,9 @@ impl CacheKit { /// /// Good for: PII, payments, GDPR/HIPAA-sensitive data. /// - /// `master_key` must be at least 32 raw bytes. + /// `master_key` must be at least 32 raw bytes. It is validated **before** + /// any Redis connection is attempted — a bad key is a deterministic local + /// error, never masked by (or paying for) network I/O. /// /// # Errors /// @@ -196,8 +198,9 @@ impl CacheKit { /// # Example /// /// ```no_run - /// # fn example() -> Result<(), cachekit::CachekitError> { - /// let cache = cachekit::CacheKit::io("ck_live_abc123")? + /// # fn example() -> Result<(), Box> { + /// let api_key = std::env::var("CACHEKIT_API_KEY")?; + /// let cache = cachekit::CacheKit::io(&api_key)? /// .namespace("edge") /// .build()?; /// # Ok(()) diff --git a/crates/cachekit/src/lib.rs b/crates/cachekit/src/lib.rs index bd80658..7619328 100644 --- a/crates/cachekit/src/lib.rs +++ b/crates/cachekit/src/lib.rs @@ -3,6 +3,50 @@ //! Supports cachekit.io SaaS, Redis, Memcached, local File, and Cloudflare //! Workers backends. Zero-knowledge encryption via AES-256-GCM with HKDF key //! derivation. +//! +//! # Getting started: intent presets +//! +//! The intent presets are the primary entry point — one call that names your +//! use case and returns a pre-configured [`CacheKitBuilder`] you can still +//! override before [`build()`](CacheKitBuilder::build): +//! +//! | Intent | Backend | L1 | Encryption | Auto-reconnect² | Reliability¹ | Default TTL | +//! |------------|-----------|------|------------|-----------------|--------------|-------------| +//! | `CacheKit::minimal`³ | Redis | Off | No | No | Off | 300 s | +//! | `CacheKit::production`³ | Redis | On | No | Yes | On | 600 s | +//! | `CacheKit::encrypted`³ | Redis | On | AES-256-GCM | Yes | On | 600 s | +//! | [`io`](CacheKit::io) | cachekit.io | On | No | n/a (HTTP) | On | 3 600 s | +//! +//! ¹ Retry with backoff + jitter, a circuit breaker, and backpressure around +//! backend ops — see [`reliability`]. Requires the default-on `reliability` +//! cargo feature. +//! ² `production`/`encrypted` re-establish a dropped Redis connection with +//! exponential backoff (100 ms → 30 s, retrying indefinitely); `minimal` is +//! fail-fast (a dropped connection stays dead). **Initial** connections fail +//! fast for every Redis preset — `io` opens no connection at construction: an +//! empty API key fails at construction, while an invalid key or unreachable +//! endpoint surfaces at the first request. +//! Auto-reconnect is connection-level repair, distinct from the per-operation +//! reliability stack. +//! ³ Requires the `redis` cargo feature; `encrypted` also needs the +//! default-on `encryption` feature. +//! +//! ```no_run +//! # async fn example() -> Result<(), Box> { +//! // Load the key from your environment/secrets manager — never hardcode it. +//! let api_key = std::env::var("CACHEKIT_API_KEY")?; +//! let cache = cachekit::CacheKit::io(&api_key)? +//! .namespace("myapp") +//! .build()?; +//! +//! cache.set("greeting", &"Hello, world!").await?; +//! let val: Option = cache.get("greeting").await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! For full control, drop down to [`CacheKit::builder`] or +//! [`CacheKit::from_env`]. // Production code lints — these only fire in src/, not tests/ #![warn(clippy::unwrap_used, clippy::expect_used, clippy::panic)]