Skip to content

Commit

Permalink
feat(): add first version of request scoped listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
thiagomini committed Jul 7, 2022
1 parent ebacd0a commit 26426c2
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 11 deletions.
3 changes: 3 additions & 0 deletions lib/constants.ts
@@ -1 +1,4 @@
import { REQUEST } from '@nestjs/core';

export const EVENT_LISTENER_METADATA = 'EVENT_LISTENER_METADATA';
export const EVENT_PAYLOAD = REQUEST;
57 changes: 48 additions & 9 deletions lib/event-subscribers.loader.ts
Expand Up @@ -3,20 +3,30 @@ import {
OnApplicationBootstrap,
OnApplicationShutdown,
} from '@nestjs/common';
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
import {
ContextIdFactory,
DiscoveryService,
MetadataScanner,
ModuleRef,
} from '@nestjs/core';
import { Injector } from '@nestjs/core/injector/injector';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { Module } from '@nestjs/core/injector/module';
import { EventEmitter2 } from 'eventemitter2';
import { EventsMetadataAccessor } from './events-metadata.accessor';

@Injectable()
export class EventSubscribersLoader
implements OnApplicationBootstrap, OnApplicationShutdown
{
private injector = new Injector();

constructor(
private readonly discoveryService: DiscoveryService,
private readonly eventEmitter: EventEmitter2,
private readonly metadataAccessor: EventsMetadataAccessor,
private readonly metadataScanner: MetadataScanner,
private readonly moduleRef: ModuleRef,
) {}

onApplicationBootstrap() {
Expand All @@ -31,23 +41,30 @@ export class EventSubscribersLoader
const providers = this.discoveryService.getProviders();
const controllers = this.discoveryService.getControllers();
[...providers, ...controllers]
.filter(wrapper => wrapper.isDependencyTreeStatic())
.filter(wrapper => wrapper.instance)
.forEach((wrapper: InstanceWrapper) => {
const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance) || {};
const isRequestScoped = !wrapper.isDependencyTreeStatic();
this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodKey: string) =>
this.subscribeToEventIfListener(instance, methodKey),
this.subscribeToEventIfListener(
instance,
methodKey,
isRequestScoped,
wrapper.host as Module,
),
);
});
}

private subscribeToEventIfListener(
private async subscribeToEventIfListener(
instance: Record<string, any>,
methodKey: string,
isRequestScoped: boolean,
moduleRef: Module,
) {
const eventListenerMetadata = this.metadataAccessor.getEventHandlerMetadata(
instance[methodKey],
Expand All @@ -60,10 +77,32 @@ export class EventSubscribersLoader
? this.eventEmitter.prependListener.bind(this.eventEmitter)
: this.eventEmitter.on.bind(this.eventEmitter);

listenerMethod(
event,
(...args: unknown[]) => instance[methodKey].call(instance, ...args),
options,
);
if (isRequestScoped) {
listenerMethod(
event,
async (...args: unknown[]) => {
const contextId = ContextIdFactory.create();
this.moduleRef.registerRequestByContextId(
args.length > 1 ? args : args[0],
contextId,
);

const contextInstance = await this.injector.loadPerContext(
instance,
moduleRef,
moduleRef.providers,
contextId,
);
return contextInstance[methodKey].call(contextInstance, ...args);
},
options,
);
} else {
listenerMethod(
event,
(...args: unknown[]) => instance[methodKey].call(instance, ...args),
options,
);
}
}
}
1 change: 1 addition & 0 deletions lib/index.ts
@@ -1,3 +1,4 @@
export { EventEmitter2 } from 'eventemitter2';
export * from './decorators';
export * from './event-emitter.module';
export { EVENT_PAYLOAD as EVENT_REF } from './constants';
30 changes: 29 additions & 1 deletion tests/e2e/module-e2e.spec.ts
Expand Up @@ -5,7 +5,9 @@ import { AppModule } from '../src/app.module';
import { EventsControllerConsumer } from '../src/events-controller.consumer';
import { EventsProviderPrependConsumer } from '../src/events-provider-prepend.consumer';
import { EventsProviderConsumer } from '../src/events-provider.consumer';
import { EventsProviderRequestScopedConsumer } from '../src/events-provider.request-scoped.consumer';
import { TEST_PROVIDER_TOKEN } from '../src/test-provider';
import { EVENT_REF } from '../../lib';

describe('EventEmitterModule - e2e', () => {
let app: INestApplication;
Expand Down Expand Up @@ -34,7 +36,10 @@ describe('EventEmitterModule - e2e', () => {

it('should be able to specify a consumer be prepended via OnEvent decorator options', async () => {
const eventsConsumerRef = app.get(EventsProviderPrependConsumer);
const prependListenerSpy = jest.spyOn(app.get(EventEmitter2), 'prependListener');
const prependListenerSpy = jest.spyOn(
app.get(EventEmitter2),
'prependListener',
);
await app.init();

expect(eventsConsumerRef.eventPayload).toEqual({ test: 'event' });
Expand All @@ -58,6 +63,29 @@ describe('EventEmitterModule - e2e', () => {
await expect(app.init()).resolves.not.toThrow();
});

it('should be able to emit a request-scoped event with a single payload', async () => {
await app.init();

expect(EventsProviderRequestScopedConsumer.injectedEventPayload).toEqual({
test: 'event',
});
});

it('should be able to emit a request-scoped event with multiple payloads', async () => {
await app.init();

expect(
EventsProviderRequestScopedConsumer.injectedEventMultiPayload,
).toEqual([
{
test: 'event',
},
{
test2: 'event2',
},
]);
});

afterEach(async () => {
await app.close();
});
Expand Down
2 changes: 2 additions & 0 deletions tests/src/app.module.ts
Expand Up @@ -3,6 +3,7 @@ import { EventEmitterModule } from '../../lib';
import { EventsControllerConsumer } from './events-controller.consumer';
import { EventsProviderPrependConsumer } from './events-provider-prepend.consumer';
import { EventsProviderConsumer } from './events-provider.consumer';
import { EventsProviderRequestScopedConsumer } from './events-provider.request-scoped.consumer';
import { EventsProducer } from './events.producer';
import { TestProvider } from './test-provider';

Expand All @@ -18,6 +19,7 @@ import { TestProvider } from './test-provider';
EventsProviderPrependConsumer,
EventsProducer,
TestProvider,
EventsProviderRequestScopedConsumer,
],
})
export class AppModule {}
24 changes: 24 additions & 0 deletions tests/src/events-provider.request-scoped.consumer.ts
@@ -0,0 +1,24 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '../../lib';
import { EVENT_REF } from '../../lib';

@Injectable()
export class EventsProviderRequestScopedConsumer {
constructor(@Inject(EVENT_REF) public eventRef: any) {
if (Array.isArray(this.eventRef)) {
EventsProviderRequestScopedConsumer.injectedEventMultiPayload =
this.eventRef;
} else {
EventsProviderRequestScopedConsumer.injectedEventPayload = this.eventRef;
}
}

public static injectedEventPayload = {};
public static injectedEventMultiPayload: any[] = [];

@OnEvent('test.*')
onTestEvent() {}

@OnEvent('multiple.*')
onMultiplePayloadEvent() {}
}
5 changes: 5 additions & 0 deletions tests/src/events.producer.ts
Expand Up @@ -7,5 +7,10 @@ export class EventsProducer implements OnApplicationBootstrap {

onApplicationBootstrap() {
this.eventEmitter.emit('test.event', { test: 'event' });
this.eventEmitter.emit(
'multiple.event',
{ test: 'event' },
{ test2: 'event2' },
);
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Expand Up @@ -9,7 +9,7 @@
"esModuleInterop": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": false,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./lib",
"skipLibCheck": true
Expand Down

0 comments on commit 26426c2

Please sign in to comment.