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