Sigma Octantis
Sigma Octantis

Reputation: 952

NodeJS 7: EventEmitter + await/async

How we can end an async function from a callback passed to an event emitter without promisifing the event emitter?

Also without using external modules, just plain NodeJS 7.x/8.x (Which supports Es6 syntax and async/await.

We want basically to mix an async function ... with an event emitter, so that it resolves when the event emitter signals end.

Also bear in mind that we won't start with the event emitter until done with some other async functions, using await.

If we had a "new Promise(...)" we would call resolve(); and the headache would be over, but in 'async' there's no 'resolve', plus we cannot use 'return' because we're inside a callback.

/*
 * Example of mixing Events + async/await.
 */

// Supose a random pomise'd function like:
function canIHazACheezBurger () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(Math.random() > 0.5);
    }, 500 + Math.random() * 500)
  });
}

/**
 * Then, we want to mix an event emitter with this logic,
 * what we want is that this function resolves the promise
 * when the event emitter signals 'end' (for example).
 * Also bear in mind that we won't start with the event emitter
 * until done with the above function.
 * If I had a "new Promise(...)" I would call resolve(); and the
 * headache would be over, but in 'async' there's no 'resolve',
 * plus I cannot use 'return' because I'm inside a callback.
 */
async function bakeMeSomeBurgers () {
  let canIHave = await canIHazACheezBurger();
  // Do something with the result, as an example.
  if (canIHave) {
    console.log('Hehe, you can have...');
  } else {
    console.log('NOPE');
  }
  // Here invoke our event emitter:
  let cook = new BurgerCooking('cheez');
  // Assume that is a normal event emitter, like for handling a download.
  cook.on('update', (percent) => {
    console.log(`The burger is ${percent}% done`);
  });
  // Here lies the problem:
  cook.on('end', () => {
    console.log('I\'ve finished the burger!');
    if (canIHave) {
      console.log('Here, take it :)');
    } else {
      console.log('Too bad you can\'t have it >:)');
    }
    // So, now... What?
    // resolve(); ? nope
    // return; ?
  });
}

Disclaimer

I want to apologize if this question is already done somewhere. The research done shows questions related to mix async with sync logic, but I've found nothing about this.

A similar question in title is this 'write async function with EventEmitter' but it has nothing to do with this question.

Upvotes: 19

Views: 21513

Answers (2)

iGroza
iGroza

Reputation: 927

Here is a class that extends Node.js EventEmitter to support async event listeners and provides a function to wait for an event to be processed completely before proceeding with the rest of your code:

import EventEmitter from 'events';

export const EventResolverSymbol = Symbol.for('EventResolver');

export class AsyncEventEmitter extends EventEmitter {
  private listenerMap = new WeakMap<
    (...args: any[]) => void,
    (...args: any[]) => void
  >();

  emitAsync(eventName: string | symbol, ...args: any[]): Promise<void> {
    return new Promise(async resolve => {
      resolve.prototype.key = EventResolverSymbol;
      super.emit(eventName, ...args, resolve);
    });
  }

  on(eventName: string | symbol, listener: (...args: any[]) => void): this {
    const wrappedListener = async (...args: any[]) => {
      // check if event called from `awaitForEventDone` function
      if (args?.length) {
        const resolver = args[args.length - 1];
        if (
          typeof resolver === 'function' &&
          resolver.prototype.key === EventResolverSymbol
        ) {
          try {
            await listener(...args);
          } catch (e) {}
          return await resolver();
        }
      }
      return await listener(...args);
    };
    this.listenerMap.set(listener, wrappedListener);
    return super.on(eventName, wrappedListener);
  }

  once(eventName: string | symbol, listener: (...args: any[]) => void): this {
    const wrappedListener = async (...args: any[]) => {
      if (args?.length) {
        const resolver = args[args.length - 1];
        if (
          typeof resolver === 'function' &&
          resolver.prototype.key === EventResolverSymbol
        ) {
          try {
            await listener(...args);
          } catch (e) {}
          // remove listeners after the event is done
          this.removeListener(eventName, listener);
          this.removeListener(eventName, wrappedListener);
          return await resolver();
        }
      }
      // remove listeners after the event is done
      this.removeListener(eventName, listener);
      this.removeListener(eventName, wrappedListener);
      return await listener(...args);
    };
    this.listenerMap.set(listener, wrappedListener);
    return super.once(eventName, wrappedListener);
  }

  removeListener(
    eventName: string | symbol,
    listener: (...args: any[]) => void,
  ): this {
    const wrappedListener = this.listenerMap.get(listener);
    if (wrappedListener) {
      this.listenerMap.delete(listener);
      return super.removeListener(eventName, wrappedListener);
    }

    return this;
  }

  off = this.removeListener;
}

Example

 // Create a new async event emitter
 const emitter = new AsyncEventEmitter();
 
 // Define an async event listener
 const listener = async (message: string) => {
   console.log('event start with param: ', message);
   await sleep(5000);
 };
 
 // Listen for 'greet' event
 emitter.on('TestEvent', listener);
 
 // Emit the 'greet' event and wait for it to be done
 emitter.emitAsync('TestEvent', 'Hello World!');
    // will be call after 5000ms delay
   .then(() => console.log('event done'));

Upvotes: 0

Bergi
Bergi

Reputation: 665456

Can we end an async function from a callback passed to an event emitter without promisifing the event emitter?

No. async/await syntax is just sugar for then calls and relies on promises.

async function bakeMeSomeBurgers () {
  let canIHave = await canIHazACheezBurger();
  if (canIHave)
    console.log('Hehe, you can have...');
  else
    console.log('NOPE');

  // Here we create and await our promise:
  await new Promise((resolve, reject) => {
    // Here invoke our event emitter:
    let cook = new BurgerCooking('cheez');
    // a normal event callback:
    cook.on('update', percent => {
      console.log(`The burger is ${percent}% done`);
    });
    cook.on('end', resolve); // call resolve when its done
    cook.on('error', reject); // don't forget this
  });

  console.log('I\'ve finished the burger!');
  if (canIHave)
    console.log('Here, take it :)');
  else
    console.log('Too bad, you can\'t have it >:)');
}

Upvotes: 34

Related Questions