@calmo/task-runner 3.8.0 → 3.8.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/coverage/coverage-final.json +5 -4
  3. package/coverage/index.html +18 -18
  4. package/coverage/lcov-report/index.html +18 -18
  5. package/coverage/lcov-report/src/EventBus.ts.html +70 -34
  6. package/coverage/lcov-report/src/TaskGraphValidationError.ts.html +1 -1
  7. package/coverage/lcov-report/src/TaskGraphValidator.ts.html +120 -60
  8. package/coverage/lcov-report/src/TaskRunner.ts.html +78 -63
  9. package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +1 -1
  10. package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +1 -1
  11. package/coverage/lcov-report/src/TaskStateManager.ts.html +17 -29
  12. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +1 -1
  13. package/coverage/lcov-report/src/contracts/ErrorTypes.ts.html +103 -0
  14. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
  15. package/coverage/lcov-report/src/contracts/index.html +23 -8
  16. package/coverage/lcov-report/src/index.html +13 -13
  17. package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
  18. package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +1 -1
  19. package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +1 -1
  20. package/coverage/lcov-report/src/strategies/index.html +1 -1
  21. package/coverage/lcov.info +238 -213
  22. package/coverage/src/EventBus.ts.html +70 -34
  23. package/coverage/src/TaskGraphValidationError.ts.html +1 -1
  24. package/coverage/src/TaskGraphValidator.ts.html +120 -60
  25. package/coverage/src/TaskRunner.ts.html +78 -63
  26. package/coverage/src/TaskRunnerBuilder.ts.html +1 -1
  27. package/coverage/src/TaskRunnerExecutionConfig.ts.html +1 -1
  28. package/coverage/src/TaskStateManager.ts.html +17 -29
  29. package/coverage/src/WorkflowExecutor.ts.html +1 -1
  30. package/coverage/src/contracts/ErrorTypes.ts.html +103 -0
  31. package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
  32. package/coverage/src/contracts/index.html +23 -8
  33. package/coverage/src/index.html +13 -13
  34. package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
  35. package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +1 -1
  36. package/coverage/src/strategies/StandardExecutionStrategy.ts.html +1 -1
  37. package/coverage/src/strategies/index.html +1 -1
  38. package/dist/EventBus.js +11 -2
  39. package/dist/EventBus.js.map +1 -1
  40. package/dist/TaskGraphValidator.d.ts +3 -0
  41. package/dist/TaskGraphValidator.js +33 -26
  42. package/dist/TaskGraphValidator.js.map +1 -1
  43. package/dist/TaskRunner.d.ts +4 -0
  44. package/dist/TaskRunner.js +38 -44
  45. package/dist/TaskRunner.js.map +1 -1
  46. package/dist/TaskStateManager.js +1 -5
  47. package/dist/TaskStateManager.js.map +1 -1
  48. package/dist/contracts/ErrorTypes.d.ts +6 -0
  49. package/dist/contracts/ErrorTypes.js +7 -0
  50. package/dist/contracts/ErrorTypes.js.map +1 -0
  51. package/dist/contracts/ValidationError.d.ts +2 -1
  52. package/package.json +1 -1
  53. package/src/EventBus.ts +27 -15
  54. package/src/TaskGraphValidator.ts +52 -32
  55. package/src/TaskRunner.ts +51 -46
  56. package/src/TaskStateManager.ts +1 -5
  57. package/src/contracts/ErrorTypes.ts +6 -0
  58. package/src/contracts/ValidationError.ts +10 -1
  59. package/test-report.xml +152 -128
@@ -2,6 +2,11 @@ import { ITaskGraphValidator } from "./contracts/ITaskGraphValidator.js";
2
2
  import { ValidationResult } from "./contracts/ValidationResult.js";
3
3
  import { ValidationError } from "./contracts/ValidationError.js";
4
4
  import { TaskGraph } from "./TaskGraph.js";
5
+ import {
6
+ ERROR_CYCLE,
7
+ ERROR_DUPLICATE_TASK,
8
+ ERROR_MISSING_DEPENDENCY,
9
+ } from "./contracts/ErrorTypes.js";
5
10
 
6
11
  export class TaskGraphValidator implements ITaskGraphValidator {
7
12
  /**
@@ -18,11 +23,46 @@ export class TaskGraphValidator implements ITaskGraphValidator {
18
23
  const errors: ValidationError[] = [];
19
24
 
20
25
  // 1. Check for duplicate tasks
26
+ const taskIds = this.checkDuplicateTasks(taskGraph, errors);
27
+
28
+ // 2. Check for missing dependencies
29
+ this.checkMissingDependencies(taskGraph, taskIds, errors);
30
+
31
+ // 3. Check for cycles
32
+ // Only run cycle detection if there are no missing dependencies, otherwise we might chase non-existent nodes.
33
+ const hasMissingDependencies = errors.some(
34
+ (e) => e.type === ERROR_MISSING_DEPENDENCY
35
+ );
36
+
37
+ if (!hasMissingDependencies) {
38
+ this.checkCycles(taskGraph, errors);
39
+ }
40
+
41
+ return {
42
+ isValid: errors.length === 0,
43
+ errors,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Creates a human-readable error message from a validation result.
49
+ * @param result The validation result containing errors.
50
+ * @returns A formatted error string.
51
+ */
52
+ createErrorMessage(result: ValidationResult): string {
53
+ const errorDetails = result.errors.map((e) => e.message);
54
+ return `Task graph validation failed: ${errorDetails.join("; ")}`;
55
+ }
56
+
57
+ private checkDuplicateTasks(
58
+ taskGraph: TaskGraph,
59
+ errors: ValidationError[]
60
+ ): Set<string> {
21
61
  const taskIds = new Set<string>();
22
62
  for (const task of taskGraph.tasks) {
23
63
  if (taskIds.has(task.id)) {
24
64
  errors.push({
25
- type: "duplicate_task",
65
+ type: ERROR_DUPLICATE_TASK,
26
66
  message: `Duplicate task detected with ID: ${task.id}`,
27
67
  details: { taskId: task.id },
28
68
  });
@@ -30,33 +70,28 @@ export class TaskGraphValidator implements ITaskGraphValidator {
30
70
  taskIds.add(task.id);
31
71
  }
32
72
  }
73
+ return taskIds;
74
+ }
33
75
 
34
- // 2. Check for missing dependencies
76
+ private checkMissingDependencies(
77
+ taskGraph: TaskGraph,
78
+ taskIds: Set<string>,
79
+ errors: ValidationError[]
80
+ ): void {
35
81
  for (const task of taskGraph.tasks) {
36
82
  for (const dependenceId of task.dependencies) {
37
83
  if (!taskIds.has(dependenceId)) {
38
84
  errors.push({
39
- type: "missing_dependency",
85
+ type: ERROR_MISSING_DEPENDENCY,
40
86
  message: `Task '${task.id}' depends on missing task '${dependenceId}'`,
41
87
  details: { taskId: task.id, missingDependencyId: dependenceId },
42
88
  });
43
89
  }
44
90
  }
45
91
  }
92
+ }
46
93
 
47
- // 3. Check for cycles
48
- // Only run cycle detection if there are no missing dependencies, otherwise we might chase non-existent nodes.
49
- const hasMissingDependencies = errors.some(
50
- (e) => e.type === "missing_dependency"
51
- );
52
-
53
- if (hasMissingDependencies) {
54
- return {
55
- isValid: errors.length === 0,
56
- errors,
57
- };
58
- }
59
-
94
+ private checkCycles(taskGraph: TaskGraph, errors: ValidationError[]): void {
60
95
  // Build adjacency list
61
96
  const adjacencyList = new Map<string, string[]>();
62
97
  for (const task of taskGraph.tasks) {
@@ -82,7 +117,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
82
117
  const cyclePath = path.slice(cycleStartIndex);
83
118
 
84
119
  errors.push({
85
- type: "cycle",
120
+ type: ERROR_CYCLE,
86
121
  message: `Cycle detected: ${cyclePath.join(" -> ")}`,
87
122
  details: { cyclePath },
88
123
  });
@@ -90,21 +125,6 @@ export class TaskGraphValidator implements ITaskGraphValidator {
90
125
  break;
91
126
  }
92
127
  }
93
-
94
- return {
95
- isValid: errors.length === 0,
96
- errors,
97
- };
98
- }
99
-
100
- /**
101
- * Creates a human-readable error message from a validation result.
102
- * @param result The validation result containing errors.
103
- * @returns A formatted error string.
104
- */
105
- createErrorMessage(result: ValidationResult): string {
106
- const errorDetails = result.errors.map((e) => e.message);
107
- return `Task graph validation failed: ${errorDetails.join("; ")}`;
108
128
  }
109
129
 
110
130
  private detectCycle(
package/src/TaskRunner.ts CHANGED
@@ -79,23 +79,14 @@ export class TaskRunner<TContext> {
79
79
  public static getMermaidGraph<T>(steps: TaskStep<T>[]): string {
80
80
  const graphLines = ["graph TD"];
81
81
 
82
- // Helper to sanitize node names or wrap them if needed
83
- // For simplicity, we just wrap in quotes and use the name as ID if it's simple
84
- // or generate an ID if strictly needed. Here we assume names are unique IDs.
85
- // We will wrap names in quotes for the label, but use the name as the ID.
86
- // Actually, Mermaid ID cannot have spaces without quotes.
87
- const safeId = (name: string) => JSON.stringify(name);
88
82
  const sanitize = (name: string) => this.sanitizeMermaidId(name);
89
83
 
90
- // Add all nodes first to ensure they exist
91
84
  for (const step of steps) {
92
- // Using the name as both ID and Label for simplicity
93
- // Format: ID["Label"]
94
- // safeId returns a quoted string (e.g. "Task Name"), so we use it directly as the label
95
- graphLines.push(` ${sanitize(step.name)}[${safeId(step.name)}]`);
85
+ graphLines.push(
86
+ ` ${sanitize(step.name)}[${JSON.stringify(step.name)}]`
87
+ );
96
88
  }
97
89
 
98
- // Add edges
99
90
  for (const step of steps) {
100
91
  if (step.dependencies) {
101
92
  for (const dep of step.dependencies) {
@@ -159,44 +150,58 @@ export class TaskRunner<TContext> {
159
150
  config?.concurrency
160
151
  );
161
152
 
162
- // We need to handle the timeout cleanup properly.
163
153
  if (config?.timeout !== undefined) {
164
- const controller = new AbortController();
165
- const timeoutId = setTimeout(() => {
166
- controller.abort(
167
- new Error(`Workflow timed out after ${config.timeout}ms`)
168
- );
169
- }, config.timeout);
170
-
171
- let effectiveSignal = controller.signal;
172
- let onAbort: (() => void) | undefined;
173
-
174
- // Handle combination of signals if user provided one
175
- if (config.signal) {
176
- if (config.signal.aborted) {
177
- // If already aborted, use it directly (WorkflowExecutor handles early abort)
178
- // We can cancel timeout immediately
179
- clearTimeout(timeoutId);
180
- effectiveSignal = config.signal;
181
- } else {
182
- // Listen to user signal to abort our controller
183
- onAbort = () => {
184
- controller.abort(config.signal?.reason);
185
- };
186
- config.signal.addEventListener("abort", onAbort);
187
- }
188
- }
154
+ return this.executeWithTimeout(
155
+ executor,
156
+ steps,
157
+ config.timeout,
158
+ config.signal
159
+ );
160
+ } else {
161
+ return executor.execute(steps, config?.signal);
162
+ }
163
+ }
189
164
 
190
- try {
191
- return await executor.execute(steps, effectiveSignal);
192
- } finally {
165
+ /**
166
+ * Executes tasks with a timeout, ensuring resources are cleaned up.
167
+ */
168
+ private async executeWithTimeout(
169
+ executor: WorkflowExecutor<TContext>,
170
+ steps: TaskStep<TContext>[],
171
+ timeout: number,
172
+ signal?: AbortSignal
173
+ ): Promise<Map<string, TaskResult>> {
174
+ const controller = new AbortController();
175
+ const timeoutId = setTimeout(() => {
176
+ controller.abort(new Error(`Workflow timed out after ${timeout}ms`));
177
+ }, timeout);
178
+
179
+ let effectiveSignal = controller.signal;
180
+ let onAbort: (() => void) | undefined;
181
+
182
+ // Handle combination of signals if user provided one
183
+ if (signal) {
184
+ if (signal.aborted) {
185
+ // If already aborted, use it directly (WorkflowExecutor handles early abort)
186
+ // We can cancel timeout immediately
193
187
  clearTimeout(timeoutId);
194
- if (config.signal && onAbort) {
195
- config.signal.removeEventListener("abort", onAbort);
196
- }
188
+ effectiveSignal = signal;
189
+ } else {
190
+ // Listen to user signal to abort our controller
191
+ onAbort = () => {
192
+ controller.abort(signal.reason);
193
+ };
194
+ signal.addEventListener("abort", onAbort);
195
+ }
196
+ }
197
+
198
+ try {
199
+ return await executor.execute(steps, effectiveSignal);
200
+ } finally {
201
+ clearTimeout(timeoutId);
202
+ if (signal && onAbort) {
203
+ signal.removeEventListener("abort", onAbort);
197
204
  }
198
- } else {
199
- return executor.execute(steps, config?.signal);
200
205
  }
201
206
  }
202
207
  }
@@ -101,20 +101,16 @@ export class TaskStateManager<TContext> {
101
101
  for (const step of this.pendingSteps) {
102
102
  // Also check running? No, running tasks are handled by AbortSignal in Executor.
103
103
  // We only cancel what is pending and hasn't started.
104
- /* v8 ignore next 1 */
105
104
  if (!this.results.has(step.name) && !this.running.has(step.name)) {
106
105
  const result: TaskResult = {
107
106
  status: "cancelled",
108
107
  message,
109
108
  };
110
109
  this.results.set(step.name, result);
110
+ this.eventBus.emit("taskEnd", { step, result });
111
111
  }
112
112
  }
113
113
  // Clear pending set as they are now "done" (cancelled)
114
- // Wait, if we clear pending steps, processDependencies won't pick them up.
115
- // The loop in Executor relies on results.size or pendingSteps.
116
- // The previous implementation iterated `steps` (all steps) to cancel.
117
- // Here we iterate `pendingSteps`.
118
114
  this.pendingSteps.clear();
119
115
  }
120
116
 
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Error type constants for task graph validation.
3
+ */
4
+ export const ERROR_DUPLICATE_TASK = "duplicate_task" as const;
5
+ export const ERROR_MISSING_DEPENDENCY = "missing_dependency" as const;
6
+ export const ERROR_CYCLE = "cycle" as const;
@@ -1,9 +1,18 @@
1
+ import {
2
+ ERROR_CYCLE,
3
+ ERROR_DUPLICATE_TASK,
4
+ ERROR_MISSING_DEPENDENCY,
5
+ } from "./ErrorTypes.js";
6
+
1
7
  /**
2
8
  * Describes a specific validation error found in the task graph.
3
9
  */
4
10
  export interface ValidationError {
5
11
  /** The type of validation error. */
6
- type: "cycle" | "missing_dependency" | "duplicate_task";
12
+ type:
13
+ | typeof ERROR_CYCLE
14
+ | typeof ERROR_MISSING_DEPENDENCY
15
+ | typeof ERROR_DUPLICATE_TASK;
7
16
  /** A human-readable message describing the error. */
8
17
  message: string;
9
18
  /** Optional detailed information about the error, e.g., the cycle path, or the task with a missing dependency. */