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 EventEmitterService anywhere 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

  1. EventEmitterModule: Configures the event emitter with options (wildcard, delimiter, etc.)
  2. EventEmitterService: Injectable service that extends EventEmitter2 for emitting and listening
  3. @OnEvent Decorator: Declarative way to mark methods as event listeners
  4. registerListenersFromProvider(s): Registers @OnEvent handlers 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

  1. Use descriptive event names: Prefer order.created over orderCreate for namespacing.

  2. Register listeners early: Call registerListenersFromProviders after app bootstrap, before emitting events.

  3. Type your payloads: Define interfaces for event payloads for better type safety.

  4. Handle errors: Use suppressErrors: false for critical listeners, or handle errors in async handlers.

  5. Use wildcards sparingly: order.* is useful for logging; avoid ** unless you need to catch everything.

  6. Keep handlers focused: Each @OnEvent handler 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