arvo-event-handler
Version:
A complete set of orthogonal event handler and orchestration primitives for Arvo based applications, featuring declarative state machines (XState), imperative resumables for agentic workflows, contract-based routing, OpenTelemetry observability, and in-me
300 lines (299 loc) • 21.8 kB
JavaScript
"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 __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.ArvoResumable = void 0;
var api_1 = require("@opentelemetry/api");
var arvo_core_1 = require("arvo-core");
var createEmitableEvent_1 = require("../ArvoOrchestrationUtils/createEmitableEvent");
var inputValidation_1 = require("../ArvoOrchestrationUtils/inputValidation");
var orchestrationExecutionWrapper_1 = require("../ArvoOrchestrationUtils/orchestrationExecutionWrapper");
var index_1 = require("../SyncEventResource/index");
var errors_1 = require("../errors");
var ArvoDomain_1 = require("../ArvoDomain");
var orchestrationExecutionState_1 = require("../ArvoOrchestrationUtils/orchestrationExecutionState");
/**
* ArvoResumable complements {@link ArvoOrchestrator} by providing imperative
* handler functions for orchestration logic instead of declarative state machines.
* While ArvoOrchestrator excels at complex static workflows with deterministic
* branching, ArvoResumable handles dynamic orchestrations where branching logic
* depends on runtime context and event data.
*
* Use this for dynamic orchestrations with context-dependent branching
* or when preferring imperative programming patterns over state machines.
*/
var ArvoResumable = /** @class */ (function () {
function ArvoResumable(param) {
var _a;
var _b, _c, _d, _e;
this.executionunits = param.executionunits;
this.source = param.contracts.self.type;
this.syncEventResource = new index_1.SyncEventResource(param.memory, (_b = param.requiresResourceLocking) !== null && _b !== void 0 ? _b : true);
this.contracts = param.contracts;
this.handler = param.handler;
this.defaultEventEmissionDomains = __assign({ systemError: [ArvoDomain_1.ArvoDomain.ORCHESTRATION_CONTEXT], services: [ArvoDomain_1.ArvoDomain.LOCAL], complete: [ArvoDomain_1.ArvoDomain.ORCHESTRATION_CONTEXT] }, ((_c = param.defaultEventEmissionDomains) !== null && _c !== void 0 ? _c : {}));
this.spanOptions = __assign(__assign({ kind: api_1.SpanKind.PRODUCER }, param.spanOptions), { attributes: __assign(__assign((_a = {}, _a[arvo_core_1.ArvoExecution.ATTR_SPAN_KIND] = arvo_core_1.ArvoExecutionSpanKind.RESUMABLE, _a[arvo_core_1.OpenInference.ATTR_SPAN_KIND] = arvo_core_1.OpenInferenceSpanKind.CHAIN, _a), ((_e = (_d = param.spanOptions) === null || _d === void 0 ? void 0 : _d.attributes) !== null && _e !== void 0 ? _e : {})), { 'arvo.handler.source': this.source, 'arvo.contract.uri': this.contracts.self.uri }) });
}
Object.defineProperty(ArvoResumable.prototype, "requiresResourceLocking", {
/** Whether this resumable requires resource locking for concurrent safety */
get: function () {
return this.syncEventResource.requiresResourceLocking;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ArvoResumable.prototype, "memory", {
/** Memory interface for state persistence and retrieval */
get: function () {
return this.syncEventResource.memory;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ArvoResumable.prototype, "domain", {
/** The contract-defined domain for the handler */
get: function () {
return this.contracts.self.domain;
},
enumerable: false,
configurable: true
});
/**
* Validates incoming event against self or service contracts.
*
* Resolves the appropriate contract (self for initialization, service for responses),
* validates schema compatibility, and ensures event data matches contract requirements.
*
* See {@link validateInputEvent} for more infromation
*/
ArvoResumable.prototype.validateInput = function (event, span) {
return (0, inputValidation_1.validateInputEvent)({
event: event,
selfContract: this.contracts.self,
serviceContracts: this.contracts.services,
span: span,
});
};
/**
* Executes the workflow handler for an incoming event.
*
* Processes initialization events or service responses through the versioned handler,
* manages state persistence, tracks expected events, and generates output events.
* Workflows in 'done' status ignore subsequent events without processing.
*
* For violation errors (transaction, config, contract), the error is thrown to enable
* retry mechanisms. For non-violation errors, system error events are emitted to the
* workflow initiator, and the workflow enters a terminal failure state.
*
* @param event - The incoming event triggering handler execution
* @param opentelemetry - Optional OpenTelemetry configuration for tracing
* @returns Object containing emitted events from the handler or system errors
*
* @throws {TransactionViolation} When distributed lock acquisition fails
* @throws {ConfigViolation} When handler resolution or contract validation fails
* @throws {ContractViolation} When event schema validation fails
* @throws {ExecutionViolation} When workflow execution encounters critical errors defined by the handler developer
*/
ArvoResumable.prototype.execute = function (event, opentelemetry) {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, orchestrationExecutionWrapper_1.executeWithOrchestrationWrapper)({
_handlerType: 'resumable',
event: event,
opentelemetry: opentelemetry !== null && opentelemetry !== void 0 ? opentelemetry : { inheritFrom: 'EVENT' },
spanOptions: __assign({ spanName: function (_a) {
var selfContractUri = _a.selfContractUri, consumedEvent = _a.consumedEvent;
return "Resumable<".concat(selfContractUri, ">@<").concat(consumedEvent.type, ">");
} }, this.spanOptions),
source: this.source,
syncEventResource: this.syncEventResource,
executionunits: this.executionunits,
systemErrorDomain: this.defaultEventEmissionDomains.systemError,
selfContract: this.contracts.self.version('latest'),
}, function (_a) { return __awaiter(_this, [_a], void 0, function (_b) {
var inputValidation, contractType, eventTypeToExpectedEvent, _i, _c, _d, _, eventList, _e, eventList_1, _evt, handler, versionedSelfContract, executionResult, rawEvents, i, emittables, eventTrackingState, newState;
var _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
var span = _b.span, otelHeaders = _b.otelHeaders, orchestrationParentSubject = _b.orchestrationParentSubject, initEventId = _b.initEventId, parsedEventSubject = _b.parsedEventSubject, state = _b.state;
return __generator(this, function (_y) {
switch (_y.label) {
case 0:
(0, arvo_core_1.logToSpan)({
level: 'INFO',
message: "Resolving handler for event ".concat(event.type),
}, span);
if (!this.handler[parsedEventSubject.orchestrator.version]) {
throw new errors_1.ConfigViolation("Handler resolution failed: No handler found matching orchestrator name='".concat(parsedEventSubject.orchestrator.name, "' and version='").concat(parsedEventSubject.orchestrator.version, "'."));
}
(0, arvo_core_1.logToSpan)({
level: 'INFO',
message: "Input validation started for event ".concat(event.type),
});
inputValidation = this.validateInput(event, span);
if (inputValidation.type === 'CONTRACT_UNRESOLVED') {
throw new errors_1.ConfigViolation('Contract validation failed - Event does not match any registered contract schemas in the resumable');
}
if (inputValidation.type === 'INVALID_DATA' || inputValidation.type === 'INVALID') {
throw new errors_1.ContractViolation("Input validation failed - Event data does not meet contract requirements: ".concat(inputValidation.error.message));
}
contractType = inputValidation.contractType;
if ((state === null || state === void 0 ? void 0 : state.status) === 'done') {
(0, arvo_core_1.logToSpan)({
level: 'INFO',
message: "The resumable has already reached the terminal state. Ignoring event(id=".concat(event.id, ")"),
});
return [2 /*return*/, {
emittables: [],
newState: state,
}];
}
// Track expected events
if (event.parentid &&
((_g = (_f = state === null || state === void 0 ? void 0 : state.events) === null || _f === void 0 ? void 0 : _f.expected) === null || _g === void 0 ? void 0 : _g[event.parentid]) &&
Array.isArray((_j = (_h = state === null || state === void 0 ? void 0 : state.events) === null || _h === void 0 ? void 0 : _h.expected) === null || _j === void 0 ? void 0 : _j[event.parentid])) {
state.events.expected[event.parentid].push(event.toJSON());
}
eventTypeToExpectedEvent = {};
for (_i = 0, _c = Object.entries((_l = (_k = state === null || state === void 0 ? void 0 : state.events) === null || _k === void 0 ? void 0 : _k.expected) !== null && _l !== void 0 ? _l : {}); _i < _c.length; _i++) {
_d = _c[_i], _ = _d[0], eventList = _d[1];
for (_e = 0, eventList_1 = eventList; _e < eventList_1.length; _e++) {
_evt = eventList_1[_e];
if (!eventTypeToExpectedEvent[_evt.type]) {
eventTypeToExpectedEvent[_evt.type] = [];
}
eventTypeToExpectedEvent[_evt.type].push(_evt);
}
}
handler = this.handler[parsedEventSubject.orchestrator.version];
versionedSelfContract = this.contracts.self.version(parsedEventSubject.orchestrator.version);
return [4 /*yield*/, handler({
span: span,
context: (_m = state === null || state === void 0 ? void 0 : state.state$$) !== null && _m !== void 0 ? _m : null,
metadata: state !== null && state !== void 0 ? state : null,
collectedEvents: eventTypeToExpectedEvent,
domain: {
event: event.domain,
self: this.contracts.self.domain,
},
input: contractType === 'self' ? event.toJSON() : null,
service: contractType === 'service' ? event.toJSON() : null,
contracts: {
self: versionedSelfContract,
services: this.contracts.services,
},
})];
case 1:
executionResult = _y.sent();
rawEvents = (_o = executionResult === null || executionResult === void 0 ? void 0 : executionResult.services) !== null && _o !== void 0 ? _o : [];
for (i = 0; i < rawEvents.length; i++) {
rawEvents[i].domain = (_p = rawEvents[i].domain) !== null && _p !== void 0 ? _p : this.defaultEventEmissionDomains.services;
}
if (executionResult === null || executionResult === void 0 ? void 0 : executionResult.output) {
rawEvents.push({
id: executionResult.output.__id,
data: executionResult.output,
type: this.contracts.self.metadata.completeEventType,
to: (_r = (_q = parsedEventSubject.meta) === null || _q === void 0 ? void 0 : _q.redirectto) !== null && _r !== void 0 ? _r : parsedEventSubject.execution.initiator,
domain: (_t = (_s = executionResult.output) === null || _s === void 0 ? void 0 : _s.__domain) !== null && _t !== void 0 ? _t : this.defaultEventEmissionDomains.complete,
executionunits: executionResult.output.__executionunits,
});
}
emittables = (0, createEmitableEvent_1.processRawEventsIntoEmittables)({
rawEvents: rawEvents,
otelHeaders: otelHeaders,
orchestrationParentSubject: orchestrationParentSubject,
sourceEvent: event,
selfContract: versionedSelfContract,
serviceContracts: this.contracts.services,
initEventId: initEventId,
executionunits: this.executionunits,
source: this.source,
}, span);
(0, arvo_core_1.logToSpan)({
level: 'INFO',
message: "Resumable execution completed. Generated events: ".concat(emittables.length),
});
eventTrackingState = {
consumed: event.toJSON(),
expected: emittables.length
? Object.fromEntries(emittables.map(function (item) { return [item.id, []]; }))
: ((_v = (_u = state === null || state === void 0 ? void 0 : state.events) === null || _u === void 0 ? void 0 : _u.expected) !== null && _v !== void 0 ? _v : null),
produced: emittables.map(function (item) { return item.toJSON(); }),
};
newState = {
__type: 'OrchestrationExecutionMemoryRecord',
executionStatus: (executionResult === null || executionResult === void 0 ? void 0 : executionResult.output)
? orchestrationExecutionState_1.OrchestrationExecutionStatus.DONE
: orchestrationExecutionState_1.OrchestrationExecutionStatus.NORMAL,
status: (executionResult === null || executionResult === void 0 ? void 0 : executionResult.output) ? 'done' : 'active',
initEventId: initEventId,
parentSubject: orchestrationParentSubject,
subject: event.subject,
events: eventTrackingState,
state$$: (_x = (_w = executionResult === null || executionResult === void 0 ? void 0 : executionResult.context) !== null && _w !== void 0 ? _w : state === null || state === void 0 ? void 0 : state.state$$) !== null && _x !== void 0 ? _x : null,
};
return [2 /*return*/, { emittables: emittables, newState: newState }];
}
});
}); })];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
};
Object.defineProperty(ArvoResumable.prototype, "systemErrorSchema", {
get: function () {
return this.contracts.self.systemError;
},
enumerable: false,
configurable: true
});
return ArvoResumable;
}());
exports.ArvoResumable = ArvoResumable;