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
JavaScript
;
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 };
}