diff --git a/lib/decorators/on-event.decorator.ts b/lib/decorators/on-event.decorator.ts index 36c04ec0..469a660c 100644 --- a/lib/decorators/on-event.decorator.ts +++ b/lib/decorators/on-event.decorator.ts @@ -1,4 +1,4 @@ -import { SetMetadata } from '@nestjs/common'; +import { extendArrayMetadata } from '@nestjs/common/utils/extend-metadata.util'; import { EVENT_LISTENER_METADATA } from '../constants'; import { OnEventOptions } from '../interfaces'; @@ -16,14 +16,29 @@ export interface OnEventMetadata { options?: OnEventOptions; } +/** + * `@OnEvent` decorator event type + */ +export type OnEventType = string | symbol | Array; + /** * Event listener decorator. * Subscribes to events based on the specified name(s). * - * @param name event to subscribe to + * @param event event to subscribe to */ export const OnEvent = ( - event: string | symbol | Array, + event: OnEventType, options?: OnEventOptions, -): MethodDecorator => - SetMetadata(EVENT_LISTENER_METADATA, { event, options } as OnEventMetadata); +): MethodDecorator => { + const decoratorFactory = (target: object, key?: any, descriptor?: any) => { + extendArrayMetadata( + EVENT_LISTENER_METADATA, + [{ event, options } as OnEventMetadata], + descriptor.value, + ); + return descriptor; + }; + decoratorFactory.KEY = EVENT_LISTENER_METADATA; + return decoratorFactory; +}; diff --git a/lib/event-subscribers.loader.ts b/lib/event-subscribers.loader.ts index 5649201b..fd11a155 100644 --- a/lib/event-subscribers.loader.ts +++ b/lib/event-subscribers.loader.ts @@ -70,31 +70,32 @@ export class EventSubscribersLoader isRequestScoped: boolean, moduleRef: Module, ) { - const eventListenerMetadata = this.metadataAccessor.getEventHandlerMetadata( - instance[methodKey], - ); - if (!eventListenerMetadata) { + const eventListenerMetadatas = + this.metadataAccessor.getEventHandlerMetadata(instance[methodKey]); + if (!eventListenerMetadatas) { return; } - const { event, options } = eventListenerMetadata; - const listenerMethod = this.getRegisterListenerMethodBasedOn(options); - - if (isRequestScoped) { - this.registerRequestScopedListener({ - event, - eventListenerInstance: instance, - listenerMethod, - listenerMethodKey: methodKey, - moduleRef, - options, - }); - } else { - listenerMethod( - event, - (...args: unknown[]) => instance[methodKey].call(instance, ...args), - options, - ); + for (const eventListenerMetadata of eventListenerMetadatas) { + const { event, options } = eventListenerMetadata; + const listenerMethod = this.getRegisterListenerMethodBasedOn(options); + + if (isRequestScoped) { + this.registerRequestScopedListener({ + event, + eventListenerInstance: instance, + listenerMethod, + listenerMethodKey: methodKey, + moduleRef, + options, + }); + } else { + listenerMethod( + event, + (...args: unknown[]) => instance[methodKey].call(instance, ...args), + options, + ); + } } } @@ -151,12 +152,12 @@ export class EventSubscribersLoader **Required explanation for the ternary below** We need the conditional below because an event can be emitted with a variable amount of arguments. - For instance, we can do `this.eventEmitter.emit('event', 'payload1', 'payload2', ..., 'payloadN');` - + For instance, we can do `this.eventEmitter.emit('event', 'payload1', 'payload2', ..., 'payloadN');` + All payload arguments are internally stored as an array. So, imagine we emitted an event as follows: `this.eventEmitter.emit('event', 'payload'); - + if we registered the original `eventPayload`, when we try to inject it in a listener, it'll be retrieved as [`payload`]. However, whoever is using this library would certainly expect the event payload to be a single string 'payload', not an array, since this is what we emitted above. diff --git a/lib/events-metadata.accessor.ts b/lib/events-metadata.accessor.ts index d6d8ce10..194a3a4a 100644 --- a/lib/events-metadata.accessor.ts +++ b/lib/events-metadata.accessor.ts @@ -7,7 +7,9 @@ import { OnEventMetadata } from './decorators'; export class EventsMetadataAccessor { constructor(private readonly reflector: Reflector) {} - getEventHandlerMetadata(target: Type): OnEventMetadata | undefined { + getEventHandlerMetadata( + target: Type, + ): OnEventMetadata[] | undefined { return this.reflector.get(EVENT_LISTENER_METADATA, target); } } diff --git a/tests/e2e/module-e2e.spec.ts b/tests/e2e/module-e2e.spec.ts index 8dfea348..fee4672d 100644 --- a/tests/e2e/module-e2e.spec.ts +++ b/tests/e2e/module-e2e.spec.ts @@ -32,8 +32,15 @@ describe('EventEmitterModule - e2e', () => { expect(eventsConsumerRef.eventPayload).toEqual(TEST_EVENT_PAYLOAD); }); + it(`should emit a "stacked-event" event to providers`, async () => { + const eventsConsumerRef = app.get(EventsProviderConsumer); + await app.init(); + + expect(eventsConsumerRef.stackedEventCalls).toEqual(2); + }); + it(`aliased providers should receive an event only once`, async () => { - const eventsConsumerRef = app.get(EventsProviderAliasedConsumer); + const eventsConsumerRef = app.get(EventsProviderAliasedConsumer); const eventSpy = jest.spyOn(eventsConsumerRef, 'eventPayload', 'set'); await app.init(); diff --git a/tests/src/events-provider.consumer.ts b/tests/src/events-provider.consumer.ts index 1e7de3f9..d5688638 100644 --- a/tests/src/events-provider.consumer.ts +++ b/tests/src/events-provider.consumer.ts @@ -4,9 +4,16 @@ import { OnEvent } from '../../lib'; @Injectable() export class EventsProviderConsumer { public eventPayload = {}; + public stackedEventCalls = 0; @OnEvent('test.*') onTestEvent(payload: Record) { this.eventPayload = payload; } + + @OnEvent('stacked1.*') + @OnEvent('stacked2.*') + onStackedEvent() { + this.stackedEventCalls++; + } } diff --git a/tests/src/events.producer.ts b/tests/src/events.producer.ts index 98423118..9cbc0346 100644 --- a/tests/src/events.producer.ts +++ b/tests/src/events.producer.ts @@ -14,5 +14,7 @@ export class EventsProducer implements OnApplicationBootstrap { this.eventEmitter.emit('test.event', TEST_EVENT_PAYLOAD); this.eventEmitter.emit('multiple.event', TEST_EVENT_MULTIPLE_PAYLOAD); this.eventEmitter.emit('string.event', TEST_EVENT_STRING_PAYLOAD); + this.eventEmitter.emit('stacked1.event', TEST_EVENT_PAYLOAD); + this.eventEmitter.emit('stacked2.event', TEST_EVENT_PAYLOAD); } }