@calmo/task-runner 1.2.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jules/sentinel.md +4 -0
- package/AGENTS.md +53 -1
- package/CHANGELOG.md +41 -15
- package/README.md +1 -1
- package/coverage/coverage-final.json +8 -4
- package/coverage/index.html +24 -9
- package/coverage/lcov-report/index.html +24 -9
- package/coverage/lcov-report/src/EventBus.ts.html +8 -8
- package/coverage/lcov-report/src/TaskGraphValidator.ts.html +143 -62
- package/coverage/lcov-report/src/TaskRunner.ts.html +213 -21
- package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +313 -0
- package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +124 -0
- package/coverage/lcov-report/src/TaskStateManager.ts.html +511 -0
- package/coverage/lcov-report/src/WorkflowExecutor.ts.html +125 -137
- package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/lcov-report/src/contracts/index.html +1 -1
- package/coverage/lcov-report/src/index.html +59 -14
- package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +190 -0
- package/coverage/lcov-report/src/strategies/index.html +116 -0
- package/coverage/lcov.info +383 -184
- package/coverage/src/EventBus.ts.html +8 -8
- package/coverage/src/TaskGraphValidator.ts.html +143 -62
- package/coverage/src/TaskRunner.ts.html +213 -21
- package/coverage/src/TaskRunnerBuilder.ts.html +313 -0
- package/coverage/src/TaskRunnerExecutionConfig.ts.html +124 -0
- package/coverage/src/TaskStateManager.ts.html +511 -0
- package/coverage/src/WorkflowExecutor.ts.html +125 -137
- package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/src/contracts/index.html +1 -1
- package/coverage/src/index.html +59 -14
- package/coverage/src/strategies/StandardExecutionStrategy.ts.html +190 -0
- package/coverage/src/strategies/index.html +116 -0
- package/dist/TaskGraphValidator.js +39 -16
- package/dist/TaskGraphValidator.js.map +1 -1
- package/dist/TaskRunner.d.ts +12 -2
- package/dist/TaskRunner.js +55 -3
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskRunnerBuilder.d.ts +33 -0
- package/dist/TaskRunnerBuilder.js +54 -0
- package/dist/TaskRunnerBuilder.js.map +1 -0
- package/dist/TaskRunnerExecutionConfig.d.ts +13 -0
- package/dist/TaskRunnerExecutionConfig.js +2 -0
- package/dist/TaskRunnerExecutionConfig.js.map +1 -0
- package/dist/TaskStateManager.d.ts +54 -0
- package/dist/TaskStateManager.js +130 -0
- package/dist/TaskStateManager.js.map +1 -0
- package/dist/TaskStatus.d.ts +1 -1
- package/dist/TaskStep.d.ts +2 -1
- package/dist/WorkflowExecutor.d.ts +11 -10
- package/dist/WorkflowExecutor.js +64 -73
- package/dist/WorkflowExecutor.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/strategies/IExecutionStrategy.d.ts +16 -0
- package/dist/strategies/IExecutionStrategy.js +2 -0
- package/dist/strategies/IExecutionStrategy.js.map +1 -0
- package/dist/strategies/StandardExecutionStrategy.d.ts +9 -0
- package/dist/strategies/StandardExecutionStrategy.js +25 -0
- package/dist/strategies/StandardExecutionStrategy.js.map +1 -0
- package/openspec/changes/add-concurrency-control/proposal.md +14 -0
- package/openspec/changes/add-concurrency-control/tasks.md +9 -0
- package/openspec/changes/add-task-retry-policy/proposal.md +13 -0
- package/openspec/changes/add-task-retry-policy/tasks.md +10 -0
- package/openspec/changes/add-workflow-preview/proposal.md +12 -0
- package/openspec/changes/add-workflow-preview/tasks.md +7 -0
- package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/tasks.md +10 -0
- package/openspec/changes/archive/2026-01-18-add-integration-tests/proposal.md +18 -0
- package/openspec/changes/archive/2026-01-18-add-integration-tests/tasks.md +17 -0
- package/openspec/changes/archive/2026-01-18-refactor-core-architecture/proposal.md +14 -0
- package/openspec/changes/archive/2026-01-18-refactor-core-architecture/tasks.md +8 -0
- package/package.json +1 -1
- package/src/TaskGraphValidator.ts +46 -19
- package/src/TaskRunner.ts +68 -4
- package/src/TaskRunnerBuilder.ts +76 -0
- package/src/TaskRunnerExecutionConfig.ts +13 -0
- package/src/TaskStateManager.ts +142 -0
- package/src/TaskStatus.ts +1 -1
- package/src/TaskStep.ts +2 -1
- package/src/WorkflowExecutor.ts +75 -79
- package/src/index.ts +4 -0
- package/src/strategies/IExecutionStrategy.ts +21 -0
- package/src/strategies/StandardExecutionStrategy.ts +35 -0
- package/test-report.xml +121 -43
- package/GEMINI.md +0 -46
- package/openspec/changes/add-external-task-cancellation/tasks.md +0 -10
- /package/openspec/changes/{add-external-task-cancellation → archive/2026-01-18-add-external-task-cancellation}/proposal.md +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## Implementation
|
|
2
|
+
- [x] 1.1 Setup `tests/integration-tests/` directory and test runner config if needed.
|
|
3
|
+
- [x] 1.2 Implement Scenario 1: Basic linear workflow (A -> B -> C) success.
|
|
4
|
+
- [x] 1.3 Implement Scenario 2: Branching workflow (A -> [B, C] -> D) success.
|
|
5
|
+
- [x] 1.4 Implement Scenario 3: Task failure and downstream skipping (A -> B(fail) -> C(skip)).
|
|
6
|
+
- [x] 1.5 Implement Scenario 4: Shared context mutation (A writes, B reads).
|
|
7
|
+
- [x] 1.6 Implement Scenario 5: Large graph execution (e.g., 20+ nodes).
|
|
8
|
+
- [x] 1.7 Implement Scenario 6: Mixed duration tasks (verifying parallel efficiency).
|
|
9
|
+
- [x] 1.8 Implement Scenario 7: Cancellation via AbortSignal in mid-execution.
|
|
10
|
+
- [x] 1.9 Implement Scenario 8: Global timeout interrupting long tasks.
|
|
11
|
+
- [x] 1.10 Implement Scenario 9: Dynamic context validation (tasks validating context state).
|
|
12
|
+
- [x] 1.11 Implement Scenario 10: Circular dependency detection (at runtime validation).
|
|
13
|
+
- [x] 1.12 Implement Scenario 11: Missing dependency handling.
|
|
14
|
+
- [x] 1.13 Implement Scenario 12: Complex "Diamond" dependency graph.
|
|
15
|
+
- [x] 1.14 Implement Scenario 13: Tasks with side-effects (e.g., file I/O or simulated network).
|
|
16
|
+
- [x] 1.15 Implement Scenario 14: Zero-dependency parallel burst (all run at once).
|
|
17
|
+
- [x] 1.16 Verification: Ensure all integration tests pass consistently.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Change: Refactor Core Architecture
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
When multiple developers work on the project, conflicts arise due to tight coupling and poor separation of concerns. Large classes like `WorkflowExecutor` are taking on too many responsibilities, and there is duplicated logic around graph traversal and state management.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Decouple `WorkflowExecutor` from `EventBus` (pass a listener interface or use a mediating controller).
|
|
8
|
+
- Extract `TaskExecutionStrategy` to allow pluggable execution modes (e.g., standard, dry-run, debug).
|
|
9
|
+
- Centralize state management for task results and context, moving it out of the executor loop.
|
|
10
|
+
- Standardize error handling and logging (QoL improvements).
|
|
11
|
+
|
|
12
|
+
## Impact
|
|
13
|
+
- Affected specs: `task-runner` (no behavior change, but structural refactor)
|
|
14
|
+
- Affected code: `src/WorkflowExecutor.ts`, `src/TaskRunner.ts`, new files for extracted logic.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Implementation
|
|
2
|
+
- [x] 1.1 Extract `TaskStateManager` to handle `TaskResult` storage, updates, and context mutations.
|
|
3
|
+
- [x] 1.2 Define `IExecutionStrategy` interface for running tasks (Strategy Pattern).
|
|
4
|
+
- [x] 1.3 Refactor `WorkflowExecutor` to use `TaskStateManager` and `IExecutionStrategy`.
|
|
5
|
+
- [x] 1.4 Move explicit event emission out of the core loop into the `TaskStateManager` or a dedicated `ExecutionObserver`.
|
|
6
|
+
- [x] 1.5 Create a factory or builder for `TaskRunner` to simplify configuration for developers.
|
|
7
|
+
- [x] 1.6 Verify that all existing tests pass with the new structure.
|
|
8
|
+
- [x] 1.7 Add "Quality of Life" improvements (e.g., better error messages with specific task context).
|
package/package.json
CHANGED
|
@@ -104,33 +104,60 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
private detectCycle(
|
|
107
|
-
|
|
107
|
+
startTaskId: string,
|
|
108
108
|
path: string[],
|
|
109
109
|
visited: Set<string>,
|
|
110
110
|
recursionStack: Set<string>,
|
|
111
111
|
adjacencyList: Map<string, string[]>
|
|
112
112
|
): boolean {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
113
|
+
// Use an explicit stack to avoid maximum call stack size exceeded errors
|
|
114
|
+
const stack: { taskId: string; index: number; dependencies: string[] }[] = [];
|
|
115
|
+
|
|
116
|
+
visited.add(startTaskId);
|
|
117
|
+
recursionStack.add(startTaskId);
|
|
118
|
+
path.push(startTaskId);
|
|
119
|
+
|
|
120
|
+
stack.push({
|
|
121
|
+
taskId: startTaskId,
|
|
122
|
+
index: 0,
|
|
123
|
+
/* v8 ignore next */
|
|
124
|
+
dependencies: adjacencyList.get(startTaskId) ?? []
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
while (stack.length > 0) {
|
|
128
|
+
const frame = stack[stack.length - 1];
|
|
129
|
+
const { taskId, dependencies } = frame;
|
|
130
|
+
|
|
131
|
+
if (frame.index < dependencies.length) {
|
|
132
|
+
const dependenceId = dependencies[frame.index];
|
|
133
|
+
frame.index++;
|
|
134
|
+
|
|
135
|
+
if (recursionStack.has(dependenceId)) {
|
|
136
|
+
// Cycle detected
|
|
137
|
+
path.push(dependenceId);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!visited.has(dependenceId)) {
|
|
142
|
+
visited.add(dependenceId);
|
|
143
|
+
recursionStack.add(dependenceId);
|
|
144
|
+
path.push(dependenceId);
|
|
145
|
+
|
|
146
|
+
stack.push({
|
|
147
|
+
taskId: dependenceId,
|
|
148
|
+
index: 0,
|
|
149
|
+
/* v8 ignore next */
|
|
150
|
+
dependencies: adjacencyList.get(dependenceId) ?? []
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
// Finished all dependencies for this node
|
|
155
|
+
recursionStack.delete(taskId);
|
|
156
|
+
path.pop();
|
|
157
|
+
stack.pop();
|
|
129
158
|
}
|
|
130
159
|
}
|
|
131
160
|
|
|
132
|
-
recursionStack.delete(taskId);
|
|
133
|
-
path.pop();
|
|
134
161
|
return false;
|
|
135
162
|
}
|
|
136
163
|
}
|
package/src/TaskRunner.ts
CHANGED
|
@@ -5,9 +5,13 @@ import { TaskGraph } from "./TaskGraph.js";
|
|
|
5
5
|
import { RunnerEventPayloads, RunnerEventListener } from "./contracts/RunnerEvents.js";
|
|
6
6
|
import { EventBus } from "./EventBus.js";
|
|
7
7
|
import { WorkflowExecutor } from "./WorkflowExecutor.js";
|
|
8
|
+
import { TaskRunnerExecutionConfig } from "./TaskRunnerExecutionConfig.js";
|
|
9
|
+
import { TaskStateManager } from "./TaskStateManager.js";
|
|
10
|
+
import { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
|
|
11
|
+
import { StandardExecutionStrategy } from "./strategies/StandardExecutionStrategy.js";
|
|
8
12
|
|
|
9
13
|
// Re-export types for backward compatibility
|
|
10
|
-
export { RunnerEventPayloads, RunnerEventListener };
|
|
14
|
+
export { RunnerEventPayloads, RunnerEventListener, TaskRunnerExecutionConfig };
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* The main class that orchestrates the execution of a list of tasks
|
|
@@ -17,6 +21,7 @@ export { RunnerEventPayloads, RunnerEventListener };
|
|
|
17
21
|
export class TaskRunner<TContext> {
|
|
18
22
|
private eventBus = new EventBus<TContext>();
|
|
19
23
|
private validator = new TaskGraphValidator();
|
|
24
|
+
private executionStrategy: IExecutionStrategy<TContext> = new StandardExecutionStrategy();
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* @param context The shared context object to be passed to each task.
|
|
@@ -44,17 +49,33 @@ export class TaskRunner<TContext> {
|
|
|
44
49
|
event: K,
|
|
45
50
|
callback: RunnerEventListener<TContext, K>
|
|
46
51
|
): void {
|
|
52
|
+
/* v8 ignore next 1 */
|
|
47
53
|
this.eventBus.off(event, callback);
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Sets the execution strategy to be used.
|
|
58
|
+
* @param strategy The execution strategy.
|
|
59
|
+
* @returns The TaskRunner instance for chaining.
|
|
60
|
+
*/
|
|
61
|
+
public setExecutionStrategy(strategy: IExecutionStrategy<TContext>): this {
|
|
62
|
+
/* v8 ignore next 2 */
|
|
63
|
+
this.executionStrategy = strategy;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
/**
|
|
51
68
|
* Executes a list of tasks, respecting their dependencies and running
|
|
52
69
|
* independent tasks in parallel.
|
|
53
70
|
* @param steps An array of TaskStep objects to be executed.
|
|
71
|
+
* @param config Optional configuration for execution (timeout, cancellation).
|
|
54
72
|
* @returns A Promise that resolves to a Map where keys are task names
|
|
55
73
|
* and values are the corresponding TaskResult objects.
|
|
56
74
|
*/
|
|
57
|
-
async execute(
|
|
75
|
+
async execute(
|
|
76
|
+
steps: TaskStep<TContext>[],
|
|
77
|
+
config?: TaskRunnerExecutionConfig
|
|
78
|
+
): Promise<Map<string, TaskResult>> {
|
|
58
79
|
// Validate the task graph before execution
|
|
59
80
|
const taskGraph: TaskGraph = {
|
|
60
81
|
tasks: steps.map((step) => ({
|
|
@@ -68,7 +89,50 @@ export class TaskRunner<TContext> {
|
|
|
68
89
|
throw new Error(this.validator.createErrorMessage(validationResult));
|
|
69
90
|
}
|
|
70
91
|
|
|
71
|
-
const
|
|
72
|
-
|
|
92
|
+
const stateManager = new TaskStateManager(this.eventBus);
|
|
93
|
+
const executor = new WorkflowExecutor(
|
|
94
|
+
this.context,
|
|
95
|
+
this.eventBus,
|
|
96
|
+
stateManager,
|
|
97
|
+
this.executionStrategy
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// We need to handle the timeout cleanup properly.
|
|
101
|
+
if (config?.timeout !== undefined) {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timeoutId = setTimeout(() => {
|
|
104
|
+
controller.abort(new Error(`Workflow timed out after ${config.timeout}ms`));
|
|
105
|
+
}, config.timeout);
|
|
106
|
+
|
|
107
|
+
let effectiveSignal = controller.signal;
|
|
108
|
+
let onAbort: (() => void) | undefined;
|
|
109
|
+
|
|
110
|
+
// Handle combination of signals if user provided one
|
|
111
|
+
if (config.signal) {
|
|
112
|
+
if (config.signal.aborted) {
|
|
113
|
+
// If already aborted, use it directly (WorkflowExecutor handles early abort)
|
|
114
|
+
// We can cancel timeout immediately
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
effectiveSignal = config.signal;
|
|
117
|
+
} else {
|
|
118
|
+
// Listen to user signal to abort our controller
|
|
119
|
+
onAbort = () => {
|
|
120
|
+
controller.abort(config.signal?.reason);
|
|
121
|
+
};
|
|
122
|
+
config.signal.addEventListener("abort", onAbort);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
return await executor.execute(steps, effectiveSignal);
|
|
128
|
+
} finally {
|
|
129
|
+
clearTimeout(timeoutId);
|
|
130
|
+
if (config.signal && onAbort) {
|
|
131
|
+
config.signal.removeEventListener("abort", onAbort);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
return executor.execute(steps, config?.signal);
|
|
136
|
+
}
|
|
73
137
|
}
|
|
74
138
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { TaskRunner } from "./TaskRunner.js";
|
|
2
|
+
import { RunnerEventPayloads, RunnerEventListener } from "./contracts/RunnerEvents.js";
|
|
3
|
+
import { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A builder for configuring and creating TaskRunner instances.
|
|
7
|
+
*/
|
|
8
|
+
export class TaskRunnerBuilder<TContext> {
|
|
9
|
+
private context: TContext;
|
|
10
|
+
private strategy?: IExecutionStrategy<TContext>;
|
|
11
|
+
private listeners: {
|
|
12
|
+
[K in keyof RunnerEventPayloads<TContext>]?: RunnerEventListener<TContext, K>[];
|
|
13
|
+
} = {};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param context The shared context object.
|
|
17
|
+
*/
|
|
18
|
+
constructor(context: TContext) {
|
|
19
|
+
this.context = context;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sets the execution strategy.
|
|
24
|
+
* @param strategy The execution strategy to use.
|
|
25
|
+
* @returns The builder instance.
|
|
26
|
+
*/
|
|
27
|
+
public useStrategy(strategy: IExecutionStrategy<TContext>): this {
|
|
28
|
+
this.strategy = strategy;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Adds an event listener.
|
|
34
|
+
* @param event The event name.
|
|
35
|
+
* @param callback The callback to execute.
|
|
36
|
+
* @returns The builder instance.
|
|
37
|
+
*/
|
|
38
|
+
public on<K extends keyof RunnerEventPayloads<TContext>>(
|
|
39
|
+
event: K,
|
|
40
|
+
callback: RunnerEventListener<TContext, K>
|
|
41
|
+
): this {
|
|
42
|
+
if (!this.listeners[event]) {
|
|
43
|
+
this.listeners[event] = [];
|
|
44
|
+
}
|
|
45
|
+
this.listeners[event]!.push(callback);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds the TaskRunner instance.
|
|
51
|
+
* @returns A configured TaskRunner.
|
|
52
|
+
*/
|
|
53
|
+
public build(): TaskRunner<TContext> {
|
|
54
|
+
const runner = new TaskRunner(this.context);
|
|
55
|
+
|
|
56
|
+
if (this.strategy) {
|
|
57
|
+
runner.setExecutionStrategy(this.strategy);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
(Object.keys(this.listeners) as Array<keyof RunnerEventPayloads<TContext>>).forEach((event) => {
|
|
61
|
+
const callbacks = this.listeners[event];
|
|
62
|
+
// callbacks is always defined because we are iterating keys of the object
|
|
63
|
+
callbacks!.forEach((callback) =>
|
|
64
|
+
runner.on(
|
|
65
|
+
event,
|
|
66
|
+
callback as unknown as RunnerEventListener<
|
|
67
|
+
TContext,
|
|
68
|
+
keyof RunnerEventPayloads<TContext>
|
|
69
|
+
>
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return runner;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for TaskRunner execution.
|
|
3
|
+
*/
|
|
4
|
+
export interface TaskRunnerExecutionConfig {
|
|
5
|
+
/**
|
|
6
|
+
* An AbortSignal to cancel the workflow externally.
|
|
7
|
+
*/
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
/**
|
|
10
|
+
* A timeout in milliseconds for the entire workflow.
|
|
11
|
+
*/
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { TaskStep } from "./TaskStep.js";
|
|
2
|
+
import { TaskResult } from "./TaskResult.js";
|
|
3
|
+
import { EventBus } from "./EventBus.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manages the state of the task execution, including results, pending steps, and running tasks.
|
|
7
|
+
* Handles dependency resolution and event emission for state changes.
|
|
8
|
+
*/
|
|
9
|
+
export class TaskStateManager<TContext> {
|
|
10
|
+
private results = new Map<string, TaskResult>();
|
|
11
|
+
private pendingSteps = new Set<TaskStep<TContext>>();
|
|
12
|
+
private running = new Set<string>();
|
|
13
|
+
|
|
14
|
+
constructor(private eventBus: EventBus<TContext>) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initializes the state with the given steps.
|
|
18
|
+
* @param steps The steps to execute.
|
|
19
|
+
*/
|
|
20
|
+
initialize(steps: TaskStep<TContext>[]): void {
|
|
21
|
+
this.pendingSteps = new Set(steps);
|
|
22
|
+
this.results.clear();
|
|
23
|
+
this.running.clear();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Processes the pending steps to identify tasks that can be started or must be skipped.
|
|
28
|
+
* Emits `taskSkipped` for skipped tasks.
|
|
29
|
+
* @returns An array of tasks that are ready to run.
|
|
30
|
+
*/
|
|
31
|
+
processDependencies(): TaskStep<TContext>[] {
|
|
32
|
+
const toRemove: TaskStep<TContext>[] = [];
|
|
33
|
+
const toRun: TaskStep<TContext>[] = [];
|
|
34
|
+
|
|
35
|
+
for (const step of this.pendingSteps) {
|
|
36
|
+
const deps = step.dependencies ?? [];
|
|
37
|
+
let blocked = false;
|
|
38
|
+
let failedDep: string | undefined;
|
|
39
|
+
|
|
40
|
+
for (const dep of deps) {
|
|
41
|
+
const depResult = this.results.get(dep);
|
|
42
|
+
if (!depResult) {
|
|
43
|
+
// Dependency not finished yet
|
|
44
|
+
blocked = true;
|
|
45
|
+
} else if (depResult.status !== "success") {
|
|
46
|
+
failedDep = dep;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (failedDep) {
|
|
52
|
+
const depResult = this.results.get(failedDep);
|
|
53
|
+
const depError = depResult?.error ? `: ${depResult.error}` : "";
|
|
54
|
+
const result: TaskResult = {
|
|
55
|
+
status: "skipped",
|
|
56
|
+
message: `Skipped because dependency '${failedDep}' failed${depError}`,
|
|
57
|
+
};
|
|
58
|
+
this.results.set(step.name, result);
|
|
59
|
+
this.eventBus.emit("taskSkipped", { step, result });
|
|
60
|
+
toRemove.push(step);
|
|
61
|
+
} else if (!blocked) {
|
|
62
|
+
toRun.push(step);
|
|
63
|
+
toRemove.push(step);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Cleanup pending set
|
|
68
|
+
for (const step of toRemove) {
|
|
69
|
+
this.pendingSteps.delete(step);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return toRun;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Marks a task as running and emits `taskStart`.
|
|
77
|
+
* @param step The task that is starting.
|
|
78
|
+
*/
|
|
79
|
+
markRunning(step: TaskStep<TContext>): void {
|
|
80
|
+
this.running.add(step.name);
|
|
81
|
+
this.eventBus.emit("taskStart", { step });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Marks a task as completed (success, failure, or cancelled during execution)
|
|
86
|
+
* and emits `taskEnd`.
|
|
87
|
+
* @param step The task that completed.
|
|
88
|
+
* @param result The result of the task.
|
|
89
|
+
*/
|
|
90
|
+
markCompleted(step: TaskStep<TContext>, result: TaskResult): void {
|
|
91
|
+
this.running.delete(step.name);
|
|
92
|
+
this.results.set(step.name, result);
|
|
93
|
+
this.eventBus.emit("taskEnd", { step, result });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Cancels all pending tasks that haven't started yet.
|
|
98
|
+
* @param message The cancellation message.
|
|
99
|
+
*/
|
|
100
|
+
cancelAllPending(message: string): void {
|
|
101
|
+
// Iterate over pendingSteps to cancel them
|
|
102
|
+
for (const step of this.pendingSteps) {
|
|
103
|
+
// Also check running? No, running tasks are handled by AbortSignal in Executor.
|
|
104
|
+
// We only cancel what is pending and hasn't started.
|
|
105
|
+
/* v8 ignore next 1 */
|
|
106
|
+
if (!this.results.has(step.name) && !this.running.has(step.name)) {
|
|
107
|
+
const result: TaskResult = {
|
|
108
|
+
status: "cancelled",
|
|
109
|
+
message,
|
|
110
|
+
};
|
|
111
|
+
this.results.set(step.name, result);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Clear pending set as they are now "done" (cancelled)
|
|
115
|
+
// Wait, if we clear pending steps, processDependencies won't pick them up.
|
|
116
|
+
// The loop in Executor relies on results.size or pendingSteps.
|
|
117
|
+
// The previous implementation iterated `steps` (all steps) to cancel.
|
|
118
|
+
// Here we iterate `pendingSteps`.
|
|
119
|
+
this.pendingSteps.clear();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns the current results map.
|
|
124
|
+
*/
|
|
125
|
+
getResults(): Map<string, TaskResult> {
|
|
126
|
+
return this.results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Checks if there are any tasks currently running.
|
|
131
|
+
*/
|
|
132
|
+
hasRunningTasks(): boolean {
|
|
133
|
+
return this.running.size > 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Checks if there are any pending tasks.
|
|
138
|
+
*/
|
|
139
|
+
hasPendingTasks(): boolean {
|
|
140
|
+
return this.pendingSteps.size > 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/TaskStatus.ts
CHANGED
package/src/TaskStep.ts
CHANGED
|
@@ -12,7 +12,8 @@ export interface TaskStep<TContext> {
|
|
|
12
12
|
/**
|
|
13
13
|
* The core logic of the task.
|
|
14
14
|
* @param context The shared context object, allowing for state to be passed between tasks.
|
|
15
|
+
* @param signal An optional AbortSignal to listen for cancellation.
|
|
15
16
|
* @returns A Promise that resolves to a TaskResult.
|
|
16
17
|
*/
|
|
17
|
-
run(context: TContext): Promise<TaskResult>;
|
|
18
|
+
run(context: TContext, signal?: AbortSignal): Promise<TaskResult>;
|
|
18
19
|
}
|