@calmo/task-runner 4.1.0 → 4.3.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/nexus.md +5 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +51 -0
- package/README.md +34 -0
- package/conductor/code_styleguides/general.md +23 -0
- package/conductor/code_styleguides/javascript.md +51 -0
- package/conductor/code_styleguides/typescript.md +43 -0
- package/conductor/product-guidelines.md +14 -0
- package/conductor/product.md +16 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +19 -0
- package/conductor/workflow.md +334 -0
- package/dist/EventBus.js +19 -18
- package/dist/EventBus.js.map +1 -1
- package/dist/PluginManager.d.ts +22 -0
- package/dist/PluginManager.js +40 -0
- package/dist/PluginManager.js.map +1 -0
- package/dist/TaskGraphValidator.d.ts +1 -1
- package/dist/TaskGraphValidator.js +16 -21
- package/dist/TaskGraphValidator.js.map +1 -1
- package/dist/TaskRunner.d.ts +8 -1
- package/dist/TaskRunner.js +37 -25
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskStateManager.d.ts +1 -0
- package/dist/TaskStateManager.js +22 -6
- package/dist/TaskStateManager.js.map +1 -1
- package/dist/TaskStep.d.ts +12 -0
- package/dist/WorkflowExecutor.js +19 -10
- package/dist/WorkflowExecutor.js.map +1 -1
- package/dist/contracts/Plugin.d.ts +30 -0
- package/dist/contracts/Plugin.js +2 -0
- package/dist/contracts/Plugin.js.map +1 -0
- package/dist/strategies/DryRunExecutionStrategy.js +2 -2
- package/dist/strategies/DryRunExecutionStrategy.js.map +1 -1
- package/dist/strategies/StandardExecutionStrategy.js +43 -1
- package/dist/strategies/StandardExecutionStrategy.js.map +1 -1
- package/openspec/changes/add-middleware-support/proposal.md +19 -0
- package/openspec/changes/add-middleware-support/specs/task-runner/spec.md +34 -0
- package/openspec/changes/add-middleware-support/tasks.md +9 -0
- package/openspec/changes/add-resource-concurrency/tasks.md +1 -0
- package/openspec/changes/allow-plugin-hooks/design.md +51 -0
- package/openspec/changes/allow-plugin-hooks/proposal.md +12 -0
- package/openspec/changes/allow-plugin-hooks/specs/post-task/spec.md +21 -0
- package/openspec/changes/allow-plugin-hooks/specs/pre-task/spec.md +32 -0
- package/openspec/changes/allow-plugin-hooks/tasks.md +7 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/design.md +45 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/proposal.md +13 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-context/spec.md +17 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-loading/spec.md +27 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/tasks.md +7 -0
- package/openspec/changes/feat-completion-dependencies/proposal.md +58 -0
- package/openspec/changes/feat-completion-dependencies/tasks.md +46 -0
- package/openspec/changes/feat-task-caching/design.md +34 -0
- package/openspec/changes/feat-task-caching/proposal.md +18 -0
- package/openspec/changes/feat-task-caching/specs/task-runner/spec.md +58 -0
- package/openspec/changes/feat-task-caching/tasks.md +24 -0
- package/openspec/changes/feat-task-loop/proposal.md +22 -0
- package/openspec/changes/feat-task-loop/specs/task-runner/spec.md +34 -0
- package/openspec/changes/feat-task-loop/tasks.md +8 -0
- package/openspec/proposals/feat-matrix-execution/proposal.md +23 -0
- package/openspec/proposals/feat-matrix-execution/specs/task-runner/spec.md +47 -0
- package/openspec/proposals/feat-matrix-execution/tasks.md +11 -0
- package/openspec/proposals/feat-task-observability/proposal.md +16 -0
- package/openspec/proposals/feat-task-observability/specs/task-runner/spec.md +14 -0
- package/openspec/proposals/feat-task-observability/tasks.md +7 -0
- package/openspec/specs/plugin-context/spec.md +19 -0
- package/openspec/specs/plugin-loading/spec.md +29 -0
- package/package.json +1 -1
- package/src/EventBus.ts +11 -15
- package/src/PluginManager.ts +43 -0
- package/src/TaskGraphValidator.ts +22 -24
- package/src/TaskRunner.ts +45 -28
- package/src/TaskStateManager.ts +20 -6
- package/src/TaskStep.ts +14 -0
- package/src/WorkflowExecutor.ts +24 -11
- package/src/contracts/Plugin.ts +32 -0
- package/src/strategies/DryRunExecutionStrategy.ts +2 -2
- package/src/strategies/StandardExecutionStrategy.ts +48 -1
- /package/openspec/changes/{feat-continue-on-error → archive/2026-02-18-feat-continue-on-error}/proposal.md +0 -0
- /package/openspec/changes/{feat-continue-on-error → archive/2026-02-18-feat-continue-on-error}/tasks.md +0 -0
- /package/openspec/changes/{feat-per-task-timeout → archive/2026-02-25-feat-per-task-timeout}/proposal.md +0 -0
- /package/openspec/changes/{feat-per-task-timeout → archive/2026-02-25-feat-per-task-timeout}/specs/task-runner/spec.md +0 -0
- /package/openspec/changes/{feat-per-task-timeout → archive/2026-02-25-feat-per-task-timeout}/tasks.md +0 -0
package/src/TaskRunner.ts
CHANGED
|
@@ -14,11 +14,10 @@ import { TaskGraphValidationError } from "./TaskGraphValidationError.js";
|
|
|
14
14
|
import { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
|
|
15
15
|
import { StandardExecutionStrategy } from "./strategies/StandardExecutionStrategy.js";
|
|
16
16
|
import { RetryingExecutionStrategy } from "./strategies/RetryingExecutionStrategy.js";
|
|
17
|
+
import { Plugin } from "./contracts/Plugin.js";
|
|
18
|
+
import { PluginManager } from "./PluginManager.js";
|
|
17
19
|
import { DryRunExecutionStrategy } from "./strategies/DryRunExecutionStrategy.js";
|
|
18
20
|
|
|
19
|
-
// Re-export types for backward compatibility
|
|
20
|
-
export { RunnerEventPayloads, RunnerEventListener, TaskRunnerExecutionConfig };
|
|
21
|
-
|
|
22
21
|
/**
|
|
23
22
|
* The main class that orchestrates the execution of a list of tasks
|
|
24
23
|
* based on their dependencies, with support for parallel execution.
|
|
@@ -30,10 +29,14 @@ export class TaskRunner<TContext> {
|
|
|
30
29
|
private executionStrategy: IExecutionStrategy<TContext> =
|
|
31
30
|
new RetryingExecutionStrategy(new StandardExecutionStrategy());
|
|
32
31
|
|
|
32
|
+
private pluginManager: PluginManager<TContext>;
|
|
33
|
+
|
|
33
34
|
/**
|
|
34
35
|
* @param context The shared context object to be passed to each task.
|
|
35
36
|
*/
|
|
36
|
-
constructor(private context: TContext) {
|
|
37
|
+
constructor(private context: TContext) {
|
|
38
|
+
this.pluginManager = new PluginManager({ events: this.eventBus });
|
|
39
|
+
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
42
|
* Subscribe to an event.
|
|
@@ -56,17 +59,25 @@ export class TaskRunner<TContext> {
|
|
|
56
59
|
event: K,
|
|
57
60
|
callback: RunnerEventListener<TContext, K>
|
|
58
61
|
): void {
|
|
59
|
-
/* v8 ignore next 1 */
|
|
60
62
|
this.eventBus.off(event, callback);
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Registers a plugin.
|
|
67
|
+
* @param plugin The plugin to register.
|
|
68
|
+
* @returns The TaskRunner instance for chaining.
|
|
69
|
+
*/
|
|
70
|
+
public use(plugin: Plugin<TContext>): this {
|
|
71
|
+
this.pluginManager.use(plugin);
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
/**
|
|
64
76
|
* Sets the execution strategy to be used.
|
|
65
77
|
* @param strategy The execution strategy.
|
|
66
78
|
* @returns The TaskRunner instance for chaining.
|
|
67
79
|
*/
|
|
68
80
|
public setExecutionStrategy(strategy: IExecutionStrategy<TContext>): this {
|
|
69
|
-
/* v8 ignore next 2 */
|
|
70
81
|
this.executionStrategy = strategy;
|
|
71
82
|
return this;
|
|
72
83
|
}
|
|
@@ -77,14 +88,16 @@ export class TaskRunner<TContext> {
|
|
|
77
88
|
* @returns A string containing the Mermaid graph definition.
|
|
78
89
|
*/
|
|
79
90
|
public static getMermaidGraph<T>(steps: TaskStep<T>[]): string {
|
|
80
|
-
const
|
|
91
|
+
const nodeLines: string[] = ["graph TD"];
|
|
92
|
+
const edgeLines = new Set<string>();
|
|
81
93
|
const idMap = new Map<string, string>();
|
|
82
94
|
const usedIds = new Set<string>();
|
|
83
95
|
const baseIdCounters = new Map<string, number>();
|
|
84
96
|
|
|
85
97
|
const getUniqueId = (name: string) => {
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
const existingId = idMap.get(name);
|
|
99
|
+
if (existingId !== undefined) {
|
|
100
|
+
return existingId;
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
const sanitized = this.sanitizeMermaidId(name);
|
|
@@ -112,30 +125,31 @@ export class TaskRunner<TContext> {
|
|
|
112
125
|
return uniqueId;
|
|
113
126
|
};
|
|
114
127
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
getUniqueId(
|
|
121
|
-
}
|
|
128
|
+
// Process nodes and edges in a single pass over input steps
|
|
129
|
+
const processedNodes = new Set<string>();
|
|
130
|
+
for (let i = 0; i < steps.length; i++) {
|
|
131
|
+
const step = steps[i];
|
|
132
|
+
const name = step.name;
|
|
133
|
+
const stepId = getUniqueId(name);
|
|
122
134
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
const sizeBefore = processedNodes.size;
|
|
136
|
+
processedNodes.add(stepId);
|
|
137
|
+
|
|
138
|
+
if (processedNodes.size !== sizeBefore) {
|
|
139
|
+
const escapedName = name.replaceAll("\"", """);
|
|
140
|
+
nodeLines.push(` ${stepId}["${escapedName}"]`);
|
|
141
|
+
}
|
|
127
142
|
|
|
128
|
-
for (const step of steps) {
|
|
129
143
|
if (step.dependencies) {
|
|
130
|
-
const
|
|
131
|
-
for (
|
|
132
|
-
const depId = getUniqueId(
|
|
133
|
-
|
|
144
|
+
const deps = step.dependencies;
|
|
145
|
+
for (let j = 0; j < deps.length; j++) {
|
|
146
|
+
const depId = getUniqueId(deps[j]);
|
|
147
|
+
edgeLines.add(` ${depId} --> ${stepId}`);
|
|
134
148
|
}
|
|
135
149
|
}
|
|
136
150
|
}
|
|
137
151
|
|
|
138
|
-
return [...
|
|
152
|
+
return nodeLines.concat([...edgeLines]).join("\n");
|
|
139
153
|
}
|
|
140
154
|
|
|
141
155
|
/**
|
|
@@ -159,6 +173,9 @@ export class TaskRunner<TContext> {
|
|
|
159
173
|
steps: TaskStep<TContext>[],
|
|
160
174
|
config?: TaskRunnerExecutionConfig
|
|
161
175
|
): Promise<Map<string, TaskResult>> {
|
|
176
|
+
// Initialize plugins
|
|
177
|
+
await this.pluginManager.initialize();
|
|
178
|
+
|
|
162
179
|
// Validate the task graph before execution
|
|
163
180
|
const taskGraph: TaskGraph = {
|
|
164
181
|
tasks: steps.map((step) => ({
|
|
@@ -197,9 +214,9 @@ export class TaskRunner<TContext> {
|
|
|
197
214
|
config.timeout,
|
|
198
215
|
config.signal
|
|
199
216
|
);
|
|
200
|
-
} else {
|
|
201
|
-
return executor.execute(steps, config?.signal);
|
|
202
217
|
}
|
|
218
|
+
|
|
219
|
+
return executor.execute(steps, config?.signal);
|
|
203
220
|
}
|
|
204
221
|
|
|
205
222
|
/**
|
package/src/TaskStateManager.ts
CHANGED
|
@@ -15,6 +15,7 @@ export class TaskStateManager<TContext> {
|
|
|
15
15
|
private dependencyGraph = new Map<string, TaskStep<TContext>[]>();
|
|
16
16
|
private dependencyCounts = new Map<string, number>();
|
|
17
17
|
private readyQueue: TaskStep<TContext>[] = [];
|
|
18
|
+
private taskDefinitions = new Map<string, TaskStep<TContext>>();
|
|
18
19
|
|
|
19
20
|
constructor(private eventBus: EventBus<TContext>) {}
|
|
20
21
|
|
|
@@ -30,8 +31,10 @@ export class TaskStateManager<TContext> {
|
|
|
30
31
|
|
|
31
32
|
this.dependencyGraph.clear();
|
|
32
33
|
this.dependencyCounts.clear();
|
|
34
|
+
this.taskDefinitions.clear();
|
|
33
35
|
|
|
34
36
|
for (const step of steps) {
|
|
37
|
+
this.taskDefinitions.set(step.name, step);
|
|
35
38
|
const deps = step.dependencies ?? [];
|
|
36
39
|
this.dependencyCounts.set(step.name, deps.length);
|
|
37
40
|
|
|
@@ -39,10 +42,12 @@ export class TaskStateManager<TContext> {
|
|
|
39
42
|
this.readyQueue.push(step);
|
|
40
43
|
} else {
|
|
41
44
|
for (const dep of deps) {
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
let dependents = this.dependencyGraph.get(dep);
|
|
46
|
+
if (dependents === undefined) {
|
|
47
|
+
dependents = [];
|
|
48
|
+
this.dependencyGraph.set(dep, dependents);
|
|
44
49
|
}
|
|
45
|
-
|
|
50
|
+
dependents.push(step);
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
}
|
|
@@ -54,7 +59,7 @@ export class TaskStateManager<TContext> {
|
|
|
54
59
|
* @returns An array of tasks that are ready to run.
|
|
55
60
|
*/
|
|
56
61
|
processDependencies(): TaskStep<TContext>[] {
|
|
57
|
-
const toRun =
|
|
62
|
+
const toRun = this.readyQueue;
|
|
58
63
|
this.readyQueue = [];
|
|
59
64
|
|
|
60
65
|
// Remove them from pendingSteps as they are now handed off to the executor
|
|
@@ -87,6 +92,13 @@ export class TaskStateManager<TContext> {
|
|
|
87
92
|
|
|
88
93
|
if (result.status === "success") {
|
|
89
94
|
this.handleSuccess(step.name);
|
|
95
|
+
} else if (result.status === "failure") {
|
|
96
|
+
// If continueOnError is true, treat as success for dependents to unblock the workflow
|
|
97
|
+
if (this.taskDefinitions.get(step.name)?.continueOnError) {
|
|
98
|
+
this.handleSuccess(step.name);
|
|
99
|
+
} else {
|
|
100
|
+
this.cascadeFailure(step.name);
|
|
101
|
+
}
|
|
90
102
|
} else {
|
|
91
103
|
this.cascadeFailure(step.name);
|
|
92
104
|
}
|
|
@@ -188,11 +200,13 @@ export class TaskStateManager<TContext> {
|
|
|
188
200
|
*/
|
|
189
201
|
private cascadeFailure(failedStepName: string): void {
|
|
190
202
|
const queue = [failedStepName];
|
|
203
|
+
// Optimization: Use index pointer instead of shift() to avoid O(N) array re-indexing
|
|
204
|
+
let head = 0;
|
|
191
205
|
// Use a set to track visited nodes in this cascade pass to avoid redundant processing,
|
|
192
206
|
// although checking results.has() in internalMarkSkipped also prevents it.
|
|
193
207
|
|
|
194
|
-
while (queue.length
|
|
195
|
-
const currentName = queue
|
|
208
|
+
while (head < queue.length) {
|
|
209
|
+
const currentName = queue[head++];
|
|
196
210
|
const dependents = this.dependencyGraph.get(currentName);
|
|
197
211
|
|
|
198
212
|
if (!dependents) continue;
|
package/src/TaskStep.ts
CHANGED
|
@@ -25,6 +25,20 @@ export interface TaskStep<TContext> {
|
|
|
25
25
|
*/
|
|
26
26
|
priority?: number;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Optional flag to indicate that the workflow should continue even if this task fails.
|
|
30
|
+
* If true, dependent tasks will execute as if this task succeeded.
|
|
31
|
+
* The task result will still be marked as "failure".
|
|
32
|
+
* Default is false.
|
|
33
|
+
*/
|
|
34
|
+
continueOnError?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Optional maximum execution time in milliseconds.
|
|
38
|
+
* If the task runs longer than this, it will be cancelled and marked as failed.
|
|
39
|
+
*/
|
|
40
|
+
timeout?: number;
|
|
41
|
+
|
|
28
42
|
/**
|
|
29
43
|
* The core logic of the task.
|
|
30
44
|
* @param context The shared context object, allowing for state to be passed between tasks.
|
package/src/WorkflowExecutor.ts
CHANGED
|
@@ -63,8 +63,22 @@ export class WorkflowExecutor<TContext> {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
try {
|
|
66
|
+
// Create a signal mechanism to wait for any task completion
|
|
67
|
+
let signalResolver!: () => void;
|
|
68
|
+
let signalPromise = new Promise<void>((resolve) => {
|
|
69
|
+
signalResolver = resolve;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const signalWork = () => {
|
|
73
|
+
signalResolver();
|
|
74
|
+
// Reset the promise for the next wait
|
|
75
|
+
signalPromise = new Promise<void>((resolve) => {
|
|
76
|
+
signalResolver = resolve;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
66
80
|
// Initial pass
|
|
67
|
-
this.processLoop(executingPromises, signal);
|
|
81
|
+
this.processLoop(executingPromises, signalWork, signal);
|
|
68
82
|
|
|
69
83
|
while (
|
|
70
84
|
this.stateManager.hasPendingTasks() ||
|
|
@@ -78,16 +92,13 @@ export class WorkflowExecutor<TContext> {
|
|
|
78
92
|
break;
|
|
79
93
|
} else {
|
|
80
94
|
// Wait for the next task to finish
|
|
81
|
-
await
|
|
95
|
+
await signalPromise;
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
if (signal?.aborted) {
|
|
85
99
|
this.stateManager.cancelAllPending(
|
|
86
100
|
ExecutionConstants.WORKFLOW_CANCELLED
|
|
87
101
|
);
|
|
88
|
-
} else {
|
|
89
|
-
// After a task finishes, check for new work
|
|
90
|
-
this.processLoop(executingPromises, signal);
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
|
|
@@ -109,6 +120,7 @@ export class WorkflowExecutor<TContext> {
|
|
|
109
120
|
*/
|
|
110
121
|
private processLoop(
|
|
111
122
|
executingPromises: Set<Promise<void>>,
|
|
123
|
+
signalWork: () => void,
|
|
112
124
|
signal?: AbortSignal
|
|
113
125
|
): void {
|
|
114
126
|
const newlyReady = this.stateManager.processDependencies();
|
|
@@ -129,12 +141,13 @@ export class WorkflowExecutor<TContext> {
|
|
|
129
141
|
|
|
130
142
|
const step = this.readyQueue.pop()!;
|
|
131
143
|
|
|
132
|
-
const taskPromise = this.executeTaskStep(step, signal)
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
const taskPromise = this.executeTaskStep(step, signal).finally(() => {
|
|
145
|
+
executingPromises.delete(taskPromise);
|
|
146
|
+
// When a task finishes, we try to run more
|
|
147
|
+
this.processLoop(executingPromises, signalWork, signal);
|
|
148
|
+
// Signal that a task has completed
|
|
149
|
+
signalWork();
|
|
150
|
+
});
|
|
138
151
|
|
|
139
152
|
executingPromises.add(taskPromise);
|
|
140
153
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EventBus } from "../EventBus.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context provided to a plugin during installation.
|
|
5
|
+
* exposes capabilities to hook into the runner lifecycle and context.
|
|
6
|
+
*/
|
|
7
|
+
export interface PluginContext<TContext> {
|
|
8
|
+
/**
|
|
9
|
+
* The event bus instance to subscribe to runner events.
|
|
10
|
+
*/
|
|
11
|
+
events: EventBus<TContext>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Interface that all plugins must implement.
|
|
16
|
+
*/
|
|
17
|
+
export interface Plugin<TContext> {
|
|
18
|
+
/**
|
|
19
|
+
* Unique name of the plugin.
|
|
20
|
+
*/
|
|
21
|
+
name: string;
|
|
22
|
+
/**
|
|
23
|
+
* Semantic version of the plugin.
|
|
24
|
+
*/
|
|
25
|
+
version: string;
|
|
26
|
+
/**
|
|
27
|
+
* Called when the plugin is installed into the runner.
|
|
28
|
+
* This is where the plugin should subscribe to events or perform setup.
|
|
29
|
+
* @param context The plugin context exposing runner capabilities.
|
|
30
|
+
*/
|
|
31
|
+
install(context: PluginContext<TContext>): void | Promise<void>;
|
|
32
|
+
}
|
|
@@ -22,9 +22,9 @@ export class DryRunExecutionStrategy<
|
|
|
22
22
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
23
|
_signal?: AbortSignal
|
|
24
24
|
): Promise<TaskResult> {
|
|
25
|
-
return
|
|
25
|
+
return {
|
|
26
26
|
status: "success",
|
|
27
27
|
message: "Dry run: simulated success " + step.name,
|
|
28
|
-
}
|
|
28
|
+
};
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -13,8 +13,51 @@ export class StandardExecutionStrategy<
|
|
|
13
13
|
context: TContext,
|
|
14
14
|
signal?: AbortSignal
|
|
15
15
|
): Promise<TaskResult> {
|
|
16
|
+
if (!step.timeout) {
|
|
17
|
+
try {
|
|
18
|
+
return await step.run(context, signal);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// Check if error is due to abort
|
|
21
|
+
if (
|
|
22
|
+
signal?.aborted &&
|
|
23
|
+
((e instanceof Error && e.name === "AbortError") ||
|
|
24
|
+
signal.reason === e)
|
|
25
|
+
) {
|
|
26
|
+
return {
|
|
27
|
+
status: "cancelled",
|
|
28
|
+
message: "Task cancelled during execution",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
status: "failure",
|
|
33
|
+
error: e instanceof Error ? e.message : String(e),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const abortController = new AbortController();
|
|
39
|
+
const timeoutSignal = signal
|
|
40
|
+
? AbortSignal.any([signal, abortController.signal])
|
|
41
|
+
: abortController.signal;
|
|
42
|
+
|
|
43
|
+
let timer: NodeJS.Timeout | undefined;
|
|
44
|
+
let resolveTimeout!: (value: TaskResult) => void;
|
|
45
|
+
|
|
16
46
|
try {
|
|
17
|
-
|
|
47
|
+
const timeoutPromise = new Promise<TaskResult>((resolve) => {
|
|
48
|
+
resolveTimeout = resolve;
|
|
49
|
+
timer = setTimeout(() => {
|
|
50
|
+
abortController.abort(new Error("Timeout"));
|
|
51
|
+
resolve({
|
|
52
|
+
status: "failure",
|
|
53
|
+
error: `Task timed out after ${step.timeout}ms`,
|
|
54
|
+
});
|
|
55
|
+
}, step.timeout);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const taskPromise = step.run(context, timeoutSignal);
|
|
59
|
+
|
|
60
|
+
return await Promise.race([taskPromise, timeoutPromise]);
|
|
18
61
|
} catch (e) {
|
|
19
62
|
// Check if error is due to abort
|
|
20
63
|
if (
|
|
@@ -30,6 +73,10 @@ export class StandardExecutionStrategy<
|
|
|
30
73
|
status: "failure",
|
|
31
74
|
error: e instanceof Error ? e.message : String(e),
|
|
32
75
|
};
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
// Settle the timeout promise to avoid memory leaks from Promise.race
|
|
79
|
+
resolveTimeout({ status: "cancelled" } as TaskResult);
|
|
33
80
|
}
|
|
34
81
|
}
|
|
35
82
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|