Skip to content

Commit

Permalink
Merge pull request #62 from moaible/retry_when_operator
Browse files Browse the repository at this point in the history
Add 'retryWhen' operator
  • Loading branch information
malcommac authored Mar 28, 2019
2 parents b9568ef + 3eb6620 commit e9f6d9d
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 62 deletions.
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**
Expand All @@ -15,6 +16,15 @@
* Version **[0.9.2](#092)**
* Version **[0.9.1](#091)**

<a name="121" />

## 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

<a name="120" />

## Hydra 1.2.0 (Swift 4)
Expand All @@ -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


<a name="102" />

## 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

<a name="101" />

## Hydra 1.0.1 (latest for Swift 3)
Expand Down
10 changes: 10 additions & 0 deletions Hydra.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -197,6 +201,7 @@
8933C7891EB5B82A000D00A4 /* HydraTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HydraTests.swift; sourceTree = "<group>"; };
AD2FAA261CD0B6D800659CF4 /* Hydra.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Hydra.plist; sourceTree = "<group>"; };
AD2FAA281CD0B6E100659CF4 /* HydraTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = HydraTests.plist; sourceTree = "<group>"; };
CE286D1C205D3FDA009F1917 /* Promise+RetryWhen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Promise+RetryWhen.swift"; path = "Hydra/Promise+RetryWhen.swift"; sourceTree = "<group>"; };
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 */
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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
Expand Down
64 changes: 8 additions & 56 deletions Sources/Hydra/Promise+Retry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value> {
guard attempts >= 1 else {
// Must be a valid attempts number
return Promise<Value>(rejected: PromiseError.invalidInput)
}

var innerPromise: Promise<Value>? = 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<Value>(in: self.context, token: self.invalidationToken) { (resolve, reject, operation) in
innerPromise = self.recover(in: self.context) { [unowned self] (error) -> (Promise<Value>) 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<Bool> in
do {
return Promise<Bool>(resolved: try condition(remainingAttempts, error))
} catch (_) {
return Promise<Bool>(rejected: error)
}
}
}
}
90 changes: 90 additions & 0 deletions Sources/Hydra/Promise+RetryWhen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Hydra
* Fullfeatured lightweight Promise & Await Library for Swift
*
* Created by: Hiromi Motodera
* Email: [email protected]
* 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<Bool>) = { _,_ in Promise<Bool>(resolved: true) }) -> Promise<Value> {
guard attempts >= 1 else {
// Must be a valid attempts number
return Promise<Value>(rejected: PromiseError.invalidInput)
}

var innerPromise: Promise<Value>? = 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<Value>(in: self.context, token: self.invalidationToken) { (resolve, reject, operation) in
innerPromise = self.recover(in: self.context) { [unowned self] (error) -> (Promise<Value>) 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<Value> { 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
}
}
88 changes: 88 additions & 0 deletions Tests/HydraTests/HydraTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int> { (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<Int> { (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<Int> { (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<Bool> in
if remainAttempts > retryableRemainAttempt {
print("retry remainAttempts is \(remainAttempts)... true")
return Promise<Bool>(resolved: true).defer(5)
} else {
print("retry remainAttempts is \(remainAttempts)... false")
return Promise<Bool>(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()
Expand Down

0 comments on commit e9f6d9d

Please sign in to comment.