Docs/event busv1.0.0-rc.1

Event Definitions

An EventDefinition is the schema-first descriptor for a named event. Before any code can publish or subscribe to an event, its definition must be registered in the event registry. Definitions provide the name, category, version, payload schema, and documentation metadata that the bus uses to validate payloads and that tooling uses to generate catalogues.

Key Concepts

  • Schema-first — defining events before writing handlers ensures that all publishers and handlers agree on the payload shape at compile time and at runtime.
  • Versioning — every definition carries a version field. Increment it when the payload shape changes in a breaking way to distinguish events during migrations.
  • Registry — definitions are stored in a module-level singleton map. Registration happens at import time via registerEventDefinition or @AIEventDefinition.
  • Type safety — EventDefinition<TPayload> is generic. Passing a definition to publishWithDefinition constrains the payload to TPayload at the TypeScript level.

EventDefinition<TPayload> Interface

interface EventDefinition<TPayload = any> {
  name: string;              // Unique event name; convention: 'domain.noun.verb'
  category: EventCategory;   // 'domain' | 'system' | 'ai'
  version: number;           // Integer version; start at 1
  schema?: Schema;           // Optional runtime payload validation schema
  description?: string;      // Human-readable purpose of this event
  tags?: string[];           // Labels for filtering and cataloguing
  owner?: string;            // Team or module responsible for this event
  docsUrl?: string;          // Link to extended documentation
  samplePayload?: TPayload;  // Example payload for documentation and tooling
}

EventCategory:

type EventCategory = 'domain' | 'system' | 'ai';

Registry Functions

registerEventDefinition

import { registerEventDefinition } from '@tagna/udiot';

registerEventDefinition(definition: EventDefinition): void

Stores the definition under definition.name. Re-registering the same name overwrites the previous entry silently.

getEventDefinition

import { getEventDefinition } from '@tagna/udiot';

getEventDefinition(name: string): EventDefinition | undefined

Returns the definition for the given name, or undefined if not registered. BaseEventBus.publish calls this internally and throws EventValidationError when it returns undefined.

listEventDefinitions

import { listEventDefinitions } from '@tagna/udiot';

listEventDefinitions(): EventDefinition[]

Returns all registered definitions. Useful for building event catalogues and documentation generators.

resetEventRegistry

import { resetEventRegistry } from '@tagna/udiot';

resetEventRegistry(): void

Clears all definitions, handlers, providers, suggestions, and diagnostics history. Use in test teardown.

API Reference

Function Signature Description
registerEventDefinition (definition: EventDefinition) => void Register an event definition
getEventDefinition (name: string) => EventDefinition | undefined Look up by name
listEventDefinitions () => EventDefinition[] All registered definitions
registerEventHandler (definition: EventHandlerDefinition) => void Register a handler
listEventHandlers (eventName?: string) => EventHandlerDefinition[] All handlers, optionally filtered
resetEventRegistry () => void Clear the registry (testing)

Basic Usage Example

import 'reflect-metadata';
import { registerEventDefinition, EventDefinition } from '@tagna/udiot';

// --- Payload types ---

interface UserRegisteredPayload {
  userId: string;
  email: string;
  plan: 'free' | 'pro' | 'enterprise';
  registeredAt: number;
}

interface OrderPlacedPayload {
  orderId: string;
  userId: string;
  totalAmount: number;
  currency: string;
  lineItems: Array<{ sku: string; quantity: number; unitPrice: number }>;
}

interface PaymentProcessedPayload {
  paymentId: string;
  orderId: string;
  amount: number;
  currency: string;
  provider: 'stripe' | 'paypal' | 'razorpay';
  status: 'succeeded' | 'failed';
}

// --- Event definitions ---

export const UserRegisteredEvent: EventDefinition<UserRegisteredPayload> = {
  name: 'user.registered',
  category: 'domain',
  version: 1,
  description: 'Emitted when a user completes registration',
  tags: ['user', 'auth'],
  owner: 'auth-team',
  samplePayload: {
    userId: 'usr_001',
    email: 'alice@example.com',
    plan: 'free',
    registeredAt: 1700000000000,
  },
};

export const OrderPlacedEvent: EventDefinition<OrderPlacedPayload> = {
  name: 'order.placed',
  category: 'domain',
  version: 2,
  description: 'Emitted when a customer places an order',
  tags: ['order', 'commerce'],
  owner: 'commerce-team',
};

export const PaymentProcessedEvent: EventDefinition<PaymentProcessedPayload> = {
  name: 'payment.processed',
  category: 'domain',
  version: 1,
  description: 'Emitted after a payment attempt (success or failure)',
  tags: ['payment', 'billing'],
  owner: 'billing-team',
};

// --- Register all at module load time ---

registerEventDefinition(UserRegisteredEvent);
registerEventDefinition(OrderPlacedEvent);
registerEventDefinition(PaymentProcessedEvent);

Common Patterns

Type-safe publish via exported definitions

Export definitions from a central module and import them where needed:

// events/index.ts
export { UserRegisteredEvent, OrderPlacedEvent, PaymentProcessedEvent };

// auth/auth.service.ts
import { getGlobalEventBus } from '@tagna/udiot';
import { UserRegisteredEvent } from '../events/index.js';

const bus = getGlobalEventBus();

// TypeScript knows the payload shape from UserRegisteredEvent's generic parameter
await bus.publishWithDefinition(UserRegisteredEvent, {
  userId: newUser.id,
  email: newUser.email,
  plan: newUser.plan,
  registeredAt: Date.now(),
});

Payload schema validation

Attach a Schema object to validate payloads at runtime. The Schema and SchemaValidator types come from @tagna/udiot's internal schema system:

import { registerEventDefinition, EventDefinition } from '@tagna/udiot';

const OrderPlacedEvent: EventDefinition<OrderPlacedPayload> = {
  name: 'order.placed',
  category: 'domain',
  version: 2,
  schema: {
    type: 'object',
    properties: {
      orderId: { type: 'string', required: true },
      totalAmount: { type: 'number', min: 0 },
    },
  },
};

registerEventDefinition(OrderPlacedEvent);

If the payload fails schema validation, publishWithDefinition throws EventValidationError before any handler is called.

Listing all events for a catalogue

import { listEventDefinitions } from '@tagna/udiot';

const catalogue = listEventDefinitions().map(def => ({
  name: def.name,
  version: def.version,
  category: def.category,
  description: def.description ?? '(no description)',
  owner: def.owner ?? 'unknown',
}));

console.table(catalogue);

Suggested events via @AISuggestedEvent

When an AI model suggests a new event should exist, attach the suggestion to the proposing service using the @AISuggestedEvent decorator. Suggestions are stored in the registry and surfaced in tooling without creating an actual event definition.

import { AISuggestedEvent } from '@tagna/udiot';

class OrderService {
  @AISuggestedEvent({
    name: 'order.shipment_delayed',
    description: 'Emitted when a shipment is delayed beyond the expected date',
    reasoning: 'Several handlers check for shipping delays on order.updated; a dedicated event would decouple them',
  })
  async checkShipmentStatus(orderId: string): Promise<void> {
    // … existing logic …
  }
}

Access suggestions:

import { listSuggestedEvents } from '@tagna/udiot';

const suggestions = listSuggestedEvents();

Integration Notes

  • Registration is global and happens at import time. If your bundler tree-shakes the definitions module, definitions will not be registered. Import the definitions module explicitly in your entry point.
  • The version field is metadata only; BaseEventBus does not use it for routing or compatibility checks. You are responsible for versioning handlers when definitions evolve.
  • samplePayload is not used for validation. It is metadata for documentation generators and AI tooling.

Pitfalls

  • Re-registering an event definition with the same name silently overwrites the previous one. If two modules register different definitions under the same name, the one imported last wins. Use a naming convention such as {domain}.{entity}.{verb} to avoid collisions.
  • getEventDefinition returns undefined for unregistered names rather than throwing. If you call it defensively, check the return value before using it.
  • resetEventRegistry clears handlers as well as definitions. In test suites that share a global bus, re-register handlers after calling reset.
From the canonical UDIOT public documentation.Try it in the playground ↗