Search code examples
javascriptnode.jsclasspromiseeventemitter

Proper way of creating a EventEmitter that works with Promises in the background


I'm creating a "class" that emits events such as error, data, downloadFile and initialize. Each event is fired after a request is made, and each event is fired by a method that has the same name:

class MyClass extends EventEmitter {
  constructor(data) {
    this.data = data

    this.initialize()
      .then(this.downloadFile)
      .then(this.data)
      .catch(this.error)
  }

  initialize() {
    const req = superagent.post('url...')
    superagent.send(data)
    const res = await req // this will actually fire the request
    this.emit('initialize')
    this.url = res.body
    return res
  }

  downloadFile() {
    const req = superagent.put(this.url)
    const res = await req; // this will actually fire the request
    req.on('progress', (progress) => this.emit('downloadFile', progress)
    //
    // save to disk
    //
    return res
  }

  data() {
    // Next in the sequence. And will fire the 'data' event: this.emit('data', data)
  }

  error(err) {
    this.emit('error', err)
  }
}

After that I have the data method to be called. My doubt is: Is there a design pattern to call the events in sequence without using Promises? Currently I'm using chaining, but I'm feeling that this isn't the best approach, maybe I'm wrong.

this.initialize()
  .then(this.downloadFile)
  .then(this.data)
  .catch(this.error)

But I feel that could be a better approach.


Answers for bergi's questions:

a) Why are you using class syntax?

Because it's easier to inherit from EventEmitter and personally I think it's more readable than using a constructor functin, e.g:

function Transformation(data) {
  this.data = data
}

// Prototype stuffs here

b) How this code is going to be used

I'm creating a client to interact with my API. The ideia is that the user can see what is happening in the background. E.g:

const data = {
 data: {},
 format: 'xls',
 saveTo: 'path/to/save/xls/file.xls'
}

const transformation = new Transformation(data)

// Events
transformation.on('initialize', () => {
  // Here the user knows that the transformation already started
})

transformation.on('fileDownloaded', () => {
  // Here the file has been downloaded to disk
})

transformation.on('data', (data) => {
  // Here the user can see details of the transformation -
  //   name,
  //   id,
  //   size,
  //   the original object,
  //   etc
})

transformation.on('error', () => {
  // Here is self explanatory, if something bad happens, this event will be fired
})

c) What it is supposed to do?

The user will be able to transform a object with data into a Excel.


Solution

  • It sounds like the transformation object you are creating is used by the caller solely for listening to the events. The user does not need a class instance with properties to get or methods to call. So don't make one. KISS (keep it super simple).

    function transform(data) {
      const out = new EventEmitter();
      async function run() {
        try {
          const url = await initialise();
          const data = await downloadFile(url);
          out.emit('data', data);
        } catch(err) {
          out.emit('error', err);
        }
      }
    
      async function initialise() {
        const req = superagent.post('url...')
        superagent.send(data)
        const res = await req // this will actually fire the request
        out.emit('initialize')
        return res.body
      }
    
      async function downloadFile(url) {
        const req = superagent.put(url)
        req.on('progress', (progress) => out.emit('downloadFile', progress)
        const res = await req; // this will actually fire the request
        //
        // save to disk
        //
        return data;
      }
    
      run();
      return out;
    }
    

    It might be even simpler to leave out the (once-only?) data and error events and just to return a promise, alongside the event emitter for progress notification:

      return {
        promise: run(), // basically just `initialise().then(downloadFile)`
        events: out
      };