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 functionpublish(event: UntypedDomainEventType): Promise<PublishResultInterface[]>- Publish an event to all registered handlersreceiveFromRemote(rawEvent: string): Promise<void>- (Hybrid only) Receive and process events from remote sources
DomainEvent
id: string- Unique event identifiertimestamp: Date- Event creation timestamptype: TType- Event type (must be implemented by subclass)payload: TPayload- Event payload data
PublishResultInterface
status: "fulfilled" | "rejected"- Handler execution statushandlerName: string- Name of the handlerreason?: string- Error reason if rejected
EventSubscription
[Symbol.dispose](): void- Unsubscribe from all registered handlersdisposed: 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
- Commands vs Queries: Use commands for write operations, queries for read operations
- Event handlers: Keep event handlers idempotent when possible
- Error handling: Both Command and Query buses track errors via monitoring services
- Type safety: Leverage TypeScript's type system for compile-time safety
- Separation of concerns: Commands modify state, queries read state, events notify about state changes