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 Zousan
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
}
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
array
passed in may optionally be aPromise
that resolves to anArray
. -
The function
fn
may return a value or aPromise
which resolves to a value to be stored in the resultingnewArray
. -
The items contained within the passed array may be
Promise
objects 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 3
Zousan.series(2.5,Math.floor) // Resolves to 2
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)
}
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
})