UNPKG

@fraktalio/fmodel-ts

Version:

Functional domain modeling with TypeScript. Optimized for event sourcing and CQRS

141 lines 11.8 kB
/* * Copyright 2023 Fraktalio D.O.O. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " * AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific * language governing permissions and limitations under the License. */ /** * An abstract algorithm to compute new events based on the old events and the command being handled. */ export class EventComputation { constructor(decider) { this.decider = decider; this.initialState = decider.initialState; } decide(command, state) { return this.decider.decide(command, state); } evolve(state, event) { return this.decider.evolve(state, event); } computeNewEvents(events, command) { const currentState = events.reduce(this.decider.evolve, this.decider.initialState); return this.decider.decide(command, currentState); } } /** * An abstract algorithm to compute new events based on the old events and the command being handled. * It returns all the events, including the events created by handling commands which are triggered by Saga - orchestration included. */ export class EventOrchestratingComputation { constructor(decider, saga) { this.decider = decider; this.saga = saga; this.initialState = decider.initialState; } decide(command, state) { return this.decider.decide(command, state); } evolve(state, event) { return this.decider.evolve(state, event); } react(event) { return this.saga.react(event); } computeNewEventsInternally(events, command) { const currentState = events.reduce(this.decider.evolve, this.decider.initialState); return this.decider.decide(command, currentState); } async computeNewEvents(events, command, fetch) { // eslint-disable-next-line functional/no-let let resultingEvents = this.computeNewEventsInternally(events, command); await asyncForEach(resultingEvents.flatMap((evt) => this.saga.react(evt)), async (cmd) => { const newEvents = this.computeNewEvents((await fetch(cmd)).map((evt) => evt).concat(resultingEvents), cmd, fetch); resultingEvents = resultingEvents.concat(await newEvents); }); return resultingEvents; } } /** * Event sourcing aggregate is using/delegating a `EventSourcingAggregate.decider` of type `IDecider`<`C`, `S`, `E`> to handle commands and produce events. * In order to handle the command, aggregate needs to fetch the current state (represented as a list of events) via `IEventRepository.fetchEvents` function, and then delegate the command to the `EventSourcingAggregate.decider` which can produce new event(s) as a result. * * * Produced events are then stored via `IEventRepository.save` function. * * @typeParam C - Commands of type `C` that this aggregate can handle * @typeParam S - Aggregate state of type `S` * @typeParam E - Events of type `E` that this aggregate can publish * @typeParam E - Version * @typeParam CM - Command Metadata * @typeParam EM - Event Metadata * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ export class EventSourcingAggregate extends EventComputation { constructor(decider, eventRepository) { super(decider); this.eventRepository = eventRepository; } async fetch(command) { return this.eventRepository.fetch(command); } async versionProvider(event) { return this.eventRepository.versionProvider(event); } async save(events, commandMetadata, versionProvider) { return this.eventRepository.save(events, commandMetadata, versionProvider); } async handle(command) { const currentEvents = await this.eventRepository.fetch(command); return this.eventRepository.save(this.computeNewEvents(currentEvents, command), command, async () => currentEvents[currentEvents.length - 1]); } } /** * Event sourcing orchestrating aggregate is using/delegating a `EventSourcingOrchestratingAggregate.decider` of type `IDecider`<`C`, `S`, `E`> to handle commands and produce events. * In order to handle the command, aggregate needs to fetch the current state (represented as a list of events) via `IEventRepository.fetchEvents` function, and then delegate the command to the `EventSourcingOrchestratingAggregate.decider` which can produce new event(s) as a result. * * If the `EventSourcingOrchestratingAggregate.decider` is combined out of many deciders via `combine` function, an optional `EventSourcingOrchestratingAggregate.saga` could be used to react on new events and send new commands to the `EventSourcingOrchestratingAggregate.decider` recursively, in one transaction. * * Produced events are then stored via `IEventRepository.save` function. * * @typeParam C - Commands of type `C` that this aggregate can handle * @typeParam S - Aggregate state of type `S` * @typeParam E - Events of type `E` that this aggregate can publish * @typeParam V - Version * @typeParam CM - Command Metadata * @typeParam EM - Event Metadata * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ export class EventSourcingOrchestratingAggregate extends EventOrchestratingComputation { constructor(decider, eventRepository, saga) { super(decider, saga); this.eventRepository = eventRepository; } async fetch(command) { return this.eventRepository.fetch(command); } async versionProvider(event) { return this.eventRepository.versionProvider(event); } async save(events, commandMetadata, versionProvider) { return this.eventRepository.save(events, commandMetadata, versionProvider); } async handle(command) { const currentEvents = await this.eventRepository.fetch(command); return this.eventRepository.save(await this.computeNewEvents(currentEvents, command, async (cmd) => await this.eventRepository.fetch(cmd)), command, this.versionProvider.bind(this)); } } async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXZlbnRzb3VyY2luZy1hZ2dyZWdhdGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvbGliL2FwcGxpY2F0aW9uL2V2ZW50c291cmNpbmctYWdncmVnYXRlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7OztHQVdHO0FBb0dIOztHQUVHO0FBQ0gsTUFBTSxPQUFnQixnQkFBZ0I7SUFDcEMsWUFBeUMsT0FBMEI7UUFBMUIsWUFBTyxHQUFQLE9BQU8sQ0FBbUI7UUFDakUsSUFBSSxDQUFDLFlBQVksR0FBRyxPQUFPLENBQUMsWUFBWSxDQUFDO0lBQzNDLENBQUM7SUFJRCxNQUFNLENBQUMsT0FBVSxFQUFFLEtBQVE7UUFDekIsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsS0FBSyxDQUFDLENBQUM7SUFDN0MsQ0FBQztJQUVELE1BQU0sQ0FBQyxLQUFRLEVBQUUsS0FBUTtRQUN2QixPQUFPLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssRUFBRSxLQUFLLENBQUMsQ0FBQztJQUMzQyxDQUFDO0lBRVMsZ0JBQWdCLENBQUMsTUFBb0IsRUFBRSxPQUFVO1FBQ3pELE1BQU0sWUFBWSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQ2hDLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUNuQixJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FDMUIsQ0FBQztRQUNGLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsT0FBTyxFQUFFLFlBQVksQ0FBQyxDQUFDO0lBQ3BELENBQUM7Q0FDRjtBQUVEOzs7R0FHRztBQUNILE1BQU0sT0FBZ0IsNkJBQTZCO0lBR2pELFlBQ3FCLE9BQTBCLEVBQzFCLElBQWlCO1FBRGpCLFlBQU8sR0FBUCxPQUFPLENBQW1CO1FBQzFCLFNBQUksR0FBSixJQUFJLENBQWE7UUFFcEMsSUFBSSxDQUFDLFlBQVksR0FBRyxP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