ptg 14.3 Domain Model and Storage 363 The test exposes our cheat, so we need to find a better way to generate ids. Listing 14.39 uses a simple variable that is incremented each time a message is added. Listing 14.39 Assigning unique integer ids var id = 0; var chatRoom = { addMessage: function (user, message, callback) { /* */ if (!err) { data = { id: id++, user: user, message: message }; } /* */ } }; Tests are passing again. You might worry that we’re not actually storing the message anywhere. That is a problem, but it’s not currently being addressed by the test case. To do so we must start testing message retrieval. 14.3.4 Fetching Messages In the next chapter we will interface with the chat backend using the comet- Client from Chapter 13, Streaming Data with Ajax and Comet. This means that chatRoom needs some way to retrieve all messages since some token. We’ll add a getMessagesSince method that accepts an id and yields an array of messages to the callback. 14.3.4.1 The getMessagesSince Method The initial test for this method in Listing 14.40 adds two messages, then tries to retrieve all messages since the id of the first. This way we don’t program any as- sumptions about how the ids are generated into the tests. Listing 14.40 Testing message retrieval testCase(exports, "chatRoom.getMessagesSince", { "should get messages since given id": function (test) { var room = Object.create(chatRoom); var user = "cjno"; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 364 Server-Side JavaScript with Node.js room.addMessage(user, "msg", function (e, first) { room.addMessage(user, "msg2", function (e, second) { room.getMessagesSince(first.id, function (e, msgs) { test.isArray(msgs); test.same(msgs, [second]); test.done(); }); }); }); } }); The test fails in the face of a missing getMessagesSince. Listing 14.41 adds an empty method that simply calls the callback without arguments. Listing 14.41 Adding getMessagesSince var chatRoom = { addMessage: function (user, message, callback) { /* */ }, getMessagesSince: function (id, callback) { callback(); } }; Because addMessage isn’t really storing the messages anywhere, there’s no way for getMessagesSince to retrieve it. In other words, to pass this test we need to fix addMessage, like Listing 14.42 shows. Listing 14.42 Actually adding messages addMessage: function (user, message, callback) { /* */ if (!err) { if (!this.messages) { this.messages = []; } var id = this.messages.length + 1; data = { id: id, user: user, message: message }; this.messages.push(data); } /* */ } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.3 Domain Model and Storage 365 Now that we have an array to store messages in, we can retrieve ids from the array’s length instead of keeping a dedicated counter around. The id adds one to the length to make it 1-based rather than 0-based. The reason for this is that getMessagesSince is supposed to retrieve all messages added after some id. Using 0-based ids we’d have to call this method with -1 to get all messages, rather than the slightly more natural looking 0. It’s just a matter of preference, you may disagree with me. Running the tests confirms that all the previous tests are still passing. As ids are now directly related to the length of the messages array, retrieval is trivial as Listing 14.43 shows. Listing 14.43 Fetching messages getMessagesSince: function (id, callback) { callback(null, this.messages.slice(id)); } And just like that, all the tests, including the one test for getMessagesSince, pass. getMessagesSince helped us properly implement addMessage, and the best case situation is now covered. However, there are a few more cases to fix for it to work reliably. • It should yield an empty array if the messages array does not exist. • It should yield an empty array if no relevant messages exist. • It could possibly not throw exceptions if no callback is provided. • The test cases for addMessage and getMessagesSince should be refactored to share setup methods. Testing and implementing these additional cases is left as an exercise. 14.3.4.2 Making addMessage Asynchronous The addMessage method, although callback-based, is still a synchronous inter- face. This is not necessarily a problem, but there is a possibility that someone using the interface spins off some heavy lifting in the callback, inadvertently caus- ing addMessage to block. To alleviate the problem we can utilize Node’s pro- cess.nextTick(callback) method, which calls its callback on the next pass of the event loop. First, Listing 14.44 tests for the desired behavior. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 366 Server-Side JavaScript with Node.js Listing 14.44 Expecting addMessage to be asynchronous "should be asynchronous": function (test) { var id; this.room.addMessage("cjno", "Hey", function (err, msg) { id = msg.id; }); this.room.getMessagesSince(id - 1, function (err, msgs) { test.equals(msgs.length, 0); test.done(); }); } This test fails because the method indeed is synchronous at this point. Listing 14.45 updates addMessage to utilize the nextTick method. Listing 14.45 Making addMessage asynchronous require("function-bind"); var id = 0; var chatRoom = { addMessage: function (user, message, callback) { process.nextTick(function () { /* */ }.bind(this)); }, /* */ } The test now passes. However, it only passes because getMessagesSince is still synchronous. The moment we make this method asynchronous as well (as we should), the test will not pass. That leaves us with checking the messages array directly. Testing implementation details is usually frowned upon, as it ties the tests too hard to the implementation. I think the test for the asynchronous behavior falls under the same category; thus, I’d rather remove that test than to add yet another one that digs inside the implementation. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.4 Promises 367 14.4 Promises One of the biggest challenges of working exclusively with asynchronous interfaces lies in deeply nested callbacks; any task that requires the result of asynchronous calls to be processed in order must be nested to ensure ordered execution. Not only is deeply nested code ugly and cumbersome to work with, it also presents a more grave problem; nested calls cannot benefit from the possibility of parallel execution, a bad trade-off to enable ordered processing. We can untangle nested callbacks using promises. A promise is a representation of an eventual value and it offers an elegant way of working with asynchronous code. When an asynchronous method uses promises, it does not accept a callback, but rather returns a promise, an object representing the eventual fulfillment of that call. The returned object is observable, allowing calling code to subscribe to success and error events on it. When the original call that spawned the promise finishes, it calls the promise’s resolve method, which causes its success callback to fire. Similarly, in the event that a call failed, the promise offers the reject method, which can be passed an exception. Using promises means that we don’t have to nest callbacks unless we truly depend on calls to occur in succession; thus, we gain more flexibility. For example, we can issue a host of asynchronous calls and have them execute in parallel, but use promises to group and process the results in any order we wish. Node no longer comes with a promise API, but Kris Zyp has a nice imple- mentation 4 that implements his proposal for a CommonJS Promise specification. The version used in this book is available from the book’s website. 5 Download it to deps/node-promise. 14.4.1 Refactoring addMessage to Use Promises We will refactor the addMessage method to use promises. As we refactor, it is vital that we run the tests between each step, and always keep them passing, to be sure we didn’t break anything. Changing the way a method works can be done by keeping the old behavior until the new behavior is in place and all tests have been updated. The fact that we can carry out a refactoring like this—changing fundamen- tal behavior—without worrying about breaking the application, is one of the true benefits of a good test suite. 4. http://github.com/kriszyp/node-promise 5. http://tddjs.com From the Library of WoweBook.Com Download from www.eBookTM.com ptg 368 Server-Side JavaScript with Node.js 14.4.1.1 Returning a Promise We will start refactoring by introducing a new test, one that expects addMessage to return a promise object, seen in Listing 14.46. Listing 14.46 Expecting addMessage to return a promise testCase(exports, "chatRoom.addMessage", { /* */ "should return a promise": function (test) { var result = this.room.addMessage("cjno", "message"); test.isObject(result); test.isFunction(result.then); test.done(); } }); Notice that I assume you’ve solved the exercise from before; the test case should now be using a setup method to create a chatRoom object, available in this.room. The test fails as the method is currently not returning an object. We’ll fix that by returning an empty promise object, as in Listing 14.47. Listing 14.47 Returning an empty promise object require("function-bind"); var Promise = require("node-promise/promise").Promise; var id = 0; var chatRoom = { addMessage: function (user, message, callback) { process.nextTick(function () { /* */ }.bind(this)); return new Promise(); }, /* */ }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.4 Promises 369 14.4.1.2 Rejecting the Promise Next up, we’ll start changing the original tests to work with promises. The first test we wrote expects addMessage to call the callback, passing an error if no username is passed to it. The updated test can be seen in Listing 14.48. Listing 14.48 Using the returned promise "should require username": function (test) { var promise = this.room.addMessage(null, "message"); promise.then(function () {}, function (err) { test.isNotNull(err); test.inherits(err, TypeError); test.done(); }); } The promise has a then method, which allows consumers to add callbacks to be called when it is fulfilled. It accepts one or two functions; the first function is the success callback and the second is the error callback. Another way of doing this is to use the addCallback and addErrback methods, but I like the way “then” reads: addMessage(user, msg).then(callback). To pass this test, we need to duplicate some efforts in addMessage, as we’re not yet ready to drop the old implementation. Listing 14.49 shows the updated method. Listing 14.49 Updating addMessage addMessage: function (user, message, callback) { var promise = new Promise(); process.nextTick(function () { /* */ if (err) { promise.reject(err, true); } }.bind(this)); return promise; } Here we call the promise’s reject method, passing it an error. Normally, the promise will throw an exception if reject is called and no error handler is From the Library of WoweBook.Com Download from www.eBookTM.com ptg 370 Server-Side JavaScript with Node.js registered. Becausethe remaining tests havenot yet been updatedto use the promise, and because we previously decided that not handling the error was permissible, we pass in true as the second argument to suppress this behavior. The test passes. The next test is similar to the one we just fixed, only it verifies that leaving out the message causes an error. Passing this test using a promise does not require further modification of addMessage, so I will leave updating the test as an exercise. 14.4.1.3 Resolving the Promise The next significant test to update is the one that asserts that the newly added message object is passed to the callback. This test only requires a small change. Because the promise has separate success and failure handlers, we can remove the error parameter to the callback. The test can be seen in Listing 14.50. Listing 14.50 Expecting the promise to emit success "should call callback with new object": function (test) { var txt = "Some message"; this.room.addMessage("cjno", txt).then(function (msg) { test.isObject(msg); test.isNumber(msg.id); test.equals(msg.message, txt); test.equals(msg.user, "cjno"); test.done(); }); } Updating the implementation is a matter of calling the promise’s resolve method, as seen in Listing 14.51. Listing 14.51 Resolving with the message addMessage: function (user, message, callback) { var promise = new Promise() process.nextTick(function () { /* */ if (!err) { /* */ this.messages.push(data); promise.resolve(data); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.4 Promises 371 /* */ }.bind(this)); return promise; } Yet another converted test passes. Converting the remaining tests should be fairly straightforward, so I will leave doing so as an exercise. Once all the tests have been updated, we need to decide whether or not we should remove the callback. Keeping it will allow users to decide which pattern they prefer to use, but it also means more code to maintain on our part. Because the promise handles all the callbacks for us, removing the manual callback means we don’t need to concern ourselves with whether or not it was passed, if it’s callable, and so on. I recommend relying solely on the promises. 14.4.2 Consuming Promises Now that the addMessage method uses promises we can simplify code that needs to add more than one message. For instance, the test that asserts that each message is given its own unique id originally used nested callbacks to add two messages and then compare them. Node-promise offers an all function, which accepts any number of promises and returns a new promise. This new promise emits success once all the promises are fulfilled. We can use this to write the unique id test in another way, as seen in Listing 14.52. Listing 14.52 Grouping promises with all /* */ var all = require("node-promise/promise").all; /* */ testCase(exports, "chatRoom.addMessage", { /* */ "should assign unique ids to messages": function (test) { var room = this.room; var messages = []; var collect = function (msg) { messages.push(msg); }; var add = all(room.addMessage("u", "a").then(collect), room.addMessage("u", "b").then(collect)); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 372 Server-Side JavaScript with Node.js add.then(function () { test.notEquals(messages[0].id, messages[1].id); test.done(); }); }, /* */ }); For consistency, the getMessagesSince method should be updated to use promises as well. I will leave doing so as yet another exercise. Try to make sure you never fail more than one test at a time while refactoring. When you’re done you should end up with something like Listing 14.53. Listing 14.53 getMessagesSince using promises getMessagesSince: function (id) { var promise = new Promise(); process.nextTick(function () { promise.resolve((this.messages || []).slice(id)); }.bind(this)); return promise; } 14.5 Event Emitters When the client polls the server for new messages, one of two things can happen. Either new messages are available, in which case the request is responded to and ended immediately, or the server should hold the request until messages are ready. So far we’ve covered the first case, but the second case, the one that enables long polling, is not yet covered. chatRoom will provide a waitForMessagesSince method, which works just like the getMessagesSince method; except if no messages are available, it will idly wait for some to become available. In order to implement this, we need chatRoom to emit an event when new messages are added. 14.5.1 Making chatRoom an Event Emitter The first test to verify that chatRoom is an event emitter is to test that it has the addListener and emit methods, as Listing 14.54 shows. From the Library of WoweBook.Com Download from www.eBookTM.com . it 1-based rather than 0-based. The reason for this is that getMessagesSince is supposed to retrieve all messages added after some id. Using 0-based ids we’d have to call this method with -1 to. 14 .40 adds two messages, then tries to retrieve all messages since the id of the first. This way we don’t program any as- sumptions about how the ids are generated into the tests. Listing 14 .40. "cjno"; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 364 Server-Side JavaScript with Node.js room.addMessage(user, "msg", function (e, first) { room.addMessage(user,