Docs/event busv1.0.0-rc.1

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:

  1. Instantiates the class with new target().
  2. Binds instance.handle to instance.
  3. Calls registerEventHandler on the global registry.
  4. Calls getGlobalEventBus().registerHandler so 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

  • @AIEventHandler calls new 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 with setGlobalEventBus(newBus) after import, the handlers from @AIEventHandler will only be on the old bus instance. Re-register manually or call hydrateHandlersFromRegistry (currently a no-op reserved for future use).
  • @AIEventBusProvider also calls new target() eagerly. The class is instantiated once and stored in the registry.

Pitfalls

  • If @AIEventHandler is applied to a class that does not have a handle method, it throws EventValidationError at decoration time (module import), which will prevent your application from starting. Ensure the method exists before the decorator fires.
  • @AISuggestedEvent stores 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.
From the canonical UDIOT public documentation.Try it in the playground ↗