14

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.

cyberwombat
  • 38,105
  • 35
  • 175
  • 251
  • Can't you just mark the callback to `idle` as `async` and then use `await` inside of it as usual? – nicholaswmin Nov 23 '17 at 06:01
  • 1
    That didn't seem to work as that was my first try. Events dont run async. Perhaps i made a coding error if you really think that should work – cyberwombat Nov 23 '17 at 06:11
  • 1
    I've added an answer - **but** - on second thought, if you can't get it to work it might be because `transporter.on` is not the same as `EventEmitter.on`. Instead it could assume internally that the callback function provided is not a `Promise`, which is more-or-less what the `async` keyword does. I'm inclined to believe that that's not the case but it is a possibility. If that's the case you might want to wrap `async` in an IIFE – nicholaswmin Nov 23 '17 at 06:31

2 Answers2

16

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')
nicholaswmin
  • 21,686
  • 15
  • 91
  • 167
  • It is an EventEmitter - https://github.com/nodemailer/nodemailer/blob/master/lib/ses-transport/index.js#L50 – cyberwombat Nov 23 '17 at 15:39
  • Ahh... I think my test framework is the issue. Calling the sample from CLI works but not form Ava. I have my outer function async but since nothing returns a promise it just ends. Will have to explore this a bit more. – cyberwombat Nov 23 '17 at 16:21
  • Yup. My test was the issue. It's of course not waiting so adding a timeout should handle the issue. I had misdiagnosed my issue. – cyberwombat Nov 23 '17 at 16:38
  • To be honest I'm thinking that you're misusing your test framework - you should be able to do something similar to Mocha's `done` in event handlers – nicholaswmin Nov 23 '17 at 19:29
  • Using `done` would work (t.end in ava) if I just test the event firing. But I need to assure the async function inside did its job so I need a little time to check that. Doesn't that make sense? – cyberwombat Nov 23 '17 at 19:34
  • You are exposing yourself to a race hazard by waiting an arbitrary amount of time - then again you are right, it really is a tricky scenario – nicholaswmin Nov 24 '17 at 12:57
  • Use a [mocking library](http://sinonjs.org/) to *mock* the call to `sendMail`. This way you can be notified when `transporter.sendMail` is called so you can end your test reliably. – nicholaswmin Nov 25 '17 at 04:44
  • I don't think that will work. My real issue is preventing the function from existing early. Essentially this is the equivalent of running an async function inside a sync one and not waiting for a callback. What might work is mocking the event emitter inside using an async though that would require changing my code to return an async fn from the `on` event as you suggested - it just would not do anything in real time. – cyberwombat Nov 25 '17 at 15:36
  • 1
    It's worth a shot though. Let me see if Sinon can help. – cyberwombat Nov 25 '17 at 15:43
  • Should your test end when `transporter.sendMail` is called successfully, either sync or async? – nicholaswmin Nov 25 '17 at 15:45
  • 1
    I got it to work - thank you for your suggestion. I ended up mocking `sendMail` to check it was fetching multiple mail and another test with mocking some non shown function that logs the result of the mail and mocking the SES call using nock. – cyberwombat Nov 25 '17 at 17:45
  • 2
    Are you sure you don't want to rename `myEmitter` to `whoLetTheDogs` with a listener on `out`? – Kyle Chadha Aug 25 '20 at 23:00
1

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);
Pavan
  • 21
  • 2
  • 1
    This is no different from inlining the `sendMail` function as an async function, as per Nik's answer – Cardin Aug 20 '19 at 05:45