UNPKG

@flowlab/all

Version:

A cool library focusing on handling various flows

546 lines (545 loc) 34.8 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 __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.WorkflowExecutor = void 0; var runtime_1 = require("../types/runtime"); var config_1 = require("../types/config"); var context_1 = require("./context"); // Assuming these exist var index_1 = require("../errors/index"); var WorkflowExecutor = /** @class */ (function () { function WorkflowExecutor(options) { var _a; this.nodeRegistry = options.nodeRegistry; this.logger = options.logger; this.persistence = options.persistence; this.scheduler = options.scheduler; this.eventManager = options.eventManager; this.getWorkflowDefinition = options.getWorkflowDefinition; this.maxLoopIterations = (_a = options.maxLoopIterations) !== null && _a !== void 0 ? _a : 1000; } WorkflowExecutor.prototype.run = function (definitionOrId_1, input_1) { return __awaiter(this, arguments, void 0, function (definitionOrId, input, contextExtras) { var definition, _a, context, currentStepId, loopGuard, stepConfig, stepResult, error_1, saveError_1; var _b; if (contextExtras === void 0) { contextExtras = {}; } return __generator(this, function (_c) { switch (_c.label) { case 0: if (!(typeof definitionOrId === 'string')) return [3 /*break*/, 2]; return [4 /*yield*/, this.getWorkflowDefinition(definitionOrId)]; case 1: _a = _c.sent(); return [3 /*break*/, 3]; case 2: _a = definitionOrId; _c.label = 3; case 3: definition = _a; if (!definition) { throw new index_1.ConfigurationError("Workflow definition '".concat(definitionOrId, "' not found.")); } if (!definition.startStepId) { throw new index_1.ConfigurationError("Workflow definition '".concat(definition.id, "' has no start step defined.")); } if (!definition.validate()) { // Added validation call throw new index_1.ConfigurationError("Workflow definition '".concat(definition.id, "' failed validation.")); } context = (0, context_1.createWorkflowContext)(definition.id, definition.name, input, contextExtras); this.logger.info("Starting workflow run: ".concat(context.workflowId, " (Def: ").concat(definition.id, ")")); return [4 /*yield*/, this.emitEvent('workflow.started', context)]; case 4: _c.sent(); // Emit event currentStepId = definition.startStepId; loopGuard = 0; _c.label = 5; case 5: _c.trys.push([5, 15, 17, 22]); _c.label = 6; case 6: if (!(currentStepId && loopGuard++ < this.maxLoopIterations)) return [3 /*break*/, 14]; stepConfig = definition.getStep(currentStepId); if (!stepConfig) { throw new index_1.WorkflowError("Step '".concat(currentStepId, "' not found in workflow '").concat(definition.id, "'."), context.workflowId); } context.logs.push("Executing step: ".concat(currentStepId, " (Type: ").concat(stepConfig.type, ")")); return [4 /*yield*/, this.executeStep(stepConfig, context)]; case 7: stepResult = _c.sent(); // Update history and persistence this.updateHistory(context, stepResult.record); if (!this.persistence) return [3 /*break*/, 9]; return [4 /*yield*/, this.persistence.saveState(context)]; case 8: _c.sent(); _c.label = 9; case 9: if (!(stepResult.status === runtime_1.NodeStatus.FAILED || stepResult.status === runtime_1.NodeStatus.CANCELLED)) return [3 /*break*/, 11]; context.status = stepResult.status; context.output = stepResult.output; // Capture last output on failure this.logger.error("Workflow run ".concat(context.workflowId, " failed at step ").concat(currentStepId, "."), stepResult.error); return [4 /*yield*/, this.emitEvent('workflow.failed', context, { stepId: currentStepId, error: (_b = stepResult.error) === null || _b === void 0 ? void 0 : _b.message })]; case 10: _c.sent(); return [3 /*break*/, 14]; // Stop execution case 11: if (stepResult.status === runtime_1.NodeStatus.COMPENSATING) { // TODO: Implement compensation flow triggering this.logger.warn("Compensation requested at step ".concat(currentStepId, ", stopping normal flow.")); context.status = runtime_1.NodeStatus.FAILED; // Mark workflow as failed if compensation needed? Or specific status? return [3 /*break*/, 14]; } currentStepId = stepResult.nextStepId; // Determine next step if (!!currentStepId) return [3 /*break*/, 13]; context.status = runtime_1.NodeStatus.COMPLETED; context.output = stepResult.output; // Capture final output this.logger.info("Workflow run ".concat(context.workflowId, " completed successfully.")); return [4 /*yield*/, this.emitEvent('workflow.completed', context)]; case 12: _c.sent(); return [3 /*break*/, 14]; // Workflow finished case 13: return [3 /*break*/, 6]; case 14: if (loopGuard >= this.maxLoopIterations) { throw new index_1.WorkflowError("Maximum loop iterations (".concat(this.maxLoopIterations, ") reached. Potential infinite loop detected."), context.workflowId); } return [3 /*break*/, 22]; case 15: error_1 = _c.sent(); context.status = runtime_1.NodeStatus.FAILED; context.error = error_1; // Store workflow-level error this.logger.error("Workflow run ".concat(context.workflowId, " encountered an unhandled error."), error_1); return [4 /*yield*/, this.emitEvent('workflow.failed', context, { error: error_1.message })]; case 16: _c.sent(); return [3 /*break*/, 22]; case 17: context.endTime = new Date(); if (!this.persistence) return [3 /*break*/, 21]; _c.label = 18; case 18: _c.trys.push([18, 20, , 21]); return [4 /*yield*/, this.persistence.saveState(context)]; case 19: _c.sent(); // Final state save return [3 /*break*/, 21]; case 20: saveError_1 = _c.sent(); this.logger.error("Failed to save final workflow state for ".concat(context.workflowId), saveError_1); return [3 /*break*/, 21]; case 21: return [7 /*endfinally*/]; case 22: return [2 /*return*/, context]; } }); }); }; // --- Step Execution Logic (Needs detailed implementation) --- WorkflowExecutor.prototype.executeStep = function (stepConfig, workflowContext) { return __awaiter(this, void 0, void 0, function () { var stepStartTime, stepStatus, stepOutput, stepError, nextStepId, record, _a, taskResult, conditionResult, err_1; return __generator(this, function (_b) { switch (_b.label) { case 0: stepStartTime = new Date(); stepStatus = runtime_1.NodeStatus.PENDING; stepOutput = undefined; stepError = undefined; nextStepId = stepConfig.nextStepId; record = { stepId: stepConfig.id, startTime: stepStartTime, status: runtime_1.NodeStatus.PENDING, }; _b.label = 1; case 1: _b.trys.push([1, 11, 12, 14]); return [4 /*yield*/, this.emitEvent('step.started', workflowContext, { stepId: stepConfig.id, type: stepConfig.type })]; case 2: _b.sent(); stepStatus = runtime_1.NodeStatus.RUNNING; record.status = stepStatus; // Update record status _a = stepConfig.type; switch (_a) { case config_1.StepType.TASK: return [3 /*break*/, 3]; case config_1.StepType.CONDITION: return [3 /*break*/, 5]; case config_1.StepType.PARALLEL: return [3 /*break*/, 7]; case config_1.StepType.SUB_WORKFLOW: return [3 /*break*/, 8]; } return [3 /*break*/, 9]; case 3: return [4 /*yield*/, this.executeTaskStep(stepConfig, workflowContext)]; case 4: taskResult = _b.sent(); stepStatus = taskResult.status; stepOutput = taskResult.output; stepError = taskResult.error; record.nodeId = stepConfig.nodeId; // Use default nextStepId unless overridden return [3 /*break*/, 10]; case 5: return [4 /*yield*/, this.executeConditionStep(stepConfig, workflowContext)]; case 6: conditionResult = _b.sent(); stepStatus = runtime_1.NodeStatus.COMPLETED; // Condition itself completes nextStepId = conditionResult.nextStepId; // Condition determines the *actual* next step record.output = { branchTaken: conditionResult.branchKey }; // Record which branch was taken if (!nextStepId) { // No matching branch, potentially end workflow if this was the last path this.logger.warn("Condition step '".concat(stepConfig.id, "' resulted in no matching branch ('").concat(conditionResult.branchKey, "'). Ending this path.")); } return [3 /*break*/, 10]; case 7: // TODO: Implement parallel execution logic // This involves running multiple steps concurrently and waiting for all. // Needs careful state management and error handling for partial failures. this.logger.warn("Parallel step execution not fully implemented yet for step: ".concat(stepConfig.id)); stepStatus = runtime_1.NodeStatus.SKIPPED; // Mark as skipped for now return [3 /*break*/, 10]; case 8: // TODO: Implement sub-workflow execution // Needs to call executor.run() recursively with the sub-workflow def/id // and handle input/output mapping. this.logger.warn("SubWorkflow step execution not fully implemented yet for step: ".concat(stepConfig.id)); stepStatus = runtime_1.NodeStatus.SKIPPED; // Mark as skipped for now return [3 /*break*/, 10]; case 9: throw new index_1.ConfigurationError("Unsupported step type: ".concat(stepConfig.type)); case 10: if (stepStatus === runtime_1.NodeStatus.FAILED && stepConfig.compensateOnFailure) { // TODO: Trigger compensation logic this.logger.warn("Compensation requested for failed step '".concat(stepConfig.id, "'. Compensation logic not fully implemented.")); // Potentially change status to COMPENSATING and stop normal flow? stepStatus = runtime_1.NodeStatus.COMPENSATING; // Signal need for compensation } return [3 /*break*/, 14]; case 11: err_1 = _b.sent(); stepStatus = runtime_1.NodeStatus.FAILED; stepError = err_1 instanceof Error ? err_1 : new index_1.WorkflowError(String(err_1), workflowContext.workflowId); this.logger.error("Error executing step ".concat(stepConfig.id, ": ").concat(stepError.message), stepError); return [3 /*break*/, 14]; case 12: record.status = stepStatus; record.endTime = new Date(); record.output = stepOutput; // Record step output record.error = stepError === null || stepError === void 0 ? void 0 : stepError.message; return [4 /*yield*/, this.emitEvent("step.".concat(stepStatus.toLowerCase()), workflowContext, { stepId: stepConfig.id, type: stepConfig.type, error: stepError === null || stepError === void 0 ? void 0 : stepError.message })]; case 13: _b.sent(); return [7 /*endfinally*/]; case 14: return [2 /*return*/, { status: stepStatus, nextStepId: nextStepId, output: stepOutput, error: stepError, record: record }]; } }); }); }; // --- Task Step Execution with Retries/Timeout --- WorkflowExecutor.prototype.executeTaskStep = function (config, workflowContext) { return __awaiter(this, void 0, void 0, function () { var maxRetries, delayMs, timeoutMs, _loop_1, this_1, attempt, state_1; var _this = this; var _a, _b, _c, _d; return __generator(this, function (_e) { switch (_e.label) { case 0: maxRetries = (_b = (_a = config.retryOptions) === null || _a === void 0 ? void 0 : _a.maxRetries) !== null && _b !== void 0 ? _b : 0; delayMs = (_d = (_c = config.retryOptions) === null || _c === void 0 ? void 0 : _c.delayMs) !== null && _d !== void 0 ? _d : 0; timeoutMs = config.timeoutMs; _loop_1 = function (attempt) { var nodeContext, nodeInfo, executionPromise, nodeOutput, nodeError, nodeStatus, implementation, nodeExecuteFn, nodeInstance, simpleFunc_1, timeoutPromise, err_2; return __generator(this, function (_f) { switch (_f.label) { case 0: nodeContext = (0, context_1.createNodeContext)(workflowContext, config.id, config.nodeId, attempt); nodeInfo = this_1.nodeRegistry.get(config.nodeId); if (!nodeInfo) { throw new index_1.ConfigurationError("Node '".concat(config.nodeId, "' not registered.")); } // --- Resolve Input Mapping --- this_1.resolveInputMapping(config, workflowContext, nodeContext); executionPromise = void 0; nodeOutput = undefined; nodeError = undefined; nodeStatus = runtime_1.NodeStatus.RUNNING; _f.label = 1; case 1: _f.trys.push([1, 10, 13, 14]); implementation = nodeInfo.implementation; nodeExecuteFn = void 0; if (!nodeInfo.isClassInstance) return [3 /*break*/, 4]; nodeInstance = implementation; // --- Optional: Role Check --- if (nodeInstance.requiredRoles && !this_1.checkRoles(workflowContext.userRole, nodeInstance.requiredRoles)) { throw new index_1.AuthorizationError("User role '".concat(workflowContext.userRole, "' not authorized for node '").concat(config.nodeId, "'. Required: ").concat(nodeInstance.requiredRoles.join(', '))); } if (!nodeInstance.validate) return [3 /*break*/, 3]; return [4 /*yield*/, nodeInstance.validate(nodeContext)]; case 2: _f.sent(); _f.label = 3; case 3: nodeExecuteFn = nodeInstance.execute.bind(nodeInstance); // Bind context return [3 /*break*/, 5]; case 4: simpleFunc_1 = implementation; // Wrap simple function to match BaseNode execute signature if needed, // or adjust BaseNode execute signature/context passing. // For simplicity, assume NodeFunction takes INodeContext now. nodeExecuteFn = function (ctx) { return __awaiter(_this, void 0, void 0, function () { var result; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, simpleFunc_1(ctx, ctx.input)]; case 1: result = _a.sent(); if (result !== undefined) { ctx.output = result; // Assume direct return is output } return [2 /*return*/]; } }); }); }; _f.label = 5; case 5: // --- Execute with Timeout --- executionPromise = nodeExecuteFn(nodeContext); if (!(timeoutMs && timeoutMs > 0)) return [3 /*break*/, 7]; timeoutPromise = new Promise(function (_, reject) { return setTimeout(function () { return reject(new index_1.TimeoutError("Node '".concat(config.nodeId, "' timed out after ").concat(timeoutMs, "ms."), config.nodeId, config.id)); }, timeoutMs); }); return [4 /*yield*/, Promise.race([executionPromise, timeoutPromise])]; case 6: _f.sent(); return [3 /*break*/, 9]; case 7: return [4 /*yield*/, executionPromise]; case 8: _f.sent(); _f.label = 9; case 9: // --- Execution Success --- nodeStatus = runtime_1.NodeStatus.COMPLETED; nodeOutput = nodeContext.output; // Get output potentially set on context // --- Resolve Output Mapping --- this_1.resolveOutputMapping(config, nodeContext, workflowContext); return [2 /*return*/, "break"]; case 10: err_2 = _f.sent(); // --- Execution Error --- nodeError = err_2 instanceof Error ? err_2 : new index_1.NodeExecutionError(String(err_2), config.nodeId, config.id); nodeContext.error = nodeError; // Record error on node context for logging/history nodeStatus = err_2 instanceof index_1.TimeoutError ? runtime_1.NodeStatus.TIMEOUT : runtime_1.NodeStatus.FAILED; this_1.logger.warn("Attempt ".concat(attempt + 1, "/").concat(maxRetries + 1, " failed for node ").concat(config.nodeId, " in step ").concat(config.id, ": ").concat(nodeError.message)); if (attempt >= maxRetries) { this_1.logger.error("Node ".concat(config.nodeId, " failed after ").concat(maxRetries, " retries in step ").concat(config.id, "."), nodeError); return [2 /*return*/, "break"]; } if (!(delayMs > 0)) return [3 /*break*/, 12]; return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, delayMs); })]; case 11: _f.sent(); // Wait before retry _f.label = 12; case 12: return [3 /*break*/, 14]; case 13: nodeContext.status = nodeStatus; // Final status for this attempt nodeContext.endTime = new Date(); // Log node execution attempt details if necessary this_1.updateHistory(workflowContext, { stepId: config.id, nodeId: config.nodeId, status: nodeStatus, startTime: nodeContext.startTime, endTime: nodeContext.endTime, error: nodeError === null || nodeError === void 0 ? void 0 : nodeError.message, retries: attempt }); return [7 /*endfinally*/]; case 14: return [2 /*return*/]; } }); }; this_1 = this; attempt = 0; _e.label = 1; case 1: if (!(attempt <= maxRetries)) return [3 /*break*/, 4]; return [5 /*yield**/, _loop_1(attempt)]; case 2: state_1 = _e.sent(); if (state_1 === "break") return [3 /*break*/, 4]; _e.label = 3; case 3: attempt++; return [3 /*break*/, 1]; case 4: // End retry loop return [2 /*return*/, { status: runtime_1.NodeStatus.COMPLETED }]; } }); }); }; // --- Condition Step Execution --- WorkflowExecutor.prototype.executeConditionStep = function (config, workflowContext) { return __awaiter(this, void 0, void 0, function () { var conditionFn, branchKey, nextStepId; return __generator(this, function (_a) { switch (_a.label) { case 0: conditionFn = config.condition; return [4 /*yield*/, conditionFn(workflowContext)]; case 1: branchKey = _a.sent(); nextStepId = config.branches[String(branchKey)]; this.logger.info("Condition step '".concat(config.id, "' evaluated to '").concat(branchKey, "'. Next step: ").concat(nextStepId || 'None')); return [2 /*return*/, { nextStepId: nextStepId, branchKey: branchKey }]; } }); }); }; // --- Helper Methods --- WorkflowExecutor.prototype.updateHistory = function (context, record) { // Ensure history is mutable locally if needed, or handle immutability context.history.push(record); // Optional: Limit history size }; WorkflowExecutor.prototype.resolveInputMapping = function (stepConfig, wfCtx, nodeCtx) { var resolvedInput = __assign({}, stepConfig.input); // Start with static input if (stepConfig.inputMapping) { for (var targetField in stepConfig.inputMapping) { var sourcePath = stepConfig.inputMapping[targetField]; resolvedInput[targetField] = this.resolveContextPath(sourcePath, wfCtx); } } // Make resolved input available on node context nodeCtx.input = Object.freeze(resolvedInput); // Make immutable this.logger.debug("Resolved input for step ".concat(stepConfig.id, ":"), resolvedInput); }; WorkflowExecutor.prototype.resolveOutputMapping = function (stepConfig, nodeCtx, wfCtx) { if (stepConfig.outputMapping && nodeCtx.output) { for (var targetPath in stepConfig.outputMapping) { var sourceField = stepConfig.outputMapping[targetPath]; var value = nodeCtx.output[sourceField]; this.setContextPath(targetPath, value, wfCtx); this.logger.debug("Mapped output '".concat(sourceField, "' to '").concat(targetPath, "' for step ").concat(stepConfig.id)); } } }; // Resolve value from context using dot notation (e.g., "variables.user.id", "input.orderId", "steps.step1.output.result") WorkflowExecutor.prototype.resolveContextPath = function (path, context) { var parts = path.split('.'); var current = context; if (parts[0] === 'variables') { current = context.variables; parts.shift(); } else if (parts[0] === 'input') { current = context.input; parts.shift(); } else if (parts[0] === 'steps') { // Access output from previous steps via history? Or a dedicated step output cache? // Accessing history might be complex. Let's assume direct variable access for now. // This part needs careful design. For simplicity, only support 'variables' and 'input' for now. this.logger.warn("Resolving step outputs via path ('".concat(path, "') not fully supported yet. Use variables.")); return undefined; } else { // Assume it's a top-level context property or variable implicitly current = context.variables; // Default to checking variables } for (var _i = 0, parts_1 = parts; _i < parts_1.length; _i++) { var part = parts_1[_i]; if (current === null || typeof current !== 'object') return undefined; current = current[part]; } return current; }; // Set value in context using dot notation (only supports 'variables' for now) WorkflowExecutor.prototype.setContextPath = function (path, value, context) { var parts = path.split('.'); if (parts[0] !== 'variables' || parts.length < 2) { this.logger.error("Output mapping path '".concat(path, "' is invalid. Only 'variables.xxx' is supported.")); return; } var current = context.variables; for (var i = 1; i < parts.length - 1; i++) { var part = parts[i]; if (current[part] === undefined || typeof current[part] !== 'object') { current[part] = {}; // Create nested objects if they don't exist } current = current[part]; } current[parts[parts.length - 1]] = value; }; WorkflowExecutor.prototype.checkRoles = function (userRole, requiredRoles) { if (!requiredRoles || requiredRoles.length === 0) return true; // No roles required if (!userRole) return false; // Roles required but user has none // Simple check: user must have at least one of the required roles // Could be more complex (e.g., all roles required) return requiredRoles.includes(userRole); }; // Helper to emit events via the event manager WorkflowExecutor.prototype.emitEvent = function (eventName, context, payload) { return __awaiter(this, void 0, void 0, function () { var err_3; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.eventManager) return [3 /*break*/, 4]; _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, this.eventManager.emit(eventName, __assign(__assign({}, (payload || {})), { workflowId: context.workflowId, definitionId: context.definitionId }), context)]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: err_3 = _a.sent(); this.logger.error("Failed to emit event '".concat(eventName, "' for workflow ").concat(context.workflowId), err_3); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; return WorkflowExecutor; }()); exports.WorkflowExecutor = WorkflowExecutor;