aldred
aldred

Reputation: 853

Unexpected asynchronous behaviour in EventEmitter

I used an EventEmitter as the following:

const EventEmitter = require('events');

const myEmitter = new EventEmitter();

function c2(num) {
  return new Promise((resolve) => {
    resolve(`c2: ${num}`);
  });
}

// eslint-disable-next-line no-underscore-dangle
// eslint-disable-next-line no-console
const doSomeStuff = async (number) => {
  try {
    console.log(`doSomeStuff: ${number}`);
    const r2 = await c2(number);
    console.log(r2);
  } catch (err) {
    throw err;
  }
};

myEmitter.on('eventOne', async (n) => {
  await doSomeStuff(n);
});

myEmitter.emit('eventOne', 1);
myEmitter.emit('eventOne', 2);
myEmitter.emit('eventOne', 3);
myEmitter.emit('eventOne', 4);

I expect a result of

doSomeStuff: 1
c2: 1
doSomeStuff: 2
c2: 2
doSomeStuff: 3
c2: 3
doSomeStuff: 4
c2: 4

However the output shows me:

doSomeStuff: 1
doSomeStuff: 2
doSomeStuff: 3
doSomeStuff: 4
c2: 1
c2: 2
c2: 3
c2: 4

As per my understanding, EventEmitter invokes the event callback function synchronously, however for some reason the callback function has not finished executing before the next callback function is invoked. I think I'm missing something very fundamental here.

Upvotes: 1

Views: 405

Answers (2)

zero298
zero298

Reputation: 26867

The event handler doesn't care about the async nature of the function. In fact, it doesn't care about the return value at all. It will just call it as soon as it can whenever it hears an event and will keep firing every time it hears an event. It won't care if it is already running a function or not.

myEmitter.on('eventOne', async (n) => {
  await doSomeStuff(n);
});

Is effectively the exact same as if you didn't have the async/await:

myEmitter.on('eventOne', (n) => {
  doSomeStuff(n);
});

Hypothetically, you could adjust your code a little such that the you do get the output that you are expecting. However, you need to introduce a singular path context such that every emitter is affecting 1 thing instead of every event triggering its own instance of the doSomeStuff. Here is an example using generator functions:

// EventEmitter Polyfill
class EventEmitter {
  constructor() {this._listeners = new Map();}
  on(e, cb) {this._listeners.set(e, [...(this._listeners.get(e) || []), cb]);}
  emit(e, payload) {for (const listener of (this._listeners.get(e) || [])) listener(payload);}
}

const myEmitter = new EventEmitter();

function c2(num) {
  return new Promise(resolve => {
    resolve(`c2: ${num}`);
  });
}

async function* doSomeStuff() {
  while (true) {
    try {
      const number = yield;
      console.log(`doSomeStuff: ${number}`);
      const r2 = await c2(number);
      console.log(r2);
    } catch (err) {
      throw err;
    }
  }
}

const someStuff = doSomeStuff();
someStuff.next(); // Start it

myEmitter.on("eventOne", n => {
  someStuff.next(n);
});

myEmitter.emit("eventOne", 1);
myEmitter.emit("eventOne", 2);
myEmitter.emit("eventOne", 3);
myEmitter.emit("eventOne", 4);

Upvotes: 2

Kai
Kai

Reputation: 3643

following the logic above, here is my opinions:

//You emitted event asynchronous, so the listeners will be called asynchronous in [A] scope
myEmitter.emit('eventOne', 1); // all 
myEmitter.emit('eventOne', 2); // of
myEmitter.emit('eventOne', 3); // us
myEmitter.emit('eventOne', 4); // start at almost the same time.

// In listener:
await doSomeStuff(n); // I run synchronously inside [event 1] scope, not [A] scope
await doSomeStuff(n); // I run synchronously inside [event 2] scope, not [A] scope
await doSomeStuff(n); // I run synchronously inside [event 3] scope, not [A] scope
await doSomeStuff(n); // I run synchronously inside [event 4] scope, not [A] scope

and the rest will be resolve asynchronously.

brief: events are emitted ASYNCHRONOUSLY so that no reason for listeners listen them SYNCHRONOUSLY.

Upvotes: 1

Related Questions