Declarative Events

5 April 2018


[ If you didn't read previous article, I would recommend read it first before moving on. ]

Let's look at the most popular example in Node:

http.createServer((request, response) => {
    // send back a response every time you get a request
}).listen(8080, '', () => {
    console.log('server is listening on')

Here method createServer uses request listener as argument, which actually works like an event: on every request it provides a response. Unlike simple async call, event is never finished and it's being invoked every time when it's needed.

It can be rewritten in the following declarative way:

new LoggedListeningServer(
  new ListeningServer(
    new CreatedServer(
      new RequestResponseEvent()
    ), 8080, ''
  ), 'server is listening on'

As you can see, RequestResponseEvent is a node of the async tree that represents request listener, but it's not a simple argument or async object. RequestResponseEvent implements Event interface and it needs to be treated in a special way, so it requires more flexibility of the whole system. But we also can create Event via AsyncObject.

Basically, Event is an interface that provides only one method: body(...args) that must be implemented by the extended classes. The main purpose of this interface is to replace functions of events(or listeners). Event is not an AsyncObject, but it represents function of some event. But you cannot use AsyncObject that represents some Event instead of the Event. In that case you can use AsyncObject that represents some function. Actually, you can use a function in the async composition instead of Event, but for readability it's better to use Event.

class Event {
  constructor () {}


  body (...args) {
    throw new Error(`Method body must be overriden with arguments ${args} of the event/eventListener you call`)

How to create an Event

Let's say we have a ReadStream and we need to be able to attach an 'open' event to it. So, we need to create an async object ReadStreamWithOpenEvent that represents ReadStream with attached 'open' event.

// Represented result is a ReadStream
class ReadStreamWithOpenEvent extends AsyncObject {
    event is an Event with body(fd)
  constructor ( readStream, event) {
    super(readStream, event)

  syncCall () {
    return (readStream, event) => {
      readStream.on('open', event)
      return readStream

Actually readStream with 'open' event has the following signature:

readStream.on('open', (fd) => {
  // here we work with fd  

So, OpenEvent would be:

class OpenEvent extends Event {
  constructor () {
    super ()

  body (fd) {
    // here we work with fd

As you can see body use the same arguments as the event of the readStream.

So, in the composition it would look something like this:

new ReadStreamWithOpenEvent(
  new CreatedSomeHowReadStream(), new OpenEvent()

The main problem of Event is that it cannot encapsulate async objects, because it's being replaced by corresponding function (which is actually body()) in a moment of construction of the async tree that contains this event. So, if you want use an event that can be constructed by async objects you can simply create AsyncObject that represents a function that is the body of the event:

class OpenEvent extends AsyncObject {
  constructor (...asyncObjects) {

  syncCall () {
    return (...resultsFromAsyncObjects) => {
      // This is body of the event
      return (fd) => {
        /* now you can use here not only fd but also
            ...resultsFromAsyncObjects */

And now the composition of objects would look something like this:

new ReadStreamWithOpenEvent(
  new CreatedSomeHowReadStream(),
  new OpenEvent(
    new SomeAsyncParameter()

Updates: you can create Event via Created async object, it will allow to encapsulate data from async objects inside of Event.