Skip to content

bluejava/zousan-plus

Repository files navigation

Zousan-plus 🐘➕

A small collection of super useful utility functions for managing complex async behavior with promises

Introduction

Extensions for Zousan 🐘.

The goal of Zousan was to stay small and light. But sometimes you wan’t some additional expressive power. Rather than package that into Zousan, I decided to make a separate entry point - and give you the choice on a per-project basis.

Just need lightning fast promises on the down low? Use Zousan.

Could benefit from additional expressive power ala map, promisify, series, or tSeries? Use Zousan-plus.

Of course, you can also just grab individual functions from this to paste into your project and use with Zousan or native Promises if you want to keep things minimal. This code is fairly small, well documented and easy to steal from. ;-)

For more information about whats to come here or where these decisions came from, check out the moreInfo document.

Downloading

Grab from the npm repository via:

npm install --save zousan-plus

or via yarn:

yarn add zousan-plus

Loading / Instantiation

ES Module

For use in modern JavaScript environments, or with a build tool that supports ES Modules such as webpack or Rollup.

import Zousan from "../extModules/zousan-plus"

commonJS

If you are using Node or another commonJS-based module loader such as Browserify or webpack, you can simply require the extended Zousan and assign to Zousan. There is no need to first define standard zousan and then extend it - this happens within zousan-plus

var Zousan = require("zousan-plus"); // You now have an extended Zousan

AMD (requireJS)

In similar fashion, you need only require the zousan-plus module, and it will internally first obtain a standard zousan, extend it, and return it:

var Zousan = require(["zousan-plus"], function(Zousan) {
	// Within this AMD require wrapper, we have access to
	// an extended Zousan in the Zousan variable
}

No Module Loader (Global)

If you are not using a module loader and/or wish to define things globally, you must first load the standard zousan and then extend it by loading zousan-plus, such as:

<script src="node_modules/zousan/zousan-min.js"></script>
<script src="node_modules/zousan-plus/zousan-plus-min.js"></script>

API

Zousan.evaluate(array | …​args) ⇒ Promise <resultsObject>

Evaluate a series of name/value pairs, optimizing the workflow for maximum asynchronisity. This is the holy grail for managing complex Promise/then chains with varying dependencies. It also manages values between promises and function calls to allow calling functions with eventual values out of order, or in multiples.

Zousan.evaluate is called with a series of name/value definitions (items). These items can be specified as a list of arguments to evaluate or contained within an Array. The resultsObject that is eventually returned contains properties for each item based on its name containing the respective value.

items can contain the following properties:

Property Name Type Description

name

<String>

The item name.

value

<ANY>

The value that will be assigned to this item. If this is a function, the function will be called. If it is a Promise, it will be resolved. Any other type is simply assigned as is.

deps

Array <String or ANY>

The list of dependencies for this item. These will be used as arguments to the function if a function is specified in the value property. If this is a String and it matches a named property in the item list, that item will be resolved before this item is evaluated.

Example
 Zousan.evaluate(
		{ name: "a", value: 100 },
		{ name: "b", value: myPromise },
		{ name: "c", value: getData, deps: [ "a", "b" ]}
	).then(function(retObj) {
			// retObj.a = 100
			// retObj.b = myPromise resolved value
			// retObj.c = result from getData(100, retObj.b)
		}

Zousan.map(array,fn) → newArray

The map function takes an Array and a function and returns a new Array containing the result of passing each respective item from the first array through the function.

This is much like the Array.map function, and in fact can be used interchangeably in many instances. The difference is:

  1. The array passed in may optionally be a Promise that resolves to an Array.

  2. The function fn may return a value or a Promise which resolves to a value to be stored in the resulting newArray.

  3. The items contained within the passed array may be Promise objects which will be resolved before passing the result into the mapping function fn.

Some examples:

Here we behave just like Array.map
var double = function(x) { return x * 2 }
var array = [5,6,7]

var newArray = Zousan.map(array, double)
	.then(function(newArray) {
			// newArray is [10,12,14]
		})
Using a transforming function that returns promises
// returns a promise of a value which resolves in the specified ms
var later = function(ms,val) {
		return new Zousan(function(resolve,reject) {
				setTimeout(resolve,ms,val)
			})
	}

// returns a promise to triple the passed value in 100ms
var tripleLater = function(x) { return later(100, x * 3) }

var array = [5,6,7]

var newArray = Zousan.map(array, tripleLater)
	.then(function(newArray) {
			// newArray is [15, 18, 21]
		})
A realistic and typical use case
// Returns a promise to resolve to album information of the album ID specified
function getAlbumInfo(albumId)
{
	return ajaxCall(getAlbumQueryURL(albumId))
}

// Pass in an array of album IDs and you will get a promise which resolves to
// an array of album information objects respectively
function getMultipleAlbumInfo(albumIdArray)
{
	return Zousan.map(albumIdArray, getAlbumInfo)
}

Zousan.namedAll(valuesObject) → Promise resultsObject

just like Promise.all except each item is a name/value pair and the resolved value is an object with name/value pairs with the resolved values.

A mix of values, functions, and promises can be used as values. Promises and functions that return promises are first resolved before assigned.

Example
return Zousan.namedAll({
		id: userId,  // Integer
		pb: startProgressBar, // function whose return is ignored
		user: getUser(userId), // returns a promise
		items: getUserItemList(userId) // returns a promise
	})
	.then(function(ob) {
			// Here ob contains the following:
			// { id: userId, pb: <??>, user: userObject <from resolved promise>, items: itemList <from promise> }
			endProgressBar()
		})
	.catch(function(err) {
			// lets hope this doesn't happen!
		})

Note: With function values, you can add the parens (execute immediately) or not. If you do, it is executed BEFORE calling Zousan.namedAll and its result (which can be a Promise) is assigned (or resolved and assigned). If you do not, namedAll will detect its a function and call it (with no arguments). If you need to pass arguments into a function, you will need to use the former style.

In the following example, the functions f1 and f2 are both evaluated and their results assigned to x and y - but f1 is executed before calling namedAll and f2 is executed during the processing in namedAll. In practice there is little distinction, and the result will be the same.

Example of immediate and non-immediate functions
return Zousan.namedAll({
		x: f1(),	// this is executed immediately - its result is used as arg to namedAll
		y: f2		// this function is passed to namedAll - and namedAll executes it
	})

Zousan.promisify(object, conf) → promisifiedObject

Pass in an Object (i.e. module) and all functions that appear to expect callbacks will have new functions created that are equivalent but return a Promise instead. The newly available "promisified" function will be named <original function name>Prom by default - but this can be confiigured by setting Zousan.PROMISIFY_FN_EXTENSION to a different extension. If Zousan.PROMISIFY_FN_EXTENSION is set to "" (empty string) then the original function will be replaced by the promisified version. This breaks some modules, so is not recommended.

The behavior of the promisification can be effected via the conf configuration object.

Promisification is an imperfect process, as it can depend on how the underlying functions are written. This promisify function works by examining all functions contained on the object and if the argument list ends with one of the recognized callback names, it is promisified. The current list of callback arguments is "cb", "callback", "done" and "callback_"

Callback functions are expected to be called with two arguments: callback(error, value). The promise will resolve when the callback is called with a falsy first argument (i.e. when the error is null or undefined), and using the second argument as the resolved value. If the first argument is set, the promise is rejected with the error value.

Warning
In some cases, promisification has been known to break certain functions or modules. Since version 2.0 of Zousan-plus (and adding rather than replacing functions) this issue has been largely mitigated. If it still occurs, try specifying only those functions that you need promisified in the fnNames configuration option.

conf configuration options

Option Description default

promisifyAll

(Previously replaceAll) Promisify all functions within the specified objects rather than examining the arguments for the presence of a callback.

false

fnNames

An array of function names to promisify within the specified object. This overrides the default behavior of examining the last argument name of each function.

null

cbArgNames

An array of callback names which overrides the default list. It is the presence of one of these named arguments as the final argument of a function which triggers promisification (unless promisifyAll or fnNames is used)

["cb", "callback", "done", "callback_"]

Zousan.promisifyFn(fn) → promisifiedFn

Promisifies a single function fn and returns it.

Zousan.series(array | …​args) → Promise

The series function takes a list (either as separate arguments or as an array) who’s items can be of any type and evaluates them one by one. A Promise is returned which will resolve to the final evaluation of the series, or reject upon a rejection/exception encountered during evaluation.

Item Evaluation

If an item is an Object or native type, it simply evaluates to itself. If it is a function, the function is called and evaluates to its return value. If it is a Promise, it evaluates to its resolved value. If it is a function that returns a Promise the function is called and the item evaluates to the Promises’s resolved value.

Similar to compose in functional libraries and languages, when an item is a function, the value of the previous item is passed in as an argument. The return/resolved value is then used for the following item.

All Native Types Example:
Zousan.series(1,2,3) // Resolves to 3
Native type and function
Zousan.series(2.5,Math.floor) // Resolves to 2
Example 1
function add6(x) { return x + 6 }

Zousan.series(3,add6,add6,log) // calls log with 15

The above function is essentially doing this:

function add6(x) { return x + 6 }

Zousan.resolve(3)
	.then(add6)
	.then(add6)
	.then(log)

Of course it is very handy when used with Promises. The following function getUserAlbumCovers takes a user Id, makes an AJAX call to obtain the user object (getUserObj), extracts the albumList property to make another AJAX call to getAlbumsByIDList to get a list of album objects, extract out each of their id values into a list and finally get the album art via the getAlbumCoversByIDList AJAX call.

function getUserAlbumCovers(userId)
{
	return Zousan.series(userId, getUserObj, prop("albumList"),
			getAlbumsByIDList, pluck("id"), getAlbumCoversByIDList)
}

Which is equivalent to:

function getUserAlbumCovers(userId)
{
	return getUserObj(userId).then(prop("albumList"))
			.then(getAlbumsByIDList).then(pluck("id")).then(getAlbumCoversByIDList)
}

As you can see, it mostly just removes the need to continuously call then on each item - which helps remove a lot of noise when trying to read a long series of tasks.

It also offers the ability to inject native types or Promises into the series directly:

function test(p) // some promise passed in
{
	return Zousan.series(user, render, p, log) // call render(user) then wait for p to complete and log the result
}

Equivalent using then chains:

function test(p) // some promise passed in
{
	return Promise.resolve(user) // call render(user) then wait for p to complete and log the result
		.then(render)
		.then(function() { return p })
		.then(log)
}

Zousan.tSeries(array | …​args) → { prom: Promise, res: [resultsArray] }

Similar to the series function above, but tracks results from each step in the series and makes them available via the res property as a results array. The Promise is accessible via the prom property.

The simplest example:
var ts = Zousan.tSeries(1,2,3)
// ts.prom is a Promise that resolves to 3
// ts.res is the array [1,2,3]
A bit more complex example:
function add6(x) { return x + 6 }

// Return the specified value plus 3 after 100ms
function add3Later(x) {
	return new Zousan(function(resolve) {
			setTimeout(resolve,100,x+3)
		})
	}

var ts = tSeries(1,2,3,add6,add3Later)
ts.prom.then(function(final) {
		// ts.res[0] = 1
		// ts.res[3] = 9
		// ts.res[4] = 12
		// final = 12
	})

License

See the LICENSE file for license rights and limitations (MIT).