I’ve recently open-sourced hubot-mood, a hubot script to store a team’s mood and get some metrics about it. We’re using it at Scopyleft.
Moods are stored in redis through the node-redis library, which uses asynchronous calls to perform operations on the redis backend.
So typically, to store an entry, you do something like the following:
function store(mood, cb) {
redis.rpush("moods", mood, function(err) {
cb(err, mood);
});
}
store("2013-02-01:n1k0:sunny", function(err, mood) {
if (err) throw err;
console.log("stored mood entry: " + mood);
});
Classic. But what if you want to perform multiple insertions, eg. to load a bunch of fixtures for your tests? I’m using mocha here:
describe("moods test", function() {
// fixtures
var moods = [
"2013-02-01:n1k0:sunny"
, "2013-02-02:n1k0:cloudy"
, "2013-02-03:n1k0:stormy"
, "2013-02-04:n1k0:rainy"
// … we could add many more
];
it("should do something useful with moods", function(done) {
store(moods[0], function(err, mood) {
assert.ifError(err);
store(moods[1], function(err, mood) {
assert.ifError(err);
store(moods[2], function(err, mood) {
assert.ifError(err);
store(moods[3], function(err, mood) {
assert.ifError(err);
// now let's test stuff with stored moods
done();
});
});
});
});
});
});
Here we go again, callback hell and unmanageable pyramids.
Async.js to the rescue
Async.js is a node library to help dealing with asynchronicity and flatten pyramids. A npm install async
later, we’re ready to go:
describe("moods tests", function() {
var moods = [
"2013-02-01:n1k0:sunny"
, "2013-02-02:n1k0:cloudy"
, "2013-02-03:n1k0:stormy"
, "2013-02-04:n1k0:rainy"
// … we could add many more
];
it("should do something useful with moods", function(done) {
async.parallel([
function(cb) {
store(mood[0], function(err, mood) {
cb(err, mood);
});
},
function(cb) {
store(mood[1], function(err, mood) {
cb(err, mood);
});
},
function(cb) {
store(mood[2], function(err, mood) {
cb(err, mood);
});
},
function(cb) {
store(mood[3], function(err, mood) {
cb(err, mood);
});
},
], function(err, moods) {
assert.ifError(err);
// now let's test stuff with stored moods
done();
});
});
});
Wait a minute, it’s not “better” at all!
Indeed, this is definitely not DRY code. But one has to be creative to turn a tool into an efficient solution; let’s invoke the powers of Array#map
to build the required callback functions out of our moods
array:
function load(fixtures, onComplete) {
async.parallel(fixtures.map(function(fixture) {
return function(cb) {
store(fixture, function(err, result) {
cb(err, result);
});
};
}), onComplete);
}
describe("moods tests", function() {
var moods = [
"2013-02-01:n1k0:sunny"
, "2013-02-02:n1k0:cloudy"
, "2013-02-03:n1k0:stormy"
, "2013-02-04:n1k0:rainy"
// … we could add many more
];
it("should do something useful with moods", function(done) {
load(moods, function(err, storedMoods) {
assert.ifError(err);
// now let's test stuff with stored moods
done();
});
});
});
Edit: there’s even a built-in async.map()
function, not sure how I missed it; so the code is even shorter:
describe("moods tests", function() {
var moods = [
"2013-02-01:n1k0:sunny"
, "2013-02-02:n1k0:cloudy"
, "2013-02-03:n1k0:stormy"
, "2013-02-04:n1k0:rainy"
// … we could add many more
];
it("should do something useful with moods", function(done) {
async.map(moods, store, function(err, storedMoods) {
assert.ifError(err);
// now let's test stuff with stored moods
done();
});
});
});
Async.js is a great package and one of the most popular of the node ecosystem, but there are many others.
Such a library combined with a functional approach provides a killer combo to solve your daily problems when programming JavaScript.