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.

281 lines (280 loc) 14.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SyncEventResource = void 0; var arvo_core_1 = require("arvo-core"); var error_1 = require("../ArvoOrchestrator/error"); var errors_1 = require("../errors"); /** * A synchronous event resource that manages machine memory state based on event subjects. * * This class provides a distributed-system-safe mechanism for persisting and retrieving machine memory * objects that are correlated with ArvoEvent subjects. It acts as a key-value store where * the event subject serves as the key and the memory object serves as the value. * * Key features: * - JSON serializable memory persistence * - Optional resource locking for distributed concurrent access control * - Subject-based memory correlation across multiple service instances * - Transaction-safe operations with proper error handling * - Optional OpenTelemetry span integration for observability * * @template T - The type of the memory object, must extend Record<string, any> and be JSON serializable * * @example * ```typescript * type MyMemory = { * counter: number; * status: string; * } * * class MemoryImplementation implements IMachineMemory<MyMemory> { ... } * * const resource = new SyncEventResource<MyMemory>( * new MemoryImplementation(), * true // enable resource locking for distributed systems * ); * ``` */ var SyncEventResource = /** @class */ (function () { function SyncEventResource(memory, requiresResourceLocking) { this.memory = memory; this.requiresResourceLocking = requiresResourceLocking; } /** * Acquires a lock on the event subject to prevent concurrent access across distributed services. * * This method ensures distributed-system-safe access to the memory resource by preventing * multiple service instances from modifying the same event subject simultaneously. If resource * locking is disabled, it will skip the lock acquisition process. The lock is subject-specific, * meaning different event subjects can be processed concurrently across services. * * @returns A promise that resolves to the lock acquisition status: * - 'ACQUIRED': Lock was successfully acquired * - 'NOT_ACQUIRED': Lock acquisition failed (resource busy by another service) * - 'NOOP': Lock acquisition was skipped (locking disabled) * * @throws {TransactionViolation} When lock acquisition fails due to system errors */ SyncEventResource.prototype.acquireLock = function (event, span) { return __awaiter(this, void 0, void 0, function () { var acquired, e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.requiresResourceLocking) { (0, arvo_core_1.logToSpan)({ level: 'INFO', message: "Skipping acquiring lock for event (subject=".concat(event.subject, ") as the resource does not required locking."), }, span); return [2 /*return*/, 'NOOP']; } _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); (0, arvo_core_1.logToSpan)({ level: 'INFO', message: 'Acquiring lock for the event', }); return [4 /*yield*/, this.memory.lock(event.subject)]; case 2: acquired = _a.sent(); return [2 /*return*/, acquired ? 'ACQUIRED' : 'NOT_ACQUIRED']; case 3: e_1 = _a.sent(); throw new error_1.TransactionViolation({ cause: error_1.TransactionViolationCause.LOCK_FAILURE, message: "Error acquiring lock for event (subject=".concat(event.subject, "): ").concat(e_1 === null || e_1 === void 0 ? void 0 : e_1.message), initiatingEvent: event, }); case 4: return [2 /*return*/]; } }); }); }; /** * Retrieves the current state from memory for the given event subject. * * This method reads the persisted memory object associated with the event's subject * from the distributed storage system. If no memory exists for the subject, it returns null. * The operation is wrapped in proper error handling to ensure transaction safety across * distributed service instances. * * @returns A promise that resolves to the memory object if found, or null if no memory exists * * @throws {TransactionViolation} When the read operation fails due to storage errors */ SyncEventResource.prototype.acquireState = function (event, span) { return __awaiter(this, void 0, void 0, function () { var e_2; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); (0, arvo_core_1.logToSpan)({ level: 'INFO', message: 'Reading machine state for the event', }, span); return [4 /*yield*/, this.memory.read(event.subject)]; case 1: return [2 /*return*/, _a.sent()]; case 2: e_2 = _a.sent(); throw new error_1.TransactionViolation({ cause: error_1.TransactionViolationCause.READ_FAILURE, message: "Error reading state for event (subject=".concat(event.subject, "): ").concat(e_2 === null || e_2 === void 0 ? void 0 : e_2.message), initiatingEvent: event, }); case 3: return [2 /*return*/]; } }); }); }; /** * Persists the updated memory state to distributed storage. * * This method writes the new memory record to the distributed storage system, associating * it with the event's subject. It provides both the new record and the previous record for * implementations that need to perform atomic updates, maintain audit trails, or handle * optimistic concurrency control in distributed environments. * * @throws {TransactionViolation} When the write operation fails due to storage errors */ SyncEventResource.prototype.persistState = function (event, record, prevRecord, span) { return __awaiter(this, void 0, void 0, function () { var e_3; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); (0, arvo_core_1.logToSpan)({ level: 'INFO', message: 'Persisting machine state to the storage', }, span); return [4 /*yield*/, this.memory.write(event.subject, record, prevRecord)]; case 1: _a.sent(); return [3 /*break*/, 3]; case 2: e_3 = _a.sent(); throw new error_1.TransactionViolation({ cause: error_1.TransactionViolationCause.WRITE_FAILURE, message: "Error writing state for event (subject=".concat(event.subject, "): ").concat(e_3 === null || e_3 === void 0 ? void 0 : e_3.message), initiatingEvent: event, }); case 3: return [2 /*return*/]; } }); }); }; /** * Validates that the event subject conforms to the ArvoOrchestrationSubject format. * * This method ensures that the event subject follows the expected schema format * required by the Arvo orchestration system. Invalid subjects will result in * execution violations to prevent processing of malformed events across the * distributed service architecture. * * @throws {ExecutionViolation} When the event subject format is invalid * * @protected */ SyncEventResource.prototype.validateEventSubject = function (event, span) { (0, arvo_core_1.logToSpan)({ level: 'INFO', message: 'Validating event subject', }, span); var isValid = arvo_core_1.ArvoOrchestrationSubject.isValid(event.subject); if (!isValid) { throw new errors_1.ExecutionViolation("Invalid event (id=".concat(event.id, ") subject format. Expected an ArvoOrchestrationSubject but received '").concat(event.subject, "'. The subject must follow the format specified by ArvoOrchestrationSubject schema")); } }; /** * Releases a previously acquired lock on the event subject. * * This method safely releases locks that were acquired during event processing to prevent * resource leaks in distributed systems. It handles cases where no lock was acquired * (NOOP operations) and provides proper error handling for unlock failures. Failed unlock * operations are logged as potential resource leaks but do not throw exceptions to avoid * disrupting the main processing flow as it assumes that the lock will have the lifedspan. * * @returns A promise that resolves to the lock release status: * - 'NOOP': No lock was acquired, so no operation was performed * - 'RELEASED': Lock was successfully released * - 'ERROR': Lock release failed, potential resource leak * * @protected */ SyncEventResource.prototype.releaseLock = function (event, acquiredLock, span) { return __awaiter(this, void 0, void 0, function () { var err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: if (acquiredLock !== 'ACQUIRED') { (0, arvo_core_1.logToSpan)({ level: 'INFO', message: 'Lock was not acquired by the process so perfroming no operation', }, span); return [2 /*return*/, 'NOOP']; } _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, this.memory.unlock(event.subject)]; case 2: _a.sent(); (0, arvo_core_1.logToSpan)({ level: 'INFO', message: 'Lock successfully released', }, span); return [2 /*return*/, 'RELEASED']; case 3: err_1 = _a.sent(); (0, arvo_core_1.logToSpan)({ level: 'ERROR', message: "Memory unlock operation failed - Possible resource leak: ".concat(err_1.message), }, span); return [2 /*return*/, 'ERROR']; case 4: return [2 /*return*/]; } }); }); }; return SyncEventResource; }()); exports.SyncEventResource = SyncEventResource;