Last active
May 9, 2020 16:01
-
-
Save jdmcd/7c667abe5b331a3202c7b396b9b2ef3d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// *********** Cacheable Protocols *********** // | |
protocol Cacheable: Codable { | |
var expirationDate: Date { get set } | |
} | |
protocol CacheableDriver { | |
func nuke(key: String) -> Future<Void> | |
func get<C: Codable>(key: String) -> Future<C?> | |
func set<C: Codable>(key: String, data: C) -> Future<Void> | |
} | |
protocol CacheStore { | |
associatedtype CacheData: Cacheable | |
associatedtype Driver: CacheableDriver | |
var driver: Driver { get } | |
var cacheKey: String { get } | |
var expirationMinutes: Int { get } | |
var eventLoop: EventLoop { get } | |
func warm() -> Future<CacheData> | |
func get() -> Future<CacheData> | |
} | |
// *********** Protocol Extensions and Methods *********** // | |
extension CacheStore { | |
private func warmAndStore() -> Future<CacheData> { | |
return driver.nuke(key: cacheKey).flatMap { _ in | |
return self.warm() | |
}.flatMap { data in | |
var dataToSave = data | |
dataToSave.expirationDate = Date().addingTimeInterval(Double(self.expirationMinutes * 60)) | |
return self.driver.set(key: self.cacheKey, data: dataToSave).transform(to: dataToSave) | |
} | |
} | |
func get() -> Future<CacheData> { | |
let driverQuery: Future<CacheData?> = driver.get(key: cacheKey) | |
return driverQuery.flatMap { storedData in | |
if let storedData = storedData { | |
if storedData.needsToUpdate() { | |
// Cache is stale, nuke + warm + store | |
return self.warmAndStore() | |
} else { | |
// Return the cached data | |
return self.eventLoop.future(storedData) | |
} | |
} else { | |
// Nothing in the cache, warm it up and return | |
return self.warmAndStore() | |
} | |
} | |
} | |
} | |
extension Cacheable { | |
func needsToUpdate() -> Bool { | |
return Date() >= expirationDate | |
} | |
} | |
// *********** Example Redis Driver *********** // | |
struct RedisCacheDriver: CacheableDriver { | |
let redis: RedisClient | |
func nuke(key: String) -> EventLoopFuture<Void> { | |
return redis.delete(RedisKey(key)).transform(to: ()) | |
} | |
func get<C>(key: String) -> EventLoopFuture<C?> where C : Decodable, C : Encodable { | |
return redis.get(RedisKey(key), asJSON: C.self) | |
} | |
func set<C>(key: String, data: C) -> EventLoopFuture<Void> where C : Decodable, C : Encodable { | |
return redis.set(RedisKey(key), toJSON: data) | |
} | |
} | |
extension CacheableDriver { | |
static func redis(client: RedisClient) -> RedisCacheDriver { | |
return RedisCacheDriver(redis: client) | |
} | |
} | |
// *********** Example Usage *********** // | |
struct ReportData: Cacheable { | |
var expirationDate: Date = Date() | |
let reportDataPointOne: Double | |
let reportDataPointTwo: Double | |
} | |
struct ReportCacheStore: CacheStore { | |
// Protocol requirements | |
let expirationMinutes = 5 | |
let cacheKey: String = "my-report-data" | |
let driver: RedisCacheDriver | |
let eventLoop: EventLoop | |
// Extra services | |
let db: Database | |
let client: Client | |
func warm() -> EventLoopFuture<ReportData> { | |
// Fake implementation for now, normally this would do some kind of work on the database | |
return eventLoop.future(ReportData(reportDataPointOne: 1.0, reportDataPointTwo: 2.0)) | |
} | |
} | |
extension Request { | |
var reportCache: ReportCacheStore { | |
ReportCacheStore( | |
driver: .redis(client: self.redis), | |
eventLoop: self.eventLoop, | |
db: self.db, | |
client: self.client | |
) | |
} | |
} | |
req.reportCache.get() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment