kevy-embedded
In-process Redis-compatible key–value store — kevy without the network.
Pure Rust, zero crates.io dependencies, builds for wasm32 as well as
native.
use ;
let s = open?;
s.set?;
assert_eq!;
# Ok::
Install
When to use
- Embedded cache — replace
lru::LruCache/moka/dashmapwith a fully Redis-semantic LRU (or LFU) that speaks all 5 data types. - Embedded persistent store — opt into AOF + snapshot via
Config::default().with_persist("./data"). Restart-safe out of the box. - WASM / single-threaded apps — use
Config::with_ttl_reaper_manual()and callStore::tick()from your own event loop. Full WASM walkthrough (browser / WASI / Cloudflare Workers) indocs/wasm.md.
When NOT to use
- You want a TCP-reachable Redis server → use the
kevycrate'sserve(...)entry point or thegoliakk/kevyDocker image.kevyserver runs the full thread-per-core reactor + cross-shard routing. - You need cross-process concurrency → kevy-embedded is single-process (one mutex). For multi-process / multi-host, the network layer is the contract — use the server.
All five Redis data types
use ;
let s = open?;
// String
s.set?;
assert_eq!;
s.incr?; // returns 1
s.incr_by?; // returns 42
// Hash
s.hset?;
assert_eq!;
// List
s.rpush?;
assert_eq!;
// Set
s.sadd?;
assert_eq!;
assert!;
// Sorted set — note the (score, member) tuple order
s.zadd?;
assert_eq!;
# Ok::
Persistence
Config::default().with_persist(dir) enables both snapshot
(dir/dump-0.rdb) and AOF (dir/aof-0.aof). On Store::open the
snapshot loads first, then the AOF replays — a fresh process picks up
exactly where the previous one left off. AOF auto-appends on every
write; fsync policy:
| Policy | Data loss on crash | Throughput |
|---|---|---|
Always |
0 bytes | ~50 % vs EverySec |
EverySec (default) |
≤ 1 second | baseline |
No |
up to ~30 s (kernel pagecache flush) | slightly faster |
use ;
let s = open?;
Store::save_snapshot() runs the equivalent of SAVE — dumps a full
snapshot synchronously. Store::rewrite_aof() runs the equivalent of
BGREWRITEAOF — rebuilds a compact AOF from current in-memory state
and atomically swaps it in. v1.0 is synchronous (blocks the calling
thread); v1.x will incrementalise.
Eviction
Set a hard memory ceiling via Config::with_max_memory(bytes) plus an
EvictionPolicy:
use ;
let s = open?;
All 8 Redis policies are supported: NoEviction, AllKeysLru,
AllKeysLfu, AllKeysRandom, VolatileLru, VolatileLfu,
VolatileRandom, VolatileTtl. LRU/LFU approximation matches Redis
(24-bit clock + sample-based selection with maxmemory-samples = 5).
Thread safety
Store::set / get / etc. take &self. Internally there's one
Mutex around the keyspace — fine for embedded use, where the
amortised cost is dwarfed by your app's work. Store is Clone
(v1.1.0+): a clone is a cheap Arc bump that reaches the same
underlying keyspace + AOF + reaper + pub/sub bus. The reaper thread is
joined and the AOF is flushed exactly once, when the last clone drops.
use ;
let s = open?;
let s2 = s.clone;
spawn;
# Ok::
For cross-core scale, use the kevy
server instead — it shards the keyspace across cores with no shared lock.
In-process pub/sub (v1.1.0+)
use ;
let s = open?;
let s2 = s.clone;
let mut sub = s.subscribe;
let _ack = sub.recv?;
s2.publish;
match sub.recv?
# Ok::
Channel + pattern subscriptions (PSUBSCRIBE glob syntax). Drop the
Subscription to unsubscribe from everything atomically. Pair with the
kevy-client URL facade to
make the same code work against an in-process bus (mem://name) in dev
and a kevy server (kevy://host:port) in prod — no scheme branching.
Migrating from lru / moka / dashmap
| If you had... | kevy-embedded equivalent | Notes |
|---|---|---|
lru::LruCache<K, V> |
Store + with_eviction(AllKeysLru) |
Byte-keys (&[u8]); with_max_memory instead of count cap |
moka::sync::Cache |
Store + with_eviction(AllKeysLfu) |
LFU matches moka's default expectation |
dashmap::DashMap |
Arc<Store> |
DashMap is concurrent; one Mutex but value is much richer (5 types, persistence) |
sled::Db |
Store + with_persist |
sled is a tree DB; kevy is a hash KV — pick by access pattern |
Versus redis::Client::open("redis://...") against a local Redis — you
lose zero performance and gain:
- No TCP roundtrip (~100 µs each)
- No serialization overhead
- One process to deploy
- No background server to monitor
You keep Redis semantics: TTL, eviction, all 5 types, byte-strings.
Maintenance hooks
For very long-running embedded use, periodically:
s.tick; // active TTL reaper — drops expired keys eagerly
s.save_snapshot?; // RDB-style dump for restart speed
s.rewrite_aof?; // compact AOF, drops redundant writes
If you're in Config::with_ttl_reaper_manual() mode (WASM /
single-threaded), tick() is the only way TTL'd keys get reaped between
accesses.
Examples
In the repo: examples/embedded.rs
— minimal CRUD; examples/embedded-cache.rs
— hard-cap LRU cache.
Dependencies
Zero crates.io dependencies. Only kevy-store (keyspace) +
kevy-persist (snapshot / AOF). The whole network layer
(kevy-rt, kevy-sys, kevy-uring) is intentionally NOT pulled in,
so kevy-embedded compiles for any target kevy-store + kevy-persist
compile for — including wasm32-unknown-unknown and wasm32-wasip1.
License
MIT OR Apache-2.0, at your option.