Handle EventEmitterModule wildcards - NestJS

Viewed 831

I don't know how to handle the following emitter:

async create(createJobDto: CreateJobDto)  {
    this.eventEmitter.emit(
        ['job','create'],
        createJobDto,
    );
}

async update(createJobDto: CreateJobDto)  {
    this.eventEmitter.emit(
        ['job','update'],
        createJobDto,
    );
}

Listener:

@OnEvent('job.**')
handleJobEvent(data: CreateJobDto) {
    console.log(data);
});

The NestJS and the EventEmitter2 docs mention that I can use an array as a wildcard, but how can I distinguish between "create" and "update" in the listener? All that I seem to get in data is the job object.

P.S. I am correctly including the EventEmitterModule in my imports:

EventEmitterModule.forRoot({
  wildcard: true
}),
5 Answers

I think that's not possible. You have to create two different listeners:

@OnEvent('job.create')
handleJobCreateEvent(data: CreateJobDto) {
    console.log(data);
});

@OnEvent('job.update')
handleJobUpdateEvent(data: CreateJobDto) {
    console.log(data);
});

For this purpose, we have an emitter.event property that identifies the currently fired event. So the NestJS emitter wrapper should have it too. Here's an example from the docs:

emitter.on('foo.*', function(value1, value2) {
  console.log(this.event, value1, value2);
});

emitter.emit('foo.bar', 1, 2); // 'foo.bar' 1 2
emitter.emit(['foo', 'bar'], 3, 4); // 'foo.bar' 3 4

emitter.emit(Symbol(), 5, 6); // Symbol() 5 6
emitter.emit(['foo', Symbol()], 7, 8); // ['foo', Symbol()] 7 8

An alternative is to override the emit function using a wrapper provider or create an enhanced version of the emitter, that will add the event name as the first/last value or modify the first parameter, however you like.

For example:

import { Global, Module, Provider } from '@nestjs/common';
import { EventEmitter2, EventEmitterModule as EventEmitterModule2 } from '@nestjs/event-emitter';
import { event, eventNS } from 'eventemitter2';
import * as _ from 'lodash';

export const EVENT_EMITTER = 'EventEmitter';

export interface EventEmitter extends EventEmitter2 {
  emitWithName: (event: event | eventNS, ...values: any[]) => boolean; //copy of the signature of emit function
}

const eventEmitterFactory: Provider = {
  provide: EVENT_EMITTER,
  useFactory: (eventEmitter2: EventEmitter2) => {
    const emitWithName = (event: event | eventNS, ...values: any[]): boolean => {
      if (_.isObject(values[0])) {
        values[0].event = event;
      }

      const valuesWithName = [...values, event];

      const result = eventEmitter2.emit(event, ...valuesWithName);
      return result;
    };

    Object.assign(eventEmitter2, { emitWithName });
    return eventEmitter2;
  },
  inject: [EventEmitter2],
};

@Global() //because EventEmitterModule is a global dynamic module
@Module({
  imports: [
    EventEmitterModule2.forRoot({
      wildcard: true,
    }),
  ],
  providers: [eventEmitterFactory],
  exports: [eventEmitterFactory],
})
export class EventEmitterModule {}

In app.module you need to import this module, and to inject the provider:

@Inject(EVENT_EMITTER) private readonly eventEmitter: EventEmitter

I tried to do something very similar but with patterns and apparently, it's not possible right now. So I'd say use a different decorator-line for each one.

Like

@OnEvent('job.create')
...
...
...
@OnEvent('job.update')

Also good to keep an eye on the opened thread: https://github.com/nestjs/nest/issues/3083

Related