diff --git a/CHANGELOG.md b/CHANGELOG.md index a52570d4..1a802c03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 1.26.1 +- **Fix**: Use OperationQueue instead of NSLock in `DefaultTokenInterceptor` +- **Update**: AsyncOperation refactoring + ### 1.26.0 - **Add**: `TIEcommerce` module with Cart, products, promocodes, bonuses and other related actions. diff --git a/LeadKit.podspec b/LeadKit.podspec index 73d7a3ad..b9a68369 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKit" - s.version = "1.26.0" + s.version = "1.26.1" s.summary = "iOS framework with a bunch of tools for rapid development" s.homepage = "https://github.com/TouchInstinct/LeadKit" s.license = "Apache License, Version 2.0" diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index d91b18ad..180eb4ca 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 82232708..4002c291 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Login, registration, confirmation and other related actions' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index 7d799182..dfa7a5ea 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Cart, products, promocodes, bonuses and other related actions' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift index 4fd50b8b..788e4547 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift @@ -22,32 +22,14 @@ import Foundation -private final class MapAsyncOperation: AsyncOperation { - private var dependencyObservation: NSKeyValueObservation? - +private final class MapAsyncOperation: DependendAsyncOperation { public init(dependency: AsyncOperation, mapOutput: @escaping (DependencyOutput) -> Result, mapFailure: @escaping (DependencyFailure) -> Failure) { - super.init() - - cancelOnCancellation(of: dependency) - - dependencyObservation = dependency.subscribe { [weak self] in - switch mapOutput($0) { - case let .success(result): - self?.handle(result: result) - - case let .failure(error): - self?.handle(error: error) - } - } onFailure: { [weak self] in - self?.handle(error: mapFailure($0)) + super.init(dependency: dependency) { + $0.mapError(mapFailure).flatMap(mapOutput) } - - addDependency(dependency) // keeps strong reference to dependency as well - - state = .isReady } } diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift index 21fb4092..9dadfc85 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift @@ -22,46 +22,45 @@ import Foundation -private final class ClosureObserverOperation: AsyncOperation { - private var dependencyObservation: NSKeyValueObservation? +private final class ClosureObserverOperation: DependendAsyncOperation { + public typealias OnResultClosure = (Result) -> Void public init(dependency: AsyncOperation, - onSuccess: ((Output) -> Void)? = nil, - onFailure: ((Failure) -> Void)? = nil, + onResult: OnResultClosure? = nil, callbackQueue: DispatchQueue = .main) { - super.init() - - cancelOnCancellation(of: dependency) - - dependencyObservation = dependency.subscribe { [weak self] result in + super.init(dependency: dependency) { result in callbackQueue.async { - onSuccess?(result) + onResult?(result) } - - self?.handle(result: result) - } onFailure: { [weak self] error in - callbackQueue.async { - onFailure?(error) - } - - self?.handle(error: error) + return result } - - addDependency(dependency) // keeps strong reference to dependency as well - - state = .isReady } } public extension AsyncOperation { + func observe(onResult: ((Result) -> Void)? = nil, + callbackQueue: DispatchQueue = .main) -> AsyncOperation { + + ClosureObserverOperation(dependency: self, + onResult: onResult, + callbackQueue: callbackQueue) + } + func observe(onSuccess: ((Output) -> Void)? = nil, onFailure: ((Failure) -> Void)? = nil, callbackQueue: DispatchQueue = .main) -> AsyncOperation { - ClosureObserverOperation(dependency: self, - onSuccess: onSuccess, - onFailure: onFailure, - callbackQueue: callbackQueue) + let onResult: ClosureObserverOperation.OnResultClosure = { + switch $0 { + case let .success(output): + onSuccess?(output) + + case let .failure(error): + onFailure?(error) + } + } + + return observe(onResult: onResult, callbackQueue: callbackQueue) } } diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation.swift index 36a80f3d..08775afa 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation.swift @@ -25,7 +25,7 @@ import Foundation open class AsyncOperation: Operation { open var result: Result? - var state: State? = nil { // isReady == false + var state: State? = nil { // nil -> isReady == false // KVO support willSet { willChangeValue(forKey: newValue.orReady.rawValue) @@ -77,34 +77,21 @@ open class AsyncOperation: Operation { // MARK: - Methods for subclass override - open func handle(result: Output) { - self.result = .success(result) - - state = .isFinished - } - - open func handle(error: Failure) { - self.result = .failure(error) + open func handle(result: Result) { + self.result = result state = .isFinished } // MARK: - Completion observation - public func subscribe(onSuccess: ((Output) -> Void)? = nil, - onFailure: ((Failure) -> Void)? = nil) -> NSKeyValueObservation { - + public func subscribe(onResult: ((Result) -> Void)? = nil) -> NSKeyValueObservation { observe(\.isFinished, options: [.new]) { object, change in if let isFinished = change.newValue, isFinished { - switch object.result { - case let .success(result)?: - onSuccess?(result) - - case let .failure(failure)?: - onFailure?(failure) - - default: - assertionFailure("Got nil result from operation when isFinished was true!") + if let result = object.result { + onResult?(result) + } else { + assertionFailure("Got nil result from operation but isFinished is true!") } } } diff --git a/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift b/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift index 485d52c5..bb216851 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift @@ -48,13 +48,7 @@ public final class ClosureAsyncOperation: AsyncOperation super.start() cancellableTask = cancellableTaskClosure { [weak self] in - switch $0 { - case let .success(result): - self?.handle(result: result) - - case let .failure(error): - self?.handle(error: error) - } + self?.handle(result: $0) } } diff --git a/TIFoundationUtils/AsyncOperation/Sources/DependendAsyncOperation.swift b/TIFoundationUtils/AsyncOperation/Sources/DependendAsyncOperation.swift new file mode 100644 index 00000000..6efc6288 --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/DependendAsyncOperation.swift @@ -0,0 +1,45 @@ +// +// 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 Foundation + +open class DependendAsyncOperation: AsyncOperation { + public var dependencyObservation: NSKeyValueObservation? + + public init(dependency: AsyncOperation, + resultObservation: @escaping (Result) -> Result) { + + super.init() + + cancelOnCancellation(of: dependency) + + dependencyObservation = dependency.subscribe { [weak self] in + self?.result = resultObservation($0) + self?.state = .isReady + } + + addDependency(dependency) // keeps strong reference to dependency as well + + state = nil // prevent start of current operation if result is not yet provided by dependency + } + +} diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index dcd13a5d..59f3a2df 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index 7f2f1f87..b95da9af 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index e3882911..66387106 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index 84eb2fe3..045cc504 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Set of helpers for map objects clustering and interacting.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index dcd00647..a6dd0148 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/Sources/Interceptors/DefaultTokenInterceptor.swift b/TINetworking/Sources/Interceptors/DefaultTokenInterceptor.swift index a1b0e528..03a5367b 100644 --- a/TINetworking/Sources/Interceptors/DefaultTokenInterceptor.swift +++ b/TINetworking/Sources/Interceptors/DefaultTokenInterceptor.swift @@ -30,7 +30,7 @@ open class DefaultTokenInterceptor: RequestInterceptor { public typealias RequestModificationClosure = (URLRequest) -> URLRequest - let refreshLock = NSLock() + let processingQueue = OperationQueue() let shouldRefreshToken: ShouldRefreshTokenClosure let refreshTokenClosure: RefreshTokenClosure @@ -45,6 +45,8 @@ open class DefaultTokenInterceptor: RequestInterceptor { self.shouldRefreshToken = shouldRefreshTokenClosure self.refreshTokenClosure = refreshTokenClosure self.requestModificationClosure = requestModificationClosure + + processingQueue.maxConcurrentOperationCount = 1 } // MARK: - RequestAdapter @@ -62,7 +64,7 @@ open class DefaultTokenInterceptor: RequestInterceptor { let modifiedRequest = requestModificationClosure?(urlRequest) ?? urlRequest - validateAndRepair(validationClosure: { shouldRefreshToken(urlRequest, nil, nil) }, + validateAndRepair(validationClosure: { self.shouldRefreshToken(urlRequest, nil, nil) }, completion: adaptCompletion, defaultCompletionResult: modifiedRequest, recoveredCompletionResult: modifiedRequest) @@ -88,36 +90,38 @@ open class DefaultTokenInterceptor: RequestInterceptor { } } - validateAndRepair(validationClosure: { shouldRefreshToken(request.request, request.response, error) }, + validateAndRepair(validationClosure: { self.shouldRefreshToken(request.request, request.response, error) }, completion: retryCompletion, defaultCompletionResult: defaultRetryStrategy, recoveredCompletionResult: .retry) .add(to: retryBag) } - open func validateAndRepair(validationClosure: () -> Bool, + open func validateAndRepair(validationClosure: @escaping () -> Bool, completion: @escaping (Result) -> Void, defaultCompletionResult: T, recoveredCompletionResult: T) -> Cancellable { - refreshLock.lock() - - if validationClosure() { - return refreshTokenClosure { [refreshLock] in - refreshLock.unlock() - - if let error = $0 { - completion(.failure(error)) - } else { - completion(.success(recoveredCompletionResult)) + let operation = ClosureAsyncOperation(cancellableTaskClosure: { [refreshTokenClosure] operationCompletion in + if validationClosure() { + return refreshTokenClosure { + if let error = $0 { + operationCompletion(.failure(error)) + } else { + operationCompletion(.success(recoveredCompletionResult)) + } } + } else { + operationCompletion(.success(defaultCompletionResult)) + + return Cancellables.nonCancellable() } - } else { - refreshLock.unlock() + }) + .observe(onResult: completion, + callbackQueue: .global()) - completion(.success(defaultCompletionResult)) + operation.add(to: processingQueue) - return Cancellables.nonCancellable() - } + return operation } } diff --git a/TINetworking/Sources/Interceptors/EndpointResponseTokenInterceptor.swift b/TINetworking/Sources/Interceptors/EndpointResponseTokenInterceptor.swift index 31c0b1af..25a7782f 100644 --- a/TINetworking/Sources/Interceptors/EndpointResponseTokenInterceptor.swift +++ b/TINetworking/Sources/Interceptors/EndpointResponseTokenInterceptor.swift @@ -52,7 +52,7 @@ open class EndpointResponseTokenInterceptor: DefaultTokenInterceptor 'MIT', :file => 'LICENSE' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 4aaf3ecd..e6bdec93 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index ecfd469c..d0a02166 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Generic pagination component.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index f02f9b8e..502fe7ed 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Core UI elements: protocols, views and helpers..' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index a1a3a852..e116ea8e 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index b8c53df7..b494a438 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITransitions/TITransitions.podspec b/TITransitions/TITransitions.podspec index d9ba5558..27b0cb2f 100644 --- a/TITransitions/TITransitions.podspec +++ b/TITransitions/TITransitions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITransitions' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Set of custom transitions to present controller. ' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index e8025fae..3546ca16 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 86c57202..65d39a9d 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index e6db32b2..cba1b830 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.26.0' + s.version = '1.26.1' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' }