diff --git a/Hydra.xcodeproj/project.xcworkspace/xcuserdata/TheInkedEngineer.xcuserdatad/UserInterfaceState.xcuserstate b/Hydra.xcodeproj/project.xcworkspace/xcuserdata/TheInkedEngineer.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 0000000..3f67d5a
Binary files /dev/null and b/Hydra.xcodeproj/project.xcworkspace/xcuserdata/TheInkedEngineer.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/Hydra.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate b/Hydra.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate
index 3e9d284..7cd1637 100644
Binary files a/Hydra.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate and b/Hydra.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/Hydra.xcodeproj/xcuserdata/TheInkedEngineer.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Hydra.xcodeproj/xcuserdata/TheInkedEngineer.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
new file mode 100644
index 0000000..d750981
--- /dev/null
+++ b/Hydra.xcodeproj/xcuserdata/TheInkedEngineer.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -0,0 +1,6 @@
+
+
+
diff --git a/Hydra.xcodeproj/xcuserdata/TheInkedEngineer.xcuserdatad/xcschemes/xcschememanagement.plist b/Hydra.xcodeproj/xcuserdata/TheInkedEngineer.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..a22f0d6
--- /dev/null
+++ b/Hydra.xcodeproj/xcuserdata/TheInkedEngineer.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ SchemeUserState
+
+ DemoApp.xcscheme_^#shared#^_
+
+ orderHint
+ 4
+
+ Hydra-iOS.xcscheme_^#shared#^_
+
+ orderHint
+ 0
+
+ Hydra-macOS.xcscheme_^#shared#^_
+
+ orderHint
+ 1
+
+ Hydra-tvOS.xcscheme_^#shared#^_
+
+ orderHint
+ 2
+
+ Hydra-watchOS.xcscheme_^#shared#^_
+
+ orderHint
+ 3
+
+
+
+
diff --git a/HydraAsync.podspec b/HydraAsync.podspec
index 8c28a85..9f342e0 100644
--- a/HydraAsync.podspec
+++ b/HydraAsync.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'HydraAsync'
- spec.version = '2.0.2'
+ spec.version = '2.0.4 '
spec.summary = 'Promises & Await: Write better async in Swift'
spec.homepage = 'https://github.com/malcommac/Hydra'
spec.license = { :type => 'MIT', :file => 'LICENSE' }
@@ -15,5 +15,5 @@ Pod::Spec.new do |spec|
spec.requires_arc = true
spec.module_name = 'Hydra'
spec.frameworks = "Foundation"
- spec.swift_version = "5.0"
+ spec.swift_versions = ['4.0', '4.1', '4.2', '5.0', '5.1', '5.3']
end
diff --git a/Package.swift b/Package.swift
index 6d6243f..02e82be 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:4.0
+// swift-tools-version:5.1
import PackageDescription
let package = Package(
@@ -9,6 +9,5 @@ let package = Package(
targets: [
.target(name: "Hydra", dependencies: []),
.testTarget(name: "HydraTests", dependencies: ["Hydra"])
- ],
- swiftLanguageVersions: [4]
+ ]
)
diff --git a/README.md b/README.md
index 5211e86..16ba0ee 100644
--- a/README.md
+++ b/README.md
@@ -345,7 +345,7 @@ invalidator.invalidate()
Await can be also used in conjuction with zip to resolve all promises from a list:
```swift
-let (resultA,resultB) = await(Promise.zip(promiseA,promiseB))
+let (resultA,resultB) = await(zip(promiseA,promiseB))
print(resultA)
print(resultB)
```
@@ -512,7 +512,7 @@ Map is used to transform a list of items into promises and resolve them in paral
`zip` allows you to join different promises (2,3 or 4) and return a tuple with the result of them. Promises are resolved in parallel.
```swift
-Promise.zip(a: getUserProfile(user), b: getUserAvatar(user), c: getUserFriends(user))
+zip(a: getUserProfile(user), b: getUserAvatar(user), c: getUserFriends(user))
.then { profile, avatar, friends in
// ... let's do something
}.catch {
diff --git a/Sources/Hydra/Commons.swift b/Sources/Hydra/Commons.swift
index 5adf6ca..7c22412 100644
--- a/Sources/Hydra/Commons.swift
+++ b/Sources/Hydra/Commons.swift
@@ -52,7 +52,7 @@ public enum PromiseError: Error {
/// Invalidatable protocol is used to control the execution of a promise from the outside
-/// You should pass an object conforms to this type at the init of your Promsie instance.
+/// You should pass an object conforms to this type at the init of your Promise instance.
/// To invalidate a Promise just return the `.isCancelled` property to `true`.
///
/// From the inside of your Promise's body you should check if the `operation.isCancelled` is `true`.
@@ -67,7 +67,7 @@ public protocol InvalidatableProtocol {
/// This is a simple implementation of the `InvalidatableProtocol` protocol.
-/// You can use or extend this class in order to provide your own bussiness logic.
+/// You can use or extend this class in order to provide your own business logic.
open class InvalidationToken: InvalidatableProtocol {
/// Current status of the promise
diff --git a/Sources/Hydra/DispatchTimerWrapper.swift b/Sources/Hydra/DispatchTimerWrapper.swift
index 10d5a1d..cf652af 100644
--- a/Sources/Hydra/DispatchTimerWrapper.swift
+++ b/Sources/Hydra/DispatchTimerWrapper.swift
@@ -42,7 +42,7 @@ internal class DispatchTimerWrapper {
timer.setEventHandler(handler: handler)
}
- func scheduleOneshot(deadline: DispatchTime, leeway: DispatchTimeInterval = .nanoseconds(0)) {
+ func scheduleOneShot(deadline: DispatchTime, leeway: DispatchTimeInterval = .nanoseconds(0)) {
timer.schedule(deadline: deadline, leeway: leeway)
}
diff --git a/Sources/Hydra/Promise+All.swift b/Sources/Hydra/Promise+All.swift
index 92e40ef..52fb97d 100644
--- a/Sources/Hydra/Promise+All.swift
+++ b/Sources/Hydra/Promise+All.swift
@@ -51,7 +51,7 @@ public func all(_ promises: S, concurrency: UInt = UInt.max) ->
}
// We want to create a Promise which groups all input Promises and return only
- // when all input promises fullfill or one of them reject.
+ // when all input promises fulfill or one of them reject.
// Promises are resolved in parallel but the array with the results of all promises is reported
// in the same order of the input promises.
let allPromise = Promise<[L]> { (resolve, reject, operation) in
@@ -66,7 +66,7 @@ public func all(_ promises: S, concurrency: UInt = UInt.max) ->
// decrement remaining promise to fulfill
countRemaining -= 1
if countRemaining == 0 {
- // if all promises are fullfilled we can resolve our chain Promise
+ // if all promises are fulfilled we can resolve our chain Promise
// with an array of all values results of our input promises (in the same order)
let allResults = promises.map({ return $0.state.value! })
resolve(allResults)
diff --git a/Sources/Hydra/Promise+Await.swift b/Sources/Hydra/Promise+Await.swift
index 0f725a1..6801389 100644
--- a/Sources/Hydra/Promise+Await.swift
+++ b/Sources/Hydra/Promise+Await.swift
@@ -63,7 +63,7 @@ public prefix func ..! (_ promise: Promise) -> T? {
/// - Parameters:
/// - context: context in which you want to execute the operation. If not specified default concurrent `awaitContext` is used instead.
/// - promise: target promise
-/// - Returns: fufilled value of the promise
+/// - Returns: fulfilled value of the promise
/// - Throws: throws an exception if promise fails due to an error
@discardableResult
public func await(in context: Context? = nil, _ promise: Promise) throws -> T {
@@ -92,17 +92,23 @@ public extension Context {
/// Awaits that the given promise fulfilled with its value or throws an error if the promise fails.
///
/// - Parameter promise: target promise
- /// - Returns: return the value of the promise
- /// - Throws: throw if promise fails
- @discardableResult
- internal func await(_ promise: Promise) throws -> T {
- guard self.queue != DispatchQueue.main else {
- // execute a promise on main context does not make sense
- // dispatch_semaphore_wait should NOT be called on the main thread.
- // more here: https://medium.com/@valentinkalchev/how-to-pause-and-resume-a-sequence-of-mutating-swift-structs-using-dispatch-semaphore-fc98eca55c0#.ipbujy4k2
- throw PromiseError.invalidContext
- }
-
+ /// - Returns: return the value of the promise
+ /// - Throws: throw if promise fails
+ @discardableResult
+ internal func await(_ promise: Promise) throws -> T {
+ #if os(Linux)
+ let isNotMainQueue = self.queue.label != DispatchQueue.main.label
+ #else
+ let isNotMainQueue = self.queue != DispatchQueue.main
+ #endif
+
+ guard isNotMainQueue else {
+ // execute a promise on main context does not make sense
+ // dispatch_semaphore_wait should NOT be called on the main thread.
+ // more here: https://medium.com/@valentinkalchev/how-to-pause-and-resume-a-sequence-of-mutating-swift-structs-using-dispatch-semaphore-fc98eca55c0#.ipbujy4k2
+ throw PromiseError.invalidContext
+ }
+
var result: T?
var error: Error?
@@ -111,16 +117,16 @@ public extension Context {
let semaphore = DispatchSemaphore(value: 0)
promise.then(in: self) { value -> Void in
- // promise is fulfilled, fillup error and resume code execution
+ // promise is fulfilled, fill-up error and resume code execution
result = value
semaphore.signal()
}.catch(in: self) { err in
- // promise is rejected, fillup error and resume code execution
+ // promise is rejected, fill-up error and resume code execution
error = err
semaphore.signal()
}
- // Wait and block code execution until promise is fullfilled or rejected
+ // Wait and block code execution until promise is full-filled or rejected
_ = semaphore.wait(timeout: .distantFuture)
guard let promiseValue = result else {
diff --git a/Sources/Hydra/Promise+Cancel.swift b/Sources/Hydra/Promise+Cancel.swift
index dadf48f..a5ea692 100644
--- a/Sources/Hydra/Promise+Cancel.swift
+++ b/Sources/Hydra/Promise+Cancel.swift
@@ -37,11 +37,11 @@ public extension Promise {
/// Catch a cancelled promise.
///
/// - Parameters:
- /// - context: context in which the body will be eecuted. If not specified `.main` is used.
+ /// - context: context in which the body will be executed. If not specified `.main` is used.
/// - body: body to execute
/// - Returns: a new void promise
@discardableResult
- func cancelled(in context: Context? = nil, _ body: @escaping (() -> (()))) -> Promise {
+ func cancelled(in context: Context? = nil, _ body: @escaping (() -> (Void))) -> Promise {
let ctx = context ?? .main
let nextPromise = Promise(in: ctx, token: self.invalidationToken) { resolve, reject, operation in
let onResolve = Observer.onResolve(ctx, { _ in
diff --git a/Sources/Hydra/Promise+Defer.swift b/Sources/Hydra/Promise+Defer.swift
index d6f9975..926068e 100644
--- a/Sources/Hydra/Promise+Defer.swift
+++ b/Sources/Hydra/Promise+Defer.swift
@@ -35,13 +35,17 @@ import Foundation
public extension Promise {
- /// Delay the executon of a Promise chain by some number of seconds from current time
+ /// Delay the execution of a Promise chain by some number of seconds from current time
///
/// - Parameters:
/// - context: context in which the body is executed (if not specified `background` is used)
/// - seconds: delay time in seconds; execution time is `.now()+seconds`
/// - Returns: the Promise to resolve to after the delay
func `defer`(in context: Context? = nil, _ seconds: TimeInterval) -> Promise {
+ guard seconds > 0 else {
+ return self
+ }
+
let ctx = context ?? .background
return self.then(in: ctx, { value in
return Promise { resolve, _, _ in
diff --git a/Sources/Hydra/Promise+Recover.swift b/Sources/Hydra/Promise+Recover.swift
index 180f04c..ed5dae2 100644
--- a/Sources/Hydra/Promise+Recover.swift
+++ b/Sources/Hydra/Promise+Recover.swift
@@ -34,7 +34,6 @@
import Foundation
public extension Promise {
-
/// Allows you to recover a failed promise by returning another promise with the same output
///
/// - Parameters:
@@ -43,8 +42,8 @@ public extension Promise {
/// - Returns: a promise
func recover(in context: Context? = nil, _ body: @escaping (Error) throws -> Promise) -> Promise {
let ctx = context ?? .background
- return Promise(in: ctx, token: self.invalidationToken, { resolve, reject, operation in
- return self.then(in: ctx, {
+ return Promise(in: ctx, token: self.invalidationToken, { [weak self] resolve, reject, operation in
+ self?.then(in: ctx, {
// If promise resolve we don't need to do anything.
resolve($0)
}).catch(in: ctx, { error in
diff --git a/Sources/Hydra/Promise+Retry.swift b/Sources/Hydra/Promise+Retry.swift
index 89c6474..9132f95 100644
--- a/Sources/Hydra/Promise+Retry.swift
+++ b/Sources/Hydra/Promise+Retry.swift
@@ -33,9 +33,17 @@
import Foundation
public extension Promise {
-
- func retry(_ attempts: Int = 3, _ condition: @escaping ((Int, Error) throws -> Bool) = { _,_ in true }) -> Promise {
- return retryWhen(attempts) { (remainingAttempts, error) -> Promise in
+
+ /// Retry the operation of the promise.
+ ///
+ /// - Parameters:
+ /// - attempts: number of attempts.
+ /// - delay: delay between each attempts (starting when failed the first time).
+ /// - condition: condition for delay.
+ /// - Returns: Promise
+ func retry(_ attempts: Int = 3, delay: TimeInterval = 0,
+ _ condition: @escaping ((Int, Error) throws -> Bool) = { _,_ in true }) -> Promise {
+ return retryWhen(attempts, delay: delay) { (remainingAttempts, error) -> Promise in
do {
return Promise(resolved: try condition(remainingAttempts, error))
} catch (_) {
diff --git a/Sources/Hydra/Promise+RetryWhen.swift b/Sources/Hydra/Promise+RetryWhen.swift
index 91399fa..121b54d 100644
--- a/Sources/Hydra/Promise+RetryWhen.swift
+++ b/Sources/Hydra/Promise+RetryWhen.swift
@@ -33,7 +33,8 @@ import Foundation
public extension Promise {
- func retryWhen(_ attempts: Int = 3, _ condition: @escaping ((Int, Error) -> Promise) = { _,_ in Promise(resolved: true) }) -> Promise {
+ func retryWhen(_ attempts: Int = 3, delay: TimeInterval = 0,
+ _ 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)
@@ -44,7 +45,7 @@ public extension Promise {
// 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
+ innerPromise = self.defer(delay).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 {
@@ -64,7 +65,7 @@ public extension Promise {
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
+ self.retryWhen(remainingAttempts, delay: delay, condition).then(in: self.context) { (result) in
resolve(result)
}.catch { (retriedError) in
reject(retriedError)
diff --git a/Sources/Hydra/Promise+Timeout.swift b/Sources/Hydra/Promise+Timeout.swift
index 4c46387..599816e 100644
--- a/Sources/Hydra/Promise+Timeout.swift
+++ b/Sources/Hydra/Promise+Timeout.swift
@@ -54,7 +54,7 @@ public extension Promise {
let errorToPass = (error ?? PromiseError.timeout)
reject(errorToPass)
}
- timer.scheduleOneshot(deadline: .now() + timeout)
+ timer.scheduleOneShot(deadline: .now() + timeout)
timer.resume()
// Observe resolve
diff --git a/Sources/Hydra/Promise.swift b/Sources/Hydra/Promise.swift
index c296eb4..ceb0664 100644
--- a/Sources/Hydra/Promise.swift
+++ b/Sources/Hydra/Promise.swift
@@ -261,7 +261,7 @@ public class Promise {
/// ```
/// let op_1: Promise = asyncGetCurrentUserProfile()
/// let op_2: Promise = asyncGetCurrentUserAvatar()
- /// let op_3: Promise<[User]> = asyncGetCUrrentUserFriends()
+ /// let op_3: Promise<[User]> = asyncGetCurrentUserFriends()
/// all(op_1.void,op_2.void,op_3.void).then { _ in
/// let userProfile = op_1.result
/// let avatar = op_2.result
diff --git a/Tests/HydraTests/HydraTests.swift b/Tests/HydraTests/HydraTests.swift
index 99a0a0b..d92f6aa 100644
--- a/Tests/HydraTests/HydraTests.swift
+++ b/Tests/HydraTests/HydraTests.swift
@@ -226,22 +226,22 @@ class HydraTestThen: XCTestCase {
/// and return another Promise with the same value of the previous promise as output.
// In this test we have tried to recover a bad call by executing a resolving promise.
// Test is passed if recover works and we get a valid result into the final `then`.
- func test_recoverPromise() {
- let exp = expectation(description: "test_recoverPromise")
- let expResult = 5
- intPromise(expResult).then { value in
- self.toStringErrorPromise(value)
- }.recover { err -> Promise in
- return self.toStringPromise("\(expResult)")
- }.then { string in
- if ("\(expResult)" != string) {
- XCTFail()
- }
- exp.fulfill()
- }
- waitForExpectations(timeout: expTimeout, handler: nil)
- }
-
+ func test_recoverPromise() {
+ let exp = expectation(description: "test_recoverPromise")
+ let expResult = 5
+ intPromise(expResult).then { value in
+ self.toStringErrorPromise(value)
+ }.recover { err -> Promise in
+ return self.toStringPromise("\(expResult)")
+ }.then { string in
+ if ("\(expResult)" != string) {
+ XCTFail()
+ }
+ exp.fulfill()
+ }
+ waitForExpectations(timeout: expTimeout, handler: nil)
+ }
+
/// If return rejected promise in `recover` operator, chain to next as its error.
func test_recover_failure() {
let exp = expectation(description: "test_recover_failure")
@@ -356,6 +356,23 @@ class HydraTestThen: XCTestCase {
}
waitForExpectations(timeout: expTimeout, handler: nil)
}
+
+ func test_thenChainAndAlways() {
+ let exp = expectation(description: "test_anyWithArray")
+ var passedThens = 0
+
+ Promise(in: .background, token: nil) { (r, rj, s) in
+ r(10)
+ }.then { _ in
+ passedThens += 1
+ }.always(in: .main) {
+ passedThens += 1
+ XCTAssertTrue(passedThens == 2, "Not all thens are passed")
+ XCTAssertTrue(Thread.isMainThread, "Failed, the operation is not on main thread as we expect")
+ exp.fulfill()
+ }
+ waitForExpectations(timeout: expTimeout, handler: nil)
+ }
/// The same test with `any` operator which takes as input an array instead of variable list of arguments
func test_anyWithArray() {
@@ -633,34 +650,67 @@ class HydraTestThen: XCTestCase {
}
//MARK: Retry Test
-
- func test_retry() {
- let exp = expectation(description: "test_retry")
-
- 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)
- }
- }.retry(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_retry() {
+ let exp = expectation(description: "test_retry")
+
+ 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)
+ }
+ }.retry(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_retryWithDelay() {
+ let exp = expectation(description: "test_retryWithDelay")
+
+ let retryAttempts = 3
+ let successOnAttempt = 3
+ var currentAttempt = 0
+ var lastFailureDate = Date.distantPast
+ let retryDelay: TimeInterval = 0
+
+ Promise { (resolve, reject, _) in
+ currentAttempt += 1
+ if currentAttempt < successOnAttempt {
+ print("attempt is \(currentAttempt)... reject")
+ lastFailureDate = Date()
+ reject(TestErrors.anotherError)
+ } else {
+ print("attempt is \(currentAttempt)... resolve")
+ resolve(5)
+ }
+ }.retry(retryAttempts, delay: retryDelay).then { value in
+ let passed = Date().timeIntervalSince(lastFailureDate)
+ XCTAssertGreaterThanOrEqual(passed, retryDelay)
+ print("value \(value) at attempt \(currentAttempt)")
+ XCTAssertEqual(currentAttempt, 3)
+ exp.fulfill()
+ }.catch { err in
+ print("failed \(err) at attempt \(currentAttempt)")
+ XCTFail()
+ }
+
+ waitForExpectations(timeout: 30, handler: nil)
+ }
+
func test_retry_allFailure() {
let exp = expectation(description: "test_retry_allFailure")