@calmo/task-runner 1.0.1 → 1.1.1
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/.github/workflows/ci.yml +7 -0
- package/CHANGELOG.md +46 -0
- package/GEMINI.md +5 -1
- package/README.md +20 -0
- package/coverage/TaskRunner.ts.html +373 -31
- package/coverage/coverage-final.json +1 -1
- package/coverage/index.html +9 -9
- package/coverage/lcov-report/TaskRunner.ts.html +685 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +116 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +108 -0
- package/dist/TaskRunner.d.ts +47 -0
- package/dist/TaskRunner.js +53 -2
- package/dist/TaskRunner.js.map +1 -1
- package/package.json +2 -2
- package/sonar-project.properties +22 -0
- package/src/TaskRunner.ts +116 -2
- package/test-report.xml +59 -0
- package/tsconfig.test.json +8 -0
package/src/TaskRunner.ts
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import { TaskStep } from "./TaskStep.js";
|
|
2
2
|
import { TaskResult } from "./TaskResult.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Define the payload for every possible event in the lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
export interface RunnerEventPayloads<TContext> {
|
|
8
|
+
workflowStart: {
|
|
9
|
+
context: TContext;
|
|
10
|
+
steps: TaskStep<TContext>[];
|
|
11
|
+
};
|
|
12
|
+
workflowEnd: {
|
|
13
|
+
context: TContext;
|
|
14
|
+
results: Map<string, TaskResult>;
|
|
15
|
+
};
|
|
16
|
+
taskStart: {
|
|
17
|
+
step: TaskStep<TContext>;
|
|
18
|
+
};
|
|
19
|
+
taskEnd: {
|
|
20
|
+
step: TaskStep<TContext>;
|
|
21
|
+
result: TaskResult;
|
|
22
|
+
};
|
|
23
|
+
taskSkipped: {
|
|
24
|
+
step: TaskStep<TContext>;
|
|
25
|
+
result: TaskResult;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A generic listener type that maps the event key to its specific payload.
|
|
31
|
+
*/
|
|
32
|
+
export type RunnerEventListener<
|
|
33
|
+
TContext,
|
|
34
|
+
K extends keyof RunnerEventPayloads<TContext>,
|
|
35
|
+
> = (data: RunnerEventPayloads<TContext>[K]) => void | Promise<void>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Helper type for the listeners map to avoid private access issues in generic contexts.
|
|
39
|
+
*/
|
|
40
|
+
type ListenerMap<TContext> = {
|
|
41
|
+
[K in keyof RunnerEventPayloads<TContext>]?: Set<
|
|
42
|
+
RunnerEventListener<TContext, K>
|
|
43
|
+
>;
|
|
44
|
+
};
|
|
45
|
+
|
|
4
46
|
/**
|
|
5
47
|
* The main class that orchestrates the execution of a list of tasks
|
|
6
48
|
* based on their dependencies, with support for parallel execution.
|
|
@@ -8,12 +50,76 @@ import { TaskResult } from "./TaskResult.js";
|
|
|
8
50
|
*/
|
|
9
51
|
export class TaskRunner<TContext> {
|
|
10
52
|
private running = new Set<string>();
|
|
53
|
+
private listeners: ListenerMap<TContext> = {};
|
|
11
54
|
|
|
12
55
|
/**
|
|
13
56
|
* @param context The shared context object to be passed to each task.
|
|
14
57
|
*/
|
|
15
58
|
constructor(private context: TContext) {}
|
|
16
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to an event.
|
|
62
|
+
* @param event The event name.
|
|
63
|
+
* @param callback The callback to execute when the event is emitted.
|
|
64
|
+
*/
|
|
65
|
+
public on<K extends keyof RunnerEventPayloads<TContext>>(
|
|
66
|
+
event: K,
|
|
67
|
+
callback: RunnerEventListener<TContext, K>
|
|
68
|
+
): void {
|
|
69
|
+
if (!this.listeners[event]) {
|
|
70
|
+
// Type assertion needed because TypeScript cannot verify that the generic K
|
|
71
|
+
// matches the specific key in the mapped type during assignment.
|
|
72
|
+
this.listeners[event] = new Set() as unknown as ListenerMap<TContext>[K];
|
|
73
|
+
}
|
|
74
|
+
// Type assertion needed to tell TS that this specific Set matches the callback type
|
|
75
|
+
(this.listeners[event] as Set<RunnerEventListener<TContext, K>>).add(
|
|
76
|
+
callback
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Unsubscribe from an event.
|
|
82
|
+
* @param event The event name.
|
|
83
|
+
* @param callback The callback to remove.
|
|
84
|
+
*/
|
|
85
|
+
public off<K extends keyof RunnerEventPayloads<TContext>>(
|
|
86
|
+
event: K,
|
|
87
|
+
callback: RunnerEventListener<TContext, K>
|
|
88
|
+
): void {
|
|
89
|
+
if (this.listeners[event]) {
|
|
90
|
+
(this.listeners[event] as Set<RunnerEventListener<TContext, K>>).delete(
|
|
91
|
+
callback
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Emit an event to all subscribers.
|
|
98
|
+
* @param event The event name.
|
|
99
|
+
* @param data The payload for the event.
|
|
100
|
+
*/
|
|
101
|
+
private emit<K extends keyof RunnerEventPayloads<TContext>>(
|
|
102
|
+
event: K,
|
|
103
|
+
data: RunnerEventPayloads<TContext>[K]
|
|
104
|
+
): void {
|
|
105
|
+
const listeners = this.listeners[event] as
|
|
106
|
+
| Set<RunnerEventListener<TContext, K>>
|
|
107
|
+
| undefined;
|
|
108
|
+
if (listeners) {
|
|
109
|
+
for (const listener of listeners) {
|
|
110
|
+
try {
|
|
111
|
+
listener(data);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// Prevent listener errors from bubbling up
|
|
114
|
+
console.error(
|
|
115
|
+
`Error in event listener for ${String(event)}:`,
|
|
116
|
+
error
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
17
123
|
/**
|
|
18
124
|
* Executes a list of tasks, respecting their dependencies and running
|
|
19
125
|
* independent tasks in parallel.
|
|
@@ -22,6 +128,8 @@ export class TaskRunner<TContext> {
|
|
|
22
128
|
* and values are the corresponding TaskResult objects.
|
|
23
129
|
*/
|
|
24
130
|
async execute(steps: TaskStep<TContext>[]): Promise<Map<string, TaskResult>> {
|
|
131
|
+
this.emit("workflowStart", { context: this.context, steps });
|
|
132
|
+
|
|
25
133
|
const results = new Map<string, TaskResult>();
|
|
26
134
|
|
|
27
135
|
while (results.size < steps.length) {
|
|
@@ -44,10 +152,12 @@ export class TaskRunner<TContext> {
|
|
|
44
152
|
(dep) => results.has(dep) && results.get(dep)?.status !== "success"
|
|
45
153
|
);
|
|
46
154
|
if (failedDep) {
|
|
47
|
-
|
|
155
|
+
const result: TaskResult = {
|
|
48
156
|
status: "skipped",
|
|
49
157
|
message: `Skipped due to failed dependency: ${failedDep}`,
|
|
50
|
-
}
|
|
158
|
+
};
|
|
159
|
+
results.set(step.name, result);
|
|
160
|
+
this.emit("taskSkipped", { step, result });
|
|
51
161
|
}
|
|
52
162
|
}
|
|
53
163
|
|
|
@@ -66,6 +176,7 @@ export class TaskRunner<TContext> {
|
|
|
66
176
|
await Promise.all(
|
|
67
177
|
readySteps.map(async (step) => {
|
|
68
178
|
this.running.add(step.name);
|
|
179
|
+
this.emit("taskStart", { step });
|
|
69
180
|
try {
|
|
70
181
|
const result = await step.run(this.context);
|
|
71
182
|
results.set(step.name, result);
|
|
@@ -76,11 +187,14 @@ export class TaskRunner<TContext> {
|
|
|
76
187
|
});
|
|
77
188
|
} finally {
|
|
78
189
|
this.running.delete(step.name);
|
|
190
|
+
const result = results.get(step.name)!;
|
|
191
|
+
this.emit("taskEnd", { step, result });
|
|
79
192
|
}
|
|
80
193
|
})
|
|
81
194
|
);
|
|
82
195
|
}
|
|
83
196
|
|
|
197
|
+
this.emit("workflowEnd", { context: this.context, results });
|
|
84
198
|
return results;
|
|
85
199
|
}
|
|
86
200
|
}
|
package/test-report.xml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
|
2
|
+
<testsuites name="vitest tests" tests="19" failures="0" errors="0" time="0.135083259">
|
|
3
|
+
<testsuite name="tests/ComplexScenario.test.ts" timestamp="2026-01-18T03:50:46.197Z" hostname="runnervmmtnos" tests="2" failures="0" errors="0" skipped="0" time="0.009883356">
|
|
4
|
+
<testcase classname="tests/ComplexScenario.test.ts" name="Complex Scenario Integration Tests > 1. All steps execute when all succeed and context is hydrated" time="0.006132912">
|
|
5
|
+
<system-out>
|
|
6
|
+
Running StepA
|
|
7
|
+
Running StepB
|
|
8
|
+
Running StepC
|
|
9
|
+
|
|
10
|
+
Running StepD
|
|
11
|
+
Running StepF
|
|
12
|
+
|
|
13
|
+
Running StepE
|
|
14
|
+
Running StepG
|
|
15
|
+
|
|
16
|
+
</system-out>
|
|
17
|
+
</testcase>
|
|
18
|
+
<testcase classname="tests/ComplexScenario.test.ts" name="Complex Scenario Integration Tests > 2. The 'skip' propagation works as intended if something breaks up in the tree" time="0.00153012">
|
|
19
|
+
</testcase>
|
|
20
|
+
</testsuite>
|
|
21
|
+
<testsuite name="tests/TaskRunner.test.ts" timestamp="2026-01-18T03:50:46.199Z" hostname="runnervmmtnos" tests="10" failures="0" errors="0" skipped="0" time="0.109323687">
|
|
22
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should run tasks in the correct sequential order" time="0.002455516">
|
|
23
|
+
</testcase>
|
|
24
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should handle an empty list of tasks gracefully" time="0.000305152">
|
|
25
|
+
</testcase>
|
|
26
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should run independent tasks in parallel" time="0.100731975">
|
|
27
|
+
</testcase>
|
|
28
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should skip dependent tasks if a root task fails" time="0.00046882">
|
|
29
|
+
</testcase>
|
|
30
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should throw an error for 'circular dependency'" time="0.00185594">
|
|
31
|
+
</testcase>
|
|
32
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should throw an error for 'missing dependency'" time="0.000228108">
|
|
33
|
+
</testcase>
|
|
34
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should handle tasks that throw an error during execution" time="0.000243576">
|
|
35
|
+
</testcase>
|
|
36
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should skip tasks whose dependencies are skipped" time="0.000262202">
|
|
37
|
+
</testcase>
|
|
38
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should handle tasks that throw a non-Error object during execution" time="0.000268865">
|
|
39
|
+
</testcase>
|
|
40
|
+
<testcase classname="tests/TaskRunner.test.ts" name="TaskRunner > should handle duplicate steps where one gets skipped due to failed dependency" time="0.00040515">
|
|
41
|
+
</testcase>
|
|
42
|
+
</testsuite>
|
|
43
|
+
<testsuite name="tests/TaskRunnerEvents.test.ts" timestamp="2026-01-18T03:50:46.201Z" hostname="runnervmmtnos" tests="7" failures="0" errors="0" skipped="0" time="0.015876216">
|
|
44
|
+
<testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events > should fire all lifecycle events in a successful run" time="0.004415742">
|
|
45
|
+
</testcase>
|
|
46
|
+
<testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events > should fire taskSkipped event when dependency fails" time="0.005824924">
|
|
47
|
+
</testcase>
|
|
48
|
+
<testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events > should not crash if a listener throws an error" time="0.001840071">
|
|
49
|
+
</testcase>
|
|
50
|
+
<testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events > should fire workflow events even for empty step list" time="0.000423804">
|
|
51
|
+
</testcase>
|
|
52
|
+
<testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events > should handle unsubscribe correctly" time="0.000460765">
|
|
53
|
+
</testcase>
|
|
54
|
+
<testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events > should safely handle off() when no listeners exist" time="0.000210355">
|
|
55
|
+
</testcase>
|
|
56
|
+
<testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events > should support multiple listeners for the same event" time="0.000482585">
|
|
57
|
+
</testcase>
|
|
58
|
+
</testsuite>
|
|
59
|
+
</testsuites>
|