Skip to content
chris of the clan edited this page Aug 24, 2015 · 1 revision

Promise what?

ClanOfTheCloud makes heavy use of promises. They're used to replace callbacks which are common in Javascriptbut cumbersome.

You've probably seen syntax where a callback function is passed as an argument: myAsyncFunction(params, function(err, result) { //..// }}.

In this case, myAsyncFunction has no return value. Instead of returning a value, it will pass it as a parameter to the callback.

This works great but it's not very readable and it makes error handling difficult (you must check for errors in every callback). Also, it get ugly when you want to compose many functions.

Promises are a solution to this problem.

First of all, a Promise is a value returned by functions. Not the value you'd expect from the function, but a promise for this value. This promise is an object, which let's you attach a callback to be called when the actual value is available, and another callback to be called if there's an error.

Show me some code

if myAsyncFunction now returns a promise, you'll call it with: var myPromise = myAsyncFunction(params); which looks better. myPromise is the promise objet. You can attach a callback to it with myPromise.then(function(result) {//..//});. This callback will be called when the value is actually available.

You can also attach an error handler with myPromise.catch(function(error) {//..//}); and the callback will be called when an error occurs.

Easy? Not so fast.

It gets better when you're writing real code :

var promise = myFirstFunction(params).then(function(firstResult) {
  
  // let's use this firstResult var to call another async function which returns a promise
  return mySecondFunction(firstResult);
});

This example needs more explaining, but we'll start with a question: what happens to the return value of the .then callback?

The return value of the .then callback is wrapped in a promise (if not already a promise), which is returned to the caller.

So promise is now a chain of promises: first myFirstFunction will run, then mySecondFunction will run, and the returned promise bubbles up into the promise variable.

This allows the composition of asynchronous method calls.

Of course, we could also handle errors in either function call with promise.catch(function(error) {//..//});. In this case, a single error handler is enough, when callbacks would have required 2 error handlers.

There's more

Depending on what my code is doing, I can add more .then to promises, because .then returns a promise.

myFirstFunction(params).then(function(firstResult) {
  
  // let's use this firstResult var to call another async function which returns a promise
  return mySecondFunction(firstResult);
}).then(console.log);

Easy way to output the result of the chain to stdout.

We're using a promises library named bluebird. You can find it here: https://github.com/petkaantonov/bluebird

You'll find even more examples and ways to use promises below.

Back to batch

When writing batches, you'll realize that every CotC API (this.*) returns a promise, and your batch must return it's result as a promise too.

function __helloworld(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
  mod.debug(JSON.stringify(params));
  return { "message" : params.request.text + " world!" }
}

Even in such a simple batch, you must remember the return value will be wrapped in a promise automatically if it's not already a promise.

So when you write a batch like:

function __myGet(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
	
	return this.gamevfs.read(this.game.getPrivateDomain(), "keyA");
}

Your batch is really returning a promise, because this.gamevfs.read is returning a promise.

If you want to make two reads instead of one, you'll use then:

function __myDoubleGet(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
	
	return this.gamevfs.read(this.game.getPrivateDomain(), "keyA").then( function(result1) {
	    return this.gamevfs.read(this.game.getPrivateDomain(), "keyB").then ( function (result2) {
	        return {resA: result1, resB: result2};
	    });
	});
}

This batch reads two keys, one after the other, and returns a promise for an object with both values. Note that in this code, it looks like the batch is really returning {resA: result1, resB: result2} when in fact is just a chain of promises that is returned... Because it's the last statement in the promise chain, it can be considered "the return value", wrapped as a promise (that's a nice benefit of promises).

Now let's say the first key will give me the name of the second key I should read :

function __cascadeRead(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
	
	return this.gamevfs.read(this.game.getPrivateDomain(), "keyName").then( function(keyName) {
	    return this.gamevfs.read(this.game.getPrivateDomain(), keyName)
	});
}

This batch is reading the keyName key to determine which key it should read next, and returns the contents of that key.

And now let's change things a little bit:

function __cascadeRead(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
	
	return this.gamevfs.read(this.game.getPrivateDomain(), "keyName").then( function(keyName) {
	    return this.gamevfs.read(this.game.getPrivateDomain(), keyName)
	}).then( function(value) { 
	    mod.debug(value);
	    return value;
	});
}

We've added a .then to the outermost promise, to the whole chain actually... This means .then can be located at any level in the call tree (where it's appropriate). It works the same as this code, where the .then is on the insidemost promise:

function __cascadeRead(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
	
	return this.gamevfs.read(this.game.getPrivateDomain(), "keyName").then( function(keyName) {
	    return this.gamevfs.read(this.game.getPrivateDomain(), keyName).then( function(value) { 
            mod.debug(value);
            return value;
        });
	});
}

Getting real work done

By now, it should feel quite complex to use promises... which is normal: the learning curve is quite steep, because it requires different thinking.

Let's spice up things a bit with loops. Say I want to read n keys, depending on params.

You must remember that batch must return a promise. There are many ways to iterate with promises. The first one is to build a chain of promises in a loop.

function __multiRead(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
	
	var n = params.n; // how many keys to read
	
	var promise = this.gamevfs.read(this.game.getPrivateDomain(), "key0");
	for (var i=1; i<n; i++) {
	    promise = promise.then(function() {
	        this.gamevfs.read(this.game.getPrivateDomain(), "key"+i);
	    });
	}
    return promise;
}

In this version of readMulti, we're chaining reads for key[0..n] by hand. The return value, promise, will contain the return value of the last promise in the chain, that is the value of key[n]... Probably not what we wanted. This approach could be useful for serial iteration though.

Another way to iterate :

function __multiRead(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
	
	var n = params.n; // how many keys to read
	
	var promises = [];
	for (var i=0; i<n; i++) {
	    promises.push(this.gamevfs.read(this.game.getPrivateDomain(), "key"+i));
    return mod.Q.all(promises);
}

In this batch, we're storing many promises in an array, but because we want a promise for an array and not an array of promises, we're using mod.Q.all(array) which will convert an array of promises into a promise for an array of values.

This is exactly what we wanted!

You may wonder how it's working... Promises "start running" as soon as they're created. So calling this.gamevfs.read starts reading. In our batch, we start n reads synchronously, which will all run concurrently, in parallel. We save every promise in an array.

And mod.Q.all(array) does its magic to wait for all promises to get their values, then returns a promise for an array with our values.

Because mod.Q.all(<array>) returns a promise, we could use these values in our batch instead of returning it...

function __multiRead(params, customData, mod) {
	"use strict;"
	// don't edit above this line // must be on line 3
	
	var n = params.n; // how many keys to read
	
	var promises = [];
	for (var i=0; i<n; i++) {
	    promises.push(this.gamevfs.read(this.game.getPrivateDomain(), "key"+i));
    var promiseForArray = mod.Q.all(promises);
    
    // say our keys contain JSON objects {value: int}
    var promiseForSum = promiseForArray.then(function(array) {
        return array.reduce(function(previous, current) {
            current = JSON.parse(current);
            return previous+current.value;
        }, 0);
    });
    return promiseForSum;
}

Now our batch will read all these keys, where each key holds an object {value:int} and when all the keys are resolved, we compute the sum of all values. (yes, I know, stupid example). This shows how promise chains must always grow longer if you need "more".

If you've read that far, I think you can now read blubird's API documentation (https://github.com/petkaantonov/bluebird/blob/master/API.md). Bluebird is richer that shown here... and learning it will make your code more efficient, and bug free!

Clone this wiki locally