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