Promisify event emitter

Convert NodeJS EventEmitter emit to a promise-returning method.

Event emitters provide the decoupling between the caller and the callee. A typical use would be (taken from Node.js Events and EventEmitter) is "ringing" a door bell

var events = require('events');
var eventEmitter = new events.EventEmitter();
function ringBell()
{
  console.log('ring ring ring');
}
eventEmitter.on('doorOpen', ringBell);
eventEmitter.emit('doorOpen');
// prints "ring ring ring"

Event emitters to me always had two limitations

  • They do not return a result to the caller. Thus I could not do something like this

      function ringBell()
      {
        return 'ring ring ring';
      }
      eventEmitter.on('doorOpen', ringBell);
      console.log('door opened', eventEmitter.emit('doorOpen'));
    
  • While they are asynchronous, there is no simple way of ordering or knowing when the emit has finished. I would love to be able to use promise-like syntax

      eventEmitter.emit('doorOpen')
          .then(function () {
              console.log('door has opened');
          });
    

Let us convert EventEmitter to return a promise to be resolved with result of the on listener. To do this, we will use my self-addressed library for converting postMessage calls to return a promise. The principle behind self-addressed is that it puts data (call it letter) into an envelope. The envelope has a stamp that allows us to later match some other message to the original data, resolving the promise. In our case, let us overwrite the emit method first.

var events = require('events');
var selfAddressed = require('self-addressed');
var eventEmitter = new events.EventEmitter();
var _emit = events.EventEmitter.prototype.emit;
eventEmitter.emit = function (name, data) {
  function mailman(address, envelope) {
    _emit.call(address, name, envelope);
  }
  return selfAddressed(mailman, this, data); // returns a promise
};

The self-addressed uses the post office delivery analogy everywhere. When we want to send a new piece of data, we use the function selfAddressed that calls the custom delivery mechanism mailman and a given address. This is necessary to decouple our promise-returning layer from the actual message passing.

Once the data has been delivered, it is hidden inside an envelope. To open an envelope we need to use selfAddressed again. We will do this inside on handler, wrapping the user-supplied event listener.

var _on = events.EventEmitter.prototype.on;
eventEmitter.on = function (name, fn) {
  function onSelfAddressedEnvelope(envelope) {
    // we could get data from envelope if needed
    var result = fn();
    selfAddressed(envelope, result);
    // somehow deliver the envelope back to the caller .emit
  }
  _on.call(this, name, onSelfAddressedEnvelope);
};

At this point, the callback functio has been called, and the result has been placed back into the envelope using selfAddressed(envelope, result); statement. We use the same function to perform every step because self-addressed has been lucky enough to only need the number of arguments to determine the desired action.

In order to send the envelope back to the caller and fulfill the promise, we need to hack the self-addressed semantics. Typically we would just use same approach as in the emit function - by sending the envelope using mailman function. This maps well to the cross-frame communication, but EventEmitters are unidirectional - there is no sending the response back to whoever called eventEmitter.emit method. Thus we just mark the envelope as the reply and call selfAddressed to deliver it, taking a shortcut.

var _on = events.EventEmitter.prototype.on;
eventEmitter.on = function (name, fn) {
  function onSelfAddressedEnvelope(envelope) {
    if (selfAddressed.is(envelope)) {
      var result = fn();
      selfAddressed(envelope, result);
      // there is nowhere to send the response envelope
      // event emitters are unidirectional.
      // so open the envelope right away!
      envelope.replies = 1;
      selfAddressed(envelope); // deliver
    }
  }
  _on.call(this, name, onSelfAddressedEnvelope);
};

That is it. Now we can use the event emitter as a promise returning asynchronous operation.

function ringBell(x)
{
  log('ringing bell', x);
  return 'ring ring ring';
}
eventEmitter.on('doorOpen', ringBell);
eventEmitter.emit('doorOpen').then(function (sound) {
  log('door has opened with', sound);
});
log('after emitted an event');

The output shows the expected sequence of events

after emitted an event
ringing bell 
door has opened with ring ring ring

Related: Journey from procedural to reactive JavaScript with stops.