TypeScript

Events

Event emitter and listeners

The event system in NitroStack enables decoupled communication between components. Events allow you to emit signals when actions occur and handle them asynchronously without tight coupling between producers and consumers.

Emitting Events

Call emitEvent() with an event name and a payload. Every handler registered for that name receives the payload.

From Tool Handlers

Emit an event after a tool has done its work, so other parts of the app can react.

import { ToolDecorator as Tool, ExecutionContext, emitEvent } from '@nitrostack/core';

export class OrderTools {
  @Tool({ name: 'create_order' })
  async createOrder(input: CreateOrderInput, ctx: ExecutionContext) {
    const order = await this.orderService.create(input);

    // Emit event for async processing
    emitEvent('order.created', {
      orderId: order.id,
      userId: ctx.auth?.subject,
      total: order.total,
      itemCount: order.items.length,
      timestamp: new Date().toISOString()
    });

    return order;
  }

  @Tool({ name: 'cancel_order' })
  async cancelOrder(input: { orderId: string; reason: string }, ctx: ExecutionContext) {
    const order = await this.orderService.cancel(input.orderId, input.reason);

    emitEvent('order.cancelled', {
      orderId: order.id,
      userId: ctx.auth?.subject,
      reason: input.reason,
      refundAmount: order.total,
      timestamp: new Date().toISOString()
    });

    return order;
  }
}

From Services

Services can emit events too, which keeps tools thin.

import { Injectable, emitEvent } from '@nitrostack/core';

@Injectable()
export class PaymentService {
  async processPayment(orderId: string, amount: number, userId: string): Promise<Payment> {
    const payment = await this.executePayment(orderId, amount);

    // Emit event using the global function
    emitEvent('payment.processed', {
      paymentId: payment.id,
      orderId,
      amount,
      userId,
      status: payment.status,
      timestamp: new Date().toISOString()
    });

    return payment;
  }

  async refundPayment(paymentId: string, reason: string): Promise<Refund> {
    const refund = await this.executeRefund(paymentId);

    emitEvent('payment.refunded', {
      refundId: refund.id,
      paymentId,
      amount: refund.amount,
      reason,
      timestamp: new Date().toISOString()
    });

    return refund;
  }
}

Listening to Events

Mark a method with @OnEvent('name') on an injectable class to run it whenever that event is emitted.

Using @OnEvent Decorator

The handler receives the event's payload.

import { Injectable, OnEvent } from '@nitrostack/core';

@Injectable()
export class NotificationService {
  constructor(
    private emailService: EmailService,
    private pushService: PushService
  ) {}

  @OnEvent('order.created')
  async handleOrderCreated(data: OrderCreatedEvent): Promise<void> {
    console.log(`Order created: ${data.orderId}`);

    // Send confirmation email
    await this.emailService.sendOrderConfirmation(data.userId, {
      orderId: data.orderId,
      total: data.total,
      itemCount: data.itemCount
    });
  }

  @OnEvent('order.cancelled')
  async handleOrderCancelled(data: OrderCancelledEvent): Promise<void> {
    console.log(`Order cancelled: ${data.orderId}`);

    // Send cancellation notification
    await this.emailService.sendOrderCancellation(data.userId, {
      orderId: data.orderId,
      reason: data.reason,
      refundAmount: data.refundAmount
    });
  }

  @OnEvent('payment.processed')
  async handlePaymentProcessed(data: PaymentProcessedEvent): Promise<void> {
    console.log(`Payment processed: ${data.paymentId}`);

    // Send receipt
    await this.emailService.sendPaymentReceipt(data.userId, {
      paymentId: data.paymentId,
      amount: data.amount
    });
  }
}

Multiple Handlers for Same Event

Any number of handlers can listen for the same event. Each runs independently.

@Injectable()
export class AnalyticsService {
  @OnEvent('order.created')
  async trackOrderCreation(data: OrderCreatedEvent): Promise<void> {
    await this.analyticsClient.track('order_created', {
      orderId: data.orderId,
      value: data.total
    });
  }
}

@Injectable()
export class InventoryService {
  @OnEvent('order.created')
  async reserveInventory(data: OrderCreatedEvent): Promise<void> {
    // Reserve inventory for the order
    await this.reserveItems(data.orderId);
  }
}

@Injectable()
export class AuditService {
  @OnEvent('order.created')
  async auditOrderCreation(data: OrderCreatedEvent): Promise<void> {
    await this.auditLog.record({
      action: 'order.created',
      entityId: data.orderId,
      userId: data.userId,
      timestamp: data.timestamp
    });
  }
}

Event Patterns

Consistent names make events easy to find and hard to mistype.

Naming Convention

Use dot notation with resource and action:

// Pattern: resource.action
'user.created'
'user.updated'
'user.deleted'

'order.created'
'order.confirmed'
'order.shipped'
'order.delivered'
'order.cancelled'

'payment.initiated'
'payment.processed'
'payment.failed'
'payment.refunded'

'inventory.reserved'
'inventory.released'
'inventory.depleted'

Event Lifecycle

Model an entity's lifecycle as a series of events, one per state change.

// Entity lifecycle events
@OnEvent('order.created')      // New order placed
@OnEvent('order.confirmed')    // Order confirmed by merchant
@OnEvent('order.processing')   // Order being prepared
@OnEvent('order.shipped')      // Order shipped
@OnEvent('order.delivered')    // Order delivered
@OnEvent('order.completed')    // Order finalized
@OnEvent('order.cancelled')    // Order cancelled

// Each handler can trigger subsequent events
@Injectable()
export class OrderLifecycleService {
  @OnEvent('order.shipped')
  async handleShipped(data: OrderShippedEvent): Promise<void> {
    // Update tracking
    await this.trackingService.initiate(data.orderId, data.trackingNumber);

    // Estimate delivery
    const estimatedDelivery = await this.calculateDelivery(data);

    // Emit follow-up event
    emitEvent('order.delivery_scheduled', {
      orderId: data.orderId,
      estimatedDelivery
    });
  }
}

Error Events

Emit an event when something fails, so a separate handler can alert, retry, or compensate.

@Injectable()
export class ErrorHandlingService {
  @OnEvent('payment.failed')
  async handlePaymentFailure(data: PaymentFailedEvent): Promise<void> {
    console.log(`Payment failed for order: ${data.orderId}`);

    // Notify customer
    await this.notificationService.sendPaymentFailure(data.userId, {
      orderId: data.orderId,
      reason: data.failureReason
    });

    // Update order status
    await this.orderService.markPaymentFailed(data.orderId);

    // Log for analysis
    await this.analyticsService.trackPaymentFailure(data);
  }
}

Event Data Design

An event's payload is a contract between the code that emits it and every handler.

Event Interface Definitions

Define a TypeScript interface for each event's payload.

// events/order.events.ts
export interface OrderCreatedEvent {
  orderId: string;
  userId: string;
  total: number;
  itemCount: number;
  items: Array<{
    productId: string;
    quantity: number;
    price: number;
  }>;
  timestamp: string;
}

export interface OrderCancelledEvent {
  orderId: string;
  userId: string;
  reason: string;
  refundAmount: number;
  timestamp: string;
}

export interface OrderShippedEvent {
  orderId: string;
  userId: string;
  trackingNumber: string;
  carrier: string;
  estimatedDelivery: string;
  timestamp: string;
}

// events/payment.events.ts
export interface PaymentProcessedEvent {
  paymentId: string;
  orderId: string;
  userId: string;
  amount: number;
  currency: string;
  method: 'card' | 'bank' | 'wallet';
  status: 'completed' | 'pending';
  timestamp: string;
}

export interface PaymentFailedEvent {
  paymentId: string;
  orderId: string;
  userId: string;
  amount: number;
  failureReason: string;
  retryable: boolean;
  timestamp: string;
}

Include Essential Context

Put everything a handler needs in the payload, so it doesn't have to query for it again.

// Include enough context for handlers to process without additional queries
emitEvent('user.registered', {
  // Identity
  userId: user.id,

  // Key attributes
  email: user.email,
  name: user.name,
  plan: user.plan,

  // Source tracking
  referralCode: input.referralCode,
  registrationSource: input.source,

  // Metadata
  timestamp: new Date().toISOString(),
  requestId: ctx.requestId
});

On this page