Skip to main content

kevy_embedded/
lib.rs

1//! kevy-embedded — kevy without the network.
2//!
3//! In-process Redis-compatible key–value store: load + reply directly from
4//! your own threads, no TCP, no shards, no reactor. Use this when you want
5//! kevy's data structures + persistence in the same address space as your
6//! app — caches, embedded databases, WASM blobs, sidecar tools.
7//!
8//! Zero crates.io dependencies: only `kevy-store` (the keyspace)
9//! and `kevy-persist` (snapshot + AOF). The whole network layer
10//! (`kevy-rt`, `kevy-sys`, `kevy-uring`) is intentionally NOT pulled in.
11//!
12//! # Quick start
13//!
14//! ```
15//! use kevy_embedded::{Store, Config};
16//!
17//! # fn main() -> kevy_embedded::KevyResult<()> {
18//! let s = Store::open(Config::default())?;
19//! s.set(b"greeting", b"hello")?;
20//! assert_eq!(s.get(b"greeting")?, Some(b"hello".to_vec()));
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! # With persistence
26//!
27//! `with_persist(dir)` enables AOF auto-append on every write and replays
28//! on `open` — restart-safe out of the box. Snapshot (`dump-0.rdb`) is
29//! loaded first if present; AOF (`aof-0.aof`) is replayed on top.
30//!
31//! ```no_run
32//! use kevy_embedded::{Store, Config};
33//!
34//! # fn main() -> kevy_embedded::KevyResult<()> {
35//! let s = Store::open(Config::default().with_persist("./data"))?;
36//! s.set(b"counter", b"42")?;
37//! drop(s); // flushes AOF on drop
38//!
39//! // Next process: state survives.
40//! let s2 = Store::open(Config::default().with_persist("./data"))?;
41//! assert_eq!(s2.get(b"counter")?, Some(b"42".to_vec()));
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # When NOT to use this crate
47//!
48//! - You want a Redis-protocol TCP server → use the `kevy` crate's
49//!   [`serve`](https://docs.rs/kevy/latest/kevy/fn.serve.html) instead.
50//! - You need cross-process concurrency → kevy-embedded is single-process
51//!   (one mutex). Multi-process needs the network layer.
52//!
53//! # Locking & concurrency
54//!
55//! The keyspace is split into `Config::shards` independent shards, each a
56//! `kevy_store::Store` behind its own `RwLock` (default: **1 shard** = one
57//! lock over the whole keyspace). A key maps to its shard by hash; writes take
58//! that shard's exclusive lock.
59//!
60//! **Reads take a *shared* per-shard lock where it's sound to.** `GET` (and the
61//! FFI zero-copy `get_shared` lane) use the shared lock whenever the active
62//! eviction policy won't consume a per-read LRU/LFU tick — `maxmemory == 0`
63//! (the default), or the `NoEviction` / `*Random` / `VolatileTtl` policies. The
64//! true LRU/LFU policies (`*Lru` / `*Lfu`) instead take the exclusive lock so
65//! each access stamps the clock the eviction scorer ranks by. Read-only
66//! aggregations (`DBSIZE`, `used_memory`, the `INFO` counters) likewise take
67//! shared locks so a full-keyspace scan doesn't stall concurrent writers.
68//!
69//! This is a **lock-correctness** property, not a throughput one: a read-only
70//! operation doesn't hold the exclusive lock against a concurrent writer on its
71//! shard. It is **not** a lock-free read path — concurrent readers still
72//! contend on the shard's `RwLock` word (a shared cache line), so read scaling
73//! is bounded by **shard count**, not core count. To spread read/write
74//! contention across cores, raise `Config::shards`.
75//!
76//! **Known limitation:** the sibling reads (`hget`, `exists`, `smembers`,
77//! `zscore`, `llen`, `scard`, `zcard`, `type_of`, `ttl_ms`, …) currently still
78//! take the shard's *write* lock even though the underlying keyspace methods
79//! are read-only. Moving them onto the shared lane is a tracked follow-up
80//! (bench-gated separately from the `GET` lane above).
81//!
82//! # Cargo features
83//!
84//! `default` is the full surface. For constrained targets (IoT / edge)
85//! cut it down with `default-features = false, features = [...]`:
86//!
87//! | feature | adds |
88//! |---------|------|
89//! | `core` | in-memory KV + TTL + pub/sub + pipeline/atomic (the minimal base) |
90//! | `persist` | snapshot + AOF durability (`with_persist`, replay on open) |
91//! | `index` | secondary indexes + views |
92//! | `text` | full-text index segments (implies `index`) |
93//! | `vector` | HNSW vector index segments (implies `index`) |
94//! | `replicate` | embed-as-replica / embed-as-writer + CDC feed (implies `persist`) |
95//! | `listener` | the read-only RESP listener |
96#![forbid(unsafe_code)]
97#![warn(missing_docs)]
98
99mod config;
100mod dispatch;
101mod info;
102// Unconditional: `OpenReport` rides the DropGuard and the Store
103// handle in every archetype (a no-persist open reports zeros); only
104// the sink WIRING stays persist-gated in config.rs.
105mod metric;
106mod ops;
107mod ops_atomic;
108mod ops_atomic_all;
109mod ops_atomic_all_reads;
110mod ops_reconcile;
111#[cfg(feature = "index")]
112mod ops_atomic_all_index;
113mod ops_bitmap;
114mod ops_bonus;
115mod ops_keyspace;
116mod ops_more;
117mod ops_p2;
118mod ops_p3;
119mod ops_pipeline;
120#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
121mod ops_feed;
122mod ops_blocking;
123mod ops_hash_ttl;
124#[cfg(feature = "index")]
125mod ops_index;
126#[cfg(feature = "index")]
127mod ops_index_sync;
128#[cfg(feature = "index")]
129mod ops_table;
130#[cfg(feature = "index")]
131mod ops_view;
132#[cfg(all(feature = "listener", not(target_arch = "wasm32")))]
133mod listener;
134mod ops_snapshot_view;
135mod ops_zset_algebra;
136mod ops_zset_flags;
137mod op_manifest;
138mod store_glue;
139mod ops_scan;
140pub use ops_atomic::AtomicCtx;
141pub use ops_atomic_all::AtomicAllShards;
142pub use ops_bitmap::BitOp;
143pub use ops_pipeline::Pipeline;
144mod pubsub;
145mod reaper;
146mod shard;
147mod pubsub_bus;
148#[cfg(feature = "persist")]
149mod replay;
150#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
151mod replica_glue;
152#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
153mod replica_runner;
154#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
155mod replica_source;
156mod store;
157mod store_inner;
158mod store_wire;
159#[cfg(feature = "persist")]
160mod store_persist;
161
162pub use config::{Config, EvictionPolicy, TtlReaperMode};
163#[cfg(feature = "tier")]
164pub use config::TierBudgetSpec;
165#[cfg(feature = "tier")]
166mod config_tier;
167#[cfg(feature = "persist")]
168pub use config::AppendFsync;
169pub use info::{KevyInfo, KevyTierInfo};
170#[cfg(feature = "persist")]
171pub use metric::KevyMetric;
172pub use metric::OpenReport;
173#[cfg(feature = "persist")]
174pub use kevy_persist::RewriteStats;
175pub use kevy_store::{
176    ExpireStats, GetShared, HExpireCode, HExpireCond, KevyError, KevyResult, ScoreBound,
177    StoreError, ZAggregate, ZaddFlags, ZaddReport,
178};
179#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
180pub use ops_feed::{Change, ChangeBatch, FeedError, PrefixInfo};
181pub use ops_snapshot_view::{Snapshot, SnapshotEntry};
182pub use ops_reconcile::ReconcileReport;
183#[cfg(feature = "index")]
184pub use ops_index::IndexPage;
185#[cfg(feature = "text")]
186pub use ops_index::highlight::{FacetCounts, MatchOpts, MatchPage};
187#[cfg(feature = "index")]
188pub use ops_index::claused::{ScalarPage, ScalarQueryOpts, ValueFilter};
189#[cfg(feature = "index")]
190pub use ops_view::ViewPage;
191#[cfg(feature = "index")]
192pub use kevy_index::{AggBy, AnnSpec, GroupStats, Leaf as ViewLeaf, Tree as ViewTree, ViewMode};
193#[cfg(feature = "index")]
194pub use kevy_index::{Cursor as IndexCursor, IndexKind, IndexValue, SegmentStats as IndexStats, ValType as IndexValType};
195// The TABLE face — the dogfood report's F7: `Store::table_declare` takes
196// a `TableSpec` the facade did not export, so the typed face of a
197// flagship v4 feature was uncallable without depending on kevy-index
198// directly. The consumer gate (tools/facadegate) now builds against
199// these from outside the workspace, which is what would have caught it.
200#[cfg(feature = "index")]
201pub use kevy_index::{IndexVerify, OrderPath, TableEnsure, TableIndex, TableSpec, TableVerify};
202// `each_prefix` hands the callback a `kevy_store::Value` — same class of
203// gap: a public signature whose type the facade could not name.
204pub use kevy_store::Value;
205pub use pubsub::{PubsubFrame, Subscription};
206pub use store::{Store, WeakStore};
207
208/// Feed kevy's clocks on `wasm32-unknown-unknown`, which has neither
209/// `Instant` nor `SystemTime`. Without a host-fed clock, TTL operations and
210/// the reaper would trap. Call [`set_clock_ns`] (monotonic ns, e.g.
211/// `Date.now() * 1e6`) before TTL-sensitive ops and once per `tick`, and
212/// [`set_wall_clock_ms`] (Unix-epoch millis) if you use `XADD` auto-IDs or
213/// `EXPIREAT`. No-ops conceptually on native targets — hence wasm-only.
214#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
215pub use kevy_store::{set_clock_ns, set_wall_clock_ms};