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()