Skip to main content
Version: v1.0

Event Bus

The Event Bus implements the publish/subscribe pattern for event-driven architectures. Events represent things that have happened in the system and can have multiple handlers.

Example: User Events

import {
DomainEvent,
InMemoryEventBus,
HybridEventBus,
type LocalEventHandler,
} from "@dxbox/use-less-react/classes";

// Define event payload
interface UserCreatedPayload {
userId: string;
email: string;
}

// Create event class
class UserCreatedEvent extends DomainEvent<"UserCreated", UserCreatedPayload> {
get type(): "UserCreated" {
return "UserCreated";
}
}

// Define event handlers
const sendWelcomeEmailHandler: LocalEventHandler<UserCreatedEvent> = async (
event,
) => {
const { email } = event.payload;
await emailService.sendWelcomeEmail(email);
};

const updateAnalyticsHandler: LocalEventHandler<UserCreatedEvent> = async (
event,
) => {
await analyticsService.track("user_created", {
userId: event.payload.userId,
timestamp: event.timestamp,
});
};

// Setup event bus
const eventBus = new InMemoryEventBus({
monitoringService: myMonitoringService,
});

// Register multiple handlers for the same event
const unsubscribe1 = eventBus.registerLocalHandler(
"UserCreated",
sendWelcomeEmailHandler,
);
const unsubscribe2 = eventBus.registerLocalHandler(
"UserCreated",
updateAnalyticsHandler,
);

// Publish event (all handlers will be called)
const event = new UserCreatedEvent({
userId: "user-123",
email: "user@example.com",
});

const results = await eventBus.publish(event);
// results contains execution status for each handler

// Unsubscribe handlers when no longer needed
unsubscribe1();
unsubscribe2();

Automatic Unsubscription with EventSubscription

For classes using @AutoDispose, you can use EventSubscription to automatically unsubscribe event handlers when the class is disposed:

import {
EventSubscription,
InMemoryEventBus,
type LocalEventHandler,
} from "@dxbox/use-less-react/classes";
import { AutoDispose } from "@dxbox/use-less-react/classes";

@AutoDispose
class NotificationManager implements Disposable {
private eventSubscriptions = new EventSubscription(() => {
return eventBus.registerLocalHandler("UserCreated", (event) => {
this.handleUserCreated(event);
});
});
// eventSubscriptions is automatically detected and disposed (has Symbol.dispose)

constructor(private eventBus: InMemoryEventBus) {}

private async handleUserCreated(event: UserCreatedEvent): Promise<void> {
// Send welcome notification
}

[Symbol.dispose](): void {
// Custom cleanup logic if needed
}
}

For multiple subscriptions, return a function that unsubscribes all handlers:

@AutoDispose
class NotificationManager implements Disposable {
private eventSubscriptions = new EventSubscription(() => {
const unsub1 = eventBus.registerLocalHandler(
"UserCreated",
this.handleUserCreated.bind(this),
);
const unsub2 = eventBus.registerLocalHandler(
"UserDeleted",
this.handleUserDeleted.bind(this),
);
const unsub3 = eventBus.registerLocalHandler(
"UserUpdated",
this.handleUserUpdated.bind(this),
);

return () => {
unsub1();
unsub2();
unsub3();
};
});

constructor(private eventBus: InMemoryEventBus) {}

[Symbol.dispose](): void {
// Custom cleanup logic if needed
}

private async handleUserCreated(event: UserCreatedEvent): Promise<void> {
// Send welcome notification
}

private async handleUserDeleted(event: UserDeletedEvent): Promise<void> {
// Cleanup notifications
}

private async handleUserUpdated(event: UserUpdatedEvent): Promise<void> {
// Update notification preferences
}
}

When the class instance is disposed (e.g., when using useDisposable in React), all event subscriptions are automatically unsubscribed, preventing memory leaks.

Hybrid Event Bus

The Hybrid Event Bus extends the InMemory Event Bus with support for remote event publishing:

import { HybridEventBus } from "@dxbox/use-less-react/classes";

// Create remote publisher
const remotePublisher: RemotePublisherInterface = {
async sendRemote(serializedEvent: string): Promise<void> {
// Send to remote service (e.g., via HTTP, WebSocket, etc.)
await fetch("/api/events", {
method: "POST",
body: serializedEvent,
});
},
};

// Setup hybrid event bus
const hybridEventBus = new HybridEventBus({
inMemoryBus: new InMemoryEventBus({ monitoringService }),
remotePublisher,
monitoringService,
});

// Publish local event
await hybridEventBus.publish(new UserCreatedEvent({ ... }));

// Publish remote event (mark with isRemote)
class RemoteUserCreatedEvent
extends UserCreatedEvent
implements RemoteDomainEventMarkerInterface
{
isRemote = true as const;
}

await hybridEventBus.publish(
new RemoteUserCreatedEvent({ userId: "user-123", email: "..." })
);

API

InMemoryEventBus / HybridEventBus

  • registerLocalHandler<THandlerOrEvent>(eventType, handler): () => void - Register an event handler, returns unsubscribe function
  • publish(event: UntypedDomainEventType): Promise<PublishResultInterface[]> - Publish an event to all registered handlers
  • receiveFromRemote(rawEvent: string): Promise<void> - (Hybrid only) Receive and process events from remote sources

DomainEvent

  • id: string - Unique event identifier
  • timestamp: Date - Event creation timestamp
  • type: TType - Event type (must be implemented by subclass)
  • payload: TPayload - Event payload data

PublishResultInterface

  • status: "fulfilled" | "rejected" - Handler execution status
  • handlerName: string - Name of the handler
  • reason?: string - Error reason if rejected

EventSubscription

  • [Symbol.dispose](): void - Unsubscribe from all registered handlers
  • disposed: boolean - Whether the subscription has been disposed

Usage with @AutoDispose:

@AutoDispose
class MyManager implements Disposable {
private subscriptions = new EventSubscription(() => {
// Register handlers and return unsubscribe function
return eventBus.registerLocalHandler("EventType", handler);
});
// subscriptions is automatically detected and disposed (has Symbol.dispose)

[Symbol.dispose](): void {
// Custom cleanup logic if needed
}
}

Best Practices

  1. Commands vs Queries: Use commands for write operations, queries for read operations
  2. Event handlers: Keep event handlers idempotent when possible
  3. Error handling: Both Command and Query buses track errors via monitoring services
  4. Type safety: Leverage TypeScript's type system for compile-time safety
  5. Separation of concerns: Commands modify state, queries read state, events notify about state changes