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//! ```
32//! use kevy_embedded::{Store, Config};
33//!
34//! # fn main() -> kevy_embedded::KevyResult<()> {
35//! # let tmp = kevy_tmpdir::TmpDir::new("embedded-persist");
36//! # let data = tmp.path();
37//! let s = Store::open(Config::default().with_persist(data))?;
38//! s.set(b"counter", b"42")?;
39//! drop(s); // flushes AOF on drop
40//!
41//! // Next process: state survives.
42//! let s2 = Store::open(Config::default().with_persist(data))?;
43//! assert_eq!(s2.get(b"counter")?, Some(b"42".to_vec()));
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! # When NOT to use this crate
49//!
50//! - You want a Redis-protocol TCP server → use the `kevy` crate's
51//!   [`serve`](https://docs.rs/kevy/latest/kevy/fn.serve.html) instead.
52//! - You need cross-process concurrency → kevy-embedded is single-process
53//!   (one mutex). Multi-process needs the network layer. A persist dir is
54//!   one engine's: `Store::open` on a dir a live engine holds — in this
55//!   process or another — errors (`flock` on `<dir>/LOCK`) instead of
56//!   letting two writers interleave one AOF.
57//!
58//! # Locking & concurrency
59//!
60//! The keyspace is split into `Config::shards` independent shards, each a
61//! `kevy_store::Store` behind its own `RwLock` (default: **1 shard** = one
62//! lock over the whole keyspace). A key maps to its shard by hash; writes take
63//! that shard's exclusive lock.
64//!
65//! **Reads take a *shared* per-shard lock where it's sound to.** `GET` (and the
66//! FFI zero-copy `get_shared` lane) use the shared lock whenever the active
67//! eviction policy won't consume a per-read LRU/LFU tick — `maxmemory == 0`
68//! (the default), or the `NoEviction` / `*Random` / `VolatileTtl` policies. The
69//! true LRU/LFU policies (`*Lru` / `*Lfu`) instead take the exclusive lock so
70//! each access stamps the clock the eviction scorer ranks by. Read-only
71//! aggregations (`DBSIZE`, `used_memory`, the `INFO` counters) likewise take
72//! shared locks so a full-keyspace scan doesn't stall concurrent writers.
73//!
74//! This is a **lock-correctness** property, not a throughput one: a read-only
75//! operation doesn't hold the exclusive lock against a concurrent writer on its
76//! shard. It is **not** a lock-free read path — concurrent readers still
77//! contend on the shard's `RwLock` word (a shared cache line), so read scaling
78//! is bounded by **shard count**, not core count. To spread read/write
79//! contention across cores, raise `Config::shards`.
80//!
81//! **Known limitation:** the sibling reads (`hget`, `exists`, `smembers`,
82//! `zscore`, `llen`, `scard`, `zcard`, `type_of`, `ttl_ms`, …) currently still
83//! take the shard's *write* lock even though the underlying keyspace methods
84//! are read-only. Moving them onto the shared lane is a tracked follow-up
85//! (bench-gated separately from the `GET` lane above).
86//!
87//! # Cargo features
88//!
89//! `default` is the full surface. For constrained targets (IoT / edge)
90//! cut it down with `default-features = false, features = [...]`:
91//!
92//! | feature | adds |
93//! |---------|------|
94//! | `core` | in-memory KV + TTL + pub/sub + pipeline/atomic (the minimal base) |
95//! | `persist` | snapshot + AOF durability (`with_persist`, replay on open) |
96//! | `index` | secondary indexes + views |
97//! | `text` | full-text index segments (implies `index`) |
98//! | `vector` | HNSW vector index segments (implies `index`) |
99//! | `replicate` | embed-as-replica / embed-as-writer + CDC feed (implies `persist`) |
100//! | `listener` | the read-only RESP listener |
101//! | `host-log` | `Store::dispatch_argv_recorded`: the frames a command's write records, for a host that keeps the log itself |
102#![forbid(unsafe_code)]
103#![warn(missing_docs)]
104
105mod config;
106#[cfg(feature = "replicate")]
107mod config_secure;
108mod config_stage;
109mod dispatch;
110#[cfg(feature = "host-log")]
111mod host_log;
112mod info;
113// Unconditional: `OpenReport` rides the DropGuard and the Store
114// handle in every archetype (a no-persist open reports zeros); only
115// the sink WIRING stays persist-gated in config.rs.
116mod assert_traits;
117#[cfg(all(feature = "listener", not(target_arch = "wasm32")))]
118mod listener;
119mod metric;
120mod modes;
121mod op_manifest;
122mod ops;
123mod ops_atomic;
124mod ops_atomic_all;
125#[cfg(feature = "index")]
126mod ops_atomic_all_index;
127mod ops_atomic_all_reads;
128mod ops_bitmap;
129mod ops_blocking;
130mod ops_bonus;
131#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
132mod ops_feed;
133mod ops_hash_ttl;
134#[cfg(feature = "index")]
135mod ops_index;
136#[cfg(feature = "index")]
137mod ops_index_changes;
138#[cfg(feature = "index")]
139mod ops_index_cold;
140#[cfg(feature = "index")]
141mod ops_index_sync;
142#[cfg(feature = "index")]
143mod ops_index_tidy;
144#[cfg(all(feature = "index", feature = "persist", not(target_arch = "wasm32")))]
145mod ops_index_window;
146mod ops_keyspace;
147mod ops_more;
148mod ops_p2;
149mod ops_p3;
150mod ops_pipeline;
151mod ops_reconcile;
152mod ops_scan;
153pub use ops_scan::KeysIter;
154#[cfg(feature = "index")]
155mod catalog_record;
156mod ops_snapshot_view;
157#[cfg(feature = "index")]
158mod ops_table;
159#[cfg(feature = "index")]
160mod ops_view;
161mod ops_zset_algebra;
162mod ops_zset_flags;
163mod store_glue;
164pub use ops_atomic::AtomicCtx;
165pub use ops_atomic_all::AtomicAllShards;
166// BITOP's operator now lives with its arithmetic in kevy-store, so
167// the server's wire path can reach the same one. Re-exported here
168// because it has been part of this crate's surface since 1.x.
169pub use kevy_store::BitOp;
170pub use ops_pipeline::Pipeline;
171mod pubsub;
172mod pubsub_bus;
173mod reaper;
174#[cfg(feature = "persist")]
175mod replay;
176#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
177mod replica_glue;
178#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
179mod replica_runner;
180#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
181mod replica_source;
182#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
183mod replica_wire;
184mod shard;
185#[cfg(feature = "persist")]
186mod shard_restore;
187#[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
188mod shard_tier;
189mod store;
190mod store_inner;
191#[cfg(feature = "persist")]
192mod store_persist;
193mod store_tick;
194mod store_wire;
195mod verb_keys;
196
197#[cfg(feature = "tier")]
198pub use config::TierBudgetSpec;
199pub use config::{Config, EvictionPolicy, TtlReaperMode};
200#[cfg(feature = "replicate")]
201pub use config_secure::{Keypair, LinkKeys};
202#[cfg(feature = "tier")]
203mod config_tier;
204#[cfg(feature = "persist")]
205pub use config::{AppendFsync, ReplayMode};
206pub use info::{KevyInfo, KevyTierCompression, KevyTierInfo};
207#[cfg(feature = "index")]
208pub use kevy_index::SortOrder;
209#[cfg(feature = "index")]
210pub use kevy_index::{AggBy, AnnSpec, GroupStats, Leaf as ViewLeaf, Tree as ViewTree, ViewMode};
211#[cfg(feature = "index")]
212pub use kevy_index::{
213    Cursor as IndexCursor, IndexKind, IndexValue, SegmentStats as IndexStats,
214    ValType as IndexValType,
215};
216#[cfg(feature = "persist")]
217pub use kevy_persist::RewriteStats;
218#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
219pub use kevy_replicate::feed::FeedPosition;
220pub use kevy_store::{
221    ExpireStats, GetShared, HExpireCode, HExpireCond, InsertPosition, KevyError, KevyResult,
222    ScoreBound, StoreError, ZAggregate, ZaddFlags, ZaddReport,
223};
224#[cfg(feature = "persist")]
225pub use metric::KevyMetric;
226pub use metric::OpenReport;
227pub use modes::CopyMode;
228#[cfg(feature = "text")]
229pub use modes::TokenPositions;
230#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
231pub use ops_feed::{Change, ChangeBatch, FeedError, PrefixInfo};
232#[cfg(feature = "index")]
233pub use ops_index::IndexPage;
234#[cfg(feature = "index")]
235pub use ops_index::advise::IdxAdvice;
236#[cfg(feature = "index")]
237pub use ops_index::claused::ScalarPage;
238#[cfg(feature = "text")]
239pub use ops_index::highlight::{FacetCounts, MatchPage};
240#[cfg(feature = "text")]
241pub use ops_index::opts::MatchOpts;
242#[cfg(feature = "index")]
243pub use ops_index::opts::{ScalarQueryOpts, ValueFilter};
244pub use ops_reconcile::ReconcileReport;
245pub use ops_snapshot_view::{Snapshot, SnapshotEntry};
246#[cfg(feature = "index")]
247pub use ops_view::ViewPage;
248// The TABLE face — the dogfood report's F7: `Store::table_declare` takes
249// a `TableSpec` the facade did not export, so the typed face of a
250// flagship v4 feature was uncallable without depending on kevy-index
251// directly. The consumer gate (tools/facadegate) now builds against
252// these from outside the workspace, which is what would have caught it.
253#[cfg(feature = "index")]
254pub use kevy_index::{IndexVerify, OrderPath, TableEnsure, TableIndex, TableSpec, TableVerify};
255// `each_prefix` hands the callback a `kevy_store::Value` — same class of
256// gap: a public signature whose type the facade could not name.
257pub use kevy_store::Value;
258pub use pubsub::{PubsubEvent, Subscription};
259pub use store::{Store, WeakStore};
260
261#[cfg(doctest)]
262#[doc = include_str!("../README.md")]
263struct ReadmeDoctests;
264
265/// Feed kevy's clocks on `wasm32-unknown-unknown`, which has neither
266/// `Instant` nor `SystemTime`. Without a host-fed clock, TTL operations and
267/// the reaper would trap. Call [`set_clock_ns`] (monotonic ns, e.g.
268/// `Date.now() * 1e6`) before TTL-sensitive ops and once per `tick`, and
269/// [`set_wall_clock_ms`] (Unix-epoch millis) if you use `XADD` auto-IDs or
270/// `EXPIREAT`. No-ops conceptually on native targets — hence wasm-only.
271#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
272pub use kevy_store::{set_clock_ns, set_wall_clock_ms};