GithubHelp home page GithubHelp logo

malcommac / hydra Goto Github PK

View Code? Open in Web Editor NEW
2.0K 2.0K 112.0 2.17 MB

⚡️ Lightweight full-featured Promises, Async & Await Library in Swift

License: MIT License

Ruby 0.72% Swift 99.28%
async await futures gcd promise-library promises swift

hydra's Introduction

Hydra

Lightweight full-featured Promises, Async & Await Library in Swift

What's this?

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+ specs and also implements modern construct like await (as seen in Async/Await specification in ES8 (ECMAScript 2017) or C#) which allows you to write async code in sync manner. Hydra supports all sexiest operators like always, validate, timeout, retry, all, any, pass, recover, map, zip, defer and retry.
Starts writing better async code with Hydra!

A more detailed look at how Hydra works can be found in ARCHITECTURE file or on Medium.

❤️ Your Support

Hi fellow developer!
You know, maintaing and developing tools consumes resources and time. While I enjoy making them your support is foundamental to allow me continue its development.

If you are using SwiftLocation or any other of my creations please consider the following options:

Introduction

A Promise is a way to represent a value that will exists, or will fail with an error, at some point in the future. You can think about it as a Swift's Optional: it may or may not be a value. A more detailed article which explain how Hydra was implemented can be found here.

Each Promise is strong-typed: this mean you create it with the value's type you are expecting for and you will be sure to receive it when Promise will be resolved (the exact term is fulfilled).

A Promise is, in fact, a proxy object; due to the fact the system knows what success value look like, composing asynchronous operation is a trivial task; with Hydra you can:

  • create a chain of dependent async operation with a single completion task and a single error handler.
  • resolve many independent async operations simultaneously and get all values at the end
  • retry or recover failed async operations
  • write async code as you may write standard sync code
  • resolve dependent async operations by passing the result of each value to the next operation, then get the final result
  • avoid callbacks, pyramid of dooms and make your code cleaner!

Since 0.9.7 Hydra implements Cancellable Promises. In order to support this new feature we have slightly modified the Body signature of the Promise; in order to make your source code compatible you just need to add the third parameter along with resolve,reject: operation. operation encapsulate the logic to support Invalidation Token. It's just and object of type PromiseStatus you can query to see if a Promise is marked to be cancelled from the outside. If you are not interested in using it in your Promise declaration just mark it as _.

To sum up your code:

return Promise<Int>(in: .main, token: token, { resolve, reject in ...

needs to be:

return Promise<Int>(in: .main, token: token, { resolve, reject, operation in // or resolve, reject, _

Creating a Promise is trivial; you need to specify the context (a GCD Queue) in which your async operations will be executed in and add your own async code as body of the Promise.

This is a simple async image downloader:

func getImage(url: String) -> Promise<UIImage> {
    return Promise<UIImage>(in: .background, { resolve, reject, _ in
        self.dataTask(with: request, completionHandler: { data, response, error in
            if let error = error {
                reject(error)
            } else if let data = data, let response = response as? HTTPURLResponse {
                resolve((data, response))
            } else {
                reject("Image cannot be decoded")
            }
        }).resume()
    })
}

You need to remember only few things:

  • a Promise is created with a type: this is the object's type you are expecting from it once fulfilled. In our case we are expecting an UIImage so our Promise is Promise<UIImage> (if a promise fail returned error must be conform to Swift's Error protocol)
  • your async code (defined into the Promise's body) must alert the promise about its completion; if you have the fulfill value you will call resolve(yourValue); if an error has occurred you can call reject(occurredError) or throw it using Swift's throw occurredError.
  • the context of a Promise define the Grand Central Dispatch's queue in which the async code will be executed in; you can use one of the defined queues (.background,.userInitiated etc. Here you can found a nice tutorial about this topic)

Using a Promise is even easier.
You can get the result of a promise by using then function; it will be called automatically when your Promise fullfill with expected value. So:

getImage(url).then(.main, { image in
	myImageView.image = image
})

As you can see even then may specify a context (by default - if not specified - is the main thread): this represent the GCD queue in which the code of the then's block will be executed (in our case we want to update an UI control so we will need to execute it in .main thread).

But what happened if your Promise fail due to a network error or if the image is not decodable? catch func allows you to handle Promise's errors (with multiple promises you may also have a single errors entry point and reduce the complexity).

getImage(url).then(.main, { image in
	myImageView.image = image
}).catch(.main, { error in
	print("Something bad occurred: \(error)")
})

Chaining Promises is the next step thought mastering Hydra. Suppose you have defined some Promises:

func loginUser(_ name:String, _ pwd: String)->Promise<User>
func getFollowers(user: User)->Promise<[Follower]>
func unfollow(followers: [Follower])->Promise<Int>

Each promise need to use the fulfilled value of the previous; plus an error in one of these should interrupt the entire chain.
Doing it with Hydra is pretty straightforward:

loginUser(username,pass).then(getFollowers).then(unfollow).then { count in
	print("Unfollowed \(count) users")
}.catch { err in
	// Something bad occurred during these calls
}

Easy uh? (Please note: in this example context is not specified so the default .main is used instead).

Cancellable Promises are a very sensitive task; by default Promises are not cancellable. Hydra allows you to cancel a promise from the outside by implementing the InvalidationToken. InvalidationToken is a concrete open class which is conform to the InvalidatableProtocol protocol. It must implement at least one Bool property called isCancelled.

When isCancelled is set to true it means someone outside the promise want to cancel the task.

It's your responsibility to check from inside the Promise's body the status of this variable by asking to operation.isCancelled. If true you can do all your best to cancel the operation; at the end of your operations just call cancel() and stop the workflow.

Your promise must be also initialized using this token instance.

This is a concrete example with UITableViewCell: working with table cells, often the result of a promise needs to be ignored. To do this, each cell can hold on to an InvalidationToken. An InvalidationToken is an execution context that can be invalidated. If the context is invalidated, then the block that is passed to it will be discarded and not executed.

To use this with table cells, the queue should be invalidated and reset on prepareForReuse().

class SomeTableViewCell: UITableViewCell {
    var token = InvalidationToken()

	func setImage(atURL url: URL) {
		downloadImage(url).then(in: .main, { image in
			self.imageView.image = image
		})
	}

	override func prepareForReuse() {
		super.prepareForReuse()
		token.invalidate() // stop current task and ignore result
		token = InvalidationToken() // new token
	}

	func downloadImage(url: URL) -> Promise<UIImage> {
		return Promise<Something>(in: .background, token: token, { (resolve, reject, operation) in
		// ... your async operation

		// somewhere in your Promise's body, for example in download progression
		// you should check for the status of the operation.
		if operation.isCancelled {
			// operation should be cancelled
			// do your best to cancel the promise's task
			operation.cancel() // request to mark the Promise as cancelled
			return // stop the workflow! it's important
		}
		// ... your async operation
		})
	}
}

Have you ever dream to write asynchronous code like its synchronous counterpart? Hydra was heavily inspired by Async/Await specification in ES8 (ECMAScript 2017) which provides a powerful way to write async doe in a sequential manner.

Using async and await is pretty simple.

NOTE: Since Hydra 2.0.6 the await function is available under Hydra.await() function in order to supress the Xcode 12.5+ warning (await will become a Swift standard function soon!)

For example the code above can be rewritten directly as:

// With `async` we have just defined a Promise which will be executed in a given
// context (if omitted `background` thread is used) and return an Int value.
let asyncFunc = async({ _ -> Int in // you must specify the return of the Promise, here an Int
	// With `await` the async code is resolved in a sync manner
	let loggedUser = try Hydra.await(loginUser(username,pass))
	// one promise...
	let followersList = try Hydra.await(getFollowers(loggedUser))
	// after another...
	let countUnfollowed = try Hydra.await(unfollow(followersList))
	// ... linearly
	// Then our async promise will be resolved with the end value
	return countUnfollowed
}).then({ value in // ... and, like a promise, the value is returned
	print("Unfollowed \(value) users")
})

Like magic! Your code will run in .background thread and you will get the result of each call only when it will be fulfilled. Async code in sync sauce!

Important Note: await is a blocking/synchronous function implemented using semaphore. Therefore, it should never be called in main thread; this is the reason we have used async to encapsulate it. Doing it in main thread will also block the UI.

async func can be used in two different options:

  • it can create and return a promise (as you have seen above)
  • it can be used to simply execute a block of code (as you will see below)

As we said we can also use async with your own block (without using promises); async accepts the context (a GCD queue) and optionally a start delay interval. Below an example of the async function which will be executed without delay in background:

async({
	print("And now some intensive task...")
	let result = try! Hydra.await(.background, { resolve,reject, _ in
		delay(10, context: .background, closure: { // jut a trick for our example
			resolve(5)
		})
	})
	print("The result is \(result)")
})

There is also an await operator:

  • await with throw: .. followed by a Promise instance: this operator must be prefixed by try and should use do/catch statement in order to handle rejection of the Promise.
  • await without throw: ..! followed by a Promise instance: this operator does not throw exceptions; in case of promise's rejection result is nil instead.

Examples:

async({
	// AWAIT OPERATOR WITH DO/CATCH: `..`
	do {
		let result_1 = try ..asyncOperation1()
		let result_2 = try ..asyncOperation2(result_1) // result_1 is always valid
	} catch {
		// something goes bad with one of these async operations
	}
})

// AWAIT OPERATOR WITH NIL-RESULT: `..!`
async({
	let result_1 = ..!asyncOperation1() // may return nil if promise fail. does not throw!
	let result_2 = ..!asyncOperation2(result_1) // you must handle nil case manually
})

When you use these methods and you are doing asynchronous, be careful to do nothing in the main thread, otherwise you risk to enter in a deadlock situation.

The last example show how to use cancellable async:

func test_invalidationTokenWithAsyncOperator() {

// create an invalidation token
let invalidator: InvalidationToken = InvalidationToken()

async(token: invalidator, { status -> String in
	Thread.sleep(forTimeInterval: 2.0)
	if status.isCancelled {
		print("Promise cancelled")
	} else {
		print("Promise resolved")
	}
	return "" // read result
}).then { _ in
	// read result
}

// Anytime you can send a cancel message to invalidate the promise
invalidator.invalidate()
}

Await can be also used in conjuction with zip to resolve all promises from a list:

let (resultA,resultB) = Hydra.await(zip(promiseA,promiseB))
print(resultA)
print(resultB)

Because promises formalize how success and failure blocks look, it's possible to build behaviors on top of them. Hydra supports:

  • always: allows you to specify a block which will be always executed both for fulfill and reject of the Promise
  • validate: allows you to specify a predica block; if predicate return false the Promise fails.
  • timeout: add a timeout timer to the Promise; if it does not fulfill or reject after given interval it will be marked as rejected.
  • all: create a Promise that resolved when the list of passed Promises resolves (promises are resolved in parallel). Promise also reject as soon as a promise reject for any reason.
  • any: create a Promise that resolves as soon as one passed from list resolves. It also reject as soon as a promise reject for any reason.
  • pass: Perform an operation in the middle of a chain that does not affect the resolved value but may reject the chain.
  • recover: Allows recovery of a Promise by returning another Promise if it fails.
  • map: Transform items to Promises and resolve them (in paralle or in series)
  • zip: Create a Promise tuple of a two promises
  • defer: defer the execution of a Promise by a given time interval.
  • cancel: cancel is called when a promise is marked as cancelled using operation.cancel()

always func is very useful if you want to execute code when the promise fulfills — regardless of whether it succeeds or fails.

showLoadingHUD("Logging in...")
loginUser(username,pass).then { user in
	print("Welcome \(user.username)")
}.catch { err in
 	print("Cannot login \(err)")
}.always {
 	hideLoadingHUD()
}

validate is a func that takes a predicate, and rejects the promise chain if that predicate fails.

getAllUsersResponse().validate { httpResponse in
	guard let httpResponse.statusCode == 200 else {
		return false
	}
	return true
}.then { usersList in
	// do something
}.catch { error in
	// request failed, or the status code was != 200
}

timeout allows you to attach a timeout timer to a Promise; if it does not resolve before elapsed interval it will be rejected with .timeoutError.

loginUser(username,pass).timeout(.main, 10, .MyCustomTimeoutError).then { user in
	// logged in
}.catch { err in
	// an error has occurred, may be `MyCustomTimeoutError
}

all is a static method that waits for all the promises you give it to fulfill, and once they have, it fulfills itself with the array of all fulfilled values (in order).

If one Promise fail the chain fail with the same error.

Execution of all promises is done in parallel.

let promises = usernameList.map { return getAvatar(username: $0) }
all(promises).then { usersAvatars in
	// you will get an array of UIImage with the avatars of input
	// usernames, all in the same order of the input.
	// Download of the avatar is done in parallel in background!
}.catch { err in
	// something bad has occurred
}

If you add promise execution concurrency restriction to all operator to avoid many usage of resource, concurrency option is it.

let promises = usernameList.map { return getAvatar(username: $0) }
all(promises, concurrency: 4).then { usersAvatars in
	// results of usersAvatars is same as `all` without concurrency.
}.catch { err in
	// something bad has occurred
}

any easily handle race conditions: as soon as one Promise of the input list resolves the handler is called and will never be called again.

let mirror_1 = "https://mirror1.mycompany.com/file"
let mirror_2 = "https://mirror2.mycompany.com/file"

any(getFile(mirror_1), getFile(mirror_2)).then { data in
	// the first fulfilled promise also resolve the any Promise
	// handler is called exactly one time!
}

pass is useful for performing an operation in the middle of a promise chain without changing the type of the Promise. You may also reject the entire chain. You can also return a Promise from the tap handler and the chain will wait for that promise to resolve (see the second then in the example below).

loginUser(user,pass).pass { userObj in 
	print("Fullname is \(user.fullname)")
}.then { userObj in
	updateLastActivity(userObj)
}.then { userObj in
	print("Login succeded!")
}

recover allows you to recover a failed Promise by returning another.

let promise = Promise<Int>(in: .background, { fulfill, reject in
	reject(AnError)
}).recover({ error in
    return Promise(in: .background, { (fulfill, reject) in
		fulfill(value)
    })
})

Map is used to transform a list of items into promises and resolve them in parallel or serially.

[urlString1,urlString2,urlString3].map {
	return self.asyncFunc2(value: $0)
}.then(.main, { dataArray in
	// get the list of all downloaded data from urls
}).catch({
	// something bad has occurred
})

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.

zip(a: getUserProfile(user), b: getUserAvatar(user), c: getUserFriends(user))
  .then { profile, avatar, friends in
	// ... let's do something
}.catch {
	// something bad as occurred. at least one of given promises failed
}

As name said, defer delays the execution of a Promise chain by some number of seconds from current time.

asyncFunc1().defer(.main, 5).then...

retry

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.
Retry also support delay parameter which specify the number of seconds to wait before a new attempt (2.0.4+).

// try to execute myAsyncFunc(); if it fails the operator try two other times
// If there is not luck for you the promise itself fails with the last catched error.
myAsyncFunc(param).retry(3).then { value in
	print("Value \(value) got at attempt #\(currentAttempt)")
}.catch { err in
	print("Failed to get a value after \(currentAttempt) attempts with error: \(err)")
}

Conditional retry allows you to control retryable if it ends with a rejection.

// If myAsyncFunc() fails the operator execute the condition block to check retryable.
// If return false in condition block, promise state rejected with last catched error.
myAsyncFunc(param).retry(3) { (remainAttempts, error) -> Bool in
  return error.isRetryable
}.then { value in
	print("Value \(value) got at attempt #\(currentAttempt)")
}.catch { err in
	print("Failed to get a value after \(currentAttempt) attempts with error: \(err)")
}

cancel is called when a promise is marked as cancelled from the Promise's body by calling the operation.cancel() function. See the Cancellable Promises for more info.

asyncFunc1().cancel(.main, {
	// promise is cancelled, do something
}).then...

Sometimes you may need to chain (using one of the available operators, like all or any) promises which returns different kind of values. Due to the nature of Promise you are not able to create an array of promises with different result types. However thanks to void property you are able to transform promise instances to generic void result type. So, for example, you can execute the following Promises and return final values directly from the Promise's result property.

let op_1: Promise<User> = asyncGetCurrentUserProfile()
let op_2: Promise<UIImage> = asyncGetCurrentUserAvatar()
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
	let friends = op_3.result
}.catch { err in
	// do something
}

You can install Hydra using CocoaPods, Carthage and Swift package manager

  • 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!
pod 'HydraAsync'

Carthage

github 'malcommac/Hydra'

Swift Package Manager

Add Hydra as dependency in your Package.swift

  import PackageDescription

  let package = Package(name: "YourPackage",
    dependencies: [
      .Package(url: "https://github.com/malcommac/Hydra.git", majorVersion: 0),
    ]
  )

Consider ❤️ support the development of this library!

Current version is compatible with:

  • Swift 5.x
  • iOS 9.0 or later
  • tvOS 9.0 or later
  • macOS 10.10 or later
  • watchOS 2.0 or later
  • Linux compatible environments
  • If you need help or you'd like to ask a general question, open an issue.
  • If you found a bug, open an issue.
  • If you have a feature request, open an issue.
  • If you want to contribute, submit a pull request.

Copyright & Acknowledgements

SwiftLocation is currently owned and maintained by Daniele Margutti.
You can follow me on Twitter @danielemargutti.
My web site is https://www.danielemargutti.com

This software is licensed under MIT License.

Follow me on:

hydra's People

Contributors

akostylev0 avatar anykao avatar bryanoltman avatar chrispix avatar daaavid avatar hachinobu avatar heckj avatar hy9be avatar malcommac avatar mcastagnolo avatar moaible avatar nhnam avatar numen31337 avatar pedrovereza avatar r-plus avatar revolter avatar tasanobu avatar terhechte avatar yaslab avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hydra's Issues

Zip with promises array

Hey, great library! I have been looking for something like this for a while.

I'd like to suggest that zip should receive an array of promises (if all of them shares the same return type) and a return a promise that combines all the inputs. It could also receive a reduce function as argument. What do you think?

I mean something like this:

var promises = [Promise<User>]()

// Some code here. 

Promise.zip(promises)
      .then { (users: [User]) in 

      }

Thanks!

Can't install using Carthage

I am getting next errors

*** Building scheme "Hydra-macOS" in Hydra.xcodeproj
** CLEAN FAILED **

The following build commands failed:
Check dependencies
(1 failure)
** BUILD FAILED **

The following build commands failed:
Check dependencies
(1 failure)
warning: no umbrella header found for target 'Hydra-macOS', module map will not be generated
warning: no umbrella header found for target 'Hydra-macOS', module map will not be generated
A shell task failed with exit code 65:
** CLEAN FAILED **

The following build commands failed:
Check dependencies
(1 failure)
** BUILD FAILED **

The following build commands failed:
Check dependencies
(1 failure)

Progress block

Hi Malcom! Is there any kind of progress block for things like uploading an avatar for example.

uploadAvatar()
.progress { p in
  // Here update progressView for example
}
.then(doSomething)
.onError(showErrorPopup)
.finally(doSomething)

Is the minimum requirement still iOS 9?

I'm using Hydra 2.0.6 with Xcode 13, but at compile time I get a message that Hydra requires iOS 12 or higher.
But I didn't have any problems when using Xcode 12 and Hydra 2.0.5.
Did the minimum requirements change? Thank you.

Showing Recent Messages
/Users/hoge/Test/ViewController.swift:11:8: Compiling for iOS 10.0, but module 'Hydra' has a minimum deployment target of iOS 12.0: /Users/hoge/Library/Developer/Xcode/DerivedData/Test-akvdfnkqlvyyxafhnfgahlouszoz/Build/Products/Debug-iphonesimulator/Hydra.framework/Modules/Hydra.swiftmodule/x86_64-apple-ios-simulator.swiftmodule

Crash on operation: PromiseStatus on iOS but not on macOS

We are using Hydra as part of a client API framework. There is a version without Promise that uses Alamofire for networking. We then add a Promise based wrapper for each API call.
The framework exists for both iOS and macOS.
Recently we upgraded to Xcode 11.2, swift 5.1 and Hydra 2.0.2 (from 1.2.1, using SPM).

Now our tests for macOS pass, but the iOS tests crash in Promise.swift, line 71:

	 public lazy var operation: PromiseStatus = {
		return PromiseStatus(token: self.invalidationToken, { [weak self] () -> () in
			self?.set(state: .cancelled)
		})
	}()

I tried to debug but could not figure out what is going on.

Here is a sample project that reproduces the bug.

The project has two targets, one for iOS and one for macOS. Both use the same source and test swift files.
The macOS tests pass but the iOS promise test fails (the standard api test passes, so the api part is ok).

Build fail on Xcode 12 beta 2

When trying to build I get the following error in Promise+Recover.swift:

Cannot convert value of type 'Promise<Void>' to closure result type()'

It works fine on Xcode 11.5.

Version 1.2.1

Hydra compile issues in Xcode 13

It seems because of the use of await function in Hydra source code, any code referencing Hydra would fail to compile in Xcode 13.
截圖 2021-06-10 下午9 53 38

Nested generics

Hi!

How about using Nested generics released in Swift 3.1?

I want to make State and Observer nested generics.

For example as below.

extension Promise {
	
	/// ....
	internal indirect enum State {
		case pending
		case resolved(_: Value)
		case rejected(_: Error)
		
		/// It continues below....
		
	}
		
	/// ....
	internal indirect enum Observer {
		typealias ResolveObserver = ((Value) -> (Void))
		typealias RejectObserver = ((Error) -> (Void))
		
		case onResolve(_: Context, _: ResolveObserver)
		case onReject(_: Context, _: RejectObserver)
		
		/// It continues below...		
	}
	
}

If there is no problem I think I will give out PR.

How to use Promise<Void> in Swift 5.2

Hi, I want to use Promise<Void> in Hydra 2.0.2 and Swift 5.2 (Xcode 11.4).

I tried to use it as following code.

Promise<Void>(in: .main, token: token) { resolve, reject, _ in
     resolve()
}

However compiler reports an error Missing argument for parameter #1 in call.

How do I change the code to use Promise<Void>?

Promise object will not dispose.

I add this line to DemoApp's viewDidLoad method.
Promise<Int>(resolved: 3).catch { _ in }

After launch the DemoApp, invoke memory visualizer in Xcode 8(below screenshot is it) that indicate two Promise object is still exist and memory leak(Strong reference cycle).

I confirmed this issue on 5692f3e.

2017-03-18 23 53 26

Retry vs Delay

Hi! First of all, I have to say that I really love this library 🙏
The reason for this issue is cause I'm not sure if I found a bug or if I misunderstood the semantics of retry with delay.

Context

I was trying to implement polling to a web server leveraging Promises.
Say we have promise: Promise<Value> which encapsulate the async call to our backend. Then I expressed polling with something like:

promise
  .validate { value in /* predicate to decide if continue polling */ }
  .retry(10, delay: pollingTime)

Expectation

Since:

  • validate rejects the promise if the predicate is not met
  • retry allow to execute source chained promise if it ends with a rejection (exactly what I need, to re-issue the web call)
  • delay is the delay between each attempt (starting when failed the first time)

My expectation was to retry the promise at most 10 times, each with a delay of pollingTime.

Result

AFAIU, the delay is implemented with the defer which in turn defers only the happy path (then) but the retryWhen is actually targeting a rejected promise. This results in the delay being applied in the only attempt that succeeds (if any).

I've also tried to encapsulate the validate inside the promise (rejecting it if the predicate is not met) but it didn't move the problem.
I could achieve what I expected only pushing the validation inside the promise and implementing a deferAll:

  func `deferAll`(in context: Context = .background, _ seconds: TimeInterval) -> Promise<Value> {
    guard seconds > 0 else { return self }

    let fireTime: DispatchTime = .now() + seconds
    return self
      .then(in: context) { value in return Promise<Value> { resolve, _, _ in
        context.queue.asyncAfter(deadline: fireTime) { resolve(value) }
      }}
      .recover { error in return Promise<Value> { _, reject, _ in
        context.queue.asyncAfter(deadline: fireTime) { reject(error) }
      }}
  }
}

and leveraging it inside the retryWhen in place of the defer.

Am I misunderstanding the semantic of retry with delay?

Thank you 😄

Promise.zip fails to compile with "cannot invoke 'zip' with an argument list of type", Xcode 8.3

Xcode 8.3 (8E162)

I'm in the process of evaluating Hydra over PromiseKit, the overall process of converting from one to the other hasn't been too hard, actually cleaned up the code a bit, but I've hit a snag

When trying to work with zip I keep getting

Error:(695, 24) cannot invoke 'zip' with an argument list of type '(a: Promise<APIAccessRestrictionState>, b: Promise<APIServiceAccessState>)'

I've expanded the code a little in an effort to try and solve the issue (and make it easier to read)

let restrictionStatePromise: Promise<APIAccessRestrictionState> = CioffiAPIManager.shared.getServicesAuthenticatedState()
let accessStatePromise: Promise<APIServiceAccessState> = CioffiAPIManager.shared.getServiceAccessState()

return Promise<Void>.zip(
			a: restrictionStatePromise,
			b: accessStatePromise)
		.then { (restrictionState: APIAccessRestrictionState,
		         accessState: APIServiceAccessState) -> Promise<Bool> in
			self.authorisedState = DefaultAPIAccessRestrictionState(from: state)
			self.serviceAccessState = state
			self.authenticationChanged()
			return Promise<Bool>(resolved: true)
		}

I've also tried using...

return Promise.zip(
		a: restrictionStatePromise,
		b: accessStatePromise)

But neither work.

APIServiceAccessState is a typealias of APIAccessRestrictionState...

public typealias APIServiceAccessState = APIAccessRestrictionState

so I'm wondering if that's the issue and if something like all might be better

Promise.all()

Promise.all is throwing a no member all error.

import Hydra 

Promise.all(promises).then({
})

Leaks?

Played around with this library a bit and noticed Xcode is reporting quite a few memory leaks using the latest version and with a simple function:

func testPromise() -> Promise<Int> {
        return Promise<Int>(in: .utility, { (resolve, reject, _) in
            resolve(5)
        })
}

testPromise().then { result in
        print(result)
}

screen shot 2017-07-30 at 17 03 32

Ubuntu compilation error

/home/user/Project/.build/checkouts/Hydra.git-8391472609009747209/Sources/Hydra/Promise+Await.swift:99:20: error: binary operator '!=' cannot be applied to two 'DispatchQueue' operands
                guard self.queue != DispatchQueue.main else {
                      ~~~~~~~~~~ ^  ~~~~~~~~~~~~~~~~~~

Swift version 5.0.1 (swift-5.0.1-RELEASE)
Target: x86_64-unknown-linux-gnu

Ubuntu 16.04.6 LTS

Cancellable Promise in a Chain

Hi guys, I have been using this library for a while and I have to say it's very easy to work with it.

I have a concern about cancellable promises, specifically the behaviour of a chain of promises when a promise in the middle has been cancelled.
Let's say we have:

A.then(B).then(C).then(D).then(E)
    .then { _ in
        print("success")
    }.catch { _ in
        print("failed")
    }.cancelled { in
        print("cancelled")
    }

If, for example, C gets cancelled while A and B resolve correctly, I'm expecting that cancelled is printed out, but what I observe is that the sequence gets stuck in pending state.
This behaviour can be checked with the following test that recalls the ones provided with the library, which fails for timeout:

  func test_cancelledPromiseChainable2() {
    let exp = expectation(description: "test_cancelledPromiseChainable2")
    
    let invalidator: InvalidationToken = InvalidationToken()
    invalidator.isCancelled = true
    
    intPromise().then { _ in
        self.test_invalidationToken(token: invalidator)
      }.then { total in
        print("Operation finished with: \(total)")
        XCTFail()
      }.catch { _ in
        print("Operation failed")
      }.cancelled {
        print("Operation cancelled")
        exp.fulfill()
    }
    
    waitForExpectations(timeout: expTimeout, handler: nil)
  }

If I'm not misinterpreting the situation, I will open a PR with a suggested fix, otherwise please let me know the reasoning behind this behaviour.

Thanks,
AT

Version 1.0.0 does not compile in Swift 3

I am currently unable to build Hydra 1.0.0 in Swift 3 for the MacOS target.

In Promise+Recover.swift, the recover method does not compile correctly because it is unable to resolve which then method to invoke. The build error is: /Hydra/Promise+Recover.swift:47:11: Ambiguous reference to member 'then(in:_:)'

If you remove the lines:
.cancelled(in: ctx, { _ -> Void in operation.cancel() })

then the swift compiler is able to resolve the appropriate then helper and the framework compiles.

Method is ambiguous for type lookup in this context

Need to rename the Hydra enum to something else because it causing the ambiguity error in those projects in which similar namination is used. For eg. In my project I used a class with name Promise and when I try to tell compiler that use the Promise of Hydra by Hydra.Promise<Int> then it start complaing that Hydra Enum doesn't contain a case promise. For more Information check issue

Relevant Issue

Importing Hydra in a file where there is a UIViewControllerRepresentable gives compilation errors

Hi everyone!
I'm using Hydra 2.0.6 on iOS on Xcode 13.2.1.

I would like to import Hydra on this file

import SwiftUI

struct VideocallViewControllerWrapper: UIViewControllerRepresentable {
    ...
}

but as soon as I import it, Xcode tells me that Type 'VideocallViewControllerWrapper' does not conform to protocol 'UIViewControllerRepresentable'. I have other files where I have successfully imported both SwiftUI and Hydra. I think the problem is the presence of UIViewControllerRepresentable.

Thank you

await does not work on ios11

Hey I used to use await to queue up my task executions

for a in self.aaa {
    try await(self.do[a].execute())
}

But I upgraded to ios 11 today and run into a EXC_BREAKPOINT here. The call stack is
image

And the error is
image

Deprecation of `await` keyword in Swift 5.4+. Using new Hydra.await()

Ciao 👋

As reported in the release notes, the await keyword will start to emit warnings in Xcode 12.5:

Deprecations
The Swift compiler emits a warning for the use of the await keyword as an unqualified identifier. Wrap await with back-ticks so Swift always treats it as an identifier, or fully qualify declarations named await (for example, by adding self if it is a reference to an instance member named await from within the instance). (SE-0296, 67000350)

Is there any plan to address this in Hydra?

Thanks 🙌

Can't get notified in Promise

return Promise( { [unowned self] (resolve, reject) in

        NotificationCenter.default.addObserver(
            forName: SOME_NOTIFICATION,
            object: nil,
            queue: nil,
            using: { (notification:Notification) -> Void in
                resolve() // never called
        })
        SOME_ACTION_TRIGGER_NOTIFICATION()
    
    })

Cancellable Promise

I would to implement cancel() func in Hydra.
These days I'll investigate by looking at other libs implementations.
(@r-plus have you some advices?)

failed build on swift 3.1 spm version

using swift 3.1 version of spm and its having trouble installing though server side project is in 4.0 (using Perfect Server Side) . too many errors to copy and paste here but It tried different major and minor versions and no success.

Promise+Recover.swift does not compile for macOS / Swift 4

I get the following compile error:

/Users/stefan/Documents/Git-Workspace/Modules/Hydra/Sources/Hydra/Promise+Recover.swift:47:11: error: ambiguous reference to member 'then(in:_:)'
                        return self.then(in: ctx, {
                               ^~~~
/Users/stefan/Documents/Git-Workspace/Modules/Hydra/Sources/Hydra/Promise+Then.swift:48:14: note: found this candidate
        public func then<N>(in context: Context? = nil, _ body: @escaping ( (Value) throws -> N) ) -> Promise<N> {
                    ^
/Users/stefan/Documents/Git-Workspace/Modules/Hydra/Sources/Hydra/Promise+Then.swift:76:14: note: found this candidate
        public func then<N>(in context: Context? = nil, _ body: @escaping ( (Value) throws -> (Promise<N>) )) -> Promise<N> {
                    ^
/Users/stefan/Documents/Git-Workspace/Modules/Hydra/Sources/Hydra/Promise+Then.swift:118:14: note: found this candidate
        public func then(in context: Context? = nil, _ body: @escaping ( (Value) throws -> () ) ) -> Promise<Value> {
                    ^

I'm using Xcode 9.0 (9A235), swift 4 and a deployment target of 10.12.

Feature request: zip analog with failed promises

I think this will be helpful to have some function analog to the 'zip' function which will accept the list of promises, wait until all of them is finished and returns both failed and resolved in the result.

Make .voidPromise() public

.voidPromise() should be public in order to be used to chain multiple Promises which return different results.

Example:

let op_1: Promise<User> = asyncGetCurrentUserProfile()
let op_2: Promise<UIImage> = asyncGetCurrentUserAvatar()
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
  let friends = op_3.result
}.catch { err in
  // do something
}

We will also rename voidPromise() func to void variable.

Hydra and iOS 9 compatibility

Nice library! This is exactly what I'm looking for. But I got this error message when I tried to integrate it with my project (by Carthage).

Module file's minimum deployment target is ios10.2 v10.2: ./Carthage/Build/iOS/Hydra.framework/Modules/Hydra.swiftmodule/arm64.swiftmodule

Add support for cancellable Promise in await operator

Let's say I use the example:

let asyncFunc = async({ _ -> Int in
  let loggedUser = try await(loginUser(username,pass))
  let followersList = try await(getFollowers(loggedUser))
  let countUnfollowed = try await(unfollow(followersList))
  return countUnfollowed
}).then({ value in
  print("Unfollowed \(value) users")
})

How would I make that cancelable?

v0.9.9 doesn't compile on Swift 4

I get 3 warnings and 4 errors when compiling on Swift 4 (even though the readme says v0.9.6 and above is swift 4 compatible). The readme also references a swift-4 branch, which doesn't seem to exist.

Fix for await function

I'm trying to use await using the syntax from the readme.

let city = try await(getCityPromise())

The getCityPromise function is declared like this:

func getCityPromise() -> Promise<String> { ... } 

But the compiler raises an error

cannot invoke 'await' with an argument list of type '(Promise<String>)'

Then I looked at the unit tests, and I saw that a special operator .. is used instead of calling the await function.

Using that operator my code doesn't raise any error anymore. Should the documentation be fixed or I'm missing something?

A second resolve in the Promise

Hi Daniele and contributors,

I was wondering if you had thoughts on this situation. I'm wrapping an Alamofire download, which would resolve a temporary url, and then the final destination url. It would be nice to have a couple of Promises. Any perspective would be greatly appreciated.

func streamFile(key: String = "default") -> Promise<URL> {
        let source = self.serverURL(forKey: key)
        let (localURL, exists) = self.fileURL()
        return Promise<URL>({ resolve, reject in
            if !exists {
                Alamofire.download(source, to: { temp, response in
                    // Temporary URL
                    resolve(temp)
                    return (localURL, [.removePreviousFile, .createIntermediateDirectories])
                }).response { response in
                    if response.error == nil {
                        // Final URL
                        resolve(response.destinationURL!)
                    }
                    else {
                        log(error: response.error.debugDescription)
                        reject(CommonError.networkConnectionError)
                    }
                }
            }
            else {
                resolve(localURL)
            }
        })
    }

How to use async function (example)

This will not run:

image

not even the print statement runs.

This however runs:

image

Does the async func always need to be used in a chain using methods like .always? The README examples seem to show this is not needed, but I am confused about its actual usage.

NOTE: the waitUntil func is from Nimble

Even if there was some sort of problem w/ the waitUntil func, shouldnt the 1st print statement at least execute before it timesout?

How to put a Promise into always?

Hey guys, first of all thank you very much for this great library!

I have dismissActivityIndicator method, which returns a promise:

func dismissActivityIndicator<T>(any: T) -> Promise<T> {
    return Promise<T> { resolve, _, _ in
        KVNProgress.dismissAnyway {
            resolve(any)
        }
    }
}

My question is how can I inject it into the promise chain, so it is always executed at end? It's fairy simple with always and a plain non-async method, but how to achieve the same result with the method returning a Promise?

help wanted would be an appropiate tag for this question, as this is not an issue :)

zip() function as global function (outside Promise class)

What I was wishing for:

all(promiseA,promiseB).then { resultA, resultB in
}

Based on the docs it would have to be

all(promiseA,promiseB).then { result in
  let resultA = result[0]
  let resultB = result[1]
}

or

Promise<Void>.zip(promiseA, promiseB).then { resultA, resultB in
}

Any thoughts?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.