Skip to content

Commit eebb5ec

Browse files
authored
Add PersistDriver.getWithTTL (#842)
* Add PersistDriver.getWithTTL * Add memory driver implementation, add tests * Return optional tuple instead of optional object * Fix documentation
1 parent d692f70 commit eebb5ec

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

Sources/Hummingbird/Storage/MemoryPersistDriver.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@ public actor MemoryPersistDriver<C: Clock>: PersistDriver where C.Duration == Du
7272
return object
7373
}
7474

75+
/// get value and time to live for key
76+
public func getWithTTL<Object: Codable & Sendable>(key: String, as type: Object.Type) async throws -> (object: Object, ttl: Duration?)? {
77+
guard let item = self.values[key] else { return nil }
78+
if let expires = item.expires {
79+
guard self.clock.now <= expires else { return nil }
80+
guard let object = item.value as? Object else { throw PersistError.invalidConversion }
81+
return (object, self.clock.now.duration(to: expires))
82+
}
83+
guard let object = item.value as? Object else { throw PersistError.invalidConversion }
84+
return (object, nil)
85+
}
86+
7587
public func remove(key: String) async throws {
7688
self.values[key] = nil
7789
}

Sources/Hummingbird/Storage/PersistDriver.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ public protocol PersistDriver: Service {
3535
/// - as: Type you want value to be returned as. If it cannot be returned as this value then nil will be returned
3636
func get<Object: Codable & Sendable>(key: String, as: Object.Type) async throws -> Object?
3737

38+
/// get value and time to live for key
39+
///
40+
/// There is a default version of this function which only returns the value. Persist drivers have to opt
41+
/// into providing the time to live. Both Valkey and Postgres drivers will return the time to live.
42+
///
43+
/// - Parameters:
44+
/// - key: Key used to look for value
45+
/// - as: Type you want value to be returned as. If it cannot be returned as this value then nil will be returned
46+
func getWithTTL<Object: Codable & Sendable>(key: String, as: Object.Type) async throws -> (object: Object, ttl: Duration?)?
47+
3848
/// remove value associated with key
3949
/// - Parameters:
4050
/// - key: Key used to look for value
@@ -62,6 +72,15 @@ extension PersistDriver {
6272
try await self.set(key: key, value: value, expires: nil)
6373
}
6474

75+
/// get value and time to live for key
76+
/// - Parameters:
77+
/// - key: Key used to look for value
78+
/// - type: Type you want value to be returned as. If it cannot be returned as this value then nil will be returned
79+
public func getWithTTL<Object: Codable & Sendable>(key: String, as type: Object.Type) async throws -> (object: Object, ttl: Duration?)? {
80+
let value = try await get(key: key, as: type)
81+
return value.map { ($0, nil) }
82+
}
83+
6584
public func run() async throws {
6685
// ignore cancellation error as we need to shutdown
6786
try? await gracefulShutdown()

Tests/HummingbirdTests/PersistTests.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ struct PersistTests {
3737
guard let tag = context.parameters.get("tag", as: String.self) else { throw HTTPError(.badRequest) }
3838
return try await persist.get(key: tag, as: String.self)
3939
}
40+
router.get("/persist/:tag/ttl") { _, context -> String? in
41+
guard let tag = context.parameters.get("tag", as: String.self) else { throw HTTPError(.badRequest) }
42+
return try await persist.getWithTTL(key: tag, as: String.self)?.ttl.map { String($0.components.seconds) }
43+
}
4044
router.delete("/persist/:tag") { _, context -> HTTPResponse.Status in
4145
guard let tag = context.parameters.get("tag", as: String.self) else { throw HTTPError(.badRequest) }
4246
try await persist.remove(key: tag)
@@ -136,6 +140,21 @@ struct PersistTests {
136140
}
137141
}
138142

143+
@Test func testTTL() async throws {
144+
let (router, _) = try createRouter()
145+
let app = Application(responder: router.buildResponder())
146+
try await app.test(.router) { client in
147+
148+
let tag1 = UUID().uuidString
149+
150+
try await client.execute(uri: "/persist/\(tag1)/10", method: .put, body: ByteBufferAllocator().buffer(string: "ThisIsTest2")) { _ in }
151+
try await client.execute(uri: "/persist/\(tag1)/ttl", method: .get) { response in
152+
let ttl = try #require(Int(String(buffer: response.body)))
153+
#expect(ttl <= 10 && ttl > 1)
154+
}
155+
}
156+
}
157+
139158
@Test func testCodable() async throws {
140159
struct TestCodable: Codable {
141160
let buffer: String

0 commit comments

Comments
 (0)