/** * High-level classification for public event consumers. * * - `domain` events represent business milestones that other modules or * external integrations may reasonably care about. * - `internal` events are service/process signals that remain useful for * subscribers, diagnostics, and automation, but are not part of the core * business language. */ export type EventCategory = "domain" | "internal"; /** * Where an event was emitted from. This helps consumers understand whether * an event originated from a workflow boundary, a lower-level service, or a * transport/runtime edge. */ export type EventSource = "workflow" | "service" | "route" | "subscriber" | "system"; /** * Optional metadata attached to an emitted event. * * Templates and adapters may extend this with runtime-specific fields such as * correlation identifiers or delivery handles. */ export interface EventMetadata { category?: EventCategory; source?: EventSource; correlationId?: string; causationId?: string; [key: string]: unknown; } /** * Standard event envelope delivered to subscribers. */ export interface EventEnvelope { /** Event name, following the `.` convention. */ name: string; /** Business payload emitted by the caller. */ data: TData; /** Optional metadata for source/taxonomy/tracing. */ metadata?: TMetadata; /** ISO timestamp indicating when the event was emitted. */ emittedAt: string; } /** * Event handler callback invoked when a subscribed event is emitted. */ export interface EventHandlerContext { /** * Emits nested events with the current runtime scheduler. Inline handlers * still complete before the nested emit resolves; deferrable handlers stay * on the scheduler supplied by the original emitter. */ eventBus: EventBus; } export type EventHandler = (event: EventEnvelope, context?: EventHandlerContext) => Promise | void; /** * Subscription handle returned from {@link EventBus.subscribe}. * * Call `unsubscribe()` to remove the handler. */ export interface Subscription { unsubscribe(): void; } /** * Abstract event bus interface. Implementations live in templates or plugins. * * Adapter examples: * - In-process (default, ships with core) * - Cloudflare Queues — edge-native * - Postgres-backed durable queue — for refund-saga-grade durability * * Event naming convention: `.` in dot-case. * Examples: `booking.created`, `quote.accepted`, `payment.received`. */ /** * Per-subscription options. */ export interface SubscribeOptions { /** * Inline handlers complete before `emit()` resolves even when the * emitter supplies a deferral scheduler (see {@link EmitOptions}). * Use for the rare subscriber whose side effects must be visible to * the code that follows the emit (e.g. read-after-write within the * same request). Default `false` — handlers are deferrable. */ inline?: boolean; } /** * Per-emit options. Call sites normally omit this; runtime adapters * (e.g. `@voyant-travel/hono`'s request-scoped bus) supply `schedule` so * deferrable handlers run after the HTTP response instead of blocking * it. */ export interface EmitOptions { /** * Receives a single promise covering all deferrable (non-`inline`) * handlers for this emit. When provided, `emit()` resolves after the * `inline` handlers only; the scheduler owns keeping the runtime * alive for the rest (Workers: `executionCtx.waitUntil`). When * omitted, all handlers complete before `emit()` resolves. */ schedule?: (pending: Promise) => void; /** * Transactional-outbox store for this emit. When present, the * envelope is persisted BEFORE any handler runs; after all handlers * settle the row is completed (every handler succeeded) or failed * (at least one error/timeout — the store schedules the retry). * A `null` return from `insert` means the event was already captured * (duplicate `metadata.eventId`) and delivery is skipped entirely. */ store?: OutboxEventStore; } /** * Minimal persistence contract the event bus needs for durable emits. * `@voyant-travel/db/outbox` provides the Postgres implementation; the bus * itself stays storage-agnostic. */ export interface OutboxEventStore { /** * Persist the envelope before delivery. Returns the stored record id, * or `null` when an event with the same `metadata.eventId` already * exists (the original capture owns delivery). */ insert(envelope: EventEnvelope): Promise<{ id: string; } | null>; /** Every handler succeeded — mark delivered. */ complete(id: string): Promise; /** * At least one handler failed or timed out. The store owns retry * scheduling (backoff) and dead-lettering. */ fail(id: string, error: string): Promise; } /** Outcome of delivering one envelope to all its subscribers. */ export interface DeliveryResult { /** Handlers invoked. */ attempted: number; /** Handlers that threw or timed out. */ failed: number; errors: string[]; } /** Stable, unique event id for envelope metadata / outbox dedup. */ export declare function generateEventId(): string; export interface EventBusOptions { /** * Per-handler timeout in milliseconds. A handler that exceeds it is * logged and no longer awaited — it is NOT cancelled (JS can't), so * it may still finish in the background. Defaults to 15s, which * bounds how long one slow third-party subscriber (CMS sync, * e-invoicing API) can hold an emit. Set `false` to disable. */ handlerTimeoutMs?: number | false; /** * Invoked when a subscriber throws or times out. The bus already logs to * console and keeps the "subscribers are fire-and-forget" contract (the * emitter and sibling handlers are unaffected); this is the hook a runtime * uses to route the failure to an error reporter (RFC voyant#1553). It MUST * NOT throw — the bus guards the call defensively, but keep it best-effort. * `error` is an `Error` for timeouts and the thrown value otherwise. */ onSubscriberError?: (event: string, error: unknown) => void; } export interface EventBus { /** Emit an event. Fire-and-forget; subscribers cannot affect the emitter. */ emit(event: string, data: TData, metadata?: TMetadata, options?: EmitOptions): Promise; /** Subscribe to an event by name. Returns an unsubscribe handle. */ subscribe(event: string, handler: EventHandler, options?: SubscribeOptions): Subscription; /** * Deliver an existing envelope to ALL its subscribers (inline and * deferrable alike), reporting per-handler failures instead of only * logging them. Used by outbox drains for redelivery — it does NOT * persist anything. Optional so third-party bus implementations * remain assignable; drains fall back to `emit` (fire-and-forget, * counted as success) when absent. */ deliver?(envelope: EventEnvelope): Promise; } /** * Create an in-process event bus. * * Handlers run **in parallel** — they are independent observers by * contract, so one slow subscriber doesn't serialize behind another. * Errors thrown by a handler are caught and logged and never affect the * emitter or sibling handlers ("subscribers are fire-and-forget"). * Each handler is bounded by {@link EventBusOptions.handlerTimeoutMs}. * * When the emitter passes {@link EmitOptions.schedule}, handlers not * marked `inline` are handed to the scheduler as one promise and * `emit()` resolves without waiting for them — this is how the HTTP * runtime moves subscriber work (third-party syncs, notifications) * after the response. */ export declare function createEventBus(options?: EventBusOptions): EventBus;