valkey_client 1.4.0
valkey_client: ^1.4.0 copied to clipboard
A modern, production-ready Dart client for Valkey (9.0.0+). Fully Redis 7.x compatible.
Valkey client #
A modern, production-ready Dart client for Valkey (9.0.0+). Fully Redis 7.x compatible.
The Goal šÆ #
The Dart ecosystem needs a high-performance, actively maintained client for the next generation of in-memory databases. This package aims to be the standard Dart client for Valkey (9.0.0+) while maintaining full compatibility with Redis (7.x+).
It is designed primarily for server-side Dart applications (server.dart) requiring a robust and fast connection to Valkey.
We also strive to maintain zero external dependencies. This is a core part of our design philosophy. It prevents dependency conflicts and ensures that valkey_client will never block your project's ability to upgrade its other packages.
Features #
- Cluster Client (v1.3.0+): Added
ValkeyClusterClientfor automatic command routing in cluster mode.- This client automatically routes commands to the correct node.
- We recommend using
ValkeyClientfor Standalone/Sentinel andValkeyClusterClientfor cluster environments. - Multi-key Support (v1.4.0+): Supports
MGETacross multiple nodes using smart Scatter-Gather pipelining.
- Built-in Connection Pooling (v1.1.0+):
ValkeyPoolfor efficient connection management (used by Standalone and Cluster clients). - Cluster Auto-Discovery (v1.2.0+): Added
client.clusterSlots()to fetch cluster topology (via theCLUSTER SLOTScommand), laying the foundation for full cluster support. - Command Timeout (v1.2.0+): Includes a built-in command timeout (via
ValkeyConnectionSettings) to prevent client hangs on non-responsive servers. - Broad Command Support:
- Strings (
GET,SET,MGET) - Hashes (
HSET,HGET,HGETALL) - Lists (
LPUSH,RPUSH,LPOP,RPOP,LRANGE) - Sets (
SADD,SREM,SMEMBERS) - Sorted Sets (
ZADD,ZREM,ZRANGE) - Key Management (
DEL,EXISTS,EXPIRE,TTL) - Transactions (
MULTI,EXEC,DISCARD) - Full Pub/Sub (
SUBSCRIBE,UNSUBSCRIBE,PSUBSCRIBE,PUNSUBSCRIBE) - Pub/Sub Introspection (
PUBSUB CHANNELS,NUMSUB,NUMPAT)
- Strings (
- Robust Parsing: Full RESP3 parser handling all core data types (
+,-,$,*,:). - Type-Safe Exceptions: Clear distinction between connection errors (
ValkeyConnectionException), server errors (ValkeyServerException), and client errors (ValkeyClientException). - Pub/Sub Ready (Standalone/Sentinel):
subscribe()returns aSubscriptionobject with aStreamand aFuture<void> readyfor easy and reliable message handling. - Production-Ready (Standalone/Sentinel):
v1.0.0is stable for production use in non-clustered environments (when used with a connection pool). This lays the foundation for the full cluster support planned for v2.0.0 (see Roadmap).
Getting Started #
Prerequisites: Running a Valkey Server #
This client requires a running Valkey server to connect to. For local development and testing, we strongly recommend using Docker.
- Install a container environment like Docker Desktop (or Rancher Desktop).
- Start a Valkey server instance by running one of the following commands in your terminal:
Option 1: No Authentication (Default)
docker run -d --name my-valkey -p 6379:6379 valkey/valkey:latest
Option 2: With Password Only
(This sets the password for the default user. Use with username: null in the client.)
docker run -d --name my-valkey-auth -p 6379:6379 valkey/valkey:latest \
--requirepass "my-super-secret-password"
Option 3: With Username and Password (ACL)
(This sets the password for the default user. Use with username: 'default' in the client.)
docker run -d --name my-valkey-acl -p 6379:6379 valkey/valkey:latest \
--user default --pass "my-super-secret-password"
- Valkey/Redis 6+ uses ACLs. The
defaultuser exists by default. To create a new user instead, simply change--user defaultto--user my-user.
(Note: The '-d' flag runs the container in "detached" mode (in the background). You can remove it if you want to see the server logs directly in your terminal.)
āļø Local Cluster Setup (for v1.3.0+ Testing) #
The Usage (Group 3) examples require a running Valkey Cluster.
Setting up a cluster on Docker Desktop (macOS/Windows) is notoriously complex due to the networking required for NAT (mapping internal container IPs to 127.0.0.1).
To solve this and encourage contributions, this repository provides a pre-configured, one-command setup file.
File Location:
This docker-compose.yml file launches a 6-node (3 Master, 3 Replica) cluster. It is already configured to handle all IP announcement (e.g., --cluster-announce-ip) and networking challenges automatically.
How to Run the Cluster:
- Download the
macos.yamlfile from the repository. - In your terminal, navigate to the file's location.
- Run
docker compose -f macos.yaml up --force-recreate. - Wait for the
cluster-initservice to logā Cluster is stable and all slots are covered!.
Your 6-node cluster is now running on 127.0.0.1:7001-7006, and you can successfully run the Usage (Group 3) examples.
Note: This configuration starts from port 7001 (instead of the common 7000) because port 7000 is often reserved by the macOS Control Center (AirPlay Receiver) service.
Usage #
valkey_client supports Standalone, Sentinel, and Cluster environments.
New users are encouraged to start with Group 1. Production applications should use Group 2 (for Standalone/Sentinel) or Group 3 (for Cluster).
Group 1: Standalone/Sentinel (Single Connection) #
This is the most basic way to connect and run commands using the ValkeyClient class. It is recommended for new users, simple tests, and scripts. (See more examples in the Example tab.)
1. Basic: Connection Patterns (from example/valkey_client_example.dart)
ValkeyClient can be configured via its constructor (fixedClient) or by passing settings to the connect() method (flexibleClient).
// 1. "Fixed Client": Constructor-based configuration
// (Match your server setup from the "Getting Started" section)
// Option 1: No Authentication
final fixedClient = ValkeyClient(host: '127.0.0.1', port: 6379);
// Option 2: Password Only
// final fixedClient = ValkeyClient(host: '127.0.0.1', port: 6379, password: 'my-super-secret-password');
// Option 3: Username + Password (ACL)
// final fixedClient = ValkeyClient(host: '127.0.0.1', port: 6379, username: 'default', password: 'my-super-secret-password');
try {
await fixedClient.connect();
print(await fixedClient.ping()); // Output: PONG
} finally {
await fixedClient.close();
}
// 2. "Flexible Client": Method-based configuration
// This pattern is useful for managing connections dynamically.
final flexibleClient = ValkeyClient(); // No config in constructor
try {
await flexibleClient.connect(
host: '127.0.0.1',
port: 6379,
// password: 'my-super-secret-password'
);
print(await flexibleClient.ping()); // Output: PONG
} finally {
await flexibleClient.close();
}
2. Standard: Basic Usage (from example/simple_example.dart)
This is the standard try-catch-finally structure to handle exceptions and ensure close() is always called.
import 'package:valkey_client/valkey_client.dart';
void main() async {
final client = ValkeyClient(host: '127.0.0.1', port: 6379);
try {
await client.connect();
await client.set('greeting', 'Hello, Valkey!');
final value = await client.get('greeting');
print(value); // Output: Hello, Valkey!
} on ValkeyConnectionException catch (e) {
print('Connection failed: $e');
} on ValkeyServerException catch (e) {
print('Server returned an error: $e');
} finally {
// Always close the connection
await client.close();
}
}
3. Application: Pub/Sub (from example/valkey_client_example.dart)
ValkeyClient supports Pub/Sub. subscribe() returns a Subscription object, and you must await sub.ready to ensure the subscription is active before publishing.
// Use two clients: one to subscribe, one to publish
final subscriber = ValkeyClient(host: '127.0.0.1', port: 6379);
final publisher = ValkeyClient(host: '127.0.0.1', port: 6379);
StreamSubscription<ValkeyMessage>? listener;
try {
await Future.wait([subscriber.connect(), publisher.connect()]);
final channel = 'news:updates';
// 1. Subscribe and get the Subscription object
final sub = subscriber.subscribe([channel]);
// 2. MUST await sub.ready before publishing
print('Waiting for subscription confirmation...');
await sub.ready.timeout(Duration(seconds: 2));
print('Subscription confirmed!');
// 3. Listen to the message stream
listener = sub.messages.listen((message) {
print('š¬ Received: ${message.message} (from channel: ${message.channel})');
});
// 4. Publish messages
await publisher.publish(channel, 'Valkey v1.3.0 has been released!');
await Future.delayed(Duration(seconds: 1)); // Wait to receive message
} catch (e) {
print('ā Pub/Sub Example Failed: $e');
} finally {
await listener?.cancel();
await Future.wait([subscriber.close(), publisher.close()]);
print('Pub/Sub clients closed.');
}
Group 2: Production Pool (Standalone/Sentinel) #
Connection Pooling (v1.1.0+)
For all applications ā and especially for production server environments with high concurrency ā it is strongly recommended to use the built-in ValkeyPool class instead of managing single ValkeyClient connections or connecting/closing individual clients.
The pool manages connections efficiently, preventing performance issues and resource exhaustion.
See below for both basic and application pooling examples for concurrent requests, including acquiring/releasing connections, handling wait queues, and choosing the right approach for your workload.
1. Basic: Pool Usage (from example/simple_pool_example.dart)
Acquire a connection with pool.acquire() and return it with pool.release().
import 'package:valkey_client/valkey_client.dart';
void main() async {
// 1. Define connection settings for the pool
final settings = ValkeyConnectionSettings(
host: '127.0.0.1',
port: 6379,
// password: 'my-super-secret-password',
);
// 2. Create a pool (e.g., max 10 connections)
final pool = ValkeyPool(connectionSettings: settings, maxConnections: 10);
ValkeyClient? client;
try {
// 3. Acquire a client from the pool
client = await pool.acquire();
// 4. Run commands
await client.set('greeting', 'Hello from ValkeyPool!');
final value = await client.get('greeting');
print(value); // Output: Hello from ValkeyPool!
} on ValkeyConnectionException catch (e) {
print('Connection or pool acquisition failed: $e');
} on ValkeyServerException catch (e) {
print('Server returned an error: $e');
} finally {
// 5. Release the client back to the pool
if (client != null) {
pool.release(client);
}
// 6. Close the pool when the application shuts down
await pool.close();
}
}
2. Application: Concurrent Pool Handling (from example/pool_example.dart)
This shows how ValkeyPool handles concurrent requests up to maxConnections and uses a wait queue when the pool is full.
import 'dart:async';
import 'package:valkey_client/valkey_client.dart';
/// Helper function to simulate a web request using the pool.
Future<void> handleRequest(ValkeyPool pool, String userId) async {
ValkeyClient? client;
try {
// 1. Acquire connection (waits if pool is full)
print('[$userId] Acquiring connection...');
client = await pool.acquire().timeout(Duration(seconds: 2));
print('[$userId] Acquired!');
// 2. Use connection
await client.set('user:$userId', 'data');
await Future.delayed(Duration(milliseconds: 500)); // Simulate work
} on ValkeyException catch (e) {
print('[$userId] Valkey Error: $e');
} on TimeoutException {
print('[$userId] Timed out waiting for a connection!');
} finally {
// 3. Release connection back to pool
if (client != null) {
print('[$userId] Releasing connection...');
pool.release(client);
}
}
}
Future<void> main() async {
final settings = ValkeyConnectionSettings(host: '127.0.0.1', port: 6379);
// Create a pool with a max of 3 connections
final pool = ValkeyPool(connectionSettings: settings, maxConnections: 3);
print('Simulating 5 concurrent requests with a pool size of 3...');
// 3. Simulate 5 concurrent requests
final futures = <Future>[
handleRequest(pool, 'UserA'),
handleRequest(pool, 'UserB'),
handleRequest(pool, 'UserC'), // These 3 get connections immediately
handleRequest(pool, 'UserD'), // This one will wait
handleRequest(pool, 'UserE'), // This one will wait
];
await Future.wait(futures);
await pool.close();
}
Group 3: Cluster Mode (Advanced) #
This group is for connecting to a Valkey Cluster Mode environment.
Recommended (v1.3.0+): Automatic Routing (from example/cluster_client_example.dart)
Use ValkeyClusterClient. This client auto-discovers the cluster topology via CLUSTER SLOTS on connect() and auto-routes commands like SET/GET to the correct node.
import 'package:valkey_client/valkey_client.dart';
void main() async {
// 1. Define initial seed nodes (only one is needed)
final initialNodes = [
ValkeyConnectionSettings(
host: '127.0.0.1',
port: 7001, // Connect to any node in the cluster
),
];
// 2. Create the cluster client
final client = ValkeyClusterClient(initialNodes);
try {
// 3. Connect (this fetches topology and builds pools)
print('Connecting to cluster...');
await client.connect();
print('ā
Cluster connected and slot map loaded.');
// 4. Run commands (will be auto-routed)
print('\nRunning SET command for "key:A"...');
await client.set('key:A', 'Hello from Node A');
print('Running SET command for "key:B"...');
await client.set('key:B', 'Hello from Node B');
print('GET response for "key:A": ${await client.get("key:A")}');
print('GET response for "key:B": ${await client.get("key:B")}');
} on ValkeyException catch (e) {
print('\nā Cluster Error: $e');
} finally {
// 5. Close all pooled cluster connections
print('\nClosing all cluster connections...');
await client.close();
}
}
Multi-key Operations (v1.4.0+): Scatter-Gather with Pipelined GETs (from example/cluster_mget_example.dart)
ValkeyClusterClient supports MGET even if keys are distributed across different nodes. It uses a Scatter-Gather strategy with pipelining to ensure high performance and correct ordering.
// Retrieve values from multiple nodes in parallel
final results = await client.mget(['key:A', 'key:B', 'key:C']);
print(results); // ['Value-A', 'Value-B', 'Value-C']
Low-Level (v1.2.0+): Manual Topology Fetch (from example/cluster_auto_discovery_example.dart)
If you need to manually inspect the topology, you can use a standard ValkeyClient (single connection) to call clusterSlots() directly.
import 'package:valkey_client/valkey_client.dart';
void main() async {
// 1. Configure a standard client to connect to ONE node
final client = ValkeyClient(host: '127.0.0.1', port: 7001);
try {
await client.connect();
print('ā
Connected to cluster node at 127.0.0.1:7001');
// 2. Run the v1.2.0 command
print('\nFetching cluster topology using CLUSTER SLOTS...');
final List<ClusterSlotRange> slotRanges = await client.clusterSlots();
// 3. Print the results
print('Cluster topology loaded. Found ${slotRanges.length} slot ranges:');
for (final range in slotRanges) {
print('--------------------');
print(' Slots: ${range.startSlot} - ${range.endSlot}');
print(' Master: ${range.master.host}:${range.master.port}');
}
} on ValkeyException catch (e) {
print('\nā Error: $e');
} finally {
await client.close();
}
}
Logging Configuration #
By default, the client logs are disabled (ValkeyLogLevel.off).
You can enable logging globally to debug connection or parsing issues.
// Enable detailed logging
ValkeyClient.setLogLevel(ValkeyLogLevel.info);
// Disable logging (default)
ValkeyClient.setLogLevel(ValkeyLogLevel.off);
Planned Features #
- Valkey 9.0.0+ Support: Full implementation of the latest commands and features.
- RESP3 Protocol: Built on the modern RESP3 protocol for richer data types and performance.
- High-Performance Async I/O: Non-blocking, asynchronous networking.
- Connection Pooling: Production-grade connection pooling suitable for high-concurrency backend servers.
- Type-Safe & Modern API: A clean, easy-to-use API for Dart developers.
More details in the Roadmap.
Contributing #
Your contributions are welcome! Please check the GitHub repository for open issues or submit a Pull Request. For major changes, please open an issue first to discuss the approach.
Maintained By #
Maintained by the developers of Visualkube at Infradise Inc. We believe in giving back to the Dart & Flutter community.
License #
This project is licensed under the Apache License 2.0.
License Change Notification (2025-10-29)
This project was initially licensed under the MIT License. As of October 29, 2025 (v0.11.0 and later), the project has been re-licensed to the Apache License 2.0.
We chose Apache 2.0 for its robust, clear, and balanced terms, which benefit both users and contributors:
- Contributor Protection (Patent Defense): Includes a defensive patent termination clause. This strongly deters users from filing patent infringement lawsuits against contributors (us).
- User Protection (Patent Grant): Explicitly grants users a patent license for any contributor patents embodied in the code, similar to MIT.
- Trademark Protection (Non-Endorsement): Includes a clause (Section 6) that restricts the use of our trademarks (like
Infradise Inc.orVisualkube), providing an effect similar to the "non-endorsement" clause in the BSD-3 license.
License Compatibility: Please note that the Apache 2.0 license is compatible with GPLv3, but it is not compatible with GPLv2.
All versions published prior to this change remain available under the MIT License. All future contributions and versions will be licensed under Apache License 2.0.