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