Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using async in event emitter

I am being challenged trying to make an async call inside an event.

Here's the code from Nodemailer - I've added the line where I need to make an async call:

let transporter = nodemailer.createTransport({
    SES: new aws.SES({
        apiVersion: '2010-12-01'
    }),
    sendingRate: 1 // max 1 messages/second
});

// Push next messages to Nodemailer
transporter.on('idle', () => {
    while (transporter.isIdle()) {

        // I need to make an async db call to get the next email in queue
        const mail = await getNextFromQueue()

        transporter.sendMail(mail);
    }
});

I found this post which suggest switching things around which makes sense however I have been unable to apply it correctly to this.

Update - The answer was to mock sendMail using Sinon.

like image 578
cyberwombat Avatar asked Nov 23 '17 05:11

cyberwombat


People also ask

Are event emitters async?

emit("event1"); The neat thing about event emitters is that they are asynchronous by nature.

Is event emitter synchronous or asynchronous?

Yes, events are synchronous and blocking. They are implemented with simple function calls. If you look at the eventEmitter code, to send an event to all listeners, it literally just iterates through an array of listeners and calls each listener callback, one after the other.

Why is it that using async functions with event handlers is problematic?

Using async functions with event handlers is problematic, because it can lead to an unhandled rejection in case of a thrown exception: const ee = new EventEmitter(); ee. on('something', async (value) => { throw new Error('kaboom'); });

Can I use async await in Node?

Async functions are available natively in Node and are denoted by the async keyword in their declaration. They always return a promise, even if you don't explicitly write them to do so. Also, the await keyword is only available inside async functions at the moment – it cannot be used in the global scope.


2 Answers

You can just mark your callback as async and use await inside of it.

The fact that it's an event handler callback makes no difference since at the end it's just a plain-old Function.

Node snippet

'use strict'

const EventEmitter = require('events')
const myEmitter = new EventEmitter()

const getDogs = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(['Woof', 'Woof', 'Woof'])
    }, 500)
  })
}

myEmitter.on('event', async () => {
  const dogs = await getDogs()
  console.log(dogs)
})

myEmitter.emit('event')

Alternative scenario

If you still can't get it to work it might be because transporter.on is not the same as EventEmitter.on - meaning it's a custom function provided by transporter.

It could assume internally that the callback function provided is not a Promise - keep in mind that labelling a function as async forces the function to always implicitly return a Promise.

If that's the case you might want to wrap the async function in an IIFE.

// ..rest of code from above

myEmitter.on('event', () => {
  // wrap into an IIFE to make sure that the callback 
  // itself is not transformed into a Promise
  (async function() {
    const dogs = await getDogs()
    console.log(dogs)
  })()
})

myEmitter.emit('event')
like image 51
nicholaswmin Avatar answered Oct 22 '22 11:10

nicholaswmin


I had a similar scenario and if I were you I would have done the following.

let transporter = nodemailer.createTransport({
SES: new aws.SES({
    apiVersion: '2010-12-01'
}),
sendingRate: 1 // max 1 messages/second
});

const sendMail = async () => {
    while (transporter.isIdle()) {
        // I need to make an async db call to get the next email in queue
        const mail = await getNextFromQueue()

        transporter.sendMail(mail);
    }
}

// Push next messages to Nodemailer
transporter.on('idle', sendMail);
like image 29
Pavan Avatar answered Oct 22 '22 10:10

Pavan