Sunday, June 1, 2014

Asychronous NodeJs

I have been doing a lot of work with Cordova lately and struggled a little to get my head around some of the event driven concepts of NodeJs, Promises, and Callbacks. This became apparent when trying to test my NodeJs with Jasmine. I had written a method much like this one:
exports.doAsync = function(my_str) {
  fs.exists(fileName, function() {
    // fileName, callback declared elsewhere
    fs.writeFile(fileName, my_str, callback);
  });
}
Even though I knew somewhere deep down in my heart that Callbacks meant asychronous I still wrote my test like so:
it("should be an async method", function () {
  //setup
  doAsync(param1);
  expect(fs.writeFile).toBeCalled();
});
This test failed everytime because I had no mechanism in place to tell test code when that first callback had been executed. You'll notice my first mistake was writing the function prototype first, which did not include a callback parameter. This anti-BDD stunt influenced my presuppositions when writing the test. To make it work correctly, I needed the following changes:
// In my file.js
exports.doAsync = function(my_str, win) {
  fs.exists(fileName, function() {
    // fileName, callback declared elsewhere
    fs.writeFile(fileName, my_str, callback);
  });
  function callback(err) {
    win(err);
  }
}
// in file.spec.js
it("should be an async method", function (done) {
  //setup
  doAsync(param1, function() {
    expect(fs.writeFile).toBeCalled();
    done();
  });
});
This is a bit contrived because to make this work you'd need a bit more setup. (You can view the source from which this is derived in the file around here). However, in this post I simply want to share a mental tool that has helped me grasp these ideas and detect my conceptual errors quicker.
I worked in a restaurant for a few years and so I imagine our single-threaded Javascript engine as a kitchen with only one cook. The cook, Long John Silver, actually has to do many things quickly to handle all the orders his waiters are taking on the floor. If we just opened and I only have one table whose ordered our famous Eggs and Salsa, I can put in the order and go have a chat with Long John while the eggs cook. When the dish is done, I could end the chat and deliver them to the customer and everyone tips 20%, even though this has been synchronous and inefficient. Obviously, when we're at the peak dinner hours everyone works asynchronously. John will do many other things while waiting for the eggs to cook.
In my test case above, I was essentially saying, "Long John, can you make me Eggs and Salsa?" He gruffly responds, "Sure" and because he's forgotten the Salsa on previous eggs, I immediately begin to ask, "Did they come with Salsa?" (past tense). He's just gotten the eggs in the pan so it hardly makes sense to ask a past-tense question about a future state of the eggs. So then, I either have to check back every five minutes asking the past tense question expect(fs.writeFile).toBeCalled() or I can simply have him text me when the meal is ready. Using the Callback or the Promise is like saying, "Just text me when your done."
This analogy works in a few other respects. If you want to test the process of how he makes eggs, you can give him fake veggies so he doesn't have to go search for them in the fridge (module dependencies or data). Either way, you can only check his work when he's finished with it.
Just remember, one cook, one thread, so you'll need to be making sure you have ways for your cook to notify you when he's done. Otherwise everyone waits in line.