kevy-client
A URL-driven, blocking RESP client for kevy. The same code switches between an in-process backend and a remote TCP server by changing one URL string.
- Pure Rust, zero
crates.ioruntime dependencies. - Backends: in-process (anonymous, named, or persistent), kevy server, any Redis-protocol server.
- All five Redis data types, transactions with
WATCH, scan iteration, and pub/sub with a borrowing iterator.
use Connection;
let mut conn = connect?;
conn.set?;
assert_eq!;
# Ok::
Install
URL backends
| URL | Backend |
|---|---|
mem:// |
Anonymous in-process, per-open fresh; no shared bus. |
mem://<name> |
Shared in-process bus keyed by <name> — two opens of the same name share one Store + pub/sub bus, even across threads. |
file:///abs/path |
Shared in-process with snapshot + AOF persistence in path. |
kevy://host:port |
TCP RESP server, kevy-native URL scheme. |
redis://host:port |
TCP RESP server, standard Redis URL. |
tcp://host:port |
TCP RESP, raw — no SELECT round-trip on connect. |
redis://user:pass@host (AUTH) and rediss:// (TLS) are rejected up
front — kevy ships without either. Front it with a TLS-terminating
sidecar and an authentication proxy if you need them.
Quick start
Same code, two backends
use Connection;
let url = var
.unwrap_or_else;
cache_smoke?;
# Ok::
Set KEVY_URL=mem://app for dev, KEVY_URL=kevy://prod:6379 for
production. No code change.
Transactions
use Connection;
#
For optimistic concurrency, watch a key before the transaction:
use Connection;
#
Transactions on the in-process backends return ErrorKind::Unsupported
— every Connection method already serialises on the embed mutex, so
MULTI's locking guarantee is a no-op in-process.
Pub/sub
use ;
#
Pattern subscriptions use psubscribe(&[&b"news.*"[..]]).
Subscriber::messages() and Subscriber::events() are borrowing
iterators. The messages() form auto-skips the
(p)?(un)?subscribe ack frames and yields (channel, payload) tuples
directly. Both iterators terminate on UnexpectedEof; transient
errors surface as Some(Err(_)) so callers decide whether to keep
going.
Anonymous mem:// (no name) is rejected by Subscriber::connect_channels —
no other producer can reach it. Use mem://<some-name> for a shared
bus.
Cluster-aware routing
ClusterClient discovers the topology of a cluster-mode kevy server
and routes each key straight to the owning shard, eliminating the
cross-shard forwarding hop:
use ClusterClient;
let mut cc = connect?; // any shard port as seed
cc.set?; // routed by CRC16
let v = cc.get?;
let removed = cc.del?; // multi-key may span shards
# Ok::
Full cluster-mode guide: docs/cluster.md.
Drop down to the raw backend
use Connection;
#
Wrap coverage matrix (kevy server 4.x × kevy-client 2.0.0)
Status legend — wrapped: typed Connection methods; partial:
the listed subset is wrapped, the rest is raw-only; raw-only: no
typed wrap, reachable through the argv passthroughs
(Connection::pipeline, Transaction::queue, idx_*_raw, or
Connection::Remote(c) => c.request(...)); n/a: deliberately out
of scope for this client.
| Server op family (docs/verb-reference.md) | Status | Notes |
|---|---|---|
| connection (PING/ECHO/SELECT/HELLO…) | partial | ping; SELECT via URL /db; RESP3 HELLO n/a (client speaks RESP2) |
| server (INFO/CONFIG/DEBUG/COMMAND…) | raw-only | admin surface — use kevy-cli or the passthroughs |
| replication (REPLICAOF/WAIT/REPL.*) | raw-only | topology tooling, not a data-path client concern |
| scan (SCAN/KEYS/RANDOMKEY) | wrapped | scan, keys, randomkey |
| generic (DEL/EXPIRE/TTL/TYPE/RENAME/COPY…) | partial | del/exists/expire/persist/ttl_ms/type_of/dbsize/flushall; RENAME/COPY/TOUCH/GETEX… raw-only |
| tx (MULTI/EXEC/DISCARD/WATCH/UNWATCH) | wrapped | multi → Transaction (typed builders + typed reply cursor), watch, unwatch; remote-only |
| pipeline (client-side batch) | wrapped | `pipeline( |
| pubsub (SUBSCRIBE/PUBLISH/PSUBSCRIBE…) | wrapped | publish + Subscriber (recv, messages(), patterns) |
| script (EVAL/EVALSHA…) | raw-only | |
| string (SET/GET/INCR/MSET/bitmaps…) | partial | set/set_with_ttl/get/incr/incr_by/mget/mset; APPEND/GETRANGE/SETNX/bitmaps raw-only |
| hash (HSET/HGET/HGETALL…) | partial | hset/hget/hdel/hlen/hgetall/hkeys/hvals; HINCRBY/HSETNX/HSCAN… raw-only |
| hash field-TTL (HEXPIRE/HPEXPIRE/HPERSIST/HTTL) | wrapped | v2.0.0: per-field codes in request order; HPEXPIREAT raw-only |
| list (LPUSH/LRANGE/LREM…) | partial | lpush/rpush/lpop/rpop/llen/lrange; LINSERT/LSET/LTRIM… raw-only |
| blocking (BLPOP/BRPOP/BZPOPMIN/BRPOPLPUSH) | partial | v2.0.0: blpop/brpop/bzpopmin (both backends really block); BRPOPLPUSH raw-only |
| set (SADD/SMEMBERS/SINTER…) | partial | sadd/srem/smembers/scard/sismember/sinter/sunion/sdiff; SPOP/SRANDMEMBER/S*STORE raw-only |
| zset (ZADD/ZRANGE/ZSCORE…) | partial | zadd/zrem/zscore/zcard/zrange; ZCOUNT/ZRANK/ZINCRBY/ZSCAN… raw-only |
| zset algebra (Z*STORE/ZINTERCARD) | partial | v2.0.0: zinterstore/zunionstore (+_with WEIGHTS/AGGREGATE)/zintercard; ZDIFFSTORE raw-only |
| stream (XADD/XREAD/XREADGROUP…) | raw-only | |
| geo (GEOADD/GEOSEARCH…) | raw-only | |
| index (IDX.CREATE/QUERY/DROP/LIST…) | partial | v2.0.0, remote-only: idx_create_range/idx_create_raw, idx_query_range/_eq/_match/_knn/_raw, idx_drop, idx_list; COUNT/VERIFY/EXPLAIN/REBUILD via idx_query_raw-style passthrough |
| view (VIEW.*) | raw-only | |
| feed / CDC (FEED.SHARDS/TAIL/READ) | wrapped | v2.0.0: feed_shards/feed_tail/feed_read — the network face of embedded changes_since; same FEEDRESYNC error text on both backends |
| migration (RESTORE/MIGRATE…) | raw-only | |
| cluster (CLUSTER SLOTS routing) | wrapped | ClusterClient (separate type, CRC16 slot routing) |
The embedded backend serves every wrapped family except the
remote-only rows (tx, pipeline, index); for those it answers
ErrorKind::Unsupported and you drop down to Connection::Embedded's
kevy_embedded::Store typed API.
API surface
Connection / generic: ping, dbsize, flush, type_of,
exists, del, expire, persist, ttl_ms.
Strings: set, set_with_ttl, get, incr, incr_by.
Hashes: hset, hget, hdel, hlen, hgetall, hkeys,
hvals.
Lists: lpush, rpush, lpop, rpop, llen, lrange.
Sets: sadd, srem, smembers, scard, sismember.
Sorted sets: zadd, zrem, zscore, zcard, zrange.
Multi-key: mget, mset, sinter, sunion, sdiff.
Blocking pops (v2.0.0): blpop, brpop, bzpopmin — real
blocking on both backends; timeout: Option<Duration> (None =
forever; the remote socket has no read timeout, so a blocking pop
never races a client-side deadline).
Hash field-TTL (v2.0.0): hexpire, hpexpire, hpersist,
httl — per-field result codes (HExpireCode) in request order,
with the NX|XX|GT|LT condition as HExpireCond.
Zset algebra (v2.0.0): zinterstore, zunionstore (plus _with
variants exposing WEIGHTS + AGGREGATE SUM|MIN|MAX), zintercard
with optional LIMIT.
Declarative indexes (v2.0.0, remote-only): idx_create_range,
idx_create_raw, idx_query_range / idx_query_eq (paged
IdxPage), idx_query_match / idx_query_knn (ranked (key, score)), idx_query_raw, idx_drop, idx_list (IdxInfo).
Change feed / CDC (v2.0.0): feed_shards, feed_tail,
feed_read (FeedBatch of offset-tagged argv frames, prefix
filtering, FEEDRESYNC cursor-rebuild contract shared with the
embedded changes_since).
Pipelining (v2.0.0, remote-only): pipeline(|p| p.cmd(...)) —
one write, in-order replies, non-atomic.
Keyspace iteration: keys(pattern),
scan(cursor, pattern, count), randomkey. In-process backends
finish in one round (any non-zero cursor returns empty); the remote
backend honours the server's real cursor.
Transactions (remote only): Connection::multi returns a
Transaction. Queue commands via the typed builders (set, get,
del, exists, incr, incr_by, mget, mset), commit with
exec, exec_typed, exec_watched, or exec_watched_typed. The
typed cursor (exec_typed) hands back a TransactionReplies with
next_ok / next_int / next_bulk / next_array_of_bulks /
expect_empty.
Pub/sub: Connection::publish for the producer side;
Subscriber for the consumer side, with recv, recv_message,
subscribe, unsubscribe, psubscribe, punsubscribe, and the
messages() / events() iterators.
Cluster routing: ClusterClient::connect discovers topology and
routes by CRC16 slot.
Async mirror
For applications already running on tokio, smol, or async-std,
use kevy-client-async
— the API surface mirrors kevy-client exactly, with .await on
every call.
License
MIT OR Apache-2.0, at your option.