// Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import 'reflect-metadata'; import { Constructor } from '@cratis/fundamentals'; import { ReducerId } from './ReducerId.js'; import { DecoratorType, TypeDiscoverer } from '../types/index.js'; /** Metadata key used to store reducer information on a class. */ const REDUCER_METADATA_KEY = 'chronicle:reducer'; /** * Metadata stored on a reducer class. */ export interface ReducerMetadata { /** The unique identifier for the reducer. */ readonly id: ReducerId; /** The optional explicit event sequence identifier. */ readonly eventSequenceId: string | undefined; /** The optional read model type produced by the reducer. */ readonly readModel: Constructor | undefined; /** * Whether the reducer is active. An inactive (passive) reducer is registered with the * Chronicle Kernel but does not automatically observe events — it can still be invoked * on demand. Defaults to true. */ readonly isActive: boolean; } /** * TypeScript decorator that marks a class as a reducer and associates it with a unique * identifier. This is the TypeScript equivalent of the C# `[Reducer]` attribute. * * Reducers observe events from an event sequence and fold them into a read model. * Each handler method receives the event, the current state (or undefined), and an * optional context, and returns the new state. * * @param id - The unique identifier for the reducer. Defaults to the class name if omitted. * @param eventSequenceId - Optional explicit event sequence identifier. * @param readModel - Optional read model type produced by the reducer. * @param isActive - Whether the reducer is active (automatically observes events). Defaults to true. * A passive reducer is registered but must be invoked on demand rather than observing automatically. * @returns A class decorator. * * @example * ```typescript * @reducer() * class EmployeeReducer { * async employeeHired(event: EmployeeHired, state?: EmployeeState): Promise { * return { ...state, name: `${event.firstName} ${event.lastName}` }; * } * } * ``` */ export function reducer(id: string = '', eventSequenceId?: string, readModel?: Constructor, isActive: boolean = true): ClassDecorator { return (target: object) => { const constructor = target as Function; const reducerId = new ReducerId(id || constructor.name); const metadata: ReducerMetadata = { id: reducerId, eventSequenceId, readModel, isActive }; Reflect.defineMetadata(REDUCER_METADATA_KEY, metadata, target); TypeDiscoverer.default.register( DecoratorType.Reducer, constructor as Constructor, reducerId.value ); if (readModel) { TypeDiscoverer.default.register( DecoratorType.ReadModel, readModel, readModel.name ); } }; } /** * Gets the {@link ReducerMetadata} associated with a class decorated with {@link reducer}. * @param target - The class constructor to retrieve the metadata for. * @returns The associated ReducerMetadata, or undefined if not decorated. */ export function getReducerMetadata(target: Function): ReducerMetadata | undefined { return Reflect.getMetadata(REDUCER_METADATA_KEY, target); } /** * Checks whether a class has been decorated with {@link reducer}. * @param target - The class constructor to check. * @returns True if the class has a reducer decorator; false otherwise. */ export function isReducer(target: Function): boolean { return Reflect.hasMetadata(REDUCER_METADATA_KEY, target); }