Event Bus Decorators
The event bus provides four decorators for the class-based registration pattern. They allow event definitions, handlers, and provider implementations to be declared declaratively alongside the classes that own them.
Available Decorators
| Decorator | Target | Purpose |
|---|---|---|
@AIEventDefinition |
Class | Register an EventDefinition and associate it with a class |
@AIEventHandler |
Class | Register a class as a handler for a named event |
@AIEventBusProvider |
Class | Register a class as an event bus provider implementation |
@AISuggestedEvent |
Class or Method | Record an AI suggestion for a new event |
reflect-metadata must be imported once in your entry point.
@AIEventDefinition(definition) — Class Decorator
Registers an EventDefinition in the global registry and attaches it as reflect-metadata on the target class. Use this to co-locate the definition with a class that conceptually owns the event.
import { AIEventDefinition, EventDefinition } from '@tagna/udiot';
interface OrderPlacedPayload {
orderId: string;
userId: string;
totalAmount: number;
}
@AIEventDefinition({
name: 'order.placed',
category: 'domain',
version: 1,
description: 'Emitted when a customer places an order',
tags: ['order', 'commerce'],
owner: 'commerce-team',
})
class OrderPlacedEventClass {}
This is equivalent to calling registerEventDefinition(definition) at module load time, but attaches the definition as metadata on the class for inspection via Reflect.getMetadata('event:definition', OrderPlacedEventClass).
@AIEventHandler(config) — Class Decorator
Instantiates the decorated class and registers its handle method as a handler on both the global registry and the global event bus. The target class must implement a handle(event: AIEvent): Promise<void> | void method.
import { AIEventHandler, AIEvent } from '@tagna/udiot';
@AIEventHandler({
event: string; // Event name to subscribe to (required)
priority?: number; // Higher runs first; default 0
maxAttempts?: number; // Retry count for async handlers; default 3
retryDelayMs?: number; // Delay between retries; default 1000 ms
filters?: EventFilterOptions;
description?: string;
aiGenerated?: boolean;
})
class WelcomeEmailHandler {
async handle(event: AIEvent<UserRegisteredPayload>): Promise<void> {
await sendWelcomeEmail(event.payload.email);
}
}
Registration behaviour:
- Instantiates the class with
new target(). - Binds
instance.handletoinstance. - Calls
registerEventHandleron the global registry. - Calls
getGlobalEventBus().registerHandlerso the handler is immediately active.
The class is instantiated once at decoration time, not per event invocation.
@AIEventBusProvider(metadata) — Class Decorator
Registers a class as an event bus provider in the global provider registry. Instantiates the class with new target() and stores the instance under metadata.name.
import { AIEventBusProvider } from '@tagna/udiot';
@AIEventBusProvider({
name: string; // Provider name key
description?: string;
supportsAsync: boolean;
supportsRetry: boolean;
maxThroughput?: number;
})
class MyCustomEventBusProvider extends BaseEventBus {
protected async dispatch<TPayload>(event: AIEvent<TPayload>, options: PublishOptions): Promise<void> {
// … custom dispatch logic …
}
}
Access the registered instance:
import { getEventBusProvider } from '@tagna/udiot';
const provider = getEventBusProvider('my-custom-provider');
@AISuggestedEvent(metadata) — Class or Method Decorator
Records that an AI model or analysis tool suggested a new event should exist. The suggestion is stored in the registry without creating an actual EventDefinition. Can be applied to a class or a method.
import { AISuggestedEvent } from '@tagna/udiot';
class OrderService {
@AISuggestedEvent({
name: 'order.shipment_delayed',
description: 'Should be emitted when a shipment exceeds its expected delivery date',
reasoning: 'Multiple handlers probe order.updated for delay conditions; a dedicated event would decouple them',
})
async processShipment(orderId: string): Promise<void> {
// existing logic
}
}
When applied to a method, the location recorded in the suggestion is ClassName.methodName. When applied to a class, it is the class name.
Access suggestions:
import { listSuggestedEvents } from '@tagna/udiot';
const suggestions = listSuggestedEvents();
// [{ name: 'order.shipment_delayed', description: '…', reasoning: '…', createdAt: …, location: 'OrderService.processShipment' }]
Manual Registration Pattern (without decorators)
If you prefer to avoid decorators or need more control over handler instantiation, use registerHandler directly on the bus:
import {
getGlobalEventBus,
registerEventDefinition,
registerEventHandler,
AIEvent,
EventHandlerContext,
} from '@tagna/udiot';
// Define and register the event
const UserLoggedInEvent = {
name: 'user.logged_in',
category: 'domain' as const,
version: 1,
};
registerEventDefinition(UserLoggedInEvent);
// Define the handler function
async function onUserLoggedIn(
event: AIEvent<{ userId: string; ip: string }>,
ctx: EventHandlerContext
): Promise<void> {
if (ctx.attempt > 1) {
console.warn(`Retry attempt ${ctx.attempt} for user.logged_in handler`);
}
await auditLog.record({ event: 'login', userId: event.payload.userId });
}
// Register on both the registry and the global bus
const handlerDef = {
eventName: 'user.logged_in',
handler: onUserLoggedIn,
options: { priority: 20, maxAttempts: 2 },
};
registerEventHandler(handlerDef);
getGlobalEventBus().registerHandler(handlerDef);
This pattern is preferred when:
- You need to inject dependencies into the handler (constructor parameters).
- You want lazy instantiation rather than eager instantiation at decorator time.
- You are writing tests where you want full control over the handler lifecycle.
Class-based Handler Pattern (without @AIEventHandler)
You can apply the class-based pattern without the decorator by registering manually in a bootstrap function:
import { getGlobalEventBus, AIEvent } from '@tagna/udiot';
class AnalyticsEventHandler {
constructor(private readonly analyticsClient: AnalyticsClient) {}
async handle(event: AIEvent<UserRegisteredPayload>): Promise<void> {
await this.analyticsClient.track('user_registered', {
userId: event.payload.userId,
plan: event.payload.plan,
});
}
}
// Bootstrap
function registerHandlers(analytics: AnalyticsClient): void {
const handler = new AnalyticsEventHandler(analytics);
getGlobalEventBus().registerHandler({
eventName: 'user.registered',
handler: handler.handle.bind(handler),
options: { priority: 5, description: 'Track registration in analytics' },
});
}
Complete Example
import 'reflect-metadata';
import {
AIEventDefinition,
AIEventHandler,
getGlobalEventBus,
registerEventDefinition,
AIEvent,
} from '@tagna/udiot';
// 1. Define the event (and register it via decorator)
@AIEventDefinition({
name: 'user.registered',
category: 'domain',
version: 1,
description: 'A new user has completed registration',
tags: ['user', 'auth'],
})
class UserRegisteredDefinition {}
// 2. Declare handlers (instantiated and registered at import time)
@AIEventHandler({
event: 'user.registered',
priority: 10,
description: 'Send welcome email to new user',
})
class WelcomeEmailHandler {
async handle(event: AIEvent<{ userId: string; email: string }>): Promise<void> {
await sendWelcomeEmail(event.payload.email);
}
}
@AIEventHandler({
event: 'user.registered',
priority: 5,
description: 'Track new registration in analytics',
})
class AnalyticsHandler {
async handle(event: AIEvent<{ userId: string; email: string }>): Promise<void> {
await analytics.track('signup', { userId: event.payload.userId });
}
}
// 3. Publish from AuthService (no knowledge of handlers)
class AuthService {
async register(email: string, password: string): Promise<string> {
const userId = await createUser(email, password);
await getGlobalEventBus().publish('user.registered', {
userId,
email,
});
return userId;
}
}
Integration Notes
@AIEventHandlercallsnew target()at decoration time (module import). If the class requires constructor arguments that are not available at import time, use the manual registration pattern instead.- Both the registry (
registerEventHandler) and the global bus (getGlobalEventBus().registerHandler) receive the handler from@AIEventHandler. If you replace the global bus withsetGlobalEventBus(newBus)after import, the handlers from@AIEventHandlerwill only be on the old bus instance. Re-register manually or callhydrateHandlersFromRegistry(currently a no-op reserved for future use). @AIEventBusProvideralso callsnew target()eagerly. The class is instantiated once and stored in the registry.
Pitfalls
- If
@AIEventHandleris applied to a class that does not have ahandlemethod, it throwsEventValidationErrorat decoration time (module import), which will prevent your application from starting. Ensure the method exists before the decorator fires. @AISuggestedEventstores suggestions in memory. They do not survive process restarts and are not persisted. Use them as development-time hints only.- Decorators run at module import time. If two modules import each other and both use event decorators, circular dependency issues may cause decorators to run before the imported types are available. Restructure by extracting event definition modules with no circular dependencies.