builderman 1.3.0 → 1.5.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/README.md +152 -36
- package/dist/errors.d.ts +12 -0
- package/dist/{pipeline-error.js → errors.js} +12 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/constants.d.ts +2 -0
- package/dist/internal/constants.js +2 -0
- package/dist/internal/execution-context.d.ts +33 -0
- package/dist/internal/execution-context.js +30 -0
- package/dist/{graph.d.ts → internal/graph.d.ts} +1 -1
- package/dist/internal/queue-manager.d.ts +24 -0
- package/dist/internal/queue-manager.js +182 -0
- package/dist/{modules → internal}/signal-handler.js +5 -2
- package/dist/internal/task-executor.d.ts +33 -0
- package/dist/{modules → internal}/task-executor.js +141 -89
- package/dist/internal/timeout-manager.d.ts +34 -0
- package/dist/internal/timeout-manager.js +85 -0
- package/dist/{util.d.ts → internal/util.d.ts} +1 -1
- package/dist/pipeline.js +152 -97
- package/dist/task.js +30 -12
- package/dist/types.d.ts +45 -3
- package/package.json +2 -2
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -1
- package/dist/modules/task-executor.d.ts +0 -25
- package/dist/pipeline-error.d.ts +0 -11
- package/dist/scheduler.d.ts +0 -20
- package/dist/scheduler.js +0 -40
- /package/dist/{graph.js → internal/graph.js} +0 -0
- /package/dist/{modules → internal}/signal-handler.d.ts +0 -0
- /package/dist/{modules → internal}/teardown-manager.d.ts +0 -0
- /package/dist/{modules → internal}/teardown-manager.js +0 -0
- /package/dist/{util.js → internal/util.js} +0 -0
package/README.md
CHANGED
|
@@ -10,28 +10,30 @@ It is designed for monorepos, long-running development processes, and CI/CD pipe
|
|
|
10
10
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
|
|
13
|
-
- [Key Features](#key-features)
|
|
14
|
-
- [Installation](#installation)
|
|
15
|
-
- [Quick Start](#quick-start)
|
|
16
|
-
- [Core Concepts](#core-concepts)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- [
|
|
13
|
+
> - [Key Features](#key-features)
|
|
14
|
+
> - [Installation](#installation)
|
|
15
|
+
> - [Quick Start](#quick-start)
|
|
16
|
+
> - [Core Concepts](#core-concepts)
|
|
17
|
+
> - [Tasks](#tasks)
|
|
18
|
+
> - [Commands & Modes](#commands--modes)
|
|
19
|
+
> - [Environment Variables](#environment-variables)
|
|
20
|
+
> - [Dependencies](#dependencies)
|
|
21
|
+
> - [Pipelines](#pipelines)
|
|
22
|
+
> - [Concurrency Control](#concurrency-control)
|
|
23
|
+
> - [Pipeline Composition](#pipeline-composition)
|
|
24
|
+
> - [Error Handling Guarantees](#error-handling-guarantees)
|
|
25
|
+
> - [Cancellation](#cancellation)
|
|
26
|
+
> - [Teardown](#teardown)
|
|
27
|
+
> - [Basic Teardown](#basic-teardown)
|
|
28
|
+
> - [Teardown Callbacks](#teardown-callbacks)
|
|
29
|
+
> - [Teardown Execution Rules](#teardown-execution-rules)
|
|
30
|
+
> - [Skipping Tasks](#skipping-tasks)
|
|
31
|
+
> - [Strict Mode](#strict-mode)
|
|
32
|
+
> - [Task-Level Skip Override](#task-level-skip-override)
|
|
33
|
+
> - [Execution Statistics](#execution-statistics)
|
|
34
|
+
> - [Pipeline Statistics](#pipeline-statistics)
|
|
35
|
+
> - [Task Statistics](#task-statistics)
|
|
36
|
+
> - [When Should I Use builderman?](#when-should-i-use-builderman)
|
|
35
37
|
|
|
36
38
|
## Key Features
|
|
37
39
|
|
|
@@ -62,17 +64,19 @@ import { task, pipeline } from "builderman"
|
|
|
62
64
|
const build = task({
|
|
63
65
|
name: "build",
|
|
64
66
|
commands: { build: "tsc" },
|
|
65
|
-
cwd: "."
|
|
67
|
+
cwd: "packages/my-package", // Optional: defaults to "."
|
|
66
68
|
})
|
|
67
69
|
|
|
68
70
|
const test = task({
|
|
69
71
|
name: "test",
|
|
70
72
|
commands: { build: "npm test" },
|
|
71
|
-
cwd: ".",
|
|
72
73
|
dependencies: [build],
|
|
74
|
+
cwd: "packages/my-package",
|
|
73
75
|
})
|
|
74
76
|
|
|
75
|
-
const result = await pipeline([build, test]).run(
|
|
77
|
+
const result = await pipeline([build, test]).run({
|
|
78
|
+
command: "build",
|
|
79
|
+
})
|
|
76
80
|
|
|
77
81
|
if (!result.ok) {
|
|
78
82
|
console.error("Pipeline failed:", result.error.message)
|
|
@@ -93,6 +97,7 @@ A **task** represents a unit of work. Each task:
|
|
|
93
97
|
- Defines commands for one or more modes
|
|
94
98
|
- May depend on other tasks
|
|
95
99
|
- May register teardown logic
|
|
100
|
+
- Has an optional working directory (`cwd`, defaults to `"."`)
|
|
96
101
|
|
|
97
102
|
```ts
|
|
98
103
|
import { task } from "builderman"
|
|
@@ -130,6 +135,89 @@ Commands may be:
|
|
|
130
135
|
- `run`: the command to execute
|
|
131
136
|
- `readyWhen`: a predicate that marks the task as ready
|
|
132
137
|
- `teardown`: cleanup logic to run after completion
|
|
138
|
+
- `env`: environment variables specific to this command
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Environment Variables
|
|
143
|
+
|
|
144
|
+
Environment variables can be provided at multiple levels, with more specific levels overriding less specific ones:
|
|
145
|
+
|
|
146
|
+
**Precedence order (highest to lowest):**
|
|
147
|
+
|
|
148
|
+
1. Command-level `env` (in command config)
|
|
149
|
+
2. Task-level `env` (in task config)
|
|
150
|
+
3. Pipeline-level `env` (in `pipeline.run()`)
|
|
151
|
+
4. Process environment variables
|
|
152
|
+
|
|
153
|
+
#### Command-Level Environment Variables
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const apiTask = task({
|
|
157
|
+
name: "api",
|
|
158
|
+
commands: {
|
|
159
|
+
dev: {
|
|
160
|
+
run: "npm run dev",
|
|
161
|
+
env: {
|
|
162
|
+
PORT: "3000",
|
|
163
|
+
NODE_ENV: "development",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Task-Level Environment Variables
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const apiTask = task({
|
|
174
|
+
name: "api",
|
|
175
|
+
commands: {
|
|
176
|
+
dev: "npm run dev",
|
|
177
|
+
build: "npm run build",
|
|
178
|
+
},
|
|
179
|
+
env: {
|
|
180
|
+
API_URL: "http://localhost:3000",
|
|
181
|
+
LOG_LEVEL: "debug",
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Pipeline-Level Environment Variables
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const result = await pipeline([apiTask]).run({
|
|
190
|
+
env: {
|
|
191
|
+
DATABASE_URL: "postgres://localhost/mydb",
|
|
192
|
+
REDIS_URL: "redis://localhost:6379",
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Nested Pipeline Environment Variables
|
|
198
|
+
|
|
199
|
+
When converting a pipeline to a task, you can provide environment variables that will be merged with the outer pipeline's environment:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const innerPipeline = pipeline([
|
|
203
|
+
/* ... */
|
|
204
|
+
])
|
|
205
|
+
const innerTask = innerPipeline.toTask({
|
|
206
|
+
name: "inner",
|
|
207
|
+
env: {
|
|
208
|
+
INNER_VAR: "inner-value",
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const outerPipeline = pipeline([innerTask])
|
|
213
|
+
const result = await outerPipeline.run({
|
|
214
|
+
env: {
|
|
215
|
+
OUTER_VAR: "outer-value",
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
In this example, tasks in `innerPipeline` will receive both `INNER_VAR` and `OUTER_VAR`, with `INNER_VAR` taking precedence if there's a conflict.
|
|
133
221
|
|
|
134
222
|
---
|
|
135
223
|
|
|
@@ -169,6 +257,31 @@ const result = await pipeline([libTask, consumerTask]).run({
|
|
|
169
257
|
})
|
|
170
258
|
```
|
|
171
259
|
|
|
260
|
+
#### Concurrency Control
|
|
261
|
+
|
|
262
|
+
By default, pipelines run as many tasks concurrently as possible (limited only by dependencies). You can limit concurrent execution using `maxConcurrency`:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const result = await pipeline([task1, task2, task3, task4, task5]).run({
|
|
266
|
+
maxConcurrency: 2, // At most 2 tasks will run simultaneously
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
When `maxConcurrency` is set:
|
|
271
|
+
|
|
272
|
+
- Tasks that are ready to run (dependencies satisfied) will start up to the limit
|
|
273
|
+
- As tasks complete, new ready tasks will start to maintain the concurrency limit
|
|
274
|
+
- Dependencies are still respected — a task won't start until its dependencies complete
|
|
275
|
+
|
|
276
|
+
This is useful for:
|
|
277
|
+
|
|
278
|
+
- Limiting resource usage (CPU, memory, network)
|
|
279
|
+
- Controlling database connection pools
|
|
280
|
+
- Managing API rate limits
|
|
281
|
+
- Reducing system load in CI environments
|
|
282
|
+
|
|
283
|
+
If `maxConcurrency` is not specified, there is no limit (tasks run concurrently as dependencies allow).
|
|
284
|
+
|
|
172
285
|
---
|
|
173
286
|
|
|
174
287
|
### Pipeline Composition
|
|
@@ -187,7 +300,11 @@ const deploy = pipeline([
|
|
|
187
300
|
])
|
|
188
301
|
|
|
189
302
|
const buildTask = build.toTask({ name: "build" })
|
|
190
|
-
const testTask = test.toTask({
|
|
303
|
+
const testTask = test.toTask({
|
|
304
|
+
name: "test",
|
|
305
|
+
dependencies: [buildTask],
|
|
306
|
+
env: { TEST_ENV: "test-value" }, // Optional: env for nested pipeline
|
|
307
|
+
})
|
|
191
308
|
const deployTask = deploy.toTask({ name: "deploy", dependencies: [testTask] })
|
|
192
309
|
|
|
193
310
|
const ci = pipeline([buildTask, testTask, deployTask])
|
|
@@ -215,16 +332,19 @@ if (!result.ok) {
|
|
|
215
332
|
console.error("Pipeline was cancelled")
|
|
216
333
|
break
|
|
217
334
|
case PipelineError.TaskFailed:
|
|
218
|
-
console.error(
|
|
335
|
+
console.error("Task failed:", result.error.message)
|
|
336
|
+
break
|
|
337
|
+
case PipelineError.TaskReadyTimeout:
|
|
338
|
+
console.error("Task was not ready in time:", result.error.message)
|
|
339
|
+
break
|
|
340
|
+
case PipelineError.TaskCompletedTimeout:
|
|
341
|
+
console.error("Task did not complete in time:", result.error.message)
|
|
219
342
|
break
|
|
220
343
|
case PipelineError.ProcessTerminated:
|
|
221
|
-
console.error("Process
|
|
344
|
+
console.error("Process terminated:", result.error.message)
|
|
222
345
|
break
|
|
223
346
|
case PipelineError.InvalidTask:
|
|
224
|
-
console.error(
|
|
225
|
-
break
|
|
226
|
-
case PipelineError.InvalidSignal:
|
|
227
|
-
console.error("Invalid abort signal")
|
|
347
|
+
console.error("Invalid task configuration:", result.error.message)
|
|
228
348
|
break
|
|
229
349
|
}
|
|
230
350
|
}
|
|
@@ -278,7 +398,6 @@ const dbTask = task({
|
|
|
278
398
|
},
|
|
279
399
|
build: "echo build",
|
|
280
400
|
},
|
|
281
|
-
cwd: ".",
|
|
282
401
|
})
|
|
283
402
|
```
|
|
284
403
|
|
|
@@ -336,7 +455,6 @@ const dbTask = task({
|
|
|
336
455
|
commands: {
|
|
337
456
|
dev: "docker-compose up",
|
|
338
457
|
},
|
|
339
|
-
cwd: ".",
|
|
340
458
|
})
|
|
341
459
|
|
|
342
460
|
const apiTask = task({
|
|
@@ -345,7 +463,6 @@ const apiTask = task({
|
|
|
345
463
|
dev: "npm run dev",
|
|
346
464
|
build: "npm run build",
|
|
347
465
|
},
|
|
348
|
-
cwd: ".",
|
|
349
466
|
dependencies: [dbTask],
|
|
350
467
|
})
|
|
351
468
|
|
|
@@ -386,7 +503,6 @@ const dbTask = task({
|
|
|
386
503
|
commands: {
|
|
387
504
|
dev: "docker-compose up",
|
|
388
505
|
},
|
|
389
|
-
cwd: ".",
|
|
390
506
|
allowSkip: true,
|
|
391
507
|
})
|
|
392
508
|
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type PipelineErrorCode = typeof PipelineError.Aborted | typeof PipelineError.ProcessTerminated | typeof PipelineError.TaskFailed | typeof PipelineError.TaskCompletedTimeout | typeof PipelineError.TaskReadyTimeout | typeof PipelineError.InvalidTask;
|
|
2
|
+
export declare class PipelineError extends Error {
|
|
3
|
+
readonly code: PipelineErrorCode;
|
|
4
|
+
readonly taskName?: string;
|
|
5
|
+
constructor(message: string, code: PipelineErrorCode, taskName?: string);
|
|
6
|
+
static Aborted: "aborted";
|
|
7
|
+
static ProcessTerminated: "process-terminated";
|
|
8
|
+
static TaskFailed: "task-failed";
|
|
9
|
+
static TaskReadyTimeout: "task-ready-timeout";
|
|
10
|
+
static TaskCompletedTimeout: "task-completed-timeout";
|
|
11
|
+
static InvalidTask: "invalid-task";
|
|
12
|
+
}
|
|
@@ -22,29 +22,35 @@ Object.defineProperty(PipelineError, "Aborted", {
|
|
|
22
22
|
enumerable: true,
|
|
23
23
|
configurable: true,
|
|
24
24
|
writable: true,
|
|
25
|
-
value:
|
|
25
|
+
value: "aborted"
|
|
26
26
|
});
|
|
27
27
|
Object.defineProperty(PipelineError, "ProcessTerminated", {
|
|
28
28
|
enumerable: true,
|
|
29
29
|
configurable: true,
|
|
30
30
|
writable: true,
|
|
31
|
-
value:
|
|
31
|
+
value: "process-terminated"
|
|
32
32
|
});
|
|
33
33
|
Object.defineProperty(PipelineError, "TaskFailed", {
|
|
34
34
|
enumerable: true,
|
|
35
35
|
configurable: true,
|
|
36
36
|
writable: true,
|
|
37
|
-
value:
|
|
37
|
+
value: "task-failed"
|
|
38
38
|
});
|
|
39
|
-
Object.defineProperty(PipelineError, "
|
|
39
|
+
Object.defineProperty(PipelineError, "TaskReadyTimeout", {
|
|
40
40
|
enumerable: true,
|
|
41
41
|
configurable: true,
|
|
42
42
|
writable: true,
|
|
43
|
-
value:
|
|
43
|
+
value: "task-ready-timeout"
|
|
44
|
+
});
|
|
45
|
+
Object.defineProperty(PipelineError, "TaskCompletedTimeout", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
value: "task-completed-timeout"
|
|
44
50
|
});
|
|
45
51
|
Object.defineProperty(PipelineError, "InvalidTask", {
|
|
46
52
|
enumerable: true,
|
|
47
53
|
configurable: true,
|
|
48
54
|
writable: true,
|
|
49
|
-
value:
|
|
55
|
+
value: "invalid-task"
|
|
50
56
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { task } from "./task.js";
|
|
2
2
|
export { pipeline } from "./pipeline.js";
|
|
3
|
-
export { PipelineError, type PipelineErrorCode } from "./
|
|
3
|
+
export { PipelineError, type PipelineErrorCode } from "./errors.js";
|
|
4
4
|
export type { Task, Pipeline, TaskConfig, Command, CommandConfig, Commands, PipelineRunConfig, PipelineTaskConfig, RunResult, PipelineStats, TaskStats, TaskStatus, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PipelineRunConfig, TaskStats } from "../types.js";
|
|
2
|
+
import type { TeardownManager } from "./teardown-manager.js";
|
|
3
|
+
import type { TimeoutManager } from "./timeout-manager.js";
|
|
4
|
+
import type { QueueManager } from "./queue-manager.js";
|
|
5
|
+
/**
|
|
6
|
+
* Represents a task execution in progress
|
|
7
|
+
*/
|
|
8
|
+
export interface TaskExecution {
|
|
9
|
+
taskId: string;
|
|
10
|
+
taskName: string;
|
|
11
|
+
process?: import("node:child_process").ChildProcess;
|
|
12
|
+
startedAt: number;
|
|
13
|
+
readyAt?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Centralized execution context that replaces scattered configuration parameters
|
|
17
|
+
* and provides unified access to execution state and helper methods
|
|
18
|
+
*/
|
|
19
|
+
export interface ExecutionContext {
|
|
20
|
+
config: PipelineRunConfig;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
spawn: typeof import("node:child_process").spawn;
|
|
23
|
+
teardownManager: TeardownManager;
|
|
24
|
+
timeoutManager: TimeoutManager;
|
|
25
|
+
queueManager: QueueManager;
|
|
26
|
+
taskStats: Map<string, TaskStats>;
|
|
27
|
+
updateTaskStatus: (taskId: string, updates: Partial<TaskStats>) => void;
|
|
28
|
+
isAborted: () => boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates an execution context for pipeline execution
|
|
32
|
+
*/
|
|
33
|
+
export declare function createExecutionContext(config: PipelineRunConfig, teardownManager: TeardownManager, timeoutManager: TimeoutManager, queueManager: QueueManager, taskStats: Map<string, TaskStats>): ExecutionContext;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Creates an execution context for pipeline execution
|
|
4
|
+
*/
|
|
5
|
+
export function createExecutionContext(config, teardownManager, timeoutManager, queueManager, taskStats) {
|
|
6
|
+
// Use dynamic import only if spawn is not provided
|
|
7
|
+
const spawnFn = config.spawn ?? spawn;
|
|
8
|
+
return {
|
|
9
|
+
config,
|
|
10
|
+
signal: config.signal,
|
|
11
|
+
spawn: spawnFn,
|
|
12
|
+
teardownManager,
|
|
13
|
+
timeoutManager,
|
|
14
|
+
queueManager,
|
|
15
|
+
taskStats,
|
|
16
|
+
updateTaskStatus(taskId, updates) {
|
|
17
|
+
const currentStats = taskStats.get(taskId);
|
|
18
|
+
const updatedStats = { ...currentStats, ...updates };
|
|
19
|
+
// Calculate duration if both start and finish times are available
|
|
20
|
+
if (updatedStats.startedAt && updatedStats.finishedAt) {
|
|
21
|
+
updatedStats.durationMs =
|
|
22
|
+
updatedStats.finishedAt - updatedStats.startedAt;
|
|
23
|
+
}
|
|
24
|
+
taskStats.set(taskId, updatedStats);
|
|
25
|
+
},
|
|
26
|
+
isAborted() {
|
|
27
|
+
return config.signal?.aborted ?? false;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { TaskGraph, Task } from "
|
|
1
|
+
import type { TaskGraph, Task } from "../types.js";
|
|
2
2
|
export declare function createTaskGraph(tasks: Task[]): TaskGraph;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TaskGraph } from "../types.js";
|
|
2
|
+
import type { TaskExecution } from "./execution-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Queue manager interface returned by createQueueManager
|
|
5
|
+
*/
|
|
6
|
+
export interface QueueManager {
|
|
7
|
+
getNextReadyTask(): string | null;
|
|
8
|
+
markRunningTaskReady(taskId: string): void;
|
|
9
|
+
markTaskComplete(taskId: string): void;
|
|
10
|
+
markTaskFailed(taskId: string): void;
|
|
11
|
+
markTaskSkipped(taskId: string): void;
|
|
12
|
+
markTaskRunning(taskId: string, execution: TaskExecution): void;
|
|
13
|
+
canExecuteMore(): boolean;
|
|
14
|
+
isComplete(): boolean;
|
|
15
|
+
hasFailed(): boolean;
|
|
16
|
+
getRunningTasks(): Map<string, TaskExecution>;
|
|
17
|
+
clearQueues(): void;
|
|
18
|
+
abortAllRunningTasks(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates a queue manager that replaces the generator-based scheduler
|
|
22
|
+
* with explicit queue-based execution state management
|
|
23
|
+
*/
|
|
24
|
+
export declare function createQueueManager(graph: TaskGraph, maxConcurrency?: number): QueueManager;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a queue manager that replaces the generator-based scheduler
|
|
3
|
+
* with explicit queue-based execution state management
|
|
4
|
+
*/
|
|
5
|
+
export function createQueueManager(graph, maxConcurrency) {
|
|
6
|
+
const readyQueue = [];
|
|
7
|
+
const waitingQueue = new Map();
|
|
8
|
+
const runningTasks = new Map();
|
|
9
|
+
const completedTasks = new Set();
|
|
10
|
+
const failedTasks = new Set();
|
|
11
|
+
const skippedTasks = new Set();
|
|
12
|
+
const maxConcurrencyLimit = maxConcurrency ?? Infinity;
|
|
13
|
+
let status = "running";
|
|
14
|
+
for (const [taskId, node] of graph.nodes) {
|
|
15
|
+
const depCount = node.dependencies.size;
|
|
16
|
+
if (depCount === 0) {
|
|
17
|
+
readyQueue.push(taskId);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
waitingQueue.set(taskId, depCount);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const updateDependentTasks = (completedTaskId) => {
|
|
24
|
+
const node = graph.nodes.get(completedTaskId);
|
|
25
|
+
if (!node)
|
|
26
|
+
return;
|
|
27
|
+
for (const dependentId of node.dependents) {
|
|
28
|
+
const currentCount = waitingQueue.get(dependentId);
|
|
29
|
+
if (currentCount === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const newCount = currentCount - 1;
|
|
32
|
+
if (newCount > 0) {
|
|
33
|
+
waitingQueue.set(dependentId, newCount);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
waitingQueue.delete(dependentId);
|
|
37
|
+
readyQueue.push(dependentId);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const allTasksFinished = () => {
|
|
41
|
+
const totalTasks = graph.nodes.size;
|
|
42
|
+
const finishedTasks = completedTasks.size + failedTasks.size + skippedTasks.size;
|
|
43
|
+
return finishedTasks === totalTasks;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Update execution status based on current queue state
|
|
47
|
+
*/
|
|
48
|
+
const updateExecutionStatus = () => {
|
|
49
|
+
if (status === "failed" || status === "aborted") {
|
|
50
|
+
return; // Don't change from terminal states
|
|
51
|
+
}
|
|
52
|
+
if (allTasksFinished()) {
|
|
53
|
+
status = "completed";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
/**
|
|
58
|
+
* Get the next ready task for execution, respecting concurrency limits
|
|
59
|
+
* Returns null if pipeline has failed or been aborted
|
|
60
|
+
*/
|
|
61
|
+
getNextReadyTask() {
|
|
62
|
+
// Don't return tasks if pipeline has failed or been aborted
|
|
63
|
+
if (status === "failed" || status === "aborted") {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (readyQueue.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
if (runningTasks.size >= maxConcurrencyLimit)
|
|
69
|
+
return null;
|
|
70
|
+
// Additional check: if we have failed tasks, don't process new ones
|
|
71
|
+
if (failedTasks.size > 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const taskId = readyQueue.shift() ?? null;
|
|
75
|
+
if (taskId) {
|
|
76
|
+
// Final check before returning - prevent race conditions
|
|
77
|
+
// Check failed tasks to prevent race conditions (status check already done above)
|
|
78
|
+
if (failedTasks.size > 0) {
|
|
79
|
+
// Put it back if we detected failure
|
|
80
|
+
readyQueue.unshift(taskId);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return taskId;
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Mark a running task as ready (via readyWhen) and update dependent tasks
|
|
88
|
+
*/
|
|
89
|
+
markRunningTaskReady(taskId) {
|
|
90
|
+
if (runningTasks.has(taskId)) {
|
|
91
|
+
updateDependentTasks(taskId);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Mark a task as complete and update dependent tasks
|
|
96
|
+
*/
|
|
97
|
+
markTaskComplete(taskId) {
|
|
98
|
+
runningTasks.delete(taskId);
|
|
99
|
+
completedTasks.add(taskId);
|
|
100
|
+
updateDependentTasks(taskId);
|
|
101
|
+
updateExecutionStatus();
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* Mark a task as failed
|
|
105
|
+
* When a task fails, we clear the ready queue immediately to prevent dependent tasks from starting.
|
|
106
|
+
* Note: This clears only the ready queue. The full cleanup (including waiting queue) happens
|
|
107
|
+
* in failPipeline via clearQueues(). This immediate ready queue clearing is critical to prevent
|
|
108
|
+
* race conditions where dependent tasks might be in the ready queue when a dependency fails.
|
|
109
|
+
* Failed tasks don't update dependents as they block the pipeline.
|
|
110
|
+
*/
|
|
111
|
+
markTaskFailed(taskId) {
|
|
112
|
+
runningTasks.delete(taskId);
|
|
113
|
+
failedTasks.add(taskId);
|
|
114
|
+
status = "failed";
|
|
115
|
+
// Clear ready queue immediately to prevent any dependent tasks from starting
|
|
116
|
+
// This is critical to prevent race conditions where dependent tasks might
|
|
117
|
+
// be in the ready queue when a dependency fails
|
|
118
|
+
readyQueue.length = 0;
|
|
119
|
+
},
|
|
120
|
+
/**
|
|
121
|
+
* Mark a task as skipped and update dependent tasks
|
|
122
|
+
*/
|
|
123
|
+
markTaskSkipped(taskId) {
|
|
124
|
+
runningTasks.delete(taskId);
|
|
125
|
+
skippedTasks.add(taskId);
|
|
126
|
+
updateDependentTasks(taskId);
|
|
127
|
+
updateExecutionStatus();
|
|
128
|
+
},
|
|
129
|
+
/**
|
|
130
|
+
* Mark a task as running
|
|
131
|
+
*/
|
|
132
|
+
markTaskRunning(taskId, execution) {
|
|
133
|
+
runningTasks.set(taskId, execution);
|
|
134
|
+
},
|
|
135
|
+
/**
|
|
136
|
+
* Check if there are more tasks that can be executed
|
|
137
|
+
*/
|
|
138
|
+
canExecuteMore() {
|
|
139
|
+
return readyQueue.length > 0 && runningTasks.size < maxConcurrencyLimit;
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Check if all tasks are complete (either completed, failed, or skipped)
|
|
143
|
+
*/
|
|
144
|
+
isComplete: allTasksFinished,
|
|
145
|
+
/**
|
|
146
|
+
* Check if any tasks have failed
|
|
147
|
+
*/
|
|
148
|
+
hasFailed() {
|
|
149
|
+
return failedTasks.size > 0;
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* Get current running tasks
|
|
153
|
+
*/
|
|
154
|
+
getRunningTasks() {
|
|
155
|
+
return new Map(runningTasks);
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* Clear all queues (used during cancellation)
|
|
159
|
+
* This prevents any pending tasks from starting, but preserves their status
|
|
160
|
+
* as "pending" in the task stats (they are not marked as aborted)
|
|
161
|
+
*/
|
|
162
|
+
clearQueues() {
|
|
163
|
+
readyQueue.length = 0;
|
|
164
|
+
waitingQueue.clear();
|
|
165
|
+
// Keep running tasks for cleanup, but mark them for termination
|
|
166
|
+
// Note: Tasks in waitingQueue remain with "pending" status - they are not
|
|
167
|
+
// moved to failed/aborted state since they never started
|
|
168
|
+
},
|
|
169
|
+
/**
|
|
170
|
+
* Mark all running tasks as aborted and clear them from running tasks
|
|
171
|
+
* Used during cancellation to ensure proper state consistency
|
|
172
|
+
*/
|
|
173
|
+
abortAllRunningTasks() {
|
|
174
|
+
// Move all running tasks to failed state
|
|
175
|
+
for (const taskId of runningTasks.keys()) {
|
|
176
|
+
failedTasks.add(taskId);
|
|
177
|
+
}
|
|
178
|
+
runningTasks.clear();
|
|
179
|
+
status = "aborted";
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -21,9 +21,12 @@ export function createSignalHandler({ abortSignal, onAborted, onProcessTerminate
|
|
|
21
21
|
// Handle abort signal if provided
|
|
22
22
|
let signalCleanup = null;
|
|
23
23
|
if (abortSignal) {
|
|
24
|
-
|
|
24
|
+
const handleAbort = () => {
|
|
25
|
+
onAborted();
|
|
26
|
+
};
|
|
27
|
+
abortSignal.addEventListener("abort", handleAbort);
|
|
25
28
|
signalCleanup = () => {
|
|
26
|
-
abortSignal.removeEventListener("abort",
|
|
29
|
+
abortSignal.removeEventListener("abort", handleAbort);
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
32
|
return {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Task } from "../types.js";
|
|
2
|
+
import type { ExecutionContext } from "./execution-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Callbacks for task execution events that replace scheduler coordination.
|
|
5
|
+
* These callbacks are invoked by the task executor to notify the queue manager
|
|
6
|
+
* of task state changes, enabling queue-based execution flow.
|
|
7
|
+
*/
|
|
8
|
+
export interface TaskExecutionCallbacks {
|
|
9
|
+
/**
|
|
10
|
+
* Called when a task becomes ready (e.g., via readyWhen condition).
|
|
11
|
+
* This allows dependent tasks to start executing.
|
|
12
|
+
*/
|
|
13
|
+
onTaskReady: (taskId: string) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Called when a task completes successfully.
|
|
16
|
+
* Updates dependent task dependency counts and moves ready tasks to execution queue.
|
|
17
|
+
*/
|
|
18
|
+
onTaskComplete: (taskId: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Called when a task fails.
|
|
21
|
+
* Triggers pipeline failure and cleanup.
|
|
22
|
+
*/
|
|
23
|
+
onTaskFailed: (taskId: string, error: Error) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Called when a task is skipped (e.g., missing command in non-strict mode).
|
|
26
|
+
* Treated similarly to completion for dependency resolution.
|
|
27
|
+
*/
|
|
28
|
+
onTaskSkipped: (taskId: string) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Executes a task (either a regular task or a nested pipeline).
|
|
32
|
+
*/
|
|
33
|
+
export declare function executeTask(task: Task, context: ExecutionContext, callbacks: TaskExecutionCallbacks): void;
|