UNPKG

arvo-event-handler

Version:

Type-safe event handler system with versioning, telemetry, and contract validation for distributed Arvo event-driven architectures, featuring routing and multi-handler support.

165 lines (164 loc) 8.13 kB
import { type SpanOptions } from '@opentelemetry/api'; import { type ArvoContract, type ArvoEvent } from 'arvo-core'; import AbstractArvoEventHandler from '../AbstractArvoEventHandler'; import type { ArvoEventHandlerOpenTelemetryOptions } from '../types'; import type { ArvoEventHandlerFunction, IArvoEventHandler } from './types'; /** * `ArvoEventHandler` is the foundational component for building stateless, * contract-bound services in the Arvo system. * * It enforces strict contract validation, version-aware handler resolution, * and safe, observable event emission — all while maintaining type safety, * traceability, and support for multi-domain workflows. * * ## What It Does * - Ensures incoming events match the contract's `type` and `dataschema` * - Resolves the correct contract version using `dataschema` * - Validates input and output data via Zod schemas * - Executes the version-specific handler function * - Emits one or more response events based on the handler result * - Supports multi-domain broadcasting via `domain[]` on the emitted events * - Automatically emits system error events (`sys.*.error`) on failure * - Integrates deeply with OpenTelemetry for tracing and observability * * ## Error Boundaries * ArvoEventHandler enforces a clear separation between: * * - **Violations** — structural, schema, or config errors that break the contract. * These are thrown and must be handled explicitly by the caller. * * - **System Errors** — runtime exceptions during execution that are caught and * emitted as standardized `sys.<contract>.error` events. * * ## Domain Broadcasting * The handler supports multi-domain event distribution. When the handler * returns an event with a `domain` array, it is broadcast to one or more * routing contexts. * * ### System Error Domain Control * By default, system error events are broadcast into the source event’s domain, * the handler’s contract domain, and the `null` domain. This fallback ensures errors * are visible across all relevant contexts. Developers can override this behavior * using the optional `systemErrorDomain` field to specify an explicit set of * domain values, including symbolic constants from {@link ArvoDomain}. * * ### Supported Domain Values: * - A **concrete domain string** like `'audit.orders'` or `'human.review'` * - `null` to emit with no domain (standard internal flow) * - A **symbolic reference** from {@link ArvoDomain} * * ### Domain Resolution Rules: * - Each item in the `domain` array is resolved via {@link resolveEventDomain} * - Duplicate domains are deduplicated before emitting * - If `domain` is omitted entirely, Arvo defaults to `[null]` * * ### Example: * ```ts * return { * type: 'evt.user.registered', * data: { ... }, * domain: ['analytics', ArvoDomain.FROM_TRIGGERING_EVENT, null] * }; * ``` * This would emit at most 3 copies of the event, domained to: * - `'analytics'` * - the domain of the incoming event * - no domain (default) * * ### Domain Usage Guidance * * > **Avoid setting `contract.domain` unless fully intentional.** * 99% emitted event should default to `null` (standard processing pipeline). * * Contract-level domains enforce implicit routing for every emitted event * in that handler, making the behavior harder to override and debug. * * Prefer: * - Explicit per-event `domain` values in handler output * - Using `null` or symbolic constants to control domain cleanly * * ## When to Use Domains * Use domains when handling for specialized contexts: * - `'human.review'` → for human-in-the-loop steps * - `'analytics.workflow'` → to pipe events into observability systems * - `'external.partner.sync'` → to route to external services */ export default class ArvoEventHandler<TContract extends ArvoContract> extends AbstractArvoEventHandler { /** Contract instance that defines the event schema and validation rules */ readonly contract: TContract; /** Computational cost metric associated with event handling operations */ readonly executionunits: number; /** OpenTelemetry configuration for event handling spans */ readonly spanOptions: SpanOptions; /** Version-specific event handler implementation map */ readonly handler: ArvoEventHandlerFunction<TContract>; /** The source identifier for events produced by this handler */ get source(): TContract['type']; readonly systemErrorDomain?: (string | null)[]; /** * The contract-defined domain for this handler, used as the default domain for emitted events. * Can be overridden by individual handler implementations for cross-domain workflows. * Returns null if no domain is specified, indicating standard processing context. */ get domain(): string | null; /** * Initializes a new ArvoEventHandler instance with the specified contract and configuration. * Validates handler implementations against contract versions during initialization. * * The constructor ensures that handler implementations exist for all supported contract * versions and configures OpenTelemetry span attributes for monitoring event handling. * * @param param - Handler configuration including contract, execution units, and handler implementations * @throws When handler implementations are missing for any contract version */ constructor(param: IArvoEventHandler<TContract>); /** * Processes an incoming event according to the handler's contract specifications. This method * handles the complete lifecycle of event processing including validation, execution, error * handling, and multi-domain event broadcasting, while maintaining detailed telemetry through OpenTelemetry. * * @param event - The incoming event to process * @param opentelemetry - Configuration for OpenTelemetry context inheritance, defaults to inheriting from the event * @returns Promise resolving to a structured result containing an array of output events * @returns Structured response containing: * - `events`: Array of events to be emitted (may contain multiple events per handler output due to domain broadcasting) * * @throws {ContractViolation} when input or output event data violates the contract schema, * or when event emission fails due to invalid data * @throws {ConfigViolation} when event type doesn't match contract type, when the * contract version expected by the event does not exist * in handler configuration, or when contract URI mismatch occurs * @throws {ExecutionViolation} for explicitly handled runtime errors that should bubble up */ execute(event: ArvoEvent, opentelemetry?: ArvoEventHandlerOpenTelemetryOptions): Promise<{ events: ArvoEvent[]; }>; /** * Provides access to the system error event schema configuration. * * The schema defines the structure of error events emitted during execution failures. * These events are automatically generated when runtime errors occur and follow a * standardized format for consistent error handling across the system. * * Error events follow the naming convention: `sys.<contract-type>.error` * * @example * For a contract handling 'com.user.create' events, system error events * will have the type 'sys.com.user.create.error' * * @returns The error event schema containing type and validation rules */ get systemErrorSchema(): import("arvo-core").ArvoContractRecord<`sys.${string}.error`, import("zod").ZodObject<{ errorName: import("zod").ZodString; errorMessage: import("zod").ZodString; errorStack: import("zod").ZodNullable<import("zod").ZodString>; }, "strip", import("zod").ZodTypeAny, { errorName: string; errorMessage: string; errorStack: string | null; }, { errorName: string; errorMessage: string; errorStack: string | null; }>>; }