0.0.3 • Published 8 years ago

then-ae v0.0.3

Weekly downloads
5
License
MIT
Repository
github
Last release
8 years ago

Ae

Then æsthetics - helper functions for working with js promises.

Usage

npm install --save then-ae

var ae = require('then-ae');

Motivation

In my experience with working with js promises over the last 2 years, I learned to appreciate a certain programming style.

At first, my code looked pretty much like the regular callback-hell based code.

readSomeSettings
	.then(function (settings) {

		var relevantPieceOfSettings = extractRelevantPiece(settings);

		return fetchDataBasedOnSettings(relevantPieceOfSettings)
			.then(function (dataInJSONFormattedText) {

				var data = JSON.parse(dataInJSONFormattedText);

				return doStuffWithIt(data);
			});
	});

The reason was that I was more comfortable working with plain data instead of promises as soon as I had the chance.

Consider this way of doing it instead:

readSomeSettings
	.then(extractRelevantPiece)
	.then(fetchDataBasedOnSettings)
	.then(JSON.parse)
	.then(doStuffWithIt)

Instead of trying to work in the "normal" synchronous world, all data is returned as soon as possible to be wrapped up in a promise. From there, then is used to transform it further.

This is a contrived example, but I find that this style of coding makes the code more readable, writable and maintainable.

However, common methods like map and reduce don't work well with then. This library fixes that.

Have a look at this less contrived example. Starting with this callback hell-ish implementation, let's refactor to a more æsthetic style.

fetchListOfUrls
	.then(function (urlsJson) {

		return Promise.all(JSON.parse(urlsJson).map(function (url) {

			return fetchUrl(url);
				.then(function (urlContent) {

					return saveToDisk(urlContent);
				})
		}))
	});

It's 3 scopes deep and just barely readable. In a real application it would be even worse. We can start with the low hanging fruit by breaking out JSON.parse and Promise.all.

fetchListOfUrls
	.then(JSON.parse)
	.then(function (urls) {

		return urlsJson.map(function (url) {

			return fetchUrl(url);
				.then(function (urlContent) {

					return saveToDisk(urlContent);
				})
		}))
	})
	.then(Promise.all.bind(Promise));

I think this is better separation of concern. Now we don't need those nested scopes. Let's un-nest them.

fetchListOfUrls
	.then(JSON.parse)
	.then(function (urls) {

		return urls.map(fetchUrl);
	})
	.then(function (urlsContent) {

		return urlsContent.map(saveToDisk);
	})
	.then(Promise.all.bind(Promise));

Shallower and clearer, but with lots of boilerplate noise. All the function () {} and return (which is easy to miss, but hard to debug, by the way) just serve to confuse the meaning of the actual code.

You might have noticed how both anonymous functions are nearly identical, and completely pointless. Let's use an implementation of mapthat works with then. Also, Promise.all.bind(Promise) looks horribly redundant.

fetchListOfUrls
	.then(JSON.parse)
	.then(ae.map(fetchUrl))
	.then(ae.map(saveToDisk))
	.then(ae.all);

Nice.

API

ae.method(methodName)(...arguments)

Arguments

methodName: String

Operates On

Object

Description

A curried function to call the method named methodName with the arguments in the second function application.

Since it is curried, you can easily implement proxy functions for any method.

Without ae:

promiseOfArray
	.then(function (array) {

		return array.slice(5);
	})

With ae:

var slice = ae.method('slice');

promiseOfArray
	.then(slice(5))

Proxies for Array.prototype.map, .filter, .reduce and .join

Arguments

The same arguments you would pass to their Array.prototype counterpart.

Operates On

any[]

Description

Use these to manipulate a promise resolving with an array, much like you would in a synchronous setting.

Without ae:

promiseOfArrayOfStrings
	.then(function (arrayOfStrings) {

		return arrayOfStrings
			.map(function (string) {

				return string.toUpperCase();
			})
			.join(', ');
	})

With ae:

promiseOfArrayOfStrings
	.then(ae.map(function (string) {

		return string.toUpperCase();
	}))
	.then(ae.join(', '))

Or slightly shorter:

promiseOfArrayOfStrings
	.then(ae.map(ae.method('toUpperCase')()))
	.then(ae.join(', '))

ae.object(propertyNames)

Arguments

propertyNames: string[]

Operates On

any[]

Description

Turns an array into an object with property names specified by the argument.

Can optionally be called with multiple parameters instead of an array, like ae.object(propertyName1 [, propertyName2...]).

Without ae:

somePromise
	.then(function (results) {

		return {
			someProperty:     results[0],
			anotherProperty:  results[1]
			lotsOfProperties: results[2]
			iCouldGoOn:       results[3]
		};
	})

With ae:

somePromise
	.then(ae.object(
		'someProperty',
		'anotherProperty',
		'lotsOfProperties',
		'iCouldGoOn'
	))

With Promise.all:

Promise.all([
	doThis,
	doThat,
	doSomethingElse
])
	.then(ae.object([
		'this',
		'that',
		'somethingElse'
	]))

ae.assert(assertionCallback, errorMessage)

Arguments

assertionCallback: function(any) => boolean errorMessage: String

Operates On

any

Description

Use the assertion callback to check the result of the previous promise. If you return false, an error with the message errorMessage will be thrown.

Without ae:

userPromise
	.then(function (user) {

		if (!user.hasPermission) {

			throw new Error('User missing permission.');
		}
	})

With ae:

userPromise
	.then(ae.assert(
		function (user) { return user.hasPermission; },
		'User missing permission.'
	))

With ES6:

userPromise
	.then(ae.assert(
		user => user.hasPermission,
		'User missing permission.'
	))

ae.pipeline

Arguments

No arguments. Don't call it, just pass it in.

Operates On

An array of Promise generators. (function => Promise)[]

Description

Takes an array of promise generators. Runs each promise generator sequentially, and passes the result of each one into the next. The resulting promise contains the result of the last generated promise. If any generated promise rejects, the pipeline ends there (no more generator is executed), and the resulting promise is rejected with the error of the failed promise.

Without ae:

arrayOfPromiseGeneratorsPromise
	.then(function (arrayOfPromiseGenerators) {

		return arrayOfPromiseGenerators
			.reduce(
				function (soFar, next) {

					return soFar.then(next);
				},
				Promise.resolve()
			);
	})

With ae:

arrayOfPromiseGeneratorsPromise
	.then(ae.pipeline)

ae.all

Arguments

No arguments. Don't call it, just pass it in.

Operates On

Promise[]

Description

Simply a bound version of Promise.all, so that it can be passed to then.

Without ae:

arrayOfPromisesPromise
	.then(Promise.all.bind(Promise))

With ae:

arrayOfPromisesPromise
	.then(ae.all)

ae.sequence

Arguments

No arguments. Don't call it, just pass it in.

Operates On

An array of Promise generators. (function => Promise)[]

Description

Takes an array of promise generators. Runs the promise generators in sequence and resolves with an array containing the results of each promise. Much like Promise.all but not in parallel. If any of the generated promises rejects, the resulting promise is rejected with the error of the failed promise. The error will have an array partialSequenceResults containing the results of the promises that resolved successfully.

Useful when you need to make sure thing are executed in order. Like db inserts following deletes, etc.

Without ae:

Way too much error prone code.

With ae:

arrayOfPromiseGenerators
	.then(ae.sequence)

ae.parallel(numWorkers)

Arguments

numWorkers: number

Operates On

An array of Promise generators. (function => Promise)[]

Description

Takes an array of promise generators. Runs the promise generators in parallel, but limited to numWorkers "threads" at any time. If any of the generated promises rejects, all workers are canceled, and the resulting promise is rejected with the error of the failed promise.

Useful when you need to convert a gazillion image files, or any other task you'd like to run in parallel, but that would use too much resources to do all at once.

Without ae:

Seriously? I don't even know. Probably close to what ae implements, copy-pasted from Stack Overflow. :P

With ae:

arrayOfResourceIntensivePromiseGenerators
	.then(ae.parallel(4))