A small collection of super useful utility functions for managing complex async behavior with promises
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.
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"
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 ZousanIn 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
}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>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 |
|---|---|---|
|
<String> |
The item name. |
|
<ANY> |
The value that will be assigned to this item. If this is a function, the function will be called. If it is a |
|
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 |
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)
}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:
-
The
arraypassed in may optionally be aPromisethat resolves to anArray. -
The function
fnmay return a value or aPromisewhich resolves to a value to be stored in the resultingnewArray. -
The items contained within the passed array may be
Promiseobjects which will be resolved before passing the result into the mapping functionfn.
Some examples:
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]
})// 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]
})// 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)
}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.
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.
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
})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.
|
| Option | Description | default |
|---|---|---|
|
(Previously |
false |
|
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 |
|
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 |
|
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.
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.
Zousan.series(1,2,3) // Resolves to 3Zousan.series(2.5,Math.floor) // Resolves to 2function add6(x) { return x + 6 }
Zousan.series(3,add6,add6,log) // calls log with 15The 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)
}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.
var ts = Zousan.tSeries(1,2,3)
// ts.prom is a Promise that resolves to 3
// ts.res is the array [1,2,3]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
})