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