UNPKG

evcojs

Version:

CQRS with CloudEvents. Segregate read and write code and use cloud events to exchange data between them.

216 lines (215 loc) 8.16 kB
import { randomUUID } from "node:crypto"; let source = "CQRS_DEFAULTSOURCE"; const commands = new Map(); const stateRebuilder = new Map(); const eventHandler = new Map(); const upcastHandler = new Map(); const stateLoader = new Map(); /** * Sets the default source for all commands and events. * * @param {string} src */ export function setSource(src) { source = src; } /** * Registers a upcaster function for a given event type. * * The upcaster function will be called before the event is processed. * The upcaster function takes an event and returns a new event. * The new event will be used instead of the original event. * * @param {string} type - The type of the event. * @param {string} context - The context of the event. * @param {function(CloudEvent<any>): CloudEvent<any>} func - The upcaster function. */ export function registerUpcaster(type, context, func) { const key = createKey(context, type); if (upcastHandler.has(key)) { throw new Error(`upcaster for ${key} already exists`); } upcastHandler.set(key, { type, context, upcast: func }); } /** * Registers an event handler for a specific event type. * * @param {string} type - The type of the event to register the handler for. * @param {function(CloudEvent<any>, any): void} func - The event handler function * that processes the event and optionally the current state. * * The event handler is added to a list of handlers associated with the event type. * If no handlers exist for the given type, a new list is created. */ export function registerEventhandler(type, func) { if (!eventHandler.has(type)) { eventHandler.set(type, []); } eventHandler.get(type).push({ type, eventHandler: func }); } /** * Registers a function that loads historical events for a specific context. * * The function is called with an array of subjects (e.g., '/book/123') and * should return a Promise that resolves with an array of historical events * associated with the given subjects. * * @param context - The context for which the events are loaded. * @param load - The function that loads the events. */ export function registerStateLoadingFunction(context, load) { stateLoader.set(context, { load, context: context }); } /** * Registers a command handler for a specific command type. * * The command handler is a function that validates business rules and * returns new events to be saved persistently. * * @param {string} type - The type of the command to register the handler for. * @param {string} context - The context in which the command is executed. * @param {function(any, any): CloudEvent<any>[]} commandHandler - The command handler function. * The function receives the command and the current state and returns an array of new events. */ export function registerCommandHandler(type, context, commandHandler) { if (commands.has(type)) { throw new Error(`Command handler for ${type} already exists`); } commands.set(type, { type, context, handle: commandHandler }); } /** * Registers a state rebuilder function for a specific event type within a given context. * * The state rebuilder function is responsible for applying the event to the current state * to produce an updated state. It is an essential part of the event sourcing pattern, * allowing the system to reconstruct the state by replaying events. * * @param {string} type - The type of the event for which the state rebuilder is registered. * @param {string} context - The context in which the state rebuilder operates. * @param {function(any): void} builderHandler - The state rebuilder function that applies * the event to the state. * * @throws Will throw an error if a state rebuilder for the given event type and context * already exists. */ export function registerStateRebuilder(type, context, builderHandler) { const key = createKey(context, type); if (stateRebuilder.has(key)) { throw new Error(`State rebuilder for ${key} already exists`); } stateRebuilder.set(key, { type, context, stateRebuilder: builderHandler, }); } /** * Applies an upcaster function to the given event if a matching upcaster is * registered. If no upcaster is registered, the event is passed through unchanged. * * @param {CloudEvent<any>} event - The event to upcast or pass through. * @returns {CloudEvent<any>} The upcasted event if an upcaster is registered, * otherwise the original event. */ function maybeUpcast(event) { const upcaster = upcastHandler.get(event.type); if (upcaster) { return upcaster.upcast(event); } return event; } /** * Executes a command and applies the returned events to the state. * * @param {Command<C>} command - The command to execute. * * @returns {Promise<void>} A promise that resolves when the command has been * executed and the state has been updated. */ export async function handleCommand(command) { const commandHandler = commands.get(command.type); if (commandHandler) { let state = await createState(commandHandler.context, command.subjects); const newEvents = commandHandler.handle(command.data, state); for (const event of newEvents) { if (command.subjects.includes(event.subject)) { state = executeEvent(commandHandler.context, event, state); } } const cloudEvents = newEvents.map((event) => { return { ...event, source: event.source ?? source, id: event.id ?? randomUUID(), time: event.time ?? new Date().toISOString(), specversion: "1.0", data: event.data, }; }); for (const cloudEvent of cloudEvents) { if (eventHandler.has(cloudEvent.type)) { eventHandler .get(cloudEvent.type) .forEach((eh) => eh.eventHandler(cloudEvent, state)); } } return state; } return null; } /** * Reconstructs the state for a given context and subjects by loading and processing historical events. * * @template C - The type of the state to be reconstructed. * * @param {string} context - The unique context for which the state is reconstructed. * @param {string[]} subjects - An array of subjects used to load relevant events. * * @returns {Promise<any>} A promise that resolves to the reconstructed state. * * The function retrieves the state loading function associated with the provided context, * loads historical events for the specified subjects, and applies each event to rebuild * the current state. If no state loading function is found, an empty state is returned. */ export async function createState(context, subjects) { const stateLoadingFunction = stateLoader.get(context); let events = stateLoadingFunction ? await stateLoadingFunction.load(subjects) : []; events = events.map(maybeUpcast); let state = null; for (const event of events) { state = executeEvent(context, event, state); } return state; } /** * Applies an event to the state by executing the associated state rebuilder. * * @param {string} context - The context of the event. * @param {CloudEvent<any>} event - The event to be applied to the state. * @param {any} [state=null] - The current state. * * @returns {any} The updated state. * * If no state rebuilder is registered for the given event type and context, the * event is ignored and the state is returned unchanged. */ function executeEvent(context, event, state) { const rebuilder = stateRebuilder.get(createKey(context, event.type)); if (rebuilder && rebuilder.context === context) { state = rebuilder.stateRebuilder(event.data, state); } return state; } /** * Creates a unique key for the given context and event type. * * @param {string} context - The context of the event. * @param {string} type - The type of the event. * * @returns {string} The unique key. */ function createKey(context, type) { return `[${context}|${type}]`; }