Docs/event busv1.0.0-rc.1

Event Bus Overview

The UDIOT event bus provides a publish-subscribe mechanism for decoupled communication between modules. Publishers emit named events with typed payloads; handlers subscribe to those event names and are invoked when a matching event is published. Neither side holds a direct reference to the other.

Key Concepts

  • Event definition — a typed descriptor (EventDefinition<TPayload>) that gives an event a name, category, version, and optional schema for payload validation.
  • Publisher — any code that calls bus.publish(eventName, payload) or bus.publishWithDefinition(definition, payload). Publishing is always asynchronous.
  • Handler — a function registered with bus.registerHandler(definition). Handlers are called in priority order for every matching event.
  • Telemetry integration — every publish call opens an events.publish span (producer kind). Every handler invocation opens an events.handler span (consumer kind). Metrics are emitted for published events and handler executions.
  • Diagnostics — bus.getDiagnostics() returns the current in-flight count, queue depth, and last dispatch timestamp.

Architecture

Publisher
    │
    │  bus.publish('user.registered', payload)
    ▼
┌─────────────────────────────────────────────┐
│               AIEventBus                     │
│  (BaseEventBus → InMemoryEventBus)           │
│                                             │
│  1. Look up EventDefinition in registry     │
│  2. Validate payload against schema          │
│  3. Build AIEvent with headers               │
│  4. Open events.publish span                │
│  5. Dispatch to registered handlers         │
└──────────────────────────────────────────────┘
    │
    ├──▶ Handler1 (EmailService.onUserRegistered)
    │       └─▶ events.handler span
    │
    ├──▶ Handler2 (AnalyticsService.onUserRegistered)
    │       └─▶ events.handler span
    │
    └──▶ Handler3 (AuditService.onUserRegistered)
            └─▶ events.handler span

When to Use the Event Bus

Use the event bus when:

  • An action in one module needs to trigger work in multiple other modules without knowing which ones.
  • You want to add new behaviour to an existing flow without modifying the code that triggers it.
  • You need audit trails or observability hooks for cross-cutting events.
  • Handler failures should not block the primary flow (use async: true in PublishOptions).

Use direct service calls when:

  • You need the return value from the called code.
  • The interaction is strictly one-to-one and always will be.
  • Ordering and synchronous completion guarantees are required.

Event Categories

type EventCategory = 'domain' | 'system' | 'ai';
Category Description
'domain' Business events: user actions, order state changes, payment events
'system' Infrastructure events: health checks, metric snapshots, cache invalidation
'ai' Model inference results, suggestion generation, AI pipeline events

Telemetry Metrics

Metric Type Labels
events.published counter eventName, category
events.handlers.started counter eventName
events.handlers.failed counter eventName
events.handlers.duration_ms histogram eventName, status (completed or failed)

Error Types

Class Thrown when
EventValidationError Event name not registered, or payload fails schema validation
EventHandlerExecutionError A handler throws and max retry attempts are exhausted
EventProviderNotFoundError A named provider is requested but not registered
  • Bus — AIEventBus, BaseEventBus, InMemoryEventBus, PublishOptions, AIEvent
  • Event Definitions — EventDefinition, createEventDefinition, registry
  • Decorators — @AIEventDefinition, @AIEventHandler, @AIEventBusProvider, @AISuggestedEvent

Pitfalls

  • Every event name must be registered via registerEventDefinition (or @AIEventDefinition) before it can be published. Calling bus.publish('unknown.event', payload) throws EventValidationError.
  • Handlers registered on BaseEventBus are local to that instance. The global bus (obtained via getGlobalEventBus()) has its own handler list. Use one consistently or bridge them explicitly.
  • In test environments, call resetEventRegistry() between tests to clear definitions and handlers. Also call bus.stop() on any InMemoryEventBus instance to clear the internal worker interval and avoid open handles.
From the canonical UDIOT public documentation.Try it in the playground ↗