UNPKG

taskx

Version:

A lightweight TypeScript library for managing complex asynchronous task dependencies with intelligent dependency graph execution and parallel optimization

309 lines (301 loc) 10.9 kB
/** * Circular dependency error class * Throw this error when a circular dependency is detected in the task network * @class CircularDependencyError * @extends {Error} */ class CircularDependencyError extends Error { /** * Create a circular dependency error instance * @constructor */ constructor() { super('Circular Dependency Error'); this.name = 'CircularDependencyError'; } } /** * Error handling strategy enumeration * @enum {string} */ var ErrorHandlingStrategy; (function (ErrorHandlingStrategy) { /** Stop all tasks when an error occurs */ ErrorHandlingStrategy["STOP_ALL"] = "stop-all"; /** Only stop downstream tasks when an error occurs */ ErrorHandlingStrategy["STOP_DOWNSTREAM"] = "stop-downstream"; })(ErrorHandlingStrategy || (ErrorHandlingStrategy = {})); /** * Get root task list * Find all tasks without parent tasks from the task list * @param {TaskNode[]} tasks - Task list * @returns {TaskNode[]} Root task list */ function getRootTasks(tasks) { /** Visited task set to avoid duplicate processing */ const visited = new Set(); /** Result array for root tasks */ const result = []; /** Processing queue, initially the input task list */ let queue = [...tasks]; /** Breadth-first traversal of all tasks */ while (queue.length > 0) { const items = queue; queue = []; items.forEach((task) => { /** Skip if the task has been visited */ if (visited.has(task)) { return; } /** Mark the task as visited */ visited.add(task); /** If the task has no parent tasks, add to result list */ if (task.parents.length === 0) { result.push(task); } /** Add parent tasks to the queue for continued processing */ queue.push(...task.parents); }); } return result; } /** * Get all tasks * Get all related tasks from the task list (including upstream and downstream) * @param {TaskNode[]} tasks - Task list * @returns {TaskNode[]} List of all related tasks */ function getAllTasks(tasks) { /** Set of all tasks */ const allTasks = new Set(); /** Processing queue, initially the input task list */ const queue = [...tasks]; /** Breadth-first traversal of all related tasks */ while (queue.length > 0) { const current = queue.shift(); /** Skip if the task has been processed */ if (allTasks.has(current)) { continue; } /** Mark the task as processed */ allTasks.add(current); /** Add downstream tasks to the queue */ queue.push(...current.next); /** Add upstream tasks to the queue */ queue.push(...current.parents); } /** Return array form of all tasks */ return Array.from(allTasks); } /** * Check if there is a circular dependency * Use topological sorting algorithm to detect if there are circular dependencies in the task network * @param {TaskNode[]} tasks - Task list * @returns {boolean} Whether there is a circular dependency */ function hasCircularDependency(tasks) { /** Build graph structure represented by adjacency list */ const graph = new Map(); /** Store the in-degree of each node */ const inDegree = new Map(); /** Initialize graph and in-degree table */ tasks.forEach((task) => { graph.set(task, [...task.next]); inDegree.set(task, 0); }); /** Calculate the in-degree of each node */ tasks.forEach((task) => { task.next.forEach((next) => { if (inDegree.has(next)) { inDegree.set(next, (inDegree.get(next) || 0) + 1); } }); }); /** Topological sorting queue */ const queue = []; /** Visited node counter */ let visitedCount = 0; /** Find all nodes with in-degree 0 and add to queue */ tasks.forEach((task) => { if (inDegree.get(task) === 0) { queue.push(task); } }); /** Topological sorting process */ while (queue.length > 0) { const current = queue.shift(); /** Increase visited count */ visitedCount++; /** Get all neighbors of the current node */ const neighbors = graph.get(current); /** Decrease the in-degree of all neighbors */ for (const neighbor of neighbors) { /** Decrease the in-degree of the neighbor node */ const degree = inDegree.get(neighbor) - 1; inDegree.set(neighbor, degree); /** If neighbor node's in-degree becomes 0, add to queue */ if (degree === 0) { queue.push(neighbor); } } } /** If visited node count is not equal to total node count, there is a circular dependency */ return visitedCount !== tasks.length; } /** * Context factory function * Create task network context based on error handling strategy * @type {ContextFactory} * @param {ErrorHandlingStrategy} errorHandlingStrategy - Error handling strategy * @returns {iTaskxContext} Task network context */ const createContext = (errorHandlingStrategy) => ({ /** Create empty execution result map */ results: new Map(), /** Create empty completed task set */ completed: new Set(), /** Set error handling strategy */ errorHandlingStrategy, }); /** * Task processor class * Responsible for managing and executing task networks * @class TaskProcessor */ class TaskProcessor { /** * Create task processor instance * @constructor * @param {iTaskProcessorConfig} [config] - Processor configuration */ constructor(_config) { this._config = _config; /** Create context using default strategy or strategy from configuration */ this._context = createContext(this._config?.errorHandlingStrategy || ErrorHandlingStrategy.STOP_ALL); } /** * Get current task network context * @readonly * @returns {iTaskxContext} Task network context */ get context() { return this._context; } /** * Process task list * Validate task network and execute all tasks * @param {TaskNode[]} tasks - List of tasks to process * @returns {Promise<void>} Asynchronous processing result * @throws {CircularDependencyError} Throw when circular dependency is detected * @throws {Error} Throw when an error occurs during task execution */ async process(tasks) { /** Get all related tasks */ const allTasks = getAllTasks(tasks); /** Get root tasks (tasks without dependencies) */ const rootTasks = getRootTasks(allTasks); /** Check if there is a circular dependency */ if (hasCircularDependency(allTasks)) { throw new CircularDependencyError(); } /** Execute all root tasks in parallel */ await Promise.all(rootTasks.map((task) => task.tryProcess(this.context))); /** If there is an error in the context, throw the error */ if (this.context.error) { throw this.context.error; } return; } } /** * Processor factory function * Create task processor instance based on configuration * @type {ProcessorFactory} * @param {iTaskProcessorConfig} config - Processor configuration * @returns {TaskProcessor} Task processor instance */ const useProcessor = (config) => new TaskProcessor(config); /** * Wrapped task class * Provides task execution and dependency management functionality * @class WrappedTask */ class TaskNode { /** * Create a wrapped task instance * @constructor * @param {AsyncMethod} process - Task execution method * @param {TaskNode[]} [parents=[]] - Parent task list */ constructor(process, parents = []) { this.process = process; this.parents = parents; /** Flag indicating whether the task has an error */ this.error = false; /** List of downstream tasks */ this.next = []; } /** * Try to process the current task * Check preconditions and error handling strategy, then execute the task and trigger downstream tasks * @param {iTaskxContext} context - Task network context * @returns {Promise<void>} Asynchronous execution result */ async tryProcess(context) { /** Define array of break condition functions */ const breakConditions = [ /** Check if parent tasks are completed or if current task is already completed */ () => this.parents.some((parent) => !context.completed.has(parent.process)) || context.completed.has(this.process), /** Check if it's STOP_ALL strategy and an error has occurred */ () => context.errorHandlingStrategy === ErrorHandlingStrategy.STOP_ALL && context.error, /** Check if it's STOP_DOWNSTREAM strategy and a parent task has an error */ () => context.errorHandlingStrategy === ErrorHandlingStrategy.STOP_DOWNSTREAM && this.parents.some((parent) => parent.error), ]; /** Find the first satisfied break condition */ const touchBreakIndex = breakConditions.findIndex((condition) => condition()); /** If there is a break condition */ if (touchBreakIndex !== -1) { /** If the break is caused by parent task error */ if (touchBreakIndex === 2) { this.error = true; } return; } try { /** Execute the task processing method */ await this.process(context); } catch (e) { /** Catch and record the error */ context.error = e; this.error = true; } finally { /** Mark the task as completed */ context.completed.add(this.process); } /** Process all downstream tasks in parallel */ await Promise.all(this.next.map((tasks) => tasks.tryProcess(context))); } /** * Add task dependency relationship * @param {...TaskNode[]} parents - Parent task list * @returns {TaskNode} Return current task instance, supporting chained calls */ dependOn(...parents) { /** Add parent tasks to the current task's parent list */ this.parents.push(...parents); /** Add current task to all parent tasks' child task list */ parents.forEach((parent) => parent.next.push(this)); return this; } } /** * Task registration function * Wrap async methods as task instances * @type {TaskRegistor} * @param {AsyncMethod} process - Async processing method * @returns {TaskNode} Wrapped task */ const registerTask = (process) => new TaskNode(process); export { CircularDependencyError, ErrorHandlingStrategy, registerTask, useProcessor };