├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── lib ├── decorators │ ├── index.ts │ └── on-event.decorator.ts ├── interfaces │ ├── event-payload-host.interface.ts │ ├── index.ts │ ├── event-emitter-options.interface.ts │ └── on-event-options.interface.ts ├── constants.ts ├── index.ts ├── event-emitter-readiness.watcher.ts ├── utils │ └── promise-with-resolvers.ts ├── events-metadata.accessor.ts ├── event-emitter.module.ts └── event-subscribers.loader.ts ├── .prettierrc ├── tests ├── src │ ├── custom-decorator-test.constants.ts │ ├── constants.ts │ ├── events-controller.consumer.ts │ ├── test-provider.ts │ ├── events-provider-prepend.consumer.ts │ ├── custom-decorator-test.consumer.ts │ ├── custom-event.decorator.ts │ ├── events-provider-aliased.consumer.ts │ ├── request-scoped-event-payload.ts │ ├── events.producer.ts │ ├── events-provider.consumer.ts │ ├── events-provider.request-scoped.consumer.ts │ ├── app.module.ts │ └── events-provider.durable-request-scoped.consumer.ts ├── jest-e2e.json └── e2e │ └── module-e2e.spec.ts ├── .release-it.json ├── renovate.json ├── .npmignore ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── Feature_request.yml │ ├── Regression.yml │ └── Bug_report.yml └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── .commitlintrc.json ├── LICENSE ├── .circleci └── config.yml ├── eslint.config.mjs ├── package.json ├── README.md └── CONTRIBUTING.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install lint-staged 2 | -------------------------------------------------------------------------------- /lib/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './on-event.decorator'; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint -c --config .commitlintrc.json -E --edit $1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /lib/interfaces/event-payload-host.interface.ts: -------------------------------------------------------------------------------- 1 | export type EventPayloadHost = { 2 | payload: T; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/src/custom-decorator-test.constants.ts: -------------------------------------------------------------------------------- 1 | export const CUSTOM_DECORATOR_EVENT = 'custom-decorator-event'; 2 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore(): release v${version}" 4 | }, 5 | "github": { 6 | "release": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { REQUEST } from '@nestjs/core'; 2 | 3 | export const EVENT_LISTENER_METADATA = 'EVENT_LISTENER_METADATA'; 4 | export const EVENT_PAYLOAD = REQUEST; 5 | -------------------------------------------------------------------------------- /lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-emitter-options.interface'; 2 | export * from './on-event-options.interface'; 3 | export * from './event-payload-host.interface'; 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "semanticCommits": true, 3 | "packageRules": [{ 4 | "depTypeList": ["devDependencies"], 5 | "automerge": true 6 | }], 7 | "extends": [ 8 | "config:base" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | tests 4 | index.ts 5 | package-lock.json 6 | tslint.json 7 | tsconfig.json 8 | .prettierrc 9 | 10 | # github 11 | .github 12 | CONTRIBUTING.md 13 | renovate.json 14 | 15 | # ci 16 | .circleci -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | 9 | # misc 10 | npm-debug.log 11 | .DS_Store 12 | 13 | # tests 14 | /test 15 | /coverage 16 | /.nyc_output 17 | 18 | # dist 19 | dist -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export { EventEmitter2 } from 'eventemitter2'; 2 | export { EVENT_PAYLOAD } from './constants'; 3 | export * from './decorators'; 4 | export * from './event-emitter-readiness.watcher'; 5 | export * from './event-emitter.module'; 6 | -------------------------------------------------------------------------------- /tests/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const TEST_EVENT_PAYLOAD = { 2 | test: 'event', 3 | }; 4 | 5 | export const TEST_EVENT_MULTIPLE_PAYLOAD = [ 6 | TEST_EVENT_PAYLOAD, 7 | { 8 | test2: 'event2', 9 | }, 10 | ]; 11 | 12 | export const TEST_EVENT_STRING_PAYLOAD = 'some-string'; 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | ## To encourage contributors to use issue templates, we don't allow blank issues 2 | blank_issues_enabled: false 3 | 4 | contact_links: 5 | - name: "\u2753 Discord Community of NestJS" 6 | url: "https://discord.gg/NestJS" 7 | about: "Please ask support questions or discuss suggestions/enhancements here." 8 | -------------------------------------------------------------------------------- /tests/src/events-controller.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Injectable } from '@nestjs/common'; 2 | import { OnEvent } from '../../lib'; 3 | 4 | @Controller() 5 | export class EventsControllerConsumer { 6 | public eventPayload = {}; 7 | 8 | @OnEvent('test.*') 9 | onTestEvent(payload: Record) { 10 | this.eventPayload = payload; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/src/test-provider.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from '@nestjs/common'; 2 | 3 | export const TEST_PROVIDER_TOKEN = 'TEST_PROVIDER'; 4 | 5 | export const TestProvider: Provider = { 6 | provide: TEST_PROVIDER_TOKEN, 7 | useFactory: () => { 8 | const testProvidedValue = { 9 | a: 5, 10 | b: 7, 11 | }; 12 | return testProvidedValue; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /tests/src/events-provider-prepend.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OnEvent } from '../../lib'; 3 | 4 | @Injectable() 5 | export class EventsProviderPrependConsumer { 6 | public eventPayload = {}; 7 | 8 | @OnEvent('test.*', { prependListener: true }) 9 | onTestEvent(payload: Record) { 10 | this.eventPayload = payload; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/src/custom-decorator-test.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CUSTOM_DECORATOR_EVENT } from './custom-decorator-test.constants'; 3 | import { CustomEvent } from './custom-event.decorator'; 4 | 5 | @Injectable() 6 | export class CustomEventDecoratorConsumer { 7 | public isEmitted = false; 8 | 9 | @CustomEvent(CUSTOM_DECORATOR_EVENT) 10 | handleCustomEvent() { 11 | this.isEmitted = true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/src/custom-event.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | import { OnEventMetadata } from '../../lib'; 4 | import { EVENT_LISTENER_METADATA } from '../../lib/constants'; 5 | 6 | import type { OnEventOptions } from '../../lib/interfaces'; 7 | 8 | export const CustomEvent = (event: string, options?: OnEventOptions) => 9 | SetMetadata(EVENT_LISTENER_METADATA, { 10 | event, 11 | options, 12 | } as OnEventMetadata); 13 | -------------------------------------------------------------------------------- /lib/interfaces/event-emitter-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ConstructorOptions } from 'eventemitter2'; 2 | 3 | /** 4 | * Options for the `EventEmitterModule`. 5 | * 6 | * @publicApi 7 | */ 8 | export interface EventEmitterModuleOptions extends ConstructorOptions { 9 | /** 10 | * If "true", registers `EventEmitterModule` as a global module. 11 | * See: https://docs.nestjs.com/modules#global-modules 12 | * 13 | * @default true 14 | */ 15 | global?: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /tests/src/events-provider-aliased.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OnEvent } from '../../lib'; 3 | 4 | @Injectable() 5 | export class EventsProviderAliasedConsumer { 6 | private _eventPayload = {}; 7 | 8 | set eventPayload(value) { 9 | this._eventPayload = value; 10 | } 11 | 12 | get eventPayload() { 13 | return this._eventPayload; 14 | } 15 | 16 | @OnEvent('test.*') 17 | onTestEvent(payload: Record) { 18 | this.eventPayload = payload; 19 | } 20 | } -------------------------------------------------------------------------------- /lib/event-emitter-readiness.watcher.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { promiseWithResolvers } from './utils/promise-with-resolvers'; 3 | 4 | @Injectable() 5 | export class EventEmitterReadinessWatcher { 6 | private readonly readyPromise = promiseWithResolvers(); 7 | 8 | waitUntilReady() { 9 | return this.readyPromise.promise; 10 | } 11 | 12 | setReady() { 13 | this.readyPromise.resolve(); 14 | } 15 | 16 | setErrored(error: Error) { 17 | this.readyPromise.reject(error); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "strict": true, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "esModuleInterop": true, 10 | "experimentalDecorators": true, 11 | "target": "ES2021", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "rootDir": "./lib", 15 | "skipLibCheck": true 16 | }, 17 | "include": ["lib/**/*"], 18 | "exclude": ["node_modules", "**/*.spec.ts", "tests"] 19 | } 20 | -------------------------------------------------------------------------------- /lib/utils/promise-with-resolvers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A polyfill for the Promise.withResolvers method that is not available in older versions of Node.js 3 | * @returns A promise and its resolve and reject functions 4 | */ 5 | export function promiseWithResolvers() { 6 | let resolve: () => void; 7 | let reject: (reason?: any) => void; 8 | const promise = new Promise((res, rej) => { 9 | resolve = res; 10 | reject = rej; 11 | }); 12 | 13 | // @ts-expect-error "resolve" and "reject" are assigned in the promise constructor 14 | return { promise, resolve, reject }; 15 | } 16 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "always", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/interfaces/on-event-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { OnOptions } from 'eventemitter2'; 2 | 3 | /** 4 | * Options for the `@OnEvent` decorator. 5 | * 6 | * @publicApi 7 | */ 8 | export type OnEventOptions = OnOptions & { 9 | /** 10 | * If "true", prepends (instead of append) the given listener to the array of listeners. 11 | * 12 | * @see https://github.com/EventEmitter2/EventEmitter2#emitterprependlistenerevent-listener-options 13 | * 14 | * @default false 15 | */ 16 | prependListener?: boolean; 17 | 18 | /** 19 | * If "true", the onEvent callback will not throw an error while handling the event. Otherwise, if "false" it will throw an error. 20 | * 21 | * @default true 22 | */ 23 | suppressErrors?: boolean; 24 | }; 25 | -------------------------------------------------------------------------------- /tests/src/request-scoped-event-payload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Class used to test injected payloads on the RequestScoped listener. 3 | * Each value stored in the instance represents a different type of payload. 4 | */ 5 | export class RequestScopedEventPayload { 6 | public objectValue: Record; 7 | public arrayValue: any[]; 8 | public stringValue: string; 9 | 10 | constructor() { 11 | this.objectValue = {}; 12 | this.arrayValue = []; 13 | this.stringValue = ''; 14 | } 15 | 16 | public setPayload(value: any) { 17 | if (Array.isArray(value)) { 18 | this.arrayValue = value; 19 | } else if (typeof value === 'string') { 20 | this.stringValue = value; 21 | } else { 22 | this.objectValue = value; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/src/events.producer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; 2 | import { EventEmitter2 } from 'eventemitter2'; 3 | import { 4 | TEST_EVENT_MULTIPLE_PAYLOAD, 5 | TEST_EVENT_PAYLOAD, 6 | TEST_EVENT_STRING_PAYLOAD, 7 | } from './constants'; 8 | 9 | @Injectable() 10 | export class EventsProducer implements OnApplicationBootstrap { 11 | constructor(private readonly eventEmitter: EventEmitter2) {} 12 | 13 | onApplicationBootstrap() { 14 | this.eventEmitter.emit('test.event', TEST_EVENT_PAYLOAD); 15 | this.eventEmitter.emit('multiple.event', TEST_EVENT_MULTIPLE_PAYLOAD); 16 | this.eventEmitter.emit('string.event', TEST_EVENT_STRING_PAYLOAD); 17 | this.eventEmitter.emit('stacked1.event', TEST_EVENT_PAYLOAD); 18 | this.eventEmitter.emit('stacked2.event', TEST_EVENT_PAYLOAD); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/events-metadata.accessor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { EVENT_LISTENER_METADATA } from './constants'; 4 | import { OnEventMetadata } from './decorators'; 5 | 6 | @Injectable() 7 | export class EventsMetadataAccessor { 8 | constructor(private readonly reflector: Reflector) {} 9 | 10 | getEventHandlerMetadata( 11 | target: Type, 12 | ): OnEventMetadata[] | undefined { 13 | // Circumvent a crash that comes from reflect-metadata if it is 14 | // given a non-object non-function target to reflect upon. 15 | if ( 16 | !target || 17 | (typeof target !== 'function' && typeof target !== 'object') 18 | ) { 19 | return undefined; 20 | } 21 | 22 | const metadata = this.reflector.get(EVENT_LISTENER_METADATA, target); 23 | if (!metadata) { 24 | return undefined; 25 | } 26 | return Array.isArray(metadata) ? metadata : [metadata]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/src/events-provider.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OnEvent } from '../../lib'; 3 | 4 | @Injectable() 5 | export class EventsProviderConsumer { 6 | public eventPayload = {}; 7 | public stackedEventCalls = 0; 8 | public errorHandlingCalls = 0; 9 | 10 | @OnEvent('test.*') 11 | onTestEvent(payload: Record) { 12 | this.eventPayload = payload; 13 | } 14 | 15 | @OnEvent('stacked1.*') 16 | @OnEvent('stacked2.*') 17 | onStackedEvent() { 18 | this.stackedEventCalls++; 19 | } 20 | 21 | @OnEvent('error-handling.provider') 22 | onErrorHandlingEvent() { 23 | this.errorHandlingCalls++; 24 | throw new Error('This is a test error'); 25 | } 26 | 27 | @OnEvent('error-handling-suppressed.provider', { suppressErrors: true }) 28 | onErrorHandlingSuppressedEvent() { 29 | this.errorHandlingCalls++; 30 | throw new Error('This is a test error'); 31 | } 32 | 33 | @OnEvent('error-throwing.provider', { suppressErrors: false }) 34 | onErrorThrowingEvent() { 35 | throw new Error('This is a test error'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/event-emitter.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { DiscoveryModule } from '@nestjs/core'; 3 | import { EventEmitter2 } from 'eventemitter2'; 4 | import { EventEmitterReadinessWatcher } from './event-emitter-readiness.watcher'; 5 | import { EventSubscribersLoader } from './event-subscribers.loader'; 6 | import { EventsMetadataAccessor } from './events-metadata.accessor'; 7 | import { EventEmitterModuleOptions } from './interfaces'; 8 | 9 | /** 10 | * @publicApi 11 | */ 12 | @Module({}) 13 | export class EventEmitterModule { 14 | static forRoot(options?: EventEmitterModuleOptions): DynamicModule { 15 | return { 16 | global: options?.global ?? true, 17 | module: EventEmitterModule, 18 | imports: [DiscoveryModule], 19 | providers: [ 20 | EventSubscribersLoader, 21 | EventsMetadataAccessor, 22 | EventEmitterReadinessWatcher, 23 | { 24 | provide: EventEmitter2, 25 | useFactory: () => new EventEmitter2(options), 26 | }, 27 | ], 28 | exports: [EventEmitter2, EventEmitterReadinessWatcher], 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 Kamil Mysliwiec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | 9 | ## PR Type 10 | What kind of change does this PR introduce? 11 | 12 | 13 | - [ ] Bugfix 14 | - [ ] Feature 15 | - [ ] Code style update (formatting, local variables) 16 | - [ ] Refactoring (no functional changes, no api changes) 17 | - [ ] Build related changes 18 | - [ ] CI related changes 19 | - [ ] Other... Please describe: 20 | 21 | ## What is the current behavior? 22 | 23 | 24 | Issue Number: N/A 25 | 26 | 27 | ## What is the new behavior? 28 | 29 | 30 | ## Does this PR introduce a breaking change? 31 | - [ ] Yes 32 | - [ ] No 33 | 34 | 35 | 36 | 37 | ## Other information 38 | -------------------------------------------------------------------------------- /tests/src/events-provider.request-scoped.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { OnEvent } from '../../lib'; 3 | import { EVENT_PAYLOAD } from '../../lib'; 4 | import { RequestScopedEventPayload } from './request-scoped-event-payload'; 5 | 6 | @Injectable() 7 | export class EventsProviderRequestScopedConsumer { 8 | constructor(@Inject(EVENT_PAYLOAD) public eventRef: any) { 9 | EventsProviderRequestScopedConsumer.injectedEventPayload.setPayload( 10 | this.eventRef, 11 | ); 12 | } 13 | 14 | public static injectedEventPayload = new RequestScopedEventPayload(); 15 | 16 | @OnEvent('test.*') 17 | onTestEvent() {} 18 | 19 | @OnEvent('multiple.*') 20 | onMultiplePayloadEvent() {} 21 | 22 | @OnEvent('string.*') 23 | onStringPayloadEvent() {} 24 | 25 | @OnEvent('error-handling.request-scoped') 26 | onErrorHandlingEvent() { 27 | throw new Error('This is a test error'); 28 | } 29 | 30 | @OnEvent('error-handling-suppressed.request-scoped', { suppressErrors: true }) 31 | onErrorHandlingSuppressedEvent() { 32 | throw new Error('This is a test error'); 33 | } 34 | 35 | @OnEvent('error-throwing.request-scoped', { suppressErrors: false }) 36 | onErrorThrowingEvent() { 37 | throw new Error('This is a test error'); 38 | } 39 | 40 | @OnEvent('not-durable') 41 | onDurableTest() { 42 | return this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &restore-cache 5 | restore_cache: 6 | key: dependency-cache-{{ checksum "package.json" }} 7 | - &install-deps 8 | run: 9 | name: Install dependencies 10 | command: npm ci --ignore-scripts 11 | - &build-packages 12 | run: 13 | name: Build 14 | command: npm run build 15 | 16 | jobs: 17 | build: 18 | working_directory: ~/nest 19 | docker: 20 | - image: cimg/node:22.14.0 21 | steps: 22 | - checkout 23 | - restore_cache: 24 | key: dependency-cache-{{ checksum "package.json" }} 25 | - run: 26 | name: Install dependencies 27 | command: npm ci --ignore-scripts 28 | - save_cache: 29 | key: dependency-cache-{{ checksum "package.json" }} 30 | paths: 31 | - ./node_modules 32 | - run: 33 | name: Build 34 | command: npm run build 35 | 36 | integration_tests: 37 | working_directory: ~/nest 38 | docker: 39 | - image: cimg/node:22.14.0 40 | steps: 41 | - checkout 42 | - *restore-cache 43 | - *install-deps 44 | - run: 45 | name: Integration tests 46 | command: npm run test:e2e 47 | 48 | workflows: 49 | version: 2 50 | build-and-test: 51 | jobs: 52 | - build 53 | - integration_tests: 54 | requires: 55 | - build 56 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | { 9 | ignores: ['eslint.config.mjs', 'tests/**/*'], 10 | }, 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommendedTypeChecked, 13 | eslintPluginPrettierRecommended, 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.node, 18 | ...globals.jest, 19 | }, 20 | ecmaVersion: 5, 21 | sourceType: 'module', 22 | parserOptions: { 23 | projectService: true, 24 | tsconfigRootDir: import.meta.dirname, 25 | }, 26 | }, 27 | }, 28 | { 29 | rules: { 30 | '@typescript-eslint/interface-name-prefix': 'off', 31 | '@typescript-eslint/explicit-function-return-type': 'off', 32 | '@typescript-eslint/explicit-module-boundary-types': 'off', 33 | '@typescript-eslint/no-explicit-any': 'off', 34 | '@typescript-eslint/no-unsafe-assignment': 'off', 35 | '@typescript-eslint/no-unsafe-return': 'off', 36 | '@typescript-eslint/no-misused-promises': 'off', 37 | '@typescript-eslint/no-unsafe-argument': 'off', 38 | '@typescript-eslint/no-unsafe-call': 'off', 39 | '@typescript-eslint/no-unsafe-member-access': 'off', 40 | }, 41 | }, 42 | ); 43 | -------------------------------------------------------------------------------- /tests/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventEmitterModule } from '../../lib'; 3 | import { CustomEventDecoratorConsumer } from './custom-decorator-test.consumer'; 4 | import { EventsControllerConsumer } from './events-controller.consumer'; 5 | import { EventsProviderAliasedConsumer } from './events-provider-aliased.consumer'; 6 | import { EventsProviderPrependConsumer } from './events-provider-prepend.consumer'; 7 | import { EventsProviderConsumer } from './events-provider.consumer'; 8 | import { EventsProviderDurableRequestScopedConsumer } from './events-provider.durable-request-scoped.consumer'; 9 | import { EventsProviderRequestScopedConsumer } from './events-provider.request-scoped.consumer'; 10 | import { EventsProducer } from './events.producer'; 11 | import { TestProvider } from './test-provider'; 12 | 13 | @Module({ 14 | imports: [ 15 | EventEmitterModule.forRoot({ 16 | wildcard: true, 17 | }), 18 | ], 19 | controllers: [EventsControllerConsumer], 20 | providers: [ 21 | EventsProviderConsumer, 22 | EventsProviderPrependConsumer, 23 | EventsProducer, 24 | TestProvider, 25 | EventsProviderRequestScopedConsumer, 26 | EventsProviderDurableRequestScopedConsumer, 27 | EventsProviderAliasedConsumer, 28 | { 29 | provide: 'AnAliasedConsumer', 30 | useExisting: EventsProviderAliasedConsumer, 31 | }, 32 | CustomEventDecoratorConsumer, 33 | ], 34 | }) 35 | export class AppModule {} 36 | -------------------------------------------------------------------------------- /lib/decorators/on-event.decorator.ts: -------------------------------------------------------------------------------- 1 | import { extendArrayMetadata } from '@nestjs/common/utils/extend-metadata.util'; 2 | import { EVENT_LISTENER_METADATA } from '../constants'; 3 | import { OnEventOptions } from '../interfaces'; 4 | 5 | /** 6 | * `@OnEvent` decorator metadata 7 | * 8 | * @publicApi 9 | */ 10 | export interface OnEventMetadata { 11 | /** 12 | * Event (name or pattern) to subscribe to. 13 | */ 14 | event: string | symbol | Array; 15 | /** 16 | * Subscription options. 17 | */ 18 | options?: OnEventOptions; 19 | } 20 | 21 | /** 22 | * `@OnEvent` decorator event type 23 | */ 24 | export type OnEventType = string | symbol | Array; 25 | 26 | /** 27 | * Event listener decorator. 28 | * Subscribes to events based on the specified name(s). 29 | * 30 | * @param event event to subscribe to 31 | * @param options listener options 32 | * @publicApi 33 | */ 34 | export const OnEvent = ( 35 | event: OnEventType, 36 | options?: OnEventOptions, 37 | ): MethodDecorator => { 38 | const decoratorFactory = ( 39 | target: object, 40 | key?: any, 41 | descriptor?: TypedPropertyDescriptor, 42 | ) => { 43 | extendArrayMetadata( 44 | EVENT_LISTENER_METADATA, 45 | [{ event, options } as OnEventMetadata], 46 | descriptor!.value as (...args: any[]) => any, 47 | ); 48 | return descriptor; 49 | }; 50 | decoratorFactory.KEY = EVENT_LISTENER_METADATA; 51 | return decoratorFactory; 52 | }; 53 | -------------------------------------------------------------------------------- /tests/src/events-provider.durable-request-scoped.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Scope } from '@nestjs/common'; 2 | import { OnEvent } from '../../lib'; 3 | import { EVENT_PAYLOAD } from '../../lib'; 4 | import { RequestScopedEventPayload } from './request-scoped-event-payload'; 5 | 6 | @Injectable({ scope: Scope.REQUEST, durable: true }) 7 | export class EventsProviderDurableRequestScopedConsumer { 8 | constructor(@Inject(EVENT_PAYLOAD) public eventRef: any) {} 9 | 10 | public static injectedEventPayload = new RequestScopedEventPayload(); 11 | 12 | private transformPayload = (payload: unknown[]) => 13 | payload.length === 1 ? payload[0] : payload; 14 | 15 | @OnEvent('test.*') 16 | onTestEvent(...payload: unknown[]) { 17 | EventsProviderDurableRequestScopedConsumer.injectedEventPayload.setPayload( 18 | this.transformPayload(payload), 19 | ); 20 | } 21 | 22 | @OnEvent('multiple.*') 23 | onMultiplePayloadEvent(...payload: unknown[]) { 24 | EventsProviderDurableRequestScopedConsumer.injectedEventPayload.setPayload( 25 | this.transformPayload(payload), 26 | ); 27 | } 28 | 29 | @OnEvent('string.*') 30 | onStringPayloadEvent(payload: string) { 31 | EventsProviderDurableRequestScopedConsumer.injectedEventPayload.setPayload( 32 | payload, 33 | ); 34 | } 35 | 36 | @OnEvent('error-handling.durable-request-scoped') 37 | onErrorHandlingEvent() { 38 | throw new Error('This is a test error'); 39 | } 40 | 41 | @OnEvent('error-handling-suppressed.durable-request-scoped', { 42 | suppressErrors: true, 43 | }) 44 | onErrorHandlingSuppressedEvent() { 45 | throw new Error('This is a test error'); 46 | } 47 | 48 | @OnEvent('error-throwing.durable-request-scoped', { suppressErrors: false }) 49 | onErrorThrowingEvent() { 50 | throw new Error('This is a test error'); 51 | } 52 | 53 | @OnEvent('durable') 54 | onDurableTest() { 55 | return this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature Request" 2 | description: "I have a suggestion \U0001F63B!" 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | --- 17 | 18 | - type: checkboxes 19 | attributes: 20 | label: "Is there an existing issue that is already proposing this?" 21 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 22 | options: 23 | - label: "I have searched the existing issues" 24 | required: true 25 | 26 | - type: textarea 27 | validations: 28 | required: true 29 | attributes: 30 | label: "Is your feature request related to a problem? Please describe it" 31 | description: "A clear and concise description of what the problem is" 32 | placeholder: | 33 | I have an issue when ... 34 | 35 | - type: textarea 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Describe the solution you'd like" 40 | description: "A clear and concise description of what you want to happen. Add any considered drawbacks" 41 | 42 | - type: textarea 43 | attributes: 44 | label: "Teachability, documentation, adoption, migration strategy" 45 | description: "If you can, explain how users will be able to use this and possibly write out a version the docs. Maybe a screenshot or design?" 46 | 47 | - type: textarea 48 | validations: 49 | required: true 50 | attributes: 51 | label: "What is the motivation / use case for changing the behavior?" 52 | description: "Describe the motivation or the concrete use case" 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nestjs/event-emitter", 3 | "version": "3.0.1", 4 | "description": "Nest - modern, fast, powerful node.js web framework (@event-emitter)", 5 | "author": "Kamil Mysliwiec", 6 | "license": "MIT", 7 | "url": "https://github.com/nestjs/event-emitter#readme", 8 | "main": "./dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "scripts": { 11 | "build": "rimraf -rf dist && tsc -p tsconfig.json", 12 | "format": "prettier --write \"{lib,test}/**/*.ts\"", 13 | "lint": "eslint 'lib/**/*.ts' --fix", 14 | "prepublish:npm": "npm run build", 15 | "publish:npm": "npm publish --access public", 16 | "prepublish:next": "npm run build", 17 | "publish:next": "npm publish --access public --tag next", 18 | "test:e2e": "jest --config ./tests/jest-e2e.json --runInBand", 19 | "prerelease": "npm run build", 20 | "release": "release-it", 21 | "prepare": "husky install" 22 | }, 23 | "dependencies": { 24 | "eventemitter2": "6.4.9" 25 | }, 26 | "devDependencies": { 27 | "@commitlint/cli": "20.2.0", 28 | "@commitlint/config-angular": "20.2.0", 29 | "@eslint/js": "9.39.2", 30 | "@nestjs/common": "11.1.10", 31 | "@nestjs/core": "11.1.10", 32 | "@nestjs/platform-express": "11.1.10", 33 | "@nestjs/testing": "11.1.10", 34 | "@types/jest": "29.5.14", 35 | "@types/node": "24.10.4", 36 | "@typescript-eslint/eslint-plugin": "8.50.1", 37 | "@typescript-eslint/parser": "8.50.1", 38 | "eslint": "9.39.2", 39 | "eslint-config-prettier": "10.1.8", 40 | "eslint-plugin-import": "2.32.0", 41 | "eslint-plugin-prettier": "5.5.4", 42 | "husky": "9.1.7", 43 | "jest": "29.7.0", 44 | "lint-staged": "16.2.7", 45 | "prettier": "3.7.4", 46 | "reflect-metadata": "0.2.2", 47 | "release-it": "19.2.1", 48 | "rimraf": "6.1.2", 49 | "rxjs": "7.8.2", 50 | "ts-jest": "29.4.6", 51 | "typescript": "5.9.3", 52 | "typescript-eslint": "8.50.1" 53 | }, 54 | "peerDependencies": { 55 | "@nestjs/common": "^10.0.0 || ^11.0.0", 56 | "@nestjs/core": "^10.0.0 || ^11.0.0" 57 | }, 58 | "lint-staged": { 59 | "*.ts": [ 60 | "prettier --write", 61 | "eslint --fix" 62 | ] 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "https://github.com/nestjs/event-emitter" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 6 | [travis-url]: https://travis-ci.org/nestjs/nest 7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 8 | [linux-url]: https://travis-ci.org/nestjs/nest 9 | 10 |

A progressive Node.js framework for building efficient and scalable server-side applications.

11 |

12 | NPM Version 13 | Package License 14 | NPM Downloads 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | 20 |

21 | 23 | 24 | ## Description 25 | 26 | Events module for [Nest](https://github.com/nestjs/nest) built on top of the [eventemitter2](https://github.com/EventEmitter2/EventEmitter2) package. 27 | 28 | ## Installation 29 | 30 | ```bash 31 | $ npm i --save @nestjs/event-emitter 32 | ``` 33 | 34 | ## Quick Start 35 | 36 | [Overview & Tutorial](https://docs.nestjs.com/techniques/events) 37 | 38 | ## Support 39 | 40 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 41 | 42 | ## Stay in touch 43 | 44 | - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) 45 | - Website - [https://nestjs.com](https://nestjs.com/) 46 | - Twitter - [@nestframework](https://twitter.com/nestframework) 47 | 48 | ## License 49 | 50 | Nest is [MIT licensed](LICENSE). 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Regression.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4A5 Regression" 2 | description: "Report an unexpected behavior while upgrading your Nest application!" 3 | labels: ["needs triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: "Did you read the migration guide?" 23 | description: "Check out the [migration guide here](https://docs.nestjs.com/migration-guide)!" 24 | options: 25 | - label: "I have read the whole migration guide" 26 | required: false 27 | 28 | - type: checkboxes 29 | attributes: 30 | label: "Is there an existing issue that is already proposing this?" 31 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 32 | options: 33 | - label: "I have searched the existing issues" 34 | required: true 35 | 36 | - type: input 37 | attributes: 38 | label: "Potential Commit/PR that introduced the regression" 39 | description: "If you have time to investigate, what PR/date/version introduced this issue" 40 | placeholder: "PR #123 or commit 5b3c4a4" 41 | 42 | - type: input 43 | attributes: 44 | label: "Versions" 45 | description: "From which version of `@nestjs/event-emitter` to which version you are upgrading" 46 | placeholder: "8.1.0 -> 8.1.3" 47 | 48 | - type: textarea 49 | validations: 50 | required: true 51 | attributes: 52 | label: "Describe the regression" 53 | description: "A clear and concise description of what the regression is" 54 | 55 | - type: textarea 56 | attributes: 57 | label: "Minimum reproduction code" 58 | description: | 59 | Please share a git repo, a gist, or step-by-step instructions. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction) 60 | **Tip:** If you leave a minimum repository, we will understand your issue faster! 61 | value: | 62 | ```ts 63 | 64 | ``` 65 | 66 | - type: textarea 67 | validations: 68 | required: true 69 | attributes: 70 | label: "Expected behavior" 71 | description: "A clear and concise description of what you expected to happend (or code)" 72 | 73 | - type: textarea 74 | attributes: 75 | label: "Other" 76 | description: | 77 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 78 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "If something isn't working as expected \U0001F914" 3 | labels: ["needs triage", "bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: "Is there an existing issue for this?" 23 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the bug you encountered" 24 | options: 25 | - label: "I have searched the existing issues" 26 | required: true 27 | 28 | - type: textarea 29 | validations: 30 | required: true 31 | attributes: 32 | label: "Current behavior" 33 | description: "How the issue manifests?" 34 | 35 | - type: input 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Minimum reproduction code" 40 | description: "An URL to some git repository or gist that reproduces this issue. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction)" 41 | placeholder: "https://github.com/..." 42 | 43 | - type: textarea 44 | attributes: 45 | label: "Steps to reproduce" 46 | description: | 47 | How the issue manifests? 48 | You could leave this blank if you alread write this in your reproduction code/repo 49 | placeholder: | 50 | 1. `npm i` 51 | 2. `npm start:dev` 52 | 3. See error... 53 | 54 | - type: textarea 55 | validations: 56 | required: true 57 | attributes: 58 | label: "Expected behavior" 59 | description: "A clear and concise description of what you expected to happend (or code)" 60 | 61 | - type: markdown 62 | attributes: 63 | value: | 64 | --- 65 | 66 | - type: input 67 | validations: 68 | required: true 69 | attributes: 70 | label: "Package version" 71 | description: | 72 | Which version of `@nestjs/event-emitter` are you using? 73 | **Tip**: Make sure that all of yours `@nestjs/*` dependencies are in sync! 74 | placeholder: "8.1.3" 75 | 76 | - type: input 77 | attributes: 78 | label: "NestJS version" 79 | description: "Which version of `@nestjs/core` are you using?" 80 | placeholder: "8.1.3" 81 | 82 | - type: input 83 | attributes: 84 | label: "Node.js version" 85 | description: "Which version of Node.js are you using?" 86 | placeholder: "14.17.6" 87 | 88 | - type: checkboxes 89 | attributes: 90 | label: "In which operating systems have you tested?" 91 | options: 92 | - label: macOS 93 | - label: Windows 94 | - label: Linux 95 | 96 | - type: markdown 97 | attributes: 98 | value: | 99 | --- 100 | 101 | - type: textarea 102 | attributes: 103 | label: "Other" 104 | description: | 105 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 106 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 107 | -------------------------------------------------------------------------------- /lib/event-subscribers.loader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | Logger, 4 | OnApplicationBootstrap, 5 | OnApplicationShutdown, 6 | } from '@nestjs/common'; 7 | import { 8 | ContextIdFactory, 9 | DiscoveryService, 10 | MetadataScanner, 11 | ModuleRef, 12 | } from '@nestjs/core'; 13 | import { Injector } from '@nestjs/core/injector/injector'; 14 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 15 | import { Module } from '@nestjs/core/injector/module'; 16 | import { EventEmitter2 } from 'eventemitter2'; 17 | import { EventEmitterReadinessWatcher } from './event-emitter-readiness.watcher'; 18 | import { EventsMetadataAccessor } from './events-metadata.accessor'; 19 | import { OnEventOptions } from './interfaces'; 20 | import { EventPayloadHost } from './interfaces/event-payload-host.interface'; 21 | 22 | @Injectable() 23 | export class EventSubscribersLoader 24 | implements OnApplicationBootstrap, OnApplicationShutdown 25 | { 26 | private readonly injector = new Injector(); 27 | private readonly logger = new Logger('Event'); 28 | 29 | constructor( 30 | private readonly discoveryService: DiscoveryService, 31 | private readonly eventEmitter: EventEmitter2, 32 | private readonly metadataAccessor: EventsMetadataAccessor, 33 | private readonly metadataScanner: MetadataScanner, 34 | private readonly moduleRef: ModuleRef, 35 | private readonly eventEmitterReadinessWatcher: EventEmitterReadinessWatcher, 36 | ) {} 37 | 38 | onApplicationBootstrap() { 39 | try { 40 | this.loadEventListeners(); 41 | this.eventEmitterReadinessWatcher.setReady(); 42 | } catch (e) { 43 | this.eventEmitterReadinessWatcher.setErrored(e as Error); 44 | } 45 | } 46 | 47 | onApplicationShutdown() { 48 | this.eventEmitter.removeAllListeners(); 49 | } 50 | 51 | loadEventListeners() { 52 | const providers = this.discoveryService.getProviders(); 53 | const controllers = this.discoveryService.getControllers(); 54 | [...providers, ...controllers] 55 | .filter(wrapper => wrapper.instance && !wrapper.isAlias) 56 | .forEach((wrapper: InstanceWrapper) => { 57 | const { instance } = wrapper; 58 | const prototype = Object.getPrototypeOf(instance) || {}; 59 | const isRequestScoped = !wrapper.isDependencyTreeStatic(); 60 | this.metadataScanner 61 | .getAllMethodNames(prototype) 62 | .forEach(methodKey => 63 | this.subscribeToEventIfListener( 64 | instance, 65 | methodKey, 66 | isRequestScoped, 67 | wrapper.host as Module, 68 | ), 69 | ); 70 | }); 71 | } 72 | 73 | private subscribeToEventIfListener( 74 | instance: Record, 75 | methodKey: string, 76 | isRequestScoped: boolean, 77 | moduleRef: Module, 78 | ) { 79 | const eventListenerMetadatas = 80 | this.metadataAccessor.getEventHandlerMetadata(instance[methodKey]); 81 | if (!eventListenerMetadatas) { 82 | return; 83 | } 84 | 85 | for (const eventListenerMetadata of eventListenerMetadatas) { 86 | const { event, options } = eventListenerMetadata; 87 | const listenerMethod = this.getRegisterListenerMethodBasedOn(options); 88 | 89 | if (isRequestScoped) { 90 | this.registerRequestScopedListener({ 91 | event, 92 | eventListenerInstance: instance, 93 | listenerMethod, 94 | listenerMethodKey: methodKey, 95 | moduleRef, 96 | options, 97 | }); 98 | } else { 99 | listenerMethod( 100 | event, 101 | (...args: unknown[]) => 102 | this.wrapFunctionInTryCatchBlocks( 103 | instance, 104 | methodKey, 105 | args, 106 | options, 107 | ), 108 | options, 109 | ); 110 | } 111 | } 112 | } 113 | 114 | private getRegisterListenerMethodBasedOn(options?: OnEventOptions) { 115 | return options?.prependListener 116 | ? this.eventEmitter.prependListener.bind(this.eventEmitter) 117 | : this.eventEmitter.on.bind(this.eventEmitter); 118 | } 119 | 120 | private registerRequestScopedListener(eventListenerContext: { 121 | listenerMethod: EventEmitter2['on']; 122 | event: string | symbol | (string | symbol)[]; 123 | eventListenerInstance: Record; 124 | moduleRef: Module; 125 | listenerMethodKey: string; 126 | options?: OnEventOptions; 127 | }) { 128 | const { 129 | listenerMethod, 130 | event, 131 | eventListenerInstance, 132 | moduleRef, 133 | listenerMethodKey, 134 | options, 135 | } = eventListenerContext; 136 | 137 | listenerMethod( 138 | event, 139 | async (...args: unknown[]) => { 140 | const request = this.getRequestFromEventPayload(args); 141 | const contextId = ContextIdFactory.getByRequest< 142 | EventPayloadHost 143 | >({ payload: request }); 144 | 145 | this.moduleRef.registerRequestByContextId(request, contextId); 146 | 147 | const contextInstance = await this.injector.loadPerContext( 148 | eventListenerInstance, 149 | moduleRef, 150 | moduleRef.providers, 151 | contextId, 152 | ); 153 | return this.wrapFunctionInTryCatchBlocks( 154 | contextInstance, 155 | listenerMethodKey, 156 | args, 157 | options, 158 | ); 159 | }, 160 | options, 161 | ); 162 | } 163 | 164 | private getRequestFromEventPayload(eventPayload: unknown[]): unknown { 165 | /* 166 | **Required explanation for the ternary below** 167 | 168 | We need the conditional below because an event can be emitted with a variable amount of arguments. 169 | For instance, we can do `this.eventEmitter.emit('event', 'payload1', 'payload2', ..., 'payloadN');` 170 | 171 | All payload arguments are internally stored as an array. So, imagine we emitted an event as follows: 172 | 173 | `this.eventEmitter.emit('event', 'payload'); 174 | 175 | if we registered the original `eventPayload`, when we try to inject it in a listener, it'll be retrieved as [`payload`]. 176 | However, whoever is using this library would certainly expect the event payload to be a single string 'payload', not an array, 177 | since this is what we emitted above. 178 | */ 179 | return eventPayload.length > 1 ? eventPayload : eventPayload[0]; 180 | } 181 | 182 | private async wrapFunctionInTryCatchBlocks( 183 | instance: Record unknown>, 184 | methodKey: string, 185 | args: unknown[], 186 | options?: OnEventOptions, 187 | ) { 188 | try { 189 | return await instance[methodKey].call(instance, ...args); 190 | } catch (e) { 191 | if (options?.suppressErrors ?? true) { 192 | const error = e as Error; 193 | this.logger.error(error.message, error.stack); 194 | } else { 195 | throw e; 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/e2e/module-e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { ContextIdFactory, createContextId } from '@nestjs/core'; 3 | import { Test } from '@nestjs/testing'; 4 | import { EventEmitter2 } from 'eventemitter2'; 5 | import { EventEmitterReadinessWatcher } from '../../lib'; 6 | import { AppModule } from '../src/app.module'; 7 | import { 8 | TEST_EVENT_MULTIPLE_PAYLOAD, 9 | TEST_EVENT_PAYLOAD, 10 | TEST_EVENT_STRING_PAYLOAD, 11 | } from '../src/constants'; 12 | import { CUSTOM_DECORATOR_EVENT } from '../src/custom-decorator-test.constants'; 13 | import { CustomEventDecoratorConsumer } from '../src/custom-decorator-test.consumer'; 14 | import { EventsControllerConsumer } from '../src/events-controller.consumer'; 15 | import { EventsProviderAliasedConsumer } from '../src/events-provider-aliased.consumer'; 16 | import { EventsProviderPrependConsumer } from '../src/events-provider-prepend.consumer'; 17 | import { EventsProviderConsumer } from '../src/events-provider.consumer'; 18 | import { EventsProviderDurableRequestScopedConsumer } from '../src/events-provider.durable-request-scoped.consumer'; 19 | import { EventsProviderRequestScopedConsumer } from '../src/events-provider.request-scoped.consumer'; 20 | import { TEST_PROVIDER_TOKEN } from '../src/test-provider'; 21 | 22 | describe('EventEmitterModule - e2e', () => { 23 | let app: INestApplication; 24 | const durableContextId = createContextId(); 25 | 26 | beforeAll(() => { 27 | ContextIdFactory.apply({ 28 | attach: (contextId, _request) => info => { 29 | return info.isTreeDurable ? durableContextId : contextId; 30 | }, 31 | }); 32 | }); 33 | 34 | beforeEach(async () => { 35 | const module = await Test.createTestingModule({ 36 | imports: [AppModule], 37 | }).compile(); 38 | 39 | app = module.createNestApplication(); 40 | }); 41 | 42 | it(`should emit a "test-event" event to providers`, async () => { 43 | const eventsConsumerRef = app.get(EventsProviderConsumer); 44 | await app.init(); 45 | 46 | expect(eventsConsumerRef.eventPayload).toEqual(TEST_EVENT_PAYLOAD); 47 | }); 48 | 49 | it(`should emit a "stacked-event" event to providers`, async () => { 50 | const eventsConsumerRef = app.get(EventsProviderConsumer); 51 | await app.init(); 52 | 53 | expect(eventsConsumerRef.stackedEventCalls).toEqual(2); 54 | }); 55 | 56 | it(`aliased providers should receive an event only once`, async () => { 57 | const eventsConsumerRef = app.get(EventsProviderAliasedConsumer); 58 | const eventSpy = jest.spyOn(eventsConsumerRef, 'eventPayload', 'set'); 59 | await app.init(); 60 | 61 | expect(eventSpy).toBeCalledTimes(1); 62 | eventSpy.mockRestore(); 63 | }); 64 | 65 | it(`event subscribers are separate per each app instance`, async () => { 66 | const eventsConsumerRef = app.get(EventsProviderAliasedConsumer); 67 | const eventSpy = jest.spyOn(eventsConsumerRef, 'eventPayload', 'set'); 68 | 69 | await app.init(); 70 | 71 | const module2 = await Test.createTestingModule({ 72 | imports: [AppModule], 73 | }).compile(); 74 | const app2 = module2.createNestApplication(); 75 | await app2.init(); 76 | 77 | expect(eventSpy).toBeCalledTimes(1); 78 | eventSpy.mockRestore(); 79 | }); 80 | 81 | it(`should emit a "test-event" event to controllers`, async () => { 82 | const eventsConsumerRef = app.get(EventsControllerConsumer); 83 | await app.init(); 84 | 85 | expect(eventsConsumerRef.eventPayload).toEqual(TEST_EVENT_PAYLOAD); 86 | }); 87 | 88 | it('should be able to specify a consumer be prepended via OnEvent decorator options', async () => { 89 | const eventsConsumerRef = app.get(EventsProviderPrependConsumer); 90 | const prependListenerSpy = jest.spyOn( 91 | app.get(EventEmitter2), 92 | 'prependListener', 93 | ); 94 | await app.init(); 95 | 96 | expect(eventsConsumerRef.eventPayload).toEqual(TEST_EVENT_PAYLOAD); 97 | expect(prependListenerSpy).toHaveBeenCalled(); 98 | }); 99 | 100 | it('should work with null prototype provider value', async () => { 101 | const moduleWithNullProvider = await Test.createTestingModule({ 102 | imports: [AppModule], 103 | }) 104 | .overrideProvider(TEST_PROVIDER_TOKEN) 105 | .useFactory({ 106 | factory: () => { 107 | const testObject = { a: 13, b: 7 }; 108 | Object.setPrototypeOf(testObject, null); 109 | return testObject; 110 | }, 111 | }) 112 | .compile(); 113 | app = moduleWithNullProvider.createNestApplication(); 114 | await expect(app.init()).resolves.not.toThrow(); 115 | }); 116 | 117 | it('should be able to emit a request-scoped event with a single payload', async () => { 118 | await app.init(); 119 | 120 | expect( 121 | EventsProviderRequestScopedConsumer.injectedEventPayload.objectValue, 122 | ).toEqual(TEST_EVENT_PAYLOAD); 123 | }); 124 | 125 | it('should be able to emit a durable request-scoped event with a single payload', async () => { 126 | await app.init(); 127 | 128 | expect( 129 | EventsProviderDurableRequestScopedConsumer.injectedEventPayload 130 | .objectValue, 131 | ).toEqual(TEST_EVENT_PAYLOAD); 132 | }); 133 | 134 | it('should be able to emit a request-scoped event with a string payload', async () => { 135 | await app.init(); 136 | 137 | expect( 138 | EventsProviderRequestScopedConsumer.injectedEventPayload.stringValue, 139 | ).toEqual(TEST_EVENT_STRING_PAYLOAD); 140 | }); 141 | 142 | it('should be able to emit a durable request-scoped event with a string payload', async () => { 143 | await app.init(); 144 | 145 | expect( 146 | EventsProviderDurableRequestScopedConsumer.injectedEventPayload 147 | .stringValue, 148 | ).toEqual(TEST_EVENT_STRING_PAYLOAD); 149 | }); 150 | 151 | it('should be able to emit a request-scoped event with multiple payloads', async () => { 152 | await app.init(); 153 | 154 | expect( 155 | EventsProviderRequestScopedConsumer.injectedEventPayload.arrayValue, 156 | ).toEqual(TEST_EVENT_MULTIPLE_PAYLOAD); 157 | }); 158 | 159 | it('should be able to emit a durable request-scoped event with multiple payloads', async () => { 160 | await app.init(); 161 | 162 | expect( 163 | EventsProviderDurableRequestScopedConsumer.injectedEventPayload 164 | .arrayValue, 165 | ).toEqual(TEST_EVENT_MULTIPLE_PAYLOAD); 166 | }); 167 | 168 | it('should work with non array metadata', async () => { 169 | await app.init(); 170 | 171 | const emitter = app.get(EventEmitter2); 172 | const customConsumer = app.get(CustomEventDecoratorConsumer); 173 | 174 | // callback called synchronysly 175 | emitter.emit(CUSTOM_DECORATOR_EVENT); 176 | 177 | expect(customConsumer.isEmitted).toBeTruthy(); 178 | }); 179 | 180 | it('should be able to gracefully recover when an unexpected error occurs from provider', async () => { 181 | const eventsConsumerRef = app.get(EventsProviderConsumer); 182 | await app.init(); 183 | 184 | const emitter = app.get(EventEmitter2); 185 | const result = emitter.emit('error-handling.provider'); 186 | 187 | expect(eventsConsumerRef.errorHandlingCalls).toEqual(1); 188 | expect(result).toBeTruthy(); 189 | }); 190 | 191 | it('should be able to gracefully recover when an unexpected error occurs from provider and suppressErrors is true', async () => { 192 | const eventsConsumerRef = app.get(EventsProviderConsumer); 193 | await app.init(); 194 | 195 | const emitter = app.get(EventEmitter2); 196 | const result = emitter.emit('error-handling-suppressed.provider'); 197 | 198 | expect(eventsConsumerRef.errorHandlingCalls).toEqual(1); 199 | expect(result).toBeTruthy(); 200 | }); 201 | 202 | it('should be able to gracefully recover when an unexpected error occurs from request scoped', async () => { 203 | await app.init(); 204 | 205 | const eventEmitter = app.get(EventEmitter2); 206 | const result = eventEmitter.emit('error-handling.request-scoped'); 207 | 208 | expect(result).toBeTruthy(); 209 | }); 210 | 211 | it('should be able to gracefully recover when an unexpected error occurs from request scoped and suppressErrors is true', async () => { 212 | await app.init(); 213 | 214 | const eventEmitter = app.get(EventEmitter2); 215 | const result = eventEmitter.emit( 216 | 'error-handling-suppressed.request-scoped', 217 | ); 218 | 219 | expect(result).toBeTruthy(); 220 | }); 221 | 222 | it('should throw when an unexpected error occurs from provider and suppressErrors is false', async () => { 223 | await app.init(); 224 | 225 | const eventEmitter = app.get(EventEmitter2); 226 | expect(eventEmitter.emitAsync('error-throwing.provider')).rejects.toThrow( 227 | 'This is a test error', 228 | ); 229 | }); 230 | 231 | it('should throw when an unexpected error occurs from request scoped and suppressErrors is false', async () => { 232 | await app.init(); 233 | 234 | const eventEmitter = app.get(EventEmitter2); 235 | expect( 236 | eventEmitter.emitAsync('error-throwing.request-scoped'), 237 | ).rejects.toThrow('This is a test error'); 238 | }); 239 | 240 | it('should be able to wait until the event emitter is ready', async () => { 241 | const eventsConsumerRef = app.get(EventsControllerConsumer); 242 | await app.init(); 243 | 244 | const eventEmitterWatcher = app.get(EventEmitterReadinessWatcher); 245 | await expect(eventEmitterWatcher.waitUntilReady()).resolves.toBeUndefined(); 246 | expect(eventsConsumerRef.eventPayload).toEqual(TEST_EVENT_PAYLOAD); 247 | }); 248 | 249 | it('should throw when an unexpected error occurs from durable request scoped and suppressErrors is false', async () => { 250 | await app.init(); 251 | 252 | const eventEmitter = app.get(EventEmitter2); 253 | expect( 254 | eventEmitter.emitAsync('error-throwing.durable-request-scoped'), 255 | ).rejects.toThrow('This is a test error'); 256 | }); 257 | 258 | it('should load durable provider once for different event emissions', async () => { 259 | await app.init(); 260 | const eventEmitter = app.get(EventEmitter2); 261 | const [durableInstance] = await eventEmitter.emitAsync('durable'); 262 | const [durableInstance2] = await eventEmitter.emitAsync('durable'); 263 | expect(durableInstance).toBe(durableInstance2); 264 | }); 265 | 266 | it('should load non-durable provider anew for different event emissions', async () => { 267 | await app.init(); 268 | const eventEmitter = app.get(EventEmitter2); 269 | const [notDurableInstance] = await eventEmitter.emitAsync('not-durable'); 270 | const [notDurableInstance2] = await eventEmitter.emitAsync('not-durable'); 271 | expect(notDurableInstance).not.toBe(notDurableInstance2); 272 | }); 273 | 274 | afterEach(async () => { 275 | await app.close(); 276 | }); 277 | }); 278 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Nest 2 | 3 | We would love for you to contribute to Nest and help make it even better than it is 4 | today! As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [Code of Conduct](#coc) 7 | - [Question or Problem?](#question) 8 | - [Issues and Bugs](#issue) 9 | - [Feature Requests](#feature) 10 | - [Submission Guidelines](#submit) 11 | - [Coding Rules](#rules) 12 | - [Commit Message Guidelines](#commit) 13 | 14 | 15 | 17 | 18 | ## Got a Question or Problem? 19 | 20 | **Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.** You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/nestjs) where the questions should be tagged with tag `nestjs`. 21 | 22 | Stack Overflow is a much better place to ask questions since: 23 | 24 | 25 | - questions and answers stay available for public viewing so your question / answer might help someone else 26 | - Stack Overflow's voting system assures that the best answers are prominently visible. 27 | 28 | To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow. 29 | 30 | If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter]. 31 | 32 | ## Found a Bug? 33 | If you find a bug in the source code, you can help us by 34 | [submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can 35 | [submit a Pull Request](#submit-pr) with a fix. 36 | 37 | ## Missing a Feature? 38 | You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub 39 | Repository. If you would like to *implement* a new feature, please submit an issue with 40 | a proposal for your work first, to be sure that we can use it. 41 | Please consider what kind of change it is: 42 | 43 | * For a **Major Feature**, first open an issue and outline your proposal so that it can be 44 | discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, 45 | and help you to craft the change so that it is successfully accepted into the project. For your issue name, please prefix your proposal with `[discussion]`, for example "[discussion]: your feature idea". 46 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 47 | 48 | ## Submission Guidelines 49 | 50 | ### Submitting an Issue 51 | 52 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. 53 | 54 | We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like: 55 | 56 | - version of NestJS used 57 | - 3rd-party libraries and their versions 58 | - and most importantly - a use-case that fails 59 | 60 | 64 | 65 | 66 | 67 | Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced. 68 | 69 | You can file new issues by filling out our [new issue form](https://github.com/nestjs/nest/issues/new). 70 | 71 | 72 | ### Submitting a Pull Request (PR) 73 | Before you submit your Pull Request (PR) consider the following guidelines: 74 | 75 | 1. Search [GitHub](https://github.com/nestjs/nest/pulls) for an open or closed PR 76 | that relates to your submission. You don't want to duplicate effort. 77 | 79 | 1. Fork the nestjs/nest repo. 80 | 1. Make your changes in a new git branch: 81 | 82 | ```shell 83 | git checkout -b my-fix-branch master 84 | ``` 85 | 86 | 1. Create your patch, **including appropriate test cases**. 87 | 1. Follow our [Coding Rules](#rules). 88 | 1. Run the full Nest test suite, as described in the [developer documentation][dev-doc], 89 | and ensure that all tests pass. 90 | 1. Commit your changes using a descriptive commit message that follows our 91 | [commit message conventions](#commit). Adherence to these conventions 92 | is necessary because release notes are automatically generated from these messages. 93 | 94 | ```shell 95 | git commit -a 96 | ``` 97 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 98 | 99 | 1. Push your branch to GitHub: 100 | 101 | ```shell 102 | git push origin my-fix-branch 103 | ``` 104 | 105 | 1. In GitHub, send a pull request to `nestjs:master`. 106 | * If we suggest changes then: 107 | * Make the required updates. 108 | * Re-run the Nest test suites to ensure tests are still passing. 109 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 110 | 111 | ```shell 112 | git rebase master -i 113 | git push -f 114 | ``` 115 | 116 | That's it! Thank you for your contribution! 117 | 118 | #### After your pull request is merged 119 | 120 | After your pull request is merged, you can safely delete your branch and pull the changes 121 | from the main (upstream) repository: 122 | 123 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 124 | 125 | ```shell 126 | git push origin --delete my-fix-branch 127 | ``` 128 | 129 | * Check out the master branch: 130 | 131 | ```shell 132 | git checkout master -f 133 | ``` 134 | 135 | * Delete the local branch: 136 | 137 | ```shell 138 | git branch -D my-fix-branch 139 | ``` 140 | 141 | * Update your master with the latest upstream version: 142 | 143 | ```shell 144 | git pull --ff upstream master 145 | ``` 146 | 147 | ## Coding Rules 148 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 149 | 150 | * All features or bug fixes **must be tested** by one or more specs (unit-tests). 151 | 154 | * We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at 155 | **100 characters**. An automated formatter is available, see 156 | [DEVELOPER.md](docs/DEVELOPER.md#clang-format). 157 | 158 | ## Commit Message Guidelines 159 | 160 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 161 | readable messages** that are easy to follow when looking through the **project history**. But also, 162 | we use the git commit messages to **generate the Nest change log**. 163 | 164 | ### Commit Message Format 165 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 166 | format that includes a **type**, a **scope** and a **subject**: 167 | 168 | ``` 169 | (): 170 | 171 | 172 | 173 |