diff --git a/CHANGELOG.md b/CHANGELOG.md index 067541a..f04d8d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ## CHANGELOG -* Version **[1.2.0](#120)** (Swift 4) +* Version **[1.2.1](#121)** (Swift 4) +* Version **[1.2.0](#120)** * Version **[1.1.0](#110)** (first Swift 4) * Version **[1.0.1](#101)** (latest for Swift 3) * Version **[1.0.0](#100)** @@ -15,6 +16,15 @@ * Version **[0.9.2](#092)** * Version **[0.9.1](#091)** + + +## Hydra 1.2.1 (Swift 4) +--- +- **Release Date**: 2017-10-27 +- **Download Version for Swift 4**: [Download 1.2.1](https://github.com/malcommac/Hydra/releases/tag/1.2.1) + +- [#56](https://github.com/malcommac/Hydra/pull/56) Fixed an issue with Promise and Zip with four parameters + ## Hydra 1.2.0 (Swift 4) @@ -39,6 +49,15 @@ - [#49](https://github.com/malcommac/Hydra/pull/49) Replaced with `(Void)` with `()` to fix warnings with Swift 4 and XCode 9 + + +## Hydra 1.0.2 (latest for Swift 3) +--- +- **Release Date**: 2017-10-27 +- **Download Version for Swift 3**: [Download 1.0.2](https://github.com/malcommac/Hydra/releases/tag/1.0.2) + +- [#56](https://github.com/malcommac/Hydra/pull/56) Fixed an issue with Promise and Zip with four parameters + ## Hydra 1.0.1 (latest for Swift 3) diff --git a/Hydra.xcodeproj/project.pbxproj b/Hydra.xcodeproj/project.pbxproj index 0202594..30ab051 100644 --- a/Hydra.xcodeproj/project.pbxproj +++ b/Hydra.xcodeproj/project.pbxproj @@ -110,6 +110,10 @@ 8933C78E1EB5B82C000D00A4 /* HydraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7891EB5B82A000D00A4 /* HydraTests.swift */; }; 8933C78F1EB5B82C000D00A4 /* HydraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7891EB5B82A000D00A4 /* HydraTests.swift */; }; 8933C7901EB5B82D000D00A4 /* HydraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7891EB5B82A000D00A4 /* HydraTests.swift */; }; + CE286D1D205D3FDA009F1917 /* Promise+RetryWhen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE286D1C205D3FDA009F1917 /* Promise+RetryWhen.swift */; }; + CE286D1E205D3FDA009F1917 /* Promise+RetryWhen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE286D1C205D3FDA009F1917 /* Promise+RetryWhen.swift */; }; + CE286D1F205D3FDA009F1917 /* Promise+RetryWhen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE286D1C205D3FDA009F1917 /* Promise+RetryWhen.swift */; }; + CE286D20205D3FDA009F1917 /* Promise+RetryWhen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE286D1C205D3FDA009F1917 /* Promise+RetryWhen.swift */; }; DD7502881C68FEDE006590AF /* Hydra.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* Hydra.framework */; }; DD7502921C690C7A006590AF /* Hydra.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* Hydra.framework */; }; /* End PBXBuildFile section */ @@ -197,6 +201,7 @@ 8933C7891EB5B82A000D00A4 /* HydraTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HydraTests.swift; sourceTree = ""; }; AD2FAA261CD0B6D800659CF4 /* Hydra.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Hydra.plist; sourceTree = ""; }; AD2FAA281CD0B6E100659CF4 /* HydraTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = HydraTests.plist; sourceTree = ""; }; + CE286D1C205D3FDA009F1917 /* Promise+RetryWhen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Promise+RetryWhen.swift"; path = "Hydra/Promise+RetryWhen.swift"; sourceTree = ""; }; DD75027A1C68FCFC006590AF /* Hydra-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Hydra-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; DD75028D1C690C7A006590AF /* Hydra-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Hydra-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -333,6 +338,7 @@ 37ACADD61F0FB4A800ED284A /* Promise+Recover.swift */, 37ACADD71F0FB4A800ED284A /* Promise+Reduce.swift */, 37ACADD81F0FB4A800ED284A /* Promise+Retry.swift */, + CE286D1C205D3FDA009F1917 /* Promise+RetryWhen.swift */, 37ACADD91F0FB4A800ED284A /* Promise+State.swift */, 37ACADDA1F0FB4A800ED284A /* Promise+Then.swift */, 37ACADDB1F0FB4A800ED284A /* Promise+Timeout.swift */, @@ -713,6 +719,7 @@ 37ACAE121F0FB4A800ED284A /* Promise+Recover.swift in Sources */, 37ACAE221F0FB4A800ED284A /* Promise+Then.swift in Sources */, 37ACAE1E1F0FB4A800ED284A /* Promise+State.swift in Sources */, + CE286D1D205D3FDA009F1917 /* Promise+RetryWhen.swift in Sources */, 37ACAE1A1F0FB4A800ED284A /* Promise+Retry.swift in Sources */, 37ACBBA41F55FD7D003E92AC /* DispatchTimerWrapper.swift in Sources */, 37ACAE0A1F0FB4A800ED284A /* Promise+Observer.swift in Sources */, @@ -751,6 +758,7 @@ 37ACADEC1F0FB4A800ED284A /* Promise+All.swift in Sources */, 37ACAE141F0FB4A800ED284A /* Promise+Recover.swift in Sources */, 37ACAE241F0FB4A800ED284A /* Promise+Then.swift in Sources */, + CE286D1F205D3FDA009F1917 /* Promise+RetryWhen.swift in Sources */, 37ACAE201F0FB4A800ED284A /* Promise+State.swift in Sources */, 37ACAE1C1F0FB4A800ED284A /* Promise+Retry.swift in Sources */, 37ACAE0C1F0FB4A800ED284A /* Promise+Observer.swift in Sources */, @@ -781,6 +789,7 @@ 37ACADED1F0FB4A800ED284A /* Promise+All.swift in Sources */, 37ACAE151F0FB4A800ED284A /* Promise+Recover.swift in Sources */, 37ACAE251F0FB4A800ED284A /* Promise+Then.swift in Sources */, + CE286D20205D3FDA009F1917 /* Promise+RetryWhen.swift in Sources */, 37ACAE211F0FB4A800ED284A /* Promise+State.swift in Sources */, 37ACAE1D1F0FB4A800ED284A /* Promise+Retry.swift in Sources */, 37ACAE0D1F0FB4A800ED284A /* Promise+Observer.swift in Sources */, @@ -811,6 +820,7 @@ 37ACADEB1F0FB4A800ED284A /* Promise+All.swift in Sources */, 37ACAE131F0FB4A800ED284A /* Promise+Recover.swift in Sources */, 37ACAE231F0FB4A800ED284A /* Promise+Then.swift in Sources */, + CE286D1E205D3FDA009F1917 /* Promise+RetryWhen.swift in Sources */, 37ACAE1F1F0FB4A800ED284A /* Promise+State.swift in Sources */, 37ACAE1B1F0FB4A800ED284A /* Promise+Retry.swift in Sources */, 37ACAE0B1F0FB4A800ED284A /* Promise+Observer.swift in Sources */, diff --git a/README.md b/README.md index a0c47cc..5211e86 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Made with ♥ in pure Swift 3.x/4.x, no dependencies, lightweight & fully portab ## Swift 3 and Swift 4 Compatibility -* **Swift 4.x**: Latest is 1.2.0 (`pod 'HydraAsync'`) -* **Swift 3.x**: Latest is 1.0.1 - Latest compatible version is 1.0.1 Download here. If you are using CocoaPods be sure to fix the release (`pod 'HydraAsync', '~> 1.0.1'`) +* **Swift 4.x**: Latest is 1.2.1 (`pod 'HydraAsync'`) +* **Swift 3.x**: Latest is 1.0.2 - Latest compatible version is 1.0.2 Download here. If you are using CocoaPods be sure to fix the release (`pod 'HydraAsync', '~> 1.0.2'`) # Hydra Hydra is full-featured lightweight library which allows you to write better async code in Swift 3.x/4.x. It's partially based on [JavaScript A+](https://promisesaplus.com) specs and also implements modern construct like `await` (as seen in [Async/Await specification in ES8 (ECMAScript 2017)](https://github.com/tc39/ecmascript-asyncawait) or C#) which allows you to write async code in sync manner. @@ -595,8 +595,8 @@ all(op_1.void,op_2.void,op_3.void).then { _ in ## Installation You can install Hydra using CocoaPods, Carthage and Swift package manager -- **Swift 3.x**: Latest compatible is 1.0.1 `pod 'HydraAsync', ~> '1.0.1'` -- **Swift 4.x**: 1.1.0 or later `pod 'HydraAsync'` +- **Swift 3.x**: Latest compatible is 1.0.2 `pod 'HydraAsync', ~> '1.0.2'` +- **Swift 4.x**: 1.2.1 or later `pod 'HydraAsync'` ### CocoaPods use_frameworks! @@ -624,7 +624,7 @@ Add Hydra as dependency in your `Package.swift` Current version is compatible with: -* Swift 4 (>= 1.1.0) or Swift 3.x (Up to 1.0.1) +* Swift 4 (>= 1.2.1) or Swift 3.x (Up to 1.0.2) * iOS 8.0 or later * tvOS 9.0 or later * macOS 10.10 or later diff --git a/Sources/Hydra/Promise+Retry.swift b/Sources/Hydra/Promise+Retry.swift index d03bdc7..bc6d115 100644 --- a/Sources/Hydra/Promise+Retry.swift +++ b/Sources/Hydra/Promise+Retry.swift @@ -34,61 +34,13 @@ import Foundation public extension Promise { - /// `retry` operator allows you to execute source chained promise if it ends with a rejection. - /// If reached the attempts the promise still rejected chained promise is also rejected along with - /// the same source error. - /// - /// - Parameters: - /// - attempts: number of retry attempts for source promise (must be a number > 1, otherwise promise is rejected with `PromiseError.invalidInput` error. - /// - condition: code block to check retryable source promise - /// - Returns: a promise which resolves when the first attempt to resolve source promise is succeded, rejects if none of the attempts ends with a success. public func retry(_ attempts: Int = 3, _ condition: @escaping ((Int, Error) throws -> Bool) = { _,_ in true }) -> Promise { - guard attempts >= 1 else { - // Must be a valid attempts number - return Promise(rejected: PromiseError.invalidInput) - } - - var innerPromise: Promise? = nil - var remainingAttempts = attempts - // We'll create a next promise which will be resolved when attempts to resolve self (source promise) - // is reached (with a fulfill or a rejection). - let nextPromise = Promise(in: self.context, token: self.invalidationToken) { (resolve, reject, operation) in - innerPromise = self.recover(in: self.context) { [unowned self] (error) -> (Promise) in - // If promise is rejected we'll decrement the attempts counter - remainingAttempts -= 1 - guard remainingAttempts >= 1 else { - // if the max number of attempts is reached - // we will end nextPromise with the last seen error - throw error - } - // If promise is rejected we will check condition that is retryable - do { - guard try condition(remainingAttempts, error) else { - throw error - } - } catch(_) { - // reject soruce promise error - throw error - } - // Reset the state of the promise - // (okay it's true, a Promise cannot change state as you know...this - // is a bit trick which will remain absolutely internal to the library itself) - self.resetState() - // Re-execute the body of the source promise to re-execute the async operation - self.runBody() - return self.retry(remainingAttempts, condition) - } - // If promise resolves nothing else to do, resolve the nextPromise! - let onResolve = Observer.onResolve(self.context, resolve) - let onReject = Observer.onReject(self.context, reject) - let onCancel = Observer.onCancel(self.context, operation.cancel) - - // Observe changes from source promise - innerPromise?.add(observers: onResolve, onReject, onCancel) - innerPromise?.runBody() - } - nextPromise.runBody() - return nextPromise - } - + return retryWhen(attempts) { (remainingAttempts, error) -> Promise in + do { + return Promise(resolved: try condition(remainingAttempts, error)) + } catch (_) { + return Promise(rejected: error) + } + } + } } diff --git a/Sources/Hydra/Promise+RetryWhen.swift b/Sources/Hydra/Promise+RetryWhen.swift new file mode 100644 index 0000000..7b5ed0d --- /dev/null +++ b/Sources/Hydra/Promise+RetryWhen.swift @@ -0,0 +1,90 @@ +/* +* Hydra +* Fullfeatured lightweight Promise & Await Library for Swift +* +* Created by: Hiromi Motodera +* Email: moai.motodera@gmail.com +* Twitter: @moaible +* +* Copyright © 2017 Daniele Margutti +* +* +* 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 + +public extension Promise { + + public func retryWhen(_ attempts: Int = 3, _ condition: @escaping ((Int, Error) -> Promise) = { _,_ in Promise(resolved: true) }) -> Promise { + guard attempts >= 1 else { + // Must be a valid attempts number + return Promise(rejected: PromiseError.invalidInput) + } + + var innerPromise: Promise? = nil + var remainingAttempts = attempts + // We'll create a next promise which will be resolved when attempts to resolve self (source promise) + // is reached (with a fulfill or a rejection). + let nextPromise = Promise(in: self.context, token: self.invalidationToken) { (resolve, reject, operation) in + innerPromise = self.recover(in: self.context) { [unowned self] (error) -> (Promise) in + // If promise is rejected we'll decrement the attempts counter + remainingAttempts -= 1 + guard remainingAttempts >= 1 else { + // if the max number of attempts is reached + // we will end nextPromise with the last seen error + throw error + } + return Promise { resolve, reject, _ in + condition (remainingAttempts, error).then(in: self.context) { (shouldRetry) in + guard shouldRetry else { + reject(error) + return + } + // Reset the state of the promise + // (okay it's true, a Promise cannot change state as you know...this + // is a bit trick which will remain absolutely internal to the library itself) + self.resetState() + // Re-execute the body of the source promise to re-execute the async operation + self.runBody() + self.retryWhen(remainingAttempts, condition).then(in: self.context) { (result) in + resolve(result) + }.catch { (retriedError) in + reject(retriedError) + } + }.catch { (_) in + // reject soruce promise error + reject(error) + } + } + } + // If promise resolves nothing else to do, resolve the nextPromise! + let onResolve = Observer.onResolve(self.context, resolve) + let onReject = Observer.onReject(self.context, reject) + let onCancel = Observer.onCancel(self.context, operation.cancel) + + // Observe changes from source promise + innerPromise?.add(observers: onResolve, onReject, onCancel) + innerPromise?.runBody() + } + nextPromise.runBody() + return nextPromise + } +} diff --git a/Tests/HydraTests/HydraTests.swift b/Tests/HydraTests/HydraTests.swift index 2c2948a..99a0a0b 100644 --- a/Tests/HydraTests/HydraTests.swift +++ b/Tests/HydraTests/HydraTests.swift @@ -721,6 +721,94 @@ class HydraTestThen: XCTestCase { waitForExpectations(timeout: expTimeout, handler: nil) } + //MARK: RetryWhen Test + + func test_retryWhen() { + let exp = expectation(description: "test_retryWhen") + + let retryAttempts = 3 + let successOnAttempt = 3 + var currentAttempt = 0 + Promise { (resolve, reject, _) in + currentAttempt += 1 + if currentAttempt < successOnAttempt { + print("attempt is \(currentAttempt)... reject") + reject(TestErrors.anotherError) + } else { + print("attempt is \(currentAttempt)... resolve") + resolve(5) + } + }.retryWhen(retryAttempts).then { value in + print("value \(value) at attempt \(currentAttempt)") + XCTAssertEqual(currentAttempt, 3) + exp.fulfill() + }.catch { (err) in + print("failed \(err) at attempt \(currentAttempt)") + XCTFail() + } + waitForExpectations(timeout: expTimeout, handler: nil) + } + + func test_retryWhen_allFailure() { + let exp = expectation(description: "test_retryWhen_allFailure") + + let retryAttempts = 3 + var currentAttempt = 0 + Promise { (resolve, reject, _) in + currentAttempt += 1 + print("attempt is \(currentAttempt)... reject") + reject(TestErrors.anotherError) + }.retryWhen(retryAttempts).then { value in + print("value \(value) at attempt \(currentAttempt)") + XCTFail() + }.catch { err in + print("failed \(err) at attempt \(currentAttempt)") + XCTAssertEqual(err as! TestErrors, .anotherError) + XCTAssertEqual(currentAttempt, 3) + exp.fulfill() + } + + waitForExpectations(timeout: expTimeout, handler: nil) + + } + + func test_retryWhen_condition() { + let exp = expectation(description: "test_retryWhen_condition") + + let retryAttempts = 5 + let successOnAttempt = 5 + let retryableRemainAttempt = 2 + var currentAttempt = 0 + Promise { (resolve, reject, _) in + currentAttempt += 1 + if currentAttempt < successOnAttempt { + print("attempt is \(currentAttempt)... reject") + reject(TestErrors.anotherError) + } else { + print("attempt is \(currentAttempt)... resolve") + resolve(5) + } + }.retryWhen(retryAttempts) { (remainAttempts, error) -> Promise in + if remainAttempts > retryableRemainAttempt { + print("retry remainAttempts is \(remainAttempts)... true") + return Promise(resolved: true).defer(5) + } else { + print("retry remainAttempts is \(remainAttempts)... false") + return Promise(resolved: false) + } + }.then { value in + print("value \(value) at attempt \(currentAttempt)") + XCTFail() + }.catch { err in + print("failed \(err) at attempt \(currentAttempt)") + XCTAssertEqual(err as! TestErrors, .anotherError) + XCTAssertEqual(currentAttempt, 3) + exp.fulfill() + } + + waitForExpectations(timeout: expTimeout, handler: nil) + } + func test_invalidationTokenWithAsyncOperator() { let exp = expectation(description: "test_retry_condition") let invalidator: InvalidationToken = InvalidationToken()