// // Copyright (c) 2022 Touch Instinct // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the Software), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import TINetworking import TIFoundationUtils import Cache import Foundation public struct EndpointCacheService: SingleValueStorage { private let serializedRequest: SerializedRequest private let multiLevelStorage: Storage public typealias ContentRequestClosure = (@escaping (Swift.Result) -> Void) -> Cancellable public typealias FetchContentCompletion = (Swift.Result) -> Void public init(serializedRequest: SerializedRequest, cacheLifetime: Expiry, jsonCodingConfigurator: JsonCodingConfigurator) throws { self.serializedRequest = serializedRequest let nameWithoutLeadingSlash: String if serializedRequest.path.starts(with: "/") { nameWithoutLeadingSlash = serializedRequest.path.drop { $0 == "/" }.string } else { nameWithoutLeadingSlash = serializedRequest.path } let diskConfig = DiskConfig(name: nameWithoutLeadingSlash, expiry: cacheLifetime) let memoryConfig = MemoryConfig(expiry: cacheLifetime, countLimit: 0, totalCostLimit: 0) let transformer = Transformer { try jsonCodingConfigurator.jsonEncoder.encode($0) } fromData: { try jsonCodingConfigurator.jsonDecoder.decode(Content.self, from: $0) } self.multiLevelStorage = try Storage(diskConfig: diskConfig, memoryConfig: memoryConfig, transformer: transformer) } // MARK: - SingleValueStorage public func hasStoredValue() -> Bool { do { return try multiLevelStorage.existsObject(forKey: serializedRequest) && !(try multiLevelStorage.isExpiredObject(forKey: serializedRequest)) } catch { return false } } public func store(value: Content) -> Swift.Result { do { return .success(try multiLevelStorage.setObject(value, forKey: serializedRequest)) } catch let storageError as Cache.StorageError { return .failure(storageError) } catch { return .failure(.decodingFailed) } } public func getValue() -> Swift.Result { guard hasStoredValue() else { return .failure(.notFound) } do { return .success(try multiLevelStorage.object(forKey: serializedRequest)) } catch let storageError as Cache.StorageError { return .failure(storageError) } catch { return .failure(.encodingFailed) } } public func deleteValue() -> Swift.Result { do { return .success(try multiLevelStorage.removeObject(forKey: serializedRequest)) } catch let storageError as Cache.StorageError { return .failure(storageError) } catch { return .failure(.notFound) } } public func async() -> SingleValueStorageAsyncWrapper { async(on: multiLevelStorage.async.serialQueue) } public func getCachedOrRequest(from requestClosure: @escaping ContentRequestClosure, completion: @escaping FetchContentCompletion) -> Cancellable { Cancellables.scoped { cancellableBag in let asyncCache = async() return asyncCache.hasStoredValue { if $0 { fetchCached(from: asyncCache, cancellableBag: cancellableBag, requestClosure: requestClosure, completion: completion) } else { requestClosure { if case let .success(newValue) = $0 { _ = store(value: newValue) } completion($0) } .add(to: cancellableBag) } } } } public func fetchCached(from asyncCache: AsyncStorage, cancellableBag: BaseCancellableBag, requestClosure: @escaping ContentRequestClosure, completion: @escaping FetchContentCompletion) where AsyncStorage.ValueType == Content { guard !cancellableBag.isCancelled else { return } asyncCache.getValue { switch $0 { case let .success(cachedValue): completion(.success(cachedValue)) case .failure: guard !cancellableBag.isCancelled else { return } requestClosure { if case let .success(newValue) = $0 { _ = store(value: newValue) } completion($0) } .add(to: cancellableBag) } } .add(to: cancellableBag) } }