Tom
Tom

Reputation: 8127

How to avoid the need to delay event emission to the next tick of the event loop?

I'm writing a Node.js application using a global event emitter. In other words, my application is built entirely around events. I find this kind of architecture working extremely well for me, with the exception of one side case which I will describe here.

Note that I do not think knowledge of Node.js is required to answer this question. Therefore I will try to keep it abstract.

Imagine the following situation:

An example (purely to illustrate this question) of an incoming request:

mediator.on('http.request', request, response, emitter) {

    //deal with the new request here, e.g.:
    response.send("Hello World.");

});

So far, so good. One can now extend this application by identifying the requested URL and emitting appropriate events:

mediator.on('http.request', request, response, emitter) {

    //identify the requested URL
    if (request.url === '/') {
        emitter.emit('root');
    }
    else {
        emitter.emit('404');
    }

});

Following this one can write a module that will deal with a root request.

mediator.on('http.request', function(request, response, emitter) {

    //when root is requested
    emitter.once('root', function() {

        response.send('Welcome to the frontpage.');

    });

});

Seems fine, right? Actually, it is potentially broken code. The reason is that the line emitter.emit('root') may be executed before the line emitter.once('root', ...). The result is that the listener never gets executed.

One could deal with this specific situation by delaying the emission of the root event to the end of the event loop:

mediator.on('http.request', request, response, emitter) {

    //identify the requested URL
    if (request.url === '/') {
        process.nextTick(function() {
            emitter.emit('root');
        });
    }
    else {
        process.nextTick(function() {
            emitter.emit('404');
        });
    }

});

The reason this works is because the emission is now delayed until the current event loop has finished, and therefore all listeners have been registered.

However, there are many issues with this approach:

  1. one of the advantages of such event based architecture is that emitting modules do not need to know who is listening to their events. Therefore it should not be necessary to decide whether the event emission needs to be delayed, because one cannot know what is going to listen for the event and if it needs it to be delayed or not.
  2. it significantly clutters and complexifies code (compare the two examples)
  3. it probably worsens performance

As a consequence, my question is: how does one avoid the need to delay event emission to the next tick of the event loop, such as in the described situation?

Update 19-01-2013

An example illustrating why this behavior is useful: to allow a http request to be handled in parallel.

mediator.on('http.request', function(req, res) {

    req.onceall('json.parsed', 'validated', 'methodoverridden', 'authenticated', function() {
        //the request has now been validated, parsed as JSON, the kind of HTTP method has been overridden when requested to and it has been authenticated
   });

});

If each event like json.parsed would emit the original request, then the above is not possible because each event is related to another request and you cannot listen for a combination of actions executed in parallel for a specific request.

Upvotes: 3

Views: 2809

Answers (4)

mgutz
mgutz

Reputation: 953

Having both a mediator which listens for events and an emitter which also listens and triggers events seems overly complicated. I'm sure there is a legit reason but my suggestion is to simplify. We use a global eventBus in our nodejs service that does something similar. For this situation, I would emit a new event.

bus.on('http:request', function(req, res) {
  if (req.url === '/')
    bus.emit('ns:root', req, res);
  else
    bus.emit('404');
});

// note the use of namespace here to target specific subsystem
bus.once('ns:root', function(req, res) {
  res.send('Welcome to the frontpage.');
});

Upvotes: 3

Teemu Ikonen
Teemu Ikonen

Reputation: 11929

I think this architecture is in trouble, as you're doing sequential work (I/O) that requires definite order of actions but still plan to build app on components that naturally allow non-deterministic order of execution.

What you can do

Include context selector in mediator.on function e.g. in this way

mediator.on('http.request > root', function( .. ) { } )

Or define it as submediator

var submediator = mediator.yield('http.request > root');
submediator.on(function( ... ) {
     emitter.once('root', ... )
}); 

This would trigger the callback only if root was emitted from http.request handler.

Another trickier way is to make background ordering, but it's not feasible with your current one mediator rules them all interface. Implement code so, that each .emit call does not actually send the event, but puts the produced event in list. Each .once puts consume event record in the same list. When all mediator.on callbacks have been executed, walk through the list, sort it by dependency order (e.g. if list has first consume 'root' and then produce 'root' swap them). Then execute consume handlers in order. If you run out of events, stop executing.

Upvotes: 1

meetamit
meetamit

Reputation: 25167

It sounds like you're starting to run into some of the disadvantages of the observer pattern (as mentioned in many books/articles that describe this pattern). My solution is not ideal – assuming an ideal one exists – but:

If you can make a simplifying assumption that the event is emitted only 1 time per emitter (i.e. emitter.emit('root'); is called only once for any emitter instance), then perhaps you can write something that works like jQuery's $.ready() event.

In that case, subscribing to emitter.once('root', function() { ... }) will check whether 'root' was emitted already, and if so, will invoke the handler anyway. And if 'root' was not emitted yet, it'll defer to the normal, existing functionality.

That's all I got.

Upvotes: 1

rdrey
rdrey

Reputation: 9529

Oi, this seems like a very broken architecture for a few reasons:

  1. How do you pass around request and response? It looks like you've got global references to them.
  2. If I answer your question, you will turn your server into a pure synchronous function and you'd lose the power of async node.js. (Requests would be queued effectively, and could only start executing once the last request is 100% finished.)

To fix this:

  1. Pass request & response to the emit() call as parameters. Now you don't need to force everything to run synchronously anymore, because when the next component handles the event, it will have a reference to the right request & response objects.
  2. Learn about other common solutions that don't need a global mediator. Look at the pattern that Connect was based on many Internet-years ago: http://howtonode.org/connect-it <- describes middleware/onion routing

Upvotes: 0

Related Questions