// Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { hasEventType } from '../events/eventTypeDecorator.js'; import { getEventSourceTypeFor } from '../events/eventSourceTypeDecorator.js'; import { getDeclaredEventStreamTypeFor } from '../events/eventStreamTypeDecorator.js'; import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js'; import type { IEventLog } from '../eventSequences/IEventLog.js'; import type { EventContext } from '../events/EventContext.js'; import type { ReactorResultHandler } from './ReactorResultHandler.js'; /** * Represents the outcome of appending a reactor handler's returned side-effect events. */ export interface ReactorSideEffectResult { /** Whether every side-effect event appended successfully. */ readonly isSuccess: boolean; /** Constraint violation and error messages, populated when {@link isSuccess} is false. */ readonly errors: string[]; } const noSideEffects: ReactorSideEffectResult = { isSuccess: true, errors: [] }; /** Dispatch to the optional application hook first, preserving the SDK event append when it declines. */ export async function dispatchReactorSideEffects(eventLog: IEventLog, result: unknown, context: EventContext, reactorType: Function, eventStore: string, namespace: string, handler?: ReactorResultHandler): Promise { if (await handler?.(result, context, reactorType, eventStore, namespace)) return; const appended = await appendReactorSideEffects(eventLog, result, context.eventSourceId, context.eventStreamType ?? 'Default', context.eventStreamId ?? context.eventSourceId, getEventSourceTypeFor(reactorType), getDeclaredEventStreamTypeFor(reactorType)); if (!appended.isSuccess) throw new Error(`Reactor side effect failed to append: ${appended.errors.join('; ')}`); } /** * Determines whether a value is shaped like an {@link EventForEventSourceId} — a plain object * carrying its own `eventSourceId` and `event` — rather than an event object returned directly. */ function isEventForEventSourceId(value: unknown): value is EventForEventSourceId { if (typeof value !== 'object' || value === null) { return false; } const candidate = value as Record; return typeof candidate.eventSourceId === 'string' && typeof candidate.event === 'object' && candidate.event !== null && hasEventType((candidate.event as object).constructor as Function); } /** * Appends the events a `@reactor`-decorated class's handler method returned as side effects, in * a single atomic `AppendMany` call — never one append per item, which would neither be atomic * nor efficient. * @param eventLog - The {@link IEventLog} to append the side effects to. * @param handlerResult - Whatever the reactor handler method returned: `undefined`/`void`, a * single event object, an array of events, a single {@link EventForEventSourceId}, an array of * them, or a mix of bare events and {@link EventForEventSourceId} entries. * @param triggeringEventSourceId - The event source id of the event that triggered the reactor, * used as the target for bare event returns. * @param triggeringEventStreamType - The event stream type of the triggering event, used for bare * event returns. * @param triggeringEventStreamId - The event stream identifier of the triggering event, used for * bare event returns. * @param reactorEventSourceType - Non-empty reactor source type metadata for bare event returns. * @param reactorEventStreamType - Non-empty declared reactor stream type metadata for bare event returns, * overriding the triggering stream type. Omitted metadata preserves the triggering stream type. * @returns The outcome of the append. Each {@link EventForEventSourceId} entry keeps its own * target metadata; bare events use the triggering target with the reactor's declared types. */ export async function appendReactorSideEffects( eventLog: IEventLog, handlerResult: unknown, triggeringEventSourceId: string, triggeringEventStreamType: string, triggeringEventStreamId: string, reactorEventSourceType?: string, reactorEventStreamType?: string ): Promise { if (handlerResult === undefined || handlerResult === null) { return noSideEffects; } const events = normalizeReactorSideEffects(handlerResult, triggeringEventSourceId, triggeringEventStreamType, triggeringEventStreamId, reactorEventSourceType, reactorEventStreamType); if (events.length === 0) { return noSideEffects; } const results = await eventLog.appendMany(events); const errors = results .filter(result => !result.isSuccess) .flatMap(result => [ ...result.constraintViolations.map(violation => violation.message), ...result.errors.map(error => error.message) ]); return { isSuccess: errors.length === 0, errors }; } /** Production return-shape normalization; unknown values are ignored by production dispatch. */ export function normalizeReactorSideEffects(handlerResult: unknown, triggeringEventSourceId: string, triggeringEventStreamType: string, triggeringEventStreamId: string, reactorEventSourceType?: string, reactorEventStreamType?: string): EventForEventSourceId[] { const items = Array.isArray(handlerResult) ? handlerResult : [handlerResult]; const events: EventForEventSourceId[] = []; for (const item of items) { if (isEventForEventSourceId(item)) { events.push(item); } else if (typeof item === 'object' && item !== null && hasEventType(item.constructor as Function)) { events.push({ eventSourceId: triggeringEventSourceId, event: item, eventStreamType: reactorEventStreamType || triggeringEventStreamType, ...(reactorEventSourceType ? { eventSourceType: reactorEventSourceType } : {}), eventStreamId: triggeringEventStreamId, subject: triggeringEventSourceId }); } // Anything else — undefined array entries, plain objects that are neither a registered // event type nor EventForEventSourceId-shaped — is not a recognized side-effect and is // silently ignored, the same as a handler that returns Task/void. } return events; }