Skip to content

Instantly share code, notes, and snippets.

@philipwalton
Last active Jan 3, 2016
Embed
What would you like to do?
Async events handling strategies
// host library code
program()
.initStuff()
.then(function() {
dispatcher.emit('beforeRenderPost')
})
.then(function() {
dispatcher.emit('afterRenderPost')
})
.then(function() {
dispatcher.emit('beforeWritePost')
})
.then(function() {
dispatcher.emit('afterWritePost')
})
.catch(errorHandler)
// now the user's code can simply be the following and the host logic
// will wait until their async function is finished before continuing
dispatcher.on('beforeRenderPost', doSomethingAsyncToPost)
@philipwalton
Copy link
Author

philipwalton commented Jan 13, 2014

Re: this blog post

Events/signals are a great way for host applications to allow third party plugin developers to build functionality on top of the application's core. They expose hook points allowing you to register a "listener" function to be executed at a particular point in time with the necessary arguments.

The problem with callbacks/events/signals (as opposed to promises) is that they don't notify you when the user's registered function has completed (assuming it's async).

For example, I'm working on a site generator that emits various events to plugin developers like: beforeRenderPost, afterRenderPost, beforeWritePost, and afterWritePost. When all your code is synchronous this works just fine, but as you can see if the listener you register on afterRenderPost doesn't complete before the post is written to disk, there's nothing you can do about it, and the host application has no idea.

I'm brainstorming a good solution to this problem, but aren't 100% happy with any of my options. Currently I'm leaning toward something like the above where the emit() method of event emitter instances returns a promise.

I like the idea of this, but I'm afraid to adds unnecessary confusion to the existing understand of events. Thoughts?

@millermedeiros
Copy link

millermedeiros commented Jan 13, 2014

was sending messages over twitter but too hard to explain things under 140 chars.. so here we go.

I would not recommend following the API you proposed on the first tweet obj.on('foo').then(cb). The main issue that I see is that what you actually need is some sort of promise queue, exposing this sort of API would allow/incentive other parts of the app to listen for the completion of all the handlers, it would also only work once for each event (since I would expect it to always return same promise every time on('foo') is called); I believe this logic should only be available to the internal module structure (the part of the app that triggers the listeners);

One simple way to implement it is to create some sort of queue and reuse a method like Q.all:

function AsyncQueue() {
  this._handlers = [];
}

AsyncQueue.prototype.execute = function(args){
  if (!this._promise) {
    var self = this;
    this._promise = Q.all(this._handlers.map(function(handler){
      return handler.apply(self, args);
    }));
  }
  return this._promise;
};

AsyncQueue.prototype.listen = function(handler){
  if (this._promise) {
    throw new Error("can't add listener after event already happened");
  }
  this._handlers.push(handler);
};

and on your app code you would do:

program.onBeforeRenderPost = new AsyncQueue();
program.onAfterRenderPost = new AsyncQueue();
program.onBeforeWritePost = new AsyncQueue();
program.onAfterWritePost = new AsyncQueue();

program
  .initStuff()
  // beforeRenderPost could be replaced by `program.onBeforeRenderPost.execute.bind(program)`
  .then(beforeRenderPost)
  .then(renderPost)
  .then(afterRenderPost)
  .then(beforeWritePost)
  .then(writePost)
  .then(afterWritePost)
  .catch(errorHandler);

function beforeRenderPost(){
  return program.onBeforeRenderPost.execute();
}

// ... etc

and user would do:

program.onBeforeWritePost.listen(doMyAsyncOperation);

that should probably be enough for your use case.

PS: doing the same thing with callbacks would not be that hard either, would basically need to check against the expected Function.length to define if you should wait for callback call or not.

@millermedeiros
Copy link

millermedeiros commented Jan 13, 2014

need to remind that promises are usually used for actions that should only happen once, that's why you probably won't see an implementation of Events that has this kind of feature. - events usually/can happen more than once.

@philipwalton
Copy link
Author

philipwalton commented Jan 14, 2014

need to remind that promises are usually used for actions that should only happen once

True, but if the .emit() function always returns a new promise instance, this wouldn't be an issue.

However, after giving it more thought I think I'm in agreement with you that events should be one-way communication. They have an established purpose and mixing in this behavior would be unnecessarily confusing.

I think it makes the most sense to create a new paradigm like the one you've outlined. I'll probably work on this for my project and hopefully release it once it's done.

@philipwalton
Copy link
Author

philipwalton commented Jan 14, 2014

would basically need to check against the expected Function.length to define if you should wait for callback call or not

I originally considered this. Like, if the callback function's .length property is one greater than the emitter's arguments.length property, assume async, but that won't work 100% of the time. Sometimes a callback will make use of the arguments object for ...rest parameter type situations, so it gets a bit hairy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment