Hi,
I like to introduce valkey-swift a new package I've been working on alongside @fabianfett. A preview release v0.1.0 was tagged today.
What is Valkey?
Valkey is a high performance data structure server primarily used as a key/value datastore. It is a fork of Redis.
Valkey-swift
Valkey-swift is a modern swift client for Valkey, built with Swift concurrency in mind. The client includes the following features:
Commands
The code for the commands is generated from the command model files Valkey publishes. This means valkey-swift supports all of Valkey's commands (string, list, set, sortedset, stream, hash, geospatial, pub/sub, hyperloglog and bloom and json modules). It is up to date with the Valkey 8.1.3 and will be easy to update when 9.0 is released imminently. Commands are available from both the client and connection.
try await valkeyClient.set("Key1", value: "Test")
let value = try await valkeyClient.get("Key1")
or
try await valkeyClient.withConnection { connection in
try await connection.set("Key1", value: "Test")
let value = try await connection.get("Key1")
}
Pipelining
Valkey Pipelining is a technique for improving performance. It sends multiple commands at the same time without waiting for the response of each individual command. It avoids the round trip time between each command, and removes the relationship between receiving the response from a request and sending the next request.
Valkey-swift provides support for pipelining in a couple of different ways.
First, you can do this using the execute(_:)
function available from both ValkeyClient
and ValkeyConnection
.
This sends all the commands off at the same time and receives a tuple of responses.
let (lpushResult, rpopResult) = await valkeyClient.execute(
LPUSH("Key2", elements: ["entry1", "entry2"]),
RPOP("Key2")
)
let count = try lpushResult.get() // 2
let value = try rpopResult.get() // ByteBuffer containing "entry1" string
The second way to take advantage of pipelining is to use Swift Concurrency. A single ValkeyConnection
can be used across concurrent tasks without issue. Unlike the execute(_:)
function the commands will be sent individually but the sending of a command is not dependent on a previous command returning a response. With this method you can use a single connection to execute an exceptionally large number of commands.
try await valkeyClient.withConnection { connection in
try await withThrowingTaskGroup(of: Void.self) { group in
// run LPUSH and RPUSH concurrently
group.addTask {
try await connection.lpush(key: "foo1", element: ["bar"])
}
group.addTask {
try await connection.rpush(key: "foo2", element: ["baz"])
}
}
}
Pub/Sub
Valkey can be used as a message broker using its publish/subscribe messaging model. A subscription is a stream of messages from a channel. The easiest way to model this is with a Swift AsyncSequence
. The valkey-swift subscription API provides a simple way to manage subscriptions with a single function call that automatically subscribes and unsubscribes from channels as needed. You provide it with a closure, it calls SUBSCRIBE
on the channels you specified, and provides an AsyncSequence
of messages from those channels. When you exit the closure, the connection sends the relevant UNSUBSCRIBE
commands. This avoids the common user error of forgetting to unsubscribe from a channel once it is no longer needed.
try await valkeyClient.withConnection { connection in
try await connection.subscribe(channels: ["channel1"]) { subscription in
for try await event in subscription {
print(String(buffer: event.message))
}
}
}
Valkey Cluster
Valkey scales horizontally with a deployment called Valkey Cluster. Data is sharded across multiple Valkey servers based on the hash of the key being accessed. It also provides a level of availability, using replicas. You can continue operations even when a node fails or is unable to communicate.
Swift-valkey includes a cluster client ValkeyClusterClient
. This includes support for:
- Election based cluster topology discovery and maintenance.
- Command routing to the appropriate node based on key hashslots.
- Handling of MOVED errors for proper cluster resharding.
- Connection pooling and failover.
- Circuit breaking during cluster disruptions.
The following example shows how to create a cluster client that uses ValkeyStaticNodeDiscovery
to find the first node in the cluster. From there the client discovers the remains of the cluster topology.
let clusterClient = ValkeyClusterClient(
clientConfiguration: clientConfiguration,
nodeDiscovery: ValkeyStaticNodeDiscovery([
.init(host: "127.0.0.1", port: 9000, useTLS: true)
]),
logger: logger
)
All the standard Valkey commands are available to the cluster client. The only requirement is that if a command references two of more keys, they all come from the same shard in the cluster.
try await clusterClient.xread(
milliseconds: 10000,
streams: .init(key: ["events"], id: ["0-0"])
)
Redis Compatibility
Valkey uses the same protocol (RESP) as Redis. Because of this valkey-swift is compatible with Redis databases as well. The only features you won't be able to use are those that have been added to Redis since v7.2.4 (when Valkey was forked).
Check it out
The github repository can be found in the valkey-io organisation GitHub - valkey-io/valkey-swift: Valkey client written in Swift.
Documentation can be found on the Swift Package Index.