Giter Site home page Giter Site logo

hydra's Introduction

Hydra

Carthage compatible CI Status Version License Platform

Love your async code again with Hydra
Made with in pure Swift 3+, no dependencies, lightweight & fully portable

★★ Star our github repository to help us! ★★

Created by Daniele Margutti (@danielemargutti)

Hydra

Hydra is full-featured lightweight library which allows you to write better async code in Swift 3+. 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 operatorsa as like always, validate, timeout, retry, all, any, pass, recover, map, zip, defer and retry. Starts writing better async code with Hydra!

Internals

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

You also may like

Do you like Hydra? I'm also working on several other opensource libraries.

Take a look here:

  • SwiftDate - Date & Timezone management in Swift
  • SwiftLocation - CoreLocation and Beacon Monitoring on steroid!
  • SwiftRichString - Elegant and painless attributed string in Swift
  • SwiftScanner - String scanner in pure Swift with full unicode support
  • SwiftSimplify - Tiny high-performance Swift Polyline Simplification Library

Index

What's a Promise?

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!

Create a Promise

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>(.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 {
            fulfill((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)

How to use a Promise

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 Multiple Promises

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

Await: async code in sync manner

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 await with Hydraw's Promises is pretty simple: for example the code above can be rewritten directly as:

do {
	let loggedUser = try await(loginUser(username,pass))
	let followersList = try await(getFollowers(loggedUser))
	let countUnfollowed = try await(unfollow(followersList))
	print("Unfollowed \(count) users")
} catch {
	print("Something bad has occurred \(error)")
}

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! (You can however pick your custom GCD queue).

You can also use await with your own block:

print("And now some intensive task...")
let result = try! 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 throws exceptions; in case of promise's rejection result is nil instead.

Examples:

// 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: `..!`
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

All Features

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.

always

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

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

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

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) }
Promise.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
}

any

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

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

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

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

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.

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

defer

As name said, defer delays the executon 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.

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

Installation

You can install Swiftline using CocoaPods, carthage and Swift package manager

CocoaPods

use_frameworks!
pod 'HydraAsync'

Carthage

github 'malcommac/Hydra'

Swift Package Manager

Add swiftline as dependency in your Package.swift

  import PackageDescription

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

Requirements

Current version is compatible with:

  • Swift 3.0+
  • iOS 8.0 or later
  • tvOS 9.0 or later
  • macOS 10.0 or later
  • watchOS 3.0 or later
  • Linux compatible environments

Credits & License

Hydra is owned and maintained by Daniele Margutti.

As open source creation any help is welcome!

The code of this library is licensed under MIT License; you can use it in commercial products without any limitation.

The only requirement is to add a line in your Credits/About section with the text below:

This software uses open source Hydra's library to manage async code.
Web: http://github.com/malcommac/Hydra
Created by Daniele Margutti and licensed under MIT License.

hydra's People

Contributors

malcommac avatar

Watchers

 avatar

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.