-
Notifications
You must be signed in to change notification settings - Fork 3
Promises
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.
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.
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.
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.
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;
});
});
}
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!