Now that we’ve designed the chat object API we can build it. As in chapter 5, we’re going to use the Fake module and the JavaScript console to avoid the use of a web server or a UI. As we progress, we should keep in mind that the “backend” is emulated by the Fake module in this chapter.
6.2.1 Start the chat object with the join method
In this section we’ll create the chat object in the Model so that we may:
■ Sign in using the spa.model.people.login(<username>) method.
■ Join the chat room using the spa.model.chat.join() method.
■ Register a callback to publish an spa-listchange event whenever the Model receives a listchange message from the backend. This indicates the list of users has changed.
Our chat object will rely on the people object to handle the sign-in and to maintain the list of online people. It won’t allow an anonymous user to join a chat room. Let’s start building the chat object in the Model as shown in listing 6.2. Changes are shown in bold:
spa.model = (function () { ...
stateMap = { ...
is_connected : false, ...
}, ...
personProto, makeCid, clearPeopleDb, completeLogin, makePerson, removePerson, people, chat, initModule;
...
// The chat object API // ---
// The chat object is available at spa.model.chat.
// The chat object provides methods and events to manage // chat messaging. Its public methods include:
// * join() - joins the chat room. This routine sets up // the chat protocol with the backend including publishers // for 'spa-listchange' and 'spa-updatechat' global
Listing 6.2 Start our chat object—spa/js/spa.model.js
Create the
stateMap.is_connected flag to indicate if the user is currently in the chat room.
// custom events. If the current user is anonymous, // join() aborts and returns false.
// ...
//
// jQuery global custom events published by the object include:
// ...
// * spa-listchange - This is published when the list of // online people changes in length (i.e. when a person // joins or leaves a chat) or when their contents change // (i.e. when a person's avatar details change).
// A subscriber to this event should get the people_db // from the people model for the updated data.
// ...
//
chat = (function () { var
_publish_listchange,
_update_list, _leave_chat, join_chat;
// Begin internal methods
_update_list = function( arg_list ) { var i, person_map, make_person_map,
people_list = arg_list[ 0 ];
clearPeopleDb();
PERSON:
for ( i = 0; i < people_list.length; i++ ) { person_map = people_list[ i ];
if ( ! person_map.name ) { continue PERSON; }
// if user defined, update css_map and skip remainder if ( stateMap.user && stateMap.user.id === person_map._id ) {
stateMap.user.css_map = person_map.css_map;
continue PERSON;
}
make_person_map = {
cid : person_map._id, css_map : person_map.css_map, id : person_map._id, name : person_map.name };
makePerson( make_person_map );
}
stateMap.people_db.sort( 'name' );
};
_publish_listchange = function ( arg_list ) { _update_list( arg_list );
$.gevent.publish( 'spa-listchange', [ arg_list ] );
};
// End internal methods _leave_chat = function () {
var sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
Create a chat namespace.
Create the _update_list method to refresh the people object when a new people list is received.
Create the _publish _listchange method to publish an spa- listchange global jQuery event with an updated people list as its data. We expect to use this method whenever a listchange message is received from the backend.
Create the _leave_chat method, which sends a leavechat message to the backend and cleans up state variables.
185 Build the chat object
stateMap.is_connected = false;
if ( sio ) { sio.emit( 'leavechat' ); } };
join_chat = function () { var sio;
if ( stateMap.is_connected ) { return false; } if ( stateMap.user.get_is_anon() ) {
console.warn( 'User must be defined before joining chat');
return false;
}
sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
sio.on( 'listchange', _publish_listchange );
stateMap.is_connected = true;
return true;
};
return {
_leave : _leave_chat, join : join_chat };
}());
initModule = function () { // initialize anonymous person stateMap.anon_user = makePerson({
cid : configMap.anon_id, id : configMap.anon_id, name : 'anonymous' });
stateMap.user = stateMap.anon_user;
};
return {
initModule : initModule, chat : chat, people : people };
}());
This is our first pass implementation of the chat object. Instead of adding more meth- ods, we want to test the ones we’ve created so far. In the next section we’ll update the Fake module to emulate the server interaction we need for testing.
6.2.2 Update Fake to respond to chat.join
Now we need to update the Fake module so it can emulate the server responses we need to test the join method. The changes we need include:
■ Include the signed-in user in the mock people list.
■ Emulate the receipt of a listchange message from the server.
Create the join_chat method so we may join the chat room. This checks if the user has already joined the chat (stateMap.is _connected) so that it doesn’t register the listchange callback more than once.
Neatly export all public chat methods.
Remove the code that inserts the mock people list into the people object, as this is now provided when the user joins the chat.
Add chat as a public object.
The first step is simple: we create a person map and push it into the people list that Fake manages. The second step is trickier, so stick with me here: the chat object regis- ters a handler for a listchange message from the backend only after the user has signed in and joined a chat. Therefore, we can add a private send_listchange function that will send a mock people list only once this handler is registered. Let’s employ these changes as shown in listing 6.3. Changes are shown in bold:
...
spa.fake = (function () { 'use strict';
var peopleList, fakeIdSerial, makeFakeId, mockSio;
fakeIdSerial = 5;
makeFakeId = function () {
return 'id_' + String( fakeIdSerial++ );
};
peopleList = [
{ name : 'Betty', _id : 'id_01', css_map : { top: 20, left: 20,
'background-color' : 'rgb( 128, 128, 128)' }
},
{ name : 'Mike', _id : 'id_02', css_map : { top: 60, left: 20,
'background-color' : 'rgb( 128, 255, 128)' }
},
{ name : 'Pebbles', _id : 'id_03', css_map : { top: 100, left: 20,
'background-color' : 'rgb( 128, 192, 192)' }
},
{ name : 'Wilma', _id : 'id_04', css_map : { top: 140, left: 20,
'background-color' : 'rgb( 192, 128, 128)' }
} ];
mockSio = (function () { var
on_sio, emit_sio,
send_listchange, listchange_idto, callback_map = {};
on_sio = function ( msg_type, callback ) { callback_map[ msg_type ] = callback;
};
emit_sio = function ( msg_type, data ) { var person_map;
Listing 6.3 Update Fake to simulate join server messages—spa/js/spa.fake.js
Create peopleList to store the mock people list as an array of maps.
187 Build the chat object
// Respond to 'adduser' event with 'userupdate' // callback after a 3s delay.
if ( msg_type === 'adduser' && callback_map.userupdate ) { setTimeout( function () {
person_map = {
_id : makeFakeId(), name : data.name, css_map : data.css_map };
peopleList.push( person_map );
callback_map.userupdate([ person_map ]);
}, 3000 );
} };
// Try once per second to use listchange callback.
// Stop trying after first success.
send_listchange = function () {
listchange_idto = setTimeout( function () { if ( callback_map.listchange ) {
callback_map.listchange([ peopleList ]);
listchange_idto = undefined;
}
else { send_listchange(); } }, 1000 );
};
// We have to start the process ...
send_listchange();
return { emit : emit_sio, on : on_sio };
}());
return { mockSio : mockSio };
}());
Now that we’ve completed part of the chat object, let’s test it as we did with the peo- ple object in chapter 5.
6.2.3 Test the chat.join method
Before we continue building our chat object, we should ensure the capabilities we have implemented so far work as expected. First let’s load our browser document (spa/spa.html), open the JavaScript console, and ensure that the SPA shows no JavaScript errors. Then, using the console, we may test our methods as shown in list- ing 6.4. Typed input is shown in bold; output is shown in italics:
// create a jQuery collection var $t = $('<div/>');
// Have $t subscribe to global custom events with test functions Listing 6.4 Test spa.model.chat.join() without a UI or server
Revise the response to an adduser message (which occurs when the user signs in) to push the user definition into the mock people list.
Add a send_listchange function that emulates the receipt of a listchange message from the backend. Once per second, this method looks for the listchange callback (which the chat object registers only after a user has signed in and joined the chat room). If the callback is found, it is executed using the mock peopleList as its argument, and send_listchange stops polling.
Add this line to start the send_listchange function.
Remove the getPeopleList method since the desired data is now provided by the listchange handler.
Create a jQuery collection ($t) that isn’t attached to the browser document.
We’ll use this for event testing.
Have the $t jQuery collection subscribe to the spa-login event with a function that prints “Hello!” and the list of arguments to the console.
$.gevent.subscribe( $t, 'spa-login', function () { console.log( 'Hello!', arguments ); });
$.gevent.subscribe( $t, 'spa-listchange', function () { console.log( '*Listchange', arguments ); });
// get the current user object
var currentUser = spa.model.people.get_user();
// confirm this is not yet signed-in currentUser.get_is_anon();
>> true
// try to join chat without being signed-in spa.model.chat.join();
>> User must be defined before joining chat // sign-in, wait 3s. The UI updates too!
spa.model.people.login( 'Fred' );
>> Hello! > [jQuery.Event, Object]
// get the people collection
var peopleDb = spa.model.people.get_db();
// show the names of all people in the collection.
peopleDb().each(function(person, idx){console.log(person.name);});
>> anonymous
>> Fred
// join the chat spa.model.chat.join();
>> true
// the spa-listchange event should fire almost immediately.
>> *Listchange > [jQuery.Event, Array[1]]
// inspect the user list again. We see the people list has // been updated to show all online people.
var peopleDb = spa.model.people.get_db();
peopleDb().each(function(person, idx){console.log(person.name);});
>> Betty
>> Fred
>> Mike
>> Pebbles
>> Wilma
We’ve completed and tested the first installment of the chat object, where we may sign in, join a chat, and inspect the people list. Now we want the chat object to handle sending and receiving messages.
6.2.4 Add messaging to the chat object
Sending and receiving messages aren’t quite as simple as they seem. As FedEx will tell you, we have to deal with logistics—the management of the transfer and receipt of the message. We’ll need to:
■ Maintain a record of the chatee—the person with whom the user is chatting.
Have the $t jQuery collection subscribe to the spa- listchange event with a function that prints “*Listchange”
and the list of arguments to the console.
Get the current user object from the people object.
Confirm the user isn’t yet signed in using the get_is_anon() method.
Try to join the chat without signing in.
Per our API specification, we’re denied.
Sign in as Fred. In the user area in the top-right corner of the browser, you’ll see the text proceed from “Please sign- in” to “... processing ...” to “Fred.” At the end of sign-in, the spa-login event will publish. This invokes the function we subscribed to the spa- login event on the $t jQuery collection, so we see the “Hello!”
message and the list of arguments.
Get the TaffyDB people collection from the people object.
Confirm that only Fred and anonymous are in the people collection.
This makes sense, as we haven’t yet joined the chat.
Join the chat.
Less than one second after join(), the spa-listchange event should publish.
This invokes the function we subscribed to the spa- listchange event on the $t jQuery collection, and so we see the “Hello!”
message along with the list of arguments.
Confirm we see a Socket.IO- style array of arguments returned. An updated user list is the first in the argument array.
Get the updated people list.
Confirm the people list now contains our mock chat-party along with our user, Fred.
189 Build the chat object
■ Send metadata such as the sender ID, name, and the recipient ID along with the message.
■ Gracefully handle the condition where a latent connection might result in our user sending a message to an offline person.
■ Publish jQuery custom global events when messages are received from the back- end so that our jQuery collections may subscribe to these events and have func- tions act upon them.
First let’s update our Model as shown in listing 6.5. Changes are shown in bold:
...
completeLogin = function ( user_list ) { ...
stateMap.people_cid_map[ user_map._id ] = stateMap.user;
chat.join();
$.gevent.publish( 'spa-login', [ stateMap.user ] );
};
...
people = (function () { ...
logout = function () {
var is_removed, user = stateMap.user;
chat._leave();
is_removed = removePerson( user );
stateMap.user = stateMap.anon_user;
$.gevent.publish( 'spa-logout', [ user ] );
return is_removed;
};
...
}());
// The chat object API // ---
// The chat object is available at spa.model.chat.
// The chat object provides methods and events to manage // chat messaging. Its public methods include:
// * join() - joins the chat room. This routine sets up // the chat protocol with the backend including publishers // for 'spa-listchange' and 'spa-updatechat' global
// custom events. If the current user is anonymous, // join() aborts and returns false.
// * get_chatee() - return the person object with whom the user // is chatting with. If there is no chatee, null is returned.
// * set_chatee( <person_id> ) - set the chatee to the person // identified by person_id. If the person_id does not exist // in the people list, the chatee is set to null. If the // person requested is already the chatee, it returns false.
Listing 6.5 Add messaging to the Model--spa/js/spa.model.js
Have completeLogin method invoke chat.join() so a user will automatically join the chat room once sign-in is complete.
Have the people._logout method invoke chat._leave() so a user will automatically exit the chat room once sign-out is complete.
Add the API docs for get_chatee(), set_chatee(), and send_msg().
// It publishes a 'spa-setchatee' global custom event.
// * send_msg( <msg_text> ) - send a message to the chatee.
// It publishes a 'spa-updatechat' global custom event.
// If the user is anonymous or the chatee is null, it // aborts and returns false.
// ...
//
// jQuery global custom events published by the object include:
// * spa-setchatee - This is published when a new chatee is // set. A map of the form:
// { old_chatee : <old_chatee_person_object>, // new_chatee : <new_chatee_person_object>
// }
// is provided as data.
// * spa-listchange - This is published when the list of // online people changes in length (i.e. when a person // joins or leaves a chat) or when their contents change // (i.e. when a person's avatar details change).
// A subscriber to this event should get the people_db // from the people model for the updated data.
// * spa-updatechat - This is published when a new message // is received or sent. A map of the form:
// { dest_id : <chatee_id>, // dest_name : <chatee_name>, // sender_id : <sender_id>, // msg_text : <message_content>
// }
// is provided as data.
//
chat = (function () { var
_publish_listchange, _publish_updatechat, _update_list, _leave_chat,
get_chatee, join_chat, send_msg, set_chatee, chatee = null;
// Begin internal methods
_update_list = function( arg_list ) { var i, person_map, make_person_map, people_list = arg_list[ 0 ], is_chatee_online = false;
clearPeopleDb();
PERSON:
for ( i = 0; i < people_list.length; i++ ) { person_map = people_list[ i ];
if ( ! person_map.name ) { continue PERSON; }
// if user defined, update css_map and skip remainder
if ( stateMap.user && stateMap.user.id === person_map._id ) { stateMap.user.css_map = person_map.css_map;
continue PERSON;
} Add the API
docs for spa- setchatee and spa- updatechat events.
Add the
is_chatee_online flag.
191 Build the chat object
make_person_map = {
cid : person_map._id, css_map : person_map.css_map, id : person_map._id, name : person_map.name };
if ( chatee && chatee.id === make_person_map.id ) { is_chatee_online = true;
}
makePerson( make_person_map );
}
stateMap.people_db.sort( 'name' );
// If chatee is no longer online, we unset the chatee // which triggers the 'spa-setchatee' global event if ( chatee && ! is_chatee_online ) { set_chatee(''); } };
_publish_listchange = function ( arg_list ) { _update_list( arg_list );
$.gevent.publish( 'spa-listchange', [ arg_list ] );
};
_publish_updatechat = function ( arg_list ) { var msg_map = arg_list[ 0 ];
if ( ! chatee ) { set_chatee( msg_map.sender_id ); } else if ( msg_map.sender_id !== stateMap.user.id && msg_map.sender_id !== chatee.id
) { set_chatee( msg_map.sender_id ); }
$.gevent.publish( 'spa-updatechat', [ msg_map ] );
};
// End internal methods _leave_chat = function () {
var sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
chatee = null;
stateMap.is_connected = false;
if ( sio ) { sio.emit( 'leavechat' ); } };
get_chatee = function () { return chatee; };
join_chat = function () { var sio;
if ( stateMap.is_connected ) { return false; } if ( stateMap.user.get_is_anon() ) {
console.warn( 'User must be defined before joining chat');
return false;
}
sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
sio.on( 'listchange', _publish_listchange );
sio.on( 'updatechat', _publish_updatechat );
stateMap.is_connected = true;
Add code to set is_chatee_online flag to true if the chatee person object is found in the updated user list.
Add code to set the chatee person object to null if it’s not found in the updated user list.
Create the
_publish_update- chat convenience method. This will publish the spa- updatechat event with a map of message details as the data.
Create the get_chatee method to return the chatee person object.
Bind _publish _update chat to handle updatechat messages received from the backend.
As a result, an spa- updatechat event is published whenever a message is received.
return true;
};
send_msg = function ( msg_text ) { var msg_map,
sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
if ( ! sio ) { return false; }
if ( ! ( stateMap.user && chatee ) ) { return false; } msg_map = {
dest_id : chatee.id, dest_name : chatee.name,
sender_id : stateMap.user.id, msg_text : msg_text
};
// we published updatechat so we can show our outgoing messages _publish_updatechat( [ msg_map ] );
sio.emit( 'updatechat', msg_map );
return true;
};
set_chatee = function ( person_id ) { var new_chatee;
new_chatee = stateMap.people_cid_map[ person_id ];
if ( new_chatee ) {
if ( chatee && chatee.id === new_chatee.id ) { return false;
} } else {
new_chatee = null;
}
$.gevent.publish( 'spa-setchatee',
{ old_chatee : chatee, new_chatee : new_chatee } );
chatee = new_chatee;
return true;
};
return {
_leave : _leave_chat, get_chatee : get_chatee, join : join_chat, send_msg : send_msg, set_chatee : set_chatee };
}());
initModule = function () { ...
};
return {
initModule : initModule, chat : chat, people : people };
}());
Create the send_msg method to send a text message and associated details.
Add code to abort sending a message if there’s no connection.
The logic also aborts if either the user or chatee isn’t set.
Add code to construct a map of message and associated details.
Add code to publish spa-updatechat events so the user may see their messages in the chat window.
Create the
set_chatee method to change the chatee object to the one provided. If the provided chatee is the same as the current one, the code does nothing and returns false.
Add code to publish an spa- setchattee event with a map of the old_chatee and new_chatee as data.
Neatly export our new public methods: get_chatee, send_msg, and set_chatee.