@eyjs/event-bus
v1.0.0
Published
Event Bus system for EyJS framework
Downloads
67
Maintainers
Readme
@eyjs/event-bus
Event Bus system for the EyJS framework. Provides domain-driven event handling, decorator-based event listeners, and seamless integration with EyJS applications using hexagonal architecture principles.
Features
- 🎯 Domain-Driven Events - Clean separation of domain events and handlers
- 🎨 Decorator-Based Handlers - Simple
@EventHandler()decorator for event listeners - 🏗️ Hexagonal Architecture - Follows clean architecture principles
- 🔧 TypeScript Support - Full TypeScript definitions and type safety
- ⚡ EyJS Integration - Works seamlessly with EyJS dependency injection
- 📝 Event Middleware - Flexible middleware system for event processing
- 🚀 In-Memory Publisher - Built-in in-memory event publisher
- 🔄 Event Store - Centralized event storage and management
Installation
# Using Bun (recommended)
bun add @eyjs/event-bus
# Using npm
npm install @eyjs/event-bus
# Using yarn
yarn add @eyjs/event-bus
# Using pnpm
pnpm add @eyjs/event-busPrerequisites
This package requires:
@eyjs/core@^1.0.4- EyJS core framework
Quick Start
1. Define Domain Events
// domain/events/user-created.event.ts
export class UserCreatedEvent {
constructor(
public readonly userId: string,
public readonly email: string,
public readonly name: string,
public readonly timestamp: Date = new Date()
) {}
}
// domain/events/user-updated.event.ts
export class UserUpdatedEvent {
constructor(
public readonly userId: string,
public readonly changes: Record<string, any>,
public readonly timestamp: Date = new Date()
) {}
}2. Create Event Handlers
import { EventHandler } from '@eyjs/event-bus'
import { Injectable } from '@eyjs/core'
import { UserCreatedEvent, UserUpdatedEvent } from '../domain/events'
@Injectable()
export class UserEventHandler {
@EventHandler(UserCreatedEvent)
async handleUserCreated(event: UserCreatedEvent) {
console.log(`User created: ${event.email}`)
// Send welcome email, create profile, etc.
}
@EventHandler(UserUpdatedEvent)
async handleUserUpdated(event: UserUpdatedEvent) {
console.log(`User updated: ${event.userId}`)
// Update search index, notify followers, etc.
}
}3. Publish Events
import { EventBus } from '@eyjs/event-bus'
import { Injectable } from '@eyjs/core'
import { UserCreatedEvent } from '../domain/events'
@Injectable()
export class UserService {
constructor(private readonly eventBus: EventBus) {}
async createUser(data: { email: string; name: string }) {
const user = await this.userRepository.create(data)
// Publish domain event
await this.eventBus.publish(new UserCreatedEvent(
user.id,
user.email,
user.name
))
return user
}
}API Reference
Core Classes
EventBus
Main event bus for publishing and handling events.
import { EventBus } from '@eyjs/event-bus'
const eventBus = new EventBus()
// Publish an event
await eventBus.publish(new UserCreatedEvent('123', '[email protected]', 'John'))
// Subscribe to events
eventBus.subscribe(UserCreatedEvent, (event) => {
console.log('User created:', event.email)
})@EventHandler(eventType)
Decorator for marking methods as event handlers.
@EventHandler(UserCreatedEvent)
async handleUserCreated(event: UserCreatedEvent) {
// Handle the event
}Event Publisher
InMemoryEventPublisher
Built-in in-memory event publisher.
import { InMemoryEventPublisher } from '@eyjs/event-bus'
const publisher = new InMemoryEventPublisher()
await publisher.publish(new UserCreatedEvent('123', '[email protected]', 'John'))Usage Examples
Basic Event Handling
import { EventHandler, EventBus } from '@eyjs/event-bus'
import { Injectable } from '@eyjs/core'
// Domain Events
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly total: number,
public readonly timestamp: Date = new Date()
) {}
}
export class OrderCancelledEvent {
constructor(
public readonly orderId: string,
public readonly reason: string,
public readonly timestamp: Date = new Date()
) {}
}
// Event Handlers
@Injectable()
export class OrderEventHandler {
@EventHandler(OrderCreatedEvent)
async handleOrderCreated(event: OrderCreatedEvent) {
console.log(`Order ${event.orderId} created for customer ${event.customerId}`)
// Send confirmation email
await this.emailService.sendOrderConfirmation(event.customerId, event.orderId)
// Update inventory
await this.inventoryService.reserveItems(event.orderId)
// Notify warehouse
await this.warehouseService.prepareOrder(event.orderId)
}
@EventHandler(OrderCancelledEvent)
async handleOrderCancelled(event: OrderCancelledEvent) {
console.log(`Order ${event.orderId} cancelled: ${event.reason}`)
// Release inventory
await this.inventoryService.releaseItems(event.orderId)
// Process refund
await this.paymentService.processRefund(event.orderId)
// Notify customer
await this.emailService.sendCancellationNotice(event.orderId)
}
}
// Service using events
@Injectable()
export class OrderService {
constructor(private readonly eventBus: EventBus) {}
async createOrder(data: { customerId: string; items: any[] }) {
const order = await this.orderRepository.create(data)
// Publish domain event
await this.eventBus.publish(new OrderCreatedEvent(
order.id,
order.customerId,
order.total
))
return order
}
async cancelOrder(orderId: string, reason: string) {
await this.orderRepository.update(orderId, { status: 'cancelled' })
// Publish domain event
await this.eventBus.publish(new OrderCancelledEvent(orderId, reason))
}
}Multiple Event Handlers
import { EventHandler } from '@eyjs/event-bus'
import { Injectable } from '@eyjs/core'
@Injectable()
export class NotificationEventHandler {
@EventHandler(UserCreatedEvent)
async sendWelcomeEmail(event: UserCreatedEvent) {
await this.emailService.sendWelcomeEmail(event.email)
}
@EventHandler(UserCreatedEvent)
async createUserProfile(event: UserCreatedEvent) {
await this.profileService.createProfile(event.userId)
}
@EventHandler(UserCreatedEvent)
async addToNewsletter(event: UserCreatedEvent) {
await this.newsletterService.subscribe(event.email)
}
}
@Injectable()
export class AnalyticsEventHandler {
@EventHandler(UserCreatedEvent)
async trackUserRegistration(event: UserCreatedEvent) {
await this.analyticsService.track('user_registered', {
userId: event.userId,
email: event.email,
timestamp: event.timestamp
})
}
}Event Middleware
import { EventMiddleware } from '@eyjs/event-bus'
// Logging middleware
export function loggingMiddleware() {
return async (event: any, next: () => Promise<void>) => {
console.log(`[${new Date().toISOString()}] Processing event: ${event.constructor.name}`)
await next()
console.log(`[${new Date().toISOString()}] Event processed: ${event.constructor.name}`)
}
}
// Error handling middleware
export function errorHandlingMiddleware() {
return async (event: any, next: () => Promise<void>) => {
try {
await next()
} catch (error) {
console.error(`Error processing event ${event.constructor.name}:`, error)
// Could send to error tracking service
}
}
}
// Usage
@Injectable()
export class UserEventHandler {
@EventHandler(UserCreatedEvent)
@Use(loggingMiddleware())
@Use(errorHandlingMiddleware())
async handleUserCreated(event: UserCreatedEvent) {
// Event handling logic
}
}Custom Event Publisher
import { EventPublisher } from '@eyjs/event-bus'
export class RedisEventPublisher implements EventPublisher {
constructor(private readonly redis: Redis) {}
async publish(event: any): Promise<void> {
const channel = `events:${event.constructor.name}`
await this.redis.publish(channel, JSON.stringify(event))
}
}
// Usage
const redisPublisher = new RedisEventPublisher(redis)
const eventBus = new EventBus(redisPublisher)Event Store Integration
import { EventStore } from '@eyjs/event-bus'
@Injectable()
export class EventStoreService {
constructor(private readonly eventStore: EventStore) {}
async saveEvent(event: any): Promise<void> {
await this.eventStore.save({
id: generateId(),
type: event.constructor.name,
data: event,
timestamp: new Date(),
version: 1
})
}
async getEvents(aggregateId: string): Promise<any[]> {
return await this.eventStore.getEvents(aggregateId)
}
}Advanced Usage
Event Sourcing
import { EventHandler, EventStore } from '@eyjs/event-bus'
@Injectable()
export class UserAggregate {
constructor(private readonly eventStore: EventStore) {}
async createUser(data: { email: string; name: string }) {
const event = new UserCreatedEvent(
generateId(),
data.email,
data.name
)
await this.eventStore.saveEvent(event)
return event
}
async updateUser(userId: string, changes: Record<string, any>) {
const event = new UserUpdatedEvent(userId, changes)
await this.eventStore.saveEvent(event)
return event
}
async getUser(userId: string) {
const events = await this.eventStore.getEvents(userId)
return this.replayEvents(events)
}
private replayEvents(events: any[]): User {
let user = new User()
for (const event of events) {
switch (event.type) {
case 'UserCreatedEvent':
user = new User(event.data.userId, event.data.email, event.data.name)
break
case 'UserUpdatedEvent':
Object.assign(user, event.data.changes)
break
}
}
return user
}
}Event Replay
import { EventBus, EventStore } from '@eyjs/event-bus'
@Injectable()
export class EventReplayService {
constructor(
private readonly eventBus: EventBus,
private readonly eventStore: EventStore
) {}
async replayEvents(fromDate: Date, toDate: Date) {
const events = await this.eventStore.getEventsByDateRange(fromDate, toDate)
for (const event of events) {
await this.eventBus.publish(event.data)
}
}
async replayEventsForAggregate(aggregateId: string) {
const events = await this.eventStore.getEvents(aggregateId)
for (const event of events) {
await this.eventBus.publish(event.data)
}
}
}Event Versioning
export class UserCreatedEventV1 {
constructor(
public readonly userId: string,
public readonly email: string,
public readonly name: string
) {}
}
export class UserCreatedEventV2 {
constructor(
public readonly userId: string,
public readonly email: string,
public readonly name: string,
public readonly phoneNumber?: string,
public readonly preferences?: Record<string, any>
) {}
}
@Injectable()
export class UserEventHandler {
@EventHandler(UserCreatedEventV1)
async handleUserCreatedV1(event: UserCreatedEventV1) {
// Handle V1 events
}
@EventHandler(UserCreatedEventV2)
async handleUserCreatedV2(event: UserCreatedEventV2) {
// Handle V2 events with additional fields
}
}Configuration
Event Bus Configuration
import { EventBus, InMemoryEventPublisher } from '@eyjs/event-bus'
const eventBus = new EventBus({
publisher: new InMemoryEventPublisher(),
middleware: [
loggingMiddleware(),
errorHandlingMiddleware()
],
retryPolicy: {
maxRetries: 3,
retryDelay: 1000
}
})Environment Configuration
# Event Bus Configuration
EVENT_BUS_TYPE=in-memory
EVENT_BUS_RETRY_ATTEMPTS=3
EVENT_BUS_RETRY_DELAY=1000
# Event Store Configuration
EVENT_STORE_TYPE=memory
EVENT_STORE_CONNECTION_STRING=redis://localhost:6379Testing
Unit Tests
import { describe, it, expect, beforeEach } from 'bun:test'
import { EventBus, InMemoryEventPublisher } from '@eyjs/event-bus'
import { UserCreatedEvent } from '../domain/events'
describe('EventBus', () => {
let eventBus: EventBus
let publisher: InMemoryEventPublisher
beforeEach(() => {
publisher = new InMemoryEventPublisher()
eventBus = new EventBus({ publisher })
})
it('should publish and handle events', async () => {
let handledEvent: UserCreatedEvent | null = null
eventBus.subscribe(UserCreatedEvent, (event) => {
handledEvent = event
})
const event = new UserCreatedEvent('123', '[email protected]', 'John')
await eventBus.publish(event)
expect(handledEvent).not.toBeNull()
expect(handledEvent?.userId).toBe('123')
expect(handledEvent?.email).toBe('[email protected]')
})
})Integration Tests
import { describe, it, expect } from 'bun:test'
describe('Event Integration', () => {
it('should handle user creation flow', async () => {
const userService = container.get(UserService)
const emailService = container.get(EmailService)
// Mock email service
const sendEmailSpy = spyOn(emailService, 'sendWelcomeEmail')
// Create user (should trigger event)
await userService.createUser({
email: '[email protected]',
name: 'Test User'
})
// Verify event was handled
expect(sendEmailSpy).toHaveBeenCalledWith('[email protected]')
})
})Performance
Optimization Tips
- Use Async Event Handlers
@EventHandler(UserCreatedEvent)
async handleUserCreated(event: UserCreatedEvent) {
// Async operations don't block other handlers
await this.emailService.sendEmail(event.email)
}- Batch Event Processing
@EventHandler(UserCreatedEvent)
async handleUserCreated(event: UserCreatedEvent) {
// Add to batch instead of processing immediately
this.batchProcessor.add(event)
}- Event Filtering
@EventHandler(UserCreatedEvent)
async handleUserCreated(event: UserCreatedEvent) {
// Only process events for specific conditions
if (event.email.endsWith('@company.com')) {
await this.processCompanyUser(event)
}
}Troubleshooting
Common Issues
1. Events not being handled
- Ensure event handlers are registered with EyJS container
- Check that
@EventHandler()decorator is applied correctly - Verify event types match exactly
2. Circular dependencies
- Use dependency injection properly
- Avoid direct imports between event handlers
- Consider using event middleware for cross-cutting concerns
3. Event ordering issues
- Events are processed asynchronously by default
- Use event middleware for ordering if needed
- Consider using event sourcing for strict ordering
Debug Mode
Enable debug logging:
DEBUG=eyjs:event-busContributing
Contributions are welcome! Please read our Contributing Guide for details.
License
This project is licensed under the MIT License - see the LICENSE file for details.
