Async Redis client library for Lean 4.
Typed commands, RESP2/RESP3 support, native async TCP, and a design built for explicit state transitions and scripted testability.
- π Async-only public client API
- π Native Lean TCP transport built on
Std.Internal.IO.Async - π§ RESP2 and RESP3 parsing, encoding, and protocol fallback
- π§± Clear layering:
Client->Connection.Manager->Connection.Runtime-> RESP codec - π Opt-in background reconnect with fixed-interval or exponential backoff strategies
- π£ Async connection lifecycle event callbacks for disconnect and reconnect logging
- π§ͺ Transport abstraction makes mocked and scripted transports easy to use in tests
- ποΈ Typed command families for generic, strings, hashes, lists, sets, and sorted sets
- π§ͺ Scripted tests for protocol, transport, connection, and typed command decoding
- π οΈ Modular internal layout split by command family for easier review and maintenance
Core:
- RESP parser and encoder
- RESP2 / RESP3 bootstrap negotiation
- default TCP transport
- connection bootstrap and opt-in background reconnect
- async client lifecycle, reconnect events, and connection state inspection
Command families:
- π Connection:
AUTH,PING,SELECT - π Strings:
GET,SET,MGET,MSET,INCR,DECR,GETEX, and related commands - π§Ύ Hashes:
HGET,HSET,HMGET,HMSET,HGETALL,HSCAN, and related commands - π Lists:
LPUSH,RPUSH,LPOP,RPOP,LRANGE,LPOS, and related commands - π§© Sets:
SADD,SREM,SMEMBERS,SINTER,SUNION,SSCAN, and related commands - π Sorted sets:
ZADD,ZSCORE,ZRANGE,ZINTER,ZUNION,ZSCAN, and related commands - π Generic:
DEL,EXISTS,EXPIRE,TTL,KEYS,TYPE,SCAN,SORT,RENAME,COPY, and related commands
Current non-goals for v1:
- sync API
- blocking Redis command variants
- pub/sub mode
- pipelines / transactions
- cluster / sentinel support
- TLS transport
| Area | Status | Notes |
|---|---|---|
| RESP2 support | Yes | parser, encoder, bootstrap fallback |
| RESP3 support | Yes | parser, encoder, typed reply handling |
| Async client API | Yes | public API is async-only |
| Native TCP transport | Yes | built on Std.Internal.IO.Async |
| Mockable custom transports | Yes | transport is a typeclass over the concrete handle type |
| Connection bootstrap | Yes | auth, HELLO negotiation, DB select |
| Background reconnect | Yes | opt-in client-owned reconnect worker with pluggable strategies |
| Connection event callbacks | Yes | async handlers, fire-and-forget delivery |
| Generic commands | Yes | key lifecycle, lookup, and server-side operations |
| String commands | Yes | mainstream v1 coverage |
| Hash commands | Yes | includes HSCAN |
| List commands | Yes | non-blocking mainstream coverage |
| Set commands | Yes | includes SSCAN |
| Sorted set commands | Yes | includes ZSCAN |
| Scripted transport tests | Yes | protocol, runtime, manager, client |
| Pipelines / transactions | No | not part of v1 |
| Pub/Sub | No | not part of v1 |
| TLS | No | intended as future extension |
| Cluster / Sentinel | No | not part of v1 |
- Lean
4.30.0 - Lake
Toolchain is pinned in lean-toolchain.
This repository is currently install-from-source.
- Clone the repository.
- Build the project:
lake build- Build the test target:
lake build LeanRedisTestimport LeanRedis
open LeanRedis
open Std.Internal.IO.Async
def example : Async (Option String) := do
let client <- Client.newDefault {
endpoint := { host := "127.0.0.1", port := 6379 }
reconnectStrategy := .exponentialBackoff {}
}
let _ <- client.connect
let _ <- client.set "greeting" "hello"
client.get "greeting"Basic connection commands:
def pingExample : Async (Option String) := do
let client <- LeanRedis.Client.newDefault {
endpoint := { host := "127.0.0.1", port := 6379 }
}
let _ <- client.connect
client.pingReconnect and event callbacks:
def reconnectingExample : Async Unit := do
let client <- LeanRedis.Client.newDefault {
endpoint := { host := "127.0.0.1", port := 6379 }
reconnectStrategy := .exponentialBackoff {
baseDelayMs := 100
maxDelayMs := 5_000
jitter := true
}
}
let _sub <- client.onEvent fun event => do
IO.println s!"redis event: {repr event}"
let _ <- client.connect
pure ()String operations:
def stringExample : Async (Option String) := do
let client <- LeanRedis.Client.newDefault {
endpoint := { host := "127.0.0.1", port := 6379 }
}
let _ <- client.connect
let _ <- client.set "counter" "1"
let _ <- client.incr "counter"
client.get "counter"Hash operations:
def hashExample : Async (Array (String Γ String)) := do
let client <- LeanRedis.Client.newDefault {
endpoint := { host := "127.0.0.1", port := 6379 }
}
let _ <- client.connect
let _ <- client.hSet "user:1" #[("name", "alice"), ("role", "admin")]
client.hGetAll "user:1"Sorted set operations:
def sortedSetExample : Async (Array LeanRedis.SortedSetEntry) := do
let client <- LeanRedis.Client.newDefault {
endpoint := { host := "127.0.0.1", port := 6379 }
}
let _ <- client.connect
let _ <- client.zAdd "scores" #[
{ score := "10", member := "alice" },
{ score := "20", member := "bob" }
]
client.zRangeWithScores "scores" 0 (-1)Mocked transport for tests:
import LeanRedis
open LeanRedis
open Std.Internal.IO.Async
structure FakeTransport where
replies : IO.Ref (Array ByteArray)
private def popReply (ref : IO.Ref (Array ByteArray)) : IO ByteArray := do
let replies <- ref.get
match replies[0]? with
| some reply =>
ref.set (replies.extract 1 replies.size)
pure reply
| none =>
pure ByteArray.empty
instance : Transport.Transport FakeTransport where
connect _ := do
let replies <- IO.mkRef #["+PONG\r\n".toUTF8]
pure { replies }
recv transport _ := do
let bytes <- popReply transport.replies
if bytes.isEmpty then
pure { bytes := ByteArray.empty, disconnect? := some .closedByPeer }
else
pure { bytes }
send _ _ := pure ()
close _ := pure ()
def pingWithMock : Async (Option String) := do
let client : Client FakeTransport <- Client.new {
endpoint := { host := "mock", port := 0 }
}
let _ <- client.connect
client.pingThis is the same mechanism used by the library test suite for scripted bootstrap, partial replies, and disconnect scenarios.
Main public entry points:
Client.newClient.newDefaultClient.connectClient.disconnectClient.isConnectedClient.connectionStatusClient.onEventClient.offEventClient.currentState
Design notes:
new*allocates client state onlynew*isIObecause it allocates mutable client state, but it does not open a connectionconnectperforms transport setup and Redis bootstrap- commands fail fast while disconnected or reconnecting
- remote disconnects trigger background reconnect only when
reconnectStrategyis enabled onEventandoffEventare lightweightIOregistration calls; callback delivery is fire-and-forget- command methods are typed and async
- command families are split into dedicated modules internally
Build the test target with:
lake build LeanRedisTestThe test suite covers:
- RESP parser basics
- incremental parsing across fragmented inputs
- command encoding
- bootstrap encoding and negotiation behavior
- scripted transport behavior
- connection bootstrap and reconnect scenarios
- typed client decoding for all implemented command families
- runtime-level scripted partial-read and disconnect handling
Tests live under Test/ and are primarily Lean-native #guard_msgs / #eval checks.
Public modules:
LeanRedisLeanRedis.CommandLeanRedis.Client
Internal command layout:
LeanRedis/Command/Base.leanLeanRedis/Command/Connection.leanLeanRedis/Command/String.leanLeanRedis/Command/Hash.leanLeanRedis/Command/List.leanLeanRedis/Command/Set.leanLeanRedis/Command/SortedSet.leanLeanRedis/Command/Generic.lean
Internal client layout:
LeanRedis/Client/Internal.leanLeanRedis/Client/Connection.leanLeanRedis/Client/String.leanLeanRedis/Client/Hash.leanLeanRedis/Client/List.leanLeanRedis/Client/Set.leanLeanRedis/Client/SortedSet.leanLeanRedis/Client/Generic.lean
Implemented and verified:
- architecture and module boundaries
- RESP protocol support
- transport abstraction and default TCP transport
- connection management
- async public client API
- connection, generic, string, hash, list, set, and sorted-set command families
Tracking details live in docs/features/TODO.md.
MIT License. See LICENSE.