Skip to content

Instantly share code, notes, and snippets.

@jdmcd
Last active May 9, 2020 16:01
Show Gist options
  • Save jdmcd/7c667abe5b331a3202c7b396b9b2ef3d to your computer and use it in GitHub Desktop.
Save jdmcd/7c667abe5b331a3202c7b396b9b2ef3d to your computer and use it in GitHub Desktop.
// *********** 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