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
JavaScript
/**
* 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 };