Files
purchases-ios/Sources/Networking/Operations/NetworkOperation.swift
T

208 lines
5.8 KiB
Swift
Raw Normal View History

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// NetworkOperation.swift
//
// Created by Joshua Liebowitz on 11/18/21.
import Foundation
/// A type that can construct a `CacheableNetworkOperation` and pre-compute a cache key.
final class CacheableNetworkOperationFactory<T: CacheableNetworkOperation> {
let creator: (_ cacheKey: String) -> T
let cacheKey: String
var operationType: T.Type { T.self }
/**
- Parameter individualizedCacheKeyPart: The part of the cacheKey that makes it unique from other operations of the
same type. Example: If you posted receipts two times in a row you'd have 2 operations. The cache key would be
PostOperation + individualizedCacheKeyPart where individualizedCacheKeyPart is whatever you determine to be unique.
*/
init(_ creator: @escaping (_ cacheKey: String) -> T, individualizedCacheKeyPart: String) {
self.creator = creator
self.cacheKey = T.cacheKey(with: individualizedCacheKeyPart)
}
func create() -> T {
return self.creator(self.cacheKey)
}
}
class CacheableNetworkOperation: NetworkOperation, CacheKeyProviding {
let cacheKey: String
init(configuration: NetworkConfiguration, cacheKey: String) {
self.cacheKey = cacheKey
super.init(configuration: configuration)
}
fileprivate static func cacheKey(with individualizedCacheKeyPart: String) -> String {
return "\(Self.self) \(individualizedCacheKeyPart)"
}
}
2024-10-02 11:53:45 +02:00
// Restating inherited @unchecked Sendable from Foundation's Operation
extension CacheableNetworkOperation: @unchecked Sendable {}
class NetworkOperation: Operation {
let httpClient: HTTPClient
private let _didStart: Atomic<Bool> = false
private var didStart: Bool { return self._didStart.value }
// Note: implementing asynchronousy `Operations` needs KVO.
// We're not using Swift's `KeyPath` verison (`willChangeValue(for:)`)
// due to it crashing on iOS 12. See https://github.com/RevenueCat/purchases-ios/pull/2008.
private let _isExecuting: Atomic<Bool> = false
2022-02-07 19:20:16 -07:00
private(set) override final var isExecuting: Bool {
get {
return self._isExecuting.value
}
set {
self.willChangeValue(forKey: #keyPath(NetworkOperation.isExecuting))
2022-02-07 19:20:16 -07:00
self._isExecuting.value = newValue
self.didChangeValue(forKey: #keyPath(NetworkOperation.isExecuting))
2022-02-07 19:20:16 -07:00
}
}
private let _isFinished: Atomic<Bool> = false
2022-02-07 19:20:16 -07:00
private(set) override final var isFinished: Bool {
get {
return self._isFinished.value
}
set {
self.willChangeValue(forKey: #keyPath(NetworkOperation.isFinished))
2022-02-07 19:20:16 -07:00
self._isFinished.value = newValue
self.didChangeValue(forKey: #keyPath(NetworkOperation.isFinished))
2022-02-07 19:20:16 -07:00
}
}
private let _isCancelled: Atomic<Bool> = false
2022-02-07 19:20:16 -07:00
private(set) override final var isCancelled: Bool {
get {
return self._isCancelled.value
}
set {
self.willChangeValue(forKey: #keyPath(NetworkOperation.isCancelled))
2022-02-07 19:20:16 -07:00
self._isCancelled.value = newValue
self.didChangeValue(forKey: #keyPath(NetworkOperation.isCancelled))
2022-02-07 19:20:16 -07:00
}
}
init(configuration: NetworkConfiguration) {
self.httpClient = configuration.httpClient
super.init()
}
deinit {
RCTestAssert(
self.didStart,
"\(type(of: self)) was deallocated but it never started. Did it need to be created?"
)
RCTestAssert(
self.isFinished,
"\(type(of: self)) started but never finished. " +
"Did the operation not call `completion` in its `begin` implementation?"
)
}
2022-02-01 09:33:54 -08:00
override final func main() {
self._didStart.value = true
2022-02-07 19:20:16 -07:00
if self.isCancelled {
self.isFinished = true
return
}
2022-02-07 19:20:16 -07:00
self.isExecuting = true
2022-02-14 08:43:31 -07:00
self.log("Started")
2022-02-07 19:20:16 -07:00
self.begin {
self.finish()
}
}
2022-02-07 19:20:16 -07:00
override final func cancel() {
self.isCancelled = true
self.isExecuting = false
self.isFinished = true
2022-02-14 08:43:31 -07:00
self.log("Cancelled")
2022-02-07 19:20:16 -07:00
}
/// Overriden by subclasses to define the body of the operation
/// - Important: this method may be called from any thread so it must be thread-safe.
2022-02-07 19:20:16 -07:00
/// - Parameter completion: must be called when the operation has finished.
func begin(completion: @escaping () -> Void) {
fatalError("Subclasses must override this method")
}
2022-02-07 19:20:16 -07:00
private final func finish() {
assert(!self.isFinished, "Operation \(type(of: self)) (\(self)) was already finished")
2022-02-14 08:43:31 -07:00
self.log("Finished")
2022-02-07 19:20:16 -07:00
self.isExecuting = false
self.isFinished = true
}
// MARK: -
final override var isAsynchronous: Bool {
return true
}
// MARK: -
internal func log(_ message: CustomStringConvertible) {
2023-06-09 09:48:26 -07:00
Logger.debug(Strings.network.operation_state(type(of: self),
state: message.description))
2022-02-07 19:20:16 -07:00
}
// MARK: -
struct Configuration: NetworkConfiguration {
let httpClient: HTTPClient
}
struct UserSpecificConfiguration: AppUserConfiguration, NetworkConfiguration {
let httpClient: HTTPClient
let appUserID: String
}
}
2024-10-02 11:53:45 +02:00
// Restating inherited @unchecked Sendable from Foundation's Operation
extension NetworkOperation: @unchecked Sendable {}
protocol AppUserConfiguration {
var appUserID: String { get }
}
protocol NetworkConfiguration {
var httpClient: HTTPClient { get }
}