HazelJS Event Emitter Package
@hazeljs/event-emitter provides decorator-based event-driven architecture for HazelJS with wildcards, namespaces, and async listeners, built on eventemitter2.
Quick Reference
- Purpose:
@hazeljs/event-emitterdecouples application components through event-driven communication — emit events from one service, handle them in others, with wildcard patterns and async support. - When to use: Use
@hazeljs/event-emitterto decouple in-process components (e.g., order created → send email + update analytics). Use@hazeljs/kafkafor cross-service event streaming instead. - Key concepts:
EventEmitterModule,@OnEvent()listener decorator,EventEmitter2(injectable), wildcard patterns, namespaces, async listeners. - Dependencies:
@hazeljs/core,eventemitter2. - Common patterns: Register
EventEmitterModule→ emit events witheventEmitter.emit('order.created', data)→ handle with@OnEvent('order.created')on service methods. - Common mistakes: Using events for cross-service communication (use Kafka or queues instead); not handling errors in async event listeners; creating circular event chains.
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. Familiar Enterprise Patterns
Follows standard event-driven patterns used in major TypeScript frameworks for an easy transition.
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 { Service } from '@hazeljs/core';
import { EventEmitterService } from '@hazeljs/event-emitter';
@Service()
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 { Service } from '@hazeljs/core';
import { OnEvent } from '@hazeljs/event-emitter';
@Service()
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 { Service } from '@hazeljs/core';
import { EventEmitterService } from '@hazeljs/event-emitter';
@Service()
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 { Service } from '@hazeljs/core';
import { OnEvent } from '@hazeljs/event-emitter';
@Service()
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.
What's Next?
- Learn about Cron for scheduled tasks that can emit events
- Explore Queue for async job processing with events
- Check out Kafka for distributed event streaming
Recipes
Recipe: Emit and Listen to Domain Events
// File: src/orders/order.events.ts
import { Service } from '@hazeljs/core';
import { EventEmitter, On } from '@hazeljs/event-emitter';
@Service()
export class OrderEvents {
constructor(private readonly emitter: EventEmitter) {}
async orderCreated(order: { id: string; total: number }) {
this.emitter.emit('order.created', order);
}
}
@Service()
export class OrderNotifier {
@On('order.created')
async sendConfirmation(order: { id: string; total: number }) {
console.log(`Order ${order.id} confirmed — $${order.total}`);
// Send email, push notification, etc.
}
}
Recipe: Cache Invalidation via Events
// File: src/cache/cache-invalidator.service.ts
import { Service } from '@hazeljs/core';
import { On } from '@hazeljs/event-emitter';
import { CacheService } from '@hazeljs/cache';
@Service()
export class CacheInvalidator {
constructor(private readonly cache: CacheService) {}
@On('product.updated')
async invalidateProductCache(data: { productId: string }) {
await this.cache.del(`product:${data.productId}`);
await this.cache.del('products:all');
}
}
Related Resources
- Kafka Package – Distributed event streaming
- Queue Package – Background job processing
- Cron Package – Scheduled task events
- Cache Package – Event-driven cache invalidation