Skip to content

Commit

Permalink
Merge pull request #657 from NtTestAlert/feat/event-array-subscriptions
Browse files Browse the repository at this point in the history
feat: array subscriptions
  • Loading branch information
kamilmysliwiec committed Feb 1, 2023
2 parents 062a9f0 + 65f5f49 commit 039292e
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 32 deletions.
25 changes: 20 additions & 5 deletions 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';

Expand All @@ -16,14 +16,29 @@ export interface OnEventMetadata {
options?: OnEventOptions;
}

/**
* `@OnEvent` decorator event type
*/
export type OnEventType = string | symbol | Array<string | symbol>;

/**
* 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<string | symbol>,
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;
};
51 changes: 26 additions & 25 deletions lib/event-subscribers.loader.ts
Expand Up @@ -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,
);
}
}
}

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion lib/events-metadata.accessor.ts
Expand Up @@ -7,7 +7,9 @@ import { OnEventMetadata } from './decorators';
export class EventsMetadataAccessor {
constructor(private readonly reflector: Reflector) {}

getEventHandlerMetadata(target: Type<unknown>): OnEventMetadata | undefined {
getEventHandlerMetadata(
target: Type<unknown>,
): OnEventMetadata[] | undefined {
return this.reflector.get(EVENT_LISTENER_METADATA, target);
}
}
9 changes: 8 additions & 1 deletion tests/e2e/module-e2e.spec.ts
Expand Up @@ -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();

Expand Down
7 changes: 7 additions & 0 deletions tests/src/events-provider.consumer.ts
Expand Up @@ -4,9 +4,16 @@ import { OnEvent } from '../../lib';
@Injectable()
export class EventsProviderConsumer {
public eventPayload = {};
public stackedEventCalls = 0;

@OnEvent('test.*')
onTestEvent(payload: Record<string, any>) {
this.eventPayload = payload;
}

@OnEvent('stacked1.*')
@OnEvent('stacked2.*')
onStackedEvent() {
this.stackedEventCalls++;
}
}
2 changes: 2 additions & 0 deletions tests/src/events.producer.ts
Expand Up @@ -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);
}
}

0 comments on commit 039292e

Please sign in to comment.