Zousan Plus - When a Promise is Not Enough

October 15, 2016

Enjoying Zousan – the fastest Promise implementation on the planet – but wish it had more accessory functionality? Check out Zousan-Plus!

When I wrote Zousan, the goal was a Promise A+ implementation that was very small and very fast. I wanted this for use in mobile sites, Internet of Things builds, and other resource-constrained environments. And of course, even with desktop browsers, servers and other resource-plentiful environments, streamlined code carries many benefits (faster to transfer, faster to execute, less code surface to test, more confidence in its security/reliability). 

But often you will find yourself wanting extended functionality - particularly when writing code that is heavily asynchronous in nature (which generally should be the case for anything non-trivial). So I wrote a companion library: Zousan Plus.

Mostly what I have found myself wanting, is something that aids in dealing with standard patterns and workflows that involve promises. Such as mapping:

Zousan.map

Take mapping for example. JavaScript coders often find themselves transforming an array via a function, as the Array.map function conveniently offers:

var myArray = [ 1, 2, 3, 4, 5, 6, 7 ]
var squares = myArray.map(function(value) {
  return value * value })

Here, we generate an array of squares of the first 7 integers by mapping those integers over a function that squares them.

But what if the function we wish to map over the array returns a promise of a value rather than the value itself? The traditional Array.map could still be used, but would differ from what you might expect in the following three ways:

  1. The functions would all be run simultaneously, rather than one after the other.
  2. The resulting array would be full of promises, not results.
  3. Upon completion of the map operation, the results would not (necessarily) be ready for use.

The first point is quite probably a non-issue, since "philosophically" a mapping function should not depend on being run serially - but it is important to understand this distinction. 

The other two points can be easily handled using the Promise.all function available in most Promise implementations (including Zousan):

var myArray = [1, 2, 3, 4, 5, 6, 7]
var squares = Zousan.all(myArray.map(squareAsync))

What if other aspects of this pattern also involved promises? What if the original array itself was actually a promise of an array - and/or if one or more of the items within the array were promises?

All these cases are handled with the Zousan.map function provided by Zousan-plus:

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

What Else?

I have added several other utility functions to Zousan-Plus:

namedAll

Similar to the standard Promise.all except each promise in the list is identified with a name and the eventual object that is returned has name/value pairs for each promise rather than an array.

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()
		})

promisify

This is useful for taking a traditional callback-based module and turning each member function into promises. Node users know what I'm talking about:

const fs = Zousan.promisify(require("fs"))

fs.readFile("path-to-file.txt", { encoding: "utf-8" })
	.then(function(contents) {
			// you have the file contents here!
		})

series

Simple syntax sugar for expressing a list of functions to call in a series. This is expresses a function composition with each function being passed the eventual result of the previous promise in the series.

function getUserAlbumCovers(userId)
{
	return Zousan.series(
		userId,
		getUserObj,
		prop("albumList"), // extracts "albumList" property
		getAlbumsByIDList, 
		pluck("id"), // extracts list of "id" from list of obj
		getAlbumCoversByIDList)
}

tSeries

This is similar to the series function above, but the values resolved at each step in the series are tracked and available when the full series resolves:

function add6(x) { return x + 6 }
function add3Later(x) { // Return the specified value plus 3 after 100ms
/* ... */ }

var ts = tSeries(1,add6,add3Later) // ts has the "res" and "prom" properties
ts.prom.then(function(final) {
		// ts.res[0] = 1
		// ts.res[1] = 9
		// ts.res[2] = 12
		// final = 12
	})

Now What?

This library is sure to grow with more useful function for dealing effectively with asynchronicity. These are all contained in a single module, but each is independent (except series depends on tSeries) so they can be copies and pasted if you just want a single function.

These are written against Zousan - but will work with any Promise A+ compliant promise implementation, such as the native Promise contained in evergreen browsers. Simply change any instance of Zousan to Promise (or assign Zousan = Promise somewhere above it).

Contributions are certainly welcome


If you or your organization could use a somewhat fanatical developer on your current or next project, lets talk. I am passionate about the user experience, easy to work with, and I get stuff done. Lets build something great!


Zousan Plus - When a Promise is Not Enough Tweet This!
Glenn is the founder of bluejava K.K., and has been providing consulting services and developing web/mobile applications for over 15 years. Glenn lives with his wife and two children in Fujisawa, Japan.
Comments

No comments yet. You can get the conversation going!

Add Comment
Displayed alongside comment
Optional. Not displayed, but used for avatar
Optional
Don't enter anything here: