UNPKG

arvo-event-handler

Version:

Type-safe event handler system with versioning, telemetry, and contract validation for distributed Arvo event-driven architectures, featuring routing and multi-handler support.

284 lines (283 loc) 13.3 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.setupArvoMachine = setupArvoMachine; var arvo_core_1 = require("arvo-core"); var xstate_1 = require("xstate"); var _1 = __importDefault(require(".")); var object_1 = require("../utils/object"); var utils_1 = require("./utils"); var uuid_1 = require("uuid"); /** * Establishes the foundation for creating Arvo-compatible state machines. * * This function configures the core elements of an Arvo state machine, including * built-in actions like `enqueueArvoEvent`, and enforces Arvo-specific constraints * to ensure compatibility with the Arvo event-driven system. * * @param param - Configuration object for the machine setup * @returns An object containing the `createMachine` function * @throws {Error} If 'actors', 'delays', or reserved action names are used in the configuration * * @description * `setupArvoMachine` is a crucial function in the Arvo ecosystem, designed to create * synchronous state machine orchestrations for Arvo's event-driven architecture. * It builds upon XState, providing a tailored implementation that: * * 1. Enforces synchronous behavior to maintain predictable state transitions * 3. Implements Arvo-specific constraints and features * * Key features: * - Synchronous execution: Ensures deterministic behavior in event-driven systems * - Built-in actions: Includes `enqueueArvoEvent` for Arvo event handling * - Constraint checking: Prevents usage of asynchronous features like 'actors' or 'delays' * * @remarks * While `setupArvoMachine` is based on XState's `setup` and `createMachine` functions, * it includes Arvo-specific modifications and restrictions. For a deeper understanding * of the underlying XState concepts, refer to the official XState documentation: * - XState setup: https://stately.ai/docs/setup * - XState createMachine: https://stately.ai/docs/machines * * @example * Here's a comprehensive example demonstrating how to use `setupArvoMachine`: * * ```typescript * import { setupArvoMachine } from 'arvo-xstate' * import { createArvoOrchestratorContract, ArvoErrorSchema, createArvoContract } from 'arvo-core' * import { z } from 'zod' * * // Define the LLM orchestrator contract * const llmContract = createArvoOrchestratorContract({ * uri: `#/orchestrators/llm/`, * type: 'llm', * versions: { * '0.0.1': { * init: z.object({ * request: z.string(), * llm: z.enum(['gpt-4', 'gpt-4o']), * }), * complete: z.object({ * response: z.string(), * }) * } * } * }) * * // Define the OpenAI service contract * const openAiContract = createArvoContract({ * uri: `#/services/openai`, * type: 'com.openai.completions', * versions: { * '0.0.1': { * accepts: z.object({ * request: z.string() * }), * emits: { * 'evt.openai.completions.success': z.object({ * response: z.string(), * }) * } * } * } * }) * * const machineId = 'machineV100' * * // Set up the Arvo machine * const llmMachine = setupArvoMachine({ * contracts: { * self: llmContract.version('0.0.1'), * services: { * openAiContract.version('0.0.1'), * } * }, * types: { * context: {} as { * request: string, * llm: string, * response: string | null, * errors: z.infer<typeof ArvoErrorSchema>[] * }, * tags: {} as 'pending' | 'success' | 'error', * }, * actions: { * log: ({context, event}) => console.log({context, event}) * }, * guards: { * isValid: ({context, event}) => Boolean(context.request) * } * }).createMachine({ * id: machineId, * context: ({input}) => ({ * request: input.request, * llm: input.llm, * response: null, * errors: [], * }), * initial: 'validate', * states: { * validate: { * always: [ * { * guard: 'isValid', * target: 'llm', * }, * { * target: 'error', * } * ] * }, * llm: { * entry: [ * { * type: 'log', * }, * emit(({context}) => ({ * type: 'com.openai.completions', * data: { * request: context.request, * }, * })) * ], * on: { * 'evt.openai.completions.success': { * actions: [ * assign({response: ({event}) => event.response}) * ], * target: 'done' * }, * 'sys.com.openai.completions.error': { * actions: [ * assign({errors: ({context, event}) => [...context.errors, event.body]}) * ], * target: 'error' * } * } * }, * done: { * type: 'final' * }, * error: { * type: 'final' * }, * } * }); * ``` * * This example demonstrates: * 1. Defining Arvo contracts for the orchestrator and a service * 2. Setting up an Arvo machine with contracts, types, actions, and guards * 3. Creating a machine with states for validation, LLM interaction, and error handling * 4. Using XState features like `emit` bound with Arvo contracts for event emitting and event handling via transitions */ function setupArvoMachine(param) { var _a; var _b, _c; var createConfigErrorMessage = function (type) { return (0, arvo_core_1.cleanString)("\n Configuration Error: '".concat(type, "' not supported in Arvo machines\n \n Arvo machines do not support XState ").concat(type === 'actor' ? 'actors' : 'delay transitions', " as they introduce asynchronous behavior.\n \n To fix:\n 1. Remove the '").concat(type, "' configuration\n 2. Use Arvo's event-driven patterns instead for asynchronous operations\n ")); }; if (param.actors) { throw new Error(createConfigErrorMessage('actor')); } if (param.delays) { throw new Error(createConfigErrorMessage('delays')); } if ((_b = param.actions) === null || _b === void 0 ? void 0 : _b.enqueueArvoEvent) { throw new Error((0, arvo_core_1.cleanString)("\n Configuration Error: Reserved action name 'enqueueArvoEvent'\n \n 'enqueueArvoEvent' is an internal Arvo system action and cannot be overridden.\n \n To fix: Use a different name for your action, such as:\n - 'queueCustomEvent'\n - 'scheduleEvent'\n - 'dispatchEvent'\n ")); } var __areServiceContractsUnique = (0, utils_1.areServiceContractsUnique)(param.contracts.services); if (!__areServiceContractsUnique.result) { throw new Error("The service contracts must have unique URIs. Multiple versions of the same contract are not allow. The contracts '".concat(__areServiceContractsUnique.keys[0], "' and '").concat(__areServiceContractsUnique.keys[1], "' have the same URI '").concat(__areServiceContractsUnique.contractUri, "'")); } var __checkIfSelfIsAService = (0, utils_1.areServiceContractsUnique)(__assign(__assign({}, param.contracts.services), (_a = {}, _a[(0, uuid_1.v4)()] = param.contracts.self, _a))); if (!__checkIfSelfIsAService.result) { throw new Error("Circular dependency detected: Machine with URI '".concat(param.contracts.self.uri, "' is registered as service '").concat(__checkIfSelfIsAService.keys[1], "'. Self-referential services create execution loops and are prohibited.")); } var combinedActions = __assign(__assign({}, ((_c = param.actions) !== null && _c !== void 0 ? _c : {})), { enqueueArvoEvent: (0, xstate_1.assign)(function (_a, param) { var _b, _c, _d, _e, _f; var context = _a.context; return (__assign(__assign({}, (context !== null && context !== void 0 ? context : {})), { arvo$$: __assign(__assign({}, ((_b = context === null || context === void 0 ? void 0 : context.arvo$$) !== null && _b !== void 0 ? _b : {})), { volatile$$: __assign(__assign({}, ((_d = (_c = context === null || context === void 0 ? void 0 : context.arvo$$) === null || _c === void 0 ? void 0 : _c.volatile$$) !== null && _d !== void 0 ? _d : {})), { eventQueue$$: __spreadArray(__spreadArray([], (((_f = (_e = context === null || context === void 0 ? void 0 : context.arvo$$) === null || _e === void 0 ? void 0 : _e.volatile$$) === null || _f === void 0 ? void 0 : _f.eventQueue$$) || []), true), [param], false) }) }) })); }) }); // Call the original setup function with modified parameters var systemSetup = (0, xstate_1.setup)({ schemas: param.schemas, types: param.types, guards: param.guards, actions: combinedActions, }); /** * Creates an Arvo-compatible XState machine. * * @param config - The configuration object for the machine * @returns An ArvoMachine instance * * @throws Error if 'invoke' or 'after' configurations are used * * @remarks * This function creates a state machine based on the provided configuration. * It performs additional checks to ensure the machine adheres to Arvo's constraints, * such as disallowing 'invoke' and 'after' configurations which could introduce * asynchronous behavior. * ``` */ var createMachine = function (config) { var _a, _b; var machineVersion = (_a = config.version) !== null && _a !== void 0 ? _a : param.contracts.self.version; if (machineVersion !== param.contracts.self.version) { throw new Error("Version mismatch: Machine version must be '".concat(param.contracts.self.version, "' or undefined, received '").concat(config.version, "'")); } var createConfigErrorMessage = function (type, path) { var location = path.join(' > '); if (type === 'invoke') { return (0, arvo_core_1.cleanString)("\n Configuration Error: 'invoke' not supported\n \n Location: ".concat(location, "\n \n Arvo machines do not support XState invocations as they introduce asynchronous behavior.\n \n To fix: Replace 'invoke' with Arvo event-driven patterns for asynchronous operations\n ")); } if (type === 'after') { return (0, arvo_core_1.cleanString)("\n Configuration Error: 'after' not supported\n \n Location: ".concat(location, "\n \n Arvo machines do not support delayed transitions as they introduce asynchronous behavior.\n \n To fix: Replace 'after' with Arvo event-driven patterns for time-based operations\n ")); } if (type === 'enqueueArvoEvent') { return (0, arvo_core_1.cleanString)("\n Configuration Error: Reserved action name 'enqueueArvoEvent'\n \n Location: ".concat(location, "\n \n 'enqueueArvoEvent' is an internal Arvo system action and cannot be used in machine configurations.\n \n To fix: Use a different name for your action\n ")); } }; for (var _i = 0, _c = (0, object_1.getAllPaths)((_b = config.states) !== null && _b !== void 0 ? _b : {}); _i < _c.length; _i++) { var item = _c[_i]; if (item.path.includes('invoke')) { throw new Error(createConfigErrorMessage('invoke', item.path)); } if (item.path.includes('after')) { throw new Error(createConfigErrorMessage('after', item.path)); } if (item.path.includes('enqueueArvoEvent')) { throw new Error(createConfigErrorMessage('enqueueArvoEvent', item.path)); } } var machine = systemSetup.createMachine(__assign({}, config)); var hasParallelStates = (0, utils_1.detectParallelStates)(machine.config); var hasMultipleNonSystemErrorEvents = Object.values(param.contracts.services).some(function (item) { return Object.keys(item.emits).length > 1; }); var requiresLocking = hasParallelStates || hasMultipleNonSystemErrorEvents; return new _1.default(config.id, machineVersion, param.contracts, machine, requiresLocking); }; return { createMachine: createMachine }; }