LeadKit/TINetworkingCache/Sources/EndpointCacheService.swift

175 lines
6.6 KiB
Swift

//
// 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<Content: Codable>: SingleValueStorage {
private let serializedRequest: SerializedRequest
private let multiLevelStorage: Storage<SerializedRequest, Content>
public typealias ContentRequestClosure<Failure: Error> = (@escaping (Swift.Result<Content, Failure>) -> Void) -> Cancellable
public typealias FetchContentCompletion<Failure: Error> = (Swift.Result<Content, Failure>) -> Void
public init(serializedRequest: SerializedRequest,
cacheLifetime: Expiry,
jsonCodingConfigurator: JsonCodingConfigurator,
appGroupDirectory: URL? = nil) 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,
directory: appGroupDirectory)
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<Void, Cache.StorageError> {
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<Content, Cache.StorageError> {
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<Void, Cache.StorageError> {
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<Self> {
async(on: multiLevelStorage.async.serialQueue)
}
public func getCachedOrRequest<Failure: Error>(from requestClosure: @escaping ContentRequestClosure<Failure>,
completion: @escaping FetchContentCompletion<Failure>) -> 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<Failure: Error,
AsyncStorage: AsyncSingleValueStorage>(from asyncCache: AsyncStorage,
cancellableBag: BaseCancellableBag,
requestClosure: @escaping ContentRequestClosure<Failure>,
completion: @escaping FetchContentCompletion<Failure>)
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)
}
}