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
versionfield. 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
registerEventDefinitionor@AIEventDefinition. - Type safety —
EventDefinition<TPayload>is generic. Passing a definition topublishWithDefinitionconstrains the payload toTPayloadat 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
versionfield is metadata only;BaseEventBusdoes not use it for routing or compatibility checks. You are responsible for versioning handlers when definitions evolve. samplePayloadis 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. getEventDefinitionreturnsundefinedfor unregistered names rather than throwing. If you call it defensively, check the return value before using it.resetEventRegistryclears handlers as well as definitions. In test suites that share a global bus, re-register handlers after calling reset.