Event Emitter Package
The @hazeljs/event-emitter package provides event-driven architecture for your HazelJS applications. It allows you to emit and listen for events across your application with a decorator-based API, similar to @nestjs/event-emitter. Built on eventemitter2, it supports wildcards, namespaces, and async listeners.
Purpose
Many applications need to decouple different parts of the system—when an order is created, you might want to send an email, update analytics, and notify inventory. Implementing this with direct method calls creates tight coupling. The @hazeljs/event-emitter package solves this by providing:
- Event-Driven Architecture: Decouple components—emitters don't need to know about listeners
- Decorator-Based: Use
@OnEvent()decorator to declare event listeners - DI Integration: Inject
EventEmitterServiceanywhere to emit events - Wildcards: Listen to event patterns (e.g.
order.*) when enabled - Multiple Listeners: A single event can have many listeners that don't depend on each other
Architecture
The package uses a service-based approach with decorator metadata for listener registration:
graph TD A["@OnEvent Decorator<br/>(Marks Methods as Listeners)"] --> B["EventEmitterModule<br/>(Module Configuration)"] B --> C["EventEmitterService<br/>(Extends EventEmitter2)"] C --> D["emit() / emitAsync()<br/>(Dispatch Events)"] C --> E["on() Listeners<br/>(Registered from @OnEvent)"] D --> E style A fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff style B fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff style C fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff style D fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff style E fill:#f59e0b,stroke:#fbbf24,stroke-width:2px,color:#fff
Key Components
- EventEmitterModule: Configures the event emitter with options (wildcard, delimiter, etc.)
- EventEmitterService: Injectable service that extends EventEmitter2 for emitting and listening
- @OnEvent Decorator: Declarative way to mark methods as event listeners
- registerListenersFromProvider(s): Registers
@OnEventhandlers from DI-resolved providers
Advantages
1. Decoupled Architecture
Emitters and listeners are independent—add new listeners without modifying existing code.
2. Decorator-Based API
Use @OnEvent('order.created') to declare listeners—clean and easy to understand.
3. Full DI Integration
Inject EventEmitterService anywhere in your application to emit events.
4. Wildcard Support
With wildcard: true, listen to patterns like order.* or user.** for flexible event handling.
5. Async Listeners
Support for async event handlers with configurable error suppression.
6. NestJS Familiar
Similar API to @nestjs/event-emitter for developers coming from NestJS.
Installation
npm install @hazeljs/event-emitter
Quick Start
1. Import EventEmitterModule
import { HazelModule } from '@hazeljs/core';
import { EventEmitterModule } from '@hazeljs/event-emitter';
@HazelModule({
imports: [EventEmitterModule.forRoot()],
providers: [OrderService, OrderEventHandler],
})
export class AppModule {}
2. Emit Events
Inject EventEmitterService and call emit():
import { Injectable } from '@hazeljs/core';
import { EventEmitterService } from '@hazeljs/event-emitter';
@Injectable()
export class OrderService {
constructor(private eventEmitter: EventEmitterService) {}
createOrder(order: Order) {
// ... create order in database
this.eventEmitter.emit('order.created', {
orderId: order.id,
order,
userId: order.userId,
});
}
}
3. Listen to Events with @OnEvent
Create an event handler class with @OnEvent decorators:
import { Injectable } from '@hazeljs/core';
import { OnEvent } from '@hazeljs/event-emitter';
@Injectable()
export class OrderEventHandler {
@OnEvent('order.created')
handleOrderCreated(payload: { orderId: string; order: Order; userId: string }) {
console.log('Order created:', payload.orderId);
// Send confirmation email, update analytics, etc.
}
}
4. Register Listeners
After your app initializes, register listeners from providers that have @OnEvent decorators:
import { EventEmitterModule } from '@hazeljs/event-emitter';
// Register from provider classes (resolves from DI container)
EventEmitterModule.registerListenersFromProviders([OrderEventHandler]);
// Or register from a specific instance
const container = Container.getInstance();
const orderHandler = container.resolve(OrderEventHandler);
EventEmitterModule.registerListenersFromProvider(orderHandler);
Configuration
Configure the event emitter via EventEmitterModule.forRoot():
EventEmitterModule.forRoot({
wildcard: true, // Enable 'order.*' style patterns
delimiter: '.', // Namespace delimiter (default: '.')
maxListeners: 10, // Max listeners per event
newListener: false, // Emit newListener event
removeListener: false,// Emit removeListener event
verboseMemoryLeak: false,
ignoreErrors: false,
isGlobal: true, // Global module (default: true)
});
Wildcard Events
When wildcard: true, you can use patterns:
// Listen to all order events (order.created, order.shipped, etc.)
@OnEvent('order.*')
handleOrderEvents(payload: unknown) {
console.log('Order event:', payload);
}
// Multi-level wildcard (order.delayed.out_of_stock)
@OnEvent('order.**')
handleAllOrderEvents(payload: unknown) {
// Catches nested events
}
@OnEvent Options
interface OnEventOptions {
async?: boolean; // Run handler asynchronously
prependListener?: boolean; // Add listener to front of queue
suppressErrors?: boolean; // Don't rethrow errors (default: true)
}
Async Listeners
@OnEvent('order.created', { async: true })
async handleOrderCreated(payload: OrderCreatedEvent) {
await sendConfirmationEmail(payload.userId);
await updateAnalytics('order_created', payload);
}
Error Handling
By default, errors in listeners are suppressed (logged but not rethrown). To rethrow:
@OnEvent('order.created', { suppressErrors: false })
handleOrderCreated(payload: OrderCreatedEvent) {
// Errors will propagate
if (!payload.orderId) throw new Error('Invalid order');
}
EventEmitterService API
The EventEmitterService extends EventEmitter2. Key methods:
// Emit event (synchronous)
eventEmitter.emit('order.created', payload);
// Emit event (asynchronous - returns promise of listener results)
await eventEmitter.emitAsync('order.created', payload);
// Direct listener registration (alternative to @OnEvent)
eventEmitter.on('custom.event', (data) => { ... });
eventEmitter.once('one-time.event', (data) => { ... });
eventEmitter.off('event', listener);
Complete Example
// app.module.ts
import { HazelModule } from '@hazeljs/core';
import { EventEmitterModule } from '@hazeljs/event-emitter';
@HazelModule({
imports: [EventEmitterModule.forRoot({ wildcard: true })],
controllers: [OrderController],
providers: [OrderService, OrderEventHandler, EmailService],
})
export class AppModule {}
// order.service.ts
import { Injectable } from '@hazeljs/core';
import { EventEmitterService } from '@hazeljs/event-emitter';
@Injectable()
export class OrderService {
constructor(private eventEmitter: EventEmitterService) {}
async createOrder(dto: CreateOrderDto) {
const order = await this.saveOrder(dto);
this.eventEmitter.emit('order.created', { orderId: order.id, order });
return order;
}
}
// order-event.handler.ts
import { Injectable } from '@hazeljs/core';
import { OnEvent } from '@hazeljs/event-emitter';
@Injectable()
export class OrderEventHandler {
constructor(private emailService: EmailService) {}
@OnEvent('order.created', { async: true })
async handleOrderCreated(payload: { orderId: string; order: Order }) {
await this.emailService.sendOrderConfirmation(payload.order);
}
@OnEvent('order.*')
logOrderEvent(payload: unknown) {
console.log('Order event received:', payload);
}
}
// main.ts - after app bootstrap
import { EventEmitterModule } from '@hazeljs/event-emitter';
import { OrderEventHandler } from './order-event.handler';
const app = new HazelApp(AppModule);
await app.listen(3000);
// Register event listeners
EventEmitterModule.registerListenersFromProviders([OrderEventHandler]);
Best Practices
-
Use descriptive event names: Prefer
order.createdoverorderCreatefor namespacing. -
Register listeners early: Call
registerListenersFromProvidersafter app bootstrap, before emitting events. -
Type your payloads: Define interfaces for event payloads for better type safety.
-
Handle errors: Use
suppressErrors: falsefor critical listeners, or handle errors in async handlers. -
Use wildcards sparingly:
order.*is useful for logging; avoid**unless you need to catch everything. -
Keep handlers focused: Each
@OnEventhandler should do one thing—emit more events if you need to chain logic.