UNPKG

chrono-forge

Version:

A comprehensive framework for building resilient Temporal workflows, advanced state management, and real-time streaming activities in TypeScript. Designed for a seamless developer experience with powerful abstractions, dynamic orchestration, and full cont

571 lines (570 loc) 26 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DSLInterpreter = DSLInterpreter; exports.convertStepsToDSL = convertStepsToDSL; const workflow_1 = require("@temporalio/workflow"); const eventemitter3_graphology_1 = require("eventemitter3-graphology"); const graphology_dag_1 = require("graphology-dag"); const dottie_1 = __importDefault(require("dottie")); async function* DSLInterpreter(dsl, injectedActivities, injectedSteps, options) { const acts = injectedActivities || (0, workflow_1.proxyActivities)({ startToCloseTimeout: '1 minute' }); const steps = injectedSteps || {}; const visualizationFormat = options?.visualizationFormat ?? 'tree'; const bindings = new Proxy(dsl.variables, { get: (target, prop) => { return dsl.variables[prop]; }, set: (target, prop, value) => { target[prop] = value; dsl.variables[prop] = value; return true; } }); const graph = buildDependencyGraph(dsl.plan, bindings); const generations = (0, graphology_dag_1.topologicalGenerations)(graph); if (generations.length === 0) { console.warn('No generations found in the graph. Skipping execution.'); return; } console.log(visualizeWorkflow(graph, visualizationFormat)); const skippedNodes = new Set(); for (const generation of generations) { for (const nodeId of generation) { const node = graph.getNodeAttributes(nodeId); if (!node?.execute) continue; const dependencies = graph.inNeighbors(nodeId); const anyRequiredDependencySkipped = dependencies.some((depId) => { if (!skippedNodes.has(depId)) { return false; } const depNode = graph.getNodeAttributes(depId); return depNode?.required === true; }); if (anyRequiredDependencySkipped) { skippedNodes.add(nodeId); console.log(`Skipping node ${nodeId} because a required dependency was skipped`); continue; } if (node.when) { try { const conditionMet = node.when(dsl.variables, dsl.plan); if (!conditionMet) { skippedNodes.add(nodeId); console.log(`Skipping node ${nodeId} because condition returned false`); if (node.type === 'sequence') { const descendants = getAllDescendants(graph, nodeId); for (const descendantId of descendants) { skippedNodes.add(descendantId); console.log(`Skipping descendant node ${descendantId} because parent sequence condition returned false`); } } continue; } } catch (error) { console.error(`Error evaluating condition for ${nodeId}:`, error); skippedNodes.add(nodeId); continue; } } if (node.wait) { try { if ((0, workflow_1.inWorkflowContext)()) { await (0, workflow_1.condition)(Array.isArray(node.wait) ? node.wait[0] : node.wait, Array.isArray(node.wait) ? node.wait[1] : undefined); } else { const waitFn = Array.isArray(node.wait) ? node.wait[0] : node.wait; const timeout = Array.isArray(node.wait) ? node.wait[1] : undefined; const startTime = Date.now(); const hasTimeout = timeout !== undefined && timeout !== null && timeout > 0; const timeoutMs = hasTimeout ? timeout * 1000 : 0; while (!waitFn(dsl.variables, dsl.plan)) { if (hasTimeout && Date.now() - startTime > timeoutMs) { console.log(`Timeout waiting for condition on node ${nodeId}`); skippedNodes.add(nodeId); break; } await new Promise((resolve) => setTimeout(resolve, 100)); } if (skippedNodes.has(nodeId)) { continue; } } } catch (error) { console.error(`Error in wait condition for ${nodeId}:`, error); skippedNodes.add(nodeId); continue; } } if (skippedNodes.has(nodeId)) { continue; } yield { nodeId, graph, bindings: dsl.variables, acts, steps, nodeIds: generation, execute: async () => await node.execute({ activities: acts, steps, variables: dsl.variables, plan: dsl.plan }) }; } } console.log('Workflow completed successfully'); } function buildDependencyGraph(plan, bindings, existingGraph, startId) { let autoIncrementId = startId ?? 0; const graph = existingGraph ?? new eventemitter3_graphology_1.DirectedGraph(); const createExecuteFunction = (type, nodeId, name, args, store, statement, executorMap) => { return async ({ activities, steps, variables, plan }) => { executorMap = executorMap ?? (type === 'activity' ? activities : steps); if (!executorMap?.[name]) { throw new Error(`${type} function '${name}' not found`); } const resolvedArgs = args.map((arg) => { const value = dottie_1.default.get(bindings, arg); if (value !== undefined) return value; return graph.hasNode(arg) ? (graph.getNodeAttribute(arg, 'result') ?? arg) : arg; }); const output = await executorMap[name](...resolvedArgs); if (output !== undefined) { if (store) { bindings[store] = output; } graph.setNodeAttribute(nodeId, 'result', output); } return output; }; }; const addNodeAndDependencies = (type, name, args, store, statement, executorMap) => { const nodeId = `${type}_${name}_${autoIncrementId++}`; graph.addNode(nodeId, { type, name, args, store, when: statement?.when, wait: statement?.wait, required: statement?.required, execute: createExecuteFunction(type, nodeId, name, args, store, statement, executorMap) }); for (const arg of args) { const dependencyNodes = Array.from(graph.nodes()).filter((n) => { const nodeResult = graph.getNodeAttribute(n, 'store'); return nodeResult === arg; }); for (const depNode of dependencyNodes) { try { graph.addDirectedEdge(depNode, nodeId); } catch { } } } return nodeId; }; const processStatement = (statement, previousNodeId) => { let nodeId; if ('sequence' in statement) { const { elements, when, wait, required } = statement.sequence; if (when || wait) { nodeId = `sequence_condition_${autoIncrementId++}`; graph.addNode(nodeId, { type: 'sequence', when, wait, required, execute: async ({ activities, steps, variables, plan }) => { const tempGraph = buildDependencyGraph({ sequence: { elements } }, bindings, undefined, autoIncrementId); autoIncrementId += 1000; await executeGraphByGenerations(tempGraph, bindings, activities, steps, { variables, plan }, { visualizationFormat: 'tree' }); } }); if (previousNodeId) { try { graph.addDirectedEdge(previousNodeId, nodeId); } catch { } } } let lastNodeId = nodeId; for (const element of elements) { lastNodeId = processStatement(element, lastNodeId); } return lastNodeId; } else if ('parallel' in statement) { const startNodeId = previousNodeId; const nodeIds = (statement?.parallel?.branches ?? []) .map((branch) => processStatement(branch, startNodeId)) .filter((id) => id !== undefined); return nodeIds.length > 0 ? nodeIds[nodeIds.length - 1] : undefined; } else { if (statement?.foreach) { nodeId = `foreach_${autoIncrementId++}`; graph.addNode(nodeId, { type: 'foreach', in: statement.foreach.in, as: statement.foreach.as, body: statement.foreach.body, condition: statement.when, wait: statement.wait, execute: async ({ activities, steps, variables, plan }) => { const itemsArray = dottie_1.default.get(bindings, statement.foreach.in, []); for (const as of itemsArray) { bindings[statement.foreach.as] = as; const tempGraph = buildDependencyGraph(statement.foreach.body, bindings, undefined, autoIncrementId); autoIncrementId += 1000; await executeGraphByGenerations(tempGraph, bindings, activities, steps, { variables, plan }, { visualizationFormat: 'tree' }); } } }); } else if (statement?.while) { nodeId = `while_${autoIncrementId++}`; graph.addNode(nodeId, { type: 'while', condition: statement.while.condition, body: statement.while.body, wait: statement.wait, execute: async ({ activities, steps, variables, plan }) => { while (await statement.while.condition(variables, plan)) { const tempGraph = buildDependencyGraph(statement.while.body, bindings, undefined, autoIncrementId); autoIncrementId += 1000; await executeGraphByGenerations(tempGraph, bindings, activities, steps, { variables, plan }, { visualizationFormat: 'tree' }); } } }); } else if (statement?.doWhile) { nodeId = `doWhile_${autoIncrementId++}`; graph.addNode(nodeId, { type: 'doWhile', condition: statement.doWhile.condition, body: statement.doWhile.body, wait: statement.wait, execute: async ({ activities, steps, variables, plan }) => { do { const tempGraph = buildDependencyGraph(statement.doWhile.body, bindings, undefined, autoIncrementId); autoIncrementId += 1000; await executeGraphByGenerations(tempGraph, bindings, activities, steps, { variables, plan }, { visualizationFormat: 'tree' }); } while (await statement.doWhile.condition(variables, plan)); } }); } else if (statement?.execute) { const { name = `${autoIncrementId++}`, with: args = [], store, code, step, activity } = statement.execute; if (activity) { nodeId = addNodeAndDependencies('activity', activity, args, store, statement); } else if (step) { nodeId = addNodeAndDependencies('step', step, args, store, statement); } else if (code) { nodeId = addNodeAndDependencies('code', name, args, store, statement, { [name]: async (...vals) => { const context = {}; for (let i = 0; i < args.length; i++) { context[args[i]] = vals[i]; } const contextProxy = new Proxy(context, { get: (target, prop) => { if (prop in target) { return target[prop]; } return bindings[prop]; }, set: (target, prop, value) => { if (args.includes(prop)) { target[prop] = value; } bindings[prop] = value; return true; } }); const fn = new Function('context', `with (context) { ${code} }`); return fn(contextProxy); } }); } } if (nodeId && previousNodeId) { try { graph.addDirectedEdge(previousNodeId, nodeId); } catch { } } return nodeId; } }; processStatement(plan); if ((0, graphology_dag_1.hasCycle)(graph)) { throw new Error('Circular dependency detected in workflow graph'); } return graph; } async function executeGraphByGenerations(graph, bindings, activities, steps, dsl, options) { const generations = (0, graphology_dag_1.topologicalGenerations)(graph); if (generations.length === 0) { console.warn('No generations found in the graph. Skipping execution.'); return; } console.log(visualizeWorkflow(graph, options?.visualizationFormat ?? 'list')); for (let genIndex = 0; genIndex < generations.length; genIndex++) { const generation = generations[genIndex]; console.log(`\x1b[1m\x1b[36mExecuting generation ${genIndex} with ${generation.length} nodes:\x1b[0m ${generation.join(', ')}`); await executeGenerationWithErrorHandling(generation, graph, activities, steps, dsl); } console.log('\x1b[1m\x1b[32mWorkflow completed successfully\x1b[0m'); } function visualizeWorkflowGenerations(graph) { const generations = (0, graphology_dag_1.topologicalGenerations)(graph); let visualization = '\x1b[1m\x1b[36mWorkflow Execution Plan:\x1b[0m\n'; generations.forEach((generation, index) => { visualization += `\n\x1b[1m\x1b[33mGeneration ${index}:\x1b[0m\n`; generation.forEach((nodeId) => { const dependencies = graph.inNeighbors(nodeId); const nodeType = graph.hasNodeAttribute(nodeId, 'type') ? graph.getNodeAttribute(nodeId, 'type') : 'unknown'; let nodeTypeColor = '\x1b[32m'; if (nodeType === 'activity') nodeTypeColor = '\x1b[35m'; if (nodeType === 'step') nodeTypeColor = '\x1b[36m'; if (nodeType === 'foreach' || nodeType === 'while' || nodeType === 'doWhile') nodeTypeColor = '\x1b[33m'; visualization += ` • \x1b[1m${nodeId}\x1b[0m [${nodeTypeColor}${nodeType}\x1b[0m]`; if (dependencies.length > 0) { visualization += ` \x1b[90m(depends on: ${dependencies.join(', ')})\x1b[0m`; } visualization += '\n'; }); }); return visualization; } function visualizeWorkflowAsTree(graph) { (0, graphology_dag_1.topologicalGenerations)(graph); let result = '\x1b[1m\x1b[36mWorkflow Tree:\x1b[0m\n'; const nodeChildren = {}; graph.forEachNode((nodeId) => { nodeChildren[nodeId] = graph.outNeighbors(nodeId); }); const rootNodes = graph.nodes().filter((node) => graph.inDegree(node) === 0); const buildTree = (nodeId, prefix = '', isLast = true) => { const nodeType = graph.hasNodeAttribute(nodeId, 'type') ? graph.getNodeAttribute(nodeId, 'type') : 'unknown'; let nodeTypeColor = '\x1b[32m'; if (nodeType === 'activity') nodeTypeColor = '\x1b[35m'; if (nodeType === 'step') nodeTypeColor = '\x1b[36m'; if (nodeType === 'foreach' || nodeType === 'while' || nodeType === 'doWhile') nodeTypeColor = '\x1b[33m'; const connector = isLast ? '└── ' : '├── '; result += `${prefix}${connector}\x1b[1m${nodeId}\x1b[0m [${nodeTypeColor}${nodeType}\x1b[0m]\n`; const newPrefix = prefix + (isLast ? ' ' : '│ '); const children = nodeChildren[nodeId] || []; children.forEach((child, index) => { buildTree(child, newPrefix, index === children.length - 1); }); }; rootNodes.forEach((rootNode, index) => { buildTree(rootNode, '', index === rootNodes.length - 1); }); return result; } function visualizeWorkflow(graph, format = 'list') { return format === 'tree' ? visualizeWorkflowAsTree(graph) : visualizeWorkflowGenerations(graph); } async function executeGenerationWithErrorHandling(generation, graph, activities, steps, dsl) { const results = await Promise.allSettled(generation.map(async (nodeId) => { try { if (graph.hasNodeAttribute(nodeId, 'execute')) { const execute = graph.getNodeAttribute(nodeId, 'execute'); await execute({ activities, steps, variables: dsl.variables, plan: dsl.plan }); } } catch (error) { console.error(`Error executing node ${nodeId}:`, error); throw error; } })); const failures = results.filter((r) => r.status === 'rejected'); if (failures.length > 0) { throw new Error(`${failures.length} operations failed in this generation`); } } function convertStepsToDSL(steps, initialVariables = {}, workflowInstance) { if (!steps || steps.length === 0) { return { variables: initialVariables, plan: { sequence: { elements: [] } } }; } const graph = new eventemitter3_graphology_1.DirectedGraph(); for (const step of steps) { graph.addNode(step.name, { metadata: step }); } for (const step of steps) { if (step.before) { const beforeSteps = Array.isArray(step.before) ? step.before : [step.before]; for (const beforeStep of beforeSteps) { if (graph.hasNode(beforeStep)) { graph.addDirectedEdge(step.name, beforeStep); } } } if (step.after) { const afterSteps = Array.isArray(step.after) ? step.after : [step.after]; for (const afterStep of afterSteps) { if (graph.hasNode(afterStep)) { graph.addDirectedEdge(afterStep, step.name); } } } } if ((0, graphology_dag_1.hasCycle)(graph)) { throw new Error('Circular dependency detected in workflow steps'); } const generations = (0, graphology_dag_1.topologicalGenerations)(graph); const dslElements = []; for (const generation of generations) { if (generation.length === 1) { const stepName = generation[0]; const stepMeta = steps.find((s) => s.name === stepName); if (stepMeta) { const statement = { execute: { step: stepMeta.method, store: stepName } }; if (stepMeta.when) { const whenFn = stepMeta.when; statement.when = function (variables, plan) { try { if (workflowInstance) { return Boolean(whenFn.call(workflowInstance, variables, plan)); } return Boolean(whenFn.call(this, variables, plan)); } catch (error) { console.error(`Error evaluating when condition for step ${stepName}:`, error); return false; } }; } if (stepMeta.condition) { const waitFn = stepMeta.condition; statement.wait = function (variables, plan) { try { if (workflowInstance) { return Boolean(waitFn.call(workflowInstance, variables, plan)); } return Boolean(waitFn.call(this, variables, plan)); } catch (error) { console.error(`Error evaluating wait condition for step ${stepName}:`, error); return false; } }; } if (stepMeta.retries !== undefined) { statement.retries = stepMeta.retries; } if (stepMeta.timeout !== undefined) { statement.timeout = stepMeta.timeout; } if (stepMeta.required !== undefined) { statement.required = stepMeta.required; } dslElements.push(statement); } } else if (generation.length > 1) { const parallelBranches = []; for (const stepName of generation) { const stepMeta = steps.find((s) => s.name === stepName); if (stepMeta) { const statement = { execute: { step: stepMeta.method, store: stepName } }; if (stepMeta.when) { const whenFn = stepMeta.when; statement.when = function (variables, plan) { try { if (workflowInstance) { return Boolean(whenFn.call(workflowInstance, variables, plan)); } return Boolean(whenFn.call(this, variables, plan)); } catch (error) { console.error(`Error evaluating when condition for step ${stepName}:`, error); return false; } }; } if (stepMeta.condition) { const waitFn = stepMeta.condition; statement.wait = function (variables, plan) { try { if (workflowInstance) { return Boolean(waitFn.call(workflowInstance, variables, plan)); } return Boolean(waitFn.call(this, variables, plan)); } catch (error) { console.error(`Error evaluating wait condition for step ${stepName}:`, error); return false; } }; } if (stepMeta.retries !== undefined) { statement.retries = stepMeta.retries; } if (stepMeta.timeout !== undefined) { statement.timeout = stepMeta.timeout; } if (stepMeta.required !== undefined) { statement.required = stepMeta.required; } parallelBranches.push(statement); } } dslElements.push({ parallel: { branches: parallelBranches } }); } } return { variables: initialVariables, plan: { sequence: { elements: dslElements } } }; } function getAllDescendants(graph, nodeId) { const descendants = new Set(); function collectDescendants(currentNodeId) { const children = graph.outNeighbors(currentNodeId); for (const childId of children) { if (!descendants.has(childId)) { descendants.add(childId); collectDescendants(childId); } } } collectDescendants(nodeId); return Array.from(descendants); }