diff --git a/lib/event-emitter-readiness.watcher.ts b/lib/event-emitter-readiness.watcher.ts new file mode 100644 index 00000000..eecc3a38 --- /dev/null +++ b/lib/event-emitter-readiness.watcher.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { promiseWithResolvers } from './utils/promise-with-resolvers'; + +@Injectable() +export class EventEmitterReadinessWatcher { + private readonly readyPromise = promiseWithResolvers(); + + waitUntilReady() { + return this.readyPromise.promise; + } + + setReady() { + this.readyPromise.resolve(); + } + + setErrored(error: Error) { + this.readyPromise.reject(error); + } +} diff --git a/lib/event-emitter.module.ts b/lib/event-emitter.module.ts index 6a1b30c1..5bb9881a 100644 --- a/lib/event-emitter.module.ts +++ b/lib/event-emitter.module.ts @@ -1,6 +1,7 @@ import { DynamicModule, Module } from '@nestjs/common'; import { DiscoveryModule } from '@nestjs/core'; import { EventEmitter2 } from 'eventemitter2'; +import { EventEmitterReadinessWatcher } from './event-emitter-readiness.watcher'; import { EventSubscribersLoader } from './event-subscribers.loader'; import { EventsMetadataAccessor } from './events-metadata.accessor'; import { EventEmitterModuleOptions } from './interfaces'; @@ -15,12 +16,13 @@ export class EventEmitterModule { providers: [ EventSubscribersLoader, EventsMetadataAccessor, + EventEmitterReadinessWatcher, { provide: EventEmitter2, useValue: new EventEmitter2(options), }, ], - exports: [EventEmitter2], + exports: [EventEmitter2, EventEmitterReadinessWatcher], }; } } diff --git a/lib/event-subscribers.loader.ts b/lib/event-subscribers.loader.ts index b30accaf..af561542 100644 --- a/lib/event-subscribers.loader.ts +++ b/lib/event-subscribers.loader.ts @@ -17,6 +17,7 @@ import { } from '@nestjs/core/injector/instance-wrapper'; import { Module } from '@nestjs/core/injector/module'; import { EventEmitter2 } from 'eventemitter2'; +import { EventEmitterReadinessWatcher } from './event-emitter-readiness.watcher'; import { EventsMetadataAccessor } from './events-metadata.accessor'; import { OnEventOptions } from './interfaces'; @@ -33,10 +34,16 @@ export class EventSubscribersLoader private readonly metadataAccessor: EventsMetadataAccessor, private readonly metadataScanner: MetadataScanner, private readonly moduleRef: ModuleRef, + private readonly eventEmitterReadinessWatcher: EventEmitterReadinessWatcher, ) {} onApplicationBootstrap() { - this.loadEventListeners(); + try { + this.loadEventListeners(); + this.eventEmitterReadinessWatcher.setReady(); + } catch (e) { + this.eventEmitterReadinessWatcher.setErrored(e as Error); + } } onApplicationShutdown() { @@ -95,7 +102,12 @@ export class EventSubscribersLoader listenerMethod( event, (...args: unknown[]) => - this.wrapFunctionInTryCatchBlocks(instance, methodKey, args, options), + this.wrapFunctionInTryCatchBlocks( + instance, + methodKey, + args, + options, + ), options, ); } diff --git a/lib/index.ts b/lib/index.ts index d46ed19f..b30e805e 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,5 @@ export { EventEmitter2 } from 'eventemitter2'; +export { EVENT_PAYLOAD } from './constants'; export * from './decorators'; +export * from './event-emitter-readiness.watcher'; export * from './event-emitter.module'; -export { EVENT_PAYLOAD } from './constants'; diff --git a/lib/utils/promise-with-resolvers.ts b/lib/utils/promise-with-resolvers.ts new file mode 100644 index 00000000..94f80223 --- /dev/null +++ b/lib/utils/promise-with-resolvers.ts @@ -0,0 +1,15 @@ +/** + * A polyfill for the Promise.withResolvers method that is not available in older versions of Node.js + * @returns A promise and its resolve and reject functions + */ +export function promiseWithResolvers() { + let resolve: () => void; + let reject: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + // @ts-expect-error "resolve" and "reject" are assigned in the promise constructor + return { promise, resolve, reject }; +} diff --git a/tests/e2e/module-e2e.spec.ts b/tests/e2e/module-e2e.spec.ts index a6d78e2c..6061e4f6 100644 --- a/tests/e2e/module-e2e.spec.ts +++ b/tests/e2e/module-e2e.spec.ts @@ -1,6 +1,7 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { EventEmitter2 } from 'eventemitter2'; +import { EventEmitterReadinessWatcher } from '../../lib'; import { AppModule } from '../src/app.module'; import { TEST_EVENT_MULTIPLE_PAYLOAD, @@ -157,7 +158,9 @@ describe('EventEmitterModule - e2e', () => { await app.init(); const eventEmitter = app.get(EventEmitter2); - const result = eventEmitter.emit('error-handling-suppressed.request-scoped'); + const result = eventEmitter.emit( + 'error-handling-suppressed.request-scoped', + ); expect(result).toBeTruthy(); }); @@ -166,14 +169,27 @@ describe('EventEmitterModule - e2e', () => { await app.init(); const eventEmitter = app.get(EventEmitter2); - expect(eventEmitter.emitAsync('error-throwing.provider')).rejects.toThrow("This is a test error"); + expect(eventEmitter.emitAsync('error-throwing.provider')).rejects.toThrow( + 'This is a test error', + ); }); it('should throw when an unexpected error occurs from request scoped and suppressErrors is false', async () => { await app.init(); const eventEmitter = app.get(EventEmitter2); - expect(eventEmitter.emitAsync('error-throwing.request-scoped')).rejects.toThrow("This is a test error"); + expect( + eventEmitter.emitAsync('error-throwing.request-scoped'), + ).rejects.toThrow('This is a test error'); + }); + + it('should be able to wait until the event emitter is ready', async () => { + const eventsConsumerRef = app.get(EventsControllerConsumer); + await app.init(); + + const eventEmitterWatcher = app.get(EventEmitterReadinessWatcher); + await expect(eventEmitterWatcher.waitUntilReady()).resolves.toBeUndefined(); + expect(eventsConsumerRef.eventPayload).toEqual(TEST_EVENT_PAYLOAD); }); afterEach(async () => {