UNPKG

@fraktalio/fmodel-ts

Version:

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

186 lines (185 loc) 8.65 kB
/** * `IDecider` represents the main decision-making algorithm. * It has three generic parameters `C`, `S`, `E` , representing the type of the values that `IDecider` may contain or use. * `IDecider` can be specialized for any type `C` or `S` or `E` because these types does not affect its behavior. * `IDecider` behaves the same for `C`=`Int` or `C`=`YourCustomType`, for example. * * `IDecider` is a pure domain interface. * * @typeParam C - Command * @typeParam S - State * @typeParam E - Event * * @param decide - A function/lambda that takes command of type `C` and input state of type `S` as parameters, and returns/emits the list of output events `E[]`> * @param evolve - A function/lambda that takes input state of type `S` and input event of type `E` as parameters, and returns the output/new state `S` * @param initialState - A starting point / An initial state of type `S` * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ export interface IDecider<C, S, E> { readonly decide: (command: C, state: S) => readonly E[]; readonly evolve: (state: S, event: E) => S; readonly initialState: S; } /** * `Decider` is a datatype that represents the main decision-making algorithm. * It has three generic parameters `C`, `S`, `E` , representing the type of the values that `Decider` may contain or use. * `Decider` can be specialized for any type `C` or `S` or `E` because these types does not affect its behavior. * `Decider` behaves the same for `C`=`Int` or `C`=`YourCustomType`, for example. * * `Decider` is a pure domain component. * * @typeParam C - Command type * @typeParam S - State type * @typeParam E - Event type * @param decide - A function/lambda that takes command of type `C` and input state of type `S` as parameters, and returns/emits the list of output events `E[]`> * @param evolve - A function/lambda that takes input state of type `S` and input event of type `E` as parameters, and returns the output/new state `S` * @param initialState - A starting point / An initial state of type `S` * * @author Иван Дугалић / Ivan Dugalic / @idugalic * * @example * ```typescript * export const orderDecider: Decider<OrderCommand, Order | null, OrderEvent> = * new Decider<OrderCommand, Order | null, OrderEvent>( * (command, currentState) => { * switch (command.kind) { * case "CreateOrderCommand": * return currentState == null * ? [ * { * version: 1, * decider: "Order", * kind: "OrderCreatedEvent", * id: command.id, * restaurantId: command.restaurantId, * menuItems: command.menuItems, * final: false, * }, * ] * : [ * { * version: 1, * decider: "Order", * kind: "OrderNotCreatedEvent", * id: command.id, * restaurantId: command.restaurantId, * menuItems: command.menuItems, * final: false, * reason: "Order already exist!", * }, * ]; * case "MarkOrderAsPreparedCommand": * return currentState !== null * ? [ * { * version: 1, * decider: "Order", * kind: "OrderPreparedEvent", * id: currentState.orderId, * final: false, * }, * ] * : [ * { * version: 1, * decider: "Order", * kind: "OrderNotPreparedEvent", * id: command.id, * reason: "Order does not exist!", * final: false, * }, * ]; * default: * // Exhaustive matching of the command type * const _: never = command; * return []; * } * }, * (currentState, event) => { * switch (event.kind) { * case "OrderCreatedEvent": * return { * orderId: event.id, * restaurantId: event.restaurantId, * menuItems: event.menuItems, * status: "CREATED", * }; * case "OrderNotCreatedEvent": * return currentState; * case "OrderPreparedEvent": * return currentState !== null * ? { * orderId: currentState.orderId, * restaurantId: currentState.restaurantId, * menuItems: currentState.menuItems, * status: "PREPARED", * } * : currentState; * case "OrderNotPreparedEvent": * return currentState; * default: * // Exhaustive matching of the event type * const _: never = event; * return currentState; * } * }, * null, * ); * ``` */ export declare class Decider<C, S, E> implements IDecider<C, S, E> { readonly decide: (command: C, state: S) => readonly E[]; readonly evolve: (state: S, event: E) => S; readonly initialState: S; constructor(decide: (command: C, state: S) => readonly E[], evolve: (state: S, event: E) => S, initialState: S); /** * Contra (Left) map on C/Command parameter - Contravariant * * @typeParam Cn - New Command */ mapContraOnCommand<Cn>(f: (cn: Cn) => C): Decider<Cn, S, E>; /** * Dimap on E/Event parameter - Profunctor * * @typeParam En - New Event */ dimapOnEvent<En>(fl: (en: En) => E, fr: (e: E) => En): Decider<C, S, En>; /** * Dimap on S/State parameter - Profunctor * * @typeParam Sn - New State */ dimapOnState<Sn>(fl: (sn: Sn) => S, fr: (s: S) => Sn): Decider<C, Sn, E>; /** * Combine multiple Deciders into one Decider - Monoid * * State/S is combined via `intersection / (S & S2)`. It only makes sense if S ans S2 are objects, not primitives. * Check alternative method `combineViaTuples` * * Intersections provide more flexibility and can handle more complex scenarios, * while tuples are more straightforward and may be more suitable for simple cases. * * Flexibility: If you anticipate needing to access individual components of the combined state separately, using tuples might be more appropriate, as it allows you to maintain separate types for each component. However, if you primarily need to treat the combined state as a single entity with all properties accessible at once, intersections might be more suitable. * * Readability: Consider which approach makes your code more readable and understandable to other developers who may be working with your codebase. Choose the approach that best communicates your intentions and the structure of your data. * * Compatibility: Consider the compatibility of your chosen approach with other libraries, frameworks, or tools you're using in your TypeScript project. Some libraries or tools might work better with one approach over the other. */ combine<C2, S2, E2>(decider2: Decider<C2, S2, E2>): Decider<C | C2, S & S2, E | E2>; /** * Combine multiple Deciders into one Decider - Monoid * * State/S is combined via `tuples / [S, S2]`. Check alternative method `combine` * * Tuples are more straightforward and may be more suitable for simple cases, * while intersections provide more flexibility and can handle more complex scenarios. * * 1. Flexibility: If you anticipate needing to access individual components of the combined state separately, using tuples might be more appropriate, as it allows you to maintain separate types for each component. However, if you primarily need to treat the combined state as a single entity with all properties accessible at once, intersections might be more suitable. * * 2. Readability: Consider which approach makes your code more readable and understandable to other developers who may be working with your codebase. Choose the approach that best communicates your intentions and the structure of your data. * * 3. Compatibility: Consider the compatibility of your chosen approach with other libraries, frameworks, or tools you're using in your TypeScript project. Some libraries or tools might work better with one approach over the other. */ combineViaTuples<C2, S2, E2>(decider2: Decider<C2, S2, E2>): Decider<C | C2, readonly [S, S2], E | E2>; }