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