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
214 lines (213 loc) • 7.16 kB
TypeScript
import 'reflect-metadata';
export interface StepOptions {
/**
* Custom name for the step. If not provided, the method name will be used.
*/
name?: string;
/**
* Condition function that determines if this step should be executed.
* The step will only run if this function returns true.
*/
on?: () => boolean;
/**
* Steps that should be executed before this step.
* Can be a single step name or an array of step names.
*/
before?: string | string[];
/**
* Steps that should be executed after this step.
* Can be a single step name or an array of step names.
*/
after?: string | string[];
/**
* Maximum number of retry attempts if the step fails.
* Default is 0 (no retries).
*/
retries?: number;
/**
* Timeout in milliseconds after which the step execution will be aborted.
* Default is undefined (no timeout).
*/
timeout?: number;
/**
* Whether this step is required for workflow completion.
* If false, workflow can complete even if this step fails or is skipped.
* Default is true.
*/
required?: boolean;
/**
* Custom error handler for this specific step.
* @param error The error that occurred during step execution
* @returns A value to use as the step result, or throws to propagate the error
*/
onError?: (error: Error) => any;
}
export interface StepMetadata {
name: string;
method: string;
on?: () => boolean;
before?: string | string[];
after?: string | string[];
retries?: number;
timeout?: number;
required?: boolean;
onError?: (error: Error) => any;
executed?: boolean;
result?: any;
error?: Error;
}
/**
* Step decorator for defining workflow steps with dependencies and execution conditions.
*
* This decorator allows you to define methods as workflow steps with specific execution
* order, conditions, retry logic, and error handling. Steps can have dependencies on other
* steps using the 'before' and 'after' options.
*
* @example
* ```typescript
* class MyWorkflow extends Workflow {
* @Step()
* async step1() {
* // This step will run first
* }
*
* @Step({ after: 'step1', retries: 3 })
* async step2() {
* // This step will run after step1 and retry up to 3 times if it fails
* }
*
* @Step({
* after: ['step1', 'step2'],
* on: () => this.someCondition,
* onError: (err) => console.error('Step failed:', err)
* })
* async conditionalStep() {
* // This step will only run if someCondition is true
* // and both step1 and step2 have completed
* }
* }
* ```
*
* To execute steps in the correct order, use the `executeSteps()` method in your workflow's
* execute method:
*
* ```typescript
* async execute() {
* return await this.executeSteps();
* }
* ```
*
* @param options Configuration options for the step
* @returns Method decorator
*/
export declare const Step: (options?: StepOptions) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
/**
* Helper method to be added to the Workflow class for executing steps in the correct order.
* This should be called from the workflow's execute method.
*
* @example
* ```typescript
* // Add this method to your Workflow base class
* protected async executeSteps(): Promise<any> {
* const steps = this.constructor._steps || [];
* const executionOrder = this.resolveStepDependencies(steps);
*
* let results: Record<string, any> = {};
*
* for (const stepName of executionOrder) {
* const step = steps.find(s => s.name === stepName);
* if (!step) continue;
*
* // Skip if condition function returns false
* if (step.on && !step.on.call(this)) {
* this.log?.debug(`Skipping step '${stepName}' because condition returned false`);
* continue;
* }
*
* this.log?.debug(`Executing step '${stepName}'`);
* try {
* results[stepName] = await this[step.method]();
* } catch (error) {
* if (step.required) {
* throw error;
* } else {
* this.log?.warn(`Non-required step '${stepName}' failed: ${error.message}`);
* }
* }
* }
*
* return results;
* }
*
* // Helper method to resolve step dependencies into execution order using Graphology.
* // This implements a proper DAG (Directed Acyclic Graph) for dependency resolution.
* private resolveStepDependenciesWithDAG(steps: StepMetadata[]): string[] {
* // Import graphology - you'll need to add this as a dependency
* // npm install graphology graphology-operators
* const { DirectedGraph } = require('graphology');
* const { topologicalSort, hasCycle } = require('graphology-operators');
*
* // Create a directed graph
* const graph = new DirectedGraph();
*
* // Add all steps as nodes
* for (const step of steps) {
* if (!graph.hasNode(step.name)) {
* graph.addNode(step.name, { metadata: step });
* }
* }
*
* // Add edges based on before/after relationships
* for (const step of steps) {
* // Handle 'before' dependencies (current step must run before these steps)
* if (step.before) {
* const beforeSteps = Array.isArray(step.before) ? step.before : [step.before];
* for (const beforeStep of beforeSteps) {
* if (graph.hasNode(beforeStep) && !graph.hasEdge(step.name, beforeStep)) {
* // Edge from current step to 'before' step
* graph.addEdge(step.name, beforeStep);
* }
* }
* }
*
* // Handle 'after' dependencies (current step must run after these steps)
* if (step.after) {
* const afterSteps = Array.isArray(step.after) ? step.after : [step.after];
* for (const afterStep of afterSteps) {
* if (graph.hasNode(afterStep) && !graph.hasEdge(afterStep, step.name)) {
* // Edge from 'after' step to current step
* graph.addEdge(afterStep, step.name);
* }
* }
* }
* }
*
* // Check for cycles in the graph
* if (hasCycle(graph)) {
* throw new Error('Circular dependency detected in workflow steps');
* }
*
* // Perform topological sort to get execution order
* return topologicalSort(graph);
* }
*
* // Legacy method for resolving step dependencies - kept for backward compatibility
* // New code should use resolveStepDependenciesWithDAG instead
* private resolveStepDependencies(steps: StepMetadata[]): string[] {
* try {
* return this.resolveStepDependenciesWithDAG(steps);
* } catch (error) {
* this.log?.warn(`Failed to resolve dependencies with DAG: ${error.message}. Falling back to legacy method.`);
*
* // ... existing implementation ...
* }
* }
* ```
*/
/**
* The Step decorator should be used with a WorkflowBase class that implements:
* 1. executeSteps() - for running the steps in dependency order
* 2. resolveStepDependencies() - for determining the proper execution order
*
* @see The example implementation in src/tests/testWorkflows/UserRegistrationWorkflow.ts
*/