@calmo/task-runner 1.2.0 → 1.2.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 (41) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/GEMINI.md +2 -1
  3. package/README.md +5 -0
  4. package/coverage/coverage-final.json +5 -2
  5. package/coverage/index.html +21 -21
  6. package/coverage/lcov-report/index.html +21 -21
  7. package/coverage/lcov-report/src/EventBus.ts.html +337 -0
  8. package/coverage/{TaskGraphValidator.ts.html → lcov-report/src/TaskGraphValidator.ts.html} +10 -10
  9. package/coverage/lcov-report/src/TaskRunner.ts.html +409 -0
  10. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +400 -0
  11. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +217 -0
  12. package/coverage/lcov-report/src/contracts/index.html +116 -0
  13. package/coverage/lcov-report/src/index.html +161 -0
  14. package/coverage/lcov.info +189 -120
  15. package/coverage/src/EventBus.ts.html +337 -0
  16. package/coverage/{lcov-report → src}/TaskGraphValidator.ts.html +10 -10
  17. package/coverage/src/TaskRunner.ts.html +409 -0
  18. package/coverage/src/WorkflowExecutor.ts.html +400 -0
  19. package/coverage/src/contracts/RunnerEvents.ts.html +217 -0
  20. package/coverage/src/contracts/index.html +116 -0
  21. package/coverage/src/index.html +161 -0
  22. package/dist/EventBus.d.ts +26 -0
  23. package/dist/EventBus.js +56 -0
  24. package/dist/EventBus.js.map +1 -0
  25. package/dist/TaskRunner.d.ts +3 -36
  26. package/dist/TaskRunner.js +7 -74
  27. package/dist/TaskRunner.js.map +1 -1
  28. package/dist/WorkflowExecutor.d.ts +23 -0
  29. package/dist/WorkflowExecutor.js +89 -0
  30. package/dist/WorkflowExecutor.js.map +1 -0
  31. package/dist/contracts/RunnerEvents.d.ts +36 -0
  32. package/dist/contracts/RunnerEvents.js +2 -0
  33. package/dist/contracts/RunnerEvents.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/EventBus.ts +84 -0
  36. package/src/TaskRunner.ts +10 -139
  37. package/src/WorkflowExecutor.ts +105 -0
  38. package/src/contracts/RunnerEvents.ts +44 -0
  39. package/test-report.xml +41 -37
  40. package/coverage/TaskRunner.ts.html +0 -796
  41. package/coverage/lcov-report/TaskRunner.ts.html +0 -796
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Handles the execution of the workflow steps.
3
+ * @template TContext The shape of the shared context object.
4
+ */
5
+ export class WorkflowExecutor {
6
+ context;
7
+ eventBus;
8
+ running = new Set();
9
+ /**
10
+ * @param context The shared context object.
11
+ * @param eventBus The event bus to emit events.
12
+ */
13
+ constructor(context, eventBus) {
14
+ this.context = context;
15
+ this.eventBus = eventBus;
16
+ }
17
+ /**
18
+ * Executes the given steps.
19
+ * @param steps The list of steps to execute.
20
+ * @returns A Promise that resolves to a map of task results.
21
+ */
22
+ async execute(steps) {
23
+ this.eventBus.emit("workflowStart", { context: this.context, steps });
24
+ const results = new Map();
25
+ const executingPromises = new Set();
26
+ // Helper to process pending steps and launch ready ones
27
+ const processPendingSteps = () => {
28
+ const pendingSteps = steps.filter((step) => !results.has(step.name) && !this.running.has(step.name));
29
+ // 1. Identify and mark skipped tasks
30
+ for (const step of pendingSteps) {
31
+ const deps = step.dependencies ?? [];
32
+ const failedDep = deps.find((dep) => results.has(dep) && results.get(dep)?.status !== "success");
33
+ if (failedDep) {
34
+ const result = {
35
+ status: "skipped",
36
+ message: `Skipped due to failed dependency: ${failedDep}`,
37
+ };
38
+ results.set(step.name, result);
39
+ this.eventBus.emit("taskSkipped", { step, result });
40
+ }
41
+ }
42
+ // Re-filter pending steps as some might have been skipped above
43
+ const readySteps = steps.filter((step) => {
44
+ if (results.has(step.name) || this.running.has(step.name))
45
+ return false;
46
+ const deps = step.dependencies ?? [];
47
+ return deps.every((dep) => results.has(dep) && results.get(dep)?.status === "success");
48
+ });
49
+ // 2. Launch ready tasks
50
+ for (const step of readySteps) {
51
+ this.running.add(step.name);
52
+ this.eventBus.emit("taskStart", { step });
53
+ const taskPromise = (async () => {
54
+ try {
55
+ const result = await step.run(this.context);
56
+ results.set(step.name, result);
57
+ }
58
+ catch (e) {
59
+ results.set(step.name, {
60
+ status: "failure",
61
+ error: e instanceof Error ? e.message : String(e),
62
+ });
63
+ }
64
+ finally {
65
+ this.running.delete(step.name);
66
+ const result = results.get(step.name);
67
+ this.eventBus.emit("taskEnd", { step, result });
68
+ }
69
+ })();
70
+ // Wrap the task promise to ensure we can track it in the Set
71
+ const trackedPromise = taskPromise.then(() => {
72
+ executingPromises.delete(trackedPromise);
73
+ });
74
+ executingPromises.add(trackedPromise);
75
+ }
76
+ };
77
+ // Initial check to start independent tasks
78
+ processPendingSteps();
79
+ while (results.size < steps.length && executingPromises.size > 0) {
80
+ // Wait for the next task to finish
81
+ await Promise.race(executingPromises);
82
+ // After a task finishes, check for new work
83
+ processPendingSteps();
84
+ }
85
+ this.eventBus.emit("workflowEnd", { context: this.context, results });
86
+ return results;
87
+ }
88
+ }
89
+ //# sourceMappingURL=WorkflowExecutor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkflowExecutor.js","sourceRoot":"","sources":["../src/WorkflowExecutor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { TaskStep } from "../TaskStep.js";
2
+ import { TaskResult } from "../TaskResult.js";
3
+ /**
4
+ * Define the payload for every possible event in the lifecycle.
5
+ */
6
+ export interface RunnerEventPayloads<TContext> {
7
+ workflowStart: {
8
+ context: TContext;
9
+ steps: TaskStep<TContext>[];
10
+ };
11
+ workflowEnd: {
12
+ context: TContext;
13
+ results: Map<string, TaskResult>;
14
+ };
15
+ taskStart: {
16
+ step: TaskStep<TContext>;
17
+ };
18
+ taskEnd: {
19
+ step: TaskStep<TContext>;
20
+ result: TaskResult;
21
+ };
22
+ taskSkipped: {
23
+ step: TaskStep<TContext>;
24
+ result: TaskResult;
25
+ };
26
+ }
27
+ /**
28
+ * A generic listener type that maps the event key to its specific payload.
29
+ */
30
+ export type RunnerEventListener<TContext, K extends keyof RunnerEventPayloads<TContext>> = (data: RunnerEventPayloads<TContext>[K]) => void | Promise<void>;
31
+ /**
32
+ * Helper type for the listeners map.
33
+ */
34
+ export type ListenerMap<TContext> = {
35
+ [K in keyof RunnerEventPayloads<TContext>]?: Set<RunnerEventListener<TContext, K>>;
36
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=RunnerEvents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RunnerEvents.js","sourceRoot":"","sources":["../../src/contracts/RunnerEvents.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calmo/task-runner",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,84 @@
1
+ import {
2
+ ListenerMap,
3
+ RunnerEventListener,
4
+ RunnerEventPayloads,
5
+ } from "./contracts/RunnerEvents.js";
6
+
7
+ /**
8
+ * Manages event subscriptions and emissions for the TaskRunner.
9
+ * @template TContext The shape of the shared context object.
10
+ */
11
+ export class EventBus<TContext> {
12
+ private listeners: ListenerMap<TContext> = {};
13
+
14
+ /**
15
+ * Subscribe to an event.
16
+ * @param event The event name.
17
+ * @param callback The callback to execute when the event is emitted.
18
+ */
19
+ public on<K extends keyof RunnerEventPayloads<TContext>>(
20
+ event: K,
21
+ callback: RunnerEventListener<TContext, K>
22
+ ): void {
23
+ if (!this.listeners[event]) {
24
+ // Type assertion needed because TypeScript cannot verify that the generic K
25
+ // matches the specific key in the mapped type during assignment.
26
+ this.listeners[event] = new Set() as unknown as ListenerMap<TContext>[K];
27
+ }
28
+ // Type assertion needed to tell TS that this specific Set matches the callback type
29
+ (this.listeners[event] as Set<RunnerEventListener<TContext, K>>).add(
30
+ callback
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Unsubscribe from an event.
36
+ * @param event The event name.
37
+ * @param callback The callback to remove.
38
+ */
39
+ public off<K extends keyof RunnerEventPayloads<TContext>>(
40
+ event: K,
41
+ callback: RunnerEventListener<TContext, K>
42
+ ): void {
43
+ if (this.listeners[event]) {
44
+ (this.listeners[event] as Set<RunnerEventListener<TContext, K>>).delete(
45
+ callback
46
+ );
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Emit an event to all subscribers.
52
+ * @param event The event name.
53
+ * @param data The payload for the event.
54
+ */
55
+ public emit<K extends keyof RunnerEventPayloads<TContext>>(
56
+ event: K,
57
+ data: RunnerEventPayloads<TContext>[K]
58
+ ): void {
59
+ const listeners = this.listeners[event] as
60
+ | Set<RunnerEventListener<TContext, K>>
61
+ | undefined;
62
+ if (listeners) {
63
+ for (const listener of listeners) {
64
+ try {
65
+ const result = listener(data);
66
+ if (result instanceof Promise) {
67
+ result.catch((error) => {
68
+ console.error(
69
+ `Error in event listener for ${String(event)}:`,
70
+ error
71
+ );
72
+ });
73
+ }
74
+ } catch (error) {
75
+ // Prevent listener errors from bubbling up
76
+ console.error(
77
+ `Error in event listener for ${String(event)}:`,
78
+ error
79
+ );
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
package/src/TaskRunner.ts CHANGED
@@ -2,48 +2,12 @@ import { TaskStep } from "./TaskStep.js";
2
2
  import { TaskResult } from "./TaskResult.js";
3
3
  import { TaskGraphValidator } from "./TaskGraphValidator.js";
4
4
  import { TaskGraph } from "./TaskGraph.js";
5
+ import { RunnerEventPayloads, RunnerEventListener } from "./contracts/RunnerEvents.js";
6
+ import { EventBus } from "./EventBus.js";
7
+ import { WorkflowExecutor } from "./WorkflowExecutor.js";
5
8
 
6
- /**
7
- * Define the payload for every possible event in the lifecycle.
8
- */
9
- export interface RunnerEventPayloads<TContext> {
10
- workflowStart: {
11
- context: TContext;
12
- steps: TaskStep<TContext>[];
13
- };
14
- workflowEnd: {
15
- context: TContext;
16
- results: Map<string, TaskResult>;
17
- };
18
- taskStart: {
19
- step: TaskStep<TContext>;
20
- };
21
- taskEnd: {
22
- step: TaskStep<TContext>;
23
- result: TaskResult;
24
- };
25
- taskSkipped: {
26
- step: TaskStep<TContext>;
27
- result: TaskResult;
28
- };
29
- }
30
-
31
- /**
32
- * A generic listener type that maps the event key to its specific payload.
33
- */
34
- export type RunnerEventListener<
35
- TContext,
36
- K extends keyof RunnerEventPayloads<TContext>,
37
- > = (data: RunnerEventPayloads<TContext>[K]) => void | Promise<void>;
38
-
39
- /**
40
- * Helper type for the listeners map to avoid private access issues in generic contexts.
41
- */
42
- type ListenerMap<TContext> = {
43
- [K in keyof RunnerEventPayloads<TContext>]?: Set<
44
- RunnerEventListener<TContext, K>
45
- >;
46
- };
9
+ // Re-export types for backward compatibility
10
+ export { RunnerEventPayloads, RunnerEventListener };
47
11
 
48
12
  /**
49
13
  * The main class that orchestrates the execution of a list of tasks
@@ -51,8 +15,7 @@ type ListenerMap<TContext> = {
51
15
  * @template TContext The shape of the shared context object.
52
16
  */
53
17
  export class TaskRunner<TContext> {
54
- private running = new Set<string>();
55
- private listeners: ListenerMap<TContext> = {};
18
+ private eventBus = new EventBus<TContext>();
56
19
  private validator = new TaskGraphValidator();
57
20
 
58
21
  /**
@@ -69,15 +32,7 @@ export class TaskRunner<TContext> {
69
32
  event: K,
70
33
  callback: RunnerEventListener<TContext, K>
71
34
  ): void {
72
- if (!this.listeners[event]) {
73
- // Type assertion needed because TypeScript cannot verify that the generic K
74
- // matches the specific key in the mapped type during assignment.
75
- this.listeners[event] = new Set() as unknown as ListenerMap<TContext>[K];
76
- }
77
- // Type assertion needed to tell TS that this specific Set matches the callback type
78
- (this.listeners[event] as Set<RunnerEventListener<TContext, K>>).add(
79
- callback
80
- );
35
+ this.eventBus.on(event, callback);
81
36
  }
82
37
 
83
38
  /**
@@ -89,38 +44,7 @@ export class TaskRunner<TContext> {
89
44
  event: K,
90
45
  callback: RunnerEventListener<TContext, K>
91
46
  ): void {
92
- if (this.listeners[event]) {
93
- (this.listeners[event] as Set<RunnerEventListener<TContext, K>>).delete(
94
- callback
95
- );
96
- }
97
- }
98
-
99
- /**
100
- * Emit an event to all subscribers.
101
- * @param event The event name.
102
- * @param data The payload for the event.
103
- */
104
- private emit<K extends keyof RunnerEventPayloads<TContext>>(
105
- event: K,
106
- data: RunnerEventPayloads<TContext>[K]
107
- ): void {
108
- const listeners = this.listeners[event] as
109
- | Set<RunnerEventListener<TContext, K>>
110
- | undefined;
111
- if (listeners) {
112
- for (const listener of listeners) {
113
- try {
114
- listener(data);
115
- } catch (error) {
116
- // Prevent listener errors from bubbling up
117
- console.error(
118
- `Error in event listener for ${String(event)}:`,
119
- error
120
- );
121
- }
122
- }
123
- }
47
+ this.eventBus.off(event, callback);
124
48
  }
125
49
 
126
50
  /**
@@ -178,60 +102,7 @@ export class TaskRunner<TContext> {
178
102
  throw new Error(`${legacyMessage} | ${detailedMessage}`);
179
103
  }
180
104
 
181
- this.emit("workflowStart", { context: this.context, steps });
182
-
183
- const results = new Map<string, TaskResult>();
184
-
185
- while (results.size < steps.length) {
186
- const pendingSteps = steps.filter(
187
- (step) => !results.has(step.name) && !this.running.has(step.name)
188
- );
189
-
190
- const readySteps = pendingSteps.filter((step) => {
191
- const deps = step.dependencies ?? [];
192
- return deps.every(
193
- (dep) => results.has(dep) && results.get(dep)?.status === "success"
194
- );
195
- });
196
-
197
- // Skip tasks with failed dependencies
198
- for (const step of pendingSteps) {
199
- const deps = step.dependencies ?? [];
200
- const failedDep = deps.find(
201
- (dep) => results.has(dep) && results.get(dep)?.status !== "success"
202
- );
203
- if (failedDep) {
204
- const result: TaskResult = {
205
- status: "skipped",
206
- message: `Skipped due to failed dependency: ${failedDep}`,
207
- };
208
- results.set(step.name, result);
209
- this.emit("taskSkipped", { step, result });
210
- }
211
- }
212
-
213
- await Promise.all(
214
- readySteps.map(async (step) => {
215
- this.running.add(step.name);
216
- this.emit("taskStart", { step });
217
- try {
218
- const result = await step.run(this.context);
219
- results.set(step.name, result);
220
- } catch (e) {
221
- results.set(step.name, {
222
- status: "failure",
223
- error: e instanceof Error ? e.message : String(e),
224
- });
225
- } finally {
226
- this.running.delete(step.name);
227
- const result = results.get(step.name)!;
228
- this.emit("taskEnd", { step, result });
229
- }
230
- })
231
- );
232
- }
233
-
234
- this.emit("workflowEnd", { context: this.context, results });
235
- return results;
105
+ const executor = new WorkflowExecutor(this.context, this.eventBus);
106
+ return executor.execute(steps);
236
107
  }
237
108
  }
@@ -0,0 +1,105 @@
1
+ import { TaskStep } from "./TaskStep.js";
2
+ import { TaskResult } from "./TaskResult.js";
3
+ import { EventBus } from "./EventBus.js";
4
+
5
+ /**
6
+ * Handles the execution of the workflow steps.
7
+ * @template TContext The shape of the shared context object.
8
+ */
9
+ export class WorkflowExecutor<TContext> {
10
+ private running = new Set<string>();
11
+
12
+ /**
13
+ * @param context The shared context object.
14
+ * @param eventBus The event bus to emit events.
15
+ */
16
+ constructor(
17
+ private context: TContext,
18
+ private eventBus: EventBus<TContext>
19
+ ) {}
20
+
21
+ /**
22
+ * Executes the given steps.
23
+ * @param steps The list of steps to execute.
24
+ * @returns A Promise that resolves to a map of task results.
25
+ */
26
+ async execute(steps: TaskStep<TContext>[]): Promise<Map<string, TaskResult>> {
27
+ this.eventBus.emit("workflowStart", { context: this.context, steps });
28
+
29
+ const results = new Map<string, TaskResult>();
30
+ const executingPromises = new Set<Promise<void>>();
31
+
32
+ // Helper to process pending steps and launch ready ones
33
+ const processPendingSteps = () => {
34
+ const pendingSteps = steps.filter(
35
+ (step) => !results.has(step.name) && !this.running.has(step.name)
36
+ );
37
+
38
+ // 1. Identify and mark skipped tasks
39
+ for (const step of pendingSteps) {
40
+ const deps = step.dependencies ?? [];
41
+ const failedDep = deps.find(
42
+ (dep) => results.has(dep) && results.get(dep)?.status !== "success"
43
+ );
44
+ if (failedDep) {
45
+ const result: TaskResult = {
46
+ status: "skipped",
47
+ message: `Skipped due to failed dependency: ${failedDep}`,
48
+ };
49
+ results.set(step.name, result);
50
+ this.eventBus.emit("taskSkipped", { step, result });
51
+ }
52
+ }
53
+
54
+ // Re-filter pending steps as some might have been skipped above
55
+ const readySteps = steps.filter((step) => {
56
+ if (results.has(step.name) || this.running.has(step.name)) return false;
57
+ const deps = step.dependencies ?? [];
58
+ return deps.every(
59
+ (dep) => results.has(dep) && results.get(dep)?.status === "success"
60
+ );
61
+ });
62
+
63
+ // 2. Launch ready tasks
64
+ for (const step of readySteps) {
65
+ this.running.add(step.name);
66
+ this.eventBus.emit("taskStart", { step });
67
+
68
+ const taskPromise = (async () => {
69
+ try {
70
+ const result = await step.run(this.context);
71
+ results.set(step.name, result);
72
+ } catch (e) {
73
+ results.set(step.name, {
74
+ status: "failure",
75
+ error: e instanceof Error ? e.message : String(e),
76
+ });
77
+ } finally {
78
+ this.running.delete(step.name);
79
+ const result = results.get(step.name)!;
80
+ this.eventBus.emit("taskEnd", { step, result });
81
+ }
82
+ })();
83
+
84
+ // Wrap the task promise to ensure we can track it in the Set
85
+ const trackedPromise = taskPromise.then(() => {
86
+ executingPromises.delete(trackedPromise);
87
+ });
88
+ executingPromises.add(trackedPromise);
89
+ }
90
+ };
91
+
92
+ // Initial check to start independent tasks
93
+ processPendingSteps();
94
+
95
+ while (results.size < steps.length && executingPromises.size > 0) {
96
+ // Wait for the next task to finish
97
+ await Promise.race(executingPromises);
98
+ // After a task finishes, check for new work
99
+ processPendingSteps();
100
+ }
101
+
102
+ this.eventBus.emit("workflowEnd", { context: this.context, results });
103
+ return results;
104
+ }
105
+ }
@@ -0,0 +1,44 @@
1
+ import { TaskStep } from "../TaskStep.js";
2
+ import { TaskResult } from "../TaskResult.js";
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.
39
+ */
40
+ export type ListenerMap<TContext> = {
41
+ [K in keyof RunnerEventPayloads<TContext>]?: Set<
42
+ RunnerEventListener<TContext, K>
43
+ >;
44
+ };