You have created a Node.js application, but you need to communicate within it by emitting a custom event.
Solution
In this solution you will create a Node.js application that will demonstrate how to create and listen for custom events. You will create an event that executes after a timeout period has expired. This represents a situation in your application that will occur when an operation completes. This then will call a function, doATask, which will return a status of whether the operation was successful or failed. There are two approaches to accomplish this.
First, you will create events that are specific to the status. This requires checking the status and creating an event specifically for that status, as well as binding to those specific events to handle the special cases. This is demonstrated in Listing 5-1.
Listing 5-1. Custom Events for Individual Statuses /**
* Custom Events */
var events = require('events'),
Chapter 5 ■ Using events and Child proCesses
function doATask(status) {
if (status === 'success') {
emitter.emit('taskSuccess'); // Specific event } else if (status === 'fail') {
emitter.emit('taskFail'); } } emitter.on('taskSuccess', function() { console.log('task success!'); }); emitter.on('taskFail', function() { console.log('task fail'); });
// call task with success status setTimeout(doATask, 500, 'success'); // set task to fail
setTimeout(doATask, 1000, 'fail');
While you see that this effectively gets the events to propagate appropriately, this still causes you to create two individual events to emit. This can easily be modified to be more streamlined and efficient, as you will see in Listing 5-2.
Listing 5-2. One Emitter to Rule Them All /**
* Custom Events */
var events = require('events'),
emitter = new events.EventEmitter(); function doATask(status) {
// This event passes arguments to detail status emitter.emit('taskComplete', 'complete', status); }
// register listener for task complete
emitter.on('taskComplete', function(type, status) {
console.log('the task is ', type, ' with status ', status); });
// call task with success status setTimeout(doATask, 500, 'success'); // set task to fail
Chapter 5 ■ Using events and Child proCesses
This is a much leaner and more efficient implementation. You can emit a single event that will work for multiple states in your application. In these examples you have seen an implementation in which the events are all handled and emitted from the same source file. In Listing 5-3 you see an example of sharing an event emitter to emit an event that will be received in a module outside of the current module.
Listing 5-3. Emitting Module /**
* Custom Events */
var events = require('events'),
emitter = new events.EventEmitter(), myModule = require('./5-1-3.js')(emitter); emitter.on('custom', function() {
console.log('custom event received'); });
emitter.emit('custom');
One other way in which you are able to create custom events for your application is to utilize the global process object. This Node.js object is an EventEmitter, and it will allow you to register events that will be shared within the process. This type of event is emitted from the code from Listing 5-4.
Listing 5-4. Emitting an Event on the Node.js Process /* Module.js file */
var myMod = module.exports = { emitEvent: function() {
process.emit('globalEvent'); }
};
How It Works
In this example you saw multiple ways in which you are able to create custom events. These events can be emitted anywhere there is a Node.js EventEmitter. The Node.js EventEmitter class is one of the cornerstones of communication between and within the modules that make up your application.
The first thing that you encounter when you build an event is the EventEmitter class. This class consists of an object collection of events that refer to the different types of events that are registered. The concept of type is simply the name that you give your event such as taskComplete or taskFail. This is important when you actually emit the event using the EventEmitter's emit method.
Listing 5-5. EventEmitter’s Emit Method
EventEmitter.prototype.emit = function(type) { var er, handler, len, args, i, listeners; if (!this._events)
Chapter 5 ■ Using events and Child proCesses
// If there is no 'error' event listener then throw. if (type === 'error') {
if (!this._events.error ||
(typeof this._events.error === 'object' && !this._events.error.length)) {
er = arguments[1]; if (this.domain) {
if (!er) er = new TypeError('Uncaught, unspecified "error" event.'); er.domainEmitter = this;
er.domain = this.domain; er.domainThrown = false; this.domain.emit('error', er); } else if (er instanceof Error) { throw er; // Unhandled 'error' event } else {
throw TypeError('Uncaught, unspecified "error" event.'); }
return false; }
}
handler = this._events[type]; if (typeof handler === 'undefined') return false;
if (this.domain && this !== process) this.domain.enter();
if (typeof handler === 'function') { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: handler.call(this, arguments[1]); break; case 3:
handler.call(this, arguments[1], arguments[2]); break;
// slower default:
len = arguments.length; args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; handler.apply(this, args); }
Chapter 5 ■ Using events and Child proCesses
args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; listeners = handler.slice(); len = listeners.length; for (i = 0; i < len; i++)
listeners[i].apply(this, args); }
if (this.domain && this !== process) this.domain.exit();
return true; };
This method consists of two main parts. First, there is the special handling of “error” events. This will emit the error event as you expect, unless there is not a listener for the error event. In this case then Node.js will throw the error and the method will return false. The second part of this method is the portion where nonerror events are handled.
After checking to make sure that there is a handler for the given event of the specified type, Node.js then checks to see if the event handler is a function. If it is a function, Node.js will parse the arguments from the emit method and apply those to the handler. This is how Listing 5-2 is able to pass the parameters to the taskComplete event. The extra arguments supplied are applied when the emit method invokes the handler.
The other solutions all utilize the same emit method, but they get the result of emitting events in different ways. Listing 5-4 represents a Node.js module that is shared throughout an application. This module contains a function that will emit an event to the rest of the app. The way this is accomplished in this solution is to leverage the knowledge that the main Node.js process is an EventEmitter. This means you simply emit the event by invoking process.emit('globalEvent'), and a portion of the application that is sharing that process will receive the event.
5-2. Adding a Listener for Custom Events
Problem
You have emitted custom events in the previous section, but without an appropriate way to bind to these events you are unable to use them. For this you need to add listeners to those events.
Solution
This solution is the counterpart to Section 5-1. In the previous section you implemented EventEmitters and emitted the events. Now you need to add listeners for those events so you can handle them in your application. The process is just as simple as emitting events, as shown in Listing 5-6.
Listing 5-6. Adding Event Listeners to Custom Events and System Events /**
* Custom Events */
var events = require('events'),
Chapter 5 ■ Using events and Child proCesses
function doATask(status) {
if (status === 'success') {
emitter.emit('taskSuccess'); // Specific event } else if (status === 'fail') {
emitter.emit('taskFail'); }
// This event passes arguments to detail status emitter.emit('taskComplete', 'complete', status); }
emitter.on('newListener', function(){
console.log('a new listener was added'); }); emitter.on('taskSuccess', function() { console.log('task success!'); }); emitter.on('taskFail', function() { console.log('task fail'); });
// register listener for task complete
emitter.on('taskComplete', function(type, status) {
console.log('the task is ', type, ' with status ', status); });
// call task with success status setTimeout(doATask, 2e3, 'success'); // set task to fail
setTimeout(doATask, 4e3, 'fail');
You can also pass your EventEmitter to an external module and then listen for events from within that separate section of code.
Listing 5-7. Event Listening from an External Module /** * External Module */ module.exports = function(emitter) { emitter.on('custom', function() { console.log('bazinga'); }); };
Just as you had emitted the event by using the Node.js process EventEmitter, you are able to bind listeners to that process and receive the events.
Chapter 5 ■ Using events and Child proCesses
Listing 5-8. Node.js Processwide Listener /**
* Global event */
var ext = require('./5-1-5.js'); process.on('globalEvent', function() { console.log('global event'); });
ext.emitEvent();
How It Works
As you examine the solution in Listing 5-6 you should quickly notice how to go about adding a listener to an event. This is as simple as calling the EventEmitter.on() method. The .on method of the EventEmitter accepts two arguments: first, the event type name; second, the listener callback, which will accept any of the arguments that were passed to the emit() event. The .on method is really just a wrapper to the EventEmitter addListener function, which takes the same two arguments. You could call this method directly in place of calling the .on function as they are the same.
Listing 5-9. EventEmitter addListener
EventEmitter.prototype.addListener = function(type, listener) { var m;
if (typeof listener !== 'function')
throw TypeError('listener must be a function'); if (!this._events)
this._events = {};
// To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener".
if (this._events.newListener)
this.emit('newListener', type, typeof listener.listener === 'function' ? listener.listener : listener);
if (!this._events[type])
// Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener;
else if (typeof this._events[type] === 'object') // If we've already got an array, just append. this._events[type].push(listener);
else
// Adding the second element, need to change to array. this._events[type] = [this._events[type], listener]; // Check for listener leak
if (typeof this._events[type] === 'object' && !this._events[type].warned) { var m;
Chapter 5 ■ Using events and Child proCesses if (this._maxListeners !== undefined) { m = this._maxListeners; } else { m = EventEmitter.defaultMaxListeners; }
if (m && m > 0 && this._events[type].length > m) { this._events[type].warned = true;
console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' +
'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); console.trace(); } } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener;
The addListener method, as you can see from the source snippet, accomplishes several tasks. First, after verifying that the listener callback is a function, the addListener method emits its own event ‘newListener’ in order to signify that a new listener of a given type has been added.
The second thing that happens is the addListener function will push the listener function to the event to which it is bound. In the previous section, this function is what became the handler function for each event type. The emit() function will then either .call() or .apply() to that function, depending on the number of arguments that are provided by the emitter itself.
Lastly in the addListener function, you will find that Node.js is very kind and attempts to protect you from potential memory leaks. It does this by checking to see if the number of listeners exceeds a predefined limit that defaults to 10. You can, of course, configure this to be a higher number by using the setMaxListeners() method as instructed by the helpful warning that appears once you exceed this number of listeners.