import SQLiteData enum StoreError: Error { case invalidJSON(String) case invalidURL(String) case encodingFailed case notFound } protocol StoreConvertible { static var cacheKeyPath: ReferenceWritableKeyPath { get } associatedtype StorageType static func fromValue(_ value: StorageType) throws(StoreError) -> Self static func toValue(_ value: Self) throws(StoreError) -> StorageType } extension StoreConvertible { static func get(_ cache: StoreCache, key: StoreKey) -> Self? { os_unfair_lock_lock(&cache.lock) defer { os_unfair_lock_unlock(&cache.lock) } return cache[keyPath: cacheKeyPath][key] } static func set(_ cache: StoreCache, key: StoreKey, value: Self?) { os_unfair_lock_lock(&cache.lock) defer { os_unfair_lock_unlock(&cache.lock) } cache[keyPath: cacheKeyPath][key] = value } } final class StoreCache { fileprivate var lock = os_unfair_lock() fileprivate var intCache: [StoreKey: Int] = [:] fileprivate var boolCache: [StoreKey: Bool] = [:] fileprivate var dateCache: [StoreKey: Date] = [:] fileprivate var stringCache: [StoreKey: String] = [:] fileprivate var urlCache: [StoreKey: URL] = [:] fileprivate var endpointArrayCache: [StoreKey: [Endpoint]] = [:] fileprivate var stringDictCache: [StoreKey: [String: String]] = [:] func get(_ key: StoreKey.Typed) -> T? { T.get(self, key: key.rawValue) } func set(_ key: StoreKey.Typed, value: T?) { T.set(self, key: key.rawValue, value: value) } } extension Int: StoreConvertible { static let cacheKeyPath = \StoreCache.intCache static func fromValue(_ value: Int) -> Int { value } static func toValue(_ value: Int) -> Int { value } } extension Bool: StoreConvertible { static let cacheKeyPath = \StoreCache.boolCache static func fromValue(_ value: Int) -> Bool { value == 1 } static func toValue(_ value: Bool) -> Int { value ? 1 : 0 } } extension Date: StoreConvertible { static let cacheKeyPath = \StoreCache.dateCache static func fromValue(_ value: Int) -> Date { Date(timeIntervalSince1970: TimeInterval(value) / 1000) } static func toValue(_ value: Date) -> Int { Int(value.timeIntervalSince1970 * 1000) } } extension String: StoreConvertible { static let cacheKeyPath = \StoreCache.stringCache static func fromValue(_ value: String) -> String { value } static func toValue(_ value: String) -> String { value } } extension URL: StoreConvertible { static let cacheKeyPath = \StoreCache.urlCache static func fromValue(_ value: String) throws(StoreError) -> URL { guard let url = URL(string: value) else { throw StoreError.invalidURL(value) } return url } static func toValue(_ value: URL) -> String { value.absoluteString } } extension StoreConvertible where Self: Codable, StorageType == String { static var jsonDecoder: JSONDecoder { JSONDecoder() } static var jsonEncoder: JSONEncoder { JSONEncoder() } static func fromValue(_ value: String) throws(StoreError) -> Self { do { return try jsonDecoder.decode(Self.self, from: Data(value.utf8)) } catch { throw StoreError.invalidJSON(value) } } static func toValue(_ value: Self) throws(StoreError) -> String { let encoded: Data do { encoded = try jsonEncoder.encode(value) } catch { throw StoreError.encodingFailed } guard let string = String(data: encoded, encoding: .utf8) else { throw StoreError.encodingFailed } return string } } extension Array: StoreConvertible where Element == Endpoint { static let cacheKeyPath = \StoreCache.endpointArrayCache typealias StorageType = String } extension Dictionary: StoreConvertible where Key == String, Value == String { static let cacheKeyPath = \StoreCache.stringDictCache typealias StorageType = String } extension Store { static let cache = StoreCache() static func get(_ conn: Database, _ key: StoreKey.Typed) throws -> T? where T.StorageType == Int { if let cached = cache.get(key) { return cached } let query = Store.select(\.intValue).where { $0.id.eq(key.rawValue) } if let value = try query.fetchOne(conn) ?? nil { let converted = try T.fromValue(value) cache.set(key, value: converted) } return nil } static func get(_ conn: Database, _ key: StoreKey.Typed) throws -> T? where T.StorageType == String { if let cached = cache.get(key) { return cached } let query = Store.select(\.stringValue).where { $0.id.eq(key.rawValue) } if let value = try query.fetchOne(conn) ?? nil { let converted = try T.fromValue(value) cache.set(key, value: converted) } return nil } static func set(_ conn: Database, _ key: StoreKey.Typed, value: T) throws where T.StorageType == Int { let converted = try T.toValue(value) try Store.upsert { Store(id: key.rawValue, stringValue: nil, intValue: converted) }.execute(conn) cache.set(key, value: value) } static func set(_ conn: Database, _ key: StoreKey.Typed, value: T) throws where T.StorageType == String { let converted = try T.toValue(value) try Store.upsert { Store(id: key.rawValue, stringValue: converted, intValue: nil) }.execute(conn) cache.set(key, value: value) } }