DocumentationReference

HazelJS Event Emitter Package

npm downloads

@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-emitter decouples 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-emitter to decouple in-process components (e.g., order created → send email + update analytics). Use @hazeljs/kafka for 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 with eventEmitter.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 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. 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

  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

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');
  }
}