// https://khalilstemmler.com/articles/typescript-domain-driven-design/chain-business-logic-domain-events/ import { AggregateRoot } from "../aggregate-root"; import { UniqueID } from "../value-objects"; import { IDomainEvent } from "./domain-event.interface"; // biome-ignore lint/complexity/noStaticOnlyClass: export class DomainEvents { private static handlersMap: { [key: string]: Array<(event: IDomainEvent) => void> } = {}; private static markedAggregates: AggregateRoot[] = []; /** * @method markAggregateForDispatch * @static * @desc Called by aggregate root objects that have created domain * events to eventually be dispatched when the infrastructure commits * the unit of work. */ public static markAggregateForDispatch(aggregate: AggregateRoot): void { const aggregateFound = !!DomainEvents.findMarkedAggregateByID(aggregate.id); if (!aggregateFound) { DomainEvents.markedAggregates.push(aggregate); } } /** * @method dispatchAggregateEvents * @static * @private * @desc Call all of the handlers for any domain events on this aggregate. */ private static dispatchAggregateEvents(aggregate: AggregateRoot): void { aggregate.domainEvents.forEach((event: IDomainEvent) => DomainEvents.dispatch(event)); } /** * @method removeAggregateFromMarkedDispatchList * @static * @desc Removes an aggregate from the marked list. */ private static removeAggregateFromMarkedDispatchList(aggregate: AggregateRoot): void { const index = DomainEvents.markedAggregates.findIndex((a) => a.equals(aggregate)); DomainEvents.markedAggregates.splice(index, 1); } /** * @method findMarkedAggregateByID * @static * @desc Finds an aggregate within the list of marked aggregates. */ private static findMarkedAggregateByID(id: UniqueID): AggregateRoot { let found!: AggregateRoot; for (const aggregate of DomainEvents.markedAggregates) { if (aggregate.id.equals(id)) { found = aggregate; } } return found; } /** * @method dispatchEventsForAggregate * @static * @desc When all we know is the ID of the aggregate, call this * in order to dispatch any handlers subscribed to events on the * aggregate. */ public static dispatchEventsForAggregate(id: UniqueID): void { const aggregate = DomainEvents.findMarkedAggregateByID(id); if (aggregate) { DomainEvents.dispatchAggregateEvents(aggregate); aggregate.clearDomainEvents(); DomainEvents.removeAggregateFromMarkedDispatchList(aggregate); } } /** * @method register * @static * @desc Register a handler to a domain event. */ public static register(callback: (event: IDomainEvent) => void, eventClassName: string): void { if (!Object.prototype.hasOwnProperty.call(DomainEvents.handlersMap, eventClassName)) { DomainEvents.handlersMap[eventClassName] = []; } DomainEvents.handlersMap[eventClassName].push(callback); } /** * @method clearHandlers * @static * @desc Useful for testing. */ public static clearHandlers(): void { DomainEvents.handlersMap = {}; } /** * @method clearMarkedAggregates * @static * @desc Useful for testing. */ public static clearMarkedAggregates(): void { DomainEvents.markedAggregates = []; } /** * @method dispatch * @static * @desc Invokes all of the subscribers to a particular domain event. */ private static dispatch(event: IDomainEvent): void { const eventClassName: string = event.constructor.name; if (Object.hasOwn(DomainEvents.handlersMap, eventClassName)) { const handlers: any[] = DomainEvents.handlersMap[eventClassName]; for (const handler of handlers) { handler(event); } } } }