From 04d6e28fd3819917dc7b806e5ec1ae370a442159 Mon Sep 17 00:00:00 2001
From: Adam Fowler
Date: Wed, 6 Aug 2025 14:14:42 +0100
Subject: [PATCH 1/6] Valkey
---
.github/workflows/ci.yml | 10 +-
.github/workflows/nightly.yml | 8 +-
.spi.yml | 2 +-
Package.swift | 18 +-
README.md | 36 ++--
Sources/HummingbirdRedis/Deprecations.swift | 26 ---
Sources/HummingbirdRedis/Persist+Redis.swift | 71 --------
Sources/HummingbirdRedis/Redis+Codable.swift | 66 -------
.../HummingbirdRedis/RedisConfiguration.swift | 128 --------------
.../RedisConnectionPoolService.swift | 166 ------------------
.../HummingbirdValkey/Persist+Valkey.swift | 80 +++++++++
Tests/HummingbirdRedisTests/RedisTests.swift | 81 ---------
.../PersistTests.swift | 29 ++-
13 files changed, 139 insertions(+), 582 deletions(-)
delete mode 100644 Sources/HummingbirdRedis/Deprecations.swift
delete mode 100644 Sources/HummingbirdRedis/Persist+Redis.swift
delete mode 100644 Sources/HummingbirdRedis/Redis+Codable.swift
delete mode 100644 Sources/HummingbirdRedis/RedisConfiguration.swift
delete mode 100644 Sources/HummingbirdRedis/RedisConnectionPoolService.swift
create mode 100644 Sources/HummingbirdValkey/Persist+Valkey.swift
delete mode 100644 Tests/HummingbirdRedisTests/RedisTests.swift
rename Tests/{HummingbirdRedisTests => HummingbirdValkeyTests}/PersistTests.swift (91%)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ac42aec..b9b15fb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ concurrency:
cancel-in-progress: true
env:
- REDIS_HOSTNAME: redis
+ VALKEY_HOSTNAME: valkey
jobs:
linux:
runs-on: ubuntu-latest
@@ -25,11 +25,11 @@ jobs:
container:
image: ${{ matrix.image }}
services:
- redis:
- image: redis
+ valkey:
+ image: valkey/valkey
ports:
- 6379:6379
- options: --entrypoint redis-server
+ options: --entrypoint valkey-server
steps:
- name: Checkout
@@ -40,7 +40,7 @@ jobs:
- name: Convert coverage files
run: |
llvm-cov export -format="lcov" \
- .build/debug/hummingbird-redisPackageTests.xctest \
+ .build/debug/hummingbird-valkeyPackageTests.xctest \
-ignore-filename-regex="\/Tests\/" \
-ignore-filename-regex="\/Benchmarks\/" \
-instr-profile .build/debug/codecov/default.profdata > info.lcov
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index fd0cd52..c2c8bf4 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
env:
- REDIS_HOSTNAME: redis
+ VALKEY_HOSTNAME: valkey
jobs:
linux:
runs-on: ubuntu-latest
@@ -15,11 +15,11 @@ jobs:
container:
image: swiftlang/swift:${{ matrix.image }}
services:
- redis:
- image: redis
+ valkey:
+ image: valkey/valkey
ports:
- 6379:6379
- options: --entrypoint redis-server
+ options: --entrypoint valkey-server
steps:
- name: Checkout
diff --git a/.spi.yml b/.spi.yml
index 3e0f91a..6e3a49a 100644
--- a/.spi.yml
+++ b/.spi.yml
@@ -1,3 +1,3 @@
version: 1
external_links:
- documentation: "https://docs.hummingbird.codes/2.0/documentation/hummingbirdredis"
+ documentation: "https://docs.hummingbird.codes/2.0/documentation/hummingbirdvalkey"
diff --git a/Package.swift b/Package.swift
index d491428..0c640c3 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,30 +1,30 @@
-// swift-tools-version:5.9
+// swift-tools-version:6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
- name: "hummingbird-redis",
- platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)],
+ name: "hummingbird-valkey",
+ platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)],
products: [
- .library(name: "HummingbirdRedis", targets: ["HummingbirdRedis"])
+ .library(name: "HummingbirdValkey", targets: ["HummingbirdValkey"])
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.5.0"),
- .package(url: "https://github.com/swift-server/RediStack.git", from: "1.4.0"),
+ .package(url: "https://github.com/valkey-io/valkey-swift.git", from: "0.1.0"),
],
targets: [
.target(
- name: "HummingbirdRedis",
+ name: "HummingbirdValkey",
dependencies: [
.product(name: "Hummingbird", package: "hummingbird"),
- .product(name: "RediStack", package: "RediStack"),
+ .product(name: "Valkey", package: "valkey-swift"),
]
),
.testTarget(
- name: "HummingbirdRedisTests",
+ name: "HummingbirdValkeyTests",
dependencies: [
- .byName(name: "HummingbirdRedis"),
+ .byName(name: "HummingbirdValkey"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdTesting", package: "hummingbird"),
]
diff --git a/README.md b/README.md
index cfb2173..07b4bc9 100644
--- a/README.md
+++ b/README.md
@@ -8,47 +8,51 @@
-
-
+
+
-# Hummingbird Redis Interface
+# Hummingbird Valkey/Redis Interface
-Redis is an open source, in-memory data structure store, used as a database, cache, and message broker.
+Valkey is an open source, in-memory data structure store, used as a database, cache, and message broker.
-This is the Hummingbird interface to [RediStack library](https://gitlab.com/mordil/RediStack.git) a Swift driver for Redis.
+This is the Hummingbird interface to the [valkey-swift library](https://github.com/valkey-io/valkey-swift.git) a Swift driver for Valkey/Redis. Currently HummingbirdValkey consists of driver for the Hummingbird persist framework.
## Usage
```swift
import Hummingbird
-import HummingbirdRedis
+import HummingbirdValkey
-let redis = try RedisConnectionPoolService(
- .init(hostname: redisHostname, port: 6379),
- logger: Logger(label: "Redis")
-)
+let valkey = ValkeyClient(.hostname(valkeyHostname, port: 6379), logger: Logger(label: "Valkey"))
+let persist = ValkeyPersistDriver(client: valkeyClient)
-// create router and add a single GET /redis route
+// create router and add a GET /valkey/{key} and PUT /valkey/{key} routes
let router = Router()
-router.get("redis") { request, _ -> String in
- let info = try await redis.send(command: "INFO").get()
- return String(describing: info)
+router.get("valkey/{key}") { request, context -> String? in
+ let key = try context.parameters.require("key")
+ return try await persist.get(key: .init(key), as: String.self)
+}
+router.put("valkey/{key}") { request, context in
+ let key = try context.parameters.require("key")
+ let value = try request.uri.queryParameters.require("value")
+ try await persist.set(key: key, value: value)
+ return HTTPResponse.Status.ok
}
// create application using router
var app = Application(
router: router,
configuration: .init(address: .hostname("127.0.0.1", port: 8080))
)
-app.addServices(redis)
+app.addServices(valkey)
// run hummingbird application
try await app.runService()
```
## Documentation
-Reference documentation for HummingbirdRedis can be found [here](https://docs.hummingbird.codes/2.0/documentation/hummingbirdredis)
+Reference documentation for HummingbirdValkey can be found [here](https://docs.hummingbird.codes/2.0/documentation/hummingbirdvalkey)
diff --git a/Sources/HummingbirdRedis/Deprecations.swift b/Sources/HummingbirdRedis/Deprecations.swift
deleted file mode 100644
index 07f93ec..0000000
--- a/Sources/HummingbirdRedis/Deprecations.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// This source file is part of the Hummingbird server framework project
-//
-// Copyright (c) 2024 the Hummingbird authors
-// Licensed under Apache License v2.0
-//
-// See LICENSE.txt for license information
-// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-//===----------------------------------------------------------------------===//
-
-// Below is a list of unavailable symbols with the "HB" prefix. These are available
-// temporarily to ease transition from the old symbols that included the "HB"
-// prefix to the new ones.
-//
-// This file will be removed before we do a 2.0 release
-
-@_documentation(visibility: internal) @available(*, unavailable, renamed: "RedisConfiguration")
-public typealias HBRedisConfiguration = RedisConfiguration
-@_documentation(visibility: internal) @available(*, unavailable, renamed: "RedisConnectionPoolService")
-public typealias HBRedisConnectionPoolService = RedisConnectionPoolService
-@_documentation(visibility: internal) @available(*, unavailable, renamed: "RedisPersistDriver")
-public typealias HBRedisPersistDriver = RedisPersistDriver
diff --git a/Sources/HummingbirdRedis/Persist+Redis.swift b/Sources/HummingbirdRedis/Persist+Redis.swift
deleted file mode 100644
index c2ea6f3..0000000
--- a/Sources/HummingbirdRedis/Persist+Redis.swift
+++ /dev/null
@@ -1,71 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// This source file is part of the Hummingbird server framework project
-//
-// Copyright (c) 2021-2022 the Hummingbird authors
-// Licensed under Apache License v2.0
-//
-// See LICENSE.txt for license information
-// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-//===----------------------------------------------------------------------===//
-
-import Hummingbird
-@preconcurrency import RediStack
-
-/// Redis driver for persist system for storing persistent cross request key/value pairs
-public struct RedisPersistDriver: PersistDriver {
- let redisConnectionPool: RedisConnectionPoolService
-
- public init(redisConnectionPoolService: RedisConnectionPoolService) {
- self.redisConnectionPool = redisConnectionPoolService
- }
-
- /// create new key with value. If key already exist throw `PersistError.duplicate` error
- public func create(key: String, value: some Codable, expires: Duration?) async throws {
- let expiration: RedisSetCommandExpiration? = expires.map { .seconds(Int($0.components.seconds)) }
- let result = try await self.redisConnectionPool.set(.init(key), toJSON: value, onCondition: .keyDoesNotExist, expiration: expiration)
- switch result {
- case .ok:
- return
- case .conditionNotMet:
- throw PersistError.duplicate
- }
- }
-
- /// set value for key. If value already exists overwrite it
- public func set(key: String, value: some Codable, expires: Duration?) async throws {
- if let expires {
- let expiration = Int(expires.components.seconds)
- _ = try await self.redisConnectionPool.set(
- .init(key),
- toJSON: value,
- onCondition: .none,
- expiration: .seconds(expiration)
- )
- } else {
- _ = try await self.redisConnectionPool.set(
- .init(key),
- toJSON: value,
- onCondition: .none,
- expiration: .keepExisting
- )
- }
- }
-
- /// get value for key
- public func get(key: String, as object: Object.Type) async throws -> Object? {
- do {
- return try await self.redisConnectionPool.get(.init(key), asJSON: object)
- } catch is DecodingError {
- throw PersistError.invalidConversion
- }
- }
-
- /// remove key
- public func remove(key: String) async throws {
- _ = try await self.redisConnectionPool.delete(.init(key)).get()
- }
-}
diff --git a/Sources/HummingbirdRedis/Redis+Codable.swift b/Sources/HummingbirdRedis/Redis+Codable.swift
deleted file mode 100644
index 392233f..0000000
--- a/Sources/HummingbirdRedis/Redis+Codable.swift
+++ /dev/null
@@ -1,66 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// This source file is part of the Hummingbird server framework project
-//
-// Copyright (c) 2021-2021 the Hummingbird authors
-// Licensed under Apache License v2.0
-//
-// See LICENSE.txt for license information
-// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-//===----------------------------------------------------------------------===//
-
-import Foundation
-import NIO
-@preconcurrency import RediStack
-
-extension RedisClient {
- /// Decodes the value associated with this keyfrom JSON.
- public func get(_ key: RedisKey, asJSON type: D.Type) async throws -> D? {
- guard let data = try await self.get(key, as: Data.self).get() else { return nil }
- return try JSONDecoder().decode(D.self, from: data)
- }
-
- /// Sets the value stored in the key provided, overwriting the previous value.
- ///
- /// Any previous expiration set on the key is discarded if the SET operation was successful.
- ///
- /// - Important: Regardless of the type of value stored at the key, it will be overwritten to a string value.
- ///
- /// [https://redis.io/commands/set](https://redis.io/commands/set)
- /// - Parameters:
- /// - key: The key to use to uniquely identify this value.
- /// - value: The value to set the key to.
- @inlinable
- public func set(_ key: RedisKey, toJSON value: some Encodable) async throws {
- try await self.set(key, to: JSONEncoder().encode(value)).get()
- }
-
- /// Sets the key to the provided value with options to control how it is set.
- ///
- /// [https://redis.io/commands/set](https://redis.io/commands/set)
- /// - Important: Regardless of the type of data stored at the key, it will be overwritten to a "string" data type.
- ///
- /// ie. If the key is a reference to a Sorted Set, its value will be overwritten to be a "string" data type.
- ///
- /// - Parameters:
- /// - key: The key to use to uniquely identify this value.
- /// - value: The value to set the key to.
- /// - condition: The condition under which the key should be set.
- /// - expiration: The expiration to use when setting the key. No expiration is set if `nil`.
- /// - Returns: A `NIO.EventLoopFuture` indicating the result of the operation;
- /// `.ok` if the operation was successful and `.conditionNotMet` if the specified `condition` was not met.
- ///
- /// If the condition `.none` was used, then the result value will always be `.ok`.
- @_disfavoredOverload
- public func set(
- _ key: RedisKey,
- toJSON value: some Encodable,
- onCondition condition: RedisSetCommandCondition = .none,
- expiration: RedisSetCommandExpiration? = nil
- ) async throws -> RedisSetCommandResult {
- try await self.set(key, to: JSONEncoder().encode(value), onCondition: condition, expiration: expiration).get()
- }
-}
diff --git a/Sources/HummingbirdRedis/RedisConfiguration.swift b/Sources/HummingbirdRedis/RedisConfiguration.swift
deleted file mode 100644
index 1ccce6b..0000000
--- a/Sources/HummingbirdRedis/RedisConfiguration.swift
+++ /dev/null
@@ -1,128 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// This source file is part of the Hummingbird server framework project
-//
-// Copyright (c) 2021-2021 the Hummingbird authors
-// Licensed under Apache License v2.0
-//
-// See LICENSE.txt for license information
-// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-//===----------------------------------------------------------------------===//
-
-import Hummingbird
-import Logging
-import NIOCore
-@preconcurrency import RediStack
-
-import struct Foundation.URL
-
-// Based of the Vapor redis configuration that can be found
-// here https://github.com/vapor/redis/blob/master/Sources/Redis/RedisConfiguration.swift
-
-public struct RedisConfiguration {
- public typealias ValidationError = RedisConnection.Configuration.ValidationError
-
- public var serverAddresses: [SocketAddress]
- public var password: String?
- public var database: Int?
- public var pool: PoolOptions
-
- public struct PoolOptions {
- public var maximumConnectionCount: RedisConnectionPoolSize
- public var minimumConnectionCount: Int
- public var connectionBackoffFactor: Float32
- public var initialConnectionBackoffDelay: TimeAmount
- public var connectionRetryTimeout: TimeAmount?
-
- public init(
- maximumConnectionCount: RedisConnectionPoolSize = .maximumActiveConnections(2),
- minimumConnectionCount: Int = 0,
- connectionBackoffFactor: Float32 = 2,
- initialConnectionBackoffDelay: TimeAmount = .milliseconds(100),
- connectionRetryTimeout: TimeAmount? = nil
- ) {
- self.maximumConnectionCount = maximumConnectionCount
- self.minimumConnectionCount = minimumConnectionCount
- self.connectionBackoffFactor = connectionBackoffFactor
- self.initialConnectionBackoffDelay = initialConnectionBackoffDelay
- self.connectionRetryTimeout = connectionRetryTimeout
- }
- }
-
- public init(url string: String, pool: PoolOptions = .init()) throws {
- guard let url = URL(string: string) else { throw ValidationError.invalidURLString }
- try self.init(url: url, pool: pool)
- }
-
- public init(url: URL, pool: PoolOptions = .init()) throws {
- guard
- let scheme = url.scheme,
- !scheme.isEmpty
- else { throw ValidationError.missingURLScheme }
- guard scheme == "redis" else { throw ValidationError.invalidURLScheme }
- guard let host = url.host, !host.isEmpty else { throw ValidationError.missingURLHost }
-
- try self.init(
- hostname: host,
- port: url.port ?? RedisConnection.Configuration.defaultPort,
- password: url.password,
- database: Int(url.lastPathComponent),
- pool: pool
- )
- }
-
- public init(
- hostname: String,
- port: Int = RedisConnection.Configuration.defaultPort,
- password: String? = nil,
- database: Int? = nil,
- pool: PoolOptions = .init()
- ) throws {
- if database != nil, database! < 0 { throw ValidationError.outOfBoundsDatabaseID }
-
- try self.init(
- serverAddresses: [.makeAddressResolvingHost(hostname, port: port)],
- password: password,
- database: database,
- pool: pool
- )
- }
-
- public init(
- serverAddresses: [SocketAddress],
- password: String? = nil,
- database: Int? = nil,
- pool: PoolOptions = .init()
- ) throws {
- self.serverAddresses = serverAddresses
- self.password = password
- self.database = database
- self.pool = pool
- }
-}
-
-extension RedisConnectionPool.Configuration {
- init(
- _ config: RedisConfiguration,
- logger: Logger
- ) {
- self.init(
- initialServerConnectionAddresses: config.serverAddresses,
- maximumConnectionCount: config.pool.maximumConnectionCount,
- connectionFactoryConfiguration: .init(
- connectionInitialDatabase: config.database,
- connectionPassword: config.password,
- connectionDefaultLogger: logger,
- tcpClient: nil
- ),
- minimumConnectionCount: config.pool.minimumConnectionCount,
- connectionBackoffFactor: config.pool.connectionBackoffFactor,
- initialConnectionBackoffDelay: config.pool.initialConnectionBackoffDelay,
- connectionRetryTimeout: config.pool.connectionRetryTimeout,
- poolDefaultLogger: logger
- )
- }
-}
diff --git a/Sources/HummingbirdRedis/RedisConnectionPoolService.swift b/Sources/HummingbirdRedis/RedisConnectionPoolService.swift
deleted file mode 100644
index 599c3d2..0000000
--- a/Sources/HummingbirdRedis/RedisConnectionPoolService.swift
+++ /dev/null
@@ -1,166 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// This source file is part of the Hummingbird server framework project
-//
-// Copyright (c) 2021-2023 the Hummingbird authors
-// Licensed under Apache License v2.0
-//
-// See LICENSE.txt for license information
-// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-//===----------------------------------------------------------------------===//
-
-import Foundation
-import Hummingbird
-import Logging
-import NIOCore
-@preconcurrency import RediStack
-import ServiceLifecycle
-
-/// Wrapper for RedisConnectionPool that conforms to ServiceLifecycle Service
-public struct RedisConnectionPoolService: Service, @unchecked Sendable {
- /// Initialize RedisConnectionPoolService
- public init(
- _ config: RedisConfiguration,
- eventLoopGroupProvider: EventLoopGroupProvider = .singleton,
- logger: Logger
- ) {
- let configuration: RedisConnectionPool.Configuration = .init(config, logger: logger)
- let eventLoop = eventLoopGroupProvider.eventLoopGroup.any()
- self.pool = .init(configuration: configuration, boundEventLoop: eventLoop)
- }
-
- public let pool: RedisConnectionPool
-
- @inlinable
- public func run() async throws {
- /// Ignore cancellation error
- try? await gracefulShutdown()
- try await self.close()
- }
-
- /// Starts the connection pool.
- ///
- /// This method is safe to call multiple times.
- /// - Parameter logger: An optional logger to use for any log statements generated while starting up the pool.
- /// If one is not provided, the pool will use its default logger.
- @inlinable
- public func activate(logger: Logger? = nil) {
- self.pool.activate(logger: logger)
- }
-
- /// Closes all connections in the pool and deactivates the pool from creating new connections.
- ///
- /// This method is safe to call multiple times.
- @inlinable
- public func close() async throws {
- let promise = self.eventLoop.makePromise(of: Void.self)
- self.pool.close(promise: promise)
- return try await promise.futureResult.get()
- }
-}
-
-extension RedisConnectionPoolService {
- /// A unique identifer to represent this connection.
- @inlinable
- public var id: UUID { self.pool.id }
- /// The count of connections that are active and available for use.
- @inlinable
- public var availableConnectionCount: Int { self.pool.availableConnectionCount }
- /// The number of connections that have been handed out and are in active use.
- @inlinable
- public var leasedConnectionCount: Int { self.pool.leasedConnectionCount }
- /// Provides limited exclusive access to a connection to be used in a user-defined specialized closure of operations.
- /// - Warning: Attempting to create PubSub subscriptions with connections leased in the closure will result in a failed `NIO.EventLoopFuture`.
- ///
- /// `RedisConnectionPool` manages PubSub state and requires exclusive control over creating PubSub subscriptions.
- /// - Important: This connection **MUST NOT** be stored outside of the closure. It is only available exclusively within the closure.
- ///
- /// All operations should be done inside the closure as chained `NIO.EventLoopFuture` callbacks.
- ///
- /// For example:
- /// ```swift
- /// let countFuture = pool.leaseConnection {
- /// let client = $0.logging(to: myLogger)
- /// return client.authorize(with: userPassword)
- /// .flatMap { connection.select(database: userDatabase) }
- /// .flatMap { connection.increment(counterKey) }
- /// }
- /// ```
- /// - Warning: Some commands change the state of the connection that are not tracked client-side,
- /// and will not be automatically reset when the connection is returned to the pool.
- ///
- /// When the connection is reused from the pool, it will retain this state and may affect future commands executed with it.
- ///
- /// For example, if `select(database:)` is used, all future commands made with this connection will be against the selected database.
- ///
- /// To protect against future issues, make sure the final commands executed are to reset the connection to it's previous known state.
- /// - Parameter operation: A closure that receives exclusive access to the provided `RedisConnection` for the lifetime of the closure for specialized Redis command chains.
- /// - Returns: A `NIO.EventLoopFuture` that resolves the value of the `NIO.EventLoopFuture` in the provided closure operation.
- @inlinable
- public func leaseConnection(_ operation: @escaping (RedisConnection) -> EventLoopFuture) -> EventLoopFuture {
- self.pool.leaseConnection(operation)
- }
-
- /// Updates the list of valid connection addresses.
- /// - Warning: This will replace any previously set list of addresses.
- /// - Note: This does not invalidate existing connections: as long as those connections continue to stay up, they will be kept by
- /// this client.
- ///
- /// However, no new connections will be made to any endpoint that is not in `newAddresses`.
- /// - Parameters:
- /// - newAddresses: The new addresses to connect to in future connections.
- /// - logger: An optional logger to use for any log statements generated while updating the target addresses.
- /// If one is not provided, the pool will use its default logger.
- @inlinable
- public func updateConnectionAddresses(_ newAddresses: [SocketAddress], logger: Logger? = nil) {
- self.pool.updateConnectionAddresses(newAddresses)
- }
-}
-
-extension RedisConnectionPoolService: RedisClient {
- @inlinable
- public var eventLoop: NIOCore.EventLoop { self.pool.eventLoop }
-
- @inlinable
- public func send(command: String, with arguments: [RediStack.RESPValue]) -> NIOCore.EventLoopFuture {
- self.pool.send(command: command, with: arguments)
- }
-
- @inlinable
- public func logging(to logger: Logging.Logger) -> RediStack.RedisClient {
- self.pool.logging(to: logger)
- }
-
- @inlinable
- public func subscribe(
- to channels: [RedisChannelName],
- messageReceiver receiver: @escaping RedisSubscriptionMessageReceiver,
- onSubscribe subscribeHandler: RedisSubscriptionChangeHandler?,
- onUnsubscribe unsubscribeHandler: RedisSubscriptionChangeHandler?
- ) -> EventLoopFuture {
- self.pool.subscribe(to: channels, messageReceiver: receiver, onSubscribe: subscribeHandler, onUnsubscribe: unsubscribeHandler)
- }
-
- @inlinable
- public func psubscribe(
- to patterns: [String],
- messageReceiver receiver: @escaping RedisSubscriptionMessageReceiver,
- onSubscribe subscribeHandler: RedisSubscriptionChangeHandler?,
- onUnsubscribe unsubscribeHandler: RedisSubscriptionChangeHandler?
- ) -> EventLoopFuture {
- self.pool.psubscribe(to: patterns, messageReceiver: receiver, onSubscribe: subscribeHandler, onUnsubscribe: unsubscribeHandler)
- }
-
- @inlinable
- public func unsubscribe(from channels: [RediStack.RedisChannelName]) -> NIOCore.EventLoopFuture {
- self.pool.unsubscribe(from: channels)
- }
-
- @inlinable
- public func punsubscribe(from patterns: [String]) -> NIOCore.EventLoopFuture {
- self.pool.punsubscribe(from: patterns)
- }
-}
diff --git a/Sources/HummingbirdValkey/Persist+Valkey.swift b/Sources/HummingbirdValkey/Persist+Valkey.swift
new file mode 100644
index 0000000..0d593c1
--- /dev/null
+++ b/Sources/HummingbirdValkey/Persist+Valkey.swift
@@ -0,0 +1,80 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Hummingbird server framework project
+//
+// Copyright (c) 2021-2022 the Hummingbird authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import Hummingbird
+import Valkey
+
+#if canImport(FoundationEssentials)
+import FoundationEssentials
+#else
+import Foundation
+#endif
+
+/// Valkey/Redis driver for persist system for storing persistent cross request key/value pairs
+public struct ValkeyPersistDriver: PersistDriver {
+ let valkey: Client
+
+ public init(client: Client) {
+ self.valkey = client
+ }
+
+ /// create new key with value. If key already exist throw `PersistError.duplicate` error
+ public func create(key: String, value: some Codable, expires: Duration?) async throws {
+ let expiration: SET.Expiration? = expires.map { .milliseconds(Int($0 / .milliseconds(1))) }
+ let jsonBuffer = try ByteBuffer(bytes: JSONEncoder().encode(value))
+ if try await self.valkey.set(.init(key), value: jsonBuffer, condition: .nx, expiration: expiration) != nil {
+ return
+ } else {
+ throw PersistError.duplicate
+ }
+ }
+
+ /// set value for key. If value already exists overwrite it
+ public func set(key: String, value: some Codable, expires: Duration?) async throws {
+ let jsonBuffer = try ByteBuffer(bytes: JSONEncoder().encode(value))
+ if let expires {
+ let expiration = SET.Expiration.milliseconds(Int(expires / .milliseconds(1)))
+ _ = try await self.valkey.set(
+ .init(key),
+ value: jsonBuffer,
+ condition: .none,
+ expiration: expiration
+ )
+ } else {
+ _ = try await self.valkey.set(
+ .init(key),
+ value: jsonBuffer,
+ condition: .none,
+ expiration: .keepttl
+ )
+ }
+ }
+
+ /// get value for key
+ public func get(key: String, as object: Object.Type) async throws -> Object? {
+ do {
+ if let value = try await self.valkey.get(.init(key)) {
+ return try JSONDecoder().decode(Object.self, from: value)
+ }
+ return nil
+ } catch is DecodingError {
+ throw PersistError.invalidConversion
+ }
+ }
+
+ /// remove key
+ public func remove(key: String) async throws {
+ _ = try await self.valkey.del(keys: [.init(key)])
+ }
+}
diff --git a/Tests/HummingbirdRedisTests/RedisTests.swift b/Tests/HummingbirdRedisTests/RedisTests.swift
deleted file mode 100644
index 982ab26..0000000
--- a/Tests/HummingbirdRedisTests/RedisTests.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// This source file is part of the Hummingbird server framework project
-//
-// Copyright (c) 2021-2021 the Hummingbird authors
-// Licensed under Apache License v2.0
-//
-// See LICENSE.txt for license information
-// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-//===----------------------------------------------------------------------===//
-
-import Hummingbird
-import HummingbirdTesting
-import Logging
-import NIOPosix
-@preconcurrency import RediStack
-import XCTest
-
-@testable import HummingbirdRedis
-
-final class HummingbirdRedisTests: XCTestCase {
- static let env = Environment()
- static let redisHostname = env.get("REDIS_HOSTNAME") ?? "localhost"
-
- func testConnectionPoolService() async throws {
- let redis = try RedisConnectionPoolService(
- .init(hostname: Self.redisHostname, port: 6379),
- logger: Logger(label: "Redis")
- )
-
- let info = try await redis.send(command: "INFO").get()
- XCTAssertEqual(info.string?.contains("redis_version"), true)
-
- try await redis.close()
- }
-
- func testSubscribe() async throws {
- let expectation = XCTestExpectation(description: "Waiting on subscription")
- expectation.expectedFulfillmentCount = 1
- let redis = try RedisConnectionPoolService(
- .init(hostname: Self.redisHostname, port: 6379),
- logger: Logger(label: "Redis")
- )
- let redis2 = try RedisConnectionPoolService(
- .init(hostname: Self.redisHostname, port: 6379),
- logger: Logger(label: "Redis")
- )
-
- _ = try await redis.subscribe(to: ["channel"]) { _, value in
- XCTAssertEqual(value, .init(from: "hello"))
- expectation.fulfill()
- }.get()
- _ = try await redis2.publish("hello", to: "channel").get()
- await fulfillment(of: [expectation], timeout: 5)
- _ = try await redis.unsubscribe(from: ["channel"]).get()
- try await redis.close()
- try await redis2.close()
- }
-
- func testRouteHandler() async throws {
- let redis = try RedisConnectionPoolService(
- .init(hostname: Self.redisHostname, port: 6379),
- logger: Logger(label: "Redis")
- )
- let router = Router()
- router.get("redis") { _, _ in
- try await redis.send(command: "INFO").map(\.description).get()
- }
- var app = Application(responder: router.buildResponder())
- app.addServices(redis)
- try await app.test(.live) { client in
- try await client.execute(uri: "/redis", method: .get) { response in
- var body = try XCTUnwrap(response.body)
- XCTAssertEqual(body.readString(length: body.readableBytes)?.contains("redis_version"), true)
- }
- }
- }
-}
diff --git a/Tests/HummingbirdRedisTests/PersistTests.swift b/Tests/HummingbirdValkeyTests/PersistTests.swift
similarity index 91%
rename from Tests/HummingbirdRedisTests/PersistTests.swift
rename to Tests/HummingbirdValkeyTests/PersistTests.swift
index 53f6e44..e19504d 100644
--- a/Tests/HummingbirdRedisTests/PersistTests.swift
+++ b/Tests/HummingbirdValkeyTests/PersistTests.swift
@@ -13,22 +13,33 @@
//===----------------------------------------------------------------------===//
import Hummingbird
-import HummingbirdRedis
import HummingbirdTesting
+import HummingbirdValkey
import Logging
-@preconcurrency import RediStack
+import Valkey
import XCTest
final class PersistTests: XCTestCase {
- static let redisHostname = Environment().get("REDIS_HOSTNAME") ?? "localhost"
+ static let valkeyHostname = Environment().get("VALKEY_HOSTNAME") ?? "localhost"
func createApplication(_ updateRouter: (Router, PersistDriver) -> Void = { _, _ in }) throws -> some ApplicationProtocol {
let router = Router()
- let redisConnectionPool = try RedisConnectionPoolService(
- .init(hostname: Self.redisHostname, port: 6379),
- logger: Logger(label: "Redis")
- )
- let persist = RedisPersistDriver(redisConnectionPoolService: redisConnectionPool)
+ var logger = Logger(label: "Valkey")
+ logger.logLevel = .debug
+ let valkeyClient = ValkeyClient(.hostname(Self.valkeyHostname, port: 6379), logger: logger)
+ let persist = ValkeyPersistDriver(client: valkeyClient)
+
+ router.get("valkey/{key}") { request, context -> String? in
+ let key = try context.parameters.require("key")
+ return try await persist.get(key: .init(key), as: String.self)
+ }
+
+ router.put("valkey/{key}") { request, context in
+ let key = try context.parameters.require("key")
+ let value = try request.uri.queryParameters.require("value")
+ try await persist.set(key: key, value: value)
+ return HTTPResponse.Status.ok
+ }
router.put("/persist/:tag") { request, context -> HTTPResponse.Status in
let buffer = try await request.body.collect(upTo: .max)
@@ -54,7 +65,7 @@ final class PersistTests: XCTestCase {
}
updateRouter(router, persist)
var app = Application(responder: router.buildResponder())
- app.addServices(redisConnectionPool, persist)
+ app.addServices(valkeyClient, persist)
return app
}
From 9f94d0dd85517193de14a270979b7ac6ac9d4487 Mon Sep 17 00:00:00 2001
From: Adam Fowler
Date: Wed, 6 Aug 2025 14:23:38 +0100
Subject: [PATCH 2/6] 2025
---
Sources/HummingbirdValkey/Persist+Valkey.swift | 2 +-
Tests/HummingbirdValkeyTests/PersistTests.swift | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Sources/HummingbirdValkey/Persist+Valkey.swift b/Sources/HummingbirdValkey/Persist+Valkey.swift
index 0d593c1..28a04ce 100644
--- a/Sources/HummingbirdValkey/Persist+Valkey.swift
+++ b/Sources/HummingbirdValkey/Persist+Valkey.swift
@@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
-// Copyright (c) 2021-2022 the Hummingbird authors
+// Copyright (c) 2021-2025 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
diff --git a/Tests/HummingbirdValkeyTests/PersistTests.swift b/Tests/HummingbirdValkeyTests/PersistTests.swift
index e19504d..29f613e 100644
--- a/Tests/HummingbirdValkeyTests/PersistTests.swift
+++ b/Tests/HummingbirdValkeyTests/PersistTests.swift
@@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
-// Copyright (c) 2021-2021 the Hummingbird authors
+// Copyright (c) 2021-2025 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
From 1cfcfd62f854082a67a7a3bf9276891bebf38a60 Mon Sep 17 00:00:00 2001
From: Adam Fowler
Date: Wed, 6 Aug 2025 14:36:38 +0100
Subject: [PATCH 3/6] Update CI
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b9b15fb..0cbba0c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 15
strategy:
matrix:
- image: ["swift:5.10", "swift:6.0", "swift:6.1", "swiftlang/swift:nightly-6.2-noble"]
+ image: ["swift:6.1", "swiftlang/swift:nightly-6.2-noble"]
container:
image: ${{ matrix.image }}
services:
From fe44e95b17ad1861e7d6b20fbbfc94cf8269a845 Mon Sep 17 00:00:00 2001
From: Adam Fowler
Date: Thu, 11 Sep 2025 17:00:21 +0100
Subject: [PATCH 4/6] No point converting to ByteBuffer when you can use Data
---
Package.swift | 2 +-
Sources/HummingbirdValkey/Persist+Valkey.swift | 9 ++++-----
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/Package.swift b/Package.swift
index 0c640c3..935f585 100644
--- a/Package.swift
+++ b/Package.swift
@@ -11,7 +11,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.5.0"),
- .package(url: "https://github.com/valkey-io/valkey-swift.git", from: "0.1.0"),
+ .package(url: "https://github.com/valkey-io/valkey-swift.git", from: "0.2.0"),
],
targets: [
.target(
diff --git a/Sources/HummingbirdValkey/Persist+Valkey.swift b/Sources/HummingbirdValkey/Persist+Valkey.swift
index 28a04ce..daa34d6 100644
--- a/Sources/HummingbirdValkey/Persist+Valkey.swift
+++ b/Sources/HummingbirdValkey/Persist+Valkey.swift
@@ -31,8 +31,8 @@ public struct ValkeyPersistDriver: Pers
/// create new key with value. If key already exist throw `PersistError.duplicate` error
public func create(key: String, value: some Codable, expires: Duration?) async throws {
- let expiration: SET.Expiration? = expires.map { .milliseconds(Int($0 / .milliseconds(1))) }
- let jsonBuffer = try ByteBuffer(bytes: JSONEncoder().encode(value))
+ let expiration: SET.Expiration? = expires.map { .milliseconds(Int($0 / .milliseconds(1))) }
+ let jsonBuffer = try JSONEncoder().encode(value)
if try await self.valkey.set(.init(key), value: jsonBuffer, condition: .nx, expiration: expiration) != nil {
return
} else {
@@ -42,14 +42,13 @@ public struct ValkeyPersistDriver: Pers
/// set value for key. If value already exists overwrite it
public func set(key: String, value: some Codable, expires: Duration?) async throws {
- let jsonBuffer = try ByteBuffer(bytes: JSONEncoder().encode(value))
+ let jsonBuffer = try JSONEncoder().encode(value)
if let expires {
- let expiration = SET.Expiration.milliseconds(Int(expires / .milliseconds(1)))
_ = try await self.valkey.set(
.init(key),
value: jsonBuffer,
condition: .none,
- expiration: expiration
+ expiration: .milliseconds(Int(expires / .milliseconds(1)))
)
} else {
_ = try await self.valkey.set(
From f4ab0e6a231451d8bfed03df43df6bec4a5ac178 Mon Sep 17 00:00:00 2001
From: Adam Fowler
Date: Fri, 12 Sep 2025 10:54:18 +0100
Subject: [PATCH 5/6] Add JSONDecoder ByteBuffer
---
.../Codable+ByteBuffer.swift | 124 ++++++++++++++++++
.../HummingbirdValkey/Persist+Valkey.swift | 2 +-
2 files changed, 125 insertions(+), 1 deletion(-)
create mode 100644 Sources/HummingbirdValkey/Codable+ByteBuffer.swift
diff --git a/Sources/HummingbirdValkey/Codable+ByteBuffer.swift b/Sources/HummingbirdValkey/Codable+ByteBuffer.swift
new file mode 100644
index 0000000..e25926c
--- /dev/null
+++ b/Sources/HummingbirdValkey/Codable+ByteBuffer.swift
@@ -0,0 +1,124 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2019-2021 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIOCore
+
+#if canImport(FoundationEssentials)
+import FoundationEssentials
+#else
+import Foundation
+#endif
+
+extension ByteBuffer {
+ /// Controls how bytes are transferred between `ByteBuffer` and other storage types.
+ @usableFromInline
+ enum _ByteTransferStrategy: Sendable {
+ /// Force a copy of the bytes.
+ case copy
+
+ /// Do not copy the bytes if at all possible.
+ case noCopy
+
+ /// Use a heuristic to decide whether to copy the bytes or not.
+ case automatic
+ }
+
+ /// Return `length` bytes starting at `index` and return the result as `Data`. This will not change the reader index.
+ /// The selected bytes must be readable or else `nil` will be returned.
+ ///
+ /// - Parameters:
+ /// - index: The starting index of the bytes of interest into the `ByteBuffer`
+ /// - length: The number of bytes of interest
+ /// - byteTransferStrategy: Controls how to transfer the bytes. See `ByteTransferStrategy` for an explanation
+ /// of the options.
+ /// - Returns: A `Data` value containing the bytes of interest or `nil` if the selected bytes are not readable.
+ @usableFromInline
+ func _getData(at index: Int, length: Int, byteTransferStrategy: _ByteTransferStrategy) -> Data? {
+ let index = index - self.readerIndex
+ guard index >= 0 && length >= 0 && index <= self.readableBytes - length else {
+ return nil
+ }
+ let doCopy: Bool
+ switch byteTransferStrategy {
+ case .copy:
+ doCopy = true
+ case .noCopy:
+ doCopy = false
+ case .automatic:
+ doCopy = length <= 256 * 1024
+ }
+
+ return self.withUnsafeReadableBytesWithStorageManagement { ptr, storageRef in
+ if doCopy {
+ return Data(
+ bytes: UnsafeMutableRawPointer(mutating: ptr.baseAddress!.advanced(by: index)),
+ count: Int(length)
+ )
+ } else {
+ let storage = storageRef.takeUnretainedValue()
+ return Data(
+ bytesNoCopy: UnsafeMutableRawPointer(mutating: ptr.baseAddress!.advanced(by: index)),
+ count: Int(length),
+ deallocator: .custom { _, _ in withExtendedLifetime(storage) {} }
+ )
+ }
+ }
+ }
+
+ /// Attempts to decode the `length` bytes from `index` using the `JSONDecoder` `decoder` as `T`.
+ ///
+ /// - Parameters:
+ /// - type: The type type that is attempted to be decoded.
+ /// - decoder: The `JSONDecoder` that is used for the decoding.
+ /// - index: The index of the first byte to decode.
+ /// - length: The number of bytes to decode.
+ /// - Returns: The decoded value if successful or `nil` if there are not enough readable bytes available.
+ @usableFromInline
+ func _getJSONDecodable(
+ _ type: T.Type,
+ decoder: JSONDecoder = JSONDecoder(),
+ at index: Int,
+ length: Int
+ ) throws -> T? {
+ guard let data = self._getData(at: index, length: length, byteTransferStrategy: .noCopy) else {
+ return nil
+ }
+ return try decoder.decode(T.self, from: data)
+ }
+}
+
+extension JSONDecoder {
+ /// Returns a value of the type you specify, decoded from a JSON object inside the readable bytes of a `ByteBuffer`.
+ ///
+ /// If the `ByteBuffer` does not contain valid JSON, this method throws the
+ /// `DecodingError.dataCorrupted(_:)` error. If a value within the JSON
+ /// fails to decode, this method throws the corresponding error.
+ ///
+ /// - Note: The provided `ByteBuffer` remains unchanged, neither the `readerIndex` nor the `writerIndex` will move.
+ /// If you would like the `readerIndex` to move, consider using `ByteBuffer.readJSONDecodable(_:length:)`.
+ ///
+ /// - Parameters:
+ /// - type: The type of the value to decode from the supplied JSON object.
+ /// - buffer: The `ByteBuffer` that contains JSON object to decode.
+ /// - Returns: The decoded object.
+ @usableFromInline
+ func _decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T {
+ try buffer.getJSONDecodable(
+ T.self,
+ decoder: self,
+ at: buffer.readerIndex,
+ length: buffer.readableBytes
+ )! // must work, enough readable bytes// must work, enough readable bytes
+ }
+}
diff --git a/Sources/HummingbirdValkey/Persist+Valkey.swift b/Sources/HummingbirdValkey/Persist+Valkey.swift
index daa34d6..c356683 100644
--- a/Sources/HummingbirdValkey/Persist+Valkey.swift
+++ b/Sources/HummingbirdValkey/Persist+Valkey.swift
@@ -64,7 +64,7 @@ public struct ValkeyPersistDriver: Pers
public func get(key: String, as object: Object.Type) async throws -> Object? {
do {
if let value = try await self.valkey.get(.init(key)) {
- return try JSONDecoder().decode(Object.self, from: value)
+ return try JSONDecoder()._decode(Object.self, from: value)
}
return nil
} catch is DecodingError {
From a21817e48d164da509e3d20538b37d4ef119a6a8 Mon Sep 17 00:00:00 2001
From: Adam Fowler
Date: Fri, 12 Sep 2025 14:58:58 +0000
Subject: [PATCH 6/6] Fix linux, remove products from Package.swift
---
Notice.txt | 20 +++++++++++++++++++
Package.swift | 13 +++++++++---
.../Codable+ByteBuffer.swift | 2 +-
.../HummingbirdValkey/Persist+Valkey.swift | 1 +
.../HummingbirdValkeyTests/PersistTests.swift | 1 +
5 files changed, 33 insertions(+), 4 deletions(-)
create mode 100644 Notice.txt
diff --git a/Notice.txt b/Notice.txt
new file mode 100644
index 0000000..aeb43d6
--- /dev/null
+++ b/Notice.txt
@@ -0,0 +1,20 @@
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the Hummingbird server framework project
+##
+## Copyright (c) 2021-2025 the Hummingbird authors
+## Licensed under Apache License v2.0
+##
+## See LICENSE.txt for license information
+## See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
+##
+## SPDX-License-Identifier: Apache-2.0
+##
+##===----------------------------------------------------------------------===##
+
+This product contains ByteBuffer Codable support from apple/swift-nio
+
+* LICENSE (Apache License 2.0):
+ * https://github.com/apple/swift-nio/blob/main/LICENSE.txt
+* HOMEPAGE
+ * https://github.com/apple/swift-nio
\ No newline at end of file
diff --git a/Package.swift b/Package.swift
index 935f585..e7e0580 100644
--- a/Package.swift
+++ b/Package.swift
@@ -3,9 +3,14 @@
import PackageDescription
+let defaultSwiftSettings: [SwiftSetting] =
+ [
+ .swiftLanguageMode(.v6),
+ .enableExperimentalFeature("AvailabilityMacro=hbValkey 1.0:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"),
+ ]
+
let package = Package(
name: "hummingbird-valkey",
- platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)],
products: [
.library(name: "HummingbirdValkey", targets: ["HummingbirdValkey"])
],
@@ -19,7 +24,8 @@ let package = Package(
dependencies: [
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "Valkey", package: "valkey-swift"),
- ]
+ ],
+ swiftSettings: defaultSwiftSettings
),
.testTarget(
name: "HummingbirdValkeyTests",
@@ -27,7 +33,8 @@ let package = Package(
.byName(name: "HummingbirdValkey"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdTesting", package: "hummingbird"),
- ]
+ ],
+ swiftSettings: defaultSwiftSettings
),
]
)
diff --git a/Sources/HummingbirdValkey/Codable+ByteBuffer.swift b/Sources/HummingbirdValkey/Codable+ByteBuffer.swift
index e25926c..f499ad3 100644
--- a/Sources/HummingbirdValkey/Codable+ByteBuffer.swift
+++ b/Sources/HummingbirdValkey/Codable+ByteBuffer.swift
@@ -114,7 +114,7 @@ extension JSONDecoder {
/// - Returns: The decoded object.
@usableFromInline
func _decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T {
- try buffer.getJSONDecodable(
+ try buffer._getJSONDecodable(
T.self,
decoder: self,
at: buffer.readerIndex,
diff --git a/Sources/HummingbirdValkey/Persist+Valkey.swift b/Sources/HummingbirdValkey/Persist+Valkey.swift
index c356683..d892a65 100644
--- a/Sources/HummingbirdValkey/Persist+Valkey.swift
+++ b/Sources/HummingbirdValkey/Persist+Valkey.swift
@@ -22,6 +22,7 @@ import Foundation
#endif
/// Valkey/Redis driver for persist system for storing persistent cross request key/value pairs
+@available(hbValkey 1.0, *)
public struct ValkeyPersistDriver: PersistDriver {
let valkey: Client
diff --git a/Tests/HummingbirdValkeyTests/PersistTests.swift b/Tests/HummingbirdValkeyTests/PersistTests.swift
index 29f613e..8dec15a 100644
--- a/Tests/HummingbirdValkeyTests/PersistTests.swift
+++ b/Tests/HummingbirdValkeyTests/PersistTests.swift
@@ -19,6 +19,7 @@ import Logging
import Valkey
import XCTest
+@available(hbValkey 1.0, *)
final class PersistTests: XCTestCase {
static let valkeyHostname = Environment().get("VALKEY_HOSTNAME") ?? "localhost"